ZPA: Audit Baseline Compliance
Keywords
zpa audit, zpa best practices, zpa baseline, zpa health check, zpa compliance review, baseline recommendations, zpa configuration audit, zpa policy audit, connector group audit, zpa hardening, zpa review, security baseline check, ZPA Baseline Recommendations v1.0
Overview
Audit a ZPA tenant's configuration against the Zscaler ZPA Baseline Recommendations v1.0 document. This skill is fully read-only — it inventories the tenant via existing list/get MCP tools, scores each finding against the baseline doc, and renders the result as a standalone Zscaler-styled web report (a single self-contained HTML file). A matching React component (ZpaAuditReport.jsx) is also generated for teams that want to embed the same view in an existing portal.
Use this skill when: An administrator asks to audit their ZPA tenant against best practices, run a baseline-compliance review, get a "ZPA health check", or see how their configuration compares to the recommended baseline.
Hard constraints — what this skill does not do
- No writes. Only
zpa_list_*andzpa_get_*tools. Never callzpa_create_*,zpa_update_*, orzpa_delete_*. - No telemetry checks. ZPA does not expose live connector CPU/memory/throughput, app probe results, session counts, or LSS log delivery health. Checks that require runtime data are not silently skipped — they are listed in the report's "Cannot Audit" section so the user knows what's missing and can request new APIs later.
- Configuration-only. Every check evaluates a field on a resource the API returns. Heuristic checks (e.g. naming-pattern matches for "sensitive" / "lss" / "discovery") are clearly marked as heuristic in the report.
- No external network calls from the report at view time. The HTML file embeds all findings inline; the only runtime external loads are the Tailwind/React/Babel CDN scripts needed to render it. (For air-gapped environments, see "Offline mode" in Step 4.)
Workflow
Step 1: Confirm scope
Before running anything, confirm with the administrator:
- Scope: full audit (all categories) or a single category (Connectors, Server Groups, Segments, Access Policy, Forwarding Policy, Timeout Policy, LSS).
- Microtenant: if the tenant uses microtenants, ask which one to inventory (default: the parent tenant). This is an inventory-scope decision only; it is not stored in the audit JSON or shown on the report.
If the user asked a generic "audit my ZPA tenant" question, default to full audit, parent tenant and proceed without further questions.
The skill does not record a tenant identifier in the report. The vanity domain, customer ID, and cloud realm live in the Zscaler MCP server's authentication context (env vars / config at server startup) and are not returned by any
zpa_list_*/zpa_get_*tool — and rather than inviting the agent to guess (or compose strings from email addresses, file names, orzscaler_check_connectivityoutput), the report header simply omits tenant identity. The audit timestamp and inventory line are enough to identify a given run.
Step 2: Inventory the tenant (read-only)
Claude Desktop pre-step (deferred tool loading). Claude Desktop lazy-loads MCP tool schemas via tool_search and refuses to invoke a tool whose schema isn't cached yet (you'll see "<tool> has not been loaded yet. Call tool_search first"). Before the inventory, prime the cache with a single search so all the read tools below are loaded in one shot:
If you skip this and call zpa_list_lss_configs (or any other tool) directly, the first call will fail with the "not loaded yet" error, you'll have to retry, and one inventory tool may end up silently skipped. This pre-step is a no-op on Cursor and Gemini CLI but is required on Claude Desktop / Cowork.
Then call these tools in parallel where possible. Capture the full result of each:
Pagination: if any list returns the maximum page size (typically 500), paginate with page_size=500, page=N until exhausted. The audit is incomplete on a partial inventory.
Errors: if a tool returns an error (e.g. service disabled, missing scope, or the "not loaded yet" deferred-loading error), record the category as "Cannot Audit — API error" with the error message and continue with the rest. Do not abort the whole audit. For "not loaded yet", retry the same tool once after running the tool_search pre-step above; if it still fails, mark the category as "Cannot Audit" and move on.
Inventory transparency: the inventory string you build here gets embedded into the report as a one-line summary so the admin can see which categories were actually fetched. Build it like:
Inventoried: 4 connector groups, 12 server groups, 30 application segments, 1 access rule, 0 forwarding rules, 2 timeout rules, 0 LSS configs.
LSS specifics: the LSS API is configuration-only (returns LSS config records and the metadata catalogs). It cannot tell you whether logs are actually flowing to the SIEM, dropping, or arriving late — that's an out-of-band, SIEM-side observation. The audit can only verify configuration shape; live delivery health is in the "Cannot Audit" section.
Step 3: Score the findings
Apply each check below to the inventory. Each finding has:
id— short stable identifier (e.g.acg-n-plus-one)category— one of:Connectors,ServerGroups,Segments,AccessPolicy,ForwardingPolicy,TimeoutPolicy,LSS,CannotAuditseverity—critical,warning,info, orcannotAudittitle— one-line human summaryevidence— short list of resource names / IDs that triggered the finding (max 10; truncate longer lists with…and N more)docRef— page or section in the baseline doc (e.g.§Health Reporting Recommendations, page 23)remediation— one or two sentences pointing the user at the right fix (often: "use thezpa/<other-skill>skill")heuristic—trueif the check relies on naming patterns rather than a definitive fieldframeworks— optional object withnist80053r5andcisV8arrays of control identifiers. Use the lookup table in Step 3a — Framework mapping verbatim; do not invent or generalize citations. Omit the field entirely (or both arrays empty) if the check has no high-confidence mapping —acg-geo-coordsis the only check intentionally left unmapped.
Important: every finding the report emits must be one of the IDs below. Do not invent new IDs; if a check does not apply, omit the finding entirely (the report only shows triggered findings — no "passes" section).
App Connector Group checks
Server Group checks
Application Segment checks
Access Policy checks
Forwarding Policy checks
Timeout Policy checks
LSS checks
The ZPA LSS API is configuration-only: it exposes LSS config records and the metadata catalogs (log types, status codes, client types, log formats). It does not stream or query log content; that ships from the LSS Connector to the SIEM out-of-band. So everything below is a config check; live delivery health belongs in the "Cannot Audit" section.
The shape of an LSS config record (from zpa_get_lss_config): config.name, config.source_log_type (ZPA internal code like zpn_trans_log), config.lss_host, config.lss_port, config.use_tls, config.filter (status code allowlist), and connector_groups[].id. Source log type internal codes:
Cannot Audit (always rendered, always the same)
These are baseline recommendations that depend on data ZPA does not expose. Always include these five findings in the output, with category: "CannotAudit" and severity: "cannotAudit", so the user knows what's missing.
Step 3a — Framework mapping
Each finding may carry an optional frameworks object that cites the NIST SP 800-53 Rev. 5 controls and CIS Critical Security Controls v8 safeguards the check evidences. The mapping was reviewed against:
- NIST SP 800-53 Rev. 5 (final, September 2020, with the 2023 patch release). Families AC, AU, CA, CM, CP, IA, SC, SI.
- CIS Critical Security Controls v8 (released May 2021, current revision v8.1, May 2024). Controls 1, 2, 3, 4, 6, 7, 8, 11, 12, 13.
Disclaimer (also rendered in the report header): Framework mappings are guidance, not certified compliance attestations. Confirm with your auditor before citing in a SOC 2, FedRAMP, ISO 27001, or NIST CSF assessment.
Quality bar — when in doubt, omit. A weak citation (mapping acg-geo-coords to "AC-something" because it touches access) is worse than no citation. The tables below cover 36 of the 37 check IDs; acg-geo-coords is intentionally unmapped (geo-routing is performance, not a security control).
Frameworks intentionally NOT mapped:
- CIS Foundations Benchmarks — these are platform-specific hardening guides (AWS / Azure / GCP / Kubernetes / Windows / RHEL etc.). No CIS Foundations Benchmark exists for ZPA, Zscaler, or any ZTNA product, so any citation would be incorrect and would be caught by an assessor familiar with the catalog.
- NIST SP 800-207 Zero Trust Architecture — architectural document, not a control catalog. Mapping is qualitative and applies at the product level, not per-finding. The disclaimer line above is enough.
- NIST CSF 2.0 — outcomes-based abstraction layer that points back at 800-53. Adding it as a third column duplicates information that's already in the 800-53 column.
Lookup table
When a check fires, attach the row's frameworks object verbatim. Use the column heading order — nist80053r5 first, cisV8 second.
App Connector Group checks
Server Group checks
Application Segment checks
Access Policy checks
Forwarding Policy checks
Timeout Policy checks
Log Streaming Service checks
Cannot Audit findings (gap-area mapping)
The ca-* findings are observability gaps — the check can't be measured today, but the control area it would have evidenced is still real. The mapping below tells the auditor which control coverage is currently uncertain.
Maintenance
This table should be reviewed when either (a) the check list in Step 3 changes, or (b) a major framework revision drops (NIST 800-53 Rev. 6, CIS Controls v9). Keep mappings conservative: omitting weak citations is always preferable to inflating the column.
Step 4: Build the audit JSON
Once scoring is complete, assemble the audit data object that the report consumes:
Validation rules for the JSON:
- Every
idmust come from the tables in Step 3. Do not invent new IDs. severitymust be one ofcritical | warning | info | cannotAudit.categorymust be one ofConnectors | ServerGroups | Segments | AccessPolicy | ForwardingPolicy | TimeoutPolicy | LSS | CannotAudit.- The five
ca-*findings are mandatory and always present. - Truncate
evidencelists to 10 entries (final entry:"…and N more"). - Heuristic checks must set
heuristic: true. frameworksmust come from the lookup table in Step 3a verbatim. Use the same casing (e.g.AC-3, notac-3;6.7, not6.07). Omit the field entirely foracg-geo-coords. Do not add framework families beyondnist80053r5andcisV8.
Step 5: Confirm delivery preference
Ask the admin once, before writing any files, how they want the report delivered. Phrase it as a single short question:
Where should I save the report? You can either (a) download — I'll generate the file and present it for you to save anywhere, or (b) specify a path — give me an absolute or workspace-relative directory and I'll write it there.
Pick the right tool for asking on the active surface:
If the user picks download (or doesn't specify a path), default to the agent's standard outputs/working directory:
- Cowork: save under the session's outputs folder, then present with
present_files(or the equivalent download surface) so the user gets a clickable file. - Claude Code / Desktop: save to the current working directory under
./reports/. - Cursor: save to the workspace root under
./reports/.
If the user specifies a path, validate it (must be a writable directory) and use it verbatim.
File naming: zpa-baseline-audit-<YYYYMMDD-HHMMSS>.html (and the matching .jsx next to it). Timestamp only — no tenant slug, since the report no longer carries tenant identity.
Step 6: Render the report
Two artifacts are written every run, side by side:
zpa-baseline-audit-<slug>-<ts>.html— the standalone Zscaler-styled web report. Open in any browser. No build step.ZpaAuditReport-<slug>-<ts>.jsx— the same view as a React component for embedding in an existing app. Optional — write only if the user asked for the JSX too, or by default if the surface supports both (Cowork: write both).
6a. Build the HTML
Read the template at templates/report.html.template (sibling of this SKILL.md). It contains a placeholder string __FINDINGS_DATA__. Replace that placeholder once with the JSON-stringified audit object from Step 4 (use stable JSON.stringify(data) — no pretty printing inside the script tag). Save the result to the chosen path.
Critical replacement rules:
- The replacement is a literal string substitution into a
<script type="application/json">block — escape any</script>substring inside evidence/remediation strings (replace with<\/script>). - Do not re-encode HTML entities. The data is read via
JSON.parse(textContent), not as HTML. - Do not modify any other part of the template. The CSS, React component, and Tailwind/CDN imports are calibrated together.
6b. Build the JSX (when requested)
Read templates/ZpaAuditReport.jsx.template and write it to the chosen directory unchanged. The component takes { data } as a prop, so the consumer also needs the audit JSON — write it next to the JSX as audit-<slug>-<ts>.json:
6c. What the report contains (visual contract)
The HTML/JSX template enforces the following layout — do not try to override it from the audit data:
- Sticky header —
zscalerwordmark in cyan→blue→purple gradient, followed by "ZPA Baseline Compliance Audit". Right side: Expand all / Collapse all / Print-or-save-PDF buttons (the print button callswindow.print()and the print stylesheet swaps to a clean white-paper look). - Title block —
<h1>"ZPA Baseline Compliance Audit" with the same gradient. Below: audit timestamp, doc reference, and the read-only disclaimer. (No tenant identifier — see Step 1.) - Framework disclaimer banner — a single italicized line directly under the title block: "Framework mappings are guidance, not certified compliance attestations. Confirm with your auditor before citing in a SOC 2, FedRAMP, ISO 27001, or NIST CSF assessment."
- Stat strip — four cards: Critical, Warning, Info, Cannot Audit. Numbers colored by severity.
- Inventory line — the one-line summary string from Step 2.
- Filter bar — search input (matches title, ID, evidence, doc ref, remediation, and framework citations), category chip row (with counts), severity chip row (with counts), framework chip row (All / NIST 800-53r5 / CIS v8 — with counts of how many findings carry that family).
- Findings list — one card per finding, sorted Critical → Warning → Info → Cannot Audit then alphabetical by title. Each card shows severity pill, category pill, heuristic pill (if applicable), the finding ID, and the title in the collapsed state. When expanded, the body shows: doc reference, evidence, remediation, and (if
frameworksis present) a "Frameworks" row of small pills — NIST controls in a neutral pill, CIS safeguards in a blue pill. Critical cards default to expanded; everything else defaults to collapsed. Click anywhere on a card to toggle. - Empty state — when filters yield zero findings, a centered "No findings match the current filters." card.
- Footer — "Generated by the
zpa-audit-baseline-complianceskill via the Zscaler MCP server." plus the doc reference.
6d. Print / Save PDF
The print button just calls window.print(). The template ships a @media print block that:
- Switches background to white, text to dark navy, removes the gradient on the wordmark/title.
- Hides the filter bar, expand/collapse buttons, and the print button itself (
.no-print). - Forces every finding card to render expanded (
[data-content] { display: block !important }). - Disables
page-break-insideon cards so each finding stays whole on one page. - Recolors the severity pills with a light, print-friendly palette (red-50/amber-50/cyan-50 backgrounds with darker text).
The user prints to PDF via the browser (Cmd/Ctrl-P → "Save as PDF"). No html2pdf or external library — keeps the file truly standalone.
6e. Offline mode (optional)
For air-gapped deployments, the user can convert the report to fully offline by:
- Downloading the three CDN scripts (Tailwind, React UMD, ReactDOM UMD, Babel standalone) and the Inter font into a sibling
vendor/directory. - Replacing the
<script src="https://…">and<link href="https://fonts…">lines with relative paths.
Mention this only if the user asks about offline / air-gapped use.
Step 7: Tell the user
State the totals, name any inventory failures, and point at the deliverable. Keep it short — the report is the deliverable, not the chat message.
"I audited your ZPA tenant against the Baseline Recommendations v1.0. Found X critical, Y warning, Z info items across N categories, plus 5 observability gaps the API can't check. Saved to
<path>— open it in your browser to filter, search, and print to PDF."
Never restate the full findings list in chat. The HTML report is the deliverable.
Common Pitfalls
- Do not write findings to a markdown table in chat. The HTML report is the deliverable. A markdown table for 25+ findings is unreadable.
- Do not invent evidence. If a check requires a field the API didn't return (older SDK, missing scope), score it as
cannotAuditwith a note rather than guessing. - Do not call write tools. This skill is read-only by design.
- Do not run telemetry checks. Anything the doc says about CPU / memory / throughput / probe results / log delivery is
cannotAudit. Period. - Heuristic checks must be flagged. Naming-pattern matches (
*sensitive*,*lss*,*contractor*) are heuristic — setheuristic: trueso the report shows the "Heuristic" pill and the user knows to verify. - Pagination matters. A partial inventory produces wrong findings. If a list returned the max page size, keep paging.
- Do not modify the templates inline. If the template needs to change, edit
templates/report.html.templateortemplates/ZpaAuditReport.jsx.templateand commit; do not patch the rendered HTML on the fly. - Escape
</script>in evidence text. Rare but possible — if any finding's evidence/remediation contains the literal sequence</script>, replace it with<\/script>before substituting into the HTML template, otherwise the data block closes prematurely. - Do not invent framework citations. Use the lookup table in Step 3a verbatim. If a finding has no row in the table (only
acg-geo-coords), omit theframeworksfield entirely. Do not extrapolate from a similar check, do not generalize a sub-control to its parent (e.g.AC-3vs.AC), and do not add CIS Foundations Benchmarks (no ZPA benchmark exists). A weak citation is worse than no citation.
When NOT to Use This Skill
- Single-rule creation (use
zpa/create-access-policy-rule, etc.). - Application onboarding (use
zpa/application_segment-onboard). - Reactive connector troubleshooting (use
zpa/troubleshoot-app-connector). - Writing changes — this skill never mutates the tenant.
Quick Reference
Inventory tools (read-only):
zpa_list_app_connector_groups,zpa_list_app_connectorszpa_list_server_groups,zpa_list_segment_groups,zpa_list_application_segmentszpa_list_access_policy_rules,zpa_list_forwarding_policy_rules,zpa_list_timeout_policy_ruleszpa_list_lss_configs,zpa_get_lss_config,zpa_list_lss_log_types,zpa_list_lss_status_codes,zpa_list_lss_client_types,zpa_get_lss_log_format
Templates (in this skill's templates/ directory):
report.html.template— single-file HTML report. Replace__FINDINGS_DATA__with the audit JSON; save as.html.ZpaAuditReport.jsx.template— React component that consumes the same audit JSON via adataprop. Save as.jsx.
Doc reference: Zscaler ZPA Baseline Recommendations v1.0 (April 2026), 41 pages. Cite the relevant page or section in each finding's docRef.
Visual style (don't override): Dark navy background (#050912), Zscaler cyan→blue→purple gradient on the wordmark and h1, severity colors red/amber/cyan/gray, Inter font, rounded 12px cards, sticky header, print stylesheet for clean PDF export.

