fix(delta): rank SRG blocks by requirement text, not CCI overlap#7971
Open
wdower wants to merge 7 commits into
Open
fix(delta): rank SRG blocks by requirement text, not CCI overlap#7971wdower wants to merge 7 commits into
wdower wants to merge 7 commits into
Conversation
Signed-off-by: Will Dower <will@dower.dev>
There was a problem hiding this comment.
Pull request overview
Refactors the cross-vendor STIG delta matcher so SRG-ID + CCI tags are treated as blocking/secondary hints rather than as a stable requirement identity. Ranking is now driven by title + check-text semantic similarity, block resolution uses globally-optimal greedy bipartite assignment, and potentialMismatch fires on weak semantic evidence (catching the Tier-1 cross-vendor blind-trust case).
Changes:
- Replace CCI-dominant Tier-2 composite (
0.7·CCI + 0.3·title) with semantic-dominant composite (0.7·semantic(title+check) + 0.3·CCI) and resolve each SRG block via bipartite assignment. - Drive
potentialMismatchoff semantic distance (Tier 1/2, threshold 0.3) and Fuse confidence (Tier 3); renamesrg-cci-tiebreak→srg-semantic-tiebreak; expose per-link triage fields (titleSimilarity,checkSimilarity,cciJaccardScore,semanticScore,blockNewCount,blockOldCount). - Emit per-block cardinality warning and per-link triage log line in
generate delta; update tests accordingly.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.
| File | Description |
|---|---|
| src/utils/delta_matching.ts | Core refactor: semantic scoring, bipartite block resolution, renamed match method, new triage fields, threshold changes. |
| src/commands/generate/delta.ts | New cardinality warning, triage-aware log lines, updated pipeline header comments. |
| test/utils/tests/delta_matching.test.ts | New Tier-2 block/order-stability/triage/check-tiebreak tests; updated mismatch-flag and weight-constant tests to semantic model. |
| test/utils/tests/cross_vendor_integration.test.ts | Rename srg-cci-tiebreak → srg-semantic-tiebreak in expectations. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
…eSrgBlock Signed-off-by: Will Dower <will@dower.dev>
The requirement-first matcher read STIG check text from `tags.check`, but both processInSpecProfile and processXCCDF store it in `descs.check` (`tags.check` is moved/emptied during parsing). The title+check semantic discriminator was therefore title-only in every real delta. Read check text from `descs.check` (with `tags.check` as a fallback for hand-built inputs). On the RHEL9 -> SLES15 validation this activates check similarity on 206/214 links and corrects 29 mappings, including body transpositions in dense SRG blocks that title-only matching still got wrong. Also surface weak-evidence `related` grafts that were silently shipping wrong bodies: - potentialMismatch now fires for `related` links, not just `primary` (a related link grafts the old body forward just like a primary, so a weak match ships a wrong body either way). Only no-match stays exempt. - Tier-3 fuse-fallback now computes the title+check semanticScore against the matched old control (was null) and gates the flag on weak semantic OR low Fuse title-confidence. - tickMatchCounter counts any flagged link (primary or related) as a possible mismatch so the summary reflects reality; the match + mismatch + related = total invariant still holds. Residual: a `related` graft whose title+check stays highly similar to the wrong old control (e.g. "owned by root" vs "group-owned by root") is not caught by any aggregate-similarity threshold; same class as the documented cross-SRG re-categorization miss. Signed-off-by: Will <will@dower.dev>
…view Adversarial review of the matcher surfaced documentation and test gaps (no correctness defect in the assignment itself). Address them: - Docstrings called the per-block assignment "globally-optimal"; it is greedy best-first, a 1/2-approximation, not a maximum-weight matching. Reword across the module/resolveSrgBlock/claimPrimaryPairings/applyRequirementFirstPipeline docstrings and state the deliberate bias (commit the single most-trustworthy link first, which suits copying a body forward). - Make order-independence actually hold rather than merely documented: add an id-based tiebreak to the pair sort so exact composite ties no longer resolve by new-profile input order. - Fix the stale `tags.check` field-doc (check text is read from `descs.check`) and soften the potentialMismatch docstring: 0.3 is an obvious-junk floor, not a correctness separator. The real matched-link semantic distribution is smooth/unimodal with no separating valley, so an unflagged mid-band link is "not obviously junk", not "verified correct". - Stats: flagged related grafts now count as posMisMatch, so relabel dupMatch as "Trusted Related Matches" and surface the flagged-related count in both the live log and the markdown report so the triage signal isn't hidden. Tests: - Positive Tier-3 flag test for the load-bearing `fuseConfidence < 0.9` gate (previously every fuse-fallback test asserted potentialMismatch=false). - Order-independence under exact composite ties. - Reciprocal transposition resolved by `descs.check` alone, in both input orders. - Add `descs.check` to the integration mini-fixtures' CRYPTO block and assert checkSimilarity > 0, so the integration test exercises the real check-text path instead of silently running title-only (the configuration that hid the original bug). Signed-off-by: Will <will@dower.dev>
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.



fix(delta): rank SRG blocks by requirement text, not CCI overlap
The previous matcher treated SRG-ID + CCI tags as a stable identifier of a specific technical requirement. They are not — they are categorization tags DISA reuses across unrelated requirements between vendor STIGs (RHEL9 vs AL2023). In cross-vendor deltas this produced confident-but-wrong pairings, concentrated in dense control families (audit, PAM), and the existing potentialMismatch flag — gated on low CCI Jaccard — missed ~88% of the real errors because the dominant failure mode produced high CCI Jaccard on wrong matches.
Changes to the matcher (
src/utils/delta_matching.ts)SEMANTIC_WEIGHT * semanticScore(title+check Jaccard) + CCI_WEIGHT * cciJaccard(0.7 / 0.3, inverted from the prior weights).delta.jsonlinks[]now surfacetitleSimilarity,checkSimilarity,cciJaccardScore,semanticScore,blockNewCount,blockOldCountfor reviewer triage.Changes to the command (
src/commands/generate/delta.ts)Block cardinality mismatch for SRG X: N new vs M oldwarning per SRG block where the matcher had to drop or pile up controls.How the mapper processes controls
Before — per-control, in input order; SRG candidate count picks the tier; ranking signal is CCI-dominant:
flowchart TD A[for each new control<br/>in input order] --> B{# of old<br/>candidates<br/>sharing its SRG} B -->|1| C[Tier 1<br/>accept lone candidate<br/>confidence = 1.0<br/>**never flagged**] B -->|N ≥ 2| D[Tier 2<br/>rank candidates by<br/>0.7 · CCI Jaccard +<br/>0.3 · title Jaccard] D --> E[claim best UNCLAIMED old;<br/>if all claimed → 'related'] E --> F{CCI Jaccard<br/>< 0.5?} F -->|yes| G[flag potentialMismatch] F -->|no| H[no flag] B -->|0| I[Tier 3<br/>Fuse fuzzy match<br/>on normalized title]After — controls grouped by SRG, each block resolved as a whole via bipartite assignment; ranking signal is requirement text (title + check); flag fires on weak semantic evidence regardless of tier:
flowchart TD A[all new controls] --> B[group new controls by SRG] B --> C[Phase 1<br/>for each SRG block:] C --> D[score every new × old pair:<br/>0.7 · semanticScore +<br/>0.3 · CCI Jaccard<br/>where semantic = title + check Jaccard] D --> E[sort pairs descending,<br/>claim globally-best free pairs as primary;<br/>leftover new controls → 'related'<br/>to their best-scoring old] E --> F{block has<br/>1 old?} F -->|yes| G[matchMethod:<br/>srg-deterministic] F -->|no| H[matchMethod:<br/>srg-semantic-tiebreak] G --> I{semantic score<br/>< 0.3?} H --> I I -->|yes| J[flag potentialMismatch] I -->|no| K[no flag] C -.->|block has new ≠ old count| W[warn: block cardinality mismatch] B --> L[Phase 2<br/>for new controls without a<br/>Phase-1 link:] L --> M[Tier 3 Fuse fuzzy match<br/>on normalized title<br/>can reach across SRG boundaries]Key shifts: SRG goes from "identifier" to "blocking hint"; ranking signal moves from CCI Jaccard to title + check Jaccard; assignment is globally-optimal within a block rather than input-order-greedy; the flag is now driven by semantic distance, which is the actual evidence of a wrong body.
Schema note
delta.jsonlinks[].matchMethodvaluesrg-cci-tiebreakis renamed tosrg-semantic-tiebreak. Downstream consumers pattern-matching on the literal need to update.Validation
Validated against the original AL2023-from-RHEL9 derivation that surfaced the bug: of the 17 controls human review re-mapped, 8 are now correctly flagged for review (vs 2 before) and 5 are now mapped to the correct RHEL9 ancestor by the bipartite pass. One residual silent miss (SV-274116) is caused by cross-vendor SRG re-categorization that within-block scoring cannot reach across; the block-cardinality warning fires for its SRG, providing indirect signal.
Breakdown of the 17 originally-misrouted AL2023 controls
"Exhibit A" = dense Tier-2 SRG block where CCI Jaccard saturated to 1.0 across candidates and the input-order-greedy pass permuted bodies onto neighbors. "Exhibit B" = Tier-1 single-candidate SRG block by accident of DISA categorization (the lone old candidate's requirement was unrelated). "Cross-SRG twin" = the AL2023 control's true RHEL9 partner exists but DISA bucketed it under a different SRG, so SRG-blocking can't reach it.
audit=1freqflushrelated(block primary flagged)name_format)journal-upload.conf)/var/loggroup-owned by rootrelated; block-cardinality warning fires for the SRG/var/logowned by rootrelated; block-cardinality warning firesaudit=1(audit before audit daemon)kexec_load_disabledancestor in RHEL9nosuidon/boot/efiTest plan
npx vitest run— 514 / 514 passnpx tsc --noEmit— cleannpx eslint <changed files>— cleansaf generate deltaagainst the AL2023-v1r3 XCCDF + RHEL9 profile from the original bug report; flag/remap behavior matches the breakdown above