diff --git a/.fusa-hara.json b/.fusa-hara.json index a897111..c73636b 100644 --- a/.fusa-hara.json +++ b/.fusa-hara.json @@ -29,7 +29,7 @@ "severity": "S2", "exposure": "E4", "controllability": "C2", - "asil": "ASIL-C" + "asil": "ASIL-B" }, "safetyGoals": [ "SG-001" @@ -47,7 +47,7 @@ "severity": "S2", "exposure": "E3", "controllability": "C2", - "asil": "ASIL-B" + "asil": "ASIL-A" }, "safetyGoals": [ "SG-002" @@ -64,7 +64,7 @@ "severity": "S2", "exposure": "E3", "controllability": "C1", - "asil": "ASIL-A" + "asil": "QM" }, "safetyGoals": [ "SG-003" @@ -99,7 +99,7 @@ "severity": "S2", "exposure": "E3", "controllability": "C2", - "asil": "ASIL-B" + "asil": "ASIL-A" }, "safetyGoals": [ "SG-005" @@ -113,7 +113,7 @@ "hazards": [ "H-001" ], - "asil": "ASIL-C", + "asil": "ASIL-B", "safeState": "Halt analysis and exit non-zero if rule engine cannot complete", "fssrRefs": [ "REQ-LINT001", @@ -128,7 +128,7 @@ "hazards": [ "H-002" ], - "asil": "ASIL-B", + "asil": "ASIL-A", "safeState": "Report configuration mismatch as ERROR finding", "fssrRefs": [ "REQ-HARA006" @@ -140,7 +140,7 @@ "hazards": [ "H-003" ], - "asil": "ASIL-A", + "asil": "QM", "safeState": "Exit code 1", "fssrRefs": [ "REQ-CLI001", @@ -166,7 +166,7 @@ "hazards": [ "H-005" ], - "asil": "ASIL-B", + "asil": "ASIL-A", "safeState": "Emit WARNING and continue", "fssrRefs": [ "REQ-CONFIG001" diff --git a/.fusa.json b/.fusa.json index 8e09b99..d2bacc8 100644 --- a/.fusa.json +++ b/.fusa.json @@ -1,11 +1,14 @@ { "version": "1", + "configVersion": "1", "project": { "name": "go-FuSa", "module": "github.com/SoundMatt/go-FuSa", "standard": "ISO26262", "asil": "ASIL-B" }, + "standard": "ISO26262", + "asil": "ASIL-B", "rules": {}, "report": { "format": "text" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bdf4a0b..e80b506 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -201,7 +201,19 @@ jobs: run: go build -o gofusa ./cmd/gofusa - name: Run gofusa check --format sarif - run: ./gofusa check --format sarif --output results.sarif || true + run: | + set +e + ./gofusa check --format sarif --output results.sarif + code=$? + set -e + # Exit 0 (clean) and 1 (findings present, gate fail) are both + # expected and still produce a valid SARIF to upload. Any higher + # exit code is a crash/usage error and must fail the job so a + # stale or empty results.sarif is not silently uploaded. + if [ "$code" -gt 1 ]; then + echo "gofusa check failed with exit $code" >&2 + exit "$code" + fi - name: Upload SARIF to GitHub Code Scanning uses: github/codeql-action/upload-sarif@v3 diff --git a/.gitignore b/.gitignore index 09cf6a2..0a52772 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ CLAUDE.md /.fusa-cache/ /.claude/ /cmd/gofusa/safety-case.json +/check-report.json +/results.sarif diff --git a/CHANGELOG.md b/CHANGELOG.md index 0c40f14..5a299fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,62 @@ Dates reference the merged commit timestamp. ## [Unreleased] +## v0.48.0 — 2026-07-30 (fix ISO 26262-3 Table 4 ASIL determination + audit remediation) + +### Fixed +- **`hara.DetermineASIL` mis-implemented ISO 26262-3:2018 Table 4** (Critical): + the hand-written S×E×C lookup table was wrong in 8 of 12 S2 cells and 11 of + 12 S3 cells, always *inflating* the rating relative to the standard's + additive rule. Replaced with the reproducible additive model the standard + actually specifies: points = S(1–3) + E(1–4) + C(1–3), with ≤6 → QM, 7 → A, + 8 → B, 9 → C, 10 → D (ASIL D reachable only at the single S3+E4+C3 cell). + `hara/hara_test.go`'s golden values, which had locked in the inflated + results, are corrected to match. As a direct consequence, this repo's own + dogfooded `.fusa-hara.json` re-derives all five hazards (H-001..H-005) + under the corrected rule — the highest ASIL among them drops from the + previously-claimed ASIL-C to **ASIL-B** (e.g. H-001's S2/E4/C2 rating, + previously miscomputed as ASIL-C, is ASIL-B under the correct additive + sum of 8). +- **`qualify.computeHash` was not reproducible**: it hashed the live + `generatedAt` timestamp (so the "integrity" hash changed on every run of + identical results) and did not sort `Results` before hashing (so + re-ordered-but-identical results produced a different digest). The hash + now excludes `generatedAt` entirely, canonicalizes via + `fusa.CanonicalizeJSON`, and sorts `results[]` by case name first, so the + digest is stable for a given qualification outcome. +- **Legacy `.fusa.json` shape**: the repo's own config had drifted to the + pre-`configVersion` shape (bare `"version": "1"`, standard nested only + under `project.standard`, uppercase `"ISO26262"` id). Normalized to the + current canonical shape — `configVersion` plus top-level `standard`/`asil` + fields alongside the legacy nested ones, for both current and older + config-loader code paths. +- **Stale committed `check-report.json`**: removed a stale, plain-text + `check-report.json` (baked in with an absolute local filesystem path) that + had been committed to the repo by mistake. Added `/check-report.json` and + `/results.sarif` to `.gitignore` so generated reports can't be + re-committed by accident. +- **CI SARIF self-scan was masked by `|| true`**: `gofusa check --format + sarif` failures (including genuine crashes/usage errors, not just a + nonzero "findings present" gate exit) were unconditionally swallowed + before the SARIF upload step, so a broken self-scan could never fail CI. + Now only exit codes 0 (clean) and 1 (findings present) are treated as + producing a valid SARIF to upload; anything higher fails the job. + +### Security +- **`impact.changedFiles` git argument injection**: `git diff` was invoked + with `--from`/`--to` refs interpolated directly into the argument list + with no `--` pathspec separator, so a ref value beginning with `-` could + be interpreted by git as an option rather than a revision. Refs starting + with `-` are now rejected outright, and a `--` separator is always placed + before the revision range. + +### Not fixed (by design) +- **`normalizeMessage` still omits Unicode NFC normalization** — the + audit's low-severity finding here is deliberately left unpatched: a + correct fix needs `golang.org/x/text`, and this repo's std-lib-only + convention (go.mod has zero dependencies) takes priority over closing this + one low-severity gap. + ## v0.47.0 — 2026-07-28 (declare x-FuSa spec v1.15.2 conformance) ### Changed diff --git a/README.md b/README.md index 9ced98c..7c30da4 100644 --- a/README.md +++ b/README.md @@ -285,7 +285,7 @@ docker build -t go-fusa . docker run --rm -v "$(pwd)":/project go-fusa check ``` -Published tags: `latest`, `0.47`, `0.47.0` (and matching semver for every release). +Published tags: `latest`, `0.48`, `0.48.0` (and matching semver for every release). ## Standards coverage diff --git a/check-report.json b/check-report.json deleted file mode 100644 index e9fc988..0000000 --- a/check-report.json +++ /dev/null @@ -1,157 +0,0 @@ -go-FuSa Safety Report -Generated: 2026-06-08 18:02:54 UTC -Project: /Users/matt/Documents/Coding/SoundMatt/go-Fusa - -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/analyze/analyze_test.go:265:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/boundary/boundary.go:377:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/boundary/boundary_test.go:304:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/cmd_init.go:47:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/cmd_safetycase.go:134:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/cmd_version.go:11:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/e2e_test.go:50:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/e2e_test.go:62:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/e2e_test.go:69:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/e2e_test.go:77:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:240:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:253:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:361:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:389:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:416:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:471:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/cmd/gofusa/main_test.go:518:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/config/config_test.go:68:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/config/config_test.go:81:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/config/config_test.go:169:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/config/config_test.go:213:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/rules.go:34:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/rules.go:60:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/rules.go:86:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/rules.go:110:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/rules.go:141:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fmea/fmea.go:373:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fmea/fmea_test.go:192:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fmea/fmea_test.go:477:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fusa_test.go:50:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/lint/lint_test.go:274:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/qualify/qualify.go:223:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/qualify/qualify_test.go:137:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/qualify/qualify_test.go:148:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release.go:415:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release.go:462:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release.go:487:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release_test.go:91:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release_test.go:147:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release_test.go:263:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/release/release_test.go:279:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/text.go:16:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/text.go:22:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/text.go:39:7) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/text.go:43:8) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/text.go:50:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/text.go:59:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/safetycase/safetycase.go:237:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/safetycase/safetycase.go:428:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/safetycase/safetycase_test.go:427:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/template/template.go:54:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/testutil/testutil_test.go:103:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:269:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:275:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:306:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:310:7) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:327:6) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:331:7) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace.go:365:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace_test.go:51:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace_test.go:66:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/trace/trace_test.go:369:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/verify/verify.go:198:5) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/verify/verify_test.go:56:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/verify/verify_test.go:140:2) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT001 return value discarded with blank identifier in multi-value call (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/verify/verify_test.go:254:3) - → assign all return values to named variables and handle each explicitly -[WARNING] LINT002 panic() call detected (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/engine.go:66:3) - → replace panic with an explicit error return; document any remaining panic usage -[INFO] LINT003 recover() call inventoried — verify it is inside a deferred function (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/engine_test.go:63:13) - → ensure recover() is called only inside a function passed to defer -[INFO] LINT006 package-level var Default introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/engine/engine.go:160:5) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) -[INFO] LINT006 package-level var ErrNoConfig introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fusa.go:20:2) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) -[INFO] LINT006 package-level var ErrInvalidConfig introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fusa.go:24:2) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) -[INFO] LINT006 package-level var ErrCheckFailed introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/fusa.go:28:2) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) -[INFO] LINT006 package-level var minimalBase introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/qualify/cases.go:4:5) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) -[INFO] LINT006 package-level var builtinCases introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/qualify/cases.go:37:5) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) -[INFO] LINT006 package-level var testFindings introduces global mutable state (/Users/matt/Documents/Coding/SoundMatt/go-Fusa/report/report_test.go:16:5) - → prefer passing state explicitly; document justification for any global variable (registries, once-initialised singletons are acceptable) - -Summary: 75 total 0 errors 67 warnings 8 infos -Result: PASS diff --git a/cmd/gofusa/cmd_v021_test.go b/cmd/gofusa/cmd_v021_test.go index 07c2dbe..9466793 100644 --- a/cmd/gofusa/cmd_v021_test.go +++ b/cmd/gofusa/cmd_v021_test.go @@ -133,8 +133,8 @@ func TestRunHara_ASIL(t *testing.T) { t.Fatalf("hara asil: exit %d, stderr: %s", code, stderr.String()) } out := stdout.String() - if !strings.Contains(out, "ASIL-C") { - t.Errorf("expected ASIL-C for S2/E4/C2, got: %s", out) + if !strings.Contains(out, "ASIL-B") { + t.Errorf("expected ASIL-B for S2/E4/C2 (2+4+2=8), got: %s", out) } } diff --git a/docs/tool-safety-manual.md b/docs/tool-safety-manual.md index 9351769..31bb9bb 100644 --- a/docs/tool-safety-manual.md +++ b/docs/tool-safety-manual.md @@ -1,6 +1,6 @@ # go-FuSa Tool Safety Manual -**Version:** 0.47.0 +**Version:** 0.48.0 **Module:** `github.com/SoundMatt/go-FuSa` **License:** Mozilla Public License 2.0 **Standards addressed:** ISO 26262, IEC 61508, ISO 21434, DO-178C diff --git a/fusa.go b/fusa.go index 134a5ef..6c5c3f6 100644 --- a/fusa.go +++ b/fusa.go @@ -23,7 +23,7 @@ import ( ) // Version is the current release of go-FuSa. -const Version = "0.47.0" +const Version = "0.48.0" // SpecVersion is the x-FuSa spec version this release implements. const SpecVersion = "1.15.2" diff --git a/hara/hara.go b/hara/hara.go index 909f3e0..94dfd41 100644 --- a/hara/hara.go +++ b/hara/hara.go @@ -240,77 +240,31 @@ func BuildCompleteness(h *HARA, reqIDs map[string]bool) Completeness { // //fusa:req REQ-HARA006 func DetermineASIL(s Severity, e Exposure, c Controllability) ASIL { - // S0 always QM - if s == SeverityS0 || s == "" { + // ISO 26262-3:2018 Table 4 is reproducible additively: assign each + // rated class a weight (S1=1,S2=2,S3=3; E1..E4=1..4; C1..C3=1..3), sum + // them, and map the total (≤6→QM, 7→A, 8→B, 9→C, 10→D). ASIL D is + // therefore reached only at the single cell S3+E4+C3 (3+4+3=10). + sv := map[Severity]int{SeverityS1: 1, SeverityS2: 2, SeverityS3: 3}[s] + ev := map[Exposure]int{ExposureE1: 1, ExposureE2: 2, ExposureE3: 3, ExposureE4: 4}[e] + cv := map[Controllability]int{ControllabilityC1: 1, ControllabilityC2: 2, ControllabilityC3: 3}[c] + + // S0/E0/C0 (and any unrated/unknown class) always yield QM. + if sv == 0 || ev == 0 || cv == 0 { return ASILQM } - // E0 always QM - if e == ExposureE0 || e == "" { + + switch sv + ev + cv { + case 7: + return ASILA + case 8: + return ASILB + case 9: + return ASILC + case 10: + return ASILD + default: // sum ≤ 6 return ASILQM } - - type key struct { - s Severity - e Exposure - c Controllability - } - table := map[key]ASIL{ - // S1 - {SeverityS1, ExposureE1, ControllabilityC0}: ASILQM, - {SeverityS1, ExposureE1, ControllabilityC1}: ASILQM, - {SeverityS1, ExposureE1, ControllabilityC2}: ASILQM, - {SeverityS1, ExposureE1, ControllabilityC3}: ASILQM, - {SeverityS1, ExposureE2, ControllabilityC0}: ASILQM, - {SeverityS1, ExposureE2, ControllabilityC1}: ASILQM, - {SeverityS1, ExposureE2, ControllabilityC2}: ASILQM, - {SeverityS1, ExposureE2, ControllabilityC3}: ASILQM, - {SeverityS1, ExposureE3, ControllabilityC0}: ASILQM, - {SeverityS1, ExposureE3, ControllabilityC1}: ASILQM, - {SeverityS1, ExposureE3, ControllabilityC2}: ASILQM, - {SeverityS1, ExposureE3, ControllabilityC3}: ASILA, - {SeverityS1, ExposureE4, ControllabilityC0}: ASILQM, - {SeverityS1, ExposureE4, ControllabilityC1}: ASILQM, - {SeverityS1, ExposureE4, ControllabilityC2}: ASILA, - {SeverityS1, ExposureE4, ControllabilityC3}: ASILB, - // S2 - {SeverityS2, ExposureE1, ControllabilityC0}: ASILQM, - {SeverityS2, ExposureE1, ControllabilityC1}: ASILQM, - {SeverityS2, ExposureE1, ControllabilityC2}: ASILQM, - {SeverityS2, ExposureE1, ControllabilityC3}: ASILQM, - {SeverityS2, ExposureE2, ControllabilityC0}: ASILQM, - {SeverityS2, ExposureE2, ControllabilityC1}: ASILQM, - {SeverityS2, ExposureE2, ControllabilityC2}: ASILA, - {SeverityS2, ExposureE2, ControllabilityC3}: ASILB, - {SeverityS2, ExposureE3, ControllabilityC0}: ASILQM, - {SeverityS2, ExposureE3, ControllabilityC1}: ASILA, - {SeverityS2, ExposureE3, ControllabilityC2}: ASILB, - {SeverityS2, ExposureE3, ControllabilityC3}: ASILC, - {SeverityS2, ExposureE4, ControllabilityC0}: ASILA, - {SeverityS2, ExposureE4, ControllabilityC1}: ASILB, - {SeverityS2, ExposureE4, ControllabilityC2}: ASILC, - {SeverityS2, ExposureE4, ControllabilityC3}: ASILD, - // S3 - {SeverityS3, ExposureE1, ControllabilityC0}: ASILQM, - {SeverityS3, ExposureE1, ControllabilityC1}: ASILA, - {SeverityS3, ExposureE1, ControllabilityC2}: ASILB, - {SeverityS3, ExposureE1, ControllabilityC3}: ASILC, - {SeverityS3, ExposureE2, ControllabilityC0}: ASILA, - {SeverityS3, ExposureE2, ControllabilityC1}: ASILB, - {SeverityS3, ExposureE2, ControllabilityC2}: ASILC, - {SeverityS3, ExposureE2, ControllabilityC3}: ASILD, - {SeverityS3, ExposureE3, ControllabilityC0}: ASILB, - {SeverityS3, ExposureE3, ControllabilityC1}: ASILC, - {SeverityS3, ExposureE3, ControllabilityC2}: ASILD, - {SeverityS3, ExposureE3, ControllabilityC3}: ASILD, - {SeverityS3, ExposureE4, ControllabilityC0}: ASILC, - {SeverityS3, ExposureE4, ControllabilityC1}: ASILD, - {SeverityS3, ExposureE4, ControllabilityC2}: ASILD, - {SeverityS3, ExposureE4, ControllabilityC3}: ASILD, - } - if a, ok := table[key{s, e, c}]; ok { - return a - } - return ASILQM } // ─── Load / Save ────────────────────────────────────────────────────────────── diff --git a/hara/hara_test.go b/hara/hara_test.go index 19979eb..9e19dbf 100644 --- a/hara/hara_test.go +++ b/hara/hara_test.go @@ -29,24 +29,23 @@ func TestDetermineASIL_Table4(t *testing.T) { {hara.SeverityS0, hara.ExposureE4, hara.ControllabilityC3, hara.ASILQM}, // E0 always QM {hara.SeverityS3, hara.ExposureE0, hara.ControllabilityC3, hara.ASILQM}, - // S1 spot checks - {hara.SeverityS1, hara.ExposureE4, hara.ControllabilityC3, hara.ASILB}, - {hara.SeverityS1, hara.ExposureE4, hara.ControllabilityC2, hara.ASILA}, - {hara.SeverityS1, hara.ExposureE4, hara.ControllabilityC1, hara.ASILQM}, - // S2 spot checks - {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC3, hara.ASILD}, - {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC2, hara.ASILC}, - {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC1, hara.ASILB}, - {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC0, hara.ASILA}, - {hara.SeverityS2, hara.ExposureE3, hara.ControllabilityC2, hara.ASILB}, - // S3 spot checks - {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC0, hara.ASILC}, - {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC1, hara.ASILD}, - {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC2, hara.ASILD}, - {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC3, hara.ASILD}, - {hara.SeverityS3, hara.ExposureE1, hara.ControllabilityC0, hara.ASILQM}, - {hara.SeverityS3, hara.ExposureE1, hara.ControllabilityC1, hara.ASILA}, - {hara.SeverityS3, hara.ExposureE1, hara.ControllabilityC3, hara.ASILC}, + // C0 always QM + {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC0, hara.ASILQM}, + // S1 spot checks (1 + E + C) + {hara.SeverityS1, hara.ExposureE4, hara.ControllabilityC3, hara.ASILB}, // 8 + {hara.SeverityS1, hara.ExposureE4, hara.ControllabilityC2, hara.ASILA}, // 7 + {hara.SeverityS1, hara.ExposureE4, hara.ControllabilityC1, hara.ASILQM}, // 6 + // S2 spot checks (2 + E + C) + {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC3, hara.ASILC}, // 9 + {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC2, hara.ASILB}, // 8 + {hara.SeverityS2, hara.ExposureE4, hara.ControllabilityC1, hara.ASILA}, // 7 + {hara.SeverityS2, hara.ExposureE3, hara.ControllabilityC2, hara.ASILA}, // 7 + // S3 spot checks (3 + E + C) + {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC1, hara.ASILB}, // 8 + {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC2, hara.ASILC}, // 9 + {hara.SeverityS3, hara.ExposureE4, hara.ControllabilityC3, hara.ASILD}, // 10 (only ASIL-D cell) + {hara.SeverityS3, hara.ExposureE1, hara.ControllabilityC1, hara.ASILQM}, // 5 + {hara.SeverityS3, hara.ExposureE1, hara.ControllabilityC3, hara.ASILA}, // 7 // Unknown combo falls back to QM {hara.Severity("SX"), hara.ExposureE4, hara.ControllabilityC3, hara.ASILQM}, } @@ -59,6 +58,54 @@ func TestDetermineASIL_Table4(t *testing.T) { } } +// TestDetermineASIL_Exhaustive verifies every rated S×E×C cell against the +// additive ISO 26262-3:2018 Table 4 model, and that every C0 cell is QM. +// +//fusa:test REQ-HARA006 +func TestDetermineASIL_Exhaustive(t *testing.T) { + sVal := map[hara.Severity]int{hara.SeverityS1: 1, hara.SeverityS2: 2, hara.SeverityS3: 3} + eVal := map[hara.Exposure]int{hara.ExposureE1: 1, hara.ExposureE2: 2, hara.ExposureE3: 3, hara.ExposureE4: 4} + cVal := map[hara.Controllability]int{hara.ControllabilityC1: 1, hara.ControllabilityC2: 2, hara.ControllabilityC3: 3} + + fromSum := func(n int) hara.ASIL { + switch n { + case 7: + return hara.ASILA + case 8: + return hara.ASILB + case 9: + return hara.ASILC + case 10: + return hara.ASILD + default: + return hara.ASILQM + } + } + + dCells := 0 + for s, sv := range sVal { + for e, ev := range eVal { + // Rated controllability C1..C3. + for c, cv := range cVal { + want := fromSum(sv + ev + cv) + if want == hara.ASILD { + dCells++ + } + if got := hara.DetermineASIL(s, e, c); got != want { + t.Errorf("DetermineASIL(%s,%s,%s) = %s, want %s", s, e, c, got, want) + } + } + // C0 (unrated controllability) is always QM. + if got := hara.DetermineASIL(s, e, hara.ControllabilityC0); got != hara.ASILQM { + t.Errorf("DetermineASIL(%s,%s,C0) = %s, want QM", s, e, got) + } + } + } + if dCells != 1 { + t.Errorf("expected exactly one ASIL-D cell (S3+E4+C3), found %d", dCells) + } +} + //fusa:test REQ-HARA006 func TestDetermineASIL_EmptySeverity(t *testing.T) { if got := hara.DetermineASIL("", hara.ExposureE4, hara.ControllabilityC3); got != hara.ASILQM { @@ -170,13 +217,13 @@ func TestValidate_Complete(t *testing.T) { Severity: hara.SeverityS2, Exposure: hara.ExposureE3, Controllability: hara.ControllabilityC2, - ASIL: hara.ASILB, + ASIL: hara.ASILA, // S2+E3+C2 = 7 → ASIL-A }, SafetyGoals: []string{"SG-001"}, }, }, SafetyGoals: []hara.SafetyGoal{ - {ID: "SG-001", Description: "goal", ASIL: hara.ASILB, FSSRRefs: []string{"REQ-TEST001"}}, + {ID: "SG-001", Description: "goal", ASIL: hara.ASILA, FSSRRefs: []string{"REQ-TEST001"}}, }, } findings := hara.Validate(h) @@ -628,7 +675,7 @@ func TestValidateASIL_MatchingASILNotFlagged(t *testing.T) { ID: "H-001", Risk: hara.RiskRating{ Severity: hara.SeverityS2, Exposure: hara.ExposureE4, Controllability: hara.ControllabilityC2, - ASIL: hara.ASILC, // S2×E4×C2 derives ASIL-C — matches + ASIL: hara.ASILB, // S2+E4+C2 = 8 → ASIL-B — matches }, }}} if findings := hara.ValidateASIL(h); len(findings) != 0 { @@ -697,7 +744,7 @@ func TestHARA008_FiresOnMismatchedASIL(t *testing.T) { //fusa:test REQ-HARA024 func TestHARA008_SilentWhenASILMatches(t *testing.T) { dir := t.TempDir() - writeHARAWithASIL(t, dir, hara.ASILC) // S2×E4×C2 genuinely derives ASIL-C + writeHARAWithASIL(t, dir, hara.ASILB) // S2+E4+C2 = 8 genuinely derives ASIL-B if findingsForRule(t, dir, "HARA008") { t.Error("HARA008 should not fire when risk.asil matches DetermineASIL(S,E,C)") } diff --git a/impact/impact.go b/impact/impact.go index a8895db..2f0a75f 100644 --- a/impact/impact.go +++ b/impact/impact.go @@ -200,13 +200,20 @@ func Analyse(projectRoot, fromRef, toRef string) (*Report, error) { // changedFiles runs git diff and returns the changed file list. func changedFiles(projectRoot, fromRef, toRef string) ([]FileChange, error) { + // Reject refs beginning with "-" so a crafted value cannot be + // interpreted by git as an option rather than a revision. + for _, ref := range []string{fromRef, toRef} { + if strings.HasPrefix(ref, "-") { + return nil, fmt.Errorf("impact: invalid ref %q: must not begin with '-'", ref) + } + } var args []string if fromRef == "" && toRef == "" { - args = []string{"diff", "--name-status", "HEAD"} + args = []string{"diff", "--name-status", "HEAD", "--"} } else if toRef == "" { - args = []string{"diff", "--name-status", fromRef + "..HEAD"} + args = []string{"diff", "--name-status", fromRef + "..HEAD", "--"} } else { - args = []string{"diff", "--name-status", fromRef + ".." + toRef} + args = []string{"diff", "--name-status", fromRef + ".." + toRef, "--"} } cmd := exec.CommandContext(context.Background(), "git", args...) diff --git a/qualify/qualify.go b/qualify/qualify.go index 618a3ed..823e73c 100644 --- a/qualify/qualify.go +++ b/qualify/qualify.go @@ -24,6 +24,7 @@ import ( "os" "path/filepath" "runtime" + "sort" "time" fusa "github.com/SoundMatt/go-FuSa" @@ -263,33 +264,44 @@ func hasFinding(findings []fusa.Finding, ruleID string) bool { } // computeHash returns a "sha256:" integrity hash of the report content -// (excluding the Hash field and common-header metadata) per §6. +// (excluding the Hash field and §3.1 common-header metadata such as +// generatedAt) per §6. The content is canonicalized with +// [fusa.CanonicalizeJSON] — not Go struct field order — so the hash is +// reproducible across x-FuSa tools (MUST-145/146), and results[] is sorted by +// case name before hashing so ordering never perturbs the digest (MUST-148). // //fusa:req REQ-QUALIFY004 func computeHash(r *Report) (string, error) { type hashable struct { - GeneratedAt time.Time `json:"generatedAt"` - GoVersion string `json:"goVersion"` - Module string `json:"module"` - Total int `json:"total"` - Passed int `json:"passed"` - Failed int `json:"failed"` - Results []Result `json:"results"` + GoVersion string `json:"goVersion"` + Module string `json:"module"` + Total int `json:"total"` + Passed int `json:"passed"` + Failed int `json:"failed"` + Results []Result `json:"results"` } + results := make([]Result, len(r.Results)) + copy(results, r.Results) + sort.Slice(results, func(i, j int) bool { + return results[i].Case.Name < results[j].Case.Name + }) h := hashable{ - GeneratedAt: r.GeneratedAt, - GoVersion: r.GoVersion, - Module: r.Module, - Total: r.Total, - Passed: r.Passed, - Failed: r.Failed, - Results: r.Results, + GoVersion: r.GoVersion, + Module: r.Module, + Total: r.Total, + Passed: r.Passed, + Failed: r.Failed, + Results: results, } data, err := json.Marshal(h) if err != nil { return "", err } - sum := sha256.Sum256(data) + canon, err := fusa.CanonicalizeJSON(data) + if err != nil { + return "", err + } + sum := sha256.Sum256(canon) return fmt.Sprintf("sha256:%x", sum), nil }