Security Testing, AI & ComplianceCompliance Hub & Reporting
On this page0

Reference

Included in: Reports on demand: All plans. Compliance hub and evidence export: Advanced. Which Plan Includes What

Compliance Hub & Reporting

Reference for the Compliance Hub: how the status of each control is worked out, how controls with no automated evidence are attested, how work is delegated and reviewed, how remediations are tracked, and how reports are generated, sealed and shared.

Where This Page Fits

Two shorter pages sit beside this one. The readiness score and how evidence is stored are on Readiness Score & Frameworks. When the background collector runs, the evidence files you upload and auditor access are on Evidence Collection. On all three, “evidence vault” means one thing: the sealed copies of report runs described under Evidence Vault, Share Links & File Verification.

What The Hub Guarantees

Compliance numbers are only useful if someone who did not produce them can trust them. Two rules make that possible.

A Broken Check Is Never Green

Evidence a check could not gather (because it could not run, gave no clear answer, or did not apply) is shown as needing attention, a counted gap. It never reads as a pass. A collector that crashed shows as an open control, not a satisfied one.

The Browser Cannot Inflate The Score

When you save an assessment, the readiness percentage is computed on the server by the same engine that builds the report. Any percentage or pass/fail counts posted by the browser are ignored (the posted figure is kept in the audit log for forensics only), and if the server cannot compute the score it records nothing rather than fall back to the browser’s numbers.

Program & Auditor Room

In plain words: Program is where a compliance manager keeps the company’s own governance records: policies, risks, vendors and the list of which controls apply. Auditor room is where they deal with the outside auditor: a read-only link, the auditor’s requests for more evidence, the download package, and a public security page for the company.

Beyond per-control evidence, the hub keeps the governance records an auditor asks for and gives you controlled ways to show them your posture. In the app these are two tabs: Program (Policies, Risks, Vendors, Statement of Applicability, document templates and your scope) and Auditor room (Evidence requests, Auditor links, Trust Center, Evidence package, Control mapping). Who may read and change them, and the endpoints behind each tab, are under Technical detail below.

Policy Register & Acknowledgments

Keep each policy with an owner and a next-review date, starting from the built-in template library or your own text. A policy can require acknowledgment: every signed-in employee can read the policies that apply to them and sign them with their typed name, and a coverage view shows who has and has not. Approval enforces separation of duties — the drafter cannot approve their own policy (administrators excepted).

Risk Register

Record risks with a likelihood and impact score, a status and a treatment. The Program tab counts the ones that are open or in treatment and flags the high-scoring ones.

Vendor Register

Track third parties with a risk decision and a next-review date. Request a review, then complete it; vendors without a decision or overdue for review are flagged. This list is separate from the vendor list under Finance: adding or changing a vendor in one does not change the other.

Statement Of Applicability

Per framework: which controls apply, and the recorded justification for each one you excluded. It follows your scope answers and N/A decisions, and can be sealed as a point-in-time record.

What These Records Are, And Are Not

The registers, the Statement of Applicability and the Trust Center page are your records and your statements. The hub stores, dates and audit-logs them; it does not certify them, and what you publish about your own posture is your responsibility.

If something here is not working as you expect, start with When something goes wrong.

The plain explanation is above. This part is for IT and developers: the exact settings, permission names, API addresses and data fields behind it. Closed: select the heading above to open it. While it is closed, your browser’s find-on-page (Ctrl+F or Cmd+F) does not search this part.

One Engine, One Set Of Numbers

The Compliance Hub keeps exactly one live ControlState per control, rolled up continuously from evidence the harvester observes across the stack — a background collector re-harvests on a timer, not only when someone opens the page. The interactive view and the scheduled delivery share the same generation engine and the same server-side score, so the report on screen and the report in the inbox cannot disagree.

Score is

Server-Recomputed

Collected-Pass Freshness

30-Day TTL

Rollup

Worst-Status-Wins

Evidence the probe could not gather — inconclusive, unknown, not_run, na, not_applicable — maps to attention, a counted gap. It never reads as a pass.

Live Control States

The live control state is the continuous per-control status that powers the hub. These routes sit under /reports/compliance/* and carry the soft complianceEnabled licence gate: allowed unless the Zonitix licensing server (the store) has explicitly disabled the Compliance Hub (an absent flag is allowed — the gate fails open).

MethodPathPurposeAuth
GET/api/v1/compliance/reports/compliance/control-statesList every live ControlState, ordered by controlId ascending.view_reporting or manage_compliance
POST/api/v1/compliance/reports/compliance/control-states/refreshRe-harvest evidence across the stack, then roll it up into live states — on demand; the background collector also runs this on its timer. With ?async=1 it starts (or joins) one background job and answers 202 at once.manage_compliance
PUT/api/v1/compliance/reports/compliance/control-states/:controlIdManually override one control (status / owner / dueDate / message / frameworks). Cannot write a pass or in_review.manage_compliance
GET/api/v1/compliance/reports/compliance/harvest-statusStatus of the evidence harvest: { running, job, report }, where report lists each source of the last run as ok or failed. Raw collector errors are shown only to manage_compliance holders and administrators; other readers get a safe summary.view_reporting or manage_compliance

The Rollup Rule An Inspector Checks

A refresh reads the append-only AuditEvidence rows tagged [ACTIVE VERIFICATION], groups them by control, and collapses each distinct signal (probe or alert) to its newest row. Then it applies a strict worst-status ordering — a control is a pass only if every signal passes:

  • Any failing signal → fail. A later-finishing pass from a different probe cannot mask a confirmed fail.
  • Else any inconclusive signal → attention. inconclusive / unknown / not_run / na / not_applicable evidence is treated as a gap, never as a pass.
  • Else any pass older than the TTL → stale. The freshness window is 30 days (freshnessTtlDays: 30); a pass that has not been re-observed inside it degrades to stale rather than staying green forever. (That window is for automatically collected evidence; a manual pass — reviewed or attested — carries a 365-day window.)
  • Otherwise → pass.

Auto-rolled states are stamped origin: "auto". The verifiable invariant on top of all of this: a state you set by hand is stamped origin: "manual" and the auto-rollup skips it entirely — a manual override is never auto-clobbered until an operator clears it. (When the rollup does touch an auto row, it preserves that row’s existing owner and dueDate.)

// Refresh: harvest + rollup, then read back the live states.

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/compliance/control-states/refresh \

-H 'Cookie: <your-session>'

// 200 response:

{ "updated": 34, "states": [ ... ], "harvestOk": true, "harvestError": null,

"report": { ... }, "failedCollectors": [ { "id": "collectCloudPosture", "label": "Cloud posture", "error": "..." } ] }

// The harvest is best-effort: if one source fails it is listed in failedCollectors and the

// rollup still runs over whatever evidence already landed. If the whole harvest fails,

// harvestOk is false and the states are re-rolled from the last good evidence.

// Background mode (what the hub's Collect now button uses) — single-flight:

curl -X POST 'https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/compliance/control-states/refresh?async=1' \

-H 'Cookie: <your-session>'

// 202 { "running": true, "joined": false, "jobId": "..." } joined: true = a job was already running

// Poll GET .../harvest-status until running is false.


// Manual override. Only these keys are read; anything else is ignored.

curl -X PUT https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/compliance/control-states/CC6.1 \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "status": "attention", "owner": "jordan@acme.cc",

"dueDate": "2026-09-30", "message": "Access review overdue - owner assigned" }'

// The saved row comes back with origin forced to "manual".

// A pass (pass / passed / verified / compliant / ok) is a 403 here: record it via

// .../control-states/:controlId/attest, or submit -> review. Writing in_review, or writing

// onto a control that is in review, is a 409. Marking N/A needs a justification (400 without).

// A missing :controlId is a 400 { "message": "controlId is required" }.

Both Writes Leave A Trail

A refresh logs COMPLIANCE_CONTROL_STATES_REFRESHED with the updated count and any failed sources (at warning level when something failed; in background mode the entry is written when the job ends); a manual override logs COMPLIANCE_CONTROL_STATE_SET with the control id and the patch, and a refused one (the 409) logs COMPLIANCE_CONTROL_ACTION_REFUSED. Reads (GET) are not logged.

What The Hub Collects By Itself

In plain words: the hub does not wait for someone to upload proof for every control. On the collector’s timer (the schedule is on Evidence Collection), and whenever a compliance manager clicks Collect now, it reads seventeen sources inside your own instance and your connected systems, and records what it found against the controls each source covers. The Evidence & integrations tab shows whether each source is healthy, which controls are covered automatically for the framework you are working on, what needs attention now, and every piece of evidence attached to a control so far.

SourceWhat it looks at
Probes, asset intelligence & identity fusionThe platform’s own automated checks and asset records.
Connected systemsYour connected identity providers, cloud accounts and device-management systems.
Device fleetPosture reported by enrolled devices.
Privileged access reviewWho holds privileged access.
Leaver reconciliationWhether people who left still hold access.
Data governance (DSAR, retention)Data-subject requests and retention settings.
Backups & disaster recoveryBackup and restore-drill status.
Directory alignmentDrift between Zonitix and your directory.
Identity & MFASign-in and second-factor posture.
Access certificationsAccess review campaigns.
Audit-log monitoringThe audit log and its integrity checks.
Policy registerWhether policies are approved and reviewed on time.
Vendor registerWhether vendors are reviewed on time.
Policy acknowledgmentsWho has signed the policies that apply to them.
Security awareness trainingTraining assignments and completions.
Cloud postureFindings from Cloud Mapping.
Security Lab resultsFindings and scan cadence from Security Lab.

Each source runs as its own step and is recorded as ok or failed, so one broken source does not stop the others. A failed source is named in the refresh response (failedCollectors, each with id, label and error), in GET /harvest-status, and in the audit entry for the run. A source that could not run never turns a control green: missing evidence rolls up as attention, as described under Live Control States above.

When a control that was passing stops passing after a run, the hub writes a COMPLIANCE_CONTROL_DRIFT audit entry for that control (warning level, with the status it left and the one it moved to) and emails the holders of manage_compliance one message listing up to 25 of the controls that drifted. The email needs outbound email to be configured; the audit entry is written either way.

The Evidence Library

The lower half of the Evidence & integrations tab lists the evidence attached to the controls of the selected framework, and opens the control each item belongs to. Files and links are attached to a control with POST /reports/compliance/evidence (see Evidence Collection for the upload itself).

MethodPathPurposeAuth
GET/api/v1/compliance/reports/compliance/evidence/libraryThe evidence attached to controls: everything, or one framework’s with ?framework= (400 on an unknown framework).view_reporting or manage_compliance

Per-Requirement Device Evidence

Controls that depend on your devices can be decided in two ways: by one check of the whole fleet, or requirement by requirement (encryption, firewall, screen lock and so on). Screen lock is the narrow one. No agent reports it, so it passes only for a Mac that has a Screen Lock profile of 15 minutes or less installed through Apple MDM, and a Windows or Linux computer in the fleet keeps it from reading as verified. A three-way switch, Per-requirement device evidence, chooses between them. It appears on a control whose evidence comes from devices, for people who hold manage_compliance or manage_security.

  • Off (off): the per-requirement results are not computed.
  • Shadow (shadow, the default): they are computed and compared with today’s result, but the single fleet check still decides. The switch shows what would change for the control if you turned it on.
  • On (on): the per-requirement results decide device controls. The app asks you to confirm, because control results can change across the fleet.

A change takes effect on the next evidence run, not at once, and is logged as COMPLIANCE_DEVICE_EVIDENCE_MODE_CHANGED. These routes carry the same soft complianceEnabled gate as the rest of the hub.

MethodPathPurposeAuth
GET/api/v1/compliance/reports/compliance/device-evidence/settingRead the mode: { mode, stored, updatedAt, default, modes }.manage_compliance or manage_security
PUT/api/v1/compliance/reports/compliance/device-evidence/settingSet the mode. Body: { mode: "off" | "shadow" | "on" }; anything else is a 400. Returns { mode, previous, changed, appliesOn }.manage_compliance or manage_security
GET/api/v1/compliance/reports/compliance/device-evidence/shadow-diffThe last run’s per-control comparison between the two methods: { mode, diff } (diff is null until a run has produced one).manage_compliance or manage_security
GET/api/v1/compliance/reports/compliance/device-evidenceThe per-requirement evidence rows for one control (?controlId=, required; ?framework= optional). 403 for an assignee asking about a control they do not own.view_reporting, manage_compliance or view_assigned_compliance

Attestations — Controls With No Automated Signal

In plain words: some controls cannot be checked by a machine, such as a board sign-off. For those, a compliance manager can state that the control is met and write down the basis. That statement is an attestation. It is done from the control’s drawer on the hub’s Overview tab, and the hub always shows it as Attested so nobody mistakes it for a checked result.

Some controls cannot be probed (a signed policy, a board sign-off). A manager records a manual pass on such a control by attesting it. The attestation is written onto the live control state itself — status pass, origin: "manual", with the attester, the time and the basis note stamped on the row — and the hub labels it Attested, never a plain green pass. An attested pass is excluded from the headline verified readiness figure and goes stale after 365 days.

MethodPathPurposeAuth
POST/api/v1/compliance/reports/compliance/control-states/:controlId/attestRecord a manual pass. Body: framework (required), note (required unless the policy is "allow"). Returns the updated state row.manage_compliance
POST/api/v1/compliance/reports/compliance/control-states/:controlId/revoke-passTake a MANUAL pass back to attention. Body: note (required, max 1000 chars), framework?. 409 unless the control holds a manual pass.manage_compliance
POST/api/v1/compliance/reports/compliance/control-states/:controlId/return-to-automatedUndo a manual verdict and let the collectors decide again. 409 if the control is in review, already automated, or no collector covers it.manage_compliance
POST/api/v1/compliance/reports/compliance/control-states/:controlId/clear-naBring a Not-applicable control back in scope. Returns { ok, mode: "automated" | "manual" }; 409 unless it is currently N/A.manage_compliance
GET/api/v1/compliance/reports/compliance/attestation-policyRead the manual-attestation policy: { mode }.view_reporting or manage_compliance
POST/api/v1/compliance/reports/compliance/attestation-policySet the mode: allow / note_required / review_only.manage_security
GET/api/v1/compliance/reports/compliance/attestationsRead-only history of the retired legacy attestations (optional ?framework=). Returns { attestations: [...] }.view_reporting or manage_compliance

// Attest a manual control. framework REQUIRED; note required under the default policy.

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/compliance/control-states/CC1.2/attest \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "framework": "SOC 2", "note": "Board charter reviewed 2026-07-01" }'

// 400 with no note under note_required; 403 under review_only;

// 409 if an automated check says the control is failing and no evidence is attached.

The Attestation Policy

Whether a single person may assert a pass is a customer setting, shown as Manual attestation under the hub’s Compliance settings. It has three modes: Allow (allow — attest freely), Note required (note_required, the default — a basis note is mandatory), and Review only (review_only — attestation is disabled and every manual pass must go through submit → review). Like the separate-reviewer toggle, the write is gated on manage_security, a right the reviewer population does not hold, so the people it governs cannot relax it.

The same actions appear in the hub on a control’s drawer: Attest (manual pass)…, Waive (mark N/A)… (a justification is required), Clear N/A…, Return to automated collection… and Revoke pass…. Attesting logs COMPLIANCE_CONTROL_ATTESTED (or COMPLIANCE_CONTROL_ATTEST_OVERRODE_REVIEW, at warning level, when it overrides a review someone else submitted); the others log COMPLIANCE_CONTROL_PASS_REVOKED, COMPLIANCE_CONTROL_RETURNED_TO_AUTOMATED and COMPLIANCE_CONTROL_NA_CLEARED. A refused action is logged as COMPLIANCE_CONTROL_ACTION_REFUSED.

The Legacy Attestation Writes Are Retired

POST and DELETE /reports/compliance/attestations no longer write anything: both answer 410 Gone with a pointer to /control-states/:controlId/attest and the submit → review loop. Only the GET remains, as read-only history. Update any script that still calls the old writers.

Delegate & Review (Work Breakdown)

In plain words: a compliance manager shares the work out on the hub’s Owners tab, giving each section or control to a named person with a due date. Each owner sees only their own list under My Work → My Compliance Tasks, attaches evidence and submits it. A second person then approves it or sends it back with a note.

A framework is rarely one person’s job. Assign each domain (section) to an owner with a due date, and where a section splits across people, redirect an individual control to a different owner — resolution is control override → section default → unassigned, so the common one-owner-per-section case stays a single row. Assigning someone auto-grants the scoped view_assigned_compliance right; on unassign the grant is reference-counted, so a person keeps access as long as they still own work anywhere.

MethodPathPurposeAuth
GET/api/v1/compliance/reports/compliance/assignmentsList section (domain) assignments (optional ?framework=).view_reporting or manage_compliance
POST/api/v1/compliance/reports/compliance/assignmentsAssign a framework domain to an employee, with a due date.manage_compliance
POST/api/v1/compliance/reports/compliance/control-assignmentsRedirect one control to a different owner (splits a section).manage_compliance
POST/api/v1/compliance/reports/compliance/assignments/batchApply a whole tree of domain + control assignments in one all-or-nothing transaction; returns the { grant, revoke } access delta.manage_compliance
GET/api/v1/compliance/reports/compliance/my-controlsThe exact controls the caller effectively owns (override-resolved) — powers My Compliance Tasks.view_assigned_compliance, manage_compliance or view_reporting

Submit → Review, With Separation Of Duties

An assignee evidences a control and submits it for review (scoped — they can only submit a control assigned to them); a reviewer then approves it to pass or reopens it to attention with a required note emailed back to the owner. Separation of duties is enforced server-side from the verified caller, not the request body: the person who submitted a control cannot approve their own unless the customer turns the policy off or the caller is administrator. The toggle is write-gated on manage_security — a right the reviewer population does not hold — so reviewers cannot disable their own SoD requirement. A never-submitted control cannot be approved straight to pass, and only a control that is actually awaiting review can be approved or sent back; a single-party pass must go through /attest (see Attestations above), which is logged distinctly. A control that is already passing, already in review, or marked Not applicable cannot be submitted (409), and one an automated check reports as failing needs evidence attached before it can be submitted or attested.

MethodPathPurposeAuth
POST/api/v1/compliance/reports/compliance/control-states/:controlId/submitAssignee submits their own control for review (→ in_review). A non-manager may submit only a control assigned to them.view_assigned_compliance or manage_compliance
POST/api/v1/compliance/reports/compliance/controls/bulk-submitSubmit up to 500 controls in one call; ownership is checked per control and skipped ones are reported.view_assigned_compliance or manage_compliance
POST/api/v1/compliance/reports/compliance/control-states/:controlId/reviewReviewer approves (→ pass) or reopens (→ attention, note required). Blocked if you submitted it.manage_compliance
GET/api/v1/compliance/reports/compliance/review-policyRead the separate-reviewer requirement (defaults ON — fail safe).view_reporting or manage_compliance
POST/api/v1/compliance/reports/compliance/review-policyEnable / disable the separate-reviewer requirement.manage_security

Program & Auditor Room: Permissions And Endpoints

For the two tabs described under Program & Auditor Room above. Reads need view_reporting or manage_compliance; writes need manage_compliance unless noted. All paths below are under /api/v1/compliance/reports/compliance.

MethodPathPurposeAuth
GET/policies · /risks · /vendorsList each register. POST creates, PUT /:id updates, DELETE /:id removes.view_reporting or manage_compliance (writes: manage_compliance)
GET/policy-templatesThe template library and what you have adopted. POST /policy-templates/adopt adopts one or many into the register.view_reporting, manage_compliance or manage_employee_training (adopt: manage_compliance)
POST/policies/approveApprove or re-approve policies. Allowed for the policy owner, manage_compliance or an administrator; never the drafter.Any authenticated session (authorised per policy)
POST/policies/:id/ackAn employee signs a policy. Body: signedName, contentHash (from GET /policies/:id/sign-document) and sections. GET /policies/ack/mine lists what the caller must sign. The signing flow is described on the Policy Signing page.Any authenticated session
GET/policies/ack/coverageAcknowledgment coverage across employees.view_reporting or manage_compliance
POST/vendors/:id/request-review · /vendors/:id/complete-reviewRun a vendor review.manage_compliance
GET/soaThe Statement of Applicability. POST /soa/seal seals it.view_reporting or manage_compliance (seal: manage_compliance)
POST/scope/applyApply your scope answers; out-of-scope controls become N/A with a reason.manage_compliance
GET/pursued-frameworksThe frameworks you are working toward (the hub switcher shows these). POST to change.view_reporting or manage_compliance (write: manage_compliance)

Auditor Links — A Read-Only Room Without A Login

Auditor room → Auditor links → Manage links → Create link mints a link to a read-only view of one framework’s controls: each control’s status, how it was established, and whether evidence is attached. The auditor needs no account — the link is the credential. It expires (30 days by default, 180 at most), can be revoked at any time, and every view is counted and logged. The view is redacted: it does not expose internal notes, personal data or your evidence files. As with report share links, the full link is shown once at creation; lose it and you revoke and create another.

From that room the auditor can raise an evidence request against a control. The people who manage compliance and the control’s owner are notified, the request appears under Auditor room → Evidence requests and on the owner’s My Compliance Tasks, and a manager marks it resolved once the evidence is in place.

MethodPathPurposeAuth
POST/auditor-sharesCreate an auditor link. Body: framework, ttlDays? (1–180, default 30), label?. Returns the link once.manage_compliance
GET/auditor-sharesList links (hint, expiry, view count). DELETE /auditor-shares/:id revokes one.view_reporting or manage_compliance (revoke: manage_compliance)
GET/auditor-requestsEvidence requests auditors raised. POST /auditor-requests/:id/resolve closes one.view_reporting or manage_compliance (resolve: manage_compliance)

Your Own Public Trust Center Page

Auditor room → Trust Center → Edit Trust Center builds a public security page for your company, served from your instance: a headline and intro, the frameworks you choose to show (as “In program” or with a readiness percentage — your choice), a list of security practices, and a contact address for report requests. Practices can be added automatically, and only from controls that are actually passing. Nothing is public until you publish it; until then the public address returns 404. Reading or editing the page needs GET / POST /trust-center (manage_compliance to edit or publish).

Readiness Digest — Retired

Earlier versions could email a lightweight readiness digest on its own cadence. That send path has been retired in favour of Scheduled reports (below), which do the same job under the recipient policy, with a sealed run history and failure alerts. A digest that was still enabled on an instance was carried over once, automatically, into an equivalent scheduled report — you will find it in the schedule list and can edit or delete it there. Nothing needs to be migrated by hand.

MethodPathPurposeAuth
POST/api/v1/compliance/reports/compliance/digest/sendRetired. Answers 410 Gone ({ code: "DIGEST_RETIRED" }) with a pointer to /reports/schedules; sends nothing.manage_compliance
GET/api/v1/compliance/reports/compliance/digest/configRead-only: the old digest settings that were carried forward, plus { retired: true }.view_reporting or manage_compliance
POST/api/v1/compliance/reports/compliance/digest/configRetired. Answers 410 Gone; changes nothing.manage_compliance

A call to either retired endpoint is still recorded in the audit log as COMPLIANCE_DIGEST_RETIRED_CALL, so an old script hitting it is visible.

Remediation Tracking

In plain words: when a control is failing, a compliance manager records who is fixing it and by when. That record is a remediation. It is created with Track fix and shows as Fix tracked next to the failing control on the hub’s Evidence & integrations tab. Deadline reminders go to the people assigned a section or control on the Owners tab, not to the owner named on a remediation.

A remediation records the fix for a control — an owner, a due date, and a status — keyed one-per (framework, controlId). framework, controlId and title are required; status defaults to open.

MethodPathPurposeAuth
GET/api/v1/compliance/reports/compliance/remediationsList remediations (optional ?framework=), ordered by dueDate ascending. Returns { remediations: [...] }.view_reporting or manage_compliance
POST/api/v1/compliance/reports/compliance/remediationsCreate or update the item for a control. Body: framework, controlId, title, owner?, dueDate?, status?.manage_compliance
DELETE/api/v1/compliance/reports/compliance/remediationsDelete tracking for a control. Query: framework, controlId.manage_compliance

// Track a fix (201). Second POST on the same key updates it in place.

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/compliance/remediations \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "framework": "ISO 27001", "controlId": "A.8.3",

"title": "Enable disk encryption on build agents",

"owner": "ops@acme.cc", "dueDate": "2026-09-15", "status": "open" }'

// Response echoes the stored row:

{ "id": 12, "framework": "ISO 27001", "controlId": "A.8.3",

"title": "...", "owner": "ops@acme.cc", "dueDate": "2026-09-15", "status": "open" }

Writes log COMPLIANCE_REMEDIATION_TRACKED; deletes log COMPLIANCE_REMEDIATION_DELETED. On an update, omitted owner / dueDate are preserved rather than nulled.

Score Recompute — The Integrity Control

Saving an assessment is the record that continuous monitoring happened — it becomes historical evidence. Because any view_reporting user could otherwise POST an inflated 100%, the server does not trust the client’s numbers. It re-runs the report engine for the framework — the same build the on-screen report, the downloads and the scheduler use — and stores that result. Only framework is read from the request; every posted number is ignored.

MethodPathPurposeAuth
POST/api/v1/compliance/reports/compliance/assessmentsPersist an assessment (201); readinessScore, counts and findings are computed server-side. 400 on an unknown framework, 503 if the server cannot compute.view_reporting
GET/api/v1/compliance/reports/compliance/assessments/:idFetch a stored assessment (with findings).view_reporting or manage_compliance
GET/api/v1/compliance/reports/compliance/assessmentsHistory for a framework (?framework=&limit=). Returns { history: [...] }.view_reporting or manage_compliance

// You may POST a readinessScore. Watch what comes back.

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/compliance/assessments \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "framework": "SOC 2", "readinessScore": 100, "passCount": 999 }'

// Response — the score is the SERVER's, not the 100 you sent:

{ "id": 87, "createdAt": "2026-08-09T...",

"readinessScore": 61, "serverComputed": true }

The stored readiness, pass / fail / manual / N/A counts and findings are the engine’s, so the home card, the opened report and the sealed vault artifact are one computation and cannot disagree. Not-applicable controls are left out of the denominator; the whole rule is under Readiness Scoring & Audit Activity. Stored findings keep the verdict and an affected count, but not the named sample of affected identities. The audit entry COMPLIANCE_REPORT_RUN records the server-computed value, the serverComputed flag, and the client’s reported value alongside it.

Fails Closed — No Client Fallback

There is no path that stores the client’s numbers. An unknown or missing framework is a 400. If the engine build fails, the call returns 503 (“nothing was recorded”) and no assessment row is written — retry once the cause is fixed. Every row this endpoint returns carries serverComputed: true.

The Reporting Engine

In plain words: the Report Engine is where anyone with reporting access picks a report from a library, runs it on live data, and exports it. It covers the operational reports (system overview, custom query, lifecycle, endpoint compliance, budget) and, where the Compliance Hub is on your plan, the framework reports. The step-by-step version is the guide Run, save and share a report.

The generation, download, schedule and run-history routes carry the soft reportingEnabled gate — allowed unless the store explicitly disabled reporting. /generate and /download build a report on demand from the same engine the scheduler uses; pdf is the default format; csv, xlsx (Excel workbook) and pptx (slides) are the alternatives, and /download also accepts zip for an auditor pack. In the app this is the Report Engine screen, under Analytics in the menu.

MethodPathPurposeAuth
POST/api/v1/compliance/reports/generateBuild a report and return it as JSON (no file, no email).view_reporting
POST/api/v1/compliance/reports/downloadBuild and return a file. ?format=pdf (default) | csv | xlsx | pptx | zip (auditor pack).view_reporting
GET/api/v1/compliance/reports/runsGeneration audit trail (?limit= max 200, ?scheduleId=), newest first.view_reporting

The report definition is validated, not coerced — a bad field is a 400, never a silently-wrong report. kind is one of compliance (default; shown in the app as Framework readiness) / overview (System overview) / query (Custom query) / lifecycle (Lifecycle reconciliation) / device-posture (Endpoint compliance) / budget (Budget report) / crosswalk (Framework crosswalk) / type2 (Type II period report); a compliance or type2 report requires a framework from the real catalog of 146 controls across 8 frameworks (CCPA, GDPR, HIPAA, HITRUST, ISO 27001, NIST CSF, PCI DSS, SOC 2) — a typo is rejected rather than scored against zero controls and emailed as a confident 0%.

What A Report May Contain: Permission And Licence Floors

view_reporting opens the engine, but a report is never a way around the permission that guards the same data on its own page. Two further floors apply on every generate, download, archive, schedule and share. Where several permissions are listed, any one is enough.

Report kind or data setExtra permissionExtra licence
Kind: budgetview_finance, manage_finance or view_finance_consolidatednone
Kind: device-postureview_devicesnone
Kinds: compliance, crosswalk, type2none beyond view_reportingcomplianceEnabled
Data set: usersview_users or manage_usersnone
Data set: groupsview_groups or manage_groupsnone
Data set: logsview_logs or manage_logsnone
Data set: devicesview_devicesnone
Data sets: controls, remediation, vendors, policiesview_reportingcomplianceEnabled
Data set: policySigningmanage_compliance or manage_employee_trainingcomplianceEnabled
Data set: trainingmanage_employee_training or manage_complianceTraining

A query report that asks for a data set the caller cannot see is refused with 403. The curated kinds (overview, compliance) still run, because they need the whole estate to score, but the raw rows of a data set the caller cannot see are stripped and only counts and totals remain. A data set or kind whose module is switched off on the licence is refused with 403, and with 402 while a subscription is lapsed. A type2 report must carry its period as window: { from, to }: both dates are required, they are read as whole UTC days, and the period can be at most 12 months (366 days); anything else is a 400.

// Generate the on-screen report as JSON.

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/generate \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "kind": "compliance", "framework": "SOC 2" }'


// Download the same report as a PDF (default) — writes to a file.

curl -X POST 'https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/download?format=pdf' \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "kind": "compliance", "framework": "SOC 2" }' -OJ

// An unknown format is a 400 { "error": "Unknown format 'docx'." }.

// A download logs REPORT_DOWNLOADED.

Run History Records Every Attempt

/reports/runs is the generation audit trail, and it records the failures as well as the successes. A run whose report generated but did not deliver is stored with status: "failed" and a deliveryError that names which half broke — the report existing but never reaching an inbox is still a failed run from the recipient’s point of view.

// A failed-delivery run row:

{

"scheduleName": "Weekly SOC 2", "trigger": "scheduled",

"status": "failed", "delivered": false,

"deliveryError": "SMTP is not configured — set it in Security Settings.",

"readinessScore": 61, "durationMs": 812

}

Broken SMTP vs. A Quiet Month

A schedule’s lastRunAt advances on every attempt, not only on success. That is deliberate: gating the next run on the last success would turn a fixed SMTP password into a mail storm, and would retry a permanently-failing schedule every tick. So a schedule that ran but keeps failing is distinguishable from one that simply had nothing to send — the run history shows attempts with status:"failed", not a gap.

Evidence Vault, Share Links & File Verification

In plain words: the evidence vault keeps a frozen copy of a report exactly as it was when it ran, with a fingerprint that proves it has not been changed since. Compliance managers use it to hand an auditor the same file they saw, to send it through a link that expires, and to stop a copy from being cleared out while a matter is open (a legal hold). Anyone holding a file can check it is genuine without an account.

A report run can be archived: the PDF, CSV and Excel renderings are stored as a point-in-time artifact with a recorded SHA-256 and a keyed seal. From there a run can be downloaded again, placed on legal hold, shared through an expiring link, bundled into an auditor pack, and checked by anyone holding the file. Slides (pptx) are a presentation format and are not kept in the vault.

MethodPathPurposeAuth
POST/api/v1/compliance/reports/vault/archiveGenerate a report now and archive it (no email). Returns 201 { id, framework, readinessScore, artifactHash }.view_reporting
GET/api/v1/compliance/reports/runs/:id/artifactDownload a stored artifact (?format=pdf | csv | xlsx). 409 if the stored bytes fail verification; pptx is a 404.view_reporting
GET/api/v1/compliance/reports/runs/:id/statusPoll one run’s progress (used by the background seal and the auditor pack).view_reporting
POST/api/v1/compliance/reports/runs/:id/holdToggle legal hold. A held run is exempt from the retention sweep. Returns { id, legalHold }.manage_compliance
POST/api/v1/compliance/reports/download/auditor-packStart an auditor pack in the background. Returns 202 { runId }; poll the status, then fetch.view_reporting
GET/api/v1/compliance/reports/download/auditor-pack/:idThe ZIP of that run’s stored, re-verified files plus manifest.json (per-file SHA-256, run id, observation window, seal status).view_reporting
POST/api/v1/compliance/reports/runs/:id/shareCreate an expiring public link to one archived run (format pdf or csv, ttlHours, optional recipientEmail / recipientLabel). The link is returned once.manage_compliance
GET/api/v1/compliance/reports/runs/:id/shareList a run’s links (a 6-character hint, never the full link).view_reporting
GET/api/v1/compliance/reports/runs/share/:linkId/viewsRecent opens of one link, from the audit log.view_reporting
DELETE/api/v1/compliance/reports/runs/share/:linkIdRevoke a link.manage_compliance
GET/api/v1/compliance/reports/savedSaved report definitions (also POST to save one, DELETE /saved/:id to remove your own).view_reporting
GET/api/v1/compliance/reports/compliance/findings/:controlId/affectedDrill into one finding: the affected records, paged (?framework=&q=&cursor=&limit=). Add .csv for an audit-logged CSV.view_reporting or manage_compliance

Every one of these applies the same per-report-kind permission floor as generating the report, so a caller who could not open a budget or device report cannot archive it, list its links or publish one. Drill-down rows are additionally limited by the data set they come from (user rows need view_users, log rows view_logs). The full table of floors is under The Reporting Engine above.

How Long A Run Is Kept

An archived run is stamped with a retention date when it is stored. The period is the reportEvidenceRetentionDays setting and defaults to 2,555 days (about seven years). There is no control for it on screen. A run on legal hold is exempt from the retention sweep for as long as the hold stands.

Share Links

A share link opens one archived artifact without a login — the link itself is the credential, so treat it like a password. It defaults to 7 days and cannot exceed 90. Only a hash of the link is stored, so it is shown once at creation and cannot be listed again; if you lose it, revoke and create another. Sharing the CSV hands out the raw rows rather than a rendered report, so it requires an explicit confirmation (confirmCsv: true). Creating a link logs REPORT_SHARE_LINK_CREATED at warning level, each open is logged, and naming a recipient ties those log entries to the person the link was issued to. A link only ever serves the stored bytes — never a freshly generated report.

“Verify This File” — No Account Needed

Each archived PDF is stamped with its run id and a verify address on your instance (/verify/report/<run id>). Anyone holding the file — your auditor, or a customer of yours — can open that page and drop the file in. The page computes the SHA-256 in the browser; the file itself is not uploaded. The instance answers only match or no match, and on a match the framework, observation window, generation time and (for a PDF) the public details of the certificate that signed it. It returns no scores, findings or names, and a no-match does not reveal whether the run exists. There is no trusted timestamp authority behind the signature, and the page says so.

// The same check from a terminal — public, rate-limited, every call audit-logged:

shasum -a 256 soc2-readiness.pdf

curl 'https://<your-instance>-api.zonitix.com/shared/verify/<run id>?sha256=<64 hex chars>'

// Logged as REPORT_VERIFY_MATCH or REPORT_VERIFY_NO_MATCH.

A match requires both the hash to equal the vault record and that record’s seal to verify, so a tampered record answers no-match. The hash outlives the retention sweep (only the stored bytes are pruned), so a file stays verifiable for as long as its run row exists.

Signed Webhooks

When a schedule delivers to a webhook, every POST carries a signature header so the receiver can prove it came from your instance and was not replayed. The signing secret is generated by the server per schedule (prefix whsec_), stored encrypted, and returned exactly once — when the webhook is added or the secret is rotated.

X-Zonitix-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256(secret, "<t>.<raw body>")>

// Receiver: recompute the HMAC over `${t}.${rawBody}` with your secret, compare in constant

// time, and reject a timestamp outside your tolerance (5 minutes is the reference window).

// A retry is signed afresh (new t) over the same stored body.

Where This Lives In The App

Reports, schedules, saved reports and the vault are on the Report Engine screen. The Export menu there offers CSV, PDF, an Excel workbook, an auditor pack (ZIP of PDF, Excel and CSV), a JSON preview of the data on screen and, for some report kinds, slides. The Evidence vault button opens the archived runs; share links and legal hold are actions on a run there, and Capture evidence snapshot (in the report’s more-actions menu, for framework reports) archives the report on screen. The same screen has an optional AI review panel for the report on screen. It is advisory only: the risk level and counts are computed by code and the model writes the prose, and it is never audit evidence or part of a sealed artifact.

Scheduled Reports (CRUD + Run-Now)

In plain words: a scheduled report is sent by itself, daily, weekly or monthly, to the people you list. It is built from live data at the moment it runs, not from a saved copy. Compliance managers set these up from the Scheduled button on the Report Engine screen; the app offers scheduling for Framework readiness reports, with a PDF and/or CSV attached.

Recurring reports, delivered by email and/or to a webhook. Reads need view_reporting; anything that changes what gets sent to people needs manage_compliance. All of these carry the soft reportingEnabled gate.

MethodPathPurposeAuth
GET/api/v1/compliance/reports/schedulesList schedules + { smtpConfigured, frameworks } so the UI can warn before saving.view_reporting
POST/api/v1/compliance/reports/schedulesCreate a schedule (full validation). Returns 201.manage_compliance
PUT/api/v1/compliance/reports/schedules/:idPartial update (404 if gone).manage_compliance
DELETE/api/v1/compliance/reports/schedules/:idDelete the schedule. Run history is KEPT as evidence.manage_compliance
POST/api/v1/compliance/reports/schedules/:id/runRun now via the same path the scheduler takes. 200 even on run failure.manage_compliance
POST/api/v1/compliance/reports/schedules/:id/webhook-secretRotate the schedule’s webhook signing secret. The new secret is returned once and the rotation is audit-logged.manage_compliance

Validation rejects rather than coerces: recipients must be valid emails (lower-cased, de-duplicated, at most 50), everyHours must be 1–2160 (90 days), and formats must be a subset of pdf / csv / xlsx / pptx (defaults to ["pdf"]). At save time nothing is silently dropped: an invalid or off-policy recipient is a 400, so you never save a schedule that looks fine and is wrong.

// Create a weekly SOC 2 schedule (168h).

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/schedules \

-H 'Content-Type: application/json' -H 'Cookie: <your-session>' \

-d '{ "name": "Weekly SOC 2", "kind": "compliance",

"framework": "SOC 2", "formats": ["pdf"],

"recipients": ["audit@acme.cc"], "everyHours": 168 }'

// Run it now — a manual test takes the exact scheduler path:

curl -X POST https://<your-instance>-api.zonitix.com/api/v1/compliance/reports/schedules/3/run \

-H 'Cookie: <your-session>'

// 200 even if the run failed — the request succeeded, the run may not have:

{ "status": "failed", "delivered": false,

"error": "SMTP is not configured — set it in Security Settings.",

"readinessScore": 61, "durationMs": 740 }

Delivery: Email, Webhook, And Who May Receive

A schedule delivers to email recipients over the customer’s own SMTP, and/or POSTs the run to a Slack / Teams / HTTPS webhook. The webhook URL is not trusted blindly: it is run through an SSRF / DNS-rebinding guard — the host is resolved and re-checked at send time, so a URL that resolves to a private or internal address is rejected (a bad URL is a 400 at save). Who may be emailed is governed by a recipient policy that is enforced in one place for both the write path and the send path, so the two cannot drift: open (any valid address, the default), domains (a domain allowlist), or directory (recipients must be known users or in the allowlisted domain). You set it in Security Settings → Report Delivery. The two paths behave differently on purpose: saving a schedule with an off-policy recipient is rejected, while at send time each run drops any recipient that is no longer on-policy — so tightening the policy later stops an existing schedule from mailing an address you have since excluded. The policy fails safe for the customer — an unreadable setting or directory falls back to open, so a lookup failure never withholds a report.

Per-Assignee Deadline Reminders

Separate from the report schedules, POST /reports/compliance/reminders/send (manage_compliance, also scheduled) emails each assignee their own overdue / due-soon assigned sections and controls over the same customer SMTP — a nudge scoped to the person assigned, not a broadcast.

Deleting A Schedule Keeps Its History

A DELETE removes the schedule but deliberately leaves the ReportRun rows in place — the record that reports were going out is exactly what an auditor asks for, and it must survive the schedule. The delete is logged as REPORT_SCHEDULE_DELETED.

SMTP Comes From Settings, Not Env

Delivery resolves SMTP from the SecuritySetting store (smtpHost, smtpPort, smtpUser, smtpPass, smtpFrom, smtpSecure), falling back to SMTP_* env only if those are unset. The stored password is decrypted at rest. Configure it in Security Settings — the schedule list ships smtpConfigured so you can see the gap before the first silent failure.

Two Licence Gates, Both Soft

Control states, attestations, the registers, remediations, assessments, evidence upload and the audit runs (/audits) sit under complianceEnabled. There is one exception inside /reports/compliance: a path whose first segment is policies, policy-templates, training, exemptions, evidence-retention, my-data-notice or people answers when either complianceEnabled or the Training licence is on, so policy signing and people evidence work on a plan that has Training without the Compliance Hub. Generation, download, runs, schedules, saved reports and the vault sit under reportingEnabled. Reading back an evidence file you already uploaded is deliberately not licence-gated. Both are soft gates: an absent flag is allowed, and a settings-lookup failure fails open — a licence flag can never knock a paid feature offline. A route 403s only when the store has set the flag explicitly to false.

Related reference