fix(evidence): per-rule OSCAL export + unmapped-rule conformance fix#83
Merged
Conversation
The OpenWatch team asked how the OSCAL option should work in a per-rule UI expansion, given OSCAL is a whole-scan document but evidence is per-rule. Investigating surfaced a real conformance bug at exactly that granularity. Bug: a result with no framework-mapped control emitted an empty OSCAL `include-controls`, which the 1.0.6 schema rejects — `reviewed-controls` is required and a control-selection must select `include-all` or a non-empty `include-controls` (minItems 1). A whole-host scan never hit this (some corpus rule is always mapped), but a single-rule document for an unmapped rule — the per-rule expansion case — produced invalid OSCAL. Fix: a shared `controlSelection` helper falls back to OSCAL `include-all` when there are no control refs, applied to both `ExportOSCALScan` (scan) and `ExportOSCAL` (remediation) paths. Per-rule entry point: `pkg/kensa.ExportOSCALOutcome` / `WriteOSCALOutcome` render a single `api.RuleOutcome` as its own valid one-finding OSCAL AR, preserving the parent scan's host context. The exporter is granularity-agnostic — a one-outcome document is a valid AR — so this is a thin wrapper that builds the single-outcome ScanResult correctly (without dropping Capabilities/Platform) rather than a second serializer. Regression tests: unmapped single-rule validates against 1.0.6 (scan + public-helper paths). Specs evidence-oscal-scan C-08/AC-09 and oscal-public-export AC-06/AC-07. No engine/capture/rollback code touched. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
remyluslosius
added a commit
that referenced
this pull request
Jun 14, 2026
…ix (#84) Stamp v0.4.2. VERSION -> 0.4.2; CHANGELOG's Unreleased entry (per-rule OSCAL export + the unmapped-rule include-all fix, PR #83) moves under the v0.4.2 heading and Unreleased resets. PATCH bump: the addition lives on pkg/kensa (ExportOSCALOutcome / WriteOSCALOutcome) and the conformance fix is in internal/evidence; the frozen api/ surface is untouched. Verification: go test ./... green; goreleaser check + snapshot build; Makefile build stamps `kensa 0.4.2`. The signed release pipeline triggers on the v0.4.2 tag. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
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.
Context
The OpenWatch team asked: OSCAL is a whole-scan document (one AR per host), but
Evidenceis per-rule — how should the OSCAL option work in the per-rule UI expansion?Investigating it surfaced a real conformance bug at exactly that granularity, plus the answer.
The design answer
CheckEvidenceis the per-rule primitive — that's what a per-rule expansion should render.ScanResultyields a valid one-finding AR. So "per-rule OSCAL" is the same exporter fed a single-rule slice — no separate serializer.This PR makes that first-class and fixes what was blocking it.
The bug (found via the question)
reviewed-controlsis required on every OSCAL result, and a control-selection must selectinclude-allor a non-emptyinclude-controls(minItems 1). The exporter emitted an emptyinclude-controlswhen a result had no framework-mapped control. A whole-host scan never hits this (some corpus rule is always mapped) — but a single-rule document for an unmapped rule produces invalid OSCAL, which is precisely the per-rule-expansion case.Fix: a shared
controlSelectionhelper falls back to OSCALinclude-allwhen there are no control refs — applied to bothExportOSCALScan(scan) andExportOSCAL(remediation) paths.Per-rule entry point
pkg/kensa.ExportOSCALOutcome(scan, outcome, hostname)/WriteOSCALOutcomerender a singleapi.RuleOutcomeas its own valid one-finding AR, preserving the parent scan's host context (HostID/Capabilities/Platform) so the caller doesn't hand-roll the slice and drop it. Thin wrapper over the granularity-agnostic exporter, not a second implementation.Tests / spec
evidence-oscal-scanC-08/AC-09 (include-all fallback);oscal-public-exportAC-06/AC-07 (per-rule helper)Gate
go test ./...green · golangci-lint 0 · specter 118/118 (evidence-oscal-scan9/9,oscal-public-export7/7). No engine/capture/rollback touched.Recommendation to OpenWatch
Show
CheckEvidencein the per-rule expansion (it's the right primitive); offer OSCAL as an export action that callsExportOSCALOutcomefor one rule orExportOSCALScanfor the whole host. Both yield valid OSCAL 1.0.6 now, including unmapped rules.🤖 Generated with Claude Code