Compliance surface — controls, evidence and posture¶
This directory is the reader-facing view of the compliance surface for someone who has to review it rather than write it: an assessor, an auditor or an internal control owner. It says where each artefact lives, what it does and does not state, and which gaps the register itself records.
Read this first. JDLib provides technical controls, evidence collection and
readiness reporting. Installing it does not make an organisation SOC 2, ISO/IEC
27001, NIST or any other framework certified or compliant, and nothing in
this directory is an audit result or an auditor's opinion. A ControlMapping
names where a control belongs in a framework (CC6.1, A.5.17, IA-5,
API2:2023, …) — it is a description, not a finding. The jdlib security
compliance command prints the same disclaimer, and
tests/unit/test_cli_security.py asserts the words not a certification and
does not make an organisation certified or compliant are in its output.
The engineering record for this surface is
docs/security/compliance-evidence.md
(phase 9). This document is navigation across it; it does not restate it.
1. What lives where¶
| Artefact | Path | What it is |
|---|---|---|
| Control register | src/jdlib/security/compliance/controls.py (CONTROL_REGISTRY, Control, ControlMapping, ControlFramework, ControlCategory, ImplementationStatus, VerificationMethod, control_by_id, controls_for_framework, frameworks_in_use) |
The controls this library implements, each mapped onto the framework families an assessor asks about. |
| Evidence record and checkout collectors | src/jdlib/security/compliance/evidence.py (SecurityEvidence, collect_configuration_evidence, collect_migration_evidence, collect_policy_evidence, CONFIGURATION_EVIDENCE_KEYS, MIGRATION_BRANCHES) |
The reproducible observation behind a claim, plus the collectors that need no database. |
| Posture evaluation | src/jdlib/security/compliance/posture.py (PostureOutcome, PostureFinding, PostureReport, Severity, evaluate_posture) |
Turns the register plus today's evidence into one finding per control. |
| Live (session-backed) collectors | src/jdlib/security/compliance/session.py (collect_isolation_evidence, collect_schema_revision_evidence) |
Observe a running database: the catalog's row-level security, and the revision a schema has actually applied. Imported explicitly by the CLI so jdlib.security.compliance stays free of the persistence layer. |
| CLI | src/jdlib/integrations/cli.py (security sub-app beside tenant, db, rls, schema) |
jdlib security posture, jdlib security evidence, jdlib security compliance. |
| Register tests | tests/unit/security/test_compliance_controls.py, tests/unit/security/test_compliance_evidence.py, tests/unit/security/test_compliance_posture.py, tests/unit/test_cli_security.py, tests/integration/test_compliance_live_collectors.py |
The invariants below are enforced here, including against a real PostgreSQL catalog. |
| Operating the checks | docs/operations/README.md |
How to run the commands, how to read their exit codes, and what remains the operator's responsibility. |
2. The control register¶
Nineteen controls, identified JDL-…, each carrying a title, description,
category, implementation status, framework mappings, evidence requirements,
evidence sources, technical owner, verification method and — for anything that
is not simply implemented — a status note explaining it. verification_method
says how the claim is shown to be true: test, inspection, analysis,
runtime (verified against a running service by the harness in tests/infra)
or external (the deployment's responsibility, not discharged by this library).
| Control | Status | Category | Frameworks |
|---|---|---|---|
JDL-AUDIT-01 Security events are recorded with fail-closed attribution |
implemented |
audit |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-AUDIT-02 Reading and exporting the trail are separately authorized |
implemented |
audit |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-AUDIT-03 Tamper-evident audit storage outside the database |
not_implemented |
audit |
soc2, iso_27001, nist_sp_800_53, cis |
JDL-AUTHN-01 Inbound token validation fails closed |
implemented |
authentication |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, owasp_api_top10, cis |
JDL-AUTHN-02 API key authentication is scoped and time-bounded |
implemented |
authentication |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-AUTHN-03 Bearer-token replay is bounded by the token's own lifetime |
not_applicable |
authentication |
owasp_asvs, nist_sp_800_53 |
JDL-AUTHZ-01 Every authorization decision goes through one policy enforcement point |
implemented |
authorization |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, owasp_api_top10, nist_csf |
JDL-AUTHZ-02 A policy engine that cannot answer denies everything |
implemented |
authorization |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-AUTHZ-03 Just-in-time privilege stops authorizing when its window closes |
implemented |
authorization |
soc2, iso_27001, nist_sp_800_53, owasp_asvs |
JDL-CONF-01 Unsafe security configuration cannot start |
implemented |
configuration |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-GW-01 Gateway-derived identity requires an authenticated integrity boundary |
implemented |
authentication |
soc2, iso_27001, nist_sp_800_53, owasp_api_top10, cis |
JDL-GW-02 Gateway plugin verification across products |
partial |
authentication |
soc2, iso_27001, cis |
JDL-ISO-01 Tenant isolation is enforced by the database, not only by the library |
implemented |
tenant_isolation |
soc2, iso_27001, nist_sp_800_53, nist_csf, cis |
JDL-ISO-02 Cross-tenant references are impossible at the schema level |
implemented |
tenant_isolation |
soc2, iso_27001, nist_sp_800_53, owasp_asvs |
JDL-LIFE-01 Tenant lifecycle transitions are bounded and recorded |
implemented |
lifecycle |
soc2, iso_27001, nist_sp_800_53, cis |
JDL-OBS-01 Security telemetry is structured, bounded and non-blocking |
implemented |
observability |
soc2, iso_27001, nist_sp_800_53, nist_csf, cis |
JDL-SECRET-01 Secrets are redacted on every path that leaves the process |
implemented |
data_protection |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-SQL-01 Raw SQL is validated by parsing, not by pattern matching |
implemented |
tenant_isolation |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
JDL-SUPPLY-01 Dependency and pipeline security gates |
partial |
supply_chain |
soc2, iso_27001, nist_sp_800_53, owasp_asvs, cis |
Generate this list from the register itself rather than trusting a copy:
jdlib security compliance --json # every control, its status and its mappings
jdlib security compliance --framework soc2
The register's own gaps¶
Recorded in the register, not omitted from it:
JDL-AUDIT-03— tamper-evident audit storage:not_implemented, ownerplatform-operations, verification methodexternal. Per-row hash chaining inside the same database was evaluated and rejected: an actor holdingUPDATE/DELETEcan drop a trigger and rewrite the chain. Detecting that requires storage the application's role cannot rewrite (WAL/CDC streaming, retention-locked object storage, a SIEM) plus a periodic anchored digest of each tenant's audit range. The residual risk — a database-level actor can alter history undetectably — is stated indocs/security/audit-hardening.md§7 and must be closed by the deployment.JDL-GW-02— gateway plugin verification. Kong 3.6 and Tyk 5.3.1 are both verified end to end: the six tests intests/infra/test_tyk_infra.pypass against the live harness, which also refuted the earlier "no JavaScript runtime" note (the JSVM exists; the middleware failed to load because the fixture named an unknown driver and the gateway was hostname-bound - seedocs/security/gateway-hardening.md§6).JDL-SUPPLY-01— dependency and pipeline gates:partial, verification methodinspection. The register's status note was written when the test/lint/type gate was the whole of CI and describes the pipeline-security work as the next phase's scope. That work has since landed as thesecurityjob (seedocs/security/ci-security.md); the register has not been re-statused since, so read the status note as a record of when it was written andci-security.mdas the current state of the gates.JDL-AUTHN-03— bearer-token replay:not_applicable. The validator keeps no server-side session, so token lifetime and audience checks are what bound a stolen token. A framework mapping is still recorded (owasp_asvs, nist_sp_800_53) because the control is described there, not because it is implemented here.
JDL-AUDIT-03, JDL-GW-02 and JDL-SUPPLY-01 are the three recorded gaps; a
test requires at least three of them, so the register cannot quietly become a
wish list.
What keeps the register honest¶
Enforced by tests/unit/security/test_compliance_controls.py:
- control ids are unique and shaped (
JDL-…); - every control has a description, a technical owner, at least one mapping, at least one evidence requirement, and a reference on every mapping;
- no silent partials —
partial,not_implementedandnot_applicableeach require a substantivestatus_note; - every cited evidence source exists on disk, so a rename or a delete cannot leave the register pointing at something that is gone.
The register's own tests were checked against a dishonest register (a deleted
status_note, an evidence source pointing at a missing file): 2 of 9 tests
fail, which is what makes them tests rather than decoration
(docs/security/compliance-evidence.md §2).
3. Framework families¶
Seven families are in use, and a test asserts each has at least one control mapped onto it:
| Framework | --framework value |
Register's name for it |
|---|---|---|
| SOC 2 Trust Services Criteria | soc2 |
SOC 2 Trust Services Criteria |
| ISO/IEC 27001 (2022 Annex A) | iso_27001 |
ISO/IEC 27001 |
| NIST Cybersecurity Framework 2.0 | nist_csf |
NIST CSF |
| NIST SP 800-53 Rev. 5 | nist_sp_800_53 |
NIST SP 800-53 |
| OWASP Application Security Verification Standard | owasp_asvs |
OWASP ASVS |
| OWASP API Security Top 10 | owasp_api_top10 |
OWASP API Security Top 10 |
| CIS Critical Security Controls v8 | cis |
CIS Controls |
Each ControlMapping carries the framework's own reference (CC6.1, A.8.5,
IA-5, API1:2023, V14.2, 12.6, PR.AA-05, …). A control may map into
several families; the reference is per mapping, not per control.
4. The evidence record¶
SecurityEvidence carries the five properties the compliance directive asks for
as structure rather than convention:
| Property | How it is carried |
|---|---|
| Structured | observations is a flat mapping of JSON scalars. |
| Timestamped | collected_at, from an injected clock. |
| Attributable | collected_by names the collector and cannot be blank. |
| Reproducible | reproducible carries the command or procedure that regenerates the observation; the CLI prints it as reproduce: …. |
| Safe | Observations pass the same sanitiser the audit trail uses (audit_metadata): a credential-shaped value is redacted, a value under a credential-named key is redacted whole, and a nested or oversized value is refused. There is deliberately no second sanitiser. |
repr prints observation keys, never values, so a stray log line cannot
become a second copy of a deployment's details.
Collectors, and the subject each observes:
| Collector | Module | Subject |
|---|---|---|
collect_configuration_evidence |
evidence.py |
The deployment's SecurityConfig. Reports only the facts on a fixed allow-list (CONFIGURATION_EVIDENCE_KEYS, 16 keys), so a configuration field added later cannot reach a report unreviewed; a test asserts the reported key set is exactly that list. |
collect_migration_evidence |
evidence.py |
The revision history this checkout carries — one head per branch (control, tenant). What a live schema has applied is the session-backed collector's job. |
collect_policy_evidence |
evidence.py |
The policy files a deployment declares — names and counts only (bounded list). A policy body is never read, so it cannot reach a report; tests/unit/test_cli_security.py asserts a marker planted inside a policy file never appears in the output. |
collect_isolation_evidence |
session.py |
The PostgreSQL catalog on the schema the engine is bound to; shares inspect_rls with jdlib rls verify, so the guard that raises and the evidence that reports cannot disagree about what they saw. |
collect_schema_revision_evidence |
session.py |
The revision a live schema has applied, compared with the checkout's expected head for the named branch. Absence and drift are observations, not errors. |
A collector that cannot reach its subject raises rather than fabricating a
result: "no evidence" is a state a report may describe (warn), an assumed
state is not.
5. Posture outcomes¶
Posture answers which controls are in force here, and how do we know, with one
finding per control. The semantics are fixed in posture.py, not left to each
rule:
| Outcome | Meaning |
|---|---|
pass |
The control is in force — a gate in a code path a caller cannot route around, or a state something actually observed. |
warn |
In force but unverified here (no evidence, or incomplete evidence — the reason names what is missing); switched off in a profile that declares itself unsafe on purpose (development); or a control the library deliberately leaves to the deployment. |
fail |
A control that should be in force is absent, disabled, or contradicted by what was observed. |
not_applicable |
The control's subject does not exist in this deployment (for example: no gateway identity trust, so the gateway trust model does not apply). |
A report also states its worst outcome — the ranking is fail > warn >
not_applicable > pass — and is_ready, which is true when nothing failed.
Every finding carries a reason of at least 20 characters; a finding for an
unknown control, citing another control's evidence, or stated twice in one
report is refused at construction. Serialization carries outcomes, reasons and
evidence summaries — never observation values — and the ordering is canonical,
so the same inputs always serialize identically.
6. The CLI¶
jdlib security posture # every control, with reasons; exits 1 when a control fails
jdlib security evidence # the records behind the report
jdlib security compliance # the register and its framework mappings
--json is available on all three. (The jdlib console script comes from a
consumer's [project.scripts] entry — this checkout installs none, so run the
app directly; the invocation is in
docs/operations/README.md §1.) The commands assess
the profile the operator declares (--environment development|test|staging|production); the
CLI cannot read a deployment's private settings and does not pretend to, so
every report says which profile it describes. Adding live observations takes
--database-url together with --schema, and --branch names the revision
branch that schema carries (control or tenant) rather than guessing.
--policy-directory adds policy-configuration evidence, --framework filters
the register view. Exit codes are documented in
docs/operations/README.md §2, together with what a
warn or a fail means for an operator.
7. What an assessor should take from this¶
- The technical controls, their framework mappings, their evidence requirements
and the artefact behind each claim are in the register, machine-readable, and
regenerable with
jdlib security compliance --json. - Observations and posture are reproducible: each evidence record names the command or procedure that produces it.
- The gaps are in the register — including a control the library deliberately does not implement — so nothing has to be inferred from absence.
- Nothing here is a certification, an audit result or advice about an external
auditor's opinion. Organisation-level controls (governance, people, physical
security, backup and retention, keys, audit-storage tamper evidence) are the
deployment's, as listed in
SECURITY.mdunder What is out of scope, and indocs/operations/README.md.