fix(compliance): make framework reports evidence-based and non-certifying - #310
parthrohit22 wants to merge 10 commits into
Conversation
TFT444
left a comment
There was a problem hiding this comment.
Two blockers before this can land. First, migration 3a76ff935bf6 shares down_revision = 'c7a2e9f1b3d4' with PR #308's migration. Both cannot merge without creating an Alembic branch fork. Coordinate with the #308 author so one migration chains off the other. Second, the get_score() empty-scan-returns-100 bug is acknowledged in the PR description but left unfixed. This creates a false security posture and needs to be addressed here or tracked as a follow-up issue before merge.
|
@TFT444 Both addressed:
Full suite (723 passed, 3 skipped - pre-existing chromadb-under-3.14 skip, unrelated) and ruff are clean. PR description updated to match. Latest commit: |
ritiksah141
left a comment
There was a problem hiding this comment.
Thanks for the substantial work here. The direction is correct—especially introducing NO_SCAN_DATA, mapping provenance, denominator exclusions, and non-certification language—but I found several correctness issues that must be resolved before merge.
1. Blocker: CIS direct mappings still overstate coverage
All 95 CIS entries are classified as direct, including entries whose IDs/names explicitly say they are not mapped to CIS, for example:
AZ-SC-001->N/A-SC-001— “not mapped in CIS Azure Foundations 2.0.0”AZ-SC-005->N/A-SC-005— “not directly mapped”AZ-NET-016->N/A-NET-016— “no direct CIS ... control”- Multiple
N/A-*backup, supply-chain, data-link, and security-operations entries
Because these are direct, they remain in the denominator and become PASS when absent from findings. This recreates the overstatement this PR is intended to prevent.
Required changes:
- Classify synthetic
N/A-*entries asnot_applicable. - Use
supportingfor partial technical evidence andorganizationalwhere a scan cannot establish the control. - Individually review mappings whose rule and control do not evaluate the same condition; do not classify the whole CIS file as direct by default.
- Add CI validation that an
N/A-*control ID, or a name/rationale saying “not mapped”/“no direct mapping,” cannot bedirect. - Complete and record the acceptance criterion requiring independent security/compliance review of a representative mapping sample.
2. Blocker: the frontend converts “no evidence” into a zero score
The backend correctly returns score: null/score_percent: null, but frontend normalization uses nullish fallback to 0:
raw.score ?? raw.score_percent ?? 0
data.score_percent ?? 0This causes no scan data to render as 0, 0%, and Poor. It also renders an all-excluded framework as 0%. That replaces a false-positive score with a false-negative score.
Required changes:
- Preserve
nulland propagate the backendstatus. - Render
Not assessed/No scan datarather than a gauge, percentage, trend point, orPoorlabel. - Distinguish
NO_SCAN_DATA,NO_IN_SCOPE_CONTROLS, and a genuinely evaluated numeric score. - Add frontend tests for these three states.
3. Blocker: historical mapping snapshots do not reproduce historical mappings
compliance_mapping_snapshot stores only pack metadata. get_compliance_score() still loads controls, mapping types, and denominator membership from the current live JSON, then labels the result using the old snapshot metadata.
After a mapping update, an old scan can therefore be evaluated with v2 controls while claiming v1 provenance. In addition, save_scan() overwrites compliance_mapping_snapshot during ON CONFLICT, so replaying a scan after a pack update mutates its historical identity.
Required changes:
- Either snapshot the complete normalized mapping used for the scan, or persist an immutable content hash/version reference that can retrieve the exact historical pack.
- Preserve the original snapshot/reference on idempotent scan replay; do not overwrite it silently.
- Store and validate a content hash in addition to a human-maintained semantic version.
- Do not silently fall back to live mappings for a response presented as historical.
- Add a test: save with v1, change live mappings to v2, query/replay the v1 scan, and prove its controls, classifications, denominator, hash, and metadata remain v1.
4. Blocker: PASS still does not prove successful rule evaluation
The current scan engine catches individual rule exceptions and still completes the scan. get_compliance_score() treats every rule absent from findings as PASS, so a failed, skipped, timed-out, or permission-denied rule can still become a pass.
The evaluation_basis disclaimer is useful but does not make the numeric score or PASS evidence-based.
Required resolution:
- Prefer merging #263 first and derive
PASSonly from persisted successful rule/resource evaluations; or - Until #263 exists, return
UNKNOWN/NOT_EVALUATEDfor absence where successful evaluation cannot be proven and exclude it from the pass denominator.
Until this is resolved, please change Closes #302 to a partial/reference relationship and keep #302 open.
5. API contract and documentation need to move together
get_score() changes from an integer to an object, but repository documentation and smoke tests describe conflicting contracts. The new successful response also omits max_score, while examples expect it. No-data smoke-test comparisons are not null-safe.
Required changes:
- Define one response schema containing at least
status,score, andmax_score. - Update API reference, frontend endpoint documentation, architecture/validation documentation, and smoke tests.
- Add route-level contract tests for
OKandNO_SCAN_DATA. - Preserve compatibility or version the endpoint if external consumers rely on the bare-number response.
The compliance NO_SCAN_DATA response should also include a consistent evaluation_basis and distinguish mapping composition from evaluation counts. in_scope_controls: 0 currently conflates “not evaluated” with “no in-scope mappings.”
6. Snapshot failures must not be silent
_build_compliance_mapping_snapshot() silently omits unreadable or invalid framework files. That can produce incomplete provenance and later fall back to live data.
Required changes:
- Log and persist snapshot completeness/error state.
- If mapping provenance is required for a completed scan, fail closed rather than silently omitting it.
- Test missing, malformed, and partially readable mapping packs.
7. Move semantic validation out of embedded CI YAML
The validation logic is valuable, but an 84-line Python program embedded in workflow YAML is difficult to unit-test and reuse.
Required changes:
- Move it to a repository script/module and invoke that from CI.
- Add invalid fixtures covering semantic versions, ISO dates, pack status,
N/A-*/mapping-type consistency, evidence-type consistency, reviewer/date requirements, and content hash generation.
8. Alembic migration ordering must be resolved before merge
PR #308 remains open and its migration shares down_revision = c7a2e9f1b3d4. If both merge unchanged, the repository will have multiple heads.
Required changes/process:
- Establish merge order explicitly.
- Rebase the second PR and chain its migration to the new head.
- Add a CI assertion that
alembic headsreturns exactly one head. - Re-run upgrade, downgrade, and upgrade against a populated database after rebasing.
Re-review checklist
- CIS and other framework mapping classifications are individually defensible.
- Synthetic
N/A-*mappings cannot enter the technical score denominator. - Independent sample review is recorded.
- Frontend preserves and visibly represents no-data/null states.
- Historical mapping results are immutable and reproducible.
- A rule cannot become
PASSwithout successful evaluation evidence. -
/api/scoreand compliance response contracts, docs, frontend, and tests agree. - Snapshot failures are explicit and fail safely.
- Mapping validation is testable outside workflow YAML.
- Alembic has exactly one head after merge-order coordination.
- New regression tests and the full CI/security suite pass.
Once these items are addressed, the PR will provide a much stronger foundation for trustworthy compliance reporting and the planned remediation automation.
de75e40 to
52b0faa
Compare
|
Thanks both for the thorough reviews — @ritiksah141's 8-item breakdown and @TFT444's two blockers caught real gaps, not nitpicks. Pushed a set of commits addressing them; here's what changed against each item (full detail in the updated PR description above):
Also rebased onto current Re-requesting review from both of you — happy to keep iterating on anything I read wrong. |
e0493bf to
ea11575
Compare
|
Seems like all of the concerns are addressed, kindly approve this @ritiksah141, @TFT444 . |
|
@parthrohit22, please resolve the conflicts and then do this 1. Rebases #310 onto the updated dev. down_revision = "d8e4f6a1b2c3"
alembic heads
|
ea11575 to
43ea824
Compare
|
@ritiksah141 Done, all six steps:
Two real issues turned up while reconciling the two branches' code, fixed rather than deferred:
Verified: full pytest suite (818 passed, 3 skipped), ruff check + format, mapping-pack validator, full frontend test/lint/build — and PR #310's CI is now green end-to-end (all 20 checks passing on |
ritiksah141
left a comment
There was a problem hiding this comment.
All good to me. Approving it again
|
@TFT444 Following up on your review from the 23rd — both items you flagged were addressed the next day:
Since then this also went through ritiksah141's full 8-item review (all addressed) and their re-approval today on the current head ( Your review is still showing as the standing |
|
@TFT444, both blockers from your earlier review now have concrete fixes on the current head: the migration is chained after #308 with a single Alembic head, and the empty-scan score returns |
TFT444
left a comment
There was a problem hiding this comment.
Both blockers resolved: Alembic chain fixed onto d8e4f6a1b2c3 and get_score now returns NO_SCAN_DATA instead of a false 100 when no scan exists.
|
@parthrohit22 branch has conflict please solve them after it good to go |
|
CI was red on this branch's head after a Fixed in the latest commit: filled in Verified: mapping-pack validator clean (was 55 errors), full backend suite (892 passed, 5 skipped — pre-existing/environment-only), |
m-khan-97
left a comment
There was a problem hiding this comment.
Parth, the mapping-pack work is strong: the Alembic chain is linear after d8e4f6a1b2c3, no-scan data is no longer reported as 100%, framework metadata and evidence semantics validate cleanly, and historical controls are captured with an integrity hash. I ran the mapping validator successfully and the focused non-route tests passed; the four local route setup errors were only because this host lacks prometheus_client, while authoritative CI is green.
I found one replay-consistency blocker in save_scan(). _scan_rule_outcomes.failed_rule_ids is stored inside compliance_mapping_snapshot, but the upsert preserves the entire first snapshot with COALESCE(scans.compliance_mapping_snapshot, EXCLUDED.compliance_mapping_snapshot). On a retry using the same scan_id, findings and status are deliberately replaced, yet the failed-rule set is not. If a transiently failed rule succeeds on the retry, the new findings are saved but compliance still reads the stale first-attempt rule as NOT_EVALUATED; the inverse is also possible. The comment calls the mapping snapshot immutable, which is correct for mapping provenance, but per-attempt execution outcomes are not immutable provenance.
Please preserve the framework snapshot while updating _scan_rule_outcomes to match the result being written, or store outcomes separately. Add a regression test that saves a scan ID with a failed rule, replays the same ID with that rule successful, and proves compliance no longer reports the stale NOT_EVALUATED state (and ideally the reverse direction as well). Once replayed findings and outcomes remain atomic, I will rereview.
|
@m-khan-97 addressed your Sep 7 blocker on
Subscription scoping, Also merged current @TFT444 @ritiksah141 re-requesting review since the scoring path changed materially from what you approved. |
ritiksah141
left a comment
There was a problem hiding this comment.
All addressed, so approving it.
|
@m-khan-97 please re-review this, I think it looks ready. Thank you. |
TFT444
left a comment
There was a problem hiding this comment.
@parthrohit22 three issues remain:
-
_scan_rule_outcomesnull-merge bug (finding.py, ON CONFLICT block): if a retry has zero failed rules,EXCLUDED.compliance_mapping_snapshot -> '_scan_rule_outcomes'is NULL and the|| jsonb_build_object(...)overwrites a prior non-null value with null. Use a CASE/WHEN guard so the key is only merged when non-null. -
N/A entries missing required fields: the new CI validator (
validate_mapping_pack.py) requiresmapping_type,evidence_type,primary_source,rationale, andreview_statuson every control. The N/A entries (AZ-DB-005, AZ-FUNC-001, etc.) only carrycontrol_id,control_name,description. The validator will fail on merge as-is. -
get_score()subscription_id not fixed:getattr(self, "subscription_id", None)always returns None because DatabaseManager has no such attribute. The subscription-scoped query branch never executes. Pass it as a parameter the same wayget_compliance_score()now does.
…tion Addresses the two real findings from the 2026-09-12 review. _scan_rule_outcomes null-merge: save_scan() only set the key when a rule had failed, so a clean retry left it absent, the ON CONFLICT block extracted SQL NULL from EXCLUDED, and jsonb_build_object stamped a JSON null over the previous attempt's outcomes. The key is now always written (an empty failed_rule_ids list is a real per-attempt result, not missing data), and the re-merge COALESCEs through the stored value to an explicit empty list, so no write path can null it out. Per-attempt refresh semantics are unchanged: a retry still replaces the prior attempt's outcomes rather than inheriting them. get_score() subscription scoping: the scoped branch read getattr(self, "subscription_id", None), which DatabaseManager never sets, so it was dead code and every production call was unscoped. subscription_id is now a parameter supplied by the route from ?subscription_id= with the AZURE_SUBSCRIPTION_ID fallback, matching get_compliance_score(); a malformed value is a 400 rather than a silently unscoped score. Also documents the subscription_id query parameter for /api/score and /api/compliance/<framework>, both of which were still listed as taking no query parameters. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
@TFT444 thanks — pushed 1.
|
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
2eb3a41 to
9e918f0
Compare
TFT444
left a comment
There was a problem hiding this comment.
All three blockers from the Sep 12 review are resolved:
_scan_rule_outcomesnull-merge now usesCOALESCE(EXCLUDED.compliance_mapping_snapshot -> '_scan_rule_outcomes', scans.compliance_mapping_snapshot -> '_scan_rule_outcomes')— a zero-findings retry can no longer overwrite a prior non-null value.- N/A entries carry the full required field set (
mapping_type,evidence_type,primary_source,rationale,review_status). The CI validator will pass. get_score()acceptssubscription_idas a parameter and appliesAND subscription_id = %scorrectly — no moregetattr(self, ...).
All earlier blockers (Alembic chain, CIS direct misclassifications, NO_SCAN_DATA false-100, COALESCE replay atomicity) were resolved in prior rounds. CI is green. Approving.
Resolves three conflicts: - CHANGELOG.md: keep both the OWASP#253 network-control entry and dev's AKS/OIDC entries. - frontend/src/utils/api.test.mjs: keep dev's memory-only bearer-token test and this branch's normalizeScore/normalizeComplianceFramework coverage; loadApiModule now accepts dev's storage option and the function shorthand these tests use. - compliance/frameworks/*.json: the 17 rules dev added (AZ-CMP-005/006 and AZ-AKS-007..021) arrived without mapping-pack evidence metadata, which the OWASP#302 validator requires. Annotated each with mapping_type, evidence_type, primary_source and rationale following the existing per-framework templates - not_applicable for the synthetic N/A-* CIS ids, direct for AZ-AKS-010 (CIS 2.1.8), supporting for ISO/NIST/SOC 2 - all left review_status pending_review so they stay out of the scored denominator until independently reviewed. Mapping packs bumped to 1.1.0. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
Merged current 1. 2. 3. Annotated all 68 entries (17 rules × 4 frameworks) following each framework's existing template:
Verification on the merged head: @m-khan-97 on your 7 Sep blocker — the evaluation-derived contract is in place on this head and survived the merge: @TFT444 @ritiksah141 re-requesting as well, since the merge touched the mapping packs your earlier findings were about. |
|
All blockers from my Sep 12 review are resolved as confirmed in my Sep 19 note. The branch is currently conflicting against dev. Please rebase onto current dev and push. Also pinging @m-khan-97 to re-review the latest commit as their Sep 7 CHANGES_REQUESTED was addressed in later pushes but they have not formally signed off on the current head. |
Brings in OWASP#325 (scan durability and idempotency), OWASP#327 (AZ-STOR-010) and OWASP#174 (DatabaseManager/route coverage). - save_scan: OWASP#325 replaced the INSERT ... ON CONFLICT with a lease-fenced UPDATE. The compliance_mapping_snapshot write now lives inside that UPDATE with the same semantics: framework provenance is only filled in when absent, _scan_rule_outcomes is re-merged on every write and never nulled. - Migration 3a76ff935bf6 now chains from d4a8c1e6b2f9 (OWASP#325's head), so alembic heads returns a single head again. - Mapping packs: kept this branch's reviewed entries and added AZ-STOR-010 to CIS (not applicable), ISO 27001 (A.13.1.3), NIST CSF (PR.AC-5) and SOC 2 (CC6.6) with the full mapping-semantics fields. - Tests: save_scan tests use the fenced signature; OWASP#174's get_score and compliance-route tests follow this branch's contracts; the admission migration tests resolve the head from the revision graph instead of pinning it. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
Merged current
Full suite against Postgres: 1487 passed, 3 skipped. @m-khan-97 your 7 Sep blocker was addressed in @TFT444 @ritiksah141 your approvals were dismissed by the merge push. Re-requesting in case you want to glance at the |
Picks up OWASP#344 (branch rulesets, post-merge CI) and OWASP#359 (evidence-grounded AI endpoints). Only CHANGELOG.md conflicted; kept both sides' entries. OWASP#359's AI routes use get_scan/get_latest_completed_scan/get_findings, whose signatures this branch leaves unchanged. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JMtsuR7tvJTsoq5KueraLf Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
Merged current Re-verified on the merged head against PostgreSQL 16:
I also rewrote the PR description. It still described the old
@m-khan-97 your 7 Sep review is the only change request still open. The fix has been on the branch since @TFT444 @ritiksah141 your approvals were dismissed by earlier merge pushes. Nothing in the logic changed in this merge. |
TFT444
left a comment
There was a problem hiding this comment.
Re-reviewed current head 81912c9. The commits since my Sep 19 approval (9e918f0) are all chore: merge upstream dev merges with no substantive changes to the compliance scoring logic, API routes, or tests. My approval of 9e918f0 stands: the three blockers I raised on Sep 12 are all resolved, CI is green, and the scoring is now evidence-derived and non-certifying as required.
The admission-test head pin (get_current_head() approach from this PR) will also unblock #352 once that migration's down_revision is fixed to a single parent. Merge order note: whichever of #310 or #352 lands second will need to repoint its down_revision to the other's revision ID. Worth coordinating with @TFT444.
Partially addresses #302. It stays open on merge because independent review of the mapping pack is still outstanding (see "Limitations").
Problem and security impact
Compliance reports could overstate assurance in ways that matter for anyone relying on them:
soc2.jsonandnist_csf.jsonrecorded the same control_id under differentcontrol_namestrings.nist_csf.jsonalso mixed CSF 1.1 subcategories with six SP 800-53 codes that don't exist in CSF.How compliance status is derived now (contract_version 3)
rule_evaluationsrows for the scan (feat: persist PASS/FAIL/ERROR/NOT_APPLICABLE per rule per resource, fix compliance score #263/feat(engine): add rule evaluation coverage contract (#263) #321), viaaggregate_status()(FAIL > ERROR > UNKNOWN > PASS > NOT_APPLICABLE). Findings are used only for severity, category and affected-resource detail.UNKNOWN. It is never a PASS inferred from the absence of a finding._scan_rule_outcomes.failed_rule_ids) is forced toERROR. That signal only ever makes a status stricter.mapping_typenot_applicableandorganizational, plus any mapping whosereview_statusis notreviewed(reported asUNREVIEWED_MAPPING). Every shipped mapping is currentlypending_review, so no unreviewed mapping can raise a score.NO_SCAN_DATA(no completed scan) returnsnullcounts. It is distinct from a completed scan with no in-scope or no reviewed controls.Implementation summary
compliance/frameworks/*.json(all 6 packs):mapping_type,evidence_type,primary_source,rationale,owner,review_statusandreview_date.mapping_pack_version,status,sourceandpublished.control_nameinconsistencies are fixed, and the six SP 800-53 codes innist_csf.jsonare remapped to CSF 1.1 subcategories.AZ-PQC-*mappings in pre-PQC frameworks and the CISN/A-*entries arenot_applicable.api/models/finding.py:get_compliance_score(framework, subscription_id)implements the model above.get_score(subscription_id)returnsNO_SCAN_DATAinstead of a false 100.subscription_idis a real parameter supplied by the routes.save_scan()writes the full framework mapping (controls plus a content hash) intoscans.compliance_mapping_snapshotinside fix(core): harden scan durability and idempotency (#303) #325's lease-fencedUPDATE. Framework provenance is only filled in when absent._scan_rule_outcomesis re-merged on every write and can never be nulled.snapshot,snapshot_no_hash,snapshot_hash_mismatchorlive_fallback_*.scanner/engine.py: recordsfailed_rule_idsfor any rule that raised or returned malformed data.alembic/versions/3a76ff935bf6_...: adds the nullablecompliance_mapping_snapshotJSONB column, chained fromd4a8c1e6b2f9(fix(core): harden scan durability and idempotency (#303) #325's head). There is a single Alembic head..github/scripts/validate_mapping_pack.py: mapping-pack semantics validator, run by CI./api/scoreand/api/compliance/<framework>accept?subscription_id=. They fall back toAZURE_SUBSCRIPTION_IDand return400on a malformed value.api.js,ScoreGauge.jsx,FrameworkCards.jsxandMonitoring.jsxpreservenullandstatusend to end instead of coercing to0.docs/compliance-mapping-pack.mdanddocs/api-reference.mddescribe the evaluation-derived semantics and response contract.Acceptance criteria (#302)
rule_evaluations, feat: persist PASS/FAIL/ERROR/NOT_APPLICABLE per rule per resource, fix compliance score #263/feat(engine): add rule evaluation coverage contract (#263) #321).ownerandreview_datestaynulluntil someone reviews it.N/A-*reclassification.Validation (on the current head, merged with
devatdf98060)alembic heads3a76ff935bf6alembic upgrade head→downgrade -1→upgrade head(PostgreSQL 16)python .github/scripts/validate_mapping_pack.py compliance/frameworkspytest tests/ --cov=api --cov=scanner --cov-fail-under=80against PostgreSQLruff check ./ruff format --check .node frontend/src/utils/api.test.mjs/aiApi.test.mjsnpm run lint/npm run buildLimitations and follow-up
review_statusispending_review, withownerandreview_dateset tonull. Because unreviewed mappings are excluded from the denominator, framework scores stay "no reviewed controls" until reviewers sign mappings off. That is deliberate: it is better than reporting a percentage built on unreviewed claims. CI rejects areviewedentry that is missing an owner or date, so review can't be faked.N/A-*fix, CISdirectclassifications are still framework-level and need the per-control review above.website/content.jsstill shows"NIST": "AC-17"forAZ-KV-002. It is static marketing content with no dependency on the framework files, so it is out of scope here.No secrets or sensitive infrastructure data
Confirmed: this PR adds no credentials, tokens, connection strings, IPs or hostnames.