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: 10 additions & 0 deletions .fusa-reqs.json
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,16 @@
"criticality": "medium",
"verification": "spec-review",
"asil": "QM"
},
{
"id": "REQ-RELAY-095",
"title": "relay conform --manifest (relay-conform/1)",
"text": "relay conform --manifest <binary> MUST emit a relay-conform/1 manifest (spec \u00a717.2) instead of findings: a JSON document with kind, manifest_version, tool, binary_version, spec_version, git_sha, capabilities_sha256, a requirements[] entry for every \u00a717 requirement (id/status/verifier), and an overall PASS/FAIL. status MUST use the vocabulary PASS/FAIL/SHAPE_ONLY/NOT_OBSERVABLE and MUST NOT report PASS for a requirement relay conform cannot actually observe. The process MUST exit 1 when overall is FAIL.",
"category": "functional",
"criticality": "high",
"verification": "test",
"safety_goal": "SG-006",
"asil": "ASIL-B"
}
]
}
170 changes: 168 additions & 2 deletions cmd/relay/conform.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ package main
import (
"bytes"
"context"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"flag"
"fmt"
Expand Down Expand Up @@ -53,19 +55,22 @@ type conformResult struct {
Findings []conformFinding `json:"findings"`
}

// runConform implements `relay conform [--format text|json] [--strict] <binary>`.
// runConform implements
// `relay conform [--format text|json] [--strict] [--manifest] <binary>`.
//
//fusa:req REQ-RELAY-052
//fusa:req REQ-RELAY-095
func runConform(stdout, stderr io.Writer, args []string) error {
fs := flag.NewFlagSet("conform", flag.ContinueOnError)
fs.SetOutput(io.Discard)
format := fs.String("format", "text", "Output format: text or json")
strict := fs.Bool("strict", false, "Treat WARN as FAIL")
manifest := fs.Bool("manifest", false, "Emit a relay-conform/1 manifest (spec §17.2) instead of findings")
if err := fs.Parse(args); err != nil {
return fmt.Errorf("relay conform: %w", err)
}
if fs.NArg() == 0 {
fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] <binary>")
fmt.Fprintln(stderr, "Usage: relay conform [--format text|json] [--strict] [--manifest] <binary>")
return exitCode(2)
}
if fs.NArg() > 1 {
Expand All @@ -78,6 +83,19 @@ func runConform(stdout, stderr io.Writer, args []string) error {
return exitCode(2)
}

if *manifest {
m := buildManifest(fs.Arg(0))
enc := json.NewEncoder(stdout)
enc.SetIndent("", " ")
if err := enc.Encode(m); err != nil {
return err
}
if m.Overall == statusFail {
return exitCode(1)
}
return nil
}

cr := conformBinary(fs.Arg(0), *strict)

switch *format {
Expand Down Expand Up @@ -152,6 +170,154 @@ func conformBinary(binary string, strict bool) conformResult {
return conformResult{Binary: binary, Result: result, Findings: all}
}

// manifestStatus is a per-requirement status in a relay-conform/1 manifest
// (spec §17.2). It is deliberately a narrower vocabulary than conformSeverity:
// a manifest never reports PASS for a requirement relay conform cannot
// actually observe.
type manifestStatus string

const (
statusPass manifestStatus = "PASS"
statusFail manifestStatus = "FAIL"
statusShapeOnly manifestStatus = "SHAPE_ONLY"
statusNotObservable manifestStatus = "NOT_OBSERVABLE"
verifierRelayConform = "relay conform"
verifierTestSuite = "implementation test suite"
verifierBuildAndTest = "implementation's own build process and test suite"
verifierCI = "implementation's own CI"
verifierManifestSplit = "relay conform (manifest content); implementation's own CI (regenerate + diff)"
)

// manifestRequirement is one entry in a relay-conform/1 manifest's
// "requirements" array (spec §17.2).
type manifestRequirement struct {
ID int `json:"id"`
Status manifestStatus `json:"status"`
Verifier string `json:"verifier"`
}

// conformManifest is the relay-conform/1 manifest document (spec §17.2,
// §17 Requirement 15).
type conformManifest struct {
Kind string `json:"kind"`
ManifestVersion string `json:"manifest_version"`
Tool string `json:"tool"`
BinaryVersion string `json:"binary_version"`
SpecVersion string `json:"spec_version"`
GitSHA string `json:"git_sha"`
CapabilitiesSHA256 string `json:"capabilities_sha256"`
Requirements []manifestRequirement `json:"requirements"`
Overall manifestStatus `json:"overall"`
}

// buildManifest generates a relay-conform/1 manifest (spec §17.2) for one
// binary. It fetches version/capabilities/status exactly once each — the
// same single-fetch discipline as conformBinary — so a manifest and a
// findings report generated from the same invocation never see different
// binary output.
//
//fusa:req REQ-RELAY-095
func buildManifest(binary string) conformManifest {
m := conformManifest{
Kind: "relay-conform-manifest",
ManifestVersion: "relay-conform/1",
}

versionJSON, verErr := runBinaryCommand(binary, []string{"version", "--format", "json"})
var vFindings []conformFinding
if verErr != nil {
vFindings = []conformFinding{fail("§17.7", "version --format json failed: %v", verErr)}
} else {
vFindings = validateVersionDoc(versionJSON)
var vdoc struct {
Tool string `json:"tool"`
Version string `json:"version"`
SpecVersion string `json:"spec_version"`
Commit string `json:"commit"`
}
_ = json.Unmarshal(versionJSON, &vdoc)
m.Tool = vdoc.Tool
m.BinaryVersion = vdoc.Version
m.SpecVersion = vdoc.SpecVersion
m.GitSHA = vdoc.Commit
}

capsJSON, capsErr := runBinaryCommand(binary, []string{"capabilities"})
var cFindings []conformFinding
if capsErr != nil {
cFindings = []conformFinding{fail("§17.7", "capabilities failed: %v", capsErr)}
} else {
cFindings = validateCapabilitiesDoc(capsJSON)
sum := sha256.Sum256(capsJSON)
m.CapabilitiesSHA256 = hex.EncodeToString(sum[:])
}

statusJSON, statusErr := runBinaryCommand(binary, []string{"status", "--format", "json"})
if statusErr != nil {
statusJSON, statusErr = runBinaryCommand(binary, []string{"status"})
}
var sFindings []conformFinding
if statusErr != nil {
sFindings = []conformFinding{fail("§17.7", "status command failed: %v", statusErr)}
} else {
sFindings = validateStatusDoc(statusJSON)
}

req1 := statusPass
if hasFail(vFindings) {
req1 = statusFail
}
req6 := statusPass
if hasFail(cFindings) {
req6 = statusFail
}
req7 := statusPass
if hasFail(vFindings) || hasFail(cFindings) || hasFail(sFindings) {
req7 = statusFail
}
req12 := statusShapeOnly
if hasFail(vFindings) {
req12 = statusFail
}

m.Requirements = []manifestRequirement{
{1, req1, verifierRelayConform},
{2, statusNotObservable, verifierTestSuite},
{3, statusNotObservable, verifierTestSuite},
{4, statusNotObservable, verifierTestSuite},
{5, statusNotObservable, verifierTestSuite},
{6, req6, verifierRelayConform},
{7, req7, verifierRelayConform},
{8, statusNotObservable, verifierTestSuite},
{9, statusNotObservable, verifierTestSuite},
{10, statusNotObservable, verifierTestSuite},
{11, statusNotObservable, verifierTestSuite},
{12, req12, verifierRelayConform},
{13, statusNotObservable, verifierBuildAndTest},
{14, statusNotObservable, verifierCI},
{15, statusNotObservable, verifierManifestSplit},
}

m.Overall = statusPass
for _, r := range m.Requirements {
if r.Status == statusFail {
m.Overall = statusFail
break
}
}
return m
}

// hasFail reports whether any finding in fs is FAIL-severity.
func hasFail(fs []conformFinding) bool {
for _, f := range fs {
if f.Severity == sevFail {
return true
}
}
return false
}

func printConformText(w io.Writer, cr conformResult) {
for _, f := range cr.Findings {
fmt.Fprintf(w, "%-4s %s %s\n", f.Severity, f.Req, f.Message)
Expand Down
Loading
Loading