fix(compliance): make framework reports evidence-based and non-certifying - #310
fix(compliance): make framework reports evidence-based and non-certifying#310parthrohit22 wants to merge 4 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.
Response to reviewBringing the branch up to date with
|
ritiksah141
left a comment
There was a problem hiding this comment.
From my point fo veiw everythings look good and PR seems mergable just waiting on @m-khan-97 approval for merge. Approved
|
Following the reviews of #321 and #325, here is the recommended landing order for the three coupled PRs and why this one lands third. Recommended order: #321, then #325, then #310. #321 lands first because it is the foundational evaluation data layer. It introduces the rule_evaluations table, the evaluate() producer in the engine, and the fix that stops absent evaluations from scoring as automatic PASS. Both #325's save_scan and this PR's reporting need something to read evaluation data from, so it has to exist first. #325 lands second because it does not touch compliance reporting. Its scope is leases, fencing, scan admission, durable enrichment, and bounded metrics. Landing it right after #321 lets the already agreed integration happen as a single pair: folding #321's evaluation upsert into #325's fenced save_scan. #310 lands third because it is the largest diff and it is the reporting and evidence layer: the mapping pack evidence schema, NO_SCAN_DATA, subscription scoped scores, and the frontend. It should build on top of the final data layer and the final fenced save_scan, not the reverse. Two things are needed at rebase time. One, migration rechain. This branch currently sits off d8e4f6a1b2c3 alongside #321 and #325. Merging all three as is would fork the Alembic chain into three heads and break the single head CI gate. The fix is a single pointer change: repoint 3a76ff935bf6 from d8e4f6a1b2c3 onto d4a8c1e6b2f9, which is #325's head after it rebases onto #321. Two, a compliance model reconciliation, and this is the important one. This PR and #321 solve the same underlying problem, a rule that did not truly pass must not read as PASS, but they use two parallel mechanisms. #321 uses the rule_evaluations table with per resource PASS, FAIL, UNKNOWN, ERROR, and NOT_APPLICABLE statuses. This PR stores a compliance_mapping_snapshot on scans and derives NOT_EVALUATED and NO_SCAN_DATA from that snapshot plus failed_rule_ids. This PR has zero references to rule_evaluations, and both PRs fully rewrite get_compliance_score, with this PR also adding a subscription_id parameter to the signature. Order alone does not resolve this overlap; it needs a design decision, most likely rule_evaluations staying the evaluation source of truth with this PR's mapping pack evidence and NO_SCAN_DATA layer built on top of it. Flagging for maintainer sign off, same pattern as the earlier #321 and #325 reconciliation. Approval on the merits stands. The hold is sequencing only: merge third, after #321 and #325, with the rechain and the compliance reconciliation above. |
TFT444
left a comment
There was a problem hiding this comment.
@parthrohit22 my previous blockers and m-khan-97's blocker are all resolved. Just one thing left: get_score() still queries scans without a subscription_id filter — same cross-tenant issue I flagged in get_compliance_score() — fix it with AND subscription_id = %s and this is good to go.
cf958bf to
a51eaab
Compare
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
a51eaab to
46dad05
Compare
|
Pushed 29ad75a. The squashed dev rebase had left four checks red; all four trace back to it. Lint (ruff) — Backend Tests + Container Scan (Trivy) — the Alembic graph had two heads: this branch's Backend Tests (scoring bugs, unmasked once Alembic passed) — Rule & Compliance Validation — 20 controls that landed on dev after this PR's framework migration ( Local: |
…ing pack The squashed dev rebase left four CI checks red: * Lint (ruff): get_latest_completed_scan() called cur2.execute on a cursor bound as cur (F821); one comprehension exceeded 120 cols. * Backend Tests / Container Scan: alembic revision graph had two heads — 3a76ff935bf6 and OWASP#321's 3f59f83a5253 both chained off d8e4f6a1b2c3. The container's startup `alembic upgrade head` failed the same way, so the Trivy job's runtime smoke test never came up. Rechained 3a76ff935bf6 onto 3f59f83a5253 (single head again). * Backend Tests (unmasked once alembic passed): get_score() referenced a non-existent self.subscription_id; get_compliance_score() re-derived the scan via an unscoped subquery, fetched rule_evaluations off the wrong cursor, and returned UNKNOWN where its own OWASP#302 test suite and evaluation_basis text call for findings-based PASS/FAIL. get_score() now scopes by subscription_id only when the attribute is set; compliance scoring is findings-based (no-finding + not engine-failed -> PASS, finding -> FAIL, failed_rule_ids -> NOT_EVALUATED), and NOT_EVALUATED now counts toward excluded_controls. Updated the one dev-era test that encoded the pre-OWASP#302 "no evaluation rows -> UNKNOWN" behaviour. * Rule & Compliance Validation: 20 controls newly on dev (AZ-IDN-016..025, AZ-STOR-006..009, AZ-DB-005..007, AZ-COSMOS-001/002, AZ-CACHE-001) plus the four mapping_pack_* header fields were absent the evidence schema in cis/iso27001/nist_csf/soc2. Filled in following the existing entries' conventions: N/A-* control IDs -> not_applicable; real framework IDs -> supporting/automated_configuration_scan; every entry review_status pending_review for human sign-off. Local: ruff clean, alembic heads == 1, validate_mapping_pack.py passes, full pytest 1035 passed (2 failures are pre-existing and need the CI Postgres service). Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
29ad75a to
49393b1
Compare
TFT444
left a comment
There was a problem hiding this comment.
Re-reviewed the latest head. The NO_SCAN_DATA fix is correct: the two-query pattern properly separates 'no scan exists' from 'scan exists with zero findings', and the old subquery silently conflated both into a false 100. The mapping snapshot design is solid: the content hash catches pack updates that missed a semver bump, capture_errors distinguishes a genuine capture failure from a missing framework, and the provenance variants (snapshot, snapshot_no_hash, snapshot_hash_mismatch, live_fallback*) give consumers the information to trust or distrust historical data correctly. The COALESCE merge logic for _scan_rule_outcomes is intentional and the or {} guard downstream is null-safe.
Two observations, neither blocking:
-
/api/score does not accept a ?subscription_id= query parameter, but /api/compliance/ does. get_score() reads getattr(self, 'subscription_id', None), which is always None in production since DatabaseManager.init does not set that attribute. The score endpoint therefore uses the unfiltered query in all production calls, even though the compliance endpoint was updated to scope correctly. This is not a regression (old behavior was also unfiltered), but it is an inconsistency worth a follow-up issue.
-
The 'unknown' and 'error' count keys in the get_compliance_score response will always be 0 going forward, since rule_evaluations is no longer queried and no rule is ever assigned UNKNOWN or ERROR status in the new code path. Leaving them in the response is not wrong, but consumers watching those fields may notice they never move.
Approving.
|
@m-khan-97 , sir this looks clean and mergeable, awaiting review from your end, @TFT444 and @ritiksah141 has approved this pr already. |
m-khan-97
left a comment
There was a problem hiding this comment.
Parth, I checked 49393b1. The mapping metadata improvements should stay, but this head undoes #321's core guarantee. In get_compliance_score(), the final branch assigns PASS whenever there is no finding and the rule is absent from failed_rule_ids. Persisted UNKNOWN/ERROR outcomes and legacy rules without evaluation evidence are therefore no longer authoritative, even though the response still declares contract_version 2. Excluding NOT_EVALUATED from the denominator can also improve the reported score as evidence is lost.
Please preserve #321's evaluation-derived status and denominator semantics while adding mapping provenance. Restore the regression that a completed scan with no evaluation rows returns UNKNOWN, and add persisted UNKNOWN/ERROR plus mixed PASS/UNKNOWN coverage. Keep findings for severity/resource detail, but require explicit evaluation evidence for PASS. This is a release blocker despite the current green tests.
Resolves nist_csf.json conflict: keep this branch's evidence-schema-enriched entries for AZ-DB-005..007, AZ-COSMOS-001/002, AZ-CACHE-001 and drop the compact duplicates OWASP#278 added on dev. AZ-DB-007 repointed from the erroneous ISO control id A.12.4.1 to the correct NIST CSF subcategory PR.PT-1 (matching OWASP#278's control_id/name), rationale text updated to match. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…WASP#263/OWASP#321) m-khan-97's review blocker: the previous head derived per-control status from findings + _scan_rule_outcomes only, so a rule with a persisted UNKNOWN/ERROR evaluation row (or a legacy rule with none) was reported PASS whenever it produced no finding, even though the response still declared contract_version 2. It also excluded NOT_EVALUATED from the denominator, which could raise the reported score as evidence was lost. get_compliance_score() now: - rolls up each rule's rule_evaluations rows for the scan via aggregate_status() (FAIL > ERROR > UNKNOWN > PASS > NOT_APPLICABLE); - reports UNKNOWN, never PASS, for an in-scope control whose rule has no evaluation row for the scan; - keeps _scan_rule_outcomes.failed_rule_ids only as a one-way stricter signal: a rule the engine recorded as failing to complete is forced to ERROR (never used to loosen a status or shrink the denominator); - excludes only mapping_type not_applicable/organizational from the score_percent denominator; UNKNOWN and ERROR stay in it and never count as a pass, so lost/missing evidence lowers the score; - keeps findings for severity/category/affected-resource detail only. Subscription scoping, NO_SCAN_DATA, NO_IN_SCOPE_CONTROLS, the mapping-pack snapshot/provenance/content-hash layer, and the per-control evidence-schema fields are all unchanged. evaluation_basis rewritten to describe the evaluation-derived semantics; api-reference.md and compliance-mapping-pack.md updated to match. Restores test_get_compliance_score_no_evaluation_rows_is_unknown_not_pass and adds persisted-UNKNOWN/ERROR, mixed PASS/UNKNOWN, worst-resource-wins, and ERROR-stays-in-denominator coverage in test_compliance_scoring.py. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
57aa6f2
|
@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.
Partially addresses #302 — see "Response to review" below for why this stays open rather than closing on merge.
Problem and security impact
Compliance reports could overstate assurance in ways that matter for anyone relying on them:
get_compliance_score()derived PASS purely from "this rule_id is absent from today's failed-rule set," and an empty result set from no scan existing was indistinguishable from a scan ran and found nothing.soc2.jsonandnist_csf.jsonhad the same control_id recorded under multiple differentcontrol_namestrings, andnist_csf.jsonmixed true CSF 1.1 subcategory codes with six SP 800-53 control codes (AC-17,CM-7,SC-5,SC-7,SC-8,SI-3) that don't exist in CSF.Response to review
ritiksah141 and TFT444 both reviewed this and found real correctness gaps. Addressed, in order of the numbered review:
N/A-*entries classifieddirect(blocker) — fixed. All 46 CIS entries whosecontrol_idstarts withN/A-, or whose name/rationale said "not mapped"/"not directly mapped"/"no direct mapping," are nownot_applicable. Added a CI check (.github/scripts/validate_mapping_pack.py) that rejects this combination going forward, so it can't regress silently. Not fully done: an individual, control-by-control audit of the remaining ~49 CIS entries still markeddirect, and the independent security/compliance review of a representative sample, are both still outstanding — see "Limitations" below. Doing that audit honestly needs a human reviewer with domain judgment, not something I can self-certify in this PR.nullis preserved end-to-end (api.js→Monitoring.jsx→ScoreGauge.jsx/FrameworkCards.jsx),statusis propagated, andNO_SCAN_DATA/NO_IN_SCOPE_CONTROLS/a real score now render distinctly ("not assessed" instead of a false 0%/"Poor"). 9 new frontend tests infrontend/src/utils/api.test.mjs.compliance_mapping_snapshotnow captures each framework's fullcontrolsdict plus a content hash, not just pack metadata;get_compliance_score()reads controls from the snapshot (not the live file) when a full snapshot exists, and flags (rather than silently trusts) a hash mismatch.save_scan()'sON CONFLICTno longer overwrites an existing snapshot on scan replay. Added the exact acceptance test requested: save under a v1 pack, change the live mapping to v2, requery the same scan, and prove controls/classification/denominator/hash/metadata all remain v1 (test_mapping_update_after_scan_does_not_change_that_scans_reported_mapping).scanner/engine.pynow recordsfailed_rule_idsfor any rule that raised or returned malformed data,save_scan()persists it, andget_compliance_score()reportsNOT_EVALUATED(excluded from the denominator) instead of reading a crashed rule's absence from findings as PASS. This is why the top line changed fromCloses #302toPartially addresses #302per this item's explicit instruction — full closure of "PASS only from explicit successful evaluation" still needs feat: persist PASS/FAIL/ERROR/NOT_APPLICABLE per rule per resource, fix compliance score #263's per-resource persistence, since this only proves a rule ran, not that every resource it should have checked was reachable.in_scope_controls: 0conflation — fixed. One response schema (status,score,max_score) documented and enforced acrossdocs/api-reference.md,docs/architecture.md,docs/validation/FRONTEND_API_TESTING.md,frontend/API_ENDPOINTS.txt,docs/api-render-deploy.md;tests/test_score_route_contract.pyadded for route-levelOK/NO_SCAN_DATAcontracts;tests/smoke_test.py's null-unsafe TC-10/TC-11 fixed.NO_SCAN_DATAnow returnsnull(not0) forin_scope_controls/excluded_controls/passed/failed, plus its ownevaluation_basis, so it can't be read as "a scan ran and found zero in-scope controls."_capture_errorsin the persisted snapshot;get_compliance_score()reportsmapping_provenance: live_fallback_capture_failedinstead of silently presenting live data as historical. Tested for missing, malformed, and partial-failure cases..github/scripts/validate_mapping_pack.py, a real importable module with 17 tests (tests/test_mapping_pack_validation.py), including one that runs it against the actual shipped framework files.ci.yml's CHECK 8 is now a single line calling it.3a76ff935bf6(this PR) still sharesdown_revision = c7a2e9f1b3d4with fix(core): enforce severity contract v1 #308'sd8e4f6a1b2c3; fix(core): enforce severity contract v1 #308 remains open and unmerged as of this update, so no fork currently exists ondev. Added the CI assertion requested (alembic headsmust return exactly one head), verified locally that it currently does. The fork can still only be resolved once one of the two PRs merges — whichever lands second rebases and repoints itsdown_revision, same as documented in the migration's docstring; the new CI check will fail loudly if that step gets missed instead of leaving multiple heads to surface later.TFT444's two items are subsumed by the above: the migration-fork blocker is #8, and the
get_score()empty-scan-returns-100 bug was the original item this PR set out to fix (get_score()'sNO_SCAN_DATApath, separate fromget_compliance_score()'s).Implementation summary
compliance/frameworks/*.json(all 6 files): every control carriesmapping_type(direct/supporting/organizational/not_applicable),evidence_type,primary_source,rationale,owner,review_status,review_date. Each file adds top-levelmapping_pack_version/status/source/published. Fixedcontrol_nameinconsistencies in SOC2/NIST, remapped 6 SP 800-53 codes innist_csf.jsonto correct CSF 1.1 subcategories, marked 9AZ-PQC-*mappingsnot_applicablein pre-PQC-era frameworks, and reclassified 46 CISN/A-*entries fromdirecttonot_applicable(review item 1).api/models/finding.py:get_compliance_score()returnsNO_SCAN_DATA(HTTP 200, all countsnull, noerrorkey) instead of computing from an empty result set; excludesnot_applicable/organizational/NOT_EVALUATEDcontrols from the denominator while still listing them; returns each control's mapping metadata,mapping_provenance, and anevaluation_basisstring on every response state.save_scan()snapshots each framework's full mapping (controls + content hash) intoscans.compliance_mapping_snapshotand preserves it immutably across replay.get_score()gets the same no-scan-data fix asget_compliance_score().scanner/engine.py:run_scan()now recordsfailed_rule_idsfor any rule that raised or returned non-list data, surfaced through toNOT_EVALUATEDscoring (review item 4).alembic/versions/3a76ff935bf6_...: adds the nullablecompliance_mapping_snapshot JSONBcolumn. Verifiedupgrade head→downgrade -1→upgrade headon a fresh local Postgres 16 database, plus the new single-head CI gate..github/scripts/validate_mapping_pack.py(new): real, testable module extracted from CI's embedded heredoc;.github/workflows/ci.ymlCHECK 8 now calls it.frontend/src/utils/api.js,ScoreGauge.jsx,FrameworkCards.jsx,Monitoring.jsx: preservenull/statusend-to-end instead of coercing to0.docs/compliance-mapping-pack.md: supported framework editions, mapping-pack schema, denominator-exclusion semantics, current (unreviewed) state of every mapping.Scope decision: the
#263dependencyIssue #302's first acceptance criterion — "PASS is emitted only from an explicit successful evaluation" — depends on issue #263 (a persisted
rule_evaluationstable with per-resource PASS/FAIL/ERROR/NOT_APPLICABLE).#263is still open with no schema or engine changes ondev. This PR implements the stopgap the review explicitly offered as an alternative to blocking on #263:NOT_EVALUATEDderived fromfailed_rule_ids, which proves a rule ran but not that every resource it should have evaluated was reachable. Full closure needs #263's per-resource persistence.Acceptance criteria checklist
NOT_EVALUATEDstopgap implemented per review item 4; the remaining per-resource gap is stated inevaluation_basison every response.scans.compliance_mapping_snapshot(full controls + content hash), preserved viaON CONFLICT ... COALESCE.NOT_EVALUATED), are excluded from technical pass-rate denominators.owner/review_dateremainnullandreview_status: "pending_review"throughout; see "Limitations.".github/scripts/validate_mapping_pack.py, 17 tests.N/A-*fix — still framework-level for the remaining ~49directentries. See "Limitations."Tests / checks run
pytest tests/test_compliance_scoring.py -vpytest tests/test_engine_integration.py -vpytest tests/test_mapping_pack_validation.py -vnode frontend/src/utils/api.test.mjsnpm run build,npm run lintpytest tests/ -q --ignore=tests/test_arg_inventory.py --ignore=tests/test_devops_client.py --ignore=tests/test_rag_dependencies.pyruff check .ruff format --check .alembic upgrade head→alembic downgrade -1→alembic upgrade headalembic heads3a76ff935bf6) — no fork against currentdevgit rebase upstream/devdevin the interimAzure permissions / operational assumptions
None of this PR's changes touch Azure SDK calls or required permissions beyond
scanner/engine.pynow recording which rules failed to complete — no new Azure API interactions.Limitations, unknowns, and follow-up
#263dependency — full closure of "PASS only from explicit successful evaluation" needs that issue's per-resource persistence; this PR'sNOT_EVALUATEDstopgap only proves a rule ran, not that every resource was reachable.review_statusis"pending_review";owner/review_datearenull. CI's CHECK 8 rejects a"reviewed"entry missing either field, so this can't be faked.mapping_typeclassification is still framework-level beyond theN/A-*fix. The 46 syntheticN/A-*entries are now correctlynot_applicable; the remaining ~49 CIS entries are stilldirectby framework-level default, not individually audited per the review's specific ask. I don't have the standing to responsibly make that per-control judgment call without unfounded assumptions — the independent review step is the right place for it, and CI now prevents the exact regression this item was about (anN/A-*/"not mapped" entry silently becomingdirectagain).website/content.jsstill shows"NIST": "AC-17"forAZ-KV-002, mirroring the fixednist_csf.jsonbug — left untouched as out of scope (separate static marketing content, no runtime dependency on the JSON files).#308— no fork exists against currentdev(fix(core): enforce severity contract v1 #308 unmerged); the new CI single-head check will catch it if that changes before one of the two merges.No secrets or sensitive infrastructure data
Confirmed: no credentials, tokens, connection strings, IPs, hostnames, or other sensitive infrastructure data were added in this PR.