diff --git a/.fusa-hara.json b/.fusa-hara.json
index b2bed98..ab99aee 100644
--- a/.fusa-hara.json
+++ b/.fusa-hara.json
@@ -65,7 +65,7 @@
"hazards": ["H-001"],
"asil": "ASIL-C",
"safeState": "Surface a decode error as a failed/skipped component rather than an empty pass",
- "fssrRef": "REQ-FO-ADP006 REQ-FO-ADP005"
+ "fssrRefs": ["REQ-FO-ADP006", "REQ-FO-ADP005"]
},
{
"id": "SG-002",
@@ -73,7 +73,7 @@
"hazards": ["H-002"],
"asil": "ASIL-C",
"safeState": "Emit a skipped component with reason; never omit an applicable language silently",
- "fssrRef": "REQ-FO-ORC003 REQ-FO-RPT003"
+ "fssrRefs": ["REQ-FO-ORC003", "REQ-FO-RPT003"]
},
{
"id": "SG-003",
@@ -81,7 +81,7 @@
"hazards": ["H-003"],
"asil": "ASIL-A",
"safeState": "Exit code 1",
- "fssrRef": "REQ-FO-CLI008 REQ-FO-RPT006 REQ-FO-ERR005"
+ "fssrRefs": ["REQ-FO-CLI008", "REQ-FO-RPT006", "REQ-FO-ERR005"]
},
{
"id": "SG-004",
@@ -89,7 +89,7 @@
"hazards": ["H-004"],
"asil": "QM",
"safeState": "Findings carry language+tool from the producing adapter",
- "fssrRef": "REQ-FO-CORE004 REQ-FO-ADP006"
+ "fssrRefs": ["REQ-FO-CORE004", "REQ-FO-ADP006"]
},
{
"id": "SG-005",
@@ -97,7 +97,7 @@
"hazards": ["H-005"],
"asil": "ASIL-B",
"safeState": "Provide an explicit refresh that recomputes before serving",
- "fssrRef": "REQ-FO-SRV003 REQ-FO-SRV004"
+ "fssrRefs": ["REQ-FO-SRV003", "REQ-FO-SRV004"]
}
]
}
diff --git a/.fusa-reqs.json b/.fusa-reqs.json
index bb2aeb1..735656a 100644
--- a/.fusa-reqs.json
+++ b/.fusa-reqs.json
@@ -2950,6 +2950,49 @@
"description": "FuSaOps shall provide an ada-FuSa adapter (adafusa) that detects .ads/.adb files and delegates check/trace to adafusa via the generic cmdAdapter path. The adafusa binary produces spec v1.11 conformant JSON so no output normalisation is required.",
"priority": "MUST",
"rationale": "Extends FuSaOps multi-language orchestration to Ada/SPARK projects using ada-FuSa v0.1.0."
+ },
+ {
+ "id": "REQ-FO-CORE008",
+ "title": "RFC 8785 canonical JSON helper",
+ "description": "FuSaOps shall provide a Canonicalize function producing RFC 8785 (JSON Canonicalization Scheme) sorted-key, whitespace-free JSON, used by every self-integrity hash field per x-FuSa spec section 6/2.7.",
+ "priority": "MUST",
+ "rationale": "A hash computed over Go struct-field-order JSON is deterministic only within one Go binary; genuine RFC 8785 canonicalization is what the spec actually requires and is reusable across every artifact hash."
+ },
+ {
+ "id": "REQ-FO-HARA005",
+ "title": "SafetyGoal.FssrRefs is a MUST, >=1-entry array",
+ "priority": "MUST",
+ "standard": "ISO 26262-8 Clause 6"
+ },
+ {
+ "id": "REQ-FO-FMEA006",
+ "title": "FMEA schema conformance: entries/item/actionPriority/requirementIds/ratingScale/summary per x-FuSa spec 9.2",
+ "priority": "MUST",
+ "standard": "IEC 61508:2010 / ISO 26262:2018 Part 8-7"
+ },
+ {
+ "id": "REQ-FO-TARA006",
+ "title": "TARA schema conformance: threats/threat/attackFeasibility/SFOP impact/summary per x-FuSa spec 9.2",
+ "priority": "MUST",
+ "standard": "ISO 21434:2021 Clause 15"
+ },
+ {
+ "id": "REQ-FO-SC006",
+ "title": "SafetyCase GSN projection: nodes/edges/completeness per the GSN Community Standard v3 and x-FuSa spec 9.2",
+ "priority": "MUST",
+ "standard": "ISO 26262, DO-178C, IEC 61508, ISO 21434"
+ },
+ {
+ "id": "REQ-FO-SAS006",
+ "title": "SAS checklist/summary projection per x-FuSa spec 9.3",
+ "priority": "MUST",
+ "standard": "DO-178C \u00a711.20"
+ },
+ {
+ "id": "REQ-FO-SCI005",
+ "title": "SCI artifacts[] projection (file/hash/version) per x-FuSa spec 9.3",
+ "priority": "MUST",
+ "standard": "DO-178C \u00a711.16"
}
]
}
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e634902..57435bc 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,57 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.
## [Unreleased]
+## [1.140.0] — 2026-07-28
+
+### feat: bring FuSaOps' own hara/fmea/tara/safetycase/sas/sci into x-FuSa spec v1.14.0 conformance
+
+Dogfoods the schema work from spec v1.13.0/v1.14.0 against FuSaOps' own
+evidence artifacts (it develops itself as an ISO 26262 ASIL-C tool). Also
+fixes two real, pre-existing bugs found by reading the actual generated
+artifacts against the spec:
+
+- **`Hash` fields across `fmea`/`tara`/`safetycase`/`sas`/`sci`/`qualify`
+ now carry the `sha256:` prefix** required by spec §2.7 for a field named
+ `hash` — every one of them previously emitted bare hex.
+- **New shared `Canonicalize` (RFC 8785 JSON Canonicalization Scheme)**
+ in the root package, replacing each package's own fixed-struct-order
+ `computeHash`, so the hash is genuinely deterministic (not just
+ deterministic within one Go binary) — the same procedure now backs
+ `qualify`'s hash too.
+- **`hara/hara.go`**: `SafetyGoal.fssrRef` (a space-delimited string
+ cramming multiple ids into one field) promoted to `fssrRefs []string`
+ (MUST, ≥1 entry) per spec §1.2.5 — closes a real ISO 26262-8 Clause 6
+ traceability gap. `.fusa-hara.json`'s 5 safety goals migrated in place
+ (all 11 referenced ids already resolved correctly).
+- **`fmea/fmea.go`**: `entries[]`/`failureMode`/`item`/`actionPriority`/
+ `mitigations`/`requirementIds`/`ratingScale`/`summary` per spec §9.2.
+ Each of the 8 failure modes now links to the real FuSaOps requirement
+ its corrective action lives in.
+- **`tara/tara.go`**: `threats[]`/`threat`/`attackFeasibility`/
+ `mitigations` renamed; `impact` is now an SFOP object (safety/financial/
+ operational/privacy, ISO 21434 Clause 15.7) instead of one generic
+ severity — each of the 8 scenarios re-rated per-axis rather than
+ copy-pasted. `FeasibilityVeryLow` corrected `very_low` → `very-low`
+ (spec enum spelling).
+- **`safetycase/safetycase.go`**: adds a GSN (Goal Structuring Notation,
+ v3) `nodes`/`edges`/`completeness` projection alongside the existing
+ `claims`/evidence model — each claim becomes a goal+strategy node pair,
+ each present evidence file becomes a solution node; a claim with no
+ evidence counts toward `completeness.undeveloped` rather than
+ fabricating a solution.
+- **`sas/sas.go`**: adds `checklist`/`summary` (spec §9.3) alongside the
+ existing three-state `activities` model.
+- **`sci/sci.go`**: adds `artifacts[]` (`file`/`hash`/`version`, spec
+ §9.3) alongside the existing tool+artefact+component `items[]`.
+- Regenerated `fmea.json`, `tara.json`, `safety-case.json` against the
+ new code (all three were badly stale — pre-dating even the prior schema
+ by several tool generations). `fmea.csv`/`tara.md`/`safety-case.md`/
+ `.mermaid` are legacy static files with no current code path producing
+ them; left untouched.
+- 7 new requirements registered (`REQ-FO-CORE008`, `REQ-FO-HARA005`,
+ `REQ-FO-FMEA006`, `REQ-FO-TARA006`, `REQ-FO-SC006`, `REQ-FO-SAS006`,
+ `REQ-FO-SCI005`).
+
## [1.139.0] — 2026-07-28
### docs: x-FuSa spec v1.14.0 — detection heuristics, attestation, traceability MUSTs, coverage metrics
diff --git a/canonjson.go b/canonjson.go
new file mode 100644
index 0000000..73b1369
--- /dev/null
+++ b/canonjson.go
@@ -0,0 +1,107 @@
+package fusaops
+
+import (
+ "bytes"
+ "encoding/json"
+ "fmt"
+ "sort"
+ "strconv"
+)
+
+// Canonicalize re-serializes JSON data per a practical subset of RFC 8785
+// (the JSON Canonicalization Scheme): object keys sorted, no insignificant
+// whitespace, numbers in shortest round-trip form. It targets the JSON
+// shapes this codebase emits (structs marshaled via encoding/json) rather
+// than adversarial input — object keys are sorted by Go's default string
+// comparison, which matches RFC 8785's UTF-16 code-unit ordering except for
+// keys containing supplementary-plane codepoints, which do not occur in any
+// x-FuSa document.
+//
+//fusa:req REQ-FO-CORE008
+func Canonicalize(data []byte) ([]byte, error) {
+ dec := json.NewDecoder(bytes.NewReader(data))
+ dec.UseNumber()
+ var v interface{}
+ if err := dec.Decode(&v); err != nil {
+ return nil, fmt.Errorf("canonjson: decode: %w", err)
+ }
+ var buf bytes.Buffer
+ if err := writeCanonical(&buf, v); err != nil {
+ return nil, err
+ }
+ return buf.Bytes(), nil
+}
+
+func writeCanonical(buf *bytes.Buffer, v interface{}) error {
+ switch x := v.(type) {
+ case nil:
+ buf.WriteString("null")
+ case bool:
+ if x {
+ buf.WriteString("true")
+ } else {
+ buf.WriteString("false")
+ }
+ case json.Number:
+ buf.WriteString(canonicalNumber(x))
+ case string:
+ buf.Write(canonicalString(x))
+ case []interface{}:
+ buf.WriteByte('[')
+ for i, e := range x {
+ if i > 0 {
+ buf.WriteByte(',')
+ }
+ if err := writeCanonical(buf, e); err != nil {
+ return err
+ }
+ }
+ buf.WriteByte(']')
+ case map[string]interface{}:
+ keys := make([]string, 0, len(x))
+ for k := range x {
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+ buf.WriteByte('{')
+ for i, k := range keys {
+ if i > 0 {
+ buf.WriteByte(',')
+ }
+ buf.Write(canonicalString(k))
+ buf.WriteByte(':')
+ if err := writeCanonical(buf, x[k]); err != nil {
+ return err
+ }
+ }
+ buf.WriteByte('}')
+ default:
+ return fmt.Errorf("canonjson: unsupported type %T", v)
+ }
+ return nil
+}
+
+// canonicalNumber formats n in the shortest round-trip form: integers with
+// no decimal point or exponent, everything else via Go's shortest-round-trip
+// float formatting.
+func canonicalNumber(n json.Number) string {
+ if i, err := n.Int64(); err == nil {
+ return strconv.FormatInt(i, 10)
+ }
+ f, err := n.Float64()
+ if err != nil {
+ return n.String()
+ }
+ return strconv.FormatFloat(f, 'g', -1, 64)
+}
+
+// canonicalString encodes s as a JSON string with HTML-escaping disabled, so
+// the output matches standard JSON string escaping rather than Go's
+// web-safe default (which would otherwise escape '<', '>', '&').
+func canonicalString(s string) []byte {
+ var buf bytes.Buffer
+ enc := json.NewEncoder(&buf)
+ enc.SetEscapeHTML(false)
+ _ = enc.Encode(s)
+ return bytes.TrimSuffix(buf.Bytes(), []byte("\n"))
+}
diff --git a/canonjson_test.go b/canonjson_test.go
new file mode 100644
index 0000000..61d5959
--- /dev/null
+++ b/canonjson_test.go
@@ -0,0 +1,112 @@
+package fusaops
+
+import (
+ "bytes"
+ "testing"
+)
+
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeSortsKeys(t *testing.T) {
+ got, err := Canonicalize([]byte(`{"b":1,"a":2,"c":{"z":1,"y":2}}`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ want := `{"a":2,"b":1,"c":{"y":2,"z":1}}`
+ if string(got) != want {
+ t.Errorf("Canonicalize() = %s, want %s", got, want)
+ }
+}
+
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeStripsWhitespace(t *testing.T) {
+ got, err := Canonicalize([]byte(`{
+ "a" : [1, 2, 3],
+ "b" : "hello world"
+ }`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ want := `{"a":[1,2,3],"b":"hello world"}`
+ if string(got) != want {
+ t.Errorf("Canonicalize() = %s, want %s", got, want)
+ }
+}
+
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeIsIdempotentAcrossKeyOrder(t *testing.T) {
+ a, err := Canonicalize([]byte(`{"x":1,"y":2}`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ b, err := Canonicalize([]byte(`{"y":2,"x":1}`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ if string(a) != string(b) {
+ t.Errorf("Canonicalize order dependence: %s != %s", a, b)
+ }
+}
+
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeNoHTMLEscaping(t *testing.T) {
+ got, err := Canonicalize([]byte(`{"a":"&\"quote\""}`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ want := `{"a":"&\"quote\""}`
+ if string(got) != want {
+ t.Errorf("Canonicalize() = %s, want %s", got, want)
+ }
+}
+
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeInvalidJSON(t *testing.T) {
+ if _, err := Canonicalize([]byte(`not json`)); err == nil {
+ t.Error("expected error for invalid JSON")
+ }
+}
+
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeIntegersNoDecimal(t *testing.T) {
+ got, err := Canonicalize([]byte(`{"n":42}`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ if string(got) != `{"n":42}` {
+ t.Errorf("Canonicalize() = %s, want integer with no decimal point", got)
+ }
+}
+
+// TestCanonicalizeFloat covers canonicalNumber's non-integer branch
+// (strconv.FormatFloat), not exercised by the integer-only cases above.
+//
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeFloat(t *testing.T) {
+ got, err := Canonicalize([]byte(`{"pct":96.3}`))
+ if err != nil {
+ t.Fatalf("Canonicalize: %v", err)
+ }
+ if string(got) != `{"pct":96.3}` {
+ t.Errorf("Canonicalize() = %s, want {\"pct\":96.3}", got)
+ }
+}
+
+// TestCanonicalizeNestedArrayError covers writeCanonical's error-propagation
+// path when an element deep inside an array/object cannot be canonicalized.
+//
+//fusa:test REQ-FO-CORE008
+func TestCanonicalizeNestedArrayError(t *testing.T) {
+ var buf bytes.Buffer
+ // A Go channel has no JSON representation, so writeCanonical must return
+ // an error rather than panic or silently drop the value.
+ err := writeCanonical(&buf, []interface{}{make(chan int)})
+ if err == nil {
+ t.Error("expected error for unsupported nested type, got nil")
+ }
+
+ buf.Reset()
+ err = writeCanonical(&buf, map[string]interface{}{"k": make(chan int)})
+ if err == nil {
+ t.Error("expected error for unsupported map-value type, got nil")
+ }
+}
diff --git a/cmd/fusaops/cmd_hara.go b/cmd/fusaops/cmd_hara.go
index f16fbfb..f922d30 100644
--- a/cmd/fusaops/cmd_hara.go
+++ b/cmd/fusaops/cmd_hara.go
@@ -120,36 +120,16 @@ func runHaraInit(args []string, projectRoot string, stdout, stderr io.Writer) in
name = filepath.Base(projectRoot)
}
+ // Per x-FuSa spec §1.2.5/§1.6 rule 1: a scaffold MUST emit empty
+ // collections, never a dummy row — placeholder text asserts a false
+ // completeness that a real "empty, honestly incomplete" state does not.
h := &hara.HARA{
- Project: name,
- Standard: *standard,
- CreatedAt: time.Now().UTC(),
- Situations: []hara.OperationalSituation{
- {ID: "OS-001", Description: "Normal operation"},
- },
- Hazards: []hara.Hazard{
- {
- ID: "H-001",
- Description: "Example hazard — replace with project-specific hazard",
- Situations: []string{"OS-001"},
- Risk: hara.RiskRating{
- Severity: hara.SeverityS2,
- Exposure: hara.ExposureE3,
- Controllability: hara.ControllabilityC2,
- ASIL: hara.DetermineASIL(hara.SeverityS2, hara.ExposureE3, hara.ControllabilityC2),
- },
- SafetyGoals: []string{"SG-001"},
- },
- },
- SafetyGoals: []hara.SafetyGoal{
- {
- ID: "SG-001",
- Description: "Example safety goal — replace with project-specific goal",
- HazardIDs: []string{"H-001"},
- ASIL: hara.DetermineASIL(hara.SeverityS2, hara.ExposureE3, hara.ControllabilityC2),
- SafeState: "safe state description",
- },
- },
+ Project: name,
+ Standard: *standard,
+ CreatedAt: time.Now().UTC(),
+ Situations: []hara.OperationalSituation{},
+ Hazards: []hara.Hazard{},
+ SafetyGoals: []hara.SafetyGoal{},
}
if err := hara.Save(path, h); err != nil {
diff --git a/cmd/fusaops/cmd_v147_v150_test.go b/cmd/fusaops/cmd_v147_v150_test.go
index 7abbb58..b5942e0 100644
--- a/cmd/fusaops/cmd_v147_v150_test.go
+++ b/cmd/fusaops/cmd_v147_v150_test.go
@@ -532,7 +532,7 @@ func TestTARAJSON(t *testing.T) {
if code > 1 {
t.Fatalf("tara json: unexpected exit code %d", code)
}
- if !strings.Contains(out, `"scenarios"`) || !strings.Contains(out, `"riskLevel"`) {
+ if !strings.Contains(out, `"threats"`) || !strings.Contains(out, `"riskLevel"`) {
t.Errorf("tara json missing expected fields: %q", out[:min(len(out), 200)])
}
}
@@ -589,7 +589,7 @@ func TestFMEAJSON(t *testing.T) {
if code > 1 {
t.Fatalf("fmea json: unexpected exit code %d", code)
}
- if !strings.Contains(out, `"failureModes"`) || !strings.Contains(out, `"rpn"`) {
+ if !strings.Contains(out, `"entries"`) || !strings.Contains(out, `"rpn"`) {
t.Errorf("fmea json missing expected fields: %q", out[:min(len(out), 200)])
}
}
diff --git a/docs/commands/fmea.md b/docs/commands/fmea.md
index 1b72fb3..9b7ebae 100644
--- a/docs/commands/fmea.md
+++ b/docs/commands/fmea.md
@@ -33,3 +33,12 @@ fusaops fmea --format json --output fmea.json
Serves IEC 61508 / ISO 26262 Part 8 dFMEA objectives for the orchestration
tool itself.
+
+## x-FuSa spec conformance
+
+The JSON shape follows x-FuSa spec §9.2's `fmea` schema: failure modes are
+under `entries[]` (each carrying `failureMode`/`item`/`actionPriority`/
+`requirementIds`), and `ratingScale`/`summary` are top-level fields. This
+package's 8 entries are a fixed, curated review of key architectural risk
+areas in the FuSaOps orchestration pipeline, not an exhaustive per-function
+scan.
diff --git a/docs/commands/safety-case.md b/docs/commands/safety-case.md
index e6bddc7..2c66a5e 100644
--- a/docs/commands/safety-case.md
+++ b/docs/commands/safety-case.md
@@ -31,7 +31,13 @@ fusaops safety-case [--dir ] [--output ] [--format text|json]
- Looks for each claim's required evidence file (e.g. `.fusaops-evidence.json`
for `verify`, `sbom.json` for the SBOM claim) under the project root.
-- The generated report includes a SHA-256 integrity hash.
+- The generated report includes a SHA-256 integrity hash (`sha256:`-prefixed).
+- Alongside `claims`/`totalClaims`/`passedClaims`, the JSON also carries a GSN
+ (Goal Structuring Notation) projection of the same data — `nodes`/`edges`/
+ `completeness` — per x-FuSa spec §9.2: each claim becomes a `goal` node
+ supported by a `strategy` node, itself supported by one `solution` node per
+ present evidence file. A claim with no present evidence counts toward
+ `completeness.undeveloped` rather than fabricating a solution.
## Example
diff --git a/docs/commands/sas.md b/docs/commands/sas.md
index 8ae881a..015eb90 100644
--- a/docs/commands/sas.md
+++ b/docs/commands/sas.md
@@ -32,3 +32,11 @@ fusaops sas [--dir ] [--output ] [--format text|json]
```bash
fusaops sas --level DAL-B --format json --output sas.json
```
+
+## x-FuSa spec conformance
+
+Alongside `activities`/`totalActivities`/`completeActivities` (this package's
+three-state complete/incomplete/N-A model), the JSON also carries a
+`checklist`/`summary` projection per x-FuSa spec §9.3 — `checklist[].present`
+is `true` for both `complete` and `N/A` activities, matching the existing
+"counts as done" semantics.
diff --git a/docs/commands/tara.md b/docs/commands/tara.md
index b4a285f..49715c7 100644
--- a/docs/commands/tara.md
+++ b/docs/commands/tara.md
@@ -33,3 +33,11 @@ fusaops tara --format json --output tara.json
Serves ISO 21434 threat analysis objectives across the FuSaOps orchestration
pipeline and the tools it invokes.
+
+## x-FuSa spec conformance
+
+The JSON shape follows x-FuSa spec §9.2's `tara` schema: threat scenarios are
+under `threats[]` (each carrying `threat`/`attackFeasibility`/`mitigations`),
+and `impact` is an SFOP object (`safety`/`financial`/`operational`/`privacy`)
+per ISO 21434 Clause 15.7 rather than a single generic severity, since a
+threat can rate differently on each axis.
diff --git a/fmea.json b/fmea.json
index 9bb0e23..f2d3c6a 100644
--- a/fmea.json
+++ b/fmea.json
@@ -1,800 +1,199 @@
{
- "format": "go-FuSa dFMEA v1",
- "generated_at": "2026-06-10T14:32:27.912521Z",
- "module": "github.com/SoundMatt/FuSaOps",
+ "generatedAt": "2026-07-28T18:47:06.891193Z",
+ "projectRoot": ".",
+ "tool": "fusaops",
+ "toolVersion": "1.140.0",
+ "standard": "IEC 61508:2010 / ISO 26262:2018 Part 8-7",
+ "ratingScale": "custom-1-10",
"entries": [
{
- "component": "FuSaOps (fusaops)",
- "function": "Rank",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/fusaops.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-CORE002"
- ]
- },
- {
- "component": "FuSaOps (fusaops)",
- "function": "String",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/fusaops.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "FuSaOps (fusaops)",
- "function": "String",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/fusaops.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "All",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "Applicable",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP008"
- ]
- },
- {
- "component": "adapter",
- "function": "AuditPack",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/capabilities.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP017"
- ]
- },
- {
- "component": "adapter",
- "function": "Available",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP003"
- ]
- },
- {
- "component": "adapter",
- "function": "Check",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP005"
- ]
- },
- {
- "component": "adapter",
- "function": "Detect",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP004"
- ]
- },
- {
- "component": "adapter",
- "function": "Language",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "MustRegister",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "Name",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "NewRegistry",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "Qualify",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/capabilities.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP015"
- ]
- },
- {
- "component": "adapter",
- "function": "Register",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "medium",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "SBOM",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/capabilities.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP016"
- ]
- },
- {
- "component": "adapter",
- "function": "Tool",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "adapter",
- "function": "Trace",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/capabilities.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ADP014"
- ]
- },
- {
- "component": "auditpack",
- "function": "JSONSource",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/auditpack/auditpack.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-PCK005"
- ]
- },
- {
- "component": "auditpack",
- "function": "Pack",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/auditpack/auditpack.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-PCK004"
- ]
- },
- {
- "component": "config",
- "function": "Default",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/config/config.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-CFG003"
- ]
- },
- {
- "component": "config",
- "function": "Load",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/config/config.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-CFG004"
- ]
- },
- {
- "component": "config",
- "function": "Save",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/config/config.go",
- "failure_modes": [
- "Returns unexpected error",
- "Partial write / data corruption"
- ],
- "effects": [
- "Silent failure propagated to caller",
- "Incorrect system state"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-CFG005"
- ]
- },
- {
- "component": "config",
- "function": "Validate",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/config/config.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-CFG006"
- ]
- },
- {
- "component": "orchestrator",
- "function": "New",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/orchestrator/orchestrator.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "orchestrator",
- "function": "Run",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/orchestrator/orchestrator.go",
- "failure_modes": [
- "Returns unexpected error",
- "Uncontrolled execution"
- ],
- "effects": [
- "Silent failure propagated to caller",
- "Resource exhaustion"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ORC003"
- ]
- },
- {
- "component": "orchestrator",
- "function": "RunAuditPack",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/orchestrator/rollup.go",
- "failure_modes": [
- "Returns unexpected error",
- "Uncontrolled execution"
- ],
- "effects": [
- "Silent failure propagated to caller",
- "Resource exhaustion"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ORC006"
- ]
- },
- {
- "component": "orchestrator",
- "function": "RunSBOM",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/orchestrator/rollup.go",
- "failure_modes": [
- "Returns unexpected error",
- "Uncontrolled execution"
- ],
- "effects": [
- "Silent failure propagated to caller",
- "Resource exhaustion"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ORC005"
- ]
- },
- {
- "component": "orchestrator",
- "function": "RunTrace",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/orchestrator/rollup.go",
- "failure_modes": [
- "Returns unexpected error",
- "Uncontrolled execution"
- ],
- "effects": [
- "Silent failure propagated to caller",
- "Resource exhaustion"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-ORC004"
- ]
- },
- {
- "component": "report",
- "function": "HasErrors",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/report/report.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-RPT006"
- ]
- },
- {
- "component": "report",
- "function": "New",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/report/report.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-RPT005"
- ]
- },
- {
- "component": "report",
- "function": "Render",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/report/report.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-RPT007"
- ]
- },
- {
- "component": "report",
- "function": "RenderToFile",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/report/report.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-RPT008"
- ]
- },
- {
- "component": "report",
- "function": "Status",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/report/report.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-RPT002"
- ]
- },
- {
- "component": "report",
- "function": "Version",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/report/html.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "low",
- "detection_control": "unit tests"
- },
- {
- "component": "sbom",
- "function": "New",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/sbom/sbom.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SBM005"
- ]
- },
- {
- "component": "sbom",
- "function": "Render",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/sbom/render.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SBM006"
- ]
- },
- {
- "component": "sbom",
- "function": "RenderToFile",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/sbom/render.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SBM006"
- ]
- },
- {
- "component": "scan",
- "function": "Languages",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/scan/scan.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SCAN002"
- ]
- },
- {
- "component": "scan",
- "function": "Scan",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/scan/scan.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SCAN003"
- ]
- },
- {
- "component": "server",
- "function": "Handler",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/server/server.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SRV004"
- ]
- },
- {
- "component": "server",
- "function": "ListenAndServe",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/server/server.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SRV005"
- ]
- },
- {
- "component": "server",
- "function": "New",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/server/server.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SRV002"
- ]
- },
- {
- "component": "server",
- "function": "Serve",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/server/server.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-SRV005"
- ]
- },
- {
- "component": "trace",
- "function": "HasGaps",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/trace.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC011"
- ]
- },
- {
- "component": "trace",
- "function": "New",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/trace.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC010"
- ]
- },
- {
- "component": "trace",
- "function": "Render",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/render.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC012"
- ]
- },
- {
- "component": "trace",
- "function": "RenderToFile",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/render.go",
- "failure_modes": [
- "Returns unexpected error"
- ],
- "effects": [
- "Silent failure propagated to caller"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC012"
- ]
- },
- {
- "component": "trace",
- "function": "Status",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/trace.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC011"
- ]
- },
- {
- "component": "trace",
- "function": "TestedPct",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/trace.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC007"
- ]
- },
- {
- "component": "trace",
- "function": "TracedPct",
- "file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/trace/trace.go",
- "failure_modes": [
- "Incorrect output"
- ],
- "effects": [
- "Incorrect system behavior"
- ],
- "severity": "high",
- "detection_control": "requirement testing + unit tests",
- "requirement_ids": [
- "REQ-FO-TRC007"
+ "id": "FM-001",
+ "component": "Adapter Registry",
+ "function": "Execute language-specific safety analysis",
+ "item": "Adapter Registry.Execute language-specific safety analysis",
+ "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.",
+ "severity": 9,
+ "occurrence": 4,
+ "detection": 3,
+ "rpn": 108,
+ "actionPriority": "high",
+ "mitigations": [
+ "fusaops adapters — lists adapter availability before analysis",
+ "CI gate fails when a required adapter is absent",
+ "fusaops sci — inventories tool presence with version and hash"
+ ],
+ "action": "Add adapter availability check to the CI gate; fail the build on missing required adapters.",
+ "requirementIds": [
+ "REQ-FO-ADP007"
+ ]
+ },
+ {
+ "id": "FM-002",
+ "component": "cmdAdapter",
+ "function": "Run x-FuSa tool and collect findings",
+ "item": "cmdAdapter.Run x-FuSa tool and collect findings",
+ "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.",
+ "severity": 8,
+ "occurrence": 3,
+ "detection": 5,
+ "rpn": 120,
+ "actionPriority": "high",
+ "mitigations": [
+ "Adapter exit code propagated as orchestrator error",
+ "Orchestrator records skipped and failed adapters in the aggregate report",
+ "fusaops qualify — validates adapter health against each subcommand"
+ ],
+ "action": "Surface adapter error prominently in aggregate report; treat adapter failure as a gate failure.",
+ "requirementIds": [
+ "REQ-FO-ORC002"
+ ]
+ },
+ {
+ "id": "FM-003",
+ "component": "Report decoder",
+ "function": "Parse adapter JSON output per x-FuSa spec",
+ "item": "Report decoder.Parse adapter JSON output per x-FuSa spec",
+ "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.",
+ "severity": 9,
+ "occurrence": 3,
+ "detection": 4,
+ "rpn": 108,
+ "actionPriority": "high",
+ "mitigations": [
+ "fusaops conform — schema conformance checks against the x-FuSa spec",
+ "fusaops qualify — validates adapter output against each spec section",
+ "SpecVersion pinning in capabilities response"
+ ],
+ "action": "Run fusaops conform in CI after every tool version update; pin tool versions in Dockerfile.",
+ "requirementIds": [
+ "REQ-FO-CNF003"
+ ]
+ },
+ {
+ "id": "FM-004",
+ "component": "trace package",
+ "function": "Cross-language requirement traceability",
+ "item": "trace package.Cross-language requirement traceability",
+ "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.",
+ "severity": 7,
+ "occurrence": 5,
+ "detection": 5,
+ "rpn": 175,
+ "actionPriority": "medium",
+ "mitigations": [
+ "go-FuSa selfcheck catches annotation errors in Go source",
+ "CI gate fails on untraced requirements",
+ "fusaops trace report flags coverage gaps"
+ ],
+ "action": "Install fusaops hooks pre-commit hook to validate annotation format before every commit.",
+ "requirementIds": [
+ "REQ-FO-TRC002"
+ ]
+ },
+ {
+ "id": "FM-005",
+ "component": "sbom package",
+ "function": "Merge per-language SBOMs into cross-language SBOM",
+ "item": "sbom package.Merge per-language SBOMs into cross-language SBOM",
+ "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.",
+ "severity": 6,
+ "occurrence": 3,
+ "detection": 4,
+ "rpn": 72,
+ "actionPriority": "medium",
+ "mitigations": [
+ "fusaops sbom warns on missing per-language SBOM inputs",
+ "fusaops sci checks SBOM artefact presence with file hash",
+ "Audit pack includes all SBOM artefacts for reviewer verification"
+ ],
+ "action": "Add SBOM completeness check to the release gate; fail release if any language SBOM is absent.",
+ "requirementIds": [
+ "REQ-FO-SBM004"
+ ]
+ },
+ {
+ "id": "FM-006",
+ "component": "auditpack / sign packages",
+ "function": "Verify integrity of evidence artefacts",
+ "item": "auditpack / sign packages.Verify integrity of evidence artefacts",
+ "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.",
+ "severity": 9,
+ "occurrence": 2,
+ "detection": 3,
+ "rpn": 54,
+ "actionPriority": "high",
+ "mitigations": [
+ "fusaops sign — HMAC-SHA256 verification step re-verifies before submission",
+ "fusaops audit-pack — includes hash manifest with all artefact digests",
+ "Release gate re-verifies all signatures before generating provenance"
+ ],
+ "action": "Mandate fusaops sign --verify as the final step of the release pipeline before submission.",
+ "requirementIds": [
+ "REQ-FO-SIGN004"
+ ]
+ },
+ {
+ "id": "FM-007",
+ "component": "cmdAdapter / orchestrator",
+ "function": "Execute adapter within configured timeout",
+ "item": "cmdAdapter / orchestrator.Execute adapter within configured timeout",
+ "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.",
+ "severity": 7,
+ "occurrence": 3,
+ "detection": 6,
+ "rpn": 126,
+ "actionPriority": "medium",
+ "mitigations": [
+ "Adapter process killed after configurable per-adapter timeout",
+ "Orchestrator records the skipped component with reason in the aggregate report",
+ "CI wall-clock timeout surfaces the failure as a build error"
+ ],
+ "action": "Set per-adapter timeout in .fusaops.json; raise alert in report when timeout fires.",
+ "requirementIds": [
+ "REQ-FO-ORC009"
+ ]
+ },
+ {
+ "id": "FM-008",
+ "component": "suppression package",
+ "function": "Filter findings against the suppression list",
+ "item": "suppression package.Filter findings against the suppression list",
+ "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.",
+ "severity": 8,
+ "occurrence": 4,
+ "detection": 4,
+ "rpn": 128,
+ "actionPriority": "high",
+ "mitigations": [
+ "fusaops suppress requires explicit rule IDs; no glob wildcards",
+ "PR review required for all changes to .fusaops-suppress.json",
+ "fusaops qualify audits active suppressions for expiry compliance"
+ ],
+ "action": "Enforce suppression expiry dates; alert in CI when expired suppressions are still active.",
+ "requirementIds": [
+ "REQ-FO-SUP001"
]
}
- ]
+ ],
+ "summary": {
+ "total": 8,
+ "highPriority": 6
+ },
+ "hash": "sha256:33fc5ca071aca851d874052cd2c68ebbe925be759aeb130ea85afa10ff52fac2"
}
diff --git a/fmea/fmea.go b/fmea/fmea.go
index 63075b0..7bc114f 100644
--- a/fmea/fmea.go
+++ b/fmea/fmea.go
@@ -11,7 +11,6 @@ package fmea
import (
"crypto/sha256"
- "encoding/hex"
"encoding/json"
"fmt"
"io"
@@ -32,57 +31,101 @@ const ReportFile = ".fusaops-fmea.json"
//fusa:req REQ-FO-FMEA001
const HighRPNThreshold = 100
+// RatingScale identifies the severity/occurrence/detection rating table in
+// use. FuSaOps predates AIAG-VDA 2019 adoption and uses its own 1-10 scale,
+// so it is named rather than claiming a standard it does not implement.
+//
+//fusa:req REQ-FO-FMEA006
+const RatingScale = "custom-1-10"
+
// FailureMode is one row in the FMEA worksheet.
//
//fusa:req REQ-FO-FMEA001
type FailureMode struct {
- ID string `json:"id"`
- Component string `json:"component"`
- Function string `json:"function"`
- Mode string `json:"mode"`
- Effect string `json:"effect"`
- Cause string `json:"cause"`
- Severity int `json:"severity"` // 1 (negligible) – 10 (catastrophic)
- Occurrence int `json:"occurrence"` // 1 (improbable) – 10 (frequent)
- Detection int `json:"detection"` // 1 (certain) – 10 (undetectable)
- RPN int `json:"rpn"` // Severity × Occurrence × Detection
- Controls []string `json:"controls"`
- Action string `json:"action"`
+ ID string `json:"id"`
+ Component string `json:"component"`
+ Function string `json:"function"`
+ Item string `json:"item"` // x-FuSa spec §9.2 single-field identity: "Component.Function"
+ Mode string `json:"failureMode"`
+ Effect string `json:"effect"`
+ Cause string `json:"cause"`
+ Severity int `json:"severity"` // 1 (negligible) – 10 (catastrophic)
+ Occurrence int `json:"occurrence"` // 1 (improbable) – 10 (frequent)
+ Detection int `json:"detection"` // 1 (certain) – 10 (undetectable)
+ RPN int `json:"rpn"` // Severity × Occurrence × Detection
+ // ActionPriority approximates the AIAG-VDA Handbook's Action Priority
+ // method (high|medium|low) — see actionPriority() for the (deliberately
+ // coarse, severity-dominant) bucketing used in place of the full S×O×D
+ // lookup table that method defines.
+ ActionPriority string `json:"actionPriority"`
+ Controls []string `json:"mitigations"`
+ Action string `json:"action"`
+ // RequirementIDs links this failure mode back to the FuSaOps requirement(s)
+ // (in .fusa-reqs.json) whose behaviour the corrective action lives in.
+ RequirementIDs []string `json:"requirementIds"`
+}
+
+// Summary rolls up the FMEA's totals and analysis-coverage metrics.
+//
+//fusa:req REQ-FO-FMEA006
+type Summary struct {
+ Total int `json:"total"`
+ HighPriority int `json:"highPriority"`
}
// FMEA is the top-level dFMEA document.
//
//fusa:req REQ-FO-FMEA001
type FMEA struct {
- GeneratedAt time.Time `json:"generatedAt"`
- ProjectRoot string `json:"projectRoot"`
- Tool string `json:"tool"`
- ToolVersion string `json:"toolVersion"`
- Standard string `json:"standard"`
- FailureModes []FailureMode `json:"failureModes"`
- TotalItems int `json:"totalItems"`
- HighRPNItems int `json:"highRpnItems"`
- Hash string `json:"hash"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ ProjectRoot string `json:"projectRoot"`
+ Tool string `json:"tool"`
+ ToolVersion string `json:"toolVersion"`
+ Standard string `json:"standard"`
+ RatingScale string `json:"ratingScale"`
+ Entries []FailureMode `json:"entries"`
+ Summary Summary `json:"summary"`
+ Hash string `json:"hash"`
}
// HasHighRPN returns true when any failure mode exceeds HighRPNThreshold.
//
//fusa:req REQ-FO-FMEA005
-func (f *FMEA) HasHighRPN() bool { return f.HighRPNItems > 0 }
+func (f *FMEA) HasHighRPN() bool { return f.Summary.HighPriority > 0 }
+
+// actionPriority approximates the AIAG-VDA Handbook's Action Priority method
+// (high|medium|low), which in the real handbook derives from a full S×O×D
+// lookup table. FuSaOps uses a deliberately coarser, severity-dominant
+// bucketing — severity ≥8 is always "high" regardless of occurrence/
+// detection, matching AIAG-VDA's own philosophy that a severe failure
+// warrants urgent attention even when rare or well-detected.
+//
+//fusa:req REQ-FO-FMEA006
+func actionPriority(severity int) string {
+ switch {
+ case severity >= 8:
+ return "high"
+ case severity >= 5:
+ return "medium"
+ default:
+ return "low"
+ }
+}
// modeSpec holds the static definition of one failure mode; RPN is computed.
type modeSpec struct {
- id string
- component string
- function string
- mode string
- effect string
- cause string
- severity int
- occurrence int
- detection int
- controls []string
- action string
+ id string
+ component string
+ function string
+ mode string
+ effect string
+ cause string
+ severity int
+ occurrence int
+ detection int
+ controls []string
+ action string
+ requirementIDs []string
}
// standardModes covers failure modes in the FuSaOps orchestration pipeline
@@ -103,7 +146,8 @@ var standardModes = []modeSpec{
"CI gate fails when a required adapter is absent",
"fusaops sci — inventories tool presence with version and hash",
},
- action: "Add adapter availability check to the CI gate; fail the build on missing required adapters.",
+ action: "Add adapter availability check to the CI gate; fail the build on missing required adapters.",
+ requirementIDs: []string{"REQ-FO-ADP007"},
},
{
id: "FM-002",
@@ -120,7 +164,8 @@ var standardModes = []modeSpec{
"Orchestrator records skipped and failed adapters in the aggregate report",
"fusaops qualify — validates adapter health against each subcommand",
},
- action: "Surface adapter error prominently in aggregate report; treat adapter failure as a gate failure.",
+ action: "Surface adapter error prominently in aggregate report; treat adapter failure as a gate failure.",
+ requirementIDs: []string{"REQ-FO-ORC002"},
},
{
id: "FM-003",
@@ -137,7 +182,8 @@ var standardModes = []modeSpec{
"fusaops qualify — validates adapter output against each spec section",
"SpecVersion pinning in capabilities response",
},
- action: "Run fusaops conform in CI after every tool version update; pin tool versions in Dockerfile.",
+ action: "Run fusaops conform in CI after every tool version update; pin tool versions in Dockerfile.",
+ requirementIDs: []string{"REQ-FO-CNF003"},
},
{
id: "FM-004",
@@ -154,7 +200,8 @@ var standardModes = []modeSpec{
"CI gate fails on untraced requirements",
"fusaops trace report flags coverage gaps",
},
- action: "Install fusaops hooks pre-commit hook to validate annotation format before every commit.",
+ action: "Install fusaops hooks pre-commit hook to validate annotation format before every commit.",
+ requirementIDs: []string{"REQ-FO-TRC002"},
},
{
id: "FM-005",
@@ -171,7 +218,8 @@ var standardModes = []modeSpec{
"fusaops sci checks SBOM artefact presence with file hash",
"Audit pack includes all SBOM artefacts for reviewer verification",
},
- action: "Add SBOM completeness check to the release gate; fail release if any language SBOM is absent.",
+ action: "Add SBOM completeness check to the release gate; fail release if any language SBOM is absent.",
+ requirementIDs: []string{"REQ-FO-SBM004"},
},
{
id: "FM-006",
@@ -188,7 +236,8 @@ var standardModes = []modeSpec{
"fusaops audit-pack — includes hash manifest with all artefact digests",
"Release gate re-verifies all signatures before generating provenance",
},
- action: "Mandate fusaops sign --verify as the final step of the release pipeline before submission.",
+ action: "Mandate fusaops sign --verify as the final step of the release pipeline before submission.",
+ requirementIDs: []string{"REQ-FO-SIGN004"},
},
{
id: "FM-007",
@@ -205,7 +254,8 @@ var standardModes = []modeSpec{
"Orchestrator records the skipped component with reason in the aggregate report",
"CI wall-clock timeout surfaces the failure as a build error",
},
- action: "Set per-adapter timeout in .fusaops.json; raise alert in report when timeout fires.",
+ action: "Set per-adapter timeout in .fusaops.json; raise alert in report when timeout fires.",
+ requirementIDs: []string{"REQ-FO-ORC009"},
},
{
id: "FM-008",
@@ -222,7 +272,8 @@ var standardModes = []modeSpec{
"PR review required for all changes to .fusaops-suppress.json",
"fusaops qualify audits active suppressions for expiry compliance",
},
- action: "Enforce suppression expiry dates; alert in CI when expired suppressions are still active.",
+ action: "Enforce suppression expiry dates; alert in CI when expired suppressions are still active.",
+ requirementIDs: []string{"REQ-FO-SUP001"},
},
}
@@ -236,28 +287,32 @@ func Build(root string) (*FMEA, error) {
Tool: "fusaops",
ToolVersion: fusaops.Version,
Standard: "IEC 61508:2010 / ISO 26262:2018 Part 8-7",
+ RatingScale: RatingScale,
}
for _, spec := range standardModes {
rpn := spec.severity * spec.occurrence * spec.detection
fm := FailureMode{
- ID: spec.id,
- Component: spec.component,
- Function: spec.function,
- Mode: spec.mode,
- Effect: spec.effect,
- Cause: spec.cause,
- Severity: spec.severity,
- Occurrence: spec.occurrence,
- Detection: spec.detection,
- RPN: rpn,
- Controls: spec.controls,
- Action: spec.action,
+ ID: spec.id,
+ Component: spec.component,
+ Function: spec.function,
+ Item: spec.component + "." + spec.function,
+ Mode: spec.mode,
+ Effect: spec.effect,
+ Cause: spec.cause,
+ Severity: spec.severity,
+ Occurrence: spec.occurrence,
+ Detection: spec.detection,
+ RPN: rpn,
+ ActionPriority: actionPriority(spec.severity),
+ Controls: spec.controls,
+ Action: spec.action,
+ RequirementIDs: spec.requirementIDs,
}
- f.FailureModes = append(f.FailureModes, fm)
- f.TotalItems++
+ f.Entries = append(f.Entries, fm)
+ f.Summary.Total++
if rpn > HighRPNThreshold {
- f.HighRPNItems++
+ f.Summary.HighPriority++
}
}
@@ -272,8 +327,12 @@ func computeHash(f *FMEA) string {
if err != nil {
return ""
}
- sum := sha256.Sum256(data)
- return hex.EncodeToString(sum[:])
+ canon, err := fusaops.Canonicalize(data)
+ if err != nil {
+ return ""
+ }
+ sum := sha256.Sum256(canon)
+ return fmt.Sprintf("sha256:%x", sum)
}
// Save writes the FMEA to path as indented JSON.
@@ -333,9 +392,9 @@ func renderText(w io.Writer, f *FMEA) error {
fmt.Fprintf(w, "Tool: %s v%s\n", f.Tool, f.ToolVersion)
fmt.Fprintf(w, "Generated: %s\n", f.GeneratedAt.Format("2006-01-02T15:04:05Z"))
fmt.Fprintf(w, "Items: %d total, %d high-RPN (>%d)\n\n",
- f.TotalItems, f.HighRPNItems, HighRPNThreshold)
+ f.Summary.Total, f.Summary.HighPriority, HighRPNThreshold)
- for _, fm := range f.FailureModes {
+ for _, fm := range f.Entries {
priority := " "
if fm.RPN > HighRPNThreshold {
priority = "!"
@@ -361,7 +420,7 @@ func renderText(w io.Writer, f *FMEA) error {
}
if f.HasHighRPN() {
fmt.Fprintf(w, "\nHIGH-RPN ITEMS: %d failure mode(s) require corrective action (RPN > %d).\n",
- f.HighRPNItems, HighRPNThreshold)
+ f.Summary.HighPriority, HighRPNThreshold)
}
return nil
}
diff --git a/fmea/fmea_test.go b/fmea/fmea_test.go
index a16fcf0..d8c4293 100644
--- a/fmea/fmea_test.go
+++ b/fmea/fmea_test.go
@@ -31,11 +31,11 @@ func TestBuildReturnsItems(t *testing.T) {
if err != nil {
t.Fatalf("Build: %v", err)
}
- if f.TotalItems == 0 {
+ if f.Summary.Total == 0 {
t.Error("Build must return at least one failure mode")
}
- if len(f.FailureModes) != f.TotalItems {
- t.Errorf("len(FailureModes)=%d, TotalItems=%d", len(f.FailureModes), f.TotalItems)
+ if len(f.Entries) != f.Summary.Total {
+ t.Errorf("len(Entries)=%d, Summary.Total=%d", len(f.Entries), f.Summary.Total)
}
}
@@ -45,7 +45,7 @@ func TestBuildRPNComputed(t *testing.T) {
if err != nil {
t.Fatalf("Build: %v", err)
}
- for _, fm := range f.FailureModes {
+ for _, fm := range f.Entries {
if fm.ID == "" {
t.Error("failure mode has empty ID")
}
@@ -63,6 +63,15 @@ func TestBuildRPNComputed(t *testing.T) {
if fm.Action == "" {
t.Errorf("%s: action must not be empty", fm.ID)
}
+ if fm.Item == "" {
+ t.Errorf("%s: item must not be empty", fm.ID)
+ }
+ if fm.ActionPriority == "" {
+ t.Errorf("%s: actionPriority must not be empty", fm.ID)
+ }
+ if len(fm.RequirementIDs) == 0 {
+ t.Errorf("%s: requirementIds must not be empty", fm.ID)
+ }
}
}
@@ -73,13 +82,13 @@ func TestBuildHighRPNCounter(t *testing.T) {
t.Fatalf("Build: %v", err)
}
var count int
- for _, fm := range f.FailureModes {
+ for _, fm := range f.Entries {
if fm.RPN > fmea.HighRPNThreshold {
count++
}
}
- if f.HighRPNItems != count {
- t.Errorf("HighRPNItems=%d, counted %d", f.HighRPNItems, count)
+ if f.Summary.HighPriority != count {
+ t.Errorf("Summary.HighPriority=%d, counted %d", f.Summary.HighPriority, count)
}
}
@@ -110,6 +119,21 @@ func TestBuildMetadata(t *testing.T) {
}
}
+// TestBuildHashHasAlgoPrefix verifies Hash carries the "sha256:" prefix
+// required by x-FuSa spec §2.7 for a field named "hash" (as opposed to a
+// field named "sha256", which stays bare hex).
+//
+//fusa:test REQ-FO-FMEA006
+func TestBuildHashHasAlgoPrefix(t *testing.T) {
+ f, err := fmea.Build(t.TempDir())
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+ if !strings.HasPrefix(f.Hash, "sha256:") {
+ t.Errorf("Hash = %q, want sha256: prefix", f.Hash)
+ }
+}
+
//fusa:test REQ-FO-FMEA002
func TestBuildHashChanges(t *testing.T) {
f1, _ := fmea.Build(t.TempDir())
@@ -126,10 +150,10 @@ func TestHasHighRPN(t *testing.T) {
if err != nil {
t.Fatalf("Build: %v", err)
}
- want := f.HighRPNItems > 0
+ want := f.Summary.HighPriority > 0
if f.HasHighRPN() != want {
- t.Errorf("HasHighRPN()=%v, want %v (HighRPNItems=%d)",
- f.HasHighRPN(), want, f.HighRPNItems)
+ t.Errorf("HasHighRPN()=%v, want %v (Summary.HighPriority=%d)",
+ f.HasHighRPN(), want, f.Summary.HighPriority)
}
}
@@ -151,8 +175,8 @@ func TestSaveLoad(t *testing.T) {
if loaded.Hash != f.Hash {
t.Errorf("Hash mismatch after round-trip: got %q, want %q", loaded.Hash, f.Hash)
}
- if loaded.TotalItems != f.TotalItems {
- t.Errorf("TotalItems mismatch: got %d, want %d", loaded.TotalItems, f.TotalItems)
+ if loaded.Summary.Total != f.Summary.Total {
+ t.Errorf("Summary.Total mismatch: got %d, want %d", loaded.Summary.Total, f.Summary.Total)
}
}
@@ -217,8 +241,8 @@ func TestRenderJSON(t *testing.T) {
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
t.Fatalf("JSON unmarshal: %v", err)
}
- if got.TotalItems != f.TotalItems {
- t.Errorf("TotalItems=%d, want %d", got.TotalItems, f.TotalItems)
+ if got.Summary.Total != f.Summary.Total {
+ t.Errorf("Summary.Total=%d, want %d", got.Summary.Total, f.Summary.Total)
}
}
@@ -248,7 +272,7 @@ func TestRenderUnknownFormat(t *testing.T) {
//fusa:test REQ-FO-FMEA004
func TestRenderTextLowRPN(t *testing.T) {
f := &fmea.FMEA{
- FailureModes: []fmea.FailureMode{
+ Entries: []fmea.FailureMode{
{ID: "FM-LOW", Component: "comp", Mode: "test", RPN: 10},
},
}
diff --git a/fusaops.go b/fusaops.go
index 87fd47c..268728e 100644
--- a/fusaops.go
+++ b/fusaops.go
@@ -20,7 +20,7 @@ import (
)
// Version is the current release of FuSaOps.
-const Version = "1.139.0"
+const Version = "1.140.0"
// SpecVersion is the x-FuSa specification version this release targets.
//
diff --git a/hara/hara.go b/hara/hara.go
index 7747fff..416af29 100644
--- a/hara/hara.go
+++ b/hara/hara.go
@@ -101,7 +101,13 @@ type SafetyGoal struct {
HazardIDs []string `json:"hazards"`
ASIL ASIL `json:"asil"`
SafeState string `json:"safeState,omitempty"`
- FSSRRef string `json:"fssrRef,omitempty"`
+ // FssrRefs lists the Functional Safety Requirement id(s), from
+ // .fusa-reqs.json, that decompose this safety goal. Per x-FuSa spec
+ // §1.2.5, a safety goal MUST have at least one — ISO 26262-8 Clause 6
+ // requires a safety goal to decompose into a traceable requirement.
+ //
+ //fusa:req REQ-FO-HARA005
+ FssrRefs []string `json:"fssrRefs"`
}
// HARA is the full hazard analysis and risk assessment for a project.
@@ -303,6 +309,12 @@ func Validate(h *HARA) []ValidationFinding {
Message: fmt.Sprintf("safety goal %s has no ASIL assigned", g.ID),
})
}
+ if len(g.FssrRefs) == 0 {
+ out = append(out, ValidationFinding{
+ SafetyGoalID: g.ID,
+ Message: fmt.Sprintf("safety goal %s has no fssrRefs — a safety goal MUST decompose into at least one functional safety requirement (x-FuSa spec §1.2.5, ISO 26262-8 Clause 6)", g.ID),
+ })
+ }
}
return out
}
diff --git a/hara/hara_test.go b/hara/hara_test.go
index c39f602..beac374 100644
--- a/hara/hara_test.go
+++ b/hara/hara_test.go
@@ -129,7 +129,7 @@ func TestValidateComplete(t *testing.T) {
},
},
SafetyGoals: []hara.SafetyGoal{
- {ID: "SG-001", Description: "test goal", ASIL: hara.ASILB},
+ {ID: "SG-001", Description: "test goal", ASIL: hara.ASILB, FssrRefs: []string{"REQ-FO-HARA001"}},
},
}
findings := hara.Validate(h)
@@ -229,6 +229,25 @@ func TestValidateNoASILOnGoal(t *testing.T) {
}
}
+//fusa:test REQ-FO-HARA005
+func TestValidateNoFssrRefsOnGoal(t *testing.T) {
+ h := &hara.HARA{
+ SafetyGoals: []hara.SafetyGoal{
+ {ID: "SG-001", Description: "no fssrRefs", ASIL: hara.ASILB},
+ },
+ }
+ findings := hara.Validate(h)
+ found := false
+ for _, f := range findings {
+ if strings.Contains(f.Message, "no fssrRefs") {
+ found = true
+ }
+ }
+ if !found {
+ t.Error("expected no-fssrRefs finding on safety goal")
+ }
+}
+
//fusa:test REQ-FO-HARA003
func TestLoadMissing(t *testing.T) {
h, err := hara.Load(t.TempDir())
diff --git a/qualify/qualify.go b/qualify/qualify.go
index 25f358a..4f10d88 100644
--- a/qualify/qualify.go
+++ b/qualify/qualify.go
@@ -294,6 +294,10 @@ func renderText(w io.Writer, r *Report) error {
return nil
}
+// computeHash canonicalizes the same fields as before (everything except
+// Hash itself) via fusaops.Canonicalize, so the hash is genuine RFC 8785
+// sorted-key canonical JSON (§6) rather than relying on Go's fixed struct
+// field order to be deterministic.
func computeHash(r *Report) string {
data, _ := json.Marshal(struct {
GeneratedAt time.Time `json:"generatedAt"`
@@ -314,6 +318,10 @@ func computeHash(r *Report) string {
r.ImplementationAuthor, r.IndependentReviewer, r.AchievableASIL,
r.Total, r.Passed, r.Failed, r.Components,
})
- h := sha256.Sum256(data)
+ canon, err := fusaops.Canonicalize(data)
+ if err != nil {
+ canon = data
+ }
+ h := sha256.Sum256(canon)
return fmt.Sprintf("sha256:%x", h)
}
diff --git a/safety-case.json b/safety-case.json
index 57e8d08..666719a 100644
--- a/safety-case.json
+++ b/safety-case.json
@@ -1,94 +1,274 @@
{
- "format": "go-FuSa Safety Case v1",
- "generatedAt": "2026-06-10T14:31:49.528893Z",
- "module": "github.com/SoundMatt/FuSaOps",
- "standard": "generic",
- "evidence": [
+ "generatedAt": "2026-07-28T18:47:06.907424Z",
+ "projectRoot": ".",
+ "tool": "fusaops",
+ "toolVersion": "1.140.0",
+ "standard": "ISO 26262",
+ "claims": [
{
- "id": "check",
- "description": "Coding standard and static analysis checks",
- "file": "check-report.json",
- "status": "absent",
- "detail": "run 'gofusa check --output check-report.json' to generate"
+ "id": "C-001",
+ "title": "Tool qualification evidence is available",
+ "strategy": "Argument by inspection of tool qualification report",
+ "evidence": [
+ {
+ "title": "Tool qualification report",
+ "path": "./.fusaops-qualify-report.json",
+ "status": "missing"
+ }
+ ],
+ "passed": false
},
{
- "id": "trace",
- "description": "Requirements traceability matrix",
- "file": ".fusa-reqs.json",
- "status": "present",
- "detail": "101 requirements"
+ "id": "C-002",
+ "title": "Requirements are traced to tests across all language components",
+ "strategy": "Argument by inspection of cross-language traceability matrix",
+ "evidence": [
+ {
+ "title": "Traceability matrix (JSON)",
+ "path": "./.fusaops-trace.json",
+ "status": "missing"
+ }
+ ],
+ "passed": false
},
{
- "id": "verify",
- "description": "Test evidence bundle",
- "file": ".fusa-evidence.json",
- "status": "present",
- "detail": "105/105 tests passed"
+ "id": "C-003",
+ "title": "Test execution evidence is captured",
+ "strategy": "Argument by inspection of structured test evidence bundle",
+ "evidence": [
+ {
+ "title": "Test evidence bundle",
+ "path": "./.fusaops-evidence.json",
+ "status": "missing"
+ }
+ ],
+ "passed": false
},
{
- "id": "qualify",
- "description": "Tool qualification report",
- "file": "qualify-report.json",
- "status": "present",
- "detail": "44/44 cases passed"
+ "id": "C-004",
+ "title": "Software Bill of Materials is complete",
+ "strategy": "Argument by inspection of cross-language SBOM",
+ "evidence": [
+ {
+ "title": "Cross-language SBOM",
+ "path": "./sbom.json",
+ "status": "present",
+ "sha256": "17e9172e968bc17d813e3c7960eb40fe89d3327c102da83365258bc2fc7d9388",
+ "size": 623
+ }
+ ],
+ "passed": true
},
{
- "id": "sbom",
- "description": "SBOM (SPDX 3.0.1)",
- "file": "sbom.json",
- "status": "present"
+ "id": "C-005",
+ "title": "Build integrity is established",
+ "strategy": "Argument by inspection of build provenance and artifact manifest",
+ "evidence": [
+ {
+ "title": "Build provenance",
+ "path": "./provenance.json",
+ "status": "present",
+ "sha256": "b599f32f336abb4f473dbb4bb7a426af815de3c0004ccf345dc70a6cb538c14b",
+ "size": 283
+ },
+ {
+ "title": "Artifact manifest (SHA-256)",
+ "path": "./artifact-manifest.json",
+ "status": "present",
+ "sha256": "bff8d3d64849cecec8173e4f117649b483c2b7d5f2ab3be571e1ab4fea46f6bf",
+ "size": 453
+ }
+ ],
+ "passed": true
},
{
- "id": "provenance",
- "description": "Build provenance",
- "file": "provenance.json",
- "status": "present"
+ "id": "C-006",
+ "title": "Problem reports are tracked and resolved",
+ "strategy": "Argument by inspection of problem report log",
+ "evidence": [
+ {
+ "title": "Problem report log",
+ "path": "./.fusaops-problems.json",
+ "status": "missing"
+ }
+ ],
+ "passed": false
+ },
+ {
+ "id": "C-007",
+ "title": "Audit evidence pack is available for review",
+ "strategy": "Argument by inspection of bundled audit evidence",
+ "evidence": [
+ {
+ "title": "Audit pack archive",
+ "path": "./audit-pack.zip",
+ "status": "present",
+ "sha256": "490f4b5c57c02ce6c835df93be110e5ce123e0537995ad2b5d8b768abee743e9",
+ "size": 17730
+ }
+ ],
+ "passed": true
+ }
+ ],
+ "totalClaims": 7,
+ "passedClaims": 3,
+ "nodes": [
+ {
+ "id": "C-001",
+ "type": "goal",
+ "text": "Tool qualification evidence is available"
+ },
+ {
+ "id": "C-001-St",
+ "type": "strategy",
+ "text": "Argument by inspection of tool qualification report"
+ },
+ {
+ "id": "C-002",
+ "type": "goal",
+ "text": "Requirements are traced to tests across all language components"
+ },
+ {
+ "id": "C-002-St",
+ "type": "strategy",
+ "text": "Argument by inspection of cross-language traceability matrix"
+ },
+ {
+ "id": "C-003",
+ "type": "goal",
+ "text": "Test execution evidence is captured"
+ },
+ {
+ "id": "C-003-St",
+ "type": "strategy",
+ "text": "Argument by inspection of structured test evidence bundle"
+ },
+ {
+ "id": "C-004",
+ "type": "goal",
+ "text": "Software Bill of Materials is complete"
+ },
+ {
+ "id": "C-004-St",
+ "type": "strategy",
+ "text": "Argument by inspection of cross-language SBOM"
+ },
+ {
+ "id": "C-004-Sn1",
+ "type": "solution",
+ "text": "Cross-language SBOM",
+ "evidence": "./sbom.json"
+ },
+ {
+ "id": "C-005",
+ "type": "goal",
+ "text": "Build integrity is established"
+ },
+ {
+ "id": "C-005-St",
+ "type": "strategy",
+ "text": "Argument by inspection of build provenance and artifact manifest"
+ },
+ {
+ "id": "C-005-Sn1",
+ "type": "solution",
+ "text": "Build provenance",
+ "evidence": "./provenance.json"
+ },
+ {
+ "id": "C-005-Sn2",
+ "type": "solution",
+ "text": "Artifact manifest (SHA-256)",
+ "evidence": "./artifact-manifest.json"
+ },
+ {
+ "id": "C-006",
+ "type": "goal",
+ "text": "Problem reports are tracked and resolved"
+ },
+ {
+ "id": "C-006-St",
+ "type": "strategy",
+ "text": "Argument by inspection of problem report log"
+ },
+ {
+ "id": "C-007",
+ "type": "goal",
+ "text": "Audit evidence pack is available for review"
+ },
+ {
+ "id": "C-007-St",
+ "type": "strategy",
+ "text": "Argument by inspection of bundled audit evidence"
+ },
+ {
+ "id": "C-007-Sn1",
+ "type": "solution",
+ "text": "Audit pack archive",
+ "evidence": "./audit-pack.zip"
}
],
- "clauses": [
- {
- "standard": "Generic",
- "clause": "CS-1",
- "title": "Coding standard compliance",
- "evidenceIds": [
- "check"
- ]
- },
- {
- "standard": "Generic",
- "clause": "CS-2",
- "title": "Requirements traceability",
- "evidenceIds": [
- "trace"
- ]
- },
- {
- "standard": "Generic",
- "clause": "CS-3",
- "title": "Test evidence",
- "evidenceIds": [
- "verify"
- ]
- },
- {
- "standard": "Generic",
- "clause": "CS-4",
- "title": "Tool qualification",
- "evidenceIds": [
- "qualify"
- ]
- },
- {
- "standard": "Generic",
- "clause": "CS-5",
- "title": "Release inventory",
- "evidenceIds": [
- "sbom",
- "provenance"
- ]
+ "edges": [
+ {
+ "from": "C-001",
+ "to": "C-001-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-002",
+ "to": "C-002-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-003",
+ "to": "C-003-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-004",
+ "to": "C-004-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-004-St",
+ "to": "C-004-Sn1",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-005",
+ "to": "C-005-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-005-St",
+ "to": "C-005-Sn1",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-005-St",
+ "to": "C-005-Sn2",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-006",
+ "to": "C-006-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-007",
+ "to": "C-007-St",
+ "type": "supportedBy"
+ },
+ {
+ "from": "C-007-St",
+ "to": "C-007-Sn1",
+ "type": "supportedBy"
}
],
- "gaps": [
- "check"
- ]
+ "completeness": {
+ "totalGoals": 7,
+ "goalsWithEvidence": 3,
+ "undeveloped": 4
+ },
+ "hash": "sha256:8a624d3327d600e5625d8f8a17c6308f417808b98a8140ba27f70ed6107bf7c7"
}
diff --git a/safetycase/safetycase.go b/safetycase/safetycase.go
index 7db256a..1c945c8 100644
--- a/safetycase/safetycase.go
+++ b/safetycase/safetycase.go
@@ -72,19 +72,83 @@ type Claim struct {
Passed bool `json:"passed"`
}
-// SafetyCase is the top-level safety argument document.
+// GSNNodeType is one of the six node types defined by the GSN Community
+// Standard (Assurance Case Working Group, v3, 2021).
+//
+//fusa:req REQ-FO-SC006
+type GSNNodeType string
+
+const (
+ GSNGoal GSNNodeType = "goal"
+ GSNStrategy GSNNodeType = "strategy"
+ GSNSolution GSNNodeType = "solution"
+ GSNContext GSNNodeType = "context"
+ GSNAssumption GSNNodeType = "assumption"
+ GSNJustification GSNNodeType = "justification"
+)
+
+// GSNNode is one node in the GSN argument graph.
+//
+//fusa:req REQ-FO-SC006
+type GSNNode struct {
+ ID string `json:"id"`
+ Type GSNNodeType `json:"type"`
+ Text string `json:"text"`
+ Evidence string `json:"evidence,omitempty"`
+}
+
+// GSNEdgeType is one of the two edge types defined by the GSN Community
+// Standard: an argument step (supportedBy) or a context/assumption/
+// justification attachment (inContextOf).
+//
+//fusa:req REQ-FO-SC006
+type GSNEdgeType string
+
+const (
+ GSNSupportedBy GSNEdgeType = "supportedBy"
+ GSNInContextOf GSNEdgeType = "inContextOf"
+)
+
+// GSNEdge is one edge in the GSN argument graph.
+//
+//fusa:req REQ-FO-SC006
+type GSNEdge struct {
+ From string `json:"from"`
+ To string `json:"to"`
+ Type GSNEdgeType `json:"type"`
+}
+
+// Completeness rolls up the GSN graph's goal/evidence coverage.
+//
+//fusa:req REQ-FO-SC006
+type Completeness struct {
+ TotalGoals int `json:"totalGoals"`
+ GoalsWithEvidence int `json:"goalsWithEvidence"`
+ Undeveloped int `json:"undeveloped"`
+}
+
+// SafetyCase is the top-level safety argument document. Claims/EvidenceRef
+// remain the primary evidence-resolution model (file-existence + SHA-256);
+// Nodes/Edges/Completeness are a GSN (Goal Structuring Notation) projection
+// of that same data, per x-FuSa spec §9.2 — each Claim becomes a goal node
+// with its strategy text as a strategy node, and each present EvidenceRef
+// becomes a solution node, so the GSN graph and the claims are two views of
+// one underlying analysis rather than a second, disconnected model.
//
//fusa:req REQ-FO-SC001
type SafetyCase struct {
- GeneratedAt time.Time `json:"generatedAt"`
- ProjectRoot string `json:"projectRoot"`
- Tool string `json:"tool"`
- ToolVersion string `json:"toolVersion"`
- Standard Standard `json:"standard"`
- Claims []Claim `json:"claims"`
- TotalClaims int `json:"totalClaims"`
- PassedClaims int `json:"passedClaims"`
- Hash string `json:"hash"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ ProjectRoot string `json:"projectRoot"`
+ Tool string `json:"tool"`
+ ToolVersion string `json:"toolVersion"`
+ Standard Standard `json:"standard"`
+ Claims []Claim `json:"claims"`
+ TotalClaims int `json:"totalClaims"`
+ PassedClaims int `json:"passedClaims"`
+ Nodes []GSNNode `json:"nodes"`
+ Edges []GSNEdge `json:"edges"`
+ Completeness Completeness `json:"completeness"`
+ Hash string `json:"hash"`
}
// HasGaps returns true when at least one claim failed due to missing evidence.
@@ -92,6 +156,45 @@ type SafetyCase struct {
//fusa:req REQ-FO-SC005
func (s *SafetyCase) HasGaps() bool { return s.PassedClaims < s.TotalClaims }
+// buildGSN projects claims into a GSN graph: each claim is a goal node
+// supported by a strategy node, itself supported by one solution node per
+// present evidence item. A claim with any missing evidence counts toward
+// Completeness.Undeveloped rather than fabricating a solution node for
+// evidence that doesn't exist.
+//
+//fusa:req REQ-FO-SC006
+func buildGSN(claims []Claim) ([]GSNNode, []GSNEdge, Completeness) {
+ var nodes []GSNNode
+ var edges []GSNEdge
+ var comp Completeness
+
+ for _, c := range claims {
+ comp.TotalGoals++
+ goalID := c.ID
+ strategyID := c.ID + "-St"
+ nodes = append(nodes, GSNNode{ID: goalID, Type: GSNGoal, Text: c.Title})
+ nodes = append(nodes, GSNNode{ID: strategyID, Type: GSNStrategy, Text: c.Strategy})
+ edges = append(edges, GSNEdge{From: goalID, To: strategyID, Type: GSNSupportedBy})
+
+ hasEvidence := false
+ for i, e := range c.Evidence {
+ if e.Status != StatusPresent {
+ continue
+ }
+ hasEvidence = true
+ solutionID := fmt.Sprintf("%s-Sn%d", c.ID, i+1)
+ nodes = append(nodes, GSNNode{ID: solutionID, Type: GSNSolution, Text: e.Title, Evidence: e.Path})
+ edges = append(edges, GSNEdge{From: strategyID, To: solutionID, Type: GSNSupportedBy})
+ }
+ if hasEvidence {
+ comp.GoalsWithEvidence++
+ } else {
+ comp.Undeveloped++
+ }
+ }
+ return nodes, edges, comp
+}
+
// evidenceSpec defines one expected evidence file.
type evidenceSpec struct {
title string
@@ -235,6 +338,8 @@ func Build(root string, std Standard) (*SafetyCase, error) {
}
}
+ sc.Nodes, sc.Edges, sc.Completeness = buildGSN(sc.Claims)
+
// Compute integrity hash over JSON representation (without Hash field).
sc.Hash = computeHash(sc)
return sc, nil
@@ -247,8 +352,12 @@ func computeHash(sc *SafetyCase) string {
if err != nil {
return ""
}
- sum := sha256.Sum256(data)
- return hex.EncodeToString(sum[:])
+ canon, err := fusaops.Canonicalize(data)
+ if err != nil {
+ return ""
+ }
+ sum := sha256.Sum256(canon)
+ return fmt.Sprintf("sha256:%x", sum)
}
// Save writes the safety case to path as indented JSON.
diff --git a/safetycase/safetycase_test.go b/safetycase/safetycase_test.go
index c3a0208..a1cae6b 100644
--- a/safetycase/safetycase_test.go
+++ b/safetycase/safetycase_test.go
@@ -131,6 +131,86 @@ func TestBuildWithEvidence(t *testing.T) {
}
}
+// TestBuildHashHasAlgoPrefix verifies Hash carries the "sha256:" prefix
+// required by x-FuSa spec §2.7 for a field named "hash".
+//
+//fusa:test REQ-FO-SC006
+func TestBuildHashHasAlgoPrefix(t *testing.T) {
+ sc, err := safetycase.Build(t.TempDir(), safetycase.StandardISO26262)
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+ if !strings.HasPrefix(sc.Hash, "sha256:") {
+ t.Errorf("Hash = %q, want sha256: prefix", sc.Hash)
+ }
+}
+
+// TestBuildGSNProjection verifies each Claim becomes a goal+strategy node
+// pair, present evidence becomes solution nodes, and a claim with no
+// evidence at all counts toward Completeness.Undeveloped rather than
+// fabricating a solution node.
+//
+//fusa:test REQ-FO-SC006
+func TestBuildGSNProjection(t *testing.T) {
+ dir := t.TempDir()
+ // Give C-001 (qualify report) real evidence so it has >=1 solution node.
+ if err := os.WriteFile(filepath.Join(dir, ".fusaops-qualify-report.json"),
+ []byte(`{"components":[]}`), 0o640); err != nil {
+ t.Fatal(err)
+ }
+ sc, err := safetycase.Build(dir, safetycase.StandardISO26262)
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+
+ if sc.Completeness.TotalGoals != sc.TotalClaims {
+ t.Errorf("Completeness.TotalGoals=%d, want %d", sc.Completeness.TotalGoals, sc.TotalClaims)
+ }
+ if sc.Completeness.GoalsWithEvidence == 0 {
+ t.Error("expected at least one goal with evidence (C-001)")
+ }
+ if sc.Completeness.Undeveloped == 0 {
+ t.Error("expected at least one undeveloped goal (no other evidence present)")
+ }
+
+ var goals, strategies, solutions int
+ for _, n := range sc.Nodes {
+ switch n.Type {
+ case safetycase.GSNGoal:
+ goals++
+ case safetycase.GSNStrategy:
+ strategies++
+ case safetycase.GSNSolution:
+ solutions++
+ if n.Evidence == "" {
+ t.Errorf("solution node %s missing evidence path", n.ID)
+ }
+ }
+ }
+ if goals != sc.TotalClaims {
+ t.Errorf("goal node count=%d, want %d", goals, sc.TotalClaims)
+ }
+ if strategies != sc.TotalClaims {
+ t.Errorf("strategy node count=%d, want %d", strategies, sc.TotalClaims)
+ }
+ if solutions == 0 {
+ t.Error("expected at least one solution node for present evidence")
+ }
+
+ var supportedBy int
+ for _, e := range sc.Edges {
+ if e.Type != safetycase.GSNSupportedBy && e.Type != safetycase.GSNInContextOf {
+ t.Errorf("unexpected edge type %q", e.Type)
+ }
+ if e.Type == safetycase.GSNSupportedBy {
+ supportedBy++
+ }
+ }
+ if supportedBy != strategies+solutions {
+ t.Errorf("supportedBy edges=%d, want %d (one goal->strategy plus one strategy->solution)", supportedBy, strategies+solutions)
+ }
+}
+
//fusa:test REQ-FO-SC002
func TestBuildEvidenceHash(t *testing.T) {
dir := t.TempDir()
diff --git a/sas/sas.go b/sas/sas.go
index 9fde1a3..8100a95 100644
--- a/sas/sas.go
+++ b/sas/sas.go
@@ -8,7 +8,6 @@ package sas
import (
"crypto/sha256"
- "encoding/hex"
"encoding/json"
"fmt"
"io"
@@ -44,19 +43,44 @@ type Activity struct {
Evidence string `json:"evidence,omitempty"` // path of supporting evidence file
}
-// SAS is the Software Accomplishment Summary document.
+// ChecklistItem is x-FuSa spec §9.3's promoted-schema view of one Activity:
+// `item`/`clause`/`present`/`evidence` rather than this package's richer
+// three-state Activity/ActivityStatus.
+//
+//fusa:req REQ-FO-SAS006
+type ChecklistItem struct {
+ Item string `json:"item"`
+ Clause string `json:"clause"`
+ Present bool `json:"present"`
+ Evidence string `json:"evidence,omitempty"`
+}
+
+// Summary rolls up the SAS's checklist totals.
+//
+//fusa:req REQ-FO-SAS006
+type Summary struct {
+ Total int `json:"total"`
+ Present int `json:"present"`
+}
+
+// SAS is the Software Accomplishment Summary document. Activities/
+// ActivityStatus remain the primary three-state (complete/incomplete/N-A)
+// model; Checklist/Summary are the boolean-`present` projection required by
+// x-FuSa spec §9.3, derived from the same Activities at Build time.
//
//fusa:req REQ-FO-SAS001
type SAS struct {
- GeneratedAt time.Time `json:"generatedAt"`
- ProjectRoot string `json:"projectRoot"`
- Tool string `json:"tool"`
- ToolVersion string `json:"toolVersion"`
- SoftwareLevel string `json:"softwareLevel"` // DAL-A through DAL-E
- Activities []Activity `json:"activities"`
- TotalActivities int `json:"totalActivities"`
- CompleteActivities int `json:"completeActivities"`
- Hash string `json:"hash"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ ProjectRoot string `json:"projectRoot"`
+ Tool string `json:"tool"`
+ ToolVersion string `json:"toolVersion"`
+ SoftwareLevel string `json:"softwareLevel"` // DAL-A through DAL-E
+ Activities []Activity `json:"activities"`
+ TotalActivities int `json:"totalActivities"`
+ CompleteActivities int `json:"completeActivities"`
+ Checklist []ChecklistItem `json:"checklist"`
+ Summary Summary `json:"summary"`
+ Hash string `json:"hash"`
}
// HasGaps returns true when at least one required activity is incomplete.
@@ -64,6 +88,31 @@ type SAS struct {
//fusa:req REQ-FO-SAS005
func (s *SAS) HasGaps() bool { return s.CompleteActivities < s.TotalActivities }
+// buildChecklist projects activities into the x-FuSa spec §9.3 checklist
+// shape: `present` is true for both StatusComplete and StatusNA (an N/A
+// activity is not a gap — same semantics Build already uses for
+// CompleteActivities), false only for StatusIncomplete.
+//
+//fusa:req REQ-FO-SAS006
+func buildChecklist(activities []Activity) ([]ChecklistItem, Summary) {
+ items := make([]ChecklistItem, 0, len(activities))
+ var summary Summary
+ for _, a := range activities {
+ present := a.Status == StatusComplete || a.Status == StatusNA
+ items = append(items, ChecklistItem{
+ Item: a.Title,
+ Clause: a.ID,
+ Present: present,
+ Evidence: a.Evidence,
+ })
+ summary.Total++
+ if present {
+ summary.Present++
+ }
+ }
+ return items, summary
+}
+
// activitySpec defines one lifecycle activity and the evidence file that
// demonstrates its completion.
type activitySpec struct {
@@ -131,6 +180,8 @@ func Build(root, softwareLevel string) (*SAS, error) {
}
}
+ s.Checklist, s.Summary = buildChecklist(s.Activities)
+
s.Hash = computeHash(s)
return s, nil
}
@@ -142,8 +193,12 @@ func computeHash(s *SAS) string {
if err != nil {
return ""
}
- sum := sha256.Sum256(data)
- return hex.EncodeToString(sum[:])
+ canon, err := fusaops.Canonicalize(data)
+ if err != nil {
+ return ""
+ }
+ sum := sha256.Sum256(canon)
+ return fmt.Sprintf("sha256:%x", sum)
}
// Save writes the SAS to path as indented JSON.
diff --git a/sas/sas_test.go b/sas/sas_test.go
index 358d84d..eb23f9b 100644
--- a/sas/sas_test.go
+++ b/sas/sas_test.go
@@ -118,6 +118,51 @@ func TestBuildWithEvidence(t *testing.T) {
}
}
+// TestBuildHashHasAlgoPrefix verifies Hash carries the "sha256:" prefix
+// required by x-FuSa spec §2.7 for a field named "hash".
+//
+//fusa:test REQ-FO-SAS006
+func TestBuildHashHasAlgoPrefix(t *testing.T) {
+ s, err := sas.Build(t.TempDir(), "DAL-C")
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+ if !strings.HasPrefix(s.Hash, "sha256:") {
+ t.Errorf("Hash = %q, want sha256: prefix", s.Hash)
+ }
+}
+
+// TestBuildChecklistProjection verifies Checklist/Summary mirror
+// Activities/TotalActivities/CompleteActivities per x-FuSa spec §9.3.
+//
+//fusa:test REQ-FO-SAS006
+func TestBuildChecklistProjection(t *testing.T) {
+ dir := t.TempDir()
+ s, err := sas.Build(dir, "DAL-C")
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+ if len(s.Checklist) != len(s.Activities) {
+ t.Errorf("len(Checklist)=%d, want %d", len(s.Checklist), len(s.Activities))
+ }
+ if s.Summary.Total != s.TotalActivities {
+ t.Errorf("Summary.Total=%d, want %d", s.Summary.Total, s.TotalActivities)
+ }
+ if s.Summary.Present != s.CompleteActivities {
+ t.Errorf("Summary.Present=%d, want %d", s.Summary.Present, s.CompleteActivities)
+ }
+ for i, item := range s.Checklist {
+ a := s.Activities[i]
+ if item.Item != a.Title || item.Clause != a.ID || item.Evidence != a.Evidence {
+ t.Errorf("Checklist[%d] = %+v, want item/clause/evidence from %+v", i, item, a)
+ }
+ wantPresent := a.Status == sas.StatusComplete || a.Status == sas.StatusNA
+ if item.Present != wantPresent {
+ t.Errorf("Checklist[%d].Present=%v, want %v (status=%s)", i, item.Present, wantPresent, a.Status)
+ }
+ }
+}
+
//fusa:test REQ-FO-SAS002
func TestBuildHashChanges(t *testing.T) {
dir := t.TempDir()
diff --git a/sci/sci.go b/sci/sci.go
index 8aad31b..02fe459 100644
--- a/sci/sci.go
+++ b/sci/sci.go
@@ -51,18 +51,34 @@ type ConfigItem struct {
Present bool `json:"present"`
}
-// SCI is the Software Configuration Index document.
+// ArtifactRef is x-FuSa spec §9.3's promoted-schema view of an evidence
+// artefact: `file`/`hash`/`version` — a project-relative subset of the
+// richer ConfigItem/Items model, covering only KindArtefact entries that
+// are actually present (a missing artefact has no hash to report).
+//
+//fusa:req REQ-FO-SCI005
+type ArtifactRef struct {
+ File string `json:"file"`
+ Hash string `json:"hash"`
+ Version string `json:"version,omitempty"`
+}
+
+// SCI is the Software Configuration Index document. Items/ConfigItem remain
+// the primary tool+artefact+component inventory; Artifacts is the
+// project-relative file/hash projection required by x-FuSa spec §9.3,
+// derived from the same artefact scan at Build time.
//
//fusa:req REQ-FO-SCI001
type SCI struct {
- GeneratedAt time.Time `json:"generatedAt"`
- ProjectRoot string `json:"projectRoot"`
- Tool string `json:"tool"`
- ToolVersion string `json:"toolVersion"`
- GoVersion string `json:"goVersion"`
- Items []ConfigItem `json:"items"`
- TotalItems int `json:"totalItems"`
- Hash string `json:"hash"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ ProjectRoot string `json:"projectRoot"`
+ Tool string `json:"tool"`
+ ToolVersion string `json:"toolVersion"`
+ GoVersion string `json:"goVersion"`
+ Items []ConfigItem `json:"items"`
+ TotalItems int `json:"totalItems"`
+ Artifacts []ArtifactRef `json:"artifacts"`
+ Hash string `json:"hash"`
}
// knownArtefacts lists the standard FuSaOps evidence artefacts.
@@ -145,6 +161,9 @@ func Build(root string, adapters []adapter.Adapter) (*SCI, error) {
Size: size,
}
s.Items = append(s.Items, item)
+ if sum != "" {
+ s.Artifacts = append(s.Artifacts, ArtifactRef{File: art.file, Hash: "sha256:" + sum})
+ }
}
s.TotalItems = len(s.Items)
@@ -159,8 +178,12 @@ func computeHash(s *SCI) string {
if err != nil {
return ""
}
- sum := sha256.Sum256(data)
- return hex.EncodeToString(sum[:])
+ canon, err := fusaops.Canonicalize(data)
+ if err != nil {
+ return ""
+ }
+ sum := sha256.Sum256(canon)
+ return fmt.Sprintf("sha256:%x", sum)
}
// Save writes the SCI to path as indented JSON.
diff --git a/sci/sci_test.go b/sci/sci_test.go
index 49744c7..8896ade 100644
--- a/sci/sci_test.go
+++ b/sci/sci_test.go
@@ -161,6 +161,33 @@ func TestBuildWithArtefacts(t *testing.T) {
if presentCount < 2 {
t.Errorf("expected at least 2 present artefacts, got %d", presentCount)
}
+ if len(s.Artifacts) != presentCount {
+ t.Errorf("len(Artifacts)=%d, want %d (one per present artefact)", len(s.Artifacts), presentCount)
+ }
+ for _, a := range s.Artifacts {
+ if a.File == "" {
+ t.Error("Artifacts[].File must not be empty")
+ }
+ if !strings.HasPrefix(a.Hash, "sha256:") {
+ t.Errorf("Artifacts[].Hash = %q, want sha256: prefix", a.Hash)
+ }
+ }
+}
+
+// TestBuildHashHasAlgoPrefix verifies the document-level Hash carries the
+// "sha256:" prefix required by x-FuSa spec §2.7 for a field named "hash"
+// (Items[].sha256 is correctly bare hex — different field name, §2.7's other
+// rule).
+//
+//fusa:test REQ-FO-SCI005
+func TestBuildHashHasAlgoPrefix(t *testing.T) {
+ s, err := sci.Build(t.TempDir(), nil)
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+ if !strings.HasPrefix(s.Hash, "sha256:") {
+ t.Errorf("Hash = %q, want sha256: prefix", s.Hash)
+ }
}
//fusa:test REQ-FO-SCI002
diff --git a/tara.json b/tara.json
index 47f1b13..2d12709 100644
--- a/tara.json
+++ b/tara.json
@@ -1,26 +1,191 @@
{
- "format": "go-FuSa TARA v1",
- "generated_at": "2026-06-10T14:32:27.98215Z",
- "module": "github.com/SoundMatt/FuSaOps",
- "entries": [
- {
- "id": "TARA-001",
- "asset": "adapter.go",
- "threat": "Command injection from variable input enables arbitrary command execution",
- "stride": [
- "E",
- "R"
- ],
- "cwe": "CWE-78",
- "attack_vector": "Network",
- "likelihood": "Medium",
- "impact": "High",
- "security_level": 3,
- "current_control": "Use exec.Command with fixed command and sanitised args",
- "residual_risk": "Low after remediation",
- "cyber_rule_id": "CYBER005",
- "source_file": "/Users/matt/Documents/Coding/SoundMatt/FuSaOps/adapter/adapter.go",
- "source_line": 138
+ "generatedAt": "2026-07-28T18:47:06.900168Z",
+ "projectRoot": ".",
+ "tool": "fusaops",
+ "toolVersion": "1.140.0",
+ "standard": "ISO 21434:2021 Chapter 9",
+ "threats": [
+ {
+ "id": "TS-001",
+ "asset": "Software Bill of Materials (SBOM)",
+ "threatProperty": "Integrity",
+ "threat": "Attacker modifies the SBOM to hide malicious components or misrepresent software composition.",
+ "damageScenario": "False software composition evidence submitted to a certification authority.",
+ "impact": {
+ "safety": "critical",
+ "financial": "major",
+ "operational": "moderate",
+ "privacy": "negligible"
+ },
+ "attackPath": "Unauthorized write access to sbom.json or sbom-spdx.json in the release directory.",
+ "attackFeasibility": "low",
+ "riskLevel": "high",
+ "treatment": "mitigate",
+ "mitigations": [
+ "fusaops sign — HMAC artifact signing",
+ "Read-only CI output directories",
+ "Hash verification via artifact-manifest"
+ ]
+ },
+ {
+ "id": "TS-002",
+ "asset": "Test Evidence Bundle",
+ "threatProperty": "Authenticity",
+ "threat": "Insider substitutes passing test results to conceal known defects.",
+ "damageScenario": "Defective software approved for use in a safety-critical system.",
+ "impact": {
+ "safety": "critical",
+ "financial": "major",
+ "operational": "moderate",
+ "privacy": "negligible"
+ },
+ "attackPath": "Modification of .fusaops-evidence.json before certification review by a developer with repository write access.",
+ "attackFeasibility": "medium",
+ "riskLevel": "critical",
+ "treatment": "mitigate",
+ "mitigations": [
+ "HMAC signing (fusaops sign)",
+ "CI-only write access to evidence artefacts",
+ "Audit trail via fusaops audit-pack"
+ ]
+ },
+ {
+ "id": "TS-003",
+ "asset": "Language Adapter Tools (go-FuSa, c-FuSa, cpp-FuSa, etc.)",
+ "threatProperty": "Integrity",
+ "threat": "Supply-chain attacker replaces a language adapter binary with a trojaned version that suppresses findings.",
+ "damageScenario": "Safety defects not detected by the analysis pipeline; defective software ships.",
+ "impact": {
+ "safety": "critical",
+ "financial": "major",
+ "operational": "major",
+ "privacy": "negligible"
+ },
+ "attackPath": "Compromise of upstream tool registry, tampered container image, or PATH manipulation in CI.",
+ "attackFeasibility": "very-low",
+ "riskLevel": "medium",
+ "treatment": "mitigate",
+ "mitigations": [
+ "SLSA provenance verification (fusaops slsa)",
+ "Tool version pinning in Dockerfile",
+ "Hash verification in Software Configuration Index (fusaops sci)"
+ ]
+ },
+ {
+ "id": "TS-004",
+ "asset": "Aggregate Safety Report",
+ "threatProperty": "Integrity",
+ "threat": "Insider or attacker modifies the aggregate report to downgrade or remove findings.",
+ "damageScenario": "Safety defects concealed from engineering and certification review.",
+ "impact": {
+ "safety": "major",
+ "financial": "moderate",
+ "operational": "moderate",
+ "privacy": "negligible"
+ },
+ "attackPath": "Direct file modification of report artefacts before or during review by a developer with local file access.",
+ "attackFeasibility": "high",
+ "riskLevel": "high",
+ "treatment": "mitigate",
+ "mitigations": [
+ "Artifact signing (fusaops sign)",
+ "Immutable CI artefact storage",
+ "Hash manifest (artifact-manifest.json)"
+ ]
+ },
+ {
+ "id": "TS-005",
+ "asset": "Signed Release Artifacts",
+ "threatProperty": "Authenticity",
+ "threat": "Attacker gains access to the HMAC signing key and creates fraudulent signed artefacts.",
+ "damageScenario": "Fraudulent signed artefacts accepted as legitimate evidence by a certification body.",
+ "impact": {
+ "safety": "critical",
+ "financial": "major",
+ "operational": "moderate",
+ "privacy": "negligible"
+ },
+ "attackPath": "Compromise of signing key stored in CI/CD secrets, developer workstation, or key management system.",
+ "attackFeasibility": "low",
+ "riskLevel": "high",
+ "treatment": "mitigate",
+ "mitigations": [
+ "Hardware security modules for key storage",
+ "Periodic key rotation",
+ "Least-privilege access to signing secrets"
+ ]
+ },
+ {
+ "id": "TS-006",
+ "asset": "Requirement Traceability Matrix",
+ "threatProperty": "Integrity",
+ "threat": "Attacker modifies or deletes requirement trace links to conceal coverage gaps.",
+ "damageScenario": "Incomplete requirements coverage not detected; defective design reaches production.",
+ "impact": {
+ "safety": "major",
+ "financial": "moderate",
+ "operational": "moderate",
+ "privacy": "negligible"
+ },
+ "attackPath": "Unauthorized commit to the repository or direct modification of .fusaops-trace.json artefact.",
+ "attackFeasibility": "medium",
+ "riskLevel": "high",
+ "treatment": "mitigate",
+ "mitigations": [
+ "PR-gated branch protection with mandatory review",
+ "Signed commits (DCO)",
+ "Artefact hash in artifact-manifest.json"
+ ]
+ },
+ {
+ "id": "TS-007",
+ "asset": "FuSaOps Orchestration Pipeline",
+ "threatProperty": "Availability",
+ "threat": "Attacker disrupts the CI/CD pipeline preventing safety analysis from running before release.",
+ "damageScenario": "Release proceeds without required safety analysis; unreviewed defects ship.",
+ "impact": {
+ "safety": "moderate",
+ "financial": "moderate",
+ "operational": "major",
+ "privacy": "negligible"
+ },
+ "attackPath": "DoS against CI infrastructure, sabotage of build scripts, or removal of adapter tool binaries.",
+ "attackFeasibility": "medium",
+ "riskLevel": "high",
+ "treatment": "mitigate",
+ "mitigations": [
+ "Required status checks for all pull requests",
+ "Redundant CI runner configuration",
+ "Pipeline integrity monitoring"
+ ]
+ },
+ {
+ "id": "TS-008",
+ "asset": "FuSaOps Configuration (.fusaops.json)",
+ "threatProperty": "Integrity",
+ "threat": "Attacker modifies configuration to exclude safety-critical components from analysis.",
+ "damageScenario": "Known-dangerous code paths excluded from safety analysis; findings suppressed globally.",
+ "impact": {
+ "safety": "critical",
+ "financial": "moderate",
+ "operational": "moderate",
+ "privacy": "negligible"
+ },
+ "attackPath": "Unauthorized commit modifying .fusaops.json to add scan exclusions or disable adapters.",
+ "attackFeasibility": "medium",
+ "riskLevel": "critical",
+ "treatment": "mitigate",
+ "mitigations": [
+ "PR review gate with code-owner approval",
+ "Git commit signing",
+ "Configuration change alerting in CI"
+ ]
}
- ]
+ ],
+ "summary": {
+ "total": 8,
+ "critical": 2,
+ "high": 5
+ },
+ "hash": "sha256:2f181b63e0a2dc6df3c80445d3fdac6d4a17f67429c19b6934c7089f709004d5"
}
diff --git a/tara/tara.go b/tara/tara.go
index f13d865..98cefa2 100644
--- a/tara/tara.go
+++ b/tara/tara.go
@@ -10,7 +10,6 @@ package tara
import (
"crypto/sha256"
- "encoding/hex"
"encoding/json"
"fmt"
"io"
@@ -46,7 +45,7 @@ const (
FeasibilityHigh Feasibility = "high" // Well-resourced attacker with easy access.
FeasibilityMedium Feasibility = "medium" // Moderate skill and access required.
FeasibilityLow Feasibility = "low" // Significant barriers; specialised access needed.
- FeasibilityVeryLow Feasibility = "very_low" // Exceptional attacker capability required.
+ FeasibilityVeryLow Feasibility = "very-low" // Exceptional attacker capability required. (x-FuSa spec §9.2 enum spelling)
)
// RiskLevel is the computed outcome of the Impact × Feasibility risk matrix.
@@ -73,6 +72,19 @@ const (
TreatmentAccept TreatmentDecision = "accept"
)
+// ImpactRating rates a threat's damage potential across ISO 21434 Clause
+// 15.7's four SFOP categories — a threat can rate differently on each axis
+// (e.g. high safety impact, negligible privacy impact), which a single
+// generic severity cannot represent.
+//
+//fusa:req REQ-FO-TARA006
+type ImpactRating struct {
+ Safety Impact `json:"safety"`
+ Financial Impact `json:"financial"`
+ Operational Impact `json:"operational"`
+ Privacy Impact `json:"privacy"`
+}
+
// ThreatScenario is one row in the TARA, covering a single threat/asset pair.
//
//fusa:req REQ-FO-TARA001
@@ -80,36 +92,77 @@ type ThreatScenario struct {
ID string `json:"id"`
Asset string `json:"asset"`
ThreatProperty string `json:"threatProperty"`
- Description string `json:"description"`
+ Threat string `json:"threat"`
DamageScenario string `json:"damageScenario"`
- Impact Impact `json:"impact"`
+ Impact ImpactRating `json:"impact"`
AttackPath string `json:"attackPath"`
- Feasibility Feasibility `json:"feasibility"`
+ Feasibility Feasibility `json:"attackFeasibility"`
RiskLevel RiskLevel `json:"riskLevel"`
Treatment TreatmentDecision `json:"treatment"`
- Controls []string `json:"controls"`
+ Controls []string `json:"mitigations"`
+ // CWE is the applicable CWE id, when the threat maps to one (SHOULD when
+ // applicable). Left empty here: FuSaOps' 8 scenarios are supply-chain and
+ // process-integrity threats, not classic single-CWE vulnerabilities.
+ CWE string `json:"cwe,omitempty"`
+}
+
+// Summary rolls up the TARA's totals and analysis-coverage metrics.
+//
+//fusa:req REQ-FO-TARA006
+type Summary struct {
+ Total int `json:"total"`
+ Critical int `json:"critical"`
+ High int `json:"high"`
}
// TARA is the top-level threat analysis document.
//
//fusa:req REQ-FO-TARA001
type TARA struct {
- GeneratedAt time.Time `json:"generatedAt"`
- ProjectRoot string `json:"projectRoot"`
- Tool string `json:"tool"`
- ToolVersion string `json:"toolVersion"`
- Standard string `json:"standard"`
- Scenarios []ThreatScenario `json:"scenarios"`
- TotalScenarios int `json:"totalScenarios"`
- CriticalScenarios int `json:"criticalScenarios"`
- HighScenarios int `json:"highScenarios"`
- Hash string `json:"hash"`
+ GeneratedAt time.Time `json:"generatedAt"`
+ ProjectRoot string `json:"projectRoot"`
+ Tool string `json:"tool"`
+ ToolVersion string `json:"toolVersion"`
+ Standard string `json:"standard"`
+ Threats []ThreatScenario `json:"threats"`
+ Summary Summary `json:"summary"`
+ Hash string `json:"hash"`
}
// HasCritical returns true when any scenario carries a critical risk level.
//
//fusa:req REQ-FO-TARA005
-func (t *TARA) HasCritical() bool { return t.CriticalScenarios > 0 }
+func (t *TARA) HasCritical() bool { return t.Summary.Critical > 0 }
+
+// impactRank maps an Impact to a comparable integer so the highest of the
+// four SFOP axes can be found.
+func impactRank(i Impact) int {
+ switch i {
+ case ImpactCritical:
+ return 3
+ case ImpactMajor:
+ return 2
+ case ImpactModerate:
+ return 1
+ default:
+ return 0
+ }
+}
+
+// highestImpact returns the most severe of ir's four SFOP axes, which drives
+// risk determination the same way a single-axis Impact used to (ISO 21434
+// Clause 15.7: overall risk considers the worst-case damage scenario).
+//
+//fusa:req REQ-FO-TARA006
+func highestImpact(ir ImpactRating) Impact {
+ best := ir.Safety
+ for _, i := range []Impact{ir.Financial, ir.Operational, ir.Privacy} {
+ if impactRank(i) > impactRank(best) {
+ best = i
+ }
+ }
+ return best
+}
// riskMatrix maps Impact × Feasibility to a RiskLevel per ISO 21434 Table 1.
func riskMatrix(imp Impact, feas Feasibility) RiskLevel {
@@ -143,14 +196,15 @@ func riskMatrix(imp Impact, feas Feasibility) RiskLevel {
}
// scenarioSpec defines the static fields of a threat scenario; RiskLevel is
-// computed from Impact × Feasibility at Build time.
+// computed from the highest of impact's four SFOP axes × feasibility at
+// Build time.
type scenarioSpec struct {
id string
asset string
property string
description string
damageScenario string
- impact Impact
+ impact ImpactRating
attackPath string
feasibility Feasibility
treatment TreatmentDecision
@@ -166,7 +220,7 @@ var standardScenarios = []scenarioSpec{
property: "Integrity",
description: "Attacker modifies the SBOM to hide malicious components or misrepresent software composition.",
damageScenario: "False software composition evidence submitted to a certification authority.",
- impact: ImpactCritical,
+ impact: ImpactRating{Safety: ImpactCritical, Financial: ImpactMajor, Operational: ImpactModerate, Privacy: ImpactNegligible},
attackPath: "Unauthorized write access to sbom.json or sbom-spdx.json in the release directory.",
feasibility: FeasibilityLow,
treatment: TreatmentMitigate,
@@ -178,7 +232,7 @@ var standardScenarios = []scenarioSpec{
property: "Authenticity",
description: "Insider substitutes passing test results to conceal known defects.",
damageScenario: "Defective software approved for use in a safety-critical system.",
- impact: ImpactCritical,
+ impact: ImpactRating{Safety: ImpactCritical, Financial: ImpactMajor, Operational: ImpactModerate, Privacy: ImpactNegligible},
attackPath: "Modification of .fusaops-evidence.json before certification review by a developer with repository write access.",
feasibility: FeasibilityMedium,
treatment: TreatmentMitigate,
@@ -190,7 +244,7 @@ var standardScenarios = []scenarioSpec{
property: "Integrity",
description: "Supply-chain attacker replaces a language adapter binary with a trojaned version that suppresses findings.",
damageScenario: "Safety defects not detected by the analysis pipeline; defective software ships.",
- impact: ImpactCritical,
+ impact: ImpactRating{Safety: ImpactCritical, Financial: ImpactMajor, Operational: ImpactMajor, Privacy: ImpactNegligible},
attackPath: "Compromise of upstream tool registry, tampered container image, or PATH manipulation in CI.",
feasibility: FeasibilityVeryLow,
treatment: TreatmentMitigate,
@@ -202,7 +256,7 @@ var standardScenarios = []scenarioSpec{
property: "Integrity",
description: "Insider or attacker modifies the aggregate report to downgrade or remove findings.",
damageScenario: "Safety defects concealed from engineering and certification review.",
- impact: ImpactMajor,
+ impact: ImpactRating{Safety: ImpactMajor, Financial: ImpactModerate, Operational: ImpactModerate, Privacy: ImpactNegligible},
attackPath: "Direct file modification of report artefacts before or during review by a developer with local file access.",
feasibility: FeasibilityHigh,
treatment: TreatmentMitigate,
@@ -214,7 +268,7 @@ var standardScenarios = []scenarioSpec{
property: "Authenticity",
description: "Attacker gains access to the HMAC signing key and creates fraudulent signed artefacts.",
damageScenario: "Fraudulent signed artefacts accepted as legitimate evidence by a certification body.",
- impact: ImpactCritical,
+ impact: ImpactRating{Safety: ImpactCritical, Financial: ImpactMajor, Operational: ImpactModerate, Privacy: ImpactNegligible},
attackPath: "Compromise of signing key stored in CI/CD secrets, developer workstation, or key management system.",
feasibility: FeasibilityLow,
treatment: TreatmentMitigate,
@@ -226,7 +280,7 @@ var standardScenarios = []scenarioSpec{
property: "Integrity",
description: "Attacker modifies or deletes requirement trace links to conceal coverage gaps.",
damageScenario: "Incomplete requirements coverage not detected; defective design reaches production.",
- impact: ImpactMajor,
+ impact: ImpactRating{Safety: ImpactMajor, Financial: ImpactModerate, Operational: ImpactModerate, Privacy: ImpactNegligible},
attackPath: "Unauthorized commit to the repository or direct modification of .fusaops-trace.json artefact.",
feasibility: FeasibilityMedium,
treatment: TreatmentMitigate,
@@ -238,7 +292,7 @@ var standardScenarios = []scenarioSpec{
property: "Availability",
description: "Attacker disrupts the CI/CD pipeline preventing safety analysis from running before release.",
damageScenario: "Release proceeds without required safety analysis; unreviewed defects ship.",
- impact: ImpactMajor,
+ impact: ImpactRating{Safety: ImpactModerate, Financial: ImpactModerate, Operational: ImpactMajor, Privacy: ImpactNegligible},
attackPath: "DoS against CI infrastructure, sabotage of build scripts, or removal of adapter tool binaries.",
feasibility: FeasibilityMedium,
treatment: TreatmentMitigate,
@@ -250,7 +304,7 @@ var standardScenarios = []scenarioSpec{
property: "Integrity",
description: "Attacker modifies configuration to exclude safety-critical components from analysis.",
damageScenario: "Known-dangerous code paths excluded from safety analysis; findings suppressed globally.",
- impact: ImpactMajor,
+ impact: ImpactRating{Safety: ImpactCritical, Financial: ImpactModerate, Operational: ImpactModerate, Privacy: ImpactNegligible},
attackPath: "Unauthorized commit modifying .fusaops.json to add scan exclusions or disable adapters.",
feasibility: FeasibilityMedium,
treatment: TreatmentMitigate,
@@ -271,12 +325,12 @@ func Build(root string) (*TARA, error) {
}
for _, spec := range standardScenarios {
- risk := riskMatrix(spec.impact, spec.feasibility)
+ risk := riskMatrix(highestImpact(spec.impact), spec.feasibility)
ts := ThreatScenario{
ID: spec.id,
Asset: spec.asset,
ThreatProperty: spec.property,
- Description: spec.description,
+ Threat: spec.description,
DamageScenario: spec.damageScenario,
Impact: spec.impact,
AttackPath: spec.attackPath,
@@ -285,13 +339,13 @@ func Build(root string) (*TARA, error) {
Treatment: spec.treatment,
Controls: spec.controls,
}
- t.Scenarios = append(t.Scenarios, ts)
- t.TotalScenarios++
+ t.Threats = append(t.Threats, ts)
+ t.Summary.Total++
switch risk {
case RiskCritical:
- t.CriticalScenarios++
+ t.Summary.Critical++
case RiskHigh:
- t.HighScenarios++
+ t.Summary.High++
}
}
@@ -306,8 +360,12 @@ func computeHash(t *TARA) string {
if err != nil {
return ""
}
- sum := sha256.Sum256(data)
- return hex.EncodeToString(sum[:])
+ canon, err := fusaops.Canonicalize(data)
+ if err != nil {
+ return ""
+ }
+ sum := sha256.Sum256(canon)
+ return fmt.Sprintf("sha256:%x", sum)
}
// Save writes the TARA to path as indented JSON.
@@ -367,12 +425,13 @@ func renderText(w io.Writer, t *TARA) error {
fmt.Fprintf(w, "Tool: %s v%s\n", t.Tool, t.ToolVersion)
fmt.Fprintf(w, "Generated: %s\n", t.GeneratedAt.Format("2006-01-02T15:04:05Z"))
fmt.Fprintf(w, "Scenarios: %d total, %d critical, %d high\n\n",
- t.TotalScenarios, t.CriticalScenarios, t.HighScenarios)
+ t.Summary.Total, t.Summary.Critical, t.Summary.High)
- for _, s := range t.Scenarios {
+ for _, s := range t.Threats {
fmt.Fprintf(w, "[%s] %s — %s (%s)\n", s.RiskLevel, s.ID, s.Asset, s.ThreatProperty)
- fmt.Fprintf(w, " Impact: %-12s Feasibility: %s\n", s.Impact, s.Feasibility)
- fmt.Fprintf(w, " Threat: %s\n", s.Description)
+ fmt.Fprintf(w, " Impact: safety=%s financial=%s operational=%s privacy=%s Feasibility: %s\n",
+ s.Impact.Safety, s.Impact.Financial, s.Impact.Operational, s.Impact.Privacy, s.Feasibility)
+ fmt.Fprintf(w, " Threat: %s\n", s.Threat)
fmt.Fprintf(w, " Damage: %s\n", s.DamageScenario)
fmt.Fprintf(w, " Treatment: %s\n", s.Treatment)
for _, c := range s.Controls {
@@ -385,7 +444,7 @@ func renderText(w io.Writer, t *TARA) error {
fmt.Fprintf(w, "Integrity: %s\n", t.Hash)
}
if t.HasCritical() {
- fmt.Fprintf(w, "\nCRITICAL RISK: %d scenario(s) require immediate treatment.\n", t.CriticalScenarios)
+ fmt.Fprintf(w, "\nCRITICAL RISK: %d scenario(s) require immediate treatment.\n", t.Summary.Critical)
}
return nil
}
diff --git a/tara/tara_test.go b/tara/tara_test.go
index 0df3a42..037f003 100644
--- a/tara/tara_test.go
+++ b/tara/tara_test.go
@@ -75,11 +75,11 @@ func TestBuildReturnsScenarios(t *testing.T) {
if err != nil {
t.Fatalf("Build: %v", err)
}
- if tr.TotalScenarios == 0 {
+ if tr.Summary.Total == 0 {
t.Error("Build must return at least one scenario")
}
- if len(tr.Scenarios) != tr.TotalScenarios {
- t.Errorf("len(Scenarios)=%d, TotalScenarios=%d", len(tr.Scenarios), tr.TotalScenarios)
+ if len(tr.Threats) != tr.Summary.Total {
+ t.Errorf("len(Threats)=%d, Summary.Total=%d", len(tr.Threats), tr.Summary.Total)
}
}
@@ -90,7 +90,7 @@ func TestBuildScenarioIDs(t *testing.T) {
t.Fatalf("Build: %v", err)
}
seen := map[string]bool{}
- for _, s := range tr.Scenarios {
+ for _, s := range tr.Threats {
if s.ID == "" {
t.Error("scenario has empty ID")
}
@@ -107,6 +107,9 @@ func TestBuildScenarioIDs(t *testing.T) {
if len(s.Controls) == 0 {
t.Errorf("%s: controls must not be empty", s.ID)
}
+ if s.Impact.Safety == "" || s.Impact.Financial == "" || s.Impact.Operational == "" || s.Impact.Privacy == "" {
+ t.Errorf("%s: all four SFOP impact axes must be set", s.ID)
+ }
}
}
@@ -117,7 +120,7 @@ func TestBuildCounters(t *testing.T) {
t.Fatalf("Build: %v", err)
}
var critCount, highCount int
- for _, s := range tr.Scenarios {
+ for _, s := range tr.Threats {
switch s.RiskLevel {
case tara.RiskCritical:
critCount++
@@ -125,11 +128,11 @@ func TestBuildCounters(t *testing.T) {
highCount++
}
}
- if tr.CriticalScenarios != critCount {
- t.Errorf("CriticalScenarios=%d, counted %d", tr.CriticalScenarios, critCount)
+ if tr.Summary.Critical != critCount {
+ t.Errorf("Summary.Critical=%d, counted %d", tr.Summary.Critical, critCount)
}
- if tr.HighScenarios != highCount {
- t.Errorf("HighScenarios=%d, counted %d", tr.HighScenarios, highCount)
+ if tr.Summary.High != highCount {
+ t.Errorf("Summary.High=%d, counted %d", tr.Summary.High, highCount)
}
}
@@ -160,6 +163,20 @@ func TestBuildMetadata(t *testing.T) {
}
}
+// TestBuildHashHasAlgoPrefix verifies Hash carries the "sha256:" prefix
+// required by x-FuSa spec §2.7 for a field named "hash".
+//
+//fusa:test REQ-FO-TARA006
+func TestBuildHashHasAlgoPrefix(t *testing.T) {
+ tr, err := tara.Build(t.TempDir())
+ if err != nil {
+ t.Fatalf("Build: %v", err)
+ }
+ if !strings.HasPrefix(tr.Hash, "sha256:") {
+ t.Errorf("Hash = %q, want sha256: prefix", tr.Hash)
+ }
+}
+
//fusa:test REQ-FO-TARA002
func TestBuildHashChanges(t *testing.T) {
tr1, _ := tara.Build(t.TempDir())
@@ -177,10 +194,10 @@ func TestHasCritical(t *testing.T) {
if err != nil {
t.Fatalf("Build: %v", err)
}
- want := tr.CriticalScenarios > 0
+ want := tr.Summary.Critical > 0
if tr.HasCritical() != want {
- t.Errorf("HasCritical()=%v, want %v (CriticalScenarios=%d)",
- tr.HasCritical(), want, tr.CriticalScenarios)
+ t.Errorf("HasCritical()=%v, want %v (Summary.Critical=%d)",
+ tr.HasCritical(), want, tr.Summary.Critical)
}
}
@@ -202,8 +219,8 @@ func TestSaveLoad(t *testing.T) {
if loaded.Hash != tr.Hash {
t.Errorf("Hash mismatch after round-trip: got %q, want %q", loaded.Hash, tr.Hash)
}
- if loaded.TotalScenarios != tr.TotalScenarios {
- t.Errorf("TotalScenarios mismatch: got %d, want %d", loaded.TotalScenarios, tr.TotalScenarios)
+ if loaded.Summary.Total != tr.Summary.Total {
+ t.Errorf("Summary.Total mismatch: got %d, want %d", loaded.Summary.Total, tr.Summary.Total)
}
}
@@ -265,8 +282,8 @@ func TestRenderJSON(t *testing.T) {
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
t.Fatalf("JSON unmarshal: %v", err)
}
- if got.TotalScenarios != tr.TotalScenarios {
- t.Errorf("TotalScenarios=%d, want %d", got.TotalScenarios, tr.TotalScenarios)
+ if got.Summary.Total != tr.Summary.Total {
+ t.Errorf("Summary.Total=%d, want %d", got.Summary.Total, tr.Summary.Total)
}
}