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
10 changes: 5 additions & 5 deletions .fusa-hara.json
Original file line number Diff line number Diff line change
Expand Up @@ -65,39 +65,39 @@
"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",
"description": "FuSaOps shall record every applicable-but-uninstalled or failed tool as an explicit skipped component so coverage gaps are visible",
"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",
"description": "FuSaOps shall exit non-zero whenever any component produces an ERROR-severity finding, or a WARNING under --strict",
"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",
"description": "FuSaOps shall tag every finding with the originating language and tool so attribution survives aggregation",
"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",
"description": "FuSaOps shall recompute the aggregate report on demand so a gating or audit decision can be made against current source",
"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"]
}
]
}
43 changes: 43 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
]
}
51 changes: 51 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
107 changes: 107 additions & 0 deletions canonjson.go
Original file line number Diff line number Diff line change
@@ -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"))
}
112 changes: 112 additions & 0 deletions canonjson_test.go
Original file line number Diff line number Diff line change
@@ -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":"<b>&\"quote\"</b>"}`))
if err != nil {
t.Fatalf("Canonicalize: %v", err)
}
want := `{"a":"<b>&\"quote\"</b>"}`
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")
}
}
Loading
Loading