Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fusa-hara.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"project": "FuSaOps",
"standard": "ISO 26262",
"standard": "iso26262",
"createdAt": "2026-06-09T00:00:00Z",
"operationalSituations": [
{
Expand Down
15 changes: 15 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
"priority": "MUST",
"rationale": "Enables tooling and CI to verify which spec version FuSaOps implements without parsing docs."
},
{
"id": "REQ-FO-CORE008",
"title": "CanonicalStandardID helper",
"description": "FuSaOps shall export a CanonicalStandardID function mapping a human-readable standard display string to its canonical lowercase id per x-FuSa spec §2.4.1, for use wherever an artifact emits a 'standard' field.",
"priority": "MUST",
"rationale": "x-FuSa spec §2.9: a 'standard' field's value must be identical (and canonical) across every output format; a single shared helper prevents each artifact package from re-deriving this inconsistently."
},
{
"id": "REQ-FO-CLI053",
"title": "fusaops version --format json",
Expand Down Expand Up @@ -3055,6 +3062,14 @@
"priority": "MUST",
"standard": "ISO 26262-3 Clause 6"
},
{
"id": "REQ-FO-HARA007",
"title": "hara.ValidateFssrRefs cross-checks safety goal fssrRefs against the requirement registry",
"description": "hara.ValidateFssrRefs shall flag any safetyGoals[].fssrRefs entry that does not resolve to an id in .fusa-reqs.json, catching a dangling reference that a non-empty check alone cannot see.",
"priority": "MUST",
"rationale": "x-FuSa spec §1.2.5: a safety goal's fssrRefs must be real, resolvable requirements, not just present.",
"standard": "ISO 26262-8 Clause 6"
},
{
"id": "REQ-FO-CLI084",
"title": "Quality-gate wiring: runQualityGate + attestationValid + per-command QualFields extraction and attestation carry-forward across fmea/hara/tara/safety-case/sas",
Expand Down
54 changes: 54 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,60 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.

## [Unreleased]

## [1.144.0] — 2026-07-28

### fix: FuSaOps' own hara/tara/safetycase/sci/fmea artifacts against spec v1.15.0 — 10 bugs from a fresh self-audit

The v1.143.0 changelog claimed FuSaOps' artifact packages already met the
spec's carry-forward mechanism — true, but a follow-up self-audit (deliberately
skeptical, same method used against the sibling tools) found 10 other real
defects that PR1/PR2 had missed, proving self-review has the same blind spots
found in every other tool this session:

- **hara**: `Validate` never cross-checked a hazard's stated `risk.asil`
against `DetermineASIL(S,E,C)` — any ASIL value passed regardless of the
actual S/E/C rating. New `ValidateFssrRefs` also cross-checks every safety
goal's `fssrRefs` against `.fusa-reqs.json`'s real ids, catching a dangling
reference that a non-empty check alone can't see.
- **hara CLI**: `hara show --format json` on a project with no
`.fusa-hara.json` exited 0 and rendered an empty HARA as if the analysis
were complete — now exits 1 (§1.2.5). The text/markdown default format is
intentionally left tolerant, per the same spec text's scoping.
- **tara**: `ThreatScenario` JSON emitted `attackPath`/`riskLevel` instead of
the spec's `attackVector`/`risk` — the SFOP enum vocabulary and the
feasibility×impact combination table were already correct, only these two
field names were wrong.
- **safetycase / sci**: `evidence[].path` / `items[].path` leaked the full
filesystem path (`root + "/" + file`) instead of a project-relative one
(§4 MUST) — the file is still stat'd via its full path, only the recorded
value changed.
- **standard field canonicalization**: new `fusaops.CanonicalStandardID`
helper maps a display string (`"ISO 26262"`) to its §2.4.1 canonical id
(`"iso26262"`) — applied in hara (real MUST per §1.2.5), tara, and
safetycase. fmea's dual-standard citation (`IEC 61508 / ISO 26262 Part
8-7`) has no single canonical id to map to and is left as a display
string; fmea/tara/safety-case don't actually carry a spec-mandated
`standard` field at all per §9.2's illustrative schemas, so this is a
consistency improvement, not a nonconformance fix, for the two that do
cleanly map.
- **fmea**: `FailureMode` had no `file` field at all, despite it being a
§9.2 MUST — added, populated with each of the 8 entries' real
representative source file.
- **common header**: `fmea`/`tara`/`safety-case`/`sas`/`sci` were all
missing the §3.1 common header (`schemaVersion`/`kind`/`language`) — added
to all five (HARA is explicitly an input file like `.fusa-reqs.json`, not
subject to the header, per §1.2.5's own text). Spec's §3.1 `kind` enum
list extended to include `fmea-report`/`tara-report`/`safety-case`/`sas`/
`sci`, which were already used in the spec's own §9.2/§9.3 schema blocks
but not in the enum's explicit list.
- **coveragePct clamp**: `coveragePct` in both fmea and tara now clamps to
100 defensively (§9.2 MUST), with a regression test seeding
analyzed > total directly at the unexported function, since neither
package's real data can currently trigger the bug by construction.

New `.fusa-reqs.json` entries: `REQ-FO-HARA007` (fssrRefs registry
cross-check), `REQ-FO-CORE008` (`CanonicalStandardID`).

## [1.143.0] — 2026-07-28

### docs: x-FuSa spec v1.15.0 — attestation carry-forward MUST + implementer guidance, from a 4-tool deep audit
Expand Down
33 changes: 33 additions & 0 deletions cmd/fusaops/cmd_extra_coverage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,9 @@
//fusa:test REQ-FO-CLI073
func TestHaraShowOutputFlag(t *testing.T) {
dir := t.TempDir()
if code, _, errb := runArgs(t, "hara", "--dir", dir, "init"); code != 0 {
t.Fatalf("hara init: code=%d err=%q", code, errb)
}
outFile := filepath.Join(dir, "hara-report.json")
code, _, errb := runArgs(t, "hara", "--dir", dir, "show", "--format", "json", "--output", outFile)
if code != 0 {
Expand All @@ -140,6 +143,36 @@
}
}

// TestHaraShowJSONAbsentFileExitsNonZero verifies hara show --format json
// exits non-zero on a project with no .fusa-hara.json, per x-FuSa spec
// §1.2.5: a hara command MUST NOT report zero hazards as if analysis were
// complete.
//
//fusa:test REQ-FO-CLI073
func TestHaraShowJSONAbsentFileExitsNonZero(t *testing.T) {
dir := t.TempDir()
code, _, errb := runArgs(t, "hara", "--dir", dir, "show", "--format", "json")
if code == 0 {
t.Errorf("hara show --format json on absent file: want non-zero, got 0")
}
if !strings.Contains(errb, ".fusa-hara.json not found") {
t.Errorf("hara show --format json on absent file: stderr=%q, want mention of missing file", errb)
}
}

// TestHaraShowTextAbsentFileStillExitsZero verifies hara show's default
// text format tolerates an absent .fusa-hara.json (the spec's exit-nonzero
// requirement is scoped to --format json, not the human-readable default).
//
//fusa:test REQ-FO-CLI073
func TestHaraShowTextAbsentFileStillExitsZero(t *testing.T) {
dir := t.TempDir()
code, _, errb := runArgs(t, "hara", "--dir", dir, "show")
if code != 0 {
t.Errorf("hara show text on absent file: want 0, got %d (err=%q)", code, errb)
}
}

// TestHaraShowMarkdown verifies hara show markdown format.
//
//fusa:test REQ-FO-CLI073
Expand Down
18 changes: 17 additions & 1 deletion cmd/fusaops/cmd_hara.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@
"path/filepath"
"time"

fusaops "github.com/SoundMatt/FuSaOps"
"github.com/SoundMatt/FuSaOps/hara"
"github.com/SoundMatt/FuSaOps/qualitybar"
"github.com/SoundMatt/FuSaOps/req"
)

// runHara dispatches the hara subcommand (show|init|asil).
Expand Down Expand Up @@ -77,6 +79,13 @@
*requireAttestation = true
}

if *format == "json" {
if _, statErr := os.Stat(filepath.Join(projectRoot, hara.HARAFile)); os.IsNotExist(statErr) {
fmt.Fprintf(stderr, "fusaops hara show: %s not found — run 'fusaops hara init' first (x-FuSa spec §1.2.5: a hara command MUST NOT report zero hazards as if analysis were complete)\n", hara.HARAFile)
return 1
}
}

h, err := hara.Load(projectRoot)
if err != nil {
fmt.Fprintf(stderr, "fusaops hara show: %v\n", err)
Expand All @@ -101,6 +110,13 @@

exit := 0
findings := hara.Validate(h)
if entries, regErr := req.LoadRegistry(projectRoot); regErr == nil {
reqIDs := make(map[string]bool, len(entries))
for _, e := range entries {
reqIDs[e.ID] = true
}
findings = append(findings, hara.ValidateFssrRefs(h, reqIDs)...)
}
if len(findings) > 0 && *output != "" {
fmt.Fprintf(stderr, "fusaops hara: %d gap(s) found — run 'fusaops hara show' for details\n", len(findings))
}
Expand Down Expand Up @@ -152,7 +168,7 @@
// completeness that a real "empty, honestly incomplete" state does not.
h := &hara.HARA{
Project: name,
Standard: *standard,
Standard: fusaops.CanonicalStandardID(*standard),
CreatedAt: time.Now().UTC(),
Situations: []hara.OperationalSituation{},
Hazards: []hara.Hazard{},
Expand Down
7 changes: 5 additions & 2 deletions cmd/fusaops/cmd_v147_v150_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@
if code > 1 {
t.Fatalf("safety-case ISO 21434: unexpected exit code %d", code)
}
if !strings.Contains(out, "ISO 21434") {
if !strings.Contains(out, "iso21434") {
t.Errorf("safety-case output missing standard name: %q", out[:min(len(out), 200)])
}
}
Expand Down Expand Up @@ -532,7 +532,7 @@
if code > 1 {
t.Fatalf("tara json: unexpected exit code %d", code)
}
if !strings.Contains(out, `"threats"`) || !strings.Contains(out, `"riskLevel"`) {
if !strings.Contains(out, `"threats"`) || !strings.Contains(out, `"risk"`) {
t.Errorf("tara json missing expected fields: %q", out[:min(len(out), 200)])
}
}
Expand Down Expand Up @@ -810,6 +810,9 @@
//fusa:test REQ-FO-CLI073
func TestHaraShowJSON(t *testing.T) {
dir := t.TempDir()
if code, _, errb := runArgs(t, "hara", "--dir", dir, "init"); code != 0 {
t.Fatalf("hara init: code=%d err=%q", code, errb)
}
code, out, errb := runArgs(t, "hara", "--dir", dir, "show", "--format", "json")
if code != 0 {
t.Fatalf("hara show json: code=%d err=%q", code, errb)
Expand Down
5 changes: 3 additions & 2 deletions docs/x-fusa-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -752,8 +752,9 @@ header, so FuSaOps (or anything) can read attribution and route decoding off
**`kind` (MUST — closed, extensible enum).** Identifies the document type so a
consumer routes generically: `check-report` (also `report`) · `trace-matrix` ·
`qualification` · `sbom` · `provenance` · `artifact-manifest` · `audit-manifest`
· `gap-report` · `capabilities`. A consumer MUST treat an unknown `kind` as
opaque (read the common header, skip the payload) — never reject it.
· `gap-report` · `capabilities` · `fmea-report` · `tara-report` · `safety-case`
· `sas` · `sci`. A consumer MUST treat an unknown `kind` as opaque (read the
common header, skip the payload) — never reject it.

The common header applies to **every** document, including the file-format
artefacts (`sbom.json`, `provenance.json`, `artifact-manifest.json`, audit-pack
Expand Down
17 changes: 14 additions & 3 deletions fmea.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
{
"generatedAt": "2026-07-28T19:19:37.850745Z",
"schemaVersion": "1.15.0",
"kind": "fmea-report",
"language": "go",
"generatedAt": "2026-07-28T21:17:46.78645Z",
"projectRoot": ".",
"tool": "fusaops",
"toolVersion": "1.141.0",
"toolVersion": "1.144.0",
"standard": "IEC 61508:2010 / ISO 26262:2018 Part 8-7",
"ratingScale": "custom-1-10",
"entries": [
Expand All @@ -11,6 +14,7 @@
"component": "Adapter Registry",
"function": "Execute language-specific safety analysis",
"item": "Adapter Registry.Execute language-specific safety analysis",
"file": "adapter/adapter.go",
"failureMode": "Adapter binary not found on PATH",
"effect": "Language component analysis silently skipped; safety defects in that language undetected.",
"cause": "Missing tool installation or PATH misconfiguration in CI environment.",
Expand All @@ -34,6 +38,7 @@
"component": "cmdAdapter",
"function": "Run x-FuSa tool and collect findings",
"item": "cmdAdapter.Run x-FuSa tool and collect findings",
"file": "adapter/adapter.go",
"failureMode": "Adapter process exits with non-zero code",
"effect": "Findings from that language component dropped from the aggregate report.",
"cause": "Tool bug, memory exhaustion, or malformed source input.",
Expand All @@ -57,6 +62,7 @@
"component": "Report decoder",
"function": "Parse adapter JSON output per x-FuSa spec",
"item": "Report decoder.Parse adapter JSON output per x-FuSa spec",
"file": "report/report.go",
"failureMode": "Adapter output schema does not conform to x-FuSa spec",
"effect": "Findings silently dropped or aggregate report mis-renders.",
"cause": "Tool version skew between adapter binary and FuSaOps decoder expectations.",
Expand All @@ -80,6 +86,7 @@
"component": "trace package",
"function": "Cross-language requirement traceability",
"item": "trace package.Cross-language requirement traceability",
"file": "trace/trace.go",
"failureMode": "Source annotation removed or misspelled",
"effect": "Requirement appears untraceable; false coverage gap reported.",
"cause": "Developer removes or typos a //fusa:req annotation during refactoring.",
Expand All @@ -103,6 +110,7 @@
"component": "sbom package",
"function": "Merge per-language SBOMs into cross-language SBOM",
"item": "sbom package.Merge per-language SBOMs into cross-language SBOM",
"file": "sbom/sbom.go",
"failureMode": "Language SBOM file missing from merge input",
"effect": "Component not included in final SBOM; unknown dependency risk reaches certification.",
"cause": "Adapter SBOM generation failed silently or output file not written.",
Expand All @@ -126,6 +134,7 @@
"component": "auditpack / sign packages",
"function": "Verify integrity of evidence artefacts",
"item": "auditpack / sign packages.Verify integrity of evidence artefacts",
"file": "sign/sign.go",
"failureMode": "Artefact file modified after signing",
"effect": "Tampered evidence passes a manual inspection that does not re-verify the HMAC.",
"cause": "Post-sign file modification (intentional or accidental) or disk corruption.",
Expand All @@ -149,6 +158,7 @@
"component": "cmdAdapter / orchestrator",
"function": "Execute adapter within configured timeout",
"item": "cmdAdapter / orchestrator.Execute adapter within configured timeout",
"file": "orchestrator/orchestrator.go",
"failureMode": "Adapter process hangs indefinitely",
"effect": "Analysis pipeline blocked; no findings produced; release delayed or silently skipped.",
"cause": "Deadlock in adapter tool, infinite loop, or unavailable external resource.",
Expand All @@ -172,6 +182,7 @@
"component": "suppression package",
"function": "Filter findings against the suppression list",
"item": "suppression package.Filter findings against the suppression list",
"file": "suppression/suppression.go",
"failureMode": "Overly broad suppression rule hides new violations",
"effect": "New safety violations match an existing suppression entry and are never reviewed.",
"cause": "Wildcard or unlimited-duration suppression entry in .fusaops-suppress.json.",
Expand Down Expand Up @@ -199,5 +210,5 @@
"coveragePct": 19.5,
"componentInventoryMethod": "count of all packages in CLAUDE.md's core orchestration-pipeline table (11) and layered compliance/evidence/workflow table (30), 41 total; componentsAnalyzed counts distinct FMEA entries, not distinct packages named (some entries span multiple packages, e.g. \"auditpack / sign packages\")"
},
"hash": "sha256:1bb7b76663c10f4c2e5a1681a16a143fe255f8633953ed75f466547f628b116b"
"hash": "sha256:52449b2f766eea3ba01b28a9a5cf311855b766ef773545eedf033595ed4d033c"
}
Loading
Loading