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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .fusa-hara.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"project": "go-FuSa",
"standard": "ISO 26262",
"standard": "iso26262",
"createdAt": "2026-06-09T00:00:00Z",
"operationalSituations": [
{
Expand Down
12 changes: 12 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -2526,6 +2526,18 @@
"text": "gofusa hara show shall support --strict/--require-attestation, escalating an unsuppressed FUSA-STUB002 finding on .fusa-hara.json to exit 1.",
"standard": "x-FuSa spec section 1.6.2"
},
{
"id": "REQ-HARA024",
"title": "HARA008 engine rule / ValidateASIL: risk.asil cross-validated against S/E/C",
"text": "ValidateASIL (and the HARA008 engine rule that wraps it) shall flag a hazard whose declared risk.asil disagrees with DetermineASIL(severity, exposure, controllability) per ISO 26262-3:2018 Table 4, skipping hazards with an incomplete S/E/C rating or an unset risk.asil; Validate shall include the same check in hara show's rendered gap list.",
"standard": "x-FuSa spec section 1.2.5"
},
{
"id": "REQ-HARA025",
"title": "hara.Load normalises a legacy display-string standard value",
"text": "hara.Load shall normalise a legacy display-string standard value (e.g. \"ISO 26262\") onto its x-FuSa spec section 2.4.1 canonical lowercase id (e.g. \"iso26262\"), leaving an already-canonical or unrecognised id unchanged; hara init's default --standard flag value is the canonical id.",
"standard": "x-FuSa spec section 2.4.1"
},
{
"id": "REQ-SAS004",
"title": "SAS ChecklistItem canonical shape",
Expand Down
25 changes: 25 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,31 @@ 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.41`, `0.41.0` (and matching semver for every release).
Published tags: `latest`, `0.42`, `0.42.0` (and matching semver for every release).

## Standards coverage

Expand Down
4 changes: 3 additions & 1 deletion cmd/gofusa/cmd_hara.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,9 @@ func runHaraInit(args []string, projectRoot string, stdout, stderr io.Writer) in
fs := flag.NewFlagSet("gofusa hara init", flag.ContinueOnError)
fs.SetOutput(stderr)
project := fs.String("project", "", "project name (default: directory name)")
standard := fs.String("standard", "ISO 26262", "safety standard (e.g. 'ISO 26262', 'IEC 61508')")
// x-FuSa spec §2.4.1: standard is a canonical lowercase id, never a
// display string ("iso26262", not "ISO 26262").
standard := fs.String("standard", "iso26262", "safety standard canonical id (e.g. 'iso26262', 'iec61508')")
if code := parseFlags(fs, args); code != 0 {
return code
}
Expand Down
4 changes: 2 additions & 2 deletions docs/tool-safety-manual.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# go-FuSa Tool Safety Manual

**Version:** 0.41.0
**Version:** 0.42.0
**Module:** `github.com/SoundMatt/go-FuSa`
**License:** Mozilla Public License 2.0
**Standards addressed:** ISO 26262, IEC 61508, ISO 21434, DO-178C
Expand Down Expand Up @@ -50,7 +50,7 @@ Capabilities:
| Data/control coupling report generation | COUP001–003 | `gofusa coupling` |
| ISO 26262 Part 6 compliance gap report | ISO26262001 | `gofusa iso26262` |
| IEC 61508 Parts 1-3 compliance gap report | IEC61508001 | `gofusa iec61508` |
| Hazard Analysis and Risk Assessment (HARA) | HARA001–005 | `gofusa hara` |
| Hazard Analysis and Risk Assessment (HARA) | HARA001–008 | `gofusa hara` |
| Finding disposition log | DISP001 | `gofusa disposition` |
| Change impact analysis | — | `gofusa impact` |
| Safety metrics trending | — | `gofusa metrics` |
Expand Down
2 changes: 1 addition & 1 deletion fusa.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ import (
)

// Version is the current release of go-FuSa.
const Version = "0.41.0"
const Version = "0.42.0"

// SpecVersion is the x-FuSa spec version this release implements.
const SpecVersion = "1.14.0"
Expand Down
98 changes: 96 additions & 2 deletions hara/hara.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
// - HARA005: max hazard ASIL exceeds project ASIL from .fusa.json
// - HARA006: safety goal with no fssrRefs (x-FuSa spec §1.2.5 MUST)
// - HARA007: fssrRefs entry dangling (no matching id in .fusa-reqs.json)
// - HARA008: hazard's stored risk.asil disagrees with DetermineASIL(S,E,C)
//
// Usage:
//
Expand Down Expand Up @@ -136,8 +137,13 @@ type SafetyGoal struct {
//
//fusa:req REQ-HARA005
type HARA struct {
Project string `json:"project"`
Standard string `json:"standard"` // "ISO 26262" or "IEC 61508"
Project string `json:"project"`
// Standard is the x-FuSa spec §2.4.1 canonical lowercase standard id
// ("iso26262", "iec61508", …) — never a display string. Load normalises
// a legacy display-string value (e.g. "ISO 26262") onto its canonical
// id for backward compatibility with hand-authored files predating this
// convention; see normalizeStandard.
Standard string `json:"standard"`
CreatedAt time.Time `json:"createdAt"`
Situations []OperationalSituation `json:"operationalSituations"`
Hazards []Hazard `json:"hazards"`
Expand Down Expand Up @@ -326,9 +332,34 @@ func Load(projectRoot string) (*HARA, error) {
if err := json.Unmarshal(data, &h); err != nil {
return nil, fmt.Errorf("%w: %s: %s", fusa.ErrInvalidConfig, HARAFile, err)
}
h.Standard = normalizeStandard(h.Standard)
return &h, nil
}

// normalizeStandard maps a legacy display-string standard value (e.g.
// "ISO 26262", as written by hara init before this normalisation existed)
// onto the x-FuSa spec §2.4.1 canonical lowercase id (e.g. "iso26262"), for
// backward compatibility with hand-authored/older .fusa-hara.json files. An
// empty value, an id that already looks canonical, or one go-FuSa does not
// recognise is returned unchanged — §2.4.1: an unrecognised id MUST be
// treated verbatim, never rejected.
//
//fusa:req REQ-HARA025
func normalizeStandard(s string) string {
switch strings.ToLower(strings.Join(strings.Fields(s), " ")) {
case "iso 26262", "iso26262":
return "iso26262"
case "iec 61508", "iec61508":
return "iec61508"
case "iso 21434", "iso21434":
return "iso21434"
case "do-178c", "do 178c", "do178c":
return "do178c"
default:
return s
}
}

// Save writes the HARA to path.
//
//fusa:req REQ-HARA008
Expand Down Expand Up @@ -399,6 +430,10 @@ func Validate(h *HARA) []ValidationFinding {
}
}

// HARA008: a hazard's declared risk.asil must match its own S/E/C rating
// (x-FuSa spec §1.2.5 MUST — see ValidateASIL doc).
out = append(out, ValidateASIL(h)...)

return out
}

Expand Down Expand Up @@ -557,6 +592,7 @@ func init() {
engine.Default.MustRegister(&ruleHARA005{})
engine.Default.MustRegister(&ruleHARA006{})
engine.Default.MustRegister(&ruleHARA007{})
engine.Default.MustRegister(&ruleHARA008{})
}

// HARA001 — no HARA file present.
Expand Down Expand Up @@ -803,3 +839,61 @@ func (r *ruleHARA007) Run(_ context.Context, projectRoot string, _ *config.Confi
}
return out, nil
}

// HARA008 — a hazard's stored risk.asil disagrees with the S/E/C-derived ASIL.
type ruleHARA008 struct{}

func (r *ruleHARA008) ID() string { return "HARA008" }
func (r *ruleHARA008) Description() string {
return "Hazard's declared risk.asil does not match DetermineASIL(severity, exposure, controllability) per ISO 26262-3:2018 Table 4 (x-FuSa spec §1.2.5 MUST)."
}

// ValidateASIL cross-checks every hazard in h with a complete S/E/C rating
// against DetermineASIL, flagging a hazard whose stored risk.asil disagrees
// with what the table derives. A hazard with an incomplete S/E/C rating is
// skipped here — that gap is HARA002's responsibility, and DetermineASIL
// would otherwise report a misleading "should be QM" for missing inputs
// rather than a genuine table mismatch.
//
//fusa:req REQ-HARA024
func ValidateASIL(h *HARA) []ValidationFinding {
var out []ValidationFinding
for _, hz := range h.Hazards {
if hz.Risk.ASIL == "" {
continue
}
if hz.Risk.Severity == "" || hz.Risk.Exposure == "" || hz.Risk.Controllability == "" {
continue
}
derived := DetermineASIL(hz.Risk.Severity, hz.Risk.Exposure, hz.Risk.Controllability)
if hz.Risk.ASIL != derived {
out = append(out, ValidationFinding{
HazardID: hz.ID,
Message: fmt.Sprintf(
"hazard %s declares risk.asil=%s but S=%s E=%s C=%s derives %s per ISO 26262-3 Table 4 (DetermineASIL)",
hz.ID, hz.Risk.ASIL, hz.Risk.Severity, hz.Risk.Exposure, hz.Risk.Controllability, derived,
),
})
}
}
return out
}

//fusa:req REQ-HARA024
func (r *ruleHARA008) Run(_ context.Context, projectRoot string, _ *config.Config) ([]fusa.Finding, error) {
h, err := Load(projectRoot)
if err != nil || len(h.Hazards) == 0 {
return nil, nil
}
var out []fusa.Finding
for _, f := range ValidateASIL(h) {
out = append(out, fusa.Finding{
RuleID: r.ID(),
Severity: fusa.SeverityWarning,
Message: f.Message,
Location: fusa.Location{File: HARAFile},
Remediation: fmt.Sprintf("update risk.asil for hazard %s in %s to match its S/E/C rating, or correct the S/E/C rating if the ASIL is right and the inputs are wrong", f.HazardID, HARAFile),
})
}
return out, nil
}
Loading
Loading