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
16 changes: 8 additions & 8 deletions .fusa-hara.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,23 +22,23 @@
"description": "Dropped finding — FuSaOps loses or hides a finding produced by a bundled x-FuSa tool during normalisation",
"source": "Parser error swallowing findings, or an unknown severity being discarded instead of normalised",
"situations": ["OS-001", "OS-002", "OS-003"],
"risk": { "severity": "S2", "exposure": "E4", "controllability": "C2", "asil": "ASIL-C" },
"risk": { "severity": "S2", "exposure": "E4", "controllability": "C2", "asil": "ASIL-B" },
"safetyGoals": ["SG-001"]
},
{
"id": "H-002",
"description": "Masked coverage gap — an applicable language tool does not run and FuSaOps reports PASS without surfacing that the language was unchecked",
"source": "Adapter silently skipped, tool not installed, or detection miss treated as success",
"situations": ["OS-002", "OS-003"],
"risk": { "severity": "S2", "exposure": "E4", "controllability": "C2", "asil": "ASIL-C" },
"risk": { "severity": "S2", "exposure": "E4", "controllability": "C2", "asil": "ASIL-B" },
"safetyGoals": ["SG-002"]
},
{
"id": "H-003",
"description": "Silent failure — fusaops check exits 0 when the aggregate contains ERROR-severity findings",
"source": "Summary mis-count or exit-code logic returning success despite errors",
"situations": ["OS-002"],
"risk": { "severity": "S2", "exposure": "E3", "controllability": "C1", "asil": "ASIL-A" },
"risk": { "severity": "S2", "exposure": "E3", "controllability": "C1", "asil": "QM" },
"safetyGoals": ["SG-003"]
},
{
Expand All @@ -54,7 +54,7 @@
"description": "Stale evidence — the served dashboard or cached aggregate report no longer reflects the current source under review",
"source": "Cache not refreshed before an audit or gating decision",
"situations": ["OS-002", "OS-003"],
"risk": { "severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B" },
"risk": { "severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A" },
"safetyGoals": ["SG-005"]
}
],
Expand All @@ -63,23 +63,23 @@
"id": "SG-001",
"description": "FuSaOps shall preserve every finding emitted by a bundled tool, normalising unknown severities to INFO rather than dropping them",
"hazards": ["H-001"],
"asil": "ASIL-C",
"asil": "ASIL-B",
"safeState": "Surface a decode error as a failed/skipped component rather than an empty pass",
"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",
"asil": "ASIL-B",
"safeState": "Emit a skipped component with reason; never omit an applicable language silently",
"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",
"asil": "QM",
"safeState": "Exit code 1",
"fssrRefs": ["REQ-FO-CLI008", "REQ-FO-RPT006", "REQ-FO-ERR005"]
},
Expand All @@ -95,7 +95,7 @@
"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",
"asil": "ASIL-A",
"safeState": "Provide an explicit refresh that recomputes before serving",
"fssrRefs": ["REQ-FO-SRV003", "REQ-FO-SRV004"]
}
Expand Down
9 changes: 5 additions & 4 deletions .fusa.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
{
"version": "1",
"configVersion": "1.0",
"project": {
"name": "FuSaOps",
"module": "github.com/SoundMatt/FuSaOps",
"standard": "ISO26262",
"asil": "ASIL-C"
"version": "0.1.0",
"module": "github.com/SoundMatt/FuSaOps"
},
"standard": "iso26262",
"asil": "ASIL-B",
"rules": {},
"report": {
"format": "text"
Expand Down
27 changes: 22 additions & 5 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -39,16 +39,30 @@ jobs:
shell: bash
run: go test -race -count=1 -coverprofile=coverage.out ./...

- name: Coverage gate (>= 80%)
if: matrix.os == 'ubuntu-latest' && matrix.go == '1.22'
# ── Coverage gate (>= 80%) ────────────────────────────────────────────────
# Standalone required job so the gate is independent of the OS/Go test matrix:
# renaming/skipping a matrix cell can never silently remove the coverage check.
coverage:
name: Coverage gate (>= 80%)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- uses: actions/setup-go@v6
with:
go-version: "1.22"

- name: go test (coverage)
shell: bash
run: go test -count=1 -coverprofile=coverage.out ./...

- name: Enforce coverage threshold
shell: bash
run: |
pct=$(go tool cover -func=coverage.out | awk '/^total:/{gsub(/%/,"",$3); print $3}')
printf "Coverage: %.1f%%\n" "$pct"
awk -v p="$pct" 'BEGIN{ if (p+0 < 80) { print "FAIL: coverage " p "% below 80% threshold"; exit 1 } print "PASS" }'

- name: Upload coverage
if: matrix.os == 'ubuntu-latest' && matrix.go == '1.22'
uses: actions/upload-artifact@v4
with:
name: coverage
Expand Down Expand Up @@ -88,7 +102,8 @@ jobs:
run: gofusa check --format json --output gofusa-check-report.json

- name: gofusa report (evidence)
run: gofusa report --format html --output gofusa-report.html || true
continue-on-error: true
run: gofusa report --format html --output gofusa-report.html

- name: Upload go-FuSa evidence
if: always()
Expand Down Expand Up @@ -116,8 +131,10 @@ jobs:
- name: Install gofusa
run: go install github.com/SoundMatt/go-FuSa/cmd/gofusa@latest
- name: Generate SARIF
run: gofusa check --format sarif --output results.sarif || true
continue-on-error: true
run: gofusa check --format sarif --output results.sarif
- name: Upload SARIF to GitHub Code Scanning
if: hashFiles('results.sarif') != ''
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
Expand Down
10 changes: 7 additions & 3 deletions .github/workflows/tools-monitor.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,14 @@ jobs:
- uses: actions/checkout@v5

- name: Report trigger
env:
EVENT_NAME: ${{ github.event_name }}
TOOL: ${{ github.event.client_payload.tool }}
VERSION: ${{ github.event.client_payload.version }}
run: |
echo "event: ${{ github.event_name }}"
echo "tool: ${{ github.event.client_payload.tool }}"
echo "version: ${{ github.event.client_payload.version }}"
echo "event: $EVENT_NAME"
echo "tool: $TOOL"
echo "version: $VERSION"

- name: Log in to GHCR
uses: docker/login-action@v3
Expand Down
48 changes: 48 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,54 @@ and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.

## [Unreleased]

## [1.148.0] — 2026-07-30

### fix: independently-verified third-party audit — ISO 26262 Table 4 ASIL bug, unauth arbitrary-file-read, and 13 other findings

An external audit (`FusaOps_Audit.zip`) was checked finding-by-finding against
this repo's live source before any patch was applied — every claim below was
independently reconfirmed, not taken on the audit's word.

- **Critical: `hara.DetermineASIL` mis-implemented ISO 26262-3:2018 Table 4.**
Every S2/S3 cell (S1 was already correct) was inflated by one to two ASIL
levels versus the standard's additive S+E+C derivation — e.g. `S3/E1/C2`
returned `ASIL-B` instead of the correct `QM`. `hara_test.go` hard-coded the
wrong values as "known-good," so a correct fix would have broken CI. Fixed
the lookup table and rewrote the test from the canonical 36-cell truth
table. The project's own `.fusa-hara.json` was regenerated: every affected
hazard/safety-goal ASIL dropped (max is now **ASIL-B**, not C), and
`.fusa.json`'s self-classification was corrected to match plus rewritten
into the canonical `configVersion`/top-level `standard`+`asil` shape
(was legacy-nested `"ISO26262"` with no `configVersion`).
- **Medium/security: unauthenticated `serve` + arbitrary file read.**
`/api/v1/diff?baseline=<path>` took a filesystem path straight from the
query string with no validation, and `serve` runs without auth unless an
operator explicitly configures credentials. Sandboxed the baseline path to
the project root.
- **Low/security: non-constant-time Basic Auth comparison.** Credential
compare used `==`; switched to a constant-time compare.
- **Medium: non-reproducible `qualify.hash`.** The §6 integrity hash included
the live `generatedAt` timestamp and hashed components in registry order
rather than sorted — changed every run. Now blanks the timestamp and sorts
before hashing.
- **Low: `sas` never wrote its `sas.md` companion**, only the JSON — now
emitted alongside it.
- Also fixed: `vv.AchievableASIL` didn't discount reviewer/executor==author
self-attestation; history writes weren't atomic; Basic-Auth webhook fires
had no timeout; `DecisionNote` mislabeled a block-coverage proxy as true
decision coverage; diff fingerprint matching didn't qualify by language;
the scheduler's stop channel could leak; `auditpack` ignored
`SOURCE_DATE_EPOCH`; dead no-op code and an unescaped Prometheus label;
`adapter.DefaultSkipDirs` was hard-coded and unconfigurable; `Location`
had no `EndLine`/`EndColumn`; two invented DO-178C/ISO 21434 clause
citations (§6.3.4 for cyclomatic complexity; "Chapter 9" for TARA, correct
is Clause 15); README requirement-count and spec-version-label drift; CI's
self-scan SARIF step and the coverage gate were both non-gating.
- Deliberately not patched (documented in the audit's own manifest): Unicode
NFC normalization in `ComputeFingerprint` would require `golang.org/x/text`,
violating this repo's std-lib-only rule; pinning tool base images/GitHub
Actions to digests/SHAs needs live registry lookups not derivable offline.

## [1.147.0] — 2026-07-29

### fix: ada-FuSa ecosystem-integration gaps found by a support-surface audit
Expand Down
12 changes: 12 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,14 @@ RUN CGO_ENABLED=0 GOOS=linux go build \
# needed by pyfusa. Java is added via openjdk21-jre-headless for jfusa.
FROM python:3.12-alpine

# OCI image metadata (source, description, licensing) for provenance tooling.
LABEL org.opencontainers.image.title="FuSaOps" \
org.opencontainers.image.description="All-in-one multi-language functional-safety orchestration image bundling the x-FuSa toolchain." \
org.opencontainers.image.source="https://github.com/SoundMatt/FuSaOps" \
org.opencontainers.image.documentation="https://github.com/SoundMatt/FuSaOps/blob/main/README.md" \
org.opencontainers.image.vendor="SoundMatt" \
org.opencontainers.image.licenses="Apache-2.0"

# git + ca-certificates back the tools' provenance / vulnerability features.
# libstdc++ is pre-staged so cpp-FuSa drops in without a base change.
# openjdk21-jre-headless provides the `java` binary required by jfusa.
Expand All @@ -66,6 +74,10 @@ COPY --from=jfusa /usr/local/bin/jfusa /usr/local/bin/jfusa
# ada-FuSa: statically-linked musl binary, no extra runtime packages needed
COPY --from=adafusa /usr/local/bin/adafusa /usr/local/bin/adafusa

# Run as an unprivileged user; /project is the caller-mounted workspace.
RUN addgroup -S fusa && adduser -S -G fusa -h /home/fusa fusa
USER fusa

WORKDIR /project
EXPOSE 8080
ENTRYPOINT ["fusaops"]
Expand Down
4 changes: 3 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,9 @@ lint:

cover:
go test -coverprofile=coverage.out ./...
go tool cover -func=coverage.out | tail -1
@pct=$$(go tool cover -func=coverage.out | awk '/^total:/{gsub(/%/,"",$$3); print $$3}'); \
printf "Coverage: %s%%\n" "$$pct"; \
awk -v p="$$pct" 'BEGIN{ if (p+0 < 80) { print "FAIL: coverage " p "% below 80% threshold"; exit 1 } print "PASS: coverage >= 80%" }'

scan: build
./$(BINARY) scan
Expand Down
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ fusaops release --output-dir dist/ # cross-language SBOM + provenance + ar
fusaops safety-case --standard "DO-178C" # assemble a structured safety argument from evidence
fusaops sci # Software Configuration Index (DO-178C §11.16)
fusaops sas --level DAL-B # Software Accomplishment Summary (DO-178C §11.20)
fusaops tara # Threat Analysis and Risk Assessment (ISO 21434 Ch. 9)
fusaops tara # Threat Analysis and Risk Assessment (ISO 21434 Clause 15)
fusaops fmea # Design FMEA of the orchestration pipeline
fusaops vuln # scan dependency manifests for known vulnerabilities
fusaops template --standards "ISO 26262" # generate safety documentation templates
Expand Down Expand Up @@ -195,7 +195,7 @@ everything in parallel:
### Spec conformance (v0.3 / updated v0.4)

- **`fusaops conform <binary>`** — validates any x-FuSa tool binary against the
spec v1.9 schema and behavioural invariants. Per spec §16 step 7, this is a
spec v1.15 schema and behavioural invariants. Per spec §16 step 7, this is a
**MUST** gate for onboarding a new language tool. See
[`docs/conformance.md`](docs/conformance.md).

Expand Down Expand Up @@ -656,7 +656,7 @@ go-FuSa-grade evidence set. It aggregates evidence relevant to
**ISO 26262, IEC 61508, ISO 21434, and DO-178C** across the languages it
orchestrates.

- **Requirements** — [`.fusa-reqs.json`](.fusa-reqs.json) (215 requirements);
- **Requirements** — [`.fusa-reqs.json`](.fusa-reqs.json) (449 requirements);
`gofusa trace` reports them all traced **and** tested.
- **HARA** — [`.fusa-hara.json`](.fusa-hara.json) (tool-failure hazards + safety goals).
- **Tool Safety Manual** — [docs/tool-safety-manual.md](docs/tool-safety-manual.md)
Expand Down
30 changes: 20 additions & 10 deletions adapter/adapter.go
Original file line number Diff line number Diff line change
Expand Up @@ -186,9 +186,11 @@
Severity string `json:"severity"`
Message string `json:"message"`
Location struct {
File string `json:"file"`
Line int `json:"line"`
Column int `json:"column"`
File string `json:"file"`
Line int `json:"line"`
Column int `json:"column"`
EndLine int `json:"endLine"`
EndColumn int `json:"endColumn"`
} `json:"location"`
Category string `json:"category"`
Disposition string `json:"disposition"`
Expand Down Expand Up @@ -216,7 +218,7 @@
RuleID: f.RuleID,
Severity: normaliseSeverity(f.Severity),
Message: f.Message,
Location: fusaops.Location{File: f.Location.File, Line: f.Location.Line, Column: f.Location.Column},
Location: fusaops.Location{File: f.Location.File, Line: f.Location.Line, Column: f.Location.Column, EndLine: f.Location.EndLine, EndColumn: f.Location.EndColumn},
Category: normaliseCategory(f.Category),
Disposition: f.Disposition,
Standard: f.Standard,
Expand Down Expand Up @@ -259,14 +261,22 @@
return "other"
}

// DefaultSkipDirs is the set of directory names skipped during language
// detection. It is a package-level variable rather than a hard-coded switch so
// a caller can tailor the skip-list (e.g. remove "out"/"dist"/"target" when a
// project legitimately keeps sources there, or add project-specific excludes)
// before running detection.
//
//fusa:req REQ-FO-ADP004
var DefaultSkipDirs = map[string]bool{
".git": true, ".hg": true, ".svn": true, "node_modules": true,
"vendor": true, "build": true, "dist": true, ".cache": true,
"target": true, "out": true, ".idea": true, ".vscode": true,
}

// skipDir reports whether a directory should be skipped during detection.
func skipDir(name string) bool {
switch name {
case ".git", ".hg", ".svn", "node_modules", "vendor", "build", "dist",
".cache", "target", "out", ".idea", ".vscode":
return true
}
return false
return DefaultSkipDirs[name]
}

// Registry holds a set of registered adapters keyed by tool name.
Expand Down
17 changes: 15 additions & 2 deletions auditpack/auditpack.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
"fmt"
"os"
"sort"
"strconv"
"time"

fusaops "github.com/SoundMatt/FuSaOps"
Expand Down Expand Up @@ -61,10 +62,22 @@ type Manifest struct {
// deterministic, reproducible bundle. A source whose content cannot be read is a
// hard error: an audit pack must never silently omit evidence.
//
// buildTime returns the timestamp stamped into the manifest and every zip
// entry. When SOURCE_DATE_EPOCH is set to a valid Unix timestamp it is honoured,
// so the archive is byte-for-byte reproducible; otherwise the current UTC time
// is used.
func buildTime() time.Time {
if v := os.Getenv("SOURCE_DATE_EPOCH"); v != "" {
if secs, err := strconv.ParseInt(v, 10, 64); err == nil {
return time.Unix(secs, 0).UTC()
}
}
return time.Now().UTC()
}

//fusa:req REQ-FO-PCK004
func Pack(dest, project string, sources []Source) (*Manifest, error) {
sort.Slice(sources, func(i, j int) bool { return sources[i].ArchivePath < sources[j].ArchivePath })

// Resolve content and hashes first so the manifest is complete before we
// start writing the archive.
type resolved struct {
Expand All @@ -73,7 +86,7 @@ func Pack(dest, project string, sources []Source) (*Manifest, error) {
}
items := make([]resolved, 0, len(sources))
manifest := &Manifest{
GeneratedAt: time.Now().UTC(),
GeneratedAt: buildTime(),
Tool: "FuSaOps",
Version: fusaops.Version,
Project: project,
Expand Down
Loading
Loading