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 13 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
|
Every rule was force-mapped into CIS/NIST/ISO27001/SOC2 with no rationale, evidence type, or source, so weak and inapplicable mappings looked identical to direct technical evidence. Each of the 380 mappings across the four frameworks now carries mapping_type (direct/supporting/organizational/ not_applicable), evidence_type, primary_source, rationale, owner, review_status, and review_date, plus top-level mapping_pack_version/status/ source/published metadata per file. Also fixes real data-quality bugs found while adding the schema: - soc2.json and nist_csf.json had the same control_id under multiple inconsistent control_name strings (e.g. SOC2 CC6.6 appeared as three different names); each control_id is now canonicalized to one name. - nist_csf.json mixed 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); these are remapped to their correct CSF 1.1 subcategories per NIST's own Appendix A crosswalk. - AZ-PQC-* rules are marked not_applicable in nist/iso27001/soc2, since those framework editions predate post-quantum migration guidance and define no relevant control; mapping them in anyway overstated coverage. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
get_compliance_score() computed status = "FAIL" if rule_id in failed_rule_ids else "PASS", so a rule that never ran (no completed scan at all) was indistinguishable from a clean pass — a fresh install with zero scans reported 100% compliant on every framework. It also forced every control into the pass-rate denominator, including ones now marked not_applicable or organizational, which would have silently counted them as passing. get_compliance_score() now: - returns an explicit NO_SCAN_DATA result (score_percent: null, HTTP 200, no "error" key) when no completed scan exists, instead of computing a score from an empty result set; - excludes not_applicable/organizational controls from score_percent's denominator while still listing them with a NOT_APPLICABLE/ORGANIZATIONAL status; - surfaces mapping_type, evidence_type, primary_source, rationale, owner, review_status and review_date per control, and an evaluation_basis string stating plainly that PASS reflects absence of findings, not confirmed per-resource execution (full closure of that gap needs the persisted rule-evaluation contract tracked in issue openshield-org#263, which this PR does not implement — see the PR description for scope). save_scan() now also snapshots each framework's mapping_pack_version/ status/source/published into a new nullable scans.compliance_mapping_snapshot JSONB column (migration 3a76ff935bf6) at scan-completion time. get_compliance_score() prefers that snapshot for the scan it reports on, so a historical report keeps showing the mapping-pack identity that was actually in effect when it ran instead of being silently reinterpreted under whatever mapping pack is deployed later. frontend/src/utils/api.js: normalizeComplianceFramework() defaults the now- nullable score_percent to 0 instead of rendering "null%". Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
Adds tests/test_compliance_scoring.py covering get_compliance_score()'s new behavior: no completed scan (NO_SCAN_DATA, not a false 100%), unknown framework, direct-mapping PASS/FAIL, not_applicable/organizational exclusion from the score denominator, mapping-pack snapshot preferred over the live file for a specific scan, _build_compliance_mapping_snapshot() reading all configured frameworks and skipping a missing file, save_scan() persisting the snapshot column, and the /api/compliance/<framework> route returning 200 (not 500) for the no-scan-data state. tests/test_clean_scan.py's three get_compliance_score tests predate the scan-existence check: they simulated "no findings" with an empty result set on every cursor call, which is also what "no scan has ever run" now looks like, and the two states must resolve differently. Updated each to supply an explicit completed-scan row via fetchone() so they continue testing what they were written to test — a clean completed scan showing all PASS — via the two-query design the fix requires, and updated the scoping test's SQL assertions to check both statements instead of assuming a single combined query. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
CHECK 7 only confirmed every compliance JSON control referenced an existing
rule file. It never validated the content of a mapping — a control could
carry no rationale, an invalid mapping_type, or a "reviewed" status with no
owner or review date, and CI would still pass.
Adds CHECK 8 (rule-validation job): every control must have mapping_type in
{direct, supporting, organizational, not_applicable}, non-empty evidence_type/
primary_source/rationale strings, review_status in {pending_review, reviewed},
and owner/review_date that are either null or non-empty. A "reviewed" control
without both owner and review_date fails, since an unverifiable review status
recreates the problem this schema exists to fix. Each framework file's four
mapping_pack_* top-level fields must also be present.
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
Adds docs/compliance-mapping-pack.md: the currently supported framework editions (CIS 2.0.0, NIST CSF 1.1, ISO/IEC 27001:2013, SOC 2 2017 TSC, NCSC UK PQC 2025, ENISA PQC 2021), the full mapping-pack schema with the meaning of each mapping_type, how score_percent's denominator excludes not_applicable/ organizational controls, how the compliance_mapping_snapshot column keeps a historical report accurate, and the current (unreviewed) state of every mapping's review_status. Updates docs/adding-a-rule.md's compliance-file section and CI-check count for the new required per-control fields, docs/security-requirements.md's existing certification disclaimer to point at the new doc, and links it from README.md's policy list. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…tion after openshield-org#308 get_score() had the same bug get_compliance_score() was fixed for: folding the completed-scan lookup into a `scan_id = (SELECT ...)` subquery cannot distinguish "no completed scan exists" from "the latest completed scan found nothing" - both produce zero rows, so the former was silently reported as a perfect 100 with no actual evidence behind it. get_score() now checks for a completed scan first and returns an explicit NO_SCAN_DATA result (score: null) when none exists, only computing a real score once one has run. Also: alembic/versions/3a76ff935bf6 shared down_revision=c7a2e9f1b3d4 with PR openshield-org#308's migration, which forks the Alembic revision graph if both merge independently. Chained this one after openshield-org#308's d8e4f6a1b2c3 instead (openshield-org#308 was opened first); noted in the migration's docstring that the ordering needs to flip if openshield-org#308 ends up merging after this PR. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
… merge-time coordination Chaining 3a76ff935bf6's down_revision onto PR openshield-org#308's d8e4f6a1b2c3 broke this PR's own CI: alembic upgrade head resolves the revision map from whatever files exist in the branch it's run against, and openshield-org#308's migration file doesn't exist here since openshield-org#308 hasn't merged yet (KeyError: 'd8e4f6a1b2c3' during Apply database migrations). Reverted to down_revision=c7a2e9f1b3d4 so this branch's own migration chain resolves again. The actual fork between this migration and openshield-org#308's can only be resolved once one of the two merges - documented in the migration's docstring for whoever merges second. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…roduce history Item 4 (PASS did not prove evaluation): scanner/engine.py's run_scan() now records failed_rule_ids for any rule that raised or returned malformed data. save_scan() persists this into compliance_mapping_snapshot under a reserved _scan_rule_outcomes key. get_compliance_score() reads it back and reports NOT_EVALUATED (excluded from the denominator like not_applicable/ organizational, but for a different reason: missing evidence, not a control the mapping pack says a scan can't establish) instead of reading a crashed rule's absence from findings as a clean PASS. Item 3 (historical snapshots did not reproduce historical mappings): _build_compliance_mapping_snapshot() now captures each framework's full controls dict plus a content hash, not just pack metadata. get_compliance_score() uses the snapshotted controls (not the live file) when a full snapshot exists, verifies the stored hash against the stored controls, and flags a mismatch instead of silently trusting a corrupted snapshot. save_scan()'s ON CONFLICT no longer overwrites an existing snapshot on scan replay (COALESCE keeps the first write), so a retried scan with the same scan_id can't mutate its historical mapping identity after a later pack update. A pre-existing metadata-only snapshot (saved before this change) is now correctly labelled live_fallback_legacy_snapshot rather than "snapshot", since it can't actually reproduce the historical controls/classification/denominator. tests/test_engine_integration.py: updated the test that used to document the observability gap this closes, and added coverage for the malformed-data path and for failed_rule_ids always being present as a list. tests/test_compliance_scoring.py: added coverage for NOT_EVALUATED scoring, failed_rule_ids persistence into _scan_rule_outcomes, the legacy-snapshot fallback, hash-mismatch detection, and the acceptance test item 3 explicitly asked for — save a scan under a v1 mapping pack, change the live mapping to v2, requery the same scan, and prove its controls/classification/denominator/ hash/metadata all remain v1. Also flagged (not fixed here, out of scope for this PR): MockAzureClient is missing get_recovery_vault_security_posture/get_function_app_security_posture/ get_private_endpoint_posture, so 15 AZ-BAK/AZ-FUNC/AZ-PE rules error against the offline test mock instead of actually running. This was invisible before failed_rule_ids existed; it's a real, separate test-infrastructure gap now tracked for its own fix. Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
… null-score handling Item 1 (CIS N/A entries overstate coverage): 46 CIS control entries whose control_id starts with N/A- (e.g. N/A-SC-001 "not mapped in CIS Azure Foundations 2.0.0") were classified mapping_type: direct, so they stayed in the scoring denominator and scored PASS when absent from findings - recreating the exact overstatement this PR exists to prevent. Reclassified all 46 to not_applicable with matching evidence_type and rewritten rationale. NIST/ISO27001/SOC2 had zero entries with this bug (verified directly). .github/scripts/validate_mapping_pack.py (new): extracted CI's embedded Python heredoc into a real, importable, testable module. Added a check the review specifically asked for - an N/A-* control_id, or a rationale/name containing "not mapped"/"not directly mapped"/"no direct mapping", can no longer be classified direct - plus semver validation, ISO date validation, and mapping_pack_status enum validation. .github/workflows/ci.yml now calls this script instead of embedding ~80 lines of heredoc Python. tests/test_mapping_pack_validation.py (new): 17 tests, including one that runs the validator against the real shipped framework files. Item 2 (frontend converts "no evidence" into a zero score): api.js's normalizeScore/normalizeComplianceFramework used `?? 0` on score fields, so a genuinely absent score (NO_SCAN_DATA, NO_IN_SCOPE_CONTROLS) rendered as a false 0%/"Poor" instead of "not assessed". Preserved null through normalizeScore, normalizeComplianceFramework, Monitoring.jsx, ScoreGauge.jsx, and FrameworkCards.jsx, and added the null/NO_SCAN_DATA/NO_IN_SCOPE_CONTROLS UI states the review asked for. frontend/src/utils/api.test.mjs (new): 9 tests covering both normalizers and an end-to-end getScore() call, following the existing aiApi.test.mjs pattern (no test runner - real source loaded via readFileSync, import.meta.env neutralized, evaluated with new Function()). api/models/finding.py: get_score()/get_compliance_score() now return max_score/status fields consistently; tests/smoke_test.py's TC-10/TC-11 null-safety bug fixed (`.get(key, default)` only applies the default when the key is *missing*, not when it's present as None, so `0 <= None <= 100` was throwing TypeError - lambdas now check status before requiring a numeric score). tests/test_score_route_contract.py (new): route-level contract tests for /api/score's OK and NO_SCAN_DATA shapes. docs/api-reference.md, docs/architecture.md, docs/validation/FRONTEND_API_TESTING.md, frontend/API_ENDPOINTS.txt, docs/api-render-deploy.md: corrected stale claims that /api/score returns a bare integer, and fixed the "Supported frameworks" table (was missing ncsc_pqc/enisa_pqc) and the "unknown framework" error example (actual code 400s via choice() validation, not the DB layer's 500). Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
… add alembic single-head CI gate
Item 5 (in_scope_controls: 0 conflates "not evaluated" with "no in-scope
mappings"): get_compliance_score()'s NO_SCAN_DATA response returned
in_scope_controls/excluded_controls/passed/failed as 0, indistinguishable
from a completed scan that genuinely found zero in-scope controls (the
separate NO_IN_SCOPE_CONTROLS case). Changed all four to null for
NO_SCAN_DATA and added an evaluation_basis string explaining why, matching
the evaluated-response shape which already carries one.
Item 8 (Alembic ordering): added a CI step ("Check Alembic revision graph
has exactly one head") that runs `alembic heads` and fails the build if it
doesn't return exactly one - the assertion the review specifically asked
for. A migration fork against an unmerged sibling PR is invisible to either
PR's own CI in isolation (each only has its own migration file on disk), so
this catches it explicitly once both land on the same branch, instead of
letting `alembic upgrade head` fail unhelpfully on whichever merges second.
Verified locally: `alembic heads` currently returns exactly one head
(3a76ff935bf6), confirming this PR's migration doesn't fork against
upstream/dev's current state.
Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
…ontract v1 Rebases this branch onto dev now that PR openshield-org#308 (severity contract v1, d8e4f6a1b2c3) has merged, and repoints 3a76ff935bf6's down_revision at it as planned in the migration's own docstring, so `alembic heads` resolves to a single head again. Reconciles openshield-org#308's atomic/idempotent save_scan() and score_counts() usage with this branch's compliance-mapping-snapshot and evidence- schema work in api/models/finding.py, api-reference.md, and architecture.md, and merges the CI step lists in ci.yml so both suites run. Two issues surfaced while reconciling the two branches' code, fixed here rather than deferred: - get_compliance_score()'s severity/category grouping query was about to run against openshield-org#308's finding.py through the RealDictCursor this branch used elsewhere in the same method. RealDictRow has no __iter__ override, so positional unpacking of its rows silently reads back key names instead of values. Restored openshield-org#308's own plain cursor for that one query, matching its tested convention, instead of carrying the bug or rewriting openshield-org#308's already-merged code. - .github/scripts/validate_mapping_pack.py flagged AZ-CMP-007 (added by the already-merged openshield-org#307) as missing the evidence-schema fields this branch's mapping-pack validation requires, across all four framework files. Filled them in following the existing sibling-rule conventions in each file. Updates the affected tests in test_clean_scan.py, test_compliance_scoring.py, and test_severity_contract.py for the new call shapes, and fixes frontend/src/utils/api.test.mjs's module- loading harness for api.js's new severity.js import from openshield-org#308 (stubbed, since the functions under test here don't call it and severity.js has its own dedicated suite). Verified: alembic heads (single head) and a full upgrade/downgrade/ upgrade/heads cycle against a local Postgres instance; full pytest suite (818 passed, 3 skipped); ruff check and format --check; mapping- pack validator; and the full frontend test/lint/build suite. Signed-off-by: Parth J Rohit <parthrohit60@gmail.com> Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
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 |
…Z-SECOPS-010 The dev merge into this branch pulled in openshield-org#277 (enterprise network and perimeter controls) and openshield-org#320, both merged since this branch was last updated. openshield-org#277's ten new AZ-NET-018..027 rules and the previously-merged AZ-SECOPS-010 carry compliance mappings across all four framework files with no mapping_type/evidence_type/primary_source/rationale/ review_status - this PR's own validate_mapping_pack.py correctly rejects that, since neither rule existed yet when this PR's evidence- schema requirement was written. Same root cause as the AZ-CMP-007 gap from openshield-org#307 fixed earlier in this branch's history. Filled in following the exact conventions of each entry's nearest sibling in the same file: - cis_azure_benchmark.json: mapping_type "not_applicable" for the N/A-* control_ids (all ten AZ-NET-0xx entries), "direct" for AZ-SECOPS-010's real numbered control (2.1.20) - matching this PR's existing framework-level-default convention for non-N/A CIS entries. - nist_csf.json / iso27001.json / soc2.json: mapping_type "supporting", evidence_type "automated_configuration_scan", with primary_source/ rationale text following the exact template of each file's sibling entries (e.g. AZ-NET-016, AZ-SECOPS-009). - owner: null, review_status: "pending_review", review_date: null, matching every other unreviewed entry in these files. Also cleans up a merge artifact in CHANGELOG.md: openshield-org#277 had added a second, malformed top-level "## Unreleased" section (missing brackets, no ### subsections) above the file's existing well-formed "## [Unreleased]" - folded its one entry into the correct section. Verified: mapping-pack validator clean (0 errors, was 55). Full backend suite (892 passed, 5 skipped - pre-existing/environment-only). alembic heads still resolves to exactly one head (3a76ff935bf6). ruff check and format --check clean. Diff confirmed scoped to exactly the 11 affected rule_ids across the 4 framework files plus the CHANGELOG cleanup - no other entries touched. Signed-off-by: Parth J Rohit <parthrohit60@gmail.com> Signed-off-by: parthrohit22 <parthrohit60@gmail.com>
|
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.
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.