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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -1284,6 +1284,18 @@
"text": "All Finding.Location.File values produced by lint and analyze rules shall be paths relative to projectRoot using forward-slash separators. Absolute paths are prohibited. This ensures fingerprints (§4.2) are portable across environments.",
"standard": "x-FuSa"
},
{
"id": "REQ-LOC-REL002",
"title": "cyber.location.file is project-relative",
"text": "All Finding.Location.File values produced by CYBER rules shall be paths relative to projectRoot using forward-slash separators, matching lint/analyze's existing REQ-LOC-REL001 behaviour; this also fixes tara.Scan's ThreatEntry.Location/SourceFile, which is sourced directly from a CYBER finding.",
"standard": "x-FuSa spec section 4"
},
{
"id": "REQ-LOC-REL003",
"title": "fmea entries[].file is project-relative",
"text": "fmea.Scan shall relativize each Entry.File against projectRoot with forward-slash separators before returning the report, per x-FuSa spec section 4's project-relative MUST.",
"standard": "x-FuSa spec section 4"
},
{
"id": "REQ-CAP-STD001",
"title": "capabilities Standards array uses canonical IDs",
Expand Down Expand Up @@ -1597,6 +1609,12 @@
"standard": "x-FuSa",
"level": "1.4.1"
},
{
"id": "REQ-TRACE012",
"title": "IsExcludedDir is the shared test-tree exclusion predicate",
"text": "trace.IsExcludedDir shall report whether a directory is vendor/, testdata/, or a dot-directory, and shall be the single implementation ScanTags/ScanFuncCoverage/ScanFuncTagCoverage and every coverage-denominator scanner (fmea.CountProjectFunctions, tara.CountProjectFiles) apply, per x-FuSa spec section 1.6 rule 4 (SHOULD).",
"standard": "x-FuSa spec section 1.6"
},
{
"id": "REQ-DOC001",
"title": "ResolveDoc falls back to the docs/safety/ scaffold path",
Expand Down Expand Up @@ -2484,6 +2502,12 @@
"text": "gofusa fmea shall support --min-coverage N (exit 1 if summary.coveragePct < N), and --strict/--require-attestation (exit 1 on an unsuppressed FUSA-STUB002 finding).",
"standard": "x-FuSa spec section 1.6.2 / section 9.2"
},
{
"id": "REQ-FMEA012",
"title": "fmea summary.coveragePct never exceeds 100",
"text": "fmea.buildSummary shall clamp summary.coveragePct to a maximum of 100, defensively, in addition to the componentsInProject >= componentsAnalyzed fallback that already guarantees this, per x-FuSa spec section 9.2 MUST.",
"standard": "x-FuSa spec section 9.2"
},
{
"id": "REQ-HARA016",
"title": "SafetyGoal.FSSRRefs replaces the singular FSSRRef field",
Expand Down Expand Up @@ -2700,6 +2724,12 @@
"text": "gofusa tara shall support --min-coverage N (exit 1 if summary.coveragePct < N), and --strict/--require-attestation (exit 1 on an unsuppressed FUSA-STUB002 finding).",
"standard": "x-FuSa spec section 1.6.2 / section 9.2"
},
{
"id": "REQ-TARA011",
"title": "tara summary.coveragePct never exceeds 100",
"text": "tara.buildSummary shall clamp summary.coveragePct to a maximum of 100, defensively, in addition to the assetsInProject >= assetsAnalyzed fallback that already guarantees this, per x-FuSa spec section 9.2 MUST.",
"standard": "x-FuSa spec section 9.2"
},
{
"id": "REQ-TARA012",
"title": "deriveRisk implements the x-FuSa spec section 9.2 risk combination table",
Expand Down
92 changes: 67 additions & 25 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,73 @@ Dates reference the merged commit timestamp.

## [Unreleased]

## v0.43.0 — 2026-07-28 (fmea/tara: project-relative paths, coveragePct clamp, wider FMEA templates)

### Fixed
- **fmea.json/tara.json no longer emit absolute file paths** under the
common `--dir`-omitted invocation (x-FuSa spec §4 MUST — project-relative,
forward-slash paths). `fmea.Scan`'s `entries[].file` and CYBER's own
`Finding.Location.File` (which `tara.Scan` sources `threats[].location`/
`sourceFile` directly from) both carried the raw, often-absolute walked
path. `fmea.Scan` now relativizes every entry's `File` against
`projectRoot` before returning; `cyber.location` now does the same thing
lint/analyze already did (§REQ-LOC-REL001), which transitively fixes
`tara.json` too since it never had its own bug — it just inherited an
absolute path from the CYBER finding it was built from (go-FuSa#59).
- **`coveragePct` now has a defensive `<= 100` clamp** in both
`fmea.buildSummary` and `tara.buildSummary` (x-FuSa spec §9.2 MUST, spec
v1.15.0). The existing `componentsInProject`/`assetsInProject` fallback
already made this mathematically unreachable today, but the clamp is
cheap insurance against a future change to that fallback silently
reintroducing the overflow the spec calls out. New regression tests use a
fixture with a non-trivial test-source tree (many `_test.go` files) to
actually exercise the exclusion logic the bug depends on.
- **fmea's own committed `fmea.json` no longer fails its own
`FUSA-STUB002` content-quality gate.** `deriveAnalysis` bucketed every
scanned function into one of only 6 fixed template strings, so a
461-function codebase like go-FuSa's own reduced to a ~0.013 distinct-
value ratio — reproducing exactly the "hundreds of FMEA rows sharing
identical boilerplate text" pattern §1.6.1 rule B exists to catch
(go-FuSa#60). Every template now weaves in the function's own component
(package/directory — a genuine per-function signal) plus, for the
dominant no-other-signal-matched bucket, whether the function has a
receiver and how many parameters it takes. Regenerated `fmea.json`/
`fmea.csv`: failureMode/effect/cause distinct-value ratios are now
~0.29/0.28/0.29 (up from ~0.013), comfortably clear of the 0.1 threshold.

### Changed
- **Coverage-denominator scanners reuse a shared test-tree exclusion
predicate** (x-FuSa spec §1.6 rule 4 SHOULD, non-binding implementer
guidance): new `trace.IsExcludedDir` is now the single implementation of
the vendor/testdata/dot-directory check `trace.ScanTags`/
`ScanFuncCoverage`/`ScanFuncTagCoverage`, `fmea.CountProjectFunctions`,
and `tara.CountProjectFiles` all previously re-implemented independently.

## v0.42.0 — 2026-07-28 (hara: risk.asil cross-validation + canonical standard id)

### Fixed
- **HARA008: `risk.asil` is now cross-validated against `DetermineASIL(S,E,C)`**
(x-FuSa spec §1.2.5 MUST — ASIL determination). Previously a hazard's
stored `risk.asil` was accepted verbatim: `DetermineASIL` was only ever
used as a *fallback* for an empty value, so a hand-edited or
copy-pasted hazard could claim any ASIL regardless of its own S/E/C
inputs, with zero findings/gaps from either `gofusa hara show` or
`check`. The new `hara.ValidateASIL` (wrapped by the new engine rule
`HARA008`, and folded into `hara.Validate`'s own gap list so `hara show`
surfaces it directly) flags a hazard whose declared `risk.asil` disagrees
with the ISO 26262-3:2018 Table 4 value for its own severity/exposure/
controllability — skipping hazards with an incomplete S/E/C rating
(HARA002's job) or no `risk.asil` set yet.
- **`standard` now uses the x-FuSa spec §2.4.1 canonical lowercase id**
(`iso26262`, not `"ISO 26262"`) in `.fusa-hara.json`: `hara init`'s
default `--standard` flag value changed from `"ISO 26262"` to
`"iso26262"`, the repo's own checked-in `.fusa-hara.json` was
normalised, and `hara.Load` now transparently normalises a legacy
display-string value (`"ISO 26262"`, `"IEC 61508"`, …) onto its
canonical id for backward compatibility with hand-authored files
predating this convention — an unrecognised id is still passed through
verbatim, never rejected.

## v0.41.0 — 2026-07-28 (tara: closed impact/risk enums per x-FuSa spec v1.14.1)

### Fixed
Expand Down Expand Up @@ -55,31 +122,6 @@ Dates reference the merged commit timestamp.
continues to be suppressed only by a valid §1.6.2 attestation, never by
disposition.

## v0.42.0 — 2026-07-28 (hara: risk.asil cross-validation + canonical standard id)

### Fixed
- **HARA008: `risk.asil` is now cross-validated against `DetermineASIL(S,E,C)`**
(x-FuSa spec §1.2.5 MUST — ASIL determination). Previously a hazard's
stored `risk.asil` was accepted verbatim: `DetermineASIL` was only ever
used as a *fallback* for an empty value, so a hand-edited or
copy-pasted hazard could claim any ASIL regardless of its own S/E/C
inputs, with zero findings/gaps from either `gofusa hara show` or
`check`. The new `hara.ValidateASIL` (wrapped by the new engine rule
`HARA008`, and folded into `hara.Validate`'s own gap list so `hara show`
surfaces it directly) flags a hazard whose declared `risk.asil` disagrees
with the ISO 26262-3:2018 Table 4 value for its own severity/exposure/
controllability — skipping hazards with an incomplete S/E/C rating
(HARA002's job) or no `risk.asil` set yet.
- **`standard` now uses the x-FuSa spec §2.4.1 canonical lowercase id**
(`iso26262`, not `"ISO 26262"`) in `.fusa-hara.json`: `hara init`'s
default `--standard` flag value changed from `"ISO 26262"` to
`"iso26262"`, the repo's own checked-in `.fusa-hara.json` was
normalised, and `hara.Load` now transparently normalises a legacy
display-string value (`"ISO 26262"`, `"IEC 61508"`, …) onto its
canonical id for backward compatibility with hand-authored files
predating this convention — an unrecognised id is still passed through
verbatim, never rejected.

## v0.36.0 — 2026-07-28 (x-FuSa spec v1.13.0/v1.14.0 — evidence-artifact schema conformance + content-quality baseline)

### Added
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -285,7 +285,7 @@ docker build -t go-fusa .
docker run --rm -v "$(pwd)":/project go-fusa check
```

Published tags: `latest`, `0.42`, `0.42.0` (and matching semver for every release).
Published tags: `latest`, `0.43`, `0.43.0` (and matching semver for every release).

## Standards coverage

Expand Down
118 changes: 118 additions & 0 deletions cmd/gofusa/cmd_pathrel_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
package main

// cmd_pathrel_test.go covers x-FuSa spec §4's project-relative MUST for
// fmea.json/tara.json under the common, day-to-day invocation shape:
// `gofusa fmea`/`gofusa tara` run from the project root with --dir omitted
// (which resolves to os.Getwd(), an absolute path) — exactly the case that
// previously produced absolute entries[].file / threats[].location.file
// (go-FuSa#59).

import (
"bytes"
"encoding/json"
"os"
"path/filepath"
"strings"
"testing"

fusa "github.com/SoundMatt/go-FuSa"
)

//fusa:test REQ-LOC-REL003
func TestRunFmea_DirOmitted_EntryFileIsRelative(t *testing.T) {
dir := t.TempDir()
src := "package main\n\n//fusa:req REQ-001\nfunc SafetyFunc() error { return nil }\n"
if err := os.WriteFile(filepath.Join(dir, "main.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}

cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.Chdir(cwd) }()
if chErr := os.Chdir(dir); chErr != nil {
t.Fatal(chErr)
}

var out, errBuf bytes.Buffer
// --dir intentionally omitted: this is the common invocation shape the
// bug reproduced under.
if code := runFmea(nil, &out, &errBuf); code != fusa.ExitOK {
t.Fatalf("runFmea: exit %d, stderr: %s", code, errBuf.String())
}

data, err := os.ReadFile(filepath.Join(dir, "fmea.json"))
if err != nil {
t.Fatalf("read fmea.json: %v", err)
}
var report struct {
Entries []struct {
File string `json:"file"`
} `json:"entries"`
}
if err := json.Unmarshal(data, &report); err != nil {
t.Fatalf("unmarshal fmea.json: %v", err)
}
if len(report.Entries) == 0 {
t.Fatal("expected at least one entry")
}
for _, e := range report.Entries {
if filepath.IsAbs(e.File) {
t.Errorf("entries[].file = %q, want project-relative (§4 MUST)", e.File)
}
}
}

//fusa:test REQ-LOC-REL002
func TestRunTara_DirOmitted_ThreatLocationIsRelative(t *testing.T) {
dir := t.TempDir()
src := "package main\n\nimport \"crypto/md5\"\n\nfunc Hash(b []byte) [16]byte { return md5.Sum(b) }\n"
if err := os.WriteFile(filepath.Join(dir, "weak.go"), []byte(src), 0o644); err != nil {
t.Fatal(err)
}

cwd, err := os.Getwd()
if err != nil {
t.Fatal(err)
}
defer func() { _ = os.Chdir(cwd) }()
if chErr := os.Chdir(dir); chErr != nil {
t.Fatal(chErr)
}

var out, errBuf bytes.Buffer
if code := runTara(nil, &out, &errBuf); code != fusa.ExitOK {
t.Fatalf("runTara: exit %d, stderr: %s", code, errBuf.String())
}

data, err := os.ReadFile(filepath.Join(dir, "tara.json"))
if err != nil {
t.Fatalf("read tara.json: %v", err)
}
var report struct {
Threats []struct {
Location struct {
File string `json:"file"`
} `json:"location"`
SourceFile string `json:"sourceFile"`
} `json:"threats"`
}
if err := json.Unmarshal(data, &report); err != nil {
t.Fatalf("unmarshal tara.json: %v", err)
}
if len(report.Threats) == 0 {
t.Fatal("expected at least one threat")
}
for _, th := range report.Threats {
if filepath.IsAbs(th.Location.File) {
t.Errorf("threats[].location.file = %q, want project-relative (§4 MUST)", th.Location.File)
}
if filepath.IsAbs(th.SourceFile) {
t.Errorf("threats[].sourceFile = %q, want project-relative (§4 MUST)", th.SourceFile)
}
if strings.Contains(th.Location.File, "\\") {
t.Errorf("threats[].location.file = %q, want forward-slash separators", th.Location.File)
}
}
}
Loading
Loading