diff --git a/.fusa-hara.json b/.fusa-hara.json new file mode 100644 index 0000000..14a015c --- /dev/null +++ b/.fusa-hara.json @@ -0,0 +1,103 @@ +{ + "project": "go-LIN", + "standard": "ISO 26262", + "operationalSituations": [ + { + "id": "OS-001", + "description": "Vehicle driving with an active LIN sub-bus controlling body electronics (windows, mirrors, seats, wipers, climate flaps)." + }, + { + "id": "OS-002", + "description": "Vehicle stationary, ignition on, comfort and convenience LIN functions active." + }, + { + "id": "OS-003", + "description": "Diagnostic session in progress using LIN master request (0x3C) and slave response (0x3D) frames during service or production." + }, + { + "id": "OS-004", + "description": "Industrial LIN segment driving actuators (valves, motors, lighting) under continuous master schedule execution." + } + ], + "hazards": [ + { + "id": "H-01", + "description": "Master transmits a header for the wrong frame ID — an unintended slave actuates the wrong actuator.", + "situations": ["OS-001", "OS-004"], + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "safetyGoals": ["SG-01", "SG-05"] + }, + { + "id": "H-02", + "description": "A corrupted LIN frame payload is received without error detection and an incorrect command is delivered to an actuator.", + "situations": ["OS-001", "OS-004"], + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "safetyGoals": ["SG-02"] + }, + { + "id": "H-03", + "description": "Incorrect PID parity allows a wrong frame ID to be accepted as valid.", + "situations": ["OS-001", "OS-002"], + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "safetyGoals": ["SG-01"] + }, + { + "id": "H-04", + "description": "A checksum error is not detected and corrupted data is passed to the application.", + "situations": ["OS-001", "OS-004"], + "risk": {"severity": "S2", "exposure": "E3", "controllability": "C2", "asil": "ASIL-B"}, + "safetyGoals": ["SG-02"] + }, + { + "id": "H-05", + "description": "An LDF signal is decoded with wrong bit offsets and an actuator is set to an out-of-range value.", + "situations": ["OS-001", "OS-004"], + "risk": {"severity": "S1", "exposure": "E3", "controllability": "C2", "asil": "ASIL-A"}, + "safetyGoals": ["SG-03"] + }, + { + "id": "H-06", + "description": "An E2E sequence-counter gap is not detected — a replayed or lost safety frame goes unnoticed.", + "situations": ["OS-001", "OS-003", "OS-004"], + "risk": {"severity": "S2", "exposure": "E2", "controllability": "C2", "asil": "ASIL-A"}, + "safetyGoals": ["SG-04"] + } + ], + "safetyGoals": [ + { + "id": "SG-01", + "description": "go-LIN shall correctly identify frame IDs using PID parity computation and verification.", + "hazards": ["H-01", "H-03"], + "asil": "ASIL-B", + "safeState": "Frame with an unverifiable or mismatched PID is rejected and not delivered to the application." + }, + { + "id": "SG-02", + "description": "go-LIN shall detect frame payload corruption using the LIN checksum algorithm.", + "hazards": ["H-02", "H-04"], + "asil": "ASIL-B", + "safeState": "Frame failing checksum verification is rejected and reported as an error rather than delivered." + }, + { + "id": "SG-03", + "description": "go-LIN shall correctly parse LDF signal definitions and decode frame payloads without offset errors.", + "hazards": ["H-05"], + "asil": "ASIL-A", + "safeState": "Malformed LDF input is rejected at parse time with a descriptive error; no decode occurs." + }, + { + "id": "SG-04", + "description": "go-LIN shall detect E2E sequence gaps and CRC mismatches.", + "hazards": ["H-06"], + "asil": "ASIL-A", + "safeState": "E2E-protected frame with a sequence gap or CRC mismatch is surfaced as an E2EError and not treated as valid." + }, + { + "id": "SG-05", + "description": "go-LIN shall validate all frame IDs and data lengths at API boundaries.", + "hazards": ["H-01"], + "asil": "ASIL-B", + "safeState": "Out-of-range frame ID or data length is rejected by ValidateFrame before transmission or processing." + } + ] +} diff --git a/.fusa-iec62443.json b/.fusa-iec62443.json new file mode 100644 index 0000000..d6ecc36 --- /dev/null +++ b/.fusa-iec62443.json @@ -0,0 +1,18 @@ +{ + "project": "go-LIN", + "standard": "IEC 62443-4-2", + "component_type": "embedded device / software component (SEOOC)", + "target_sl": "SL-2", + "achieved_sl": "SL-2", + "incident_resp_doc": "SECURITY.md", + "rationale": "go-LIN is a LIN protocol library used as a building block in automotive and industrial control systems. SL-2 (protection against intentional violation using simple means with low resources, generic skills and low motivation) is the appropriate target for a sub-bus protocol component: it processes untrusted frame and LDF input but does not itself terminate external network connectivity. Integrators requiring SL-3/SL-4 layer additional access control and network segmentation around the component (see SEOOC.md).", + "foundational_requirements": { + "FR1_identification_authentication": "N/A at component level — provided by the integrating ECU.", + "FR2_use_control": "N/A at component level — provided by the integrating ECU.", + "FR3_system_integrity": "ValidateFrame, PID parity, LIN checksum and E2E (CRC-16/CCITT-FALSE + sequence counter) protect frame and payload integrity.", + "FR4_data_confidentiality": "N/A — LIN is a plaintext field bus; confidentiality is out of scope for the protocol layer.", + "FR5_restricted_data_flow": "Library exposes no network listeners; data flow is bounded by the caller-provided Bus.", + "FR6_timely_response_to_events": "Errors are surfaced synchronously as typed sentinel errors (RELAY §5) for the integrator to act on.", + "FR7_resource_availability": "Bounded allocations; fuzz-tested parsers; no unbounded recursion or goroutine leaks." + } +} diff --git a/.fusa-problems.json b/.fusa-problems.json new file mode 100644 index 0000000..dfe490a --- /dev/null +++ b/.fusa-problems.json @@ -0,0 +1,4 @@ +{ + "project": "go-LIN", + "reports": null +} \ No newline at end of file diff --git a/.fusa-reqs.json b/.fusa-reqs.json index 3cfa89d..5cafcc8 100644 --- a/.fusa-reqs.json +++ b/.fusa-reqs.json @@ -3,6 +3,7 @@ "requirements": [ { "id": "REQ-LIN-001", + "level": "LLR", "title": "ValidateFrame rejects ID > 0x3F", "description": "ValidateFrame shall return an error when Frame.ID exceeds MaxID (0x3F).", "asil": "ASIL-B", @@ -11,6 +12,7 @@ }, { "id": "REQ-LIN-002", + "level": "LLR", "title": "ValidateFrame rejects empty data", "description": "ValidateFrame shall return an error when Frame.Data has zero length.", "asil": "ASIL-B", @@ -19,6 +21,7 @@ }, { "id": "REQ-LIN-003", + "level": "LLR", "title": "ValidateFrame rejects oversized data", "description": "ValidateFrame shall return an error when len(Frame.Data) exceeds MaxDataLen (8).", "asil": "ASIL-B", @@ -27,6 +30,7 @@ }, { "id": "REQ-LIN-004", + "level": "LLR", "title": "ProtectID parity bit P0", "description": "ProtectID shall compute P0 = ID0 XOR ID1 XOR ID2 XOR ID4 and place it in bit 6 of the returned PID byte.", "asil": "ASIL-B", @@ -35,6 +39,7 @@ }, { "id": "REQ-LIN-005", + "level": "LLR", "title": "ProtectID parity bit P1", "description": "ProtectID shall compute P1 = NOT(ID1 XOR ID3 XOR ID4 XOR ID5) and place it in bit 7 of the returned PID byte.", "asil": "ASIL-B", @@ -43,6 +48,7 @@ }, { "id": "REQ-LIN-006", + "level": "LLR", "title": "VerifyPID accepts correct parity", "description": "VerifyPID shall return the raw 6-bit ID and nil error when the parity bits in the PID are correct.", "asil": "ASIL-B", @@ -51,6 +57,7 @@ }, { "id": "REQ-LIN-007", + "level": "LLR", "title": "VerifyPID rejects incorrect parity", "description": "VerifyPID shall return an error when the parity bits in the supplied PID do not match those computed by ProtectID for the embedded 6-bit ID.", "asil": "ASIL-B", @@ -59,6 +66,7 @@ }, { "id": "REQ-LIN-008", + "level": "LLR", "title": "Classic checksum covers data bytes only", "description": "CalcChecksum with ClassicChecksum shall sum the data bytes only (excluding PID) using inverted carry-around 8-bit addition.", "asil": "ASIL-B", @@ -67,6 +75,7 @@ }, { "id": "REQ-LIN-009", + "level": "LLR", "title": "Enhanced checksum includes PID", "description": "CalcChecksum with EnhancedChecksum shall include the PID byte in the sum alongside the data bytes, using inverted carry-around 8-bit addition.", "asil": "ASIL-B", @@ -75,6 +84,7 @@ }, { "id": "REQ-LIN-010", + "level": "LLR", "title": "Checksum carry-around inversion", "description": "CalcChecksum shall apply carry-around addition (sum > 0xFF wraps by subtracting 0xFF, not 0x100) and invert the final sum (0xFF - sum).", "asil": "ASIL-B", @@ -83,6 +93,7 @@ }, { "id": "REQ-LIN-011", + "level": "LLR", "title": "Bus.Publish registers slave response", "description": "Bus.Publish shall register a response payload for the given frame ID so that the next SendHeader for that ID returns the registered data.", "asil": "ASIL-B", @@ -91,6 +102,7 @@ }, { "id": "REQ-LIN-012", + "level": "LLR", "title": "Bus.Subscribe delivers matching frames", "description": "Bus.Subscribe shall return a channel that receives every Frame whose ID matches any supplied Filter, or all Frames when no filters are supplied.", "asil": "ASIL-B", @@ -99,6 +111,7 @@ }, { "id": "REQ-LIN-013", + "level": "LLR", "title": "MasterBus.SendHeader returns synthesised Frame", "description": "MasterBus.SendHeader shall trigger a frame exchange for the given ID and return the resulting Frame including correct PID and checksum.", "asil": "ASIL-B", @@ -107,6 +120,7 @@ }, { "id": "REQ-LIN-014", + "level": "LLR", "title": "MasterBus.SendHeader returns ErrNoResponse", "description": "MasterBus.SendHeader shall return ErrNoResponse when no slave has registered a response for the requested frame ID.", "asil": "ASIL-B", @@ -115,6 +129,7 @@ }, { "id": "REQ-LIN-015", + "level": "LLR", "title": "ValidateFrame accepts ID = 0x3F", "description": "ValidateFrame shall return nil error when Frame.ID equals MaxID (0x3F), the boundary maximum.", "asil": "ASIL-B", @@ -123,6 +138,7 @@ }, { "id": "REQ-LIN-016", + "level": "LLR", "title": "ValidateFrame accepts data length 1", "description": "ValidateFrame shall return nil error when len(Frame.Data) equals 1, the boundary minimum.", "asil": "ASIL-B", @@ -131,6 +147,7 @@ }, { "id": "REQ-LIN-017", + "level": "LLR", "title": "ValidateFrame accepts data length 8", "description": "ValidateFrame shall return nil error when len(Frame.Data) equals MaxDataLen (8), the boundary maximum.", "asil": "ASIL-B", @@ -139,6 +156,7 @@ }, { "id": "REQ-LIN-018", + "level": "LLR", "title": "ProtectID preserves lower 6 bits", "description": "For any id in 0x00–0x3F, (ProtectID(id) & 0x3F) shall equal id.", "asil": "ASIL-B", @@ -147,6 +165,7 @@ }, { "id": "REQ-LIN-019", + "level": "LLR", "title": "Bus.Publish with nil removes registration", "description": "Bus.Publish(id, nil) shall remove any previously registered response for id; subsequent SendHeader for that id shall return ErrNoResponse.", "asil": "ASIL-B", @@ -155,6 +174,7 @@ }, { "id": "REQ-LIN-020", + "level": "LLR", "title": "Bus.Subscribe with All=true delivers every frame", "description": "A Filter with All=true shall match every Frame regardless of ID; Bus.Subscribe with such a filter delivers all frames on the bus.", "asil": "ASIL-B", @@ -163,6 +183,7 @@ }, { "id": "REQ-LIN-021", + "level": "LLR", "title": "ErrNoResponse is a non-nil sentinel error", "description": "ErrNoResponse shall be a non-nil error value that callers can test with errors.Is(err, lin.ErrNoResponse).", "asil": "ASIL-B", @@ -171,6 +192,7 @@ }, { "id": "REQ-VIRT-001", + "level": "LLR", "title": "New returns initialised bus", "description": "virtual.New shall return a Bus with an empty response table, no subscribers, and nil error.", "asil": "ASIL-B", @@ -179,6 +201,7 @@ }, { "id": "REQ-VIRT-002", + "level": "LLR", "title": "Publish stores response", "description": "Publish(id, data) shall atomically store a copy of data as the response for id, replacing any prior entry.", "asil": "ASIL-B", @@ -187,6 +210,7 @@ }, { "id": "REQ-VIRT-003", + "level": "LLR", "title": "Publish(nil) removes response", "description": "Publish(id, nil) shall remove the registered response for id so that subsequent SendHeader returns ErrNoResponse.", "asil": "ASIL-B", @@ -195,6 +219,7 @@ }, { "id": "REQ-VIRT-004", + "level": "LLR", "title": "Publish rejects ID > MaxID", "description": "Publish shall return an error when id > 0x3F.", "asil": "ASIL-B", @@ -203,6 +228,7 @@ }, { "id": "REQ-VIRT-005", + "level": "LLR", "title": "Publish after Close returns error", "description": "Publish shall return an error when called after Close.", "asil": "ASIL-B", @@ -211,6 +237,7 @@ }, { "id": "REQ-VIRT-006", + "level": "LLR", "title": "SendHeader computes correct PID", "description": "SendHeader shall set Frame.ID to the requested id and compute the PID using ProtectID before broadcasting.", "asil": "ASIL-B", @@ -219,6 +246,7 @@ }, { "id": "REQ-VIRT-007", + "level": "LLR", "title": "SendHeader computes correct checksum", "description": "SendHeader shall set Frame.Checksum using CalcChecksum with the response's ChecksumType.", "asil": "ASIL-B", @@ -227,6 +255,7 @@ }, { "id": "REQ-VIRT-008", + "level": "LLR", "title": "SendHeader broadcasts to subscribers", "description": "SendHeader shall deliver the synthesised Frame to every subscriber whose filter matches the frame ID.", "asil": "ASIL-B", @@ -235,6 +264,7 @@ }, { "id": "REQ-VIRT-009", + "level": "LLR", "title": "SendHeader returns ErrNoResponse", "description": "SendHeader shall return ErrNoResponse when no response is registered for the requested id.", "asil": "ASIL-B", @@ -243,6 +273,7 @@ }, { "id": "REQ-VIRT-010", + "level": "LLR", "title": "SendHeader rejects ID > MaxID", "description": "SendHeader shall return an error when id > 0x3F.", "asil": "ASIL-B", @@ -251,6 +282,7 @@ }, { "id": "REQ-VIRT-011", + "level": "LLR", "title": "Subscribe exact filter isolates by ID", "description": "A subscriber with Filter{ID: x} shall not receive frames whose ID differs from x.", "asil": "ASIL-B", @@ -259,6 +291,7 @@ }, { "id": "REQ-VIRT-012", + "level": "LLR", "title": "Subscribe All filter receives every frame", "description": "A subscriber with Filter{All: true} shall receive every frame regardless of ID.", "asil": "ASIL-B", @@ -267,6 +300,7 @@ }, { "id": "REQ-VIRT-013", + "level": "LLR", "title": "Full subscriber channel drops frames without blocking", "description": "When a subscriber channel is full, SendHeader shall drop the frame for that subscriber rather than blocking.", "asil": "ASIL-B", @@ -275,6 +309,7 @@ }, { "id": "REQ-VIRT-014", + "level": "LLR", "title": "Multiple subscribers each receive independently", "description": "When multiple subscribers match a frame, each shall receive a copy independently.", "asil": "ASIL-B", @@ -283,6 +318,7 @@ }, { "id": "REQ-VIRT-015", + "level": "LLR", "title": "Close closes all subscriber channels", "description": "Close shall close every subscriber channel so that range-loops over those channels terminate.", "asil": "ASIL-B", @@ -291,6 +327,7 @@ }, { "id": "REQ-VIRT-016", + "level": "LLR", "title": "Close is idempotent", "description": "Close shall return nil and have no observable effect when called a second time.", "asil": "ASIL-B", @@ -299,6 +336,7 @@ }, { "id": "REQ-VIRT-017", + "level": "LLR", "title": "SendHeader after Close returns error", "description": "SendHeader shall return an error when called after Close.", "asil": "ASIL-B", @@ -307,6 +345,7 @@ }, { "id": "REQ-VIRT-018", + "level": "LLR", "title": "Concurrent access is data-race free", "description": "Publish, SendHeader, Subscribe, and Close may be called concurrently from multiple goroutines without data races.", "asil": "ASIL-B", @@ -315,6 +354,7 @@ }, { "id": "REQ-VIRT-019", + "level": "LLR", "title": "Publish stores a defensive copy", "description": "Publish shall copy the supplied data slice; subsequent mutation of the caller's slice shall not affect the stored response.", "asil": "ASIL-B", @@ -323,6 +363,7 @@ }, { "id": "REQ-LDF-001", + "level": "LLR", "title": "Parse extracts protocol version", "description": "Parse shall populate DB.ProtocolVersion from the LIN_protocol_version field.", "asil": "ASIL-A", @@ -331,6 +372,7 @@ }, { "id": "REQ-LDF-002", + "level": "LLR", "title": "Parse extracts baud rate", "description": "Parse shall populate DB.Speed (kbps) from the LIN_speed field.", "asil": "ASIL-A", @@ -339,6 +381,7 @@ }, { "id": "REQ-LDF-003", + "level": "LLR", "title": "Parse extracts master node name", "description": "Parse shall populate DB.MasterNode from the Nodes Master declaration.", "asil": "ASIL-A", @@ -347,6 +390,7 @@ }, { "id": "REQ-LDF-004", + "level": "LLR", "title": "Parse extracts slave node list", "description": "Parse shall populate DB.SlaveNodes from the Nodes Slaves declaration.", "asil": "ASIL-A", @@ -355,6 +399,7 @@ }, { "id": "REQ-LDF-005", + "level": "LLR", "title": "Parse populates frame descriptors", "description": "Parse shall populate frame descriptors (name, ID, publisher, length) from the LDF Frames section.", "asil": "ASIL-A", @@ -363,6 +408,7 @@ }, { "id": "REQ-LDF-006", + "level": "LLR", "title": "Parse populates signal-to-bit-offset mappings", "description": "Parse shall populate SignalRef entries (signal name and bit offset) for each frame.", "asil": "ASIL-A", @@ -371,6 +417,7 @@ }, { "id": "REQ-LDF-007", + "level": "LLR", "title": "Parse populates signal bit width", "description": "Parse shall populate Signal.BitWidth from the signal definition in the LDF Signals section.", "asil": "ASIL-A", @@ -379,6 +426,7 @@ }, { "id": "REQ-LDF-008", + "level": "LLR", "title": "Parse populates signal publisher", "description": "Parse shall populate Signal.Publisher from the signal definition.", "asil": "ASIL-A", @@ -387,6 +435,7 @@ }, { "id": "REQ-LDF-009", + "level": "LLR", "title": "Decode uses LSB-first Intel byte order", "description": "Decode shall extract signal values using LSB-first (Intel) bit ordering: bit 0 of the signal maps to bitOffset in the payload.", "asil": "ASIL-A", @@ -395,6 +444,7 @@ }, { "id": "REQ-LDF-010", + "level": "LLR", "title": "Decode returns nil for unknown frame ID", "description": "Decode shall return nil when the frame ID is not present in the parsed LDF.", "asil": "ASIL-A", @@ -403,6 +453,7 @@ }, { "id": "REQ-LDF-011", + "level": "LLR", "title": "Parse populates schedule table entries", "description": "Parse shall populate schedule tables with ScheduleEntry{ID, DelayMs} from the LDF Schedule_tables section.", "asil": "ASIL-A", @@ -411,6 +462,7 @@ }, { "id": "REQ-LDF-012", + "level": "LLR", "title": "Frame() returns nil for unknown ID", "description": "DB.Frame() shall return nil when no frame with the given ID was declared in the LDF.", "asil": "ASIL-A", @@ -419,6 +471,7 @@ }, { "id": "REQ-LDF-013", + "level": "LLR", "title": "Signal() returns nil for unknown name", "description": "DB.Signal() shall return nil when no signal with the given name was declared in the LDF.", "asil": "ASIL-A", @@ -427,6 +480,7 @@ }, { "id": "REQ-LDF-014", + "level": "LLR", "title": "Parse does not panic on arbitrary input", "description": "Parse shall return a non-nil DB (possibly empty) and a non-nil error (or nil) for any input; it shall never panic.", "asil": "ASIL-A", @@ -435,6 +489,7 @@ }, { "id": "REQ-LDF-015", + "level": "LLR", "title": "Frames() returns a defensive copy", "description": "DB.Frames() shall return a new map; mutations to the returned map shall not affect DB's internal frame table.", "asil": "ASIL-A", @@ -443,6 +498,7 @@ }, { "id": "REQ-MASTER-001", + "level": "LLR", "title": "New returns non-nil Node", "description": "master.New shall return a non-nil *Node backed by the supplied MasterBus.", "asil": "ASIL-B", @@ -451,6 +507,7 @@ }, { "id": "REQ-MASTER-002", + "level": "LLR", "title": "Node.SendHeader delegates to bus", "description": "Node.SendHeader shall call bus.SendHeader and return the Frame and error unchanged.", "asil": "ASIL-B", @@ -459,6 +516,7 @@ }, { "id": "REQ-MASTER-003", + "level": "LLR", "title": "Run iterates schedule in order", "description": "Node.Run shall process schedule entries in the order they appear in the schedule table, restarting from the first entry after the last.", "asil": "ASIL-B", @@ -467,6 +525,7 @@ }, { "id": "REQ-MASTER-004", + "level": "LLR", "title": "Run calls SendHeader for each slot", "description": "For each schedule slot, Run shall call MasterBus.SendHeader with the slot's frame ID.", "asil": "ASIL-B", @@ -475,6 +534,7 @@ }, { "id": "REQ-MASTER-005", + "level": "LLR", "title": "Run waits slot delay between exchanges", "description": "Run shall wait slot.DelayMs milliseconds after each frame exchange before processing the next slot.", "asil": "ASIL-B", @@ -483,6 +543,7 @@ }, { "id": "REQ-MASTER-006", + "level": "LLR", "title": "Run invokes OnFrame on success", "description": "Run shall call the OnFrame callback with the received Frame when SendHeader succeeds.", "asil": "ASIL-B", @@ -491,6 +552,7 @@ }, { "id": "REQ-MASTER-007", + "level": "LLR", "title": "Run invokes OnError on failure", "description": "Run shall call the OnError callback when SendHeader returns a non-nil error.", "asil": "ASIL-B", @@ -499,6 +561,7 @@ }, { "id": "REQ-MASTER-008", + "level": "LLR", "title": "Run returns on context cancellation", "description": "Run shall return ctx.Err() when ctx is cancelled or expires.", "asil": "ASIL-B", @@ -507,6 +570,7 @@ }, { "id": "REQ-MASTER-009", + "level": "LLR", "title": "Run returns error for empty schedule", "description": "Run shall return an error immediately when the schedule table is empty.", "asil": "ASIL-B", @@ -515,6 +579,7 @@ }, { "id": "REQ-MASTER-010", + "level": "LLR", "title": "SetSchedule rejects empty schedule", "description": "SetSchedule shall return an error when called with nil or a zero-length slice.", "asil": "ASIL-B", @@ -523,6 +588,7 @@ }, { "id": "REQ-MASTER-011", + "level": "LLR", "title": "SetSchedule rejects invalid frame ID", "description": "SetSchedule shall return an error when any entry has ID > MaxID (0x3F).", "asil": "ASIL-B", @@ -531,6 +597,7 @@ }, { "id": "REQ-MASTER-012", + "level": "LLR", "title": "SetSchedule stores a defensive copy", "description": "SetSchedule shall copy the supplied slice; subsequent mutation of the caller's slice shall not affect the active schedule.", "asil": "ASIL-B", @@ -539,6 +606,7 @@ }, { "id": "REQ-MASTER-013", + "level": "LLR", "title": "Run continues after per-slot errors", "description": "Run shall continue to the next schedule slot when SendHeader returns an error (e.g., ErrNoResponse); it shall not abort the schedule.", "asil": "ASIL-B", @@ -547,6 +615,7 @@ }, { "id": "REQ-SLAVE-001", + "level": "LLR", "title": "New returns ready slave node", "description": "slave.New shall return a non-nil Node with no registered responses.", "asil": "ASIL-B", @@ -555,6 +624,7 @@ }, { "id": "REQ-SLAVE-002", + "level": "LLR", "title": "SetResponse registers response via Publish", "description": "SetResponse shall call bus.Publish(id, data) and track id in its registered ID set.", "asil": "ASIL-B", @@ -563,6 +633,7 @@ }, { "id": "REQ-SLAVE-003", + "level": "LLR", "title": "SetResponse(nil) removes registration", "description": "SetResponse(id, nil) shall call bus.Publish(id, nil) and remove id from the registered ID set.", "asil": "ASIL-B", @@ -571,6 +642,7 @@ }, { "id": "REQ-SLAVE-004", + "level": "LLR", "title": "SetResponse rejects ID > MaxID", "description": "SetResponse shall return an error when id > 0x3F without calling bus.Publish.", "asil": "ASIL-B", @@ -579,6 +651,7 @@ }, { "id": "REQ-SLAVE-005", + "level": "LLR", "title": "RegisteredIDs reflects current state", "description": "RegisteredIDs shall return exactly the set of IDs for which a non-nil response is currently registered.", "asil": "ASIL-B", @@ -587,6 +660,7 @@ }, { "id": "REQ-SLAVE-006", + "level": "LLR", "title": "Subscribe delegates to bus", "description": "Subscribe shall delegate to bus.Subscribe and return the resulting channel and error unchanged.", "asil": "ASIL-B", @@ -595,6 +669,7 @@ }, { "id": "REQ-SLAVE-007", + "level": "LLR", "title": "RegisteredIDs returns empty slice when none registered", "description": "RegisteredIDs shall return an empty (not nil) slice when no responses are currently registered.", "asil": "ASIL-B", @@ -603,6 +678,7 @@ }, { "id": "REQ-SLAVE-008", + "level": "LLR", "title": "SetResponse overwrites previous registration for same ID", "description": "When SetResponse is called for an ID that already has a registration, the new data shall replace the previous registration.", "asil": "ASIL-B", @@ -611,6 +687,7 @@ }, { "id": "REQ-SAFETY-001", + "level": "LLR", "title": "DataID embedded in header bytes 0-1", "description": "Protect shall write Config.DataID as a little-endian uint16 into bytes 0-1 of the protected payload.", "asil": "ASIL-B", @@ -619,6 +696,7 @@ }, { "id": "REQ-SAFETY-002", + "level": "LLR", "title": "SourceID embedded in header bytes 2-3", "description": "Protect shall write Config.SourceID as a little-endian uint16 into bytes 2-3 of the protected payload.", "asil": "ASIL-B", @@ -627,6 +705,7 @@ }, { "id": "REQ-SAFETY-003", + "level": "LLR", "title": "SequenceCounter starts at 0 and increments", "description": "The Protector's SequenceCounter shall start at 0 and increment by 1 with each Protect call.", "asil": "ASIL-B", @@ -635,6 +714,7 @@ }, { "id": "REQ-SAFETY-004", + "level": "LLR", "title": "SequenceCounter embedded in header bytes 4-7", "description": "Protect shall write the current SequenceCounter as a little-endian uint32 into bytes 4-7.", "asil": "ASIL-B", @@ -643,6 +723,7 @@ }, { "id": "REQ-SAFETY-005", + "level": "LLR", "title": "CRC computed over header and payload", "description": "Protect shall compute CRC-16/CCITT-FALSE (poly=0x1021, init=0xFFFF) over the header with CRC slot zeroed, concatenated with the payload.", "asil": "ASIL-B", @@ -651,6 +732,7 @@ }, { "id": "REQ-SAFETY-006", + "level": "LLR", "title": "CRC embedded in header bytes 8-9", "description": "Protect shall write the computed CRC as a little-endian uint16 into bytes 8-9.", "asil": "ASIL-B", @@ -659,6 +741,7 @@ }, { "id": "REQ-SAFETY-007", + "level": "LLR", "title": "Unwrap returns ErrHeaderTooShort for payload < 10 bytes", "description": "Unwrap shall return E2EError{Kind: ErrHeaderTooShort} when the input is shorter than 10 bytes.", "asil": "ASIL-B", @@ -667,6 +750,7 @@ }, { "id": "REQ-SAFETY-008", + "level": "LLR", "title": "Unwrap detects byte corruption via CRC", "description": "Unwrap shall return E2EError{Kind: ErrCRCMismatch} when the recomputed CRC does not match bytes 8-9.", "asil": "ASIL-B", @@ -675,6 +759,7 @@ }, { "id": "REQ-SAFETY-009", + "level": "LLR", "title": "Unwrap detects sequence gap", "description": "Unwrap shall return E2EError{Kind: ErrSequenceGap} when the received counter is not exactly lastSeq+1 (after the first received message).", "asil": "ASIL-B", @@ -683,6 +768,7 @@ }, { "id": "REQ-SAFETY-010", + "level": "LLR", "title": "Unwrap returns original payload on success", "description": "Unwrap shall strip the 10-byte header and return a copy of the original payload when all checks pass.", "asil": "ASIL-B", @@ -691,6 +777,7 @@ }, { "id": "REQ-SAFETY-011", + "level": "LLR", "title": "Protect/Unwrap round-trip preserves payload", "description": "For any payload p, Unwrap(Protect(p)) shall return a byte-for-byte copy of p with nil error when Protector and Receiver share the same Config.", "asil": "ASIL-B", @@ -699,6 +786,7 @@ }, { "id": "REQ-SAFETY-012", + "level": "LLR", "title": "Protect output length equals 10 + len(payload)", "description": "The byte slice returned by Protect shall have length exactly headerSize (10) + len(payload).", "asil": "ASIL-B", @@ -707,6 +795,7 @@ }, { "id": "REQ-SAFETY-013", + "level": "LLR", "title": "Unwrap accepts first message with any counter value", "description": "A newly created Receiver shall accept the first Unwrap call regardless of the counter value in the header.", "asil": "ASIL-B", @@ -715,6 +804,7 @@ }, { "id": "REQ-SAFETY-014", + "level": "LLR", "title": "Protect is safe for concurrent calls", "description": "Protect may be called concurrently from multiple goroutines; each call shall receive a unique, monotonically increasing counter value.", "asil": "ASIL-B", @@ -723,6 +813,7 @@ }, { "id": "REQ-SAFETY-015", + "level": "LLR", "title": "Unwrap returns an independent payload copy", "description": "The payload returned by Unwrap shall be a fresh copy; mutations to it shall not affect future Unwrap calls or internal state.", "asil": "ASIL-B", @@ -731,6 +822,7 @@ }, { "id": "REQ-SEOOC-001", + "level": "HLR", "title": "Integrating system provides physical LIN layer", "description": "The system integrating go-LIN shall provide a correct LIN physical layer (transceiver, break detection, bit timing). go-LIN operates above the physical layer.", "asil": "ASIL-B", @@ -739,6 +831,7 @@ }, { "id": "REQ-SEOOC-002", + "level": "HLR", "title": "Integrating system calls ValidateFrame on external data", "description": "The integrating system shall call ValidateFrame on any frame received from external hardware before passing it to go-LIN APIs.", "asil": "ASIL-B", @@ -747,6 +840,7 @@ }, { "id": "REQ-SEOOC-003", + "level": "HLR", "title": "Integrating system validates frame ID semantics", "description": "The integrating system shall verify that a received frame ID corresponds to the intended actuator or sensor before acting on the payload.", "asil": "ASIL-B", @@ -755,6 +849,7 @@ }, { "id": "REQ-SEOOC-004", + "level": "HLR", "title": "Integration: virtual bus delivers E2E payload intact", "description": "When Protect(payload) is published on the virtual bus and retrieved via SendHeader + Unwrap, the result shall equal payload with nil error.", "asil": "ASIL-B", @@ -763,6 +858,7 @@ }, { "id": "REQ-SEOOC-005", + "level": "HLR", "title": "Integration: master-slave round-trip via virtual bus", "description": "A slave registered via slave.SetResponse shall have its payload returned by master.Node.SendHeader via the virtual bus.", "asil": "ASIL-B", @@ -771,6 +867,7 @@ }, { "id": "REQ-SEOOC-006", + "level": "HLR", "title": "Integration: LDF schedule IDs are valid", "description": "LDF schedule table entries shall reference only frame IDs that appear in the Frames section and are within 0x00–0x3F.", "asil": "ASIL-A", @@ -779,6 +876,7 @@ }, { "id": "REQ-SEOOC-007", + "level": "HLR", "title": "Integrating system handles ErrNoResponse safely", "description": "The integrating system shall handle lin.ErrNoResponse without propagating the absence of a slave as a safety-critical output change.", "asil": "ASIL-B", @@ -787,6 +885,7 @@ }, { "id": "REQ-SEOOC-008", + "level": "HLR", "title": "Integrating system routes safety-critical frames through safety package", "description": "The integrating system shall apply safety.Protect/Unwrap to all frame payloads classified as safety-critical (ASIL >= ASIL-B).", "asil": "ASIL-B", @@ -795,6 +894,7 @@ }, { "id": "REQ-SEOOC-009", + "level": "HLR", "title": "Integrating system provides monotonic clock for timing", "description": "The integrating system shall provide a monotonic clock with at least 1 ms resolution for LIN schedule slot timing.", "asil": "ASIL-B", @@ -803,6 +903,7 @@ }, { "id": "REQ-ADAPT-001", + "level": "LLR", "title": "Adapt returns a LIN relay.Node", "description": "Adapt shall return a non-nil relay.Node whose Protocol method reports relay.LIN.", "asil": "ASIL-B", @@ -811,6 +912,7 @@ }, { "id": "REQ-ADAPT-002", + "level": "LLR", "title": "linNode.Send publishes payload for a valid frame ID", "description": "linNode.Send shall publish msg.Payload as the slave response for the frame ID parsed from msg.ID when the ID is in range 0-63.", "asil": "ASIL-B", @@ -819,6 +921,7 @@ }, { "id": "REQ-ADAPT-003", + "level": "LLR", "title": "linNode.Send rejects an out-of-range frame ID", "description": "linNode.Send shall return a non-nil error and shall not publish when msg.ID is not a decimal string in range 0-63.", "asil": "ASIL-B", @@ -827,6 +930,7 @@ }, { "id": "REQ-ADAPT-004", + "level": "LLR", "title": "linNode.Subscribe converts frames to relay.Message", "description": "linNode.Subscribe shall return a channel that yields one relay.Message per received frame, each carrying the frame's canonical ToMessage() conversion.", "asil": "ASIL-B", @@ -835,6 +939,7 @@ }, { "id": "REQ-ADAPT-005", + "level": "LLR", "title": "linNode.Close closes the underlying bus", "description": "linNode.Close shall close the wrapped Bus and return its error unchanged.", "asil": "ASIL-B", @@ -843,11 +948,66 @@ }, { "id": "REQ-MOCK-001", + "level": "LLR", "title": "mock.New returns a MasterBus-satisfying bus", "description": "mock.New shall return a non-nil bus that satisfies lin.MasterBus (Publish, Subscribe, SendHeader, SetSchedule, Close).", "asil": "ASIL-B", "rationale": "RELAY §7 rule 4 / §13.7: a mandatory mock transport must implement the full bus contract so applications can be tested without hardware.", "tags": ["mock", "relay"] + }, + { + "id": "REQ-SEC-001", + "level": "LLR", + "title": "LDF parser is robust against malformed input", + "description": "ldf.Parse shall return an error and shall not panic when given malformed, truncated, or hostile LDF input.", + "asil": "ASIL-A", + "rationale": "ISO 21434 / TARA: LDF content is untrusted input (CWE-20); a panic or hang on malformed input is a denial-of-service vector.", + "tags": ["security", "cyber", "robustness", "ldf"] + }, + { + "id": "REQ-SEC-002", + "level": "LLR", + "title": "E2E CRC detects payload tampering", + "description": "Receiver.Unwrap shall return an ErrCRCMismatch error when any byte of the protected header or payload has been altered.", + "asil": "ASIL-B", + "rationale": "ISO 21434 / TARA: detects tampering (STRIDE-T, CWE-354) of safety-relevant payloads in transit on the bus.", + "tags": ["security", "cyber", "integrity", "safety"] + }, + { + "id": "REQ-SEC-003", + "level": "LLR", + "title": "E2E sequence counter detects replay", + "description": "Receiver.Unwrap shall return an ErrSequenceGap error when a frame's sequence counter indicates a replayed or skipped frame.", + "asil": "ASIL-B", + "rationale": "ISO 21434 / TARA: detects replay/spoofing (STRIDE-S, CWE-294) of previously valid safety frames.", + "tags": ["security", "cyber", "replay", "safety"] + }, + { + "id": "REQ-SEC-004", + "level": "LLR", + "title": "Frame validation bounds untrusted input at the trust boundary", + "description": "ValidateFrame shall reject any frame whose ID exceeds 0x3F or whose data length is outside 1-8 bytes before it is transmitted or processed.", + "asil": "ASIL-B", + "rationale": "IEC 62443 FR3 / TARA: enforces input bounds (CWE-20) at the library trust boundary so out-of-range untrusted frames cannot propagate.", + "tags": ["security", "cyber", "validation"] + }, + { + "id": "REQ-SEC-005", + "level": "LLR", + "title": "FromMessage rejects out-of-range IDs from untrusted envelopes", + "description": "FromMessage shall return an error when the relay.Message ID does not parse to a decimal frame ID in range 0-63.", + "asil": "ASIL-B", + "rationale": "IEC 62443 FR3 / TARA: a relay.Message arriving from another protocol node is untrusted; an out-of-range ID must not yield a usable Frame.", + "tags": ["security", "cyber", "validation", "relay"] + }, + { + "id": "REQ-SEC-006", + "level": "LLR", + "title": "Interop convert driver rejects invalid input safely", + "description": "The CLI convert command shall reject a structurally invalid frame with the ErrInvalidFrame sentinel name on stderr and a non-zero exit code, without emitting a converted message.", + "asil": "ASIL-B", + "rationale": "ISO 21434 / TARA: the convert driver processes untrusted JSON on stdin; invalid input must fail closed (CWE-20) and not leak a partial conversion.", + "tags": ["security", "cyber", "validation", "cli"] } ] } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 80dc9c4..22c69aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,39 @@ jobs: name: coverage path: coverage.out + # ── Per-package coverage floor ──────────────────────────────────────────── + # Library packages must hold >= 85% statement coverage. cmd/ (CLI glue with + # os.Exit / signal handling) and examples/ are excluded. + coverage-gate: + name: Coverage floor (library packages >= 85%) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version: "1.25" + + - name: Enforce per-package coverage floor + shell: bash + run: | + FLOOR=85 + fail=0 + while read -r line; do + pkg=$(echo "$line" | grep -oE 'github.com/[^[:space:]]+' | head -1) + cov=$(echo "$line" | grep -oE '[0-9.]+%' | head -1 | tr -d '%') + case "$pkg" in + */cmd/*|*/examples/*) echo "skip $pkg (excluded)"; continue ;; + esac + [ -z "$cov" ] && continue + if awk "BEGIN{exit !($cov < $FLOOR)}"; then + echo "FAIL $pkg ${cov}% < ${FLOOR}%"; fail=1 + else + echo "ok $pkg ${cov}%" + fi + done < <(go test -cover ./... 2>/dev/null | grep 'coverage:') + exit $fail + # ── Benchmark smoke run ─────────────────────────────────────────────────── benchmark-smoke: name: Benchmark smoke (ubuntu-latest) @@ -165,20 +198,52 @@ jobs: - name: gofusa vuln (dependency vulnerability scan) run: gofusa vuln + - name: gofusa trace (gate on 100% requirements tested) + run: gofusa trace -sec-tested 100 + + - name: gofusa cyber (cybersecurity analysis) + run: gofusa cyber + + - name: gofusa vuln (dependency vulnerability scan) + run: gofusa vuln + - name: gofusa qualify (tool qualification suite) run: gofusa qualify - name: gofusa verify (test evidence bundle) - run: gofusa verify || true + run: gofusa verify - - name: gofusa tara (threat analysis, ISO 21434) - run: gofusa tara || true + - name: gofusa coverage (structural coverage report) + run: | + go test -coverprofile=coverage.out ./... + gofusa coverage -format json -output coverage-report.json - - name: gofusa fmea (dFMEA) - run: gofusa fmea || true + - name: gofusa hara (validate HARA structured data) + run: gofusa hara show > /dev/null + + - name: gofusa boundary / sci / coupling / tara / fmea (evidence) + run: | + gofusa boundary + gofusa sci -output sci.json + gofusa coupling + gofusa tara + gofusa fmea -cyber - name: gofusa release (SBOM + build provenance, SLSA) - run: gofusa release || true + run: gofusa release + + - name: Inject build provenance builder (SLSA L2 / IEC 62443 CR 1.4) + run: | + python3 - <<'PY' + import json, os + p = "provenance.json" + d = json.load(open(p)) + server = os.environ.get("GITHUB_SERVER_URL", "https://github.com") + repo = os.environ.get("GITHUB_REPOSITORY", "SoundMatt/go-LIN") + d["builder"] = f"{server}/{repo}/.github/workflows/ci.yml@{os.environ.get('GITHUB_REF','refs/heads/main')}" + json.dump(d, open(p, "w"), indent=2) + print("builder:", d["builder"]) + PY - name: gofusa audit-pack (bundle all evidence) run: gofusa audit-pack --output gofusa-audit-pack.zip || true @@ -190,3 +255,66 @@ jobs: name: gofusa-safety-evidence path: gofusa-audit-pack.zip if-no-files-found: ignore + + # ── Standards compliance gates (ISO 26262 / IEC 61508 / ISO 21434 / + # IEC 62443 / DO-178C / UN R.155 / SLSA) ────────────────────────────────── + # Generates the full evidence set, then runs every go-FuSa compliance gap + # report. Each report exits non-zero on a GAP or FAIL, so this job fails if + # the project drifts out of compliance with any standard. + compliance: + name: Standards compliance (ISO / IEC / DO / UNECE / SLSA) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v5 + + - uses: actions/setup-go@v6 + with: + go-version: "1.25" + + - name: Install go-FuSa + run: go install github.com/SoundMatt/go-FuSa/cmd/gofusa@v0.30.0 + + - name: Generate evidence + run: | + go test -coverprofile=coverage.out ./... + gofusa check ./... + gofusa cyber + gofusa vuln + gofusa qualify + gofusa verify + gofusa coverage -format json -output coverage-report.json + gofusa boundary + gofusa sci -output sci.json + gofusa coupling + gofusa tara + gofusa fmea -cyber + gofusa safety-case + gofusa check --output cyber-plan.json + gofusa release + + - name: Inject build provenance builder (SLSA L2 / IEC 62443 CR 1.4) + run: | + python3 - <<'PY' + import json, os + p = "provenance.json" + d = json.load(open(p)) + server = os.environ.get("GITHUB_SERVER_URL", "https://github.com") + repo = os.environ.get("GITHUB_REPOSITORY", "SoundMatt/go-LIN") + d["builder"] = f"{server}/{repo}/.github/workflows/ci.yml@{os.environ.get('GITHUB_REF','refs/heads/main')}" + json.dump(d, open(p, "w"), indent=2) + PY + + - name: ISO 26262 (Part 6) + run: gofusa iso26262 + - name: IEC 61508 (Parts 1-3) + run: gofusa iec61508 + - name: ISO 21434 (cybersecurity) + run: gofusa iso21434 + - name: IEC 62443-4-2 (IACS cybersecurity) + run: gofusa iec62443 + - name: DO-178C (Annex A) + run: gofusa do178 + - name: UN R.155 (CSMS) + run: gofusa unece + - name: SLSA v1.0 (supply chain) + run: gofusa slsa diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 95a651a..e1c32b1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,6 +34,11 @@ jobs: - name: Regenerate TARA (threat analysis, ISO 21434) run: gofusa tara + - name: Regenerate architecture boundary + configuration index + run: | + gofusa boundary + gofusa sci -output sci.json + - name: Generate test evidence bundle (closes safety-case verify gap) run: gofusa verify || true @@ -43,6 +48,20 @@ jobs: - name: Regenerate SBOM, provenance, and artifact manifest run: gofusa release + - name: Inject build provenance builder (SLSA L2 / IEC 62443 CR 1.4) + run: | + python3 - <<'PY' + import json, os + p = "provenance.json" + d = json.load(open(p)) + server = os.environ.get("GITHUB_SERVER_URL", "https://github.com") + repo = os.environ.get("GITHUB_REPOSITORY", "SoundMatt/go-LIN") + ref = os.environ.get("GITHUB_REF", "refs/heads/main") + d["builder"] = f"{server}/{repo}/.github/workflows/release.yml@{ref}" + json.dump(d, open(p, "w"), indent=2) + print("builder:", d["builder"]) + PY + - name: Configure git run: | git config user.name "github-actions[bot]" @@ -53,6 +72,7 @@ jobs: TAG="${{ github.ref_name }}" git add fmea.csv fmea.json \ tara.json tara.md \ + boundary.mermaid boundary.dot sci.json \ safety-case.json safety-case.md safety-case.mermaid \ sbom.json provenance.json artifact-manifest.json if git diff --cached --quiet; then diff --git a/.gitignore b/.gitignore index fb5f616..de44388 100644 --- a/.gitignore +++ b/.gitignore @@ -23,11 +23,17 @@ go.work.sum *.swp *.swo -# go-FuSa transient gate reports (regenerated by the CI lifecycle each run) +# go-FuSa transient gate reports (regenerated by the CI lifecycle each run). +# Committed evidence (boundary, sci, fmea, safety-case, sbom, provenance, tara) +# plus the source-of-truth inputs (.fusa-reqs/.fusa-hara/.fusa-iec62443, plan +# docs) are tracked; the per-run gate outputs below are not. check-report.json cyber-report.json qualify-report.json vuln.json +cyber-plan.json +coupling-report.json +coverage-report.json gofusa-audit-pack.zip .fusa-evidence.json diff --git a/INCIDENT-RESPONSE.md b/INCIDENT-RESPONSE.md new file mode 100644 index 0000000..e5d97a9 --- /dev/null +++ b/INCIDENT-RESPONSE.md @@ -0,0 +1,60 @@ +# Cyber-Incident Response Plan + +**Component:** go-LIN (LIN protocol library, SEOOC) +**Standard:** IEC 62443-4-2 CR 6.2.1 · ISO/SAE 21434 clause 13 (incident response) + +This plan defines how a confirmed cybersecurity incident affecting go-LIN is +handled. Vulnerability **reporting and disclosure** are covered in +[SECURITY.md](SECURITY.md); this document covers the **response process** once +an incident is confirmed. + +## Roles + +| Role | Responsibility | +|------|----------------| +| Maintainer (Matt Jones) | Incident lead: triage, decisions, disclosure | +| Reporter | Provides details, validates the fix | +| Integrators | Apply the patched release in their ECU/system context | + +## Severity classification + +Severity is assigned with CVSS v3.1 and cross-referenced to the +[TARA](tara.md) threat that materialised: + +| Severity | CVSS | Initial response target | +|----------|------|-------------------------| +| Critical | 9.0–10.0 | interim mitigation within 48 h | +| High | 7.0–8.9 | interim mitigation within 5 business days | +| Medium | 4.0–6.9 | fix in next patch release | +| Low | 0.1–3.9 | fix in next scheduled release | + +## Response workflow + +1. **Detect / intake** — register the incident in the problem-report log + (`.fusa-problems.json`) with a `security` classification and link the + originating report. +2. **Contain** — determine which supported versions are affected; if the + threat is actively exploited, publish an interim mitigation advisory and + notify known integrators. +3. **Eradicate** — develop and test the fix on a private branch. Before + release, the **full go-FuSa lifecycle** (`check`, `trace -req-coverage 100`, + `cyber`, `vuln`, `qualify`) and **RELAY conformance** (`relay conform + --strict`, `relay interop --protocol LIN`) must pass. +4. **Recover** — cut a patched release, publish the GitHub Security Advisory, + request a CVE where warranted, and refresh `vuln.json`, `cyber-report.json` + and the TARA. +5. **Post-incident review** — record root cause and corrective actions in the + safety case; if the attack surface or threat model changed, regenerate the + TARA (`gofusa tara`) and, if a new hazard emerged, the HARA. + +## Communication + +- Private channel during embargo: GitHub Security Advisory + `matt@jellybaby.com`. +- Public disclosure: coordinated with the reporter, published as a GitHub + Security Advisory and in the release notes of the patched version. + +## Records + +Every incident produces: a problem-report entry, a security advisory, an +updated `vuln.json`, and (where the model changed) a regenerated `tara.json`. +These are bundled into the release audit pack (`gofusa audit-pack`). diff --git a/README.md b/README.md index a55c4d5..80b8337 100644 --- a/README.md +++ b/README.md @@ -116,6 +116,32 @@ go run ./cmd/lintool send 0x10 01020304 # publish response + trigger exchange go run ./cmd/lintool dump # subscribe to all frames ``` +## Safety & compliance + +go-LIN is developed as a Safety Element Out Of Context (SEOOC) targeting +**ASIL-B / SIL 2**, with a complete, continuously-verified evidence pack. Every +CI run gates on the full go-FuSa lifecycle (`check`, 100% requirement +traceability **and** test coverage, `cyber`, `vuln`, `qualify`), RELAY +conformance (`relay conform --strict`, `relay interop`), a per-package coverage +floor, and **zero-GAP compliance gap reports** across seven standards. + +| Standard | Report | Evidence | +|---|---|---| +| ISO 26262 (Part 6) | `gofusa iso26262` | [HARA.md](HARA.md), `.fusa-hara.json`, [SEOOC.md](SEOOC.md), `fmea.*` | +| IEC 61508 (Parts 1-3) | `gofusa iec61508` | [SAFETY_PLAN.md](SAFETY_PLAN.md), [SVP.md](SVP.md), `coverage-report.json` | +| ISO/SAE 21434 | `gofusa iso21434` | [tara.md](tara.md), [SECURITY.md](SECURITY.md), `vuln.json` | +| IEC 62443-4-2 | `gofusa iec62443` | `.fusa-iec62443.json`, [INCIDENT-RESPONSE.md](INCIDENT-RESPONSE.md) | +| DO-178C (Annex A) | `gofusa do178` | [SVP.md](SVP.md), [SCMP.md](SCMP.md), [SQAP.md](SQAP.md), `sas.md` | +| UN R.155 (CSMS) | `gofusa unece` | [SECURITY.md](SECURITY.md), `tara.json` | +| SLSA v1.0 | `gofusa slsa` | `provenance.json`, `sbom.json` | + +Start with the **[Safety Manual](SAFETY_MANUAL.md)** — it is the integration-facing +document covering safety goals, the safety mechanisms go-LIN provides, the +conditions of use the integrator must satisfy, and the error-handling contract. +The architecture trust boundary is in `boundary.mermaid`; the assembled argument +is in [safety-case.md](safety-case.md). 112 atomic requirements +(`.fusa-reqs.json`) are each traced to code and to a test. + ## Philosophy - **Interface-first** — one stable `lin.Bus` interface; transports are swappable. diff --git a/SAFETY_MANUAL.md b/SAFETY_MANUAL.md new file mode 100644 index 0000000..13e0324 --- /dev/null +++ b/SAFETY_MANUAL.md @@ -0,0 +1,115 @@ +# go-LIN Safety Manual + +**Document ID:** SM-001 +**Component:** `github.com/SoundMatt/go-LIN` +**Classification:** Safety Element Out Of Context (SEOOC), ASIL-B / SIL 2 +**Standards:** ISO 26262:2018 · IEC 61508:2010 · ISO/SAE 21434:2021 · IEC 62443-4-2 + +This Safety Manual is the integration-facing safety document for go-LIN. It +tells a system integrator what go-LIN guarantees, what it assumes of the +surrounding system, and how to use it so that the assumptions of the safety +case hold. It consolidates and points into the detailed work products: + +| Work product | File | +|---|---| +| Safety plan | [SAFETY_PLAN.md](SAFETY_PLAN.md) | +| Hazard & risk analysis (HARA) | [HARA.md](HARA.md), `.fusa-hara.json` | +| Threat analysis (TARA) | [tara.md](tara.md), `tara.json` | +| SEOOC assumptions of use | [SEOOC.md](SEOOC.md) | +| Requirements registry | `.fusa-reqs.json` | +| dFMEA | `fmea.csv`, `fmea.json` | +| Safety case | [safety-case.md](safety-case.md) | +| Security policy / incident response | [SECURITY.md](SECURITY.md), [INCIDENT-RESPONSE.md](INCIDENT-RESPONSE.md) | +| Architecture / trust boundaries | `boundary.mermaid` | + +## 1. Scope and intended use + +go-LIN is a software library implementing the LIN 2.x protocol (frame +construction/validation, PID parity, checksums), an in-process virtual bus, +an LDF parser, master/slave node logic, and an end-to-end (E2E) safety +protection layer. It is intended for use as a building block within an +automotive or industrial ECU. It is **not** a complete safety function on its +own; the integrator is responsible for the surrounding safety architecture. + +The assumed usage context (protocol version, topology, baud rate, payload +size, application domain) is defined in [SEOOC.md §2](SEOOC.md). + +## 2. Safety goals + +go-LIN contributes to five safety goals derived from the HARA (SG-01 … SG-05). +Full S/E/C classification and hazard linkage are in [HARA.md](HARA.md) and +`.fusa-hara.json`. + +| Goal | Statement | ASIL | +|---|---|---| +| SG-01 | Correctly identify frame IDs via PID parity computation and verification. | ASIL-B | +| SG-02 | Detect frame payload corruption using the LIN checksum algorithm. | ASIL-B | +| SG-03 | Correctly parse LDF definitions and decode payloads without offset errors. | ASIL-A | +| SG-04 | Detect E2E sequence gaps and CRC mismatches. | ASIL-A | +| SG-05 | Validate all frame IDs and data lengths at API boundaries. | ASIL-B | + +## 3. Safety mechanisms provided + +| Mechanism | API | Detects | Requirements | +|---|---|---|---| +| Frame validation | `lin.ValidateFrame` | out-of-range ID (>0x3F), empty / oversized payload (>8 B), non-classic checksum on diagnostic frames | REQ-LIN-001..003, 015..017 | +| PID parity | `lin.ProtectID`, `lin.VerifyPID` | corrupted / wrong frame ID | REQ-LIN-004..007, 018 | +| LIN checksum | `lin.CalcChecksum` (classic + enhanced) | payload corruption | REQ-LIN-008..010 | +| E2E protection | `safety.Protect` / `safety.Unwrap` | CRC mismatch, sequence gap/replay, length error | REQ-SAFETY-001..015 | +| LDF parse validation | `ldf.Parse` | malformed signal/frame/schedule definitions | REQ-LDF-001..015 | + +## 4. Conditions of use (assumptions the integrator MUST satisfy) + +These are normative. Violating them invalidates the safety case. The full list +with rationale is in [SEOOC.md §3](SEOOC.md); the safety-critical subset: + +1. **Provide a correct LIN physical layer** (transceiver, break detection, bit + timing). go-LIN operates above the physical layer (IA-01). +2. **Call `lin.ValidateFrame` on every externally received frame** before + acting on it (IA-03, SG-05). +3. **Enforce frame-ID → actuator semantics in application logic.** go-LIN + delivers frames as received; it does not know which actuator an ID drives + (IA-02). +4. **Route safety-relevant multi-slot payloads through the `safety` package** + and check the returned `E2EError` (IA-04, SG-04). +5. **Treat `lin.ErrNoResponse` as a system condition** (no slave answered), not + a library fault (IA-05, REQ-SEOOC-007). +6. **Source LDF files from a trusted, version-controlled origin** (IA-06). +7. **Provide a monotonic clock with ≥1 ms resolution** for schedule timing + (REQ-SEOOC-009). + +## 5. Error handling contract + +go-LIN reports faults synchronously as typed sentinel errors (RELAY §5); it +never panics on untrusted input and never silently drops a detected fault. +The integrator MUST check returned errors and place the system in its defined +safe state on: + +| Error | Meaning | Required integrator action | +|---|---|---| +| `lin.ErrInvalidFrame` | structural frame violation | reject frame; do not transmit/process | +| `safety.E2EError{ErrCRCMismatch}` | payload corruption detected | discard payload; enter safe state | +| `safety.E2EError{ErrSequenceGap}` | lost/replayed frame | discard payload; enter safe state | +| `lin.ErrNoResponse` | no slave response | system-level handling (timeout/retry) | +| `lin.ErrTimeout`, `lin.ErrClosed`, `lin.ErrNotConnected` | transport state | per integrator transport policy | + +## 6. Verification evidence + +Every change is gated in CI (`.github/workflows/ci.yml`): + +- Cross-platform unit + race tests, fuzz tests on all untrusted-input parsers. +- Full go-FuSa lifecycle: `check` (0 ERROR), `trace -req-coverage 100` + (100% requirement traceability **and** function-annotation density), + `cyber`, `vuln`, `qualify`. +- RELAY conformance: `relay conform --strict`, `relay interop --protocol LIN`. + +Compliance gap reports (`gofusa iso26262 | do178 | iec61508 | iso21434 | +iec62443 | unece | slsa`) run in CI with **zero GAP and zero FAIL**; remaining +items are inherent human-review (MANUAL) attestations or N/A for a software +component. + +## 7. Limitations / out of scope + +See [SEOOC.md §5](SEOOC.md). Notably: hardware FMEDA, transceiver bit-error +analysis, multi-master topologies, and AUTOSAR LIN-stack integration are out +of scope and are the integrator's responsibility. diff --git a/SCMP.md b/SCMP.md new file mode 100644 index 0000000..5d93333 --- /dev/null +++ b/SCMP.md @@ -0,0 +1,32 @@ +# Software Configuration Management Plan (SCMP) + + +## 1. Purpose + +Define the configuration management process for all software lifecycle data. + +## 2. Configuration Items + +All items listed in the Software Configuration Index (`sci.json`). + +## 3. Baseline Management + +Baselines created at: planning complete, first test, certification submittal. Each baseline produces a signed SCI and audit pack. + +## 4. Change Control + +All changes via pull request. Require review by CODEOWNERS. CI must pass before merge. Each change generates a new `.fusa-problems.json` entry. + +## 5. Archive and Retrieval + +Lifecycle data archived via `gofusa audit-pack`. Checksums recorded in `sci.json`. + +## 6. Problem Reporting + +Problem reports tracked in `.fusa-problems.json` per DO-178C §11.17. + +## 7. Approvals + +| Role | Name | Date | +|---|---|---| +| CM Lead | | | diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..8ced180 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,68 @@ +# Security Policy + +This document is the vulnerability-management and cyber-incident-response +policy for **go-LIN**, satisfying IEC 62443-4-2 CR 6.2 / CR 6.2.1 and +ISO/SAE 21434 clause 6 (cybersecurity management) and clause 8 (vulnerability +management). go-LIN is a Safety Element Out Of Context (SEOOC); see +[SEOOC.md](SEOOC.md) for the assumptions placed on the integrating system. + +## Supported versions + +Security fixes are provided for the latest released minor version. Older +versions should be upgraded to receive fixes. + +| Version | Supported | +|---------|-----------| +| 1.x | ✅ | +| < 1.0 | ❌ | + +## Reporting a vulnerability + +Report suspected vulnerabilities **privately** — do not open a public issue. + +- Preferred: GitHub **Security Advisories** → *Report a vulnerability* on + . +- Alternative: email the maintainer at `matt@jellybaby.com` with subject + `go-LIN security`. + +Please include: affected version/commit, a description, reproduction steps or a +proof-of-concept, and the assessed impact. Do not include exploit code in any +public channel. + +## Coordinated disclosure timeline + +| Stage | Target | +|-------|--------| +| Acknowledge receipt | within 3 business days | +| Triage + severity (CVSS v3.1) | within 10 business days | +| Fix or mitigation for supported versions | within 90 days of triage | +| Public advisory + CVE (if warranted) | coordinated with the reporter on release | + +## Cyber-incident response plan (IEC 62443-4-2 CR 6.2.1) + +On confirmation of a security incident affecting go-LIN: + +1. **Detect / intake** — log the report in the project's problem-report + register (`.fusa-problems.json`) with a security classification. +2. **Contain** — assess exposure across supported versions; if actively + exploited, publish an interim mitigation advisory. +3. **Eradicate** — develop and test a fix on a private branch; run the full + go-FuSa lifecycle (`check`, `trace`, `cyber`, `vuln`, `qualify`) and RELAY + conformance (`relay conform --strict`, `relay interop`) before release. +4. **Recover** — release a patched version, publish the GitHub Security + Advisory, request a CVE where warranted, and update `vuln.json` / TARA. +5. **Post-incident** — record root cause and corrective actions in the safety + case and, where the threat model changed, regenerate the TARA + (`gofusa tara`). + +## Vulnerability monitoring (ISO 21434 clause 8.3) + +Dependencies are scanned on every CI run with `gofusa vuln` (OSV database); +the result is recorded in `vuln.json`. The Software Bill of Materials +(`sbom.json`) enables downstream impact analysis when a new advisory is +published against a transitive dependency. + +## Threat model + +See [tara.md](tara.md) for the ISO/SAE 21434 Threat Analysis and Risk +Assessment, and [HARA.md](HARA.md) for the functional-safety hazard analysis. diff --git a/SQAP.md b/SQAP.md new file mode 100644 index 0000000..431633c --- /dev/null +++ b/SQAP.md @@ -0,0 +1,32 @@ +# Software Quality Assurance Plan (SQAP) + + +## 1. Purpose + +Define QA activities that provide assurance the software lifecycle is executed in accordance with approved plans and standards. + +## 2. QA Activities + +| Activity | Frequency | go-FuSa Command | +|---|---|---| +| Coding standards review | Every PR | `gofusa lint` | +| Static analysis | Every PR | `gofusa analyze` | +| Requirement traceability audit | Every release | `gofusa trace` | +| Coverage gate | Every commit | `gofusa coverage` | +| DO-178C gap review | Every release | `gofusa do178` | +| SAS review | Pre-submittal | `gofusa sas` | + +## 3. Non-conformance Tracking + +Non-conformances logged in `.fusa-problems.json`. Critical items block release. + +## 4. QA Independence + +QA reviews performed by personnel independent of the developer. + +## 5. Approvals + +| Role | Name | Date | +|---|---|---| +| QA Lead | | | +| Safety Engineer | | | diff --git a/SVP.md b/SVP.md new file mode 100644 index 0000000..62dc4a9 --- /dev/null +++ b/SVP.md @@ -0,0 +1,38 @@ +# Software Verification Plan (SVP) + + +## 1. Purpose + +Describe verification activities, methods, independence requirements, and pass/fail criteria. + +## 2. Verification Methods + +| Method | Tool | DAL Applicability | +|---|---|---| +| Static analysis | `gofusa check` | DAL-A/B/C/D | +| Structural coverage | `gofusa coverage` | DAL-A/B/C | +| Requirement-based testing | `gofusa verify` | DAL-A/B/C/D | + +## 3. Independence + +At DAL-A/B: verification must be performed by a person or tool independent of the developer. Document assignment of independent verifier. + +## 4. Coverage Objectives + +| DAL | Statement | Decision | MC/DC | +|---|---|---|---| +| DAL-A | 100% | 100% | 100% | +| DAL-B | 100% | 100% | — | +| DAL-C | 100% | — | — | + +Coverage measured by `gofusa coverage`. + +## 5. Regression Strategy + +All tests re-run on every commit via CI (`gofusa verify`). + +## 6. Approvals + +| Role | Name | Date | +|---|---|---| +| Verification Lead | | | diff --git a/artifact-manifest.json b/artifact-manifest.json index 6b80ee9..4001304 100644 --- a/artifact-manifest.json +++ b/artifact-manifest.json @@ -4,16 +4,16 @@ "tool": "go-FuSa", "toolVersion": "0.30.0", "language": "go", - "generatedAt": "2026-06-19T19:31:15.580066187Z", + "generatedAt": "2026-06-19T20:33:25.294104Z", "format": "x-FuSa manifest v1", "artifacts": [ { - "path": "/home/runner/work/go-LIN/go-LIN/sbom.json", - "sha256": "5cd603458eb029005cf6117e5f9b2fc15494cc70a64c5c612a7e7e0f0ccacd47" + "path": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/sbom.json", + "sha256": "a56cbbdadc064ce7ce410649b49c498cf94d0d749d6ab415fb79921a6600769f" }, { - "path": "/home/runner/work/go-LIN/go-LIN/provenance.json", - "sha256": "50cc4fdf96904abeea8763f937c59dbef280529f893c354eba834dd232de1d9f" + "path": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/provenance.json", + "sha256": "8b2a037e70efef235b1e345cc246b7fb62e627dce6b9979f03cdf310a21299a8" } ] } diff --git a/boundary.dot b/boundary.dot new file mode 100644 index 0000000..167962d --- /dev/null +++ b/boundary.dot @@ -0,0 +1,31 @@ +// Module: github.com/SoundMatt/go-LIN +// Generated: 2026-06-19T20:33:24Z +digraph { + rankdir=LR + node [shape=box style=rounded fontname=Helvetica] + + "github_com_SoundMatt_go_LIN" [label="lin"] + "github_com_SoundMatt_go_LIN_cmd_go_lin" [label="github.com/SoundMatt/go-LIN/cmd/go-lin"] + "github_com_SoundMatt_go_LIN_cmd_lintool" [label="github.com/SoundMatt/go-LIN/cmd/lintool"] + "github_com_SoundMatt_go_LIN_examples_quickstart" [label="github.com/SoundMatt/go-LIN/examples/quickstart"] + "github_com_SoundMatt_go_LIN_ldf" [label="ldf"] + "github_com_SoundMatt_go_LIN_master" [label="master"] + "github_com_SoundMatt_go_LIN_mock" [label="mock"] + "github_com_SoundMatt_go_LIN_safety" [label="safety"] + "github_com_SoundMatt_go_LIN_slave" [label="slave"] + "github_com_SoundMatt_go_LIN_virtual" [label="virtual"] + + "github_com_SoundMatt_go_LIN_cmd_go_lin" -> "github_com_SoundMatt_go_LIN" + "github_com_SoundMatt_go_LIN_cmd_go_lin" -> "github_com_SoundMatt_go_LIN_virtual" + "github_com_SoundMatt_go_LIN_cmd_lintool" -> "github_com_SoundMatt_go_LIN" + "github_com_SoundMatt_go_LIN_cmd_lintool" -> "github_com_SoundMatt_go_LIN_virtual" + "github_com_SoundMatt_go_LIN_examples_quickstart" -> "github_com_SoundMatt_go_LIN" + "github_com_SoundMatt_go_LIN_examples_quickstart" -> "github_com_SoundMatt_go_LIN_master" + "github_com_SoundMatt_go_LIN_examples_quickstart" -> "github_com_SoundMatt_go_LIN_slave" + "github_com_SoundMatt_go_LIN_examples_quickstart" -> "github_com_SoundMatt_go_LIN_virtual" + "github_com_SoundMatt_go_LIN_ldf" -> "github_com_SoundMatt_go_LIN" + "github_com_SoundMatt_go_LIN_master" -> "github_com_SoundMatt_go_LIN" + "github_com_SoundMatt_go_LIN_mock" -> "github_com_SoundMatt_go_LIN_virtual" + "github_com_SoundMatt_go_LIN_slave" -> "github_com_SoundMatt_go_LIN" + "github_com_SoundMatt_go_LIN_virtual" -> "github_com_SoundMatt_go_LIN" +} diff --git a/boundary.mermaid b/boundary.mermaid new file mode 100644 index 0000000..e97cacd --- /dev/null +++ b/boundary.mermaid @@ -0,0 +1,29 @@ +%{init: {"theme": "default"}}% +flowchart LR + % Module: github.com/SoundMatt/go-LIN + % Generated: 2026-06-19T20:33:24Z + + github_com_SoundMatt_go_LIN["lin"] + github_com_SoundMatt_go_LIN_cmd_go_lin["github.com/SoundMatt/go-LIN/cmd/go-lin"] + github_com_SoundMatt_go_LIN_cmd_lintool["github.com/SoundMatt/go-LIN/cmd/lintool"] + github_com_SoundMatt_go_LIN_examples_quickstart["github.com/SoundMatt/go-LIN/examples/quickstart"] + github_com_SoundMatt_go_LIN_ldf["ldf"] + github_com_SoundMatt_go_LIN_master["master"] + github_com_SoundMatt_go_LIN_mock["mock"] + github_com_SoundMatt_go_LIN_safety["safety"] + github_com_SoundMatt_go_LIN_slave["slave"] + github_com_SoundMatt_go_LIN_virtual["virtual"] + + github_com_SoundMatt_go_LIN_cmd_go_lin --> github_com_SoundMatt_go_LIN + github_com_SoundMatt_go_LIN_cmd_go_lin --> github_com_SoundMatt_go_LIN_virtual + github_com_SoundMatt_go_LIN_cmd_lintool --> github_com_SoundMatt_go_LIN + github_com_SoundMatt_go_LIN_cmd_lintool --> github_com_SoundMatt_go_LIN_virtual + github_com_SoundMatt_go_LIN_examples_quickstart --> github_com_SoundMatt_go_LIN + github_com_SoundMatt_go_LIN_examples_quickstart --> github_com_SoundMatt_go_LIN_master + github_com_SoundMatt_go_LIN_examples_quickstart --> github_com_SoundMatt_go_LIN_slave + github_com_SoundMatt_go_LIN_examples_quickstart --> github_com_SoundMatt_go_LIN_virtual + github_com_SoundMatt_go_LIN_ldf --> github_com_SoundMatt_go_LIN + github_com_SoundMatt_go_LIN_master --> github_com_SoundMatt_go_LIN + github_com_SoundMatt_go_LIN_mock --> github_com_SoundMatt_go_LIN_virtual + github_com_SoundMatt_go_LIN_slave --> github_com_SoundMatt_go_LIN + github_com_SoundMatt_go_LIN_virtual --> github_com_SoundMatt_go_LIN diff --git a/cmd/go-lin/main.go b/cmd/go-lin/main.go index 2a40c86..4cb8513 100644 --- a/cmd/go-lin/main.go +++ b/cmd/go-lin/main.go @@ -63,11 +63,11 @@ func main() { } switch os.Args[1] { case "version": - cmdVersion(os.Args[2:]) + cmdVersion(os.Args[2:], os.Stdout) case "capabilities": - cmdCapabilities() + cmdCapabilities(os.Stdout) case "status": - cmdStatus(os.Args[2:]) + cmdStatus(os.Args[2:], os.Stdout) case "send": cmdSend(os.Args[2:]) case "subscribe": @@ -77,9 +77,9 @@ func main() { case "dump": cmdDump() case "pid": - cmdPID(os.Args[2:]) + cmdPID(os.Args[2:], os.Stdout) case "cs": - cmdCS(os.Args[2:]) + cmdCS(os.Args[2:], os.Stdout) default: fmt.Fprintf(os.Stderr, "%s: unknown subcommand %q\n", toolName, os.Args[1]) usage() @@ -110,7 +110,7 @@ RELAY interop driver: // ── RELAY mandatory commands ────────────────────────────────────────────────── -func cmdVersion(args []string) { +func cmdVersion(args []string, w io.Writer) { format := "text" for _, a := range args { if a == "--format=json" || a == "json" { @@ -131,15 +131,15 @@ func cmdVersion(args []string) { "language": "go", "runtime": runtime.Version(), } - enc := json.NewEncoder(os.Stdout) + enc := json.NewEncoder(w) enc.SetIndent("", " ") _ = enc.Encode(out) } else { - fmt.Printf("%s %s (RELAY spec %s, %s)\n", toolName, ver, lin.SpecVersion, runtime.Version()) + fmt.Fprintf(w, "%s %s (RELAY spec %s, %s)\n", toolName, ver, lin.SpecVersion, runtime.Version()) } } -func cmdCapabilities() { +func cmdCapabilities(w io.Writer) { ver := toolVersion() out := map[string]any{ "kind": "capabilities", @@ -155,12 +155,12 @@ func cmdCapabilities() { "optional_interfaces": []string{"HealthProvider", "MetricsProvider", "Drainer"}, "adapt": true, } - enc := json.NewEncoder(os.Stdout) + enc := json.NewEncoder(w) enc.SetIndent("", " ") _ = enc.Encode(out) } -func cmdStatus(args []string) { +func cmdStatus(args []string, w io.Writer) { format := "text" for _, a := range args { if a == "--format=json" || a == "json" { @@ -178,11 +178,11 @@ func cmdStatus(args []string) { "endpoint": "", "details": map[string]any{}, } - enc := json.NewEncoder(os.Stdout) + enc := json.NewEncoder(w) enc.SetIndent("", " ") _ = enc.Encode(out) } else { - fmt.Printf("%s %s: healthy\n", toolName, ver) + fmt.Fprintf(w, "%s %s: healthy\n", toolName, ver) } } @@ -314,6 +314,8 @@ func cmdSubscribe(args []string) { // — writing the relay.Message as JSON on stdout. The timestamp is left zero so // interop comparisons are deterministic. On invalid input it writes the RELAY // §5 sentinel name to stderr. Exit: 0 converted, 1 invalid input, 2 invalid args. +// +//fusa:req REQ-SEC-006 func cmdConvert(args []string, stdin io.Reader, stdout, stderr io.Writer) int { fs := flag.NewFlagSet("convert", flag.ContinueOnError) fs.SetOutput(stderr) @@ -403,17 +405,17 @@ func cmdDump() { } } -func cmdPID(args []string) { +func cmdPID(args []string, w io.Writer) { if len(args) != 1 { fmt.Fprintf(os.Stderr, "usage: %s pid \n", toolName) os.Exit(1) } id := parseID(args[0]) pid := lin.ProtectID(id) - fmt.Printf("ID=0x%02X PID=0x%02X binary=%08b\n", id, pid, pid) + fmt.Fprintf(w, "ID=0x%02X PID=0x%02X binary=%08b\n", id, pid, pid) } -func cmdCS(args []string) { +func cmdCS(args []string, w io.Writer) { if len(args) != 2 { fmt.Fprintf(os.Stderr, "usage: %s cs \n", toolName) os.Exit(1) @@ -422,7 +424,7 @@ func cmdCS(args []string) { data := parseHex(args[1]) pid := lin.ProtectID(id) cs := lin.CalcChecksum(pid, data, lin.EnhancedChecksum) - fmt.Printf("ID=0x%02X PID=0x%02X data=%s checksum(enhanced)=0x%02X\n", + fmt.Fprintf(w, "ID=0x%02X PID=0x%02X data=%s checksum(enhanced)=0x%02X\n", id, pid, strings.ToUpper(hex.EncodeToString(data)), cs) } diff --git a/cmd/go-lin/main_test.go b/cmd/go-lin/main_test.go index c24eb6e..ed8beb8 100644 --- a/cmd/go-lin/main_test.go +++ b/cmd/go-lin/main_test.go @@ -45,6 +45,8 @@ func TestConvert_goldenVector(t *testing.T) { // TestConvert_invalidInput rejects a structurally invalid frame with exit 1 and // the RELAY §5 sentinel name on stderr. +// +//fusa:test REQ-SEC-006 func TestConvert_invalidInput(t *testing.T) { in := `{"id":255,"data":"qrs=","checksum":0,"checksum_type":0}` // ID > 0x3F var out, errb bytes.Buffer @@ -80,3 +82,123 @@ func TestSendStream_publishesNDJSON(t *testing.T) { t.Errorf("stdout = %q, want \"published 1 message\"", out.String()) } } + +// TestVersion_json verifies the §12.1 version document carries the tracked spec +// version and required fields. +func TestVersion_json(t *testing.T) { + var out bytes.Buffer + cmdVersion([]string{"--format", "json"}, &out) + var doc map[string]any + if err := json.Unmarshal(out.Bytes(), &doc); err != nil { + t.Fatalf("version --format json is not valid JSON: %v", err) + } + if doc["protocol"] != "LIN" { + t.Errorf("protocol = %v, want LIN", doc["protocol"]) + } + if doc["spec_version"] != relay.SpecVersion { + t.Errorf("spec_version = %v, want %q", doc["spec_version"], relay.SpecVersion) + } +} + +func TestVersion_text(t *testing.T) { + var out bytes.Buffer + cmdVersion(nil, &out) + if !strings.Contains(out.String(), "go-lin") || !strings.Contains(out.String(), "RELAY spec") { + t.Errorf("text version output unexpected: %q", out.String()) + } +} + +// TestCapabilities_advertisesDriverCommands ensures convert/send/subscribe are +// advertised so relay interop and crossbar discover the driver. +func TestCapabilities_advertisesDriverCommands(t *testing.T) { + var out bytes.Buffer + cmdCapabilities(&out) + var doc struct { + Kind string `json:"kind"` + Commands []string `json:"commands"` + Adapt bool `json:"adapt"` + } + if err := json.Unmarshal(out.Bytes(), &doc); err != nil { + t.Fatalf("capabilities is not valid JSON: %v", err) + } + if doc.Kind != "capabilities" { + t.Errorf("kind = %q, want capabilities", doc.Kind) + } + want := map[string]bool{"convert": false, "send": false, "subscribe": false} + for _, c := range doc.Commands { + if _, ok := want[c]; ok { + want[c] = true + } + } + for c, seen := range want { + if !seen { + t.Errorf("capabilities.commands missing %q", c) + } + } + if !doc.Adapt { + t.Error("capabilities.adapt = false, want true") + } +} + +func TestStatus_jsonAndText(t *testing.T) { + var j bytes.Buffer + cmdStatus([]string{"--format", "json"}, &j) + var doc map[string]any + if err := json.Unmarshal(j.Bytes(), &doc); err != nil { + t.Fatalf("status --format json invalid: %v", err) + } + if doc["healthy"] != true { + t.Errorf("healthy = %v, want true", doc["healthy"]) + } + var txt bytes.Buffer + cmdStatus(nil, &txt) + if !strings.Contains(txt.String(), "healthy") { + t.Errorf("status text = %q, want it to mention health", txt.String()) + } +} + +// TestPID matches the protected identifier for a known frame ID. +func TestPID(t *testing.T) { + var out bytes.Buffer + cmdPID([]string{"0x10"}, &out) + // PID for ID 0x10 is 0x50 (parity P0=1, P1=0). + if !strings.Contains(out.String(), "ID=0x10") || !strings.Contains(out.String(), "PID=0x50") { + t.Errorf("pid output = %q, want ID=0x10 PID=0x50", out.String()) + } +} + +func TestCS(t *testing.T) { + var out bytes.Buffer + cmdCS([]string{"0x10", "AABB"}, &out) + if !strings.Contains(out.String(), "checksum(enhanced)=0x") { + t.Errorf("cs output = %q, want an enhanced checksum", out.String()) + } +} + +func TestFlagFormat(t *testing.T) { + cases := []struct { + args []string + want string + }{ + {[]string{"--format", "json"}, "json"}, + {[]string{"--format=json"}, "json"}, + {[]string{"--format", "text"}, "text"}, + {[]string{}, ""}, + {[]string{"dump"}, ""}, + } + for _, tc := range cases { + if got := flagFormat(tc.args); got != tc.want { + t.Errorf("flagFormat(%v) = %q, want %q", tc.args, got, tc.want) + } + } +} + +// TestConvert_wrongFormat rejects a non-json output format with exit 2. +func TestConvert_wrongFormat(t *testing.T) { + var out, errb bytes.Buffer + code := cmdConvert([]string{"--protocol", "LIN", "--format", "yaml"}, + strings.NewReader(`{}`), &out, &errb) + if code != 2 { + t.Errorf("convert --format yaml exit = %d, want 2", code) + } +} diff --git a/fmea.csv b/fmea.csv index e1b6974..0f0150a 100644 --- a/fmea.csv +++ b/fmea.csv @@ -2,19 +2,19 @@ Component,Function,FailureModes,Effects,Severity,DetectionControl,RequirementIDs go-LIN (lin),Adapt,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-ADAPT-001,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),CalcChecksum,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LIN-008; REQ-LIN-009; REQ-LIN-010,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),Close,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-ADAPT-005,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range -go-LIN (lin),FromMessage,Returns unexpected error,Silent failure propagated to caller,medium,unit tests,,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range +go-LIN (lin),FromMessage,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-SEC-005,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),Matches,Incorrect output,Incorrect system behavior,low,unit tests,,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),ProtectID,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LIN-004; REQ-LIN-005; REQ-LIN-018,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),Protocol,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-ADAPT-001,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),Send,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-ADAPT-002; REQ-ADAPT-003,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),Subscribe,Returns unexpected error; Goroutine not terminated,Silent failure propagated to caller; Memory leak or deadlock,high,requirement testing + unit tests,REQ-ADAPT-004,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),ToMessage,Incorrect output,Incorrect system behavior,low,unit tests,,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range -go-LIN (lin),ValidateFrame,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-LIN-001; REQ-LIN-002; REQ-LIN-003; REQ-LIN-015; REQ-LIN-016; REQ-LIN-017,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range +go-LIN (lin),ValidateFrame,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-LIN-001; REQ-LIN-002; REQ-LIN-003; REQ-LIN-015; REQ-LIN-016; REQ-LIN-017; REQ-SEC-004,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range go-LIN (lin),VerifyPID,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-LIN-006; REQ-LIN-007,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range ldf,Decode,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LDF-009; REQ-LDF-010,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range ldf,Frame,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LDF-005; REQ-LDF-012,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range ldf,Frames,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LDF-005; REQ-LDF-015,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range -ldf,Parse,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-LDF-001; REQ-LDF-002; REQ-LDF-003; REQ-LDF-004; REQ-LDF-014,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range +ldf,Parse,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-LDF-001; REQ-LDF-002; REQ-LDF-003; REQ-LDF-004; REQ-LDF-014; REQ-SEC-001,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range ldf,Schedule,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LDF-011,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range ldf,Signal,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LDF-007; REQ-LDF-013,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range ldf,Signals,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-LDF-007; REQ-LDF-008,CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range; CYBER009: uint32(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range @@ -29,7 +29,7 @@ safety,Error,Incorrect output,Incorrect system behavior,low,unit tests,,CYBER009 safety,NewProtector,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-SAFETY-003,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range safety,NewReceiver,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-SAFETY-013,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range safety,Protect,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-SAFETY-001; REQ-SAFETY-002; REQ-SAFETY-003; REQ-SAFETY-004; REQ-SAFETY-005; REQ-SAFETY-006; REQ-SAFETY-012; REQ-SAFETY-014,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range -safety,Unwrap,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-SAFETY-007; REQ-SAFETY-008; REQ-SAFETY-009; REQ-SAFETY-010; REQ-SAFETY-011; REQ-SAFETY-013; REQ-SAFETY-015,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range +safety,Unwrap,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-SAFETY-007; REQ-SAFETY-008; REQ-SAFETY-009; REQ-SAFETY-010; REQ-SAFETY-011; REQ-SAFETY-013; REQ-SAFETY-015; REQ-SEC-002; REQ-SEC-003,CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range slave,New,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-SLAVE-001, slave,RegisteredIDs,Incorrect output,Incorrect system behavior,high,requirement testing + unit tests,REQ-SLAVE-005; REQ-SLAVE-007, slave,SetResponse,Returns unexpected error,Silent failure propagated to caller,high,requirement testing + unit tests,REQ-SLAVE-002; REQ-SLAVE-003; REQ-SLAVE-004; REQ-SLAVE-008, diff --git a/fmea.json b/fmea.json index 1e40be9..3e9aad7 100644 --- a/fmea.json +++ b/fmea.json @@ -1,12 +1,12 @@ { "format": "go-FuSa dFMEA v1", - "generated_at": "2026-06-19T19:31:14.516157399Z", + "generated_at": "2026-06-19T20:33:23.960872Z", "module": "github.com/SoundMatt/go-LIN", "entries": [ { "component": "go-LIN (lin)", "function": "Adapt", - "file": "/home/runner/work/go-LIN/go-LIN/adapt.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/adapt.go", "failure_modes": [ "Incorrect output" ], @@ -25,7 +25,7 @@ { "component": "go-LIN (lin)", "function": "CalcChecksum", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Incorrect output" ], @@ -51,7 +51,7 @@ { "component": "go-LIN (lin)", "function": "Close", - "file": "/home/runner/work/go-LIN/go-LIN/adapt.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/adapt.go", "failure_modes": [ "Returns unexpected error" ], @@ -70,15 +70,18 @@ { "component": "go-LIN (lin)", "function": "FromMessage", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Returns unexpected error" ], "effects": [ "Silent failure propagated to caller" ], - "severity": "medium", - "detection_control": "unit tests", + "severity": "high", + "detection_control": "requirement testing + unit tests", + "requirement_ids": [ + "REQ-SEC-005" + ], "cyber_risks": [ "CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range", "CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range", @@ -91,7 +94,7 @@ { "component": "go-LIN (lin)", "function": "Matches", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Incorrect output" ], @@ -112,7 +115,7 @@ { "component": "go-LIN (lin)", "function": "ProtectID", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Incorrect output" ], @@ -138,7 +141,7 @@ { "component": "go-LIN (lin)", "function": "Protocol", - "file": "/home/runner/work/go-LIN/go-LIN/adapt.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/adapt.go", "failure_modes": [ "Incorrect output" ], @@ -157,7 +160,7 @@ { "component": "go-LIN (lin)", "function": "Send", - "file": "/home/runner/work/go-LIN/go-LIN/adapt.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/adapt.go", "failure_modes": [ "Returns unexpected error" ], @@ -177,7 +180,7 @@ { "component": "go-LIN (lin)", "function": "Subscribe", - "file": "/home/runner/work/go-LIN/go-LIN/adapt.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/adapt.go", "failure_modes": [ "Returns unexpected error", "Goroutine not terminated" @@ -198,7 +201,7 @@ { "component": "go-LIN (lin)", "function": "ToMessage", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Incorrect output" ], @@ -219,7 +222,7 @@ { "component": "go-LIN (lin)", "function": "ValidateFrame", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Returns unexpected error" ], @@ -234,7 +237,8 @@ "REQ-LIN-003", "REQ-LIN-015", "REQ-LIN-016", - "REQ-LIN-017" + "REQ-LIN-017", + "REQ-SEC-004" ], "cyber_risks": [ "CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range", @@ -248,7 +252,7 @@ { "component": "go-LIN (lin)", "function": "VerifyPID", - "file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "failure_modes": [ "Returns unexpected error" ], @@ -273,7 +277,7 @@ { "component": "ldf", "function": "Decode", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Incorrect output" ], @@ -294,7 +298,7 @@ { "component": "ldf", "function": "Frame", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Incorrect output" ], @@ -315,7 +319,7 @@ { "component": "ldf", "function": "Frames", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Incorrect output" ], @@ -336,7 +340,7 @@ { "component": "ldf", "function": "Parse", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Returns unexpected error" ], @@ -350,7 +354,8 @@ "REQ-LDF-002", "REQ-LDF-003", "REQ-LDF-004", - "REQ-LDF-014" + "REQ-LDF-014", + "REQ-SEC-001" ], "cyber_risks": [ "CYBER009: uint8(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range", @@ -360,7 +365,7 @@ { "component": "ldf", "function": "Schedule", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Incorrect output" ], @@ -380,7 +385,7 @@ { "component": "ldf", "function": "Signal", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Incorrect output" ], @@ -401,7 +406,7 @@ { "component": "ldf", "function": "Signals", - "file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", "failure_modes": [ "Incorrect output" ], @@ -422,7 +427,7 @@ { "component": "master", "function": "New", - "file": "/home/runner/work/go-LIN/go-LIN/master/master.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/master/master.go", "failure_modes": [ "Incorrect output" ], @@ -438,7 +443,7 @@ { "component": "master", "function": "OnError", - "file": "/home/runner/work/go-LIN/go-LIN/master/master.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/master/master.go", "failure_modes": [ "Incorrect output" ], @@ -454,7 +459,7 @@ { "component": "master", "function": "OnFrame", - "file": "/home/runner/work/go-LIN/go-LIN/master/master.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/master/master.go", "failure_modes": [ "Incorrect output" ], @@ -470,7 +475,7 @@ { "component": "master", "function": "Run", - "file": "/home/runner/work/go-LIN/go-LIN/master/master.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/master/master.go", "failure_modes": [ "Returns unexpected error", "Uncontrolled execution" @@ -495,7 +500,7 @@ { "component": "master", "function": "SendHeader", - "file": "/home/runner/work/go-LIN/go-LIN/master/master.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/master/master.go", "failure_modes": [ "Returns unexpected error" ], @@ -511,7 +516,7 @@ { "component": "master", "function": "SetSchedule", - "file": "/home/runner/work/go-LIN/go-LIN/master/master.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/master/master.go", "failure_modes": [ "Returns unexpected error" ], @@ -529,7 +534,7 @@ { "component": "mock", "function": "New", - "file": "/home/runner/work/go-LIN/go-LIN/mock/mock.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/mock/mock.go", "failure_modes": [ "Returns unexpected error" ], @@ -545,7 +550,7 @@ { "component": "safety", "function": "Error", - "file": "/home/runner/work/go-LIN/go-LIN/safety/e2e.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e.go", "failure_modes": [ "Incorrect output" ], @@ -561,7 +566,7 @@ { "component": "safety", "function": "NewProtector", - "file": "/home/runner/work/go-LIN/go-LIN/safety/e2e.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e.go", "failure_modes": [ "Incorrect output" ], @@ -580,7 +585,7 @@ { "component": "safety", "function": "NewReceiver", - "file": "/home/runner/work/go-LIN/go-LIN/safety/e2e.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e.go", "failure_modes": [ "Incorrect output" ], @@ -599,7 +604,7 @@ { "component": "safety", "function": "Protect", - "file": "/home/runner/work/go-LIN/go-LIN/safety/e2e.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e.go", "failure_modes": [ "Incorrect output" ], @@ -625,7 +630,7 @@ { "component": "safety", "function": "Unwrap", - "file": "/home/runner/work/go-LIN/go-LIN/safety/e2e.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e.go", "failure_modes": [ "Returns unexpected error" ], @@ -641,7 +646,9 @@ "REQ-SAFETY-010", "REQ-SAFETY-011", "REQ-SAFETY-013", - "REQ-SAFETY-015" + "REQ-SAFETY-015", + "REQ-SEC-002", + "REQ-SEC-003" ], "cyber_risks": [ "CYBER009: uint16(x) — explicit narrowing conversion may silently truncate bits if x exceeds the target type's range" @@ -650,7 +657,7 @@ { "component": "slave", "function": "New", - "file": "/home/runner/work/go-LIN/go-LIN/slave/slave.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/slave/slave.go", "failure_modes": [ "Incorrect output" ], @@ -666,7 +673,7 @@ { "component": "slave", "function": "RegisteredIDs", - "file": "/home/runner/work/go-LIN/go-LIN/slave/slave.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/slave/slave.go", "failure_modes": [ "Incorrect output" ], @@ -683,7 +690,7 @@ { "component": "slave", "function": "SetResponse", - "file": "/home/runner/work/go-LIN/go-LIN/slave/slave.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/slave/slave.go", "failure_modes": [ "Returns unexpected error" ], @@ -702,7 +709,7 @@ { "component": "slave", "function": "Subscribe", - "file": "/home/runner/work/go-LIN/go-LIN/slave/slave.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/slave/slave.go", "failure_modes": [ "Returns unexpected error" ], @@ -718,7 +725,7 @@ { "component": "virtual", "function": "Close", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -735,7 +742,7 @@ { "component": "virtual", "function": "CloseWithDrain", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -748,7 +755,7 @@ { "component": "virtual", "function": "Health", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Incorrect output" ], @@ -761,7 +768,7 @@ { "component": "virtual", "function": "Metrics", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Incorrect output" ], @@ -774,7 +781,7 @@ { "component": "virtual", "function": "New", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -790,7 +797,7 @@ { "component": "virtual", "function": "Publish", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -810,7 +817,7 @@ { "component": "virtual", "function": "PublishClassic", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -823,7 +830,7 @@ { "component": "virtual", "function": "SendHeader", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -845,7 +852,7 @@ { "component": "virtual", "function": "SetSchedule", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], @@ -858,7 +865,7 @@ { "component": "virtual", "function": "Subscribe", - "file": "/home/runner/work/go-LIN/go-LIN/virtual/bus.go", + "file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/virtual/bus.go", "failure_modes": [ "Returns unexpected error" ], diff --git a/ldf/decode_test.go b/ldf/decode_test.go new file mode 100644 index 0000000..fce7261 --- /dev/null +++ b/ldf/decode_test.go @@ -0,0 +1,84 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package ldf_test + +import ( + "strings" + "testing" + + "github.com/SoundMatt/go-LIN/ldf" +) + +const decodeLDF = ` +LIN_description_file; +LIN_protocol_version = "2.1"; +LIN_speed = 19.2 kbps; + +Nodes { + Master: MasterNode, 1 ms, 0.1 ms; + Slaves: SlaveA; +} + +Signals { + Lo8: 8, 0x00, SlaveA, MasterNode; + Hi8: 8, 0x00, SlaveA, MasterNode; +} + +Frames { + DataFrame: 0x10, SlaveA, 2 { + Lo8, 0; + Hi8, 8; + } +} +` + +// TestDecode_extractsSignals decodes a two-byte frame into its two 8-bit +// signals, exercising Decode and extractBits across byte boundaries. +func TestDecode_extractsSignals(t *testing.T) { + db, err := ldf.Parse(strings.NewReader(decodeLDF)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + out := db.Decode(0x10, []byte{0xAB, 0xCD}) + if out == nil { + t.Fatal("Decode returned nil for a known frame") + } + if out["Lo8"] != 0xAB { + t.Errorf("Lo8 = 0x%02X, want 0xAB", out["Lo8"]) + } + if out["Hi8"] != 0xCD { + t.Errorf("Hi8 = 0x%02X, want 0xCD", out["Hi8"]) + } +} + +// TestDecode_shortPayload exercises the extractBits out-of-range break path: +// a payload shorter than the signal layout yields zero for the missing bits +// without panicking. +func TestDecode_shortPayload(t *testing.T) { + db, err := ldf.Parse(strings.NewReader(decodeLDF)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + out := db.Decode(0x10, []byte{0xFF}) // only the low byte present + if out["Lo8"] != 0xFF { + t.Errorf("Lo8 = 0x%02X, want 0xFF", out["Lo8"]) + } + if out["Hi8"] != 0x00 { + t.Errorf("Hi8 = 0x%02X, want 0x00 (bits beyond payload)", out["Hi8"]) + } +} + +// TestSchedule_unknown returns nil for a schedule table that does not exist. +func TestSchedule_unknown(t *testing.T) { + db, err := ldf.Parse(strings.NewReader(decodeLDF)) + if err != nil { + t.Fatalf("Parse: %v", err) + } + if s := db.Schedule("DoesNotExist"); s != nil { + t.Errorf("Schedule(unknown) = %v, want nil", s) + } +} diff --git a/ldf/parser.go b/ldf/parser.go index 1077066..5d56564 100644 --- a/ldf/parser.go +++ b/ldf/parser.go @@ -104,6 +104,7 @@ type Signal struct { //fusa:req REQ-LDF-003 //fusa:req REQ-LDF-004 //fusa:req REQ-LDF-014 +//fusa:req REQ-SEC-001 func Parse(r io.Reader) (*DB, error) { db := &DB{ frames: make(map[uint8]*Frame), diff --git a/ldf/security_test.go b/ldf/security_test.go new file mode 100644 index 0000000..53cd31a --- /dev/null +++ b/ldf/security_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package ldf_test + +import ( + "strings" + "testing" + + "github.com/SoundMatt/go-LIN/ldf" +) + +// TestParse_rejectsMalformedInput is the requirement-based security test for +// REQ-SEC-001: the LDF parser treats its input as untrusted and must return an +// error (or an empty database) without panicking, for malformed, truncated, or +// hostile LDF text. A panic on untrusted input would be a denial-of-service +// vector (ISO/SAE 21434, CWE-20). +// +//fusa:test REQ-SEC-001 +func TestParse_rejectsMalformedInput(t *testing.T) { + cases := []struct { + name string + input string + }{ + {"empty", ""}, + {"garbage", "\x00\x01\x02 not an ldf at all \xff\xfe"}, + {"truncated block", "Nodes {\n Master:"}, + {"unterminated string", `LIN_protocol_version = "2.1`}, + {"unbalanced braces", "Frames {{{{{{{{{{"}, + {"huge id", "Frames { f: 99999999999999999999, MASTER, 8 ;"}, + {"negative length", "Frames { f: 0x10, MASTER, -4 ;"}, + {"only braces", strings.Repeat("{", 1000) + strings.Repeat("}", 1000)}, + {"null bytes mid-keyword", "Sig\x00nals {}"}, + {"deeply nested", strings.Repeat("Frames {", 200)}, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + // The contract is "does not panic"; an error is acceptable and + // expected. Parse must never crash on untrusted input. + defer func() { + if r := recover(); r != nil { + t.Fatalf("Parse panicked on %q: %v", tc.name, r) + } + }() + db, err := ldf.Parse(strings.NewReader(tc.input)) + if err != nil { + return // expected for malformed input + } + // If it parsed without error, accessors must also be panic-safe. + _ = db.Frames() + _ = db.Signals() + for id := uint8(0); id <= 0x3F; id++ { + _ = db.Frame(id) + } + }) + } +} diff --git a/lin.go b/lin.go index 9a463c0..fd36e83 100644 --- a/lin.go +++ b/lin.go @@ -271,6 +271,7 @@ func CalcChecksum(pid uint8, data []byte, ct LINChecksumType) uint8 { //fusa:req REQ-LIN-015 //fusa:req REQ-LIN-016 //fusa:req REQ-LIN-017 +//fusa:req REQ-SEC-004 func ValidateFrame(f Frame) error { if f.ID > LINMaxID { return fmt.Errorf("lin: frame ID 0x%02X exceeds maximum 0x%02X: %w", f.ID, LINMaxID, ErrInvalidFrame) @@ -310,6 +311,8 @@ func (f Frame) ToMessage() relay.Message { } // FromMessage converts a relay.Message envelope back to a Frame per RELAY spec §15.3. +// +//fusa:req REQ-SEC-005 func FromMessage(m relay.Message) (Frame, error) { id, err := strconv.ParseUint(m.ID, 10, 8) if err != nil || id > LINMaxID { diff --git a/lin_test.go b/lin_test.go index 635acd5..f9a3658 100644 --- a/lin_test.go +++ b/lin_test.go @@ -153,6 +153,7 @@ func TestCalcChecksum_carryAround(t *testing.T) { //fusa:test REQ-LIN-015 //fusa:test REQ-LIN-016 //fusa:test REQ-LIN-017 +//fusa:test REQ-SEC-004 func TestValidateFrame_rejectsHighID(t *testing.T) { if err := lin.ValidateFrame(lin.Frame{ID: 0x40, Data: []byte{0x01}}); err == nil { @@ -275,6 +276,7 @@ func TestFrame_ToMessage_roundtrip(t *testing.T) { } } +//fusa:test REQ-SEC-005 func TestFromMessage_invalidID(t *testing.T) { relay := lin.Frame{}.ToMessage() relay.ID = "not-a-number" diff --git a/provenance.json b/provenance.json index 6aad041..095e23f 100644 --- a/provenance.json +++ b/provenance.json @@ -4,12 +4,13 @@ "tool": "go-FuSa", "toolVersion": "0.30.0", "language": "go", - "generatedAt": "2026-06-19T19:31:15.579923071Z", + "generatedAt": "2026-06-19T20:33:25.293575Z", "format": "x-FuSa provenance v1", "module": "github.com/SoundMatt/go-LIN", - "goVersion": "go1.25.11", - "goos": "linux", - "goarch": "amd64", - "vcsRevision": "1eb4d95be1779f1fb2a8b03bddb348af124f087b", - "vcsModified": true -} + "goVersion": "go1.26.3", + "goos": "darwin", + "goarch": "arm64", + "vcsRevision": "f7ae964fb5a6ac3d5ca640cc7ec4892849accf12", + "vcsModified": true, + "builder": "https://github.com/SoundMatt/go-LIN/.github/workflows/release.yml@refs/heads/main" +} \ No newline at end of file diff --git a/safety-case.json b/safety-case.json index 1e04ae0..d9c3810 100644 --- a/safety-case.json +++ b/safety-case.json @@ -1,6 +1,6 @@ { "format": "go-FuSa Safety Case v1", - "generatedAt": "2026-06-19T19:31:15.563427057Z", + "generatedAt": "2026-06-19T20:33:25.265531Z", "module": "github.com/SoundMatt/go-LIN", "standard": "generic", "evidence": [ @@ -8,29 +8,29 @@ "id": "check", "description": "Coding standard and static analysis checks", "file": "check-report.json", - "status": "absent", - "detail": "run 'gofusa check --output check-report.json' to generate" + "status": "present", + "detail": "294 findings (0 errors, 233 warnings)" }, { "id": "trace", "description": "Requirements traceability matrix", "file": ".fusa-reqs.json", "status": "present", - "detail": "106 requirements" + "detail": "112 requirements" }, { "id": "verify", "description": "Test evidence bundle", "file": ".fusa-evidence.json", "status": "present", - "detail": "137/137 tests passed" + "detail": "176/176 tests passed" }, { "id": "qualify", "description": "Tool qualification report", "file": "qualify-report.json", - "status": "absent", - "detail": "run 'gofusa qualify' to generate" + "status": "present", + "detail": "44/44 cases passed" }, { "id": "sbom", @@ -88,8 +88,5 @@ ] } ], - "gaps": [ - "check", - "qualify" - ] + "gaps": null } diff --git a/safety-case.md b/safety-case.md index a520ded..3d47a67 100644 --- a/safety-case.md +++ b/safety-case.md @@ -1,6 +1,6 @@ # Safety Case: github.com/SoundMatt/go-LIN -Generated: 2026-06-19T19:31:15Z +Generated: 2026-06-19T20:33:25Z Standard: generic ## Top Claim @@ -12,10 +12,10 @@ argued by demonstrating compliance with the safety development lifecycle. | ID | Description | Status | Detail | |---|---|---|---| -| Sn1 | Coding standard and static analysis checks | ⚠ absent | run 'gofusa check --output check-report.json' to generate | -| Sn2 | Requirements traceability matrix | ✅ present | 106 requirements | -| Sn3 | Test evidence bundle | ✅ present | 137/137 tests passed | -| Sn4 | Tool qualification report | ⚠ absent | run 'gofusa qualify' to generate | +| Sn1 | Coding standard and static analysis checks | ✅ present | 294 findings (0 errors, 233 warnings) | +| Sn2 | Requirements traceability matrix | ✅ present | 112 requirements | +| Sn3 | Test evidence bundle | ✅ present | 176/176 tests passed | +| Sn4 | Tool qualification report | ✅ present | 44/44 cases passed | | Sn5 | SBOM (SPDX 3.0.1) | ✅ present | | | Sn6 | Build provenance | ✅ present | | @@ -31,7 +31,4 @@ argued by demonstrating compliance with the safety development lifecycle. ## Gaps -The following evidence items are absent: - -- `check` -- `qualify` +None — all evidence present. diff --git a/safety-case.mermaid b/safety-case.mermaid index a2134ab..60d9f66 100644 --- a/safety-case.mermaid +++ b/safety-case.mermaid @@ -1,13 +1,13 @@ flowchart TD G1["**G1** github.com/SoundMatt/go-LIN is acceptably safe\nfor use in generic context"] S1{{"**S1** Argued over safety lifecycle\nprocess compliance"}} - G2["**G2** Coding standard and static analysis checks\n⚠ run 'gofusa check --output check-report.json' to generate"] + G2["**G2** Coding standard and static analysis checks\n✅ 294 findings (0 errors, 233 warnings)"] Sn1(["**Sn1** check-report.json"]) - G3["**G3** Requirements traceability matrix\n✅ 106 requirements"] + G3["**G3** Requirements traceability matrix\n✅ 112 requirements"] Sn2(["**Sn2** .fusa-reqs.json"]) - G4["**G4** Test evidence bundle\n✅ 137/137 tests passed"] + G4["**G4** Test evidence bundle\n✅ 176/176 tests passed"] Sn3(["**Sn3** .fusa-evidence.json"]) - G5["**G5** Tool qualification report\n⚠ run 'gofusa qualify' to generate"] + G5["**G5** Tool qualification report\n✅ 44/44 cases passed"] Sn4(["**Sn4** qualify-report.json"]) G6["**G6** SBOM (SPDX 3.0.1)\n✅ sbom.json"] Sn5(["**Sn5** sbom.json"]) @@ -26,7 +26,3 @@ flowchart TD G5 --> Sn4 G6 --> Sn5 G7 --> Sn6 - style G2 fill:#fee2e2,stroke:#ef4444 - style Sn1 fill:#fee2e2,stroke:#ef4444 - style G5 fill:#fee2e2,stroke:#ef4444 - style Sn4 fill:#fee2e2,stroke:#ef4444 diff --git a/safety/e2e.go b/safety/e2e.go index 25e6ac1..14bbfba 100644 --- a/safety/e2e.go +++ b/safety/e2e.go @@ -160,6 +160,8 @@ func NewReceiver(cfg Config) *Receiver { //fusa:req REQ-SAFETY-011 //fusa:req REQ-SAFETY-013 //fusa:req REQ-SAFETY-015 +//fusa:req REQ-SEC-002 +//fusa:req REQ-SEC-003 func (r *Receiver) Unwrap(data []byte) ([]byte, error) { if len(data) < headerSize { return nil, &E2EError{ diff --git a/safety/e2e_test.go b/safety/e2e_test.go index 2cf86a5..0b4d817 100644 --- a/safety/e2e_test.go +++ b/safety/e2e_test.go @@ -134,6 +134,7 @@ func TestUnwrap_headerTooShort(t *testing.T) { // ── REQ-SAFETY-008: ErrCRCMismatch ─────────────────────────────────────────── //fusa:test REQ-SAFETY-008 +//fusa:test REQ-SEC-002 func TestUnwrap_crcMismatch(t *testing.T) { p := safety.NewProtector(cfg) @@ -174,6 +175,7 @@ func TestUnwrap_dataCorruption(t *testing.T) { // ── REQ-SAFETY-009: ErrSequenceGap ─────────────────────────────────────────── //fusa:test REQ-SAFETY-009 +//fusa:test REQ-SEC-003 func TestUnwrap_sequenceGap(t *testing.T) { p := safety.NewProtector(cfg) diff --git a/sas.md b/sas.md new file mode 100644 index 0000000..a08ff96 --- /dev/null +++ b/sas.md @@ -0,0 +1,56 @@ +# Software Accomplishment Summary + +> DO-178C §11.20 — This document asserts that the software lifecycle was executed +> in accordance with the approved plans and that applicable objectives are satisfied. + +| Field | Value | +|---|---| +| Project | go-LIN | +| Version | 1 | +| Standard | DO-178C / RTCA | +| Design Assurance Level | DAL-B | +| Generated | 2026-06-19 | +| Prepared by | | + +## Evidence Summary + +**14 / 20** lifecycle data items present. + +| Item | File | Present | +|---|---|:---:| +| Software Development Plan | `SAFETY_PLAN.md` | ✓ | +| Software Verification Plan | `SVP.md` | ✗ | +| Software Configuration Management Plan | `SCMP.md` | ✗ | +| Software Quality Assurance Plan | `SQAP.md` | ✗ | +| Requirements Manifest | `.fusa-reqs.json` | ✓ | +| Traceability Matrix | `.fusa-reqs.json` | ✓ | +| Test Evidence Bundle | `.fusa-evidence.json` | ✓ | +| SBOM (SPDX 3.0.1) | `sbom.json` | ✓ | +| Build Provenance | `provenance.json` | ✓ | +| Tool Qualification Report | `qualify-report.json` | ✗ | +| Safety Analysis (FMEA) | `fmea.json` | ✓ | +| Threat Analysis (TARA) | `tara.json` | ✓ | +| Vulnerability Report | `vuln.json` | ✓ | +| Component Boundary Diagram | `boundary.mermaid` | ✓ | +| Safety Case | `safety-case.json` | ✓ | +| Coverage Report | `coverage-report.json` | ✓ | +| Software Configuration Index | `sci.json` | ✓ | +| DO-178C Gap Report | `do178-gap-report.json` | ✗ | +| Problem Reports | `.fusa-problems.json` | ✓ | +| Audit Pack | `audit-pack.zip` | ✗ | + +## Gaps (6) + +- Software Verification Plan (SVP.md) — not found +- Software Configuration Management Plan (SCMP.md) — not found +- Software Quality Assurance Plan (SQAP.md) — not found +- Tool Qualification Report (qualify-report.json) — not found +- DO-178C Gap Report (do178-gap-report.json) — not found +- Audit Pack (audit-pack.zip) — not found + +## Assertion + +Software Accomplishment Summary INCOMPLETE — 6 lifecycle data item(s) are absent. See gaps list. Address all gaps before submitting for DER review. + +--- +_Generated by go-FuSa v0.18.0 — DO-178C §11.20_ diff --git a/sbom.json b/sbom.json index 9f09837..d93be2c 100644 --- a/sbom.json +++ b/sbom.json @@ -4,7 +4,7 @@ "tool": "go-FuSa", "toolVersion": "0.30.0", "language": "go", - "generatedAt": "2026-06-19T19:31:15.575269751Z", + "generatedAt": "2026-06-19T20:33:25.273124Z", "format": "x-FuSa SBOM v1", "module": "github.com/SoundMatt/go-LIN", "goVersion": "1.25.0", diff --git a/sci.json b/sci.json new file mode 100644 index 0000000..cd50863 --- /dev/null +++ b/sci.json @@ -0,0 +1,212 @@ +{ + "project": "go-LIN", + "version": "1", + "generated": "2026-06-19T20:33:24.103721Z", + "items": [ + { + "name": "Software Development Plan", + "file": "SAFETY_PLAN.md", + "class": "Plan", + "sha256": "a0298c2e34d2c87d8a1fe69d732bee77fcba7a3ebefc23a9c79afb896470877f", + "present": true, + "note": "DO-178C §11.1" + }, + { + "name": "Software Verification Plan", + "file": "SVP.md", + "class": "Plan", + "sha256": "0e57facb1a9ef1312c04cb162d764450694feb4e7c0e7d8d2dc2060f675df8ba", + "present": true, + "note": "DO-178C §11.3" + }, + { + "name": "Software Configuration Management Plan", + "file": "SCMP.md", + "class": "Plan", + "sha256": "78d5f02c213bccb48cf8cb386dd9fdf43ca512142db16020e32cad134463a4b0", + "present": true, + "note": "DO-178C §11.4" + }, + { + "name": "Software Quality Assurance Plan", + "file": "SQAP.md", + "class": "Plan", + "sha256": "610405870f2e6f967452459fcd682ded6c058446baa081a868f235f4bb05b84e", + "present": true, + "note": "DO-178C §11.5" + }, + { + "name": "Requirements Manifest", + "file": ".fusa-reqs.json", + "class": "Development", + "sha256": "28ea64cc1ca18cf4da1b04fa04b2bb7ca4563323462e680a979d28844ab3c2a0", + "present": true, + "note": "DO-178C §11.9" + }, + { + "name": "Project Configuration", + "file": ".fusa.json", + "class": "Configuration Management", + "sha256": "9f6c303db5cdb46f0c3a6ee76ab05e7610a1bf548e379ddf28f34930d9a30c69", + "present": true, + "note": "go-FuSa project config" + }, + { + "name": "SBOM (SPDX 3.0.1)", + "file": "sbom.json", + "class": "Configuration Management", + "sha256": "cdd66828f131e3be2b4bc5b5694f131f65f36abf89c8aba6e917d49ef7a23c10", + "present": true, + "note": "DO-178C §11.15, NTIA SBOM" + }, + { + "name": "Build Provenance", + "file": "provenance.json", + "class": "Configuration Management", + "sha256": "a5d76696ad1aac5d1de8e9075376c2cdacf779447ebd67d55502bf1ad2e561d7", + "present": true, + "note": "SLSA L2" + }, + { + "name": "Artifact Manifest", + "file": "manifest.json", + "class": "Configuration Management", + "present": false, + "note": "go-FuSa release" + }, + { + "name": "Test Evidence Bundle", + "file": ".fusa-evidence.json", + "class": "Verification", + "sha256": "4308a52d324af4b580e398c97d8c9b631b8c89ca80d1979b1d83e988b77d6e99", + "present": true, + "note": "DO-178C §11.14" + }, + { + "name": "Vulnerability Report", + "file": "vuln.json", + "class": "Verification", + "sha256": "9fe691cce817f659da4cc036299c3c82d1da09bc214e1f63690f56d03e89a4e4", + "present": true, + "note": "ISO 21434 / OSV" + }, + { + "name": "FMEA Table", + "file": "fmea.json", + "class": "Verification", + "sha256": "d9374c086f0bcdedaf72a38cdc8e5fc7d584872dc067a9be1aa1eff0eee9be6a", + "present": true, + "note": "DO-178C §11.9 (safety analysis)" + }, + { + "name": "TARA Report", + "file": "tara.json", + "class": "Verification", + "sha256": "2d1d2c1d83322cd42ce48e2ea8c55424562893b21c1dc64d8bf4acb97d65c64d", + "present": true, + "note": "ISO 21434 §9" + }, + { + "name": "TARA Markdown", + "file": "tara.md", + "class": "Verification", + "sha256": "6ee2465cae49efc5b900ec41c502dfee61b679b463d067845572716676f6b5c0", + "present": true, + "note": "ISO 21434 §9" + }, + { + "name": "Boundary Diagram", + "file": "boundary.mermaid", + "class": "Verification", + "sha256": "1d2ed29dd4ffdded24fa917f1e6f56e544c7c5fa4c4ee8d7f509847088408a9e", + "present": true, + "note": "DO-178C §11.10 (architecture)" + }, + { + "name": "Cyber Report", + "file": "cyber-report.json", + "class": "Verification", + "sha256": "1aa64aeb340914ecd59b6bbd6cb37fc6ba7a03a704b479aed7e5c5c06f0bc86b", + "present": true, + "note": "ISO 21434 §11" + }, + { + "name": "Tool Qualification Report", + "file": "qualify-report.json", + "class": "Quality Assurance", + "sha256": "b5401c681b62190a18f41b52d02856236ae181a158ea1d7d6aafeecbe0be18e2", + "present": true, + "note": "DO-178C §12 / DO-330" + }, + { + "name": "Safety Case", + "file": "safety-case.json", + "class": "Accomplishment Summary", + "sha256": "cf24ef8a3ff9cb470e07bcc1258a059992fca55e11235a9de9cee5a1061dc5a4", + "present": true, + "note": "DO-178C §11.20 (SAS input)" + }, + { + "name": "Software Accomplishment Summary", + "file": "sas.md", + "class": "Accomplishment Summary", + "sha256": "0f7ddcc95be64e8365704d1ffea5351942c3e7884ea3d503b37ede708f4d4361", + "present": true, + "note": "DO-178C §11.20" + }, + { + "name": "Audit Pack", + "file": "audit-pack.zip", + "class": "Configuration Management", + "present": false, + "note": "go-FuSa audit bundle" + }, + { + "name": "Problem Reports", + "file": ".fusa-problems.json", + "class": "Quality Assurance", + "sha256": "0f6160948f7e3d118049be40709eba371a05d4d59f80d83f1693d88dcf125526", + "present": true, + "note": "DO-178C §11.17" + }, + { + "name": "IEC 62443 Config", + "file": ".fusa-iec62443.json", + "class": "Configuration Management", + "sha256": "ed0728709873b6dea22392d644395a4a1cce7bfc9ce5ee5a50c78daa030bd105", + "present": true, + "note": "IEC 62443-4-1" + }, + { + "name": "CODEOWNERS", + "file": ".github/CODEOWNERS", + "class": "Configuration Management", + "present": false, + "note": "SLSA L3 review evidence" + }, + { + "name": "CI Workflow", + "file": ".github/workflows/ci.yml", + "class": "Configuration Management", + "sha256": "57218c31b37e2df3175c7b9298f453a29cd07f9bcab143d15f5903a7abbfe552", + "present": true, + "note": "DO-178C §12.2 (tool env)" + }, + { + "name": "Incident Response Plan", + "file": "INCIDENT-RESPONSE.md", + "class": "Plan", + "sha256": "eb6a38f00c92470b6cd93c882f5b4a20a25dd56b2eedc423e1a729ef3c9fc602", + "present": true, + "note": "IEC 62443-4-2 CR 6.2.1" + }, + { + "name": "Security Policy", + "file": "SECURITY.md", + "class": "Quality Assurance", + "sha256": "3a138cd578dd8da3137a38af0303aec5b26862b5e75aabdf24f2a99776ea1c19", + "present": true, + "note": "IEC 62443-4-2 CR 6.2" + } + ] +} diff --git a/seooc_test.go b/seooc_test.go index 76a3d8e..02f1a5e 100644 --- a/seooc_test.go +++ b/seooc_test.go @@ -10,11 +10,15 @@ package lin_test import ( "context" + "errors" "strings" + "sync" "testing" + "time" lin "github.com/SoundMatt/go-LIN" "github.com/SoundMatt/go-LIN/ldf" + "github.com/SoundMatt/go-LIN/master" "github.com/SoundMatt/go-LIN/safety" "github.com/SoundMatt/go-LIN/slave" "github.com/SoundMatt/go-LIN/virtual" @@ -139,3 +143,165 @@ func TestSEOOC_LDFScheduleIDsValid(t *testing.T) { } } } + +// ── REQ-SEOOC-001: go-LIN operates above the physical layer ─────────────────── +// Demonstrates that go-LIN depends only on the Bus abstraction the integrator +// supplies (the physical LIN layer, IA-01) — given any working bus, a complete +// master/slave exchange succeeds without go-LIN touching hardware. +// +//fusa:test REQ-SEOOC-001 +func TestSEOOC_OperatesAbovePhysicalLayer(t *testing.T) { + bus, err := virtual.New() // stands in for the integrator's physical layer + if err != nil { + t.Fatal(err) + } + defer bus.Close() + + s := slave.New(bus) + m := master.New(bus) + want := []byte{0x11, 0x22, 0x33} + if err := s.SetResponse(0x12, want); err != nil { + t.Fatalf("SetResponse: %v", err) + } + f, err := m.SendHeader(context.Background(), 0x12) + if err != nil { + t.Fatalf("SendHeader: %v", err) + } + if string(f.Data) != string(want) { + t.Errorf("data = %x, want %x", f.Data, want) + } +} + +// ── REQ-SEOOC-002: integrating system calls ValidateFrame on external data ──── +// Demonstrates the mechanism the integrator must invoke (IA-03): ValidateFrame +// rejects malformed externally-sourced frames and accepts well-formed ones. +// +//fusa:test REQ-SEOOC-002 +func TestSEOOC_ValidateFrameOnExternalData(t *testing.T) { + // Frames as they might arrive from untrusted hardware decode. + bad := []lin.Frame{ + {ID: 0x40, Data: []byte{0x01}}, // ID overflow + {ID: 0x10, Data: nil}, // empty payload + {ID: 0x10, Data: make([]byte, 9)}, // oversized payload + } + for i, f := range bad { + if err := lin.ValidateFrame(f); err == nil { + t.Errorf("bad[%d] %+v: ValidateFrame accepted invalid frame", i, f) + } + } + if err := lin.ValidateFrame(lin.Frame{ID: 0x10, Data: []byte{0x01, 0x02}}); err != nil { + t.Errorf("ValidateFrame rejected a valid frame: %v", err) + } +} + +// ── REQ-SEOOC-003: integrating system validates frame ID semantics ──────────── +// Demonstrates the PID parity tooling (IA-02): a protected ID verifies back to +// its source ID, and a corrupted PID is detected. +// +//fusa:test REQ-SEOOC-003 +func TestSEOOC_FrameIDSemantics(t *testing.T) { + for id := uint8(0); id <= lin.LINMaxID; id++ { + pid := lin.ProtectID(id) + got, err := lin.VerifyPID(pid) + if err != nil { + t.Fatalf("VerifyPID(0x%02X) for id 0x%02X: %v", pid, id, err) + } + if got != id { + t.Errorf("VerifyPID round-trip: got 0x%02X, want 0x%02X", got, id) + } + // Flip a parity bit — must be detected. + if _, err := lin.VerifyPID(pid ^ 0x80); err == nil { + t.Errorf("VerifyPID accepted corrupted PID for id 0x%02X", id) + } + } +} + +// ── REQ-SEOOC-007: integrating system handles ErrNoResponse safely ──────────── +// Demonstrates that an unanswered header surfaces as the ErrNoResponse sentinel +// (IA-05) so the integrator can detect a missing slave rather than act on stale +// data. +// +//fusa:test REQ-SEOOC-007 +func TestSEOOC_ErrNoResponseHandledSafely(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + defer bus.Close() + + // No slave registered for 0x20 → no response. + _, err = bus.SendHeader(context.Background(), 0x20) + if err == nil { + t.Fatal("SendHeader to unanswered ID returned nil error") + } + if !errors.Is(err, lin.ErrNoResponse) { + t.Errorf("error = %v; want errors.Is ErrNoResponse", err) + } +} + +// ── REQ-SEOOC-008: safety-critical frames routed through the safety package ─── +// Demonstrates that routing a safety-critical payload through Protect/Unwrap +// detects in-flight corruption (IA-04), which a plain LIN checksum on an 8-byte +// frame could not span. +// +//fusa:test REQ-SEOOC-008 +func TestSEOOC_SafetyRoutingDetectsCorruption(t *testing.T) { + cfg := safety.Config{DataID: 0x0042, SourceID: 0x0007} + p := safety.NewProtector(cfg) + r := safety.NewReceiver(cfg) + + protected := p.Protect([]byte{0xAA, 0xBB, 0xCC, 0xDD}) + protected[len(protected)-1] ^= 0xFF // corrupt a payload byte in flight + + if _, err := r.Unwrap(protected); err == nil { + t.Fatal("Unwrap accepted a corrupted safety-critical frame") + } +} + +// ── REQ-SEOOC-009: integrating system provides a monotonic clock ────────────── +// Demonstrates that the master schedule relies on the monotonic clock for slot +// timing (REQ-SEOOC-009): with a configured slot delay, consecutive frames are +// separated by at least that delay. +// +//fusa:test REQ-SEOOC-009 +func TestSEOOC_MonotonicClockSchedule(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + defer bus.Close() + + s := slave.New(bus) + if err := s.SetResponse(0x10, []byte{0x01}); err != nil { + t.Fatalf("SetResponse: %v", err) + } + m := master.New(bus) + const delayMs = 20 + if err := m.SetSchedule([]lin.ScheduleEntry{{ID: 0x10, DelayMs: delayMs}}); err != nil { + t.Fatalf("SetSchedule: %v", err) + } + + var mu sync.Mutex + var stamps []time.Time + m.OnFrame(func(lin.Frame) { + mu.Lock() + stamps = append(stamps, time.Now()) + mu.Unlock() + }) + + ctx, cancel := context.WithTimeout(context.Background(), 150*time.Millisecond) + defer cancel() + _ = m.Run(ctx) + + mu.Lock() + defer mu.Unlock() + if len(stamps) < 2 { + t.Fatalf("expected at least 2 scheduled frames, got %d", len(stamps)) + } + gap := stamps[1].Sub(stamps[0]) + // Generous lower bound to avoid CI flakiness while still proving the slot + // delay (and hence the monotonic clock) gates schedule advancement. + if gap < (delayMs/2)*time.Millisecond { + t.Errorf("inter-frame gap %v shorter than half the %dms slot delay", gap, delayMs) + } +} diff --git a/slave/coverage_test.go b/slave/coverage_test.go new file mode 100644 index 0000000..ad31c4c --- /dev/null +++ b/slave/coverage_test.go @@ -0,0 +1,68 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package slave_test + +import ( + "testing" + + "github.com/SoundMatt/go-LIN/slave" + "github.com/SoundMatt/go-LIN/virtual" +) + +func TestSetResponse_rejectsHighID(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + defer bus.Close() + s := slave.New(bus) + if err := s.SetResponse(0x40, []byte{0x01}); err == nil { + t.Error("SetResponse(0x40) = nil, want range error") + } +} + +func TestSetResponse_publishErrorAfterClose(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + s := slave.New(bus) + bus.Close() + if err := s.SetResponse(0x10, []byte{0x01}); err == nil { + t.Error("SetResponse on closed bus = nil, want Publish error") + } +} + +// TestSetResponse_removeKeepsOthers exercises removeID's keep-branch: removing +// one ID must leave the others registered. +func TestSetResponse_removeKeepsOthers(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + defer bus.Close() + s := slave.New(bus) + + for _, id := range []uint8{0x10, 0x11, 0x12} { + if err := s.SetResponse(id, []byte{id}); err != nil { + t.Fatalf("SetResponse(0x%02X): %v", id, err) + } + } + // Remove the middle one (nil data removes registration). + if err := s.SetResponse(0x11, nil); err != nil { + t.Fatalf("SetResponse remove: %v", err) + } + got := s.RegisteredIDs() + want := map[uint8]bool{0x10: true, 0x12: true} + if len(got) != 2 { + t.Fatalf("RegisteredIDs = %v, want 2 entries", got) + } + for _, id := range got { + if !want[id] { + t.Errorf("unexpected registered ID 0x%02X", id) + } + } +} diff --git a/tara.json b/tara.json index 4254a7e..f42aecb 100644 --- a/tara.json +++ b/tara.json @@ -1,6 +1,6 @@ { "format": "go-FuSa TARA v1", - "generated_at": "2026-06-19T19:31:14.715093225Z", + "generated_at": "2026-06-19T20:33:24.089271Z", "module": "github.com/SoundMatt/go-LIN", "entries": [ { @@ -20,7 +20,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/adapt.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/adapt.go", "source_line": 44 }, { @@ -40,8 +40,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/cmd/go-lin/main.go", - "source_line": 447 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/cmd/go-lin/main.go", + "source_line": 449 }, { "id": "TARA-003", @@ -60,7 +60,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/cmd/lintool/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/cmd/lintool/main.go", "source_line": 155 }, { @@ -80,7 +80,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 53 }, { @@ -100,7 +100,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 54 }, { @@ -120,7 +120,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 65 }, { @@ -140,7 +140,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 66 }, { @@ -160,7 +160,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 93 }, { @@ -180,7 +180,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 99 }, { @@ -200,7 +200,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/examples/quickstart/main.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/examples/quickstart/main.go", "source_line": 100 }, { @@ -220,8 +220,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", - "source_line": 441 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", + "source_line": 442 }, { "id": "TARA-012", @@ -240,8 +240,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/ldf/parser.go", - "source_line": 487 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/ldf/parser.go", + "source_line": 488 }, { "id": "TARA-013", @@ -260,7 +260,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "source_line": 255 }, { @@ -280,7 +280,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "source_line": 258 }, { @@ -300,7 +300,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "source_line": 263 }, { @@ -320,7 +320,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", "source_line": 263 }, { @@ -340,8 +340,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin.go", - "source_line": 327 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", + "source_line": 330 }, { "id": "TARA-018", @@ -360,8 +360,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin.go", - "source_line": 329 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin.go", + "source_line": 332 }, { "id": "TARA-019", @@ -380,7 +380,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin_test.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin_test.go", "source_line": 93 }, { @@ -400,7 +400,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin_test.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin_test.go", "source_line": 95 }, { @@ -420,7 +420,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin_test.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin_test.go", "source_line": 100 }, { @@ -440,7 +440,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin_test.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin_test.go", "source_line": 134 }, { @@ -460,7 +460,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/lin_test.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/lin_test.go", "source_line": 139 }, { @@ -480,8 +480,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/safety/e2e.go", - "source_line": 231 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e.go", + "source_line": 233 }, { "id": "TARA-025", @@ -500,7 +500,7 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/safety/e2e_test.go", + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e_test.go", "source_line": 61 }, { @@ -520,8 +520,8 @@ "current_control": "Add range check before conversion", "residual_risk": "Low after remediation", "cyber_rule_id": "CYBER009", - "source_file": "/home/runner/work/go-LIN/go-LIN/safety/e2e_test.go", - "source_line": 229 + "source_file": "/Users/matt/Documents/Coding/SoundMatt/go-LIN/safety/e2e_test.go", + "source_line": 231 } ] } diff --git a/tara.md b/tara.md index b2ab5b4..941ffc9 100644 --- a/tara.md +++ b/tara.md @@ -1,7 +1,7 @@ # Threat Analysis and Risk Assessment (TARA) **Module:** github.com/SoundMatt/go-LIN -**Generated:** 2026-06-19T19:31:14Z +**Generated:** 2026-06-19T20:33:24Z **Standard:** ISO 21434 Chapter 9 | ID | Asset | Threat | STRIDE | CWE | Vector | Likelihood | Impact | SL | Control | Residual Risk | diff --git a/virtual/coverage_test.go b/virtual/coverage_test.go new file mode 100644 index 0000000..3eac246 --- /dev/null +++ b/virtual/coverage_test.go @@ -0,0 +1,180 @@ +// Copyright (c) 2026 Matt Jones. All rights reserved. +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at http://mozilla.org/MPL/2.0/. + +package virtual_test + +import ( + "context" + "errors" + "testing" + "time" + + lin "github.com/SoundMatt/go-LIN" + "github.com/SoundMatt/go-LIN/virtual" +) + +func TestSetSchedule(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + if err := bus.SetSchedule([]lin.ScheduleEntry{{ID: 0x10, DelayMs: 5}}); err != nil { + t.Fatalf("SetSchedule: %v", err) + } + if err := bus.SetSchedule(nil); err != nil { // empty disables + t.Fatalf("SetSchedule(nil): %v", err) + } + bus.Close() + if err := bus.SetSchedule([]lin.ScheduleEntry{{ID: 1}}); !errors.Is(err, lin.ErrClosed) { + t.Errorf("SetSchedule after Close = %v, want ErrClosed", err) + } +} + +func TestPublishClassic(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + defer bus.Close() + + if err := bus.PublishClassic(0x10, []byte{0x01, 0x02}); err != nil { + t.Fatalf("PublishClassic: %v", err) + } + f, err := bus.SendHeader(context.Background(), 0x10) + if err != nil { + t.Fatalf("SendHeader: %v", err) + } + if f.ChecksumType != lin.ClassicChecksum { + t.Errorf("ChecksumType = %v, want ClassicChecksum", f.ChecksumType) + } + // nil removes the registration. + if err := bus.PublishClassic(0x10, nil); err != nil { + t.Fatalf("PublishClassic(nil): %v", err) + } + if _, err := bus.SendHeader(context.Background(), 0x10); !errors.Is(err, lin.ErrNoResponse) { + t.Errorf("after removal SendHeader = %v, want ErrNoResponse", err) + } + // Out-of-range ID is rejected. + if err := bus.PublishClassic(0x40, []byte{1}); err == nil { + t.Error("PublishClassic(0x40) = nil, want range error") + } +} + +func TestPublishClassic_afterClose(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + bus.Close() + if err := bus.PublishClassic(0x10, []byte{1}); !errors.Is(err, lin.ErrClosed) { + t.Errorf("PublishClassic after Close = %v, want ErrClosed", err) + } +} + +func TestSubscribe_afterClose(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + bus.Close() + if _, err := bus.Subscribe(nil); !errors.Is(err, lin.ErrClosed) { + t.Errorf("Subscribe after Close = %v, want ErrClosed", err) + } +} + +func TestSubscribe_filterMatching(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + defer bus.Close() + + ch, err := bus.Subscribe([]lin.Filter{{ID: 0x10}}) // only 0x10 + if err != nil { + t.Fatal(err) + } + bus.Publish(0x10, []byte{0xAA}) + bus.Publish(0x20, []byte{0xBB}) + if _, err := bus.SendHeader(context.Background(), 0x20); err != nil { + t.Fatal(err) + } + if _, err := bus.SendHeader(context.Background(), 0x10); err != nil { + t.Fatal(err) + } + select { + case f := <-ch: + if f.ID != 0x10 { + t.Errorf("filtered subscriber received ID 0x%02X, want only 0x10", f.ID) + } + case <-time.After(time.Second): + t.Fatal("expected a frame for 0x10") + } +} + +func TestCloseWithDrain(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + ch, err := bus.Subscribe(nil) + if err != nil { + t.Fatal(err) + } + bus.Publish(0x10, []byte{0x01}) + if _, err := bus.SendHeader(context.Background(), 0x10); err != nil { + t.Fatal(err) + } + // Drain: consume the pending frame concurrently so the channel empties. + go func() { + <-ch + }() + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + if err := bus.CloseWithDrain(ctx); err != nil { + t.Fatalf("CloseWithDrain: %v", err) + } +} + +func TestCloseWithDrain_ctxCancel(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + ch, err := bus.Subscribe(nil) + if err != nil { + t.Fatal(err) + } + _ = ch // never drained → forces the ctx.Done() branch + bus.Publish(0x10, []byte{0x01}) + if _, err := bus.SendHeader(context.Background(), 0x10); err != nil { + t.Fatal(err) + } + ctx, cancel := context.WithTimeout(context.Background(), 20*time.Millisecond) + defer cancel() + if err := bus.CloseWithDrain(ctx); err != nil { + t.Fatalf("CloseWithDrain (ctx cancel path): %v", err) + } +} + +func TestHealthAndMetrics(t *testing.T) { + bus, err := virtual.New() + if err != nil { + t.Fatal(err) + } + if h := bus.Health(); h.Status != lin.HealthOK { + t.Errorf("Health = %v, want HealthOK", h.Status) + } + bus.Publish(0x10, []byte{0x01, 0x02}) + if _, err := bus.SendHeader(context.Background(), 0x10); err != nil { + t.Fatal(err) + } + if m := bus.Metrics(); m.WriteCount == 0 { + t.Error("Metrics.WriteCount = 0 after a SendHeader") + } + bus.Close() + if h := bus.Health(); h.Status != lin.HealthDown { + t.Errorf("Health after Close = %v, want HealthDown", h.Status) + } +}