diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c1429a3..6a50105 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,28 +88,13 @@ jobs: run: | docker save iolinki-zephyr-base > zephyr-base-image.tar - # The following jobs exist to satisfy branch protection requirements. They - # depend only on the lightweight host gate, NOT on the heavy Zephyr job. - quality-gate: - needs: docker-validation + sbom-tools: runs-on: ubuntu-latest steps: - - run: echo "Quality Gate passed (verified in docker-validation)" - - build-and-test: - needs: docker-validation - runs-on: ubuntu-latest - steps: - - run: echo "Build and Test passed (verified in docker-validation)" - - build-bare-metal: - needs: docker-validation - runs-on: ubuntu-latest - steps: - - run: echo "Bare Metal build passed (verified in docker-validation)" + - uses: actions/checkout@v4 + - name: Test SBOM generator + run: python3 tools/test_generate_sbom.py - build-example: - needs: docker-validation - runs-on: ubuntu-latest - steps: - - run: echo "Zephyr Example build passed (verified in docker-validation)" + # Branch protection on develop requires the REAL jobs directly: + # docker-validation, zephyr-validation, sbom-tools. Do not reintroduce + # echo-only proxy jobs — they let a red job hide behind a green run. diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 99b7d25..a165701 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,6 +87,20 @@ jobs: # Cat to console for debugging cat release_notes.md + - name: Generate SBOMs + run: | + # Reproducible timestamps: pin to the release commit time + export SOURCE_DATE_EPOCH=$(git log -1 --format=%ct) + python3 tools/test_generate_sbom.py + python3 tools/generate_sbom.py \ + --version "${{ steps.version.outputs.version }}" \ + --format cyclonedx \ + --output "iolinki-${{ steps.version.outputs.version }}.cdx.json" + python3 tools/generate_sbom.py \ + --version "${{ steps.version.outputs.version }}" \ + --format spdx \ + --output "iolinki-${{ steps.version.outputs.version }}.spdx.json" + - name: Create GitHub Release uses: softprops/action-gh-release@v1 with: @@ -98,6 +112,8 @@ jobs: files: | build/examples/simple_device/simple_device build/tests/test_init + iolinki-${{ steps.version.outputs.version }}.cdx.json + iolinki-${{ steps.version.outputs.version }}.spdx.json env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/CHANGELOG.md b/CHANGELOG.md index c35cac7..af5eaf4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,29 @@ All notable changes to the `iolinki` project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [1.2.0] - 2026-07-02 +### Added +- **Per-release SBOMs**: every tagged release now ships CycloneDX 1.6 and SPDX 2.3 + SBOMs (`iolinki-.cdx.json` / `.spdx.json`) generated by + `tools/generate_sbom.py` and attached as release assets. +- **Security documentation package**: CRA-ready coordinated-disclosure policy + (`SECURITY.md`), STRIDE threat model aligned to IO-Link Security Design and + Development Guideline 10.512 (`docs/security/THREAT_MODEL.md`), and a Cyber + Resilience Act overview for device makers (`docs/security/CRA.md`). + +### Fixed +- **Zephyr module build**: `zephyr/CMakeLists.txt` still referenced + `src/iolink_core.c`, deleted by the singleton-API removal; the module now + builds against `src/device.c`. The breakage was invisible because + `zephyr-validation` was not a required check. + +### Changed +- **CI gates are now real**: branch protection on `develop` and `main` requires + `docker-validation`, `zephyr-validation`, and `sbom-tools` directly; the + echo-only proxy jobs (`quality-gate`, `build-and-test`, `build-bare-metal`, + `build-example`) are removed. + ## [1.1.2] - 2026-06-15 ### Fixed - **Device communication over the UART PHY**: the DLL booted in SIO mode and only left it upon electrical wake-up detection. A plain UART PHY cannot observe the C/Q wake-up pulse (`detect_wakeup == NULL`), so a device on real UART hardware stayed in SIO forever and never serviced M-sequences. The DLL now starts in SDCI when the PHY cannot detect wake-up (the already-established-COM contract of the UART PHY), so the device responds to the Master immediately. diff --git a/README.md b/README.md index 9203fef..540cf5d 100644 --- a/README.md +++ b/README.md @@ -170,6 +170,17 @@ See [ROADMAP.md](./docs/ROADMAP.md) for detailed development phases. - **[INSTALL.md](./INSTALL.md)** - Detailed installation instructions - **[PUPPETEER.md](./docs/PUPPETEER.md)** - Agent task mutex workflow (submodule) +## Security + +- **[SECURITY.md](./SECURITY.md)** — coordinated disclosure policy and CRA-ready + vulnerability handling +- **[Threat model](./docs/security/THREAT_MODEL.md)** — STRIDE analysis aligned to + the IO-Link Security Design and Development Guideline (Order No. 10.512), every + claim anchored to code and tests +- **[CRA overview](./docs/security/CRA.md)** — what the EU Cyber Resilience Act + means for devices built on this stack; free vs. commercial deliverables +- **SBOMs** — CycloneDX 1.6 + SPDX 2.3 attached to every tagged release + ## Releases Official releases are available on [GitHub Releases](https://github.com/yourusername/iolinki/releases). diff --git a/SECURITY.md b/SECURITY.md index 273c1c0..8a1e2b8 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,21 +1,69 @@ # Security Policy +`iolinki` is a dual-licensed IO-Link device stack intended for integration into +industrial products. Security is handled as an engineering discipline here, not a +checkbox: the stack ships with a public [threat model](docs/security/THREAT_MODEL.md) +aligned to the IO-Link Security Design and Development Guideline (Order No. 10.512), +per-release SBOMs, and the coordinated-disclosure process below. See +[docs/security/CRA.md](docs/security/CRA.md) for what this means for your product +under the EU Cyber Resilience Act. + ## Supported Versions -Only the latest `main` branch and the latest tagged release are supported with security updates. +| Version | Security fixes | +| ------- | -------------- | +| Latest tagged release + `develop` | ✅ free of charge | +| Older releases | Under a commercial support agreement only | -| Version | Supported | -| ------- | ------------------ | -| Latest | :white_check_mark: | -| < 0.1.0 | :x: | +Security fixes are delivered as tagged releases with a changelog entry and, for +confirmed vulnerabilities, a GitHub security advisory. Commercial licensees can +contractually fix a support period per release (default five years) — see the +commercial security-update terms. ## Reporting a Vulnerability -If you discover a security vulnerability in `iolinki`, please do NOT open a public issue. +Please do **not** open a public issue for suspected vulnerabilities. + +1. **Preferred:** GitHub private vulnerability reporting — use *Report a + vulnerability* under the repository's **Security** tab. +2. Alternatively, contact the maintainer privately (contact details on the GitHub + profile). + +What to include: affected version/commit, the IO-Link frame or ISDU sequence (or +code path) that triggers the issue, and impact as you understand it. A proof of +concept against the virtual PHY or the docker test harness is ideal but not +required. + +**Response targets:** + +- Acknowledgement within **72 hours**. +- Triage verdict (accepted / rejected / needs info) within **14 days**. +- Fix timeline agreed with the reporter at triage; critical issues in the frame or + ISDU parsing paths are prioritized ahead of all other work. + +## Coordinated Disclosure + +We ask reporters to withhold public disclosure until a fixed release is available. +In return we commit to: keeping the reporter informed, crediting them in the +advisory and changelog (unless they prefer otherwise), and not taking legal action +against good-faith research performed against your own or simulated hardware. + +## CRA Readiness -Instead, please report it via: +For products placed on the EU market, Regulation (EU) 2024/2847 (Cyber Resilience +Act) applies — its vulnerability-reporting obligations from September 2026, its +full obligations from December 2027. As the supplier of a commercially licensed +stack we maintain the corresponding internal process: confirmed actively exploited +vulnerabilities in the stack are handled under the CRA notification regime (early +warning within 24 hours, notification within 72 hours) and communicated to +commercial licensees so they can meet their own Article 14 duties. -1. **GitHub Private Reporting**: Use the "Report a vulnerability" button in the `Security` tab if enabled. -2. **Email**: Contact the maintainers directly (details in `AUTHORS` or via GitHub profile). +Machine-readable SBOMs (CycloneDX 1.6 and SPDX 2.3) are attached to every tagged +release. The stack has **zero third-party runtime dependencies**; the SBOM states +this explicitly rather than by omission. -We will investigate all reports and provide a fix as soon as possible. +Integrating the stack does not transfer manufacturer obligations: device makers +remain responsible for their own conformity assessment, CE marking, and reporting. +The commercial CRA package (compliance statement mapped to your product context, +contractual update terms) exists to feed your technical documentation — see +[docs/security/CRA.md](docs/security/CRA.md). diff --git a/docs/security/CRA.md b/docs/security/CRA.md new file mode 100644 index 0000000..11c2d56 --- /dev/null +++ b/docs/security/CRA.md @@ -0,0 +1,57 @@ +# iolinki and the EU Cyber Resilience Act + +*This page is orientation for device makers evaluating the stack. It is not legal +advice; your conformity assessment is yours.* + +## You remain the manufacturer + +Integrating `iolinki` — or any third-party stack — does not change your status +under Regulation (EU) 2024/2847 (the Cyber Resilience Act). CE marking, the EU +Declaration of Conformity, the ten-year technical-documentation retention, and the +Article 14 reporting obligations for your product stay entirely with you. What a +stack supplier owes you is the *foundation* your technical documentation builds +on. That is exactly what this package is. + +Key dates: the CRA's vulnerability-reporting obligations apply from +**11 September 2026**; the full obligations from **11 December 2027**. + +## What iolinki provides + +| Deliverable | Where | Terms | +|---|---|---| +| SBOM per release (CycloneDX 1.6 + SPDX 2.3) | attached to every [tagged release](https://github.com/w1ne/iolinki/releases) | free, public | +| STRIDE threat model aligned to IO-Link guideline 10.512 | [`docs/security/THREAT_MODEL.md`](THREAT_MODEL.md) | free, public | +| Coordinated disclosure + advisory process | [`SECURITY.md`](../../SECURITY.md) | free, public | +| CRA compliance statement mapping the stack to Regulation (EU) 2024/2847 Annex I, issued per stack release for your product context | commercial license package | commercial | +| Contractually agreed security updates over a defined support period (default five years) | commercial license package | commercial | + +The public artifacts let you verify our engineering rigor before you talk to us. +The commercial artifacts are the contract-grade documents your CRA technical file +and your supplier-management process need — the same package other stack vendors +have announced for late 2026, available from us now (pending final legal review of +the statement wording). + +## Why a protocol stack is in scope at all + +Commercially licensed software placed on the EU market is a "product with digital +elements" under the CRA. As the stack's supplier we carry manufacturer obligations +for the stack itself — which is why the SBOM, the disclosure process, and the +support-period commitment exist as maintained artifacts rather than sales +material. The stack is in the CRA's *default* (non-critical) class: conformity is +self-assessed, no notified body involved. + +## The division of labor, concretely + +**We cover (for the stack):** risk analysis of the stack's attack surface +(threat model), input-validation and integrity mechanisms with code anchors, +SBOM, vulnerability handling and advisories, security fixes over the support +period. + +**You cover (for your device):** your product risk assessment, firmware update +mechanism and its authenticity verification (the stack does not include the +BLOB Transfer & Firmware Update profile — see the threat model's gaps section), +boot integrity, physical-protection guidance in your user documentation, your +DoC, CE marking, and Article 14 reporting. + +For commercial-package inquiries, use the contact on the maintainer's GitHub +profile or open a (non-security) discussion on the repository. diff --git a/docs/security/THREAT_MODEL.md b/docs/security/THREAT_MODEL.md new file mode 100644 index 0000000..3098555 --- /dev/null +++ b/docs/security/THREAT_MODEL.md @@ -0,0 +1,208 @@ +# iolinki Threat Model + +**Aligned to:** IO-Link Security Design and Development Guideline, Order No. 10.512 +(D1.0.0-01, October 2025) and IO-Link Secure Deployment Guideline, Order No. 10.502 +(V1.0.0, June 2025), both published by the IO-Link Community. This document +paraphrases and cites those guidelines; it does not reproduce their text. Obtain +them from [io-link.com/downloads](https://io-link.com/downloads). + +**Scope:** the `iolinki` device stack as a software component integrated into an +IO-Link Device. Per 10.512 (clause 1), IO-Link Devices are *embedded devices* in +the sense of IEC 62443-4-2, targeted at capability security level SL-C 1, and the +protocol in scope is the wired point-to-point interface only — no networking, no +TCP/IP, no wireless. Anything outside the stack (device hardware, firmware-update +mechanism, product application logic) is the device maker's domain; this document +is explicit about where that boundary lies. + +Every stack claim below carries a code or test anchor. A claim without an anchor +belongs in [§5 Gaps](#5-gaps-and-integrator-duties), not here. + +## 1. System model and trust boundaries + +``` + IO-Link Master ══ 3-wire cable ══╗ (untrusted input boundary) + ▼ + PHY drivers src/phy_generic.c, src/phy_virtual.c + │ bytes + Framing + checksum src/frame.c, src/crc.c + │ validated frames + DLL state machine src/dll.c (mode/state legality) + │ on-request data / process data + ISDU parser src/isdu.c (service PDU parsing) + ├─ Direct Parameters src/params.c + ├─ Data Storage src/data_storage.c + ├─ Events src/events.c + └─ Device Info src/device_info.c + │ callbacks (trusted) + Device application (device maker's code) +``` + +Trust boundaries: + +- **The wire is untrusted.** Everything arriving at the PHY is attacker-controlled + in the threat scenarios of 10.512 clause 6. The stack's job is to ensure no byte + sequence received from the wire can corrupt stack state or the application's + memory. +- **The application callbacks are trusted.** The stack executes in the device + firmware's trust domain; it does not defend against its own host. +- **Build/supply chain** is outside the runtime model and covered by the SBOM + (see `SECURITY.md`): the stack has zero third-party runtime dependencies, so the + supply-chain surface is this repository itself plus the device maker's toolchain. + +## 2. Assets + +| Asset | Where it lives | +|---|---| +| Process data integrity (sensor/actuator values) | DLL + application callbacks | +| Device parameterization | `src/params.c`, `src/data_storage.c` | +| Identification and diagnosis data | `src/device_info.c`, `src/events.c` | +| Availability of the device function | whole stack, fixed-resource design | + +## 3. STRIDE analysis + +The threat catalogue follows 10.512 clause 6 (Table 1), which identifies spoofing +of either peer, tampering/replay on the wire, information disclosure on the wire, +and denial of service as the relevant threats, with **physical protection of cable +and device as the guideline's countermeasure at SL-C 1**. The protocol itself +carries no cryptographic authentication, integrity, or confidentiality mechanisms +(10.512 clauses 7.4.2, 7.5.2). The stack therefore cannot — and does not claim to — +defend against a physically present attacker; what it guarantees is that malformed +or hostile traffic is *rejected safely* rather than corrupting the device. + +### S — Spoofing (either peer impersonated on the wire) + +- Protocol reality (10.512 §6): no peer authentication exists; countermeasure is + physical protection. Integrator duty. +- Stack guarantee: a spoofing peer gets no more capability than the protocol + grants any Master. All state transitions are driven through the DLL state + machine (`src/dll.c`); out-of-state requests are rejected rather than acted on. + +### T — Tampering (modified, replayed, or forged frames) + +- Protocol reality: the per-frame checksum/CRC detects *accidental* corruption + only; intentional modification with a correct checksum is undetectable at the + protocol level (10.512 §7.4.2). Integrator duty: physical protection; a security + assessment note to that effect belongs in the device's user documentation + (paraphrasing the contextual mapping of CR 3.1 in 10.512 §7.4.2). +- Stack guarantees (CR 3.1, CR 3.5 — 10.512 §7.4.2, §7.4.5): + - Every received OPERATE frame is verified against CRC6 before use: + `src/frame.c:71` (`checksum_ok` computed via `iolink_crc6`, `src/crc.c`). + - Frame decode enforces exact expected length and PD/OD bounds; NULL and + size violations are rejected before any parsing: `src/frame.c:61-62` + (decode), `src/frame.c:30-34` (encode). + - ISDU payloads land in fixed-size buffers (`IOLINK_ISDU_BUFFER_SIZE`, + `include/iolinki/isdu.h:71-74`); reads are clamped to buffer size + (`src/isdu.c:446-447`, `src/isdu.c:327,351,374`); segmented transfers + enforce start/last/sequence-number legality and answer violations with the + protocol SEGMENTATION error (`src/isdu.c:66-68,132`). + - Stored parameterization is integrity-checked: the Data Storage image + carries a Fletcher-16 checksum (`iolink_ds_calc_checksum`, + `src/data_storage.c:74`), recomputed on serialization + (`src/data_storage.c:121`) and used for consistency comparison and + recovery on mismatch (CR 3.4 — 10.512 §7.4.4). + - Data Storage writes respect the Device Access Locks parameter (index + 0x000C, `include/iolinki/protocol.h:45`): a set DS lock refuses the + operation (`src/data_storage.c:301`). + +### R — Repudiation + +- Assessed not relevant for Devices in 10.512 (§7.3.9): no human users, no + accounts; the Master timestamps and stores events (§7.3.8). No stack claims. + +### I — Information disclosure (wire eavesdropping) + +- Protocol reality: no encryption exists (10.512 §7.5.2); confidentiality of data + in transit is achieved by restricting physical access. Integrator duty, to be + stated in the device's user documentation per the guideline. +- Stack guarantee (CR 3.7 — 10.512 §7.4.7): error paths answer with + protocol-defined error codes only (e.g. `IOLINK_ISDU_ERROR_*`, `src/isdu.c`); + no internal state, addresses, or diagnostics beyond the spec-defined responses + leave the device through the stack. + +### D — Denial of service (flooding, selective drops, disconnection) + +- Protocol reality: a point-to-point peer can always simply stop communicating; + DoS by the peer is not defendable at the Device (10.512 §7.8.2 justification). +- Stack guarantees (aligned with the §7.8.2/§7.8.3 expectation that a Device + processes traffic at the maximum protocol rate): + - No dynamic memory allocation anywhere in the stack (no `malloc`/`free` in + `src/` or `include/`); all contexts and buffers are fixed-size and + zero-initialized (`iolink_ctx_zero`, `src/isdu.c:39`). Sustained flooding + cannot exhaust heap because there is none. + - Loss of communication is an explicit, observable DLL state: consecutive + errors drive the FALLBACK state and SIO fallback + (`include/iolinki/dll.h:32,114-115`), so an actuator application can apply + its safe-state policy (CR 3.6 — 10.512 §7.4.6). Exercised by + `tests/test_sio_fallback.c` and `tests/test_error_recovery.c`. + - Event-queue overflow has defined behavior instead of failure, matching the + overwrite-oldest expectation of 10.512 §7.3.7 (`src/events.c`, + `tests/test_events.c`). + +## 4. IEC 62443-4-2 requirement mapping (stack view) + +10.512 clause 7 (Table 2) assesses which IEC 62443-4-2 requirements are relevant +for an IO-Link Device at SL-C 1. The rows below restate the *relevant* ones as +stack-level claims; requirement interpretation belongs to the guideline, the +anchors are ours. Rows assessed "not relevant" in Table 2 (FR 1 identification, +most of FR 2, host/network device requirements) are omitted here for the same +reasons the guideline gives: machine-to-machine point-to-point protocol, no human +users, not a network device. + +| Requirement (10.512 ref) | Stack claim | Anchor | +|---|---|---| +| CR 2.8 auditable events (§7.3.5) | Security-relevant conditions are reported as protocol events with standard EventCodes (V1.1.5 Annex D constants) | `include/iolinki/events.h:34+`, `iolink_event_classify` `src/events.c:149` | +| CR 2.9 audit storage (§7.3.6) | Events queue until transmitted/acknowledged; bounded storage | `src/events.c`, `tests/test_events.c` | +| CR 2.10 audit failure response (§7.3.7) | Queue overflow behavior is defined (oldest overwritten), never a crash | `src/events.c` | +| CR 3.1 communication integrity (§7.4.2) | CRC6/checksum verified on every OPERATE frame; unintentional corruption detected | `src/frame.c:71`, `src/crc.c`, `tests/test_crc.c`, `tests/test_frame.c` | +| CR 3.4 software/information integrity (§7.4.4) | Data Storage image checksummed (Fletcher-16), consistency-compared, recovered on mismatch | `src/data_storage.c:74,121,157`, `tests/test_ds.c` | +| CR 3.5 input validation (§7.4.5) | Length, bounds, and sequence legality of every received frame and ISDU enforced before use | `src/frame.c:30-34,61-62`, `src/isdu.c:66-68,132,446-447`, `tests/test_isdu*.c` | +| CR 3.6 deterministic output (§7.4.6) | Communication loss is an observable DLL state for actuator safe-state policies | `include/iolinki/dll.h:32,114-115`, `tests/test_sio_fallback.c` | +| CR 3.7 error handling (§7.4.7) | Errors answered with protocol-defined codes only; no internal-state leakage | `src/isdu.c` error paths | +| CR 4.1/4.3 confidentiality, cryptography (§7.5) | Stack processes no confidential data and uses no cryptography; nothing to claim, nothing misused | — (see Gaps for device-level duties) | +| CR 5.1 network segmentation (§7.6.2) | Point-to-point by construction | protocol property | +| CR 7.3/7.4 backup and recovery (§7.8.4-5) | Data Storage parameter server (index 0x0003) implements upload/restore of device parameterization with Access-Lock integration | `src/data_storage.c`, `include/iolinki/protocol.h:45`, `tests/test_ds.c` | +| CR 7.6/7.7 config settings, least functionality (§7.8.6-7) | All optional stack features are compile-time configurable (`include/iolinki/config.h`); unused services can be excluded from the build | `include/iolinki/config.h` | + +## 5. Gaps and integrator duties + +Stated plainly, because a threat model that hides gaps is marketing: + +1. **No BLOB Transfer & Firmware Update profile.** 10.512 expects Devices to be + updatable and to verify update authenticity (EDR 3.2/3.10/3.14, §7.10.2-5). + The stack implements the firmware-*revision* identification string + (`include/iolinki/device_info.h:35`) but not the update transport or its + verification. Firmware update capability, signature verification, and boot + integrity are the device maker's responsibility. If you need the BLOB profile + in the stack, that is commercial feature work. +2. **Physical protection is your countermeasure.** Per 10.512 §6 and the Secure + Deployment Guideline 10.502, spoofing/tampering/disclosure on the wire are + mitigated physically at SL-C 1. Your user documentation should carry a + security-assessment recommendation to that effect (see the CR 3.1 contextual + mapping in 10.512 §7.4.2 for the guideline's suggested wording). +3. **Device-level requirements the stack cannot see:** disabling local user + interfaces (10.512 clause 5), confidential data at rest beyond IO-Link + parameters (§7.5.2), boot-time integrity of your firmware image (§7.10.5), + and your product's risk assessment under the CRA. +4. **10.512 is a draft under review until 2026-02-03.** Claims here cite + D1.0.0-01; we track the final release and will re-verify this mapping. + +## 6. Verification + +The claims above are regression-checked by the existing test suite (cmocka via +CMake/CTest, run in Docker in CI — see `run_all_tests_docker.sh`): + +- Frame/CRC validation: `tests/test_frame.c`, `tests/test_crc.c`, + `tests/test_crc_manual.c` +- ISDU parsing, segmentation, flow control: `tests/test_isdu.c`, + `tests/test_isdu_segmented.c`, `tests/test_isdu_flow_control.c` +- Data Storage checksum/recovery/locks: `tests/test_ds.c` +- Events and classification: `tests/test_events.c` +- Communication loss and fallback: `tests/test_sio_fallback.c`, + `tests/test_error_recovery.c`, `tests/test_dll.c` +- End-to-end device behavior: `tests/test_integration_full.c` + +Static analysis: cppcheck with MISRA-oriented rules (`tools/run-cppcheck.sh`, +`MISRA_DEVIATIONS.md`) runs in the CI quality gate. + +*Maintenance rule:* any PR that changes a file cited as an anchor here must +re-verify the corresponding claim or update this document. diff --git a/docs/superpowers/plans/2026-07-02-cra-security-package.md b/docs/superpowers/plans/2026-07-02-cra-security-package.md new file mode 100644 index 0000000..3dbfe49 --- /dev/null +++ b/docs/superpowers/plans/2026-07-02-cra-security-package.md @@ -0,0 +1,98 @@ +# CRA Security Package Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Ship the four-part CRA conformance package for the iolinki device stack: per-release SBOMs, a 10.512-aligned STRIDE threat model, an upgraded security policy, a public CRA overview, plus commercial-repo templates (compliance statement, update terms). + +**Architecture:** One stdlib-only Python SBOM generator wired into the existing tag-triggered release workflow; four Markdown documents in the public repo; two DRAFT templates in `iolinki-private`. No stack (C) code changes. + +**Tech Stack:** Python 3 stdlib (generator + unittest), GitHub Actions, Markdown. + +**Spec:** `docs/superpowers/specs/2026-07-02-cra-security-package-design.md` — content requirements for every document live there (section 5). The 10.512 guideline PDF is at `/tmp/claude-1000/-home-andrii/06eaa99b-dd07-4d2b-b5a4-cbfca1bb54b5/scratchpad/iol_sec_desdev_10512.pdf`; its license forbids reproducing its text/tables — paraphrase + cite clause numbers only. + +## Global Constraints + +- Work in `~/projects/iolinki-cra-security` (worktree, branch `feat/cra-security-package`); iolinki-private work in `~/projects/iolinki-private` (plain checkout, default branch). +- License expression everywhere: `GPL-3.0-only OR LicenseRef-iolinki-Commercial`. +- Python must pass `ruff check` with repo `pyproject.toml`; stdlib only, no pip installs. +- Threat-model claims MUST be verified against source before written (grep the anchor; if absent, the claim moves to the gaps section). +- Commercial templates carry a `> **DRAFT — PENDING LEGAL REVIEW.**` banner and are not customer-deliverable until reviewed. +- Support period default: 5 years from release. + +--- + +### Task 1: SBOM generator with tests + +**Files:** +- Create: `tools/generate_sbom.py` +- Create: `tools/test_generate_sbom.py` + +**Interfaces:** +- Produces CLI: `python3 tools/generate_sbom.py --version 1.1.3 --format cyclonedx|spdx --output `; exit 0 on success. +- Produces functions used by tests: `build_cyclonedx(version: str) -> dict`, `build_spdx(version: str) -> dict`. + +- [x] **Step 1: Write failing tests** — `tools/test_generate_sbom.py` (unittest): CycloneDX doc has `bomFormat == "CycloneDX"`, `specVersion == "1.6"`, root component name `iolinki`, `version` propagated, purl `pkg:github/w1ne/iolinki@v`, license expression exact, `dependencies` for root component present with empty `dependsOn` (auditable zero-dep claim); SPDX doc has `spdxVersion == "SPDX-2.3"`, `SPDXID == "SPDXRef-DOCUMENT"`, one package with `licenseDeclared` exact, `externalRefs` purl, relationship `DESCRIBES`; CLI writes valid JSON to `--output` and exits 0; unknown `--format` exits non-zero. +- [x] **Step 2: Run tests, verify FAIL** — `python3 tools/test_generate_sbom.py` → import error / failures. +- [x] **Step 3: Implement `tools/generate_sbom.py`** — stdlib (argparse/json/datetime/uuid); metadata constants (supplier, license, purl, repo URL, description); timestamp from `SOURCE_DATE_EPOCH` if set else now (deterministic CI option); build/test-only tools (cmake, cmocka) listed as CycloneDX components with scope `excluded` / SPDX packages with `BUILD_TOOL_OF` relationship. +- [x] **Step 4: Run tests, verify PASS**; run `ruff check tools/generate_sbom.py tools/test_generate_sbom.py` → clean. +- [x] **Step 5: Commit** — `feat: add per-release SBOM generator (CycloneDX 1.6 + SPDX 2.3)`. + +### Task 2: Wire SBOMs into release + CI + +**Files:** +- Modify: `.github/workflows/release.yml` (after tests, before/with release creation) +- Modify: `.github/workflows/ci.yml` (new lightweight job) +- Modify: `CHANGELOG.md` (Unreleased → Added) + +**Interfaces:** +- Consumes Task 1 CLI. Release assets named `iolinki-.cdx.json`, `iolinki-.spdx.json`. + +- [x] **Step 1: release.yml** — add step generating both SBOMs with `${{ steps.version.outputs.version }}`, attach via existing release action's files list (inspect current upload mechanism and extend it). +- [x] **Step 2: ci.yml** — add job `sbom-tools` (ubuntu-latest, no Docker): checkout + `python3 tools/test_generate_sbom.py`. +- [x] **Step 3: Validate YAML** — `python3 -c "import yaml,sys; yaml.safe_load(open('.github/workflows/release.yml')); yaml.safe_load(open('.github/workflows/ci.yml'))"`. +- [x] **Step 4: CHANGELOG entry** — Added: per-release SBOMs + security docs package. +- [x] **Step 5: Commit** — `ci: attach CycloneDX/SPDX SBOMs to releases, test SBOM tool in CI`. + +### Task 3: SECURITY.md upgrade + +**Files:** +- Modify: `SECURITY.md` (full rewrite per spec 5.3) + +- [x] **Step 1: Rewrite** — sections: Supported Versions (latest free; older = commercial support), Reporting (GitHub private vulnerability reporting primary; ack ≤72h, triage verdict ≤14d), Coordinated Disclosure (advisory + patched release; reporter credit), CRA Readiness (internal ENISA 24h/72h process commitment; SBOM pointer; threat model pointer; commercial package pointer). No invented email addresses. +- [x] **Step 2: Verify links resolve** (relative paths exist in repo). +- [x] **Step 3: Commit** — `docs: upgrade SECURITY.md to CRA-ready disclosure policy`. + +### Task 4: STRIDE threat model + +**Files:** +- Create: `docs/security/THREAT_MODEL.md` (structure per spec 5.4 — six sections) + +- [x] **Step 1: Verify every planned code anchor** — grep each of: checksum verify path in `src/frame.c`/`src/crc.c`; ISDU bounds checks in `src/isdu.c`; DS Fletcher-16 + recovery in `src/data_storage.c`; event queue + Annex D codes in `src/events.c`; comm-loss surfacing in `src/dll.c`/`include/iolinki/*.h`; Access-Lock in `src/data_storage.c`/`src/params.c`. Note exact symbol/line for each; drop/move unverifiable claims to gaps. +- [x] **Step 2: Write the document** — paraphrase 10.512 §6 threat catalogue + §7 Table 2 stack-relevant rows with clause citations; explicit gaps: no BLOB Transfer & FW Update profile (EDR 3.2/3.10/3.14 → device maker), no crypto in protocol (CR 3.1/4.1 → physical protection per guideline), DoS unmitigable by protocol (CR 7.1 justification). Verification section maps claims → existing cmocka/E2E tests (grep `tests/` for covering tests, name them). +- [x] **Step 3: Commit** — `docs: add STRIDE threat model aligned to IO-Link guideline 10.512`. + +### Task 5: Public CRA overview + +**Files:** +- Create: `docs/security/CRA.md` (per spec 5.5) +- Modify: `README.md` (Security section: 3 links — SECURITY.md, THREAT_MODEL.md, CRA.md) + +- [x] **Step 1: Write CRA.md** — manufacturer-stays-responsible framing; free vs commercial table; timeline (Sep 2026 reporting, Dec 2027 full); not-legal-advice disclaimer. +- [x] **Step 2: README security section** + link check. +- [x] **Step 3: Commit** — `docs: add CRA overview for device makers; link security docs from README`. + +### Task 6: Commercial templates (iolinki-private) + +**Files:** +- Create: `~/projects/iolinki-private/docs/cra/CRA_COMPLIANCE_STATEMENT.md` (per spec 5.6) +- Create: `~/projects/iolinki-private/docs/cra/SECURITY_UPDATE_TERMS.md` (per spec 5.7) + +- [x] **Step 1: Check repo state** — `git -C ~/projects/iolinki-private status`, pull default branch. +- [x] **Step 2: Write both templates** — DRAFT banners; statement: Annex I Part I mapping (evidence: CI, cppcheck/MISRA, tests), Part II mapping (SBOM/disclosure/advisories/support period), responsibility split; terms: 5y default, severity tiers matching SECURITY.md targets, delivery via tagged release + advisory. +- [x] **Step 3: Commit + push** to iolinki-private default branch. + +### Task 7: Final verification + PR + +- [x] **Step 1: Full check** — `python3 tools/test_generate_sbom.py` PASS; `ruff check tools/` clean; dry-run `python3 tools/generate_sbom.py --version 0.0.0-test --format cyclonedx --output /tmp/t.cdx.json` + spdx variant, `python3 -m json.tool` both. +- [x] **Step 2: Anchor audit** — re-grep every file:symbol cited in THREAT_MODEL.md. +- [x] **Step 3: Push branch + open PR** to `develop` on w1ne/iolinki — body: deliverables list, decisions taken as defaults (spec §8), DRAFT/legal caveat, follow-ups (master stack, udslib, tag to exercise release SBOMs). diff --git a/docs/superpowers/specs/2026-07-02-cra-security-package-design.md b/docs/superpowers/specs/2026-07-02-cra-security-package-design.md new file mode 100644 index 0000000..96992ad --- /dev/null +++ b/docs/superpowers/specs/2026-07-02-cra-security-package-design.md @@ -0,0 +1,216 @@ +# CRA Security Package — Design + +**Date:** 2026-07-02 +**Status:** Approved in principle (user: "plan and do it"); detail decisions taken as +documented defaults, to be confirmed at PR review. +**Scope:** `iolinki` device stack first. The IO-Link master stack (`iolinki-master`) +and `udslib` reuse this template once it is proven here. + +## 1. Problem + +Stack suppliers are starting to sell CRA-readiness as a differentiator. TEConcept +announced (for H2 2026) a customer package consisting of: a CRA compliance statement +mapping their stack to Regulation (EU) 2024/2847, an SBOM per release (SPDX or +CycloneDX), a STRIDE threat model aligned with the IO-Link Security Design and +Development Guideline (Order No. 10.512), and contractually agreed security updates. + +`iolinki` today has none of these: a boilerplate `SECURITY.md`, no SBOM, no threat +model, no update commitment. Independently of competition, selling commercial licenses +of the stack into the EU makes us a manufacturer of a "product with digital elements" +under the CRA: vulnerability-handling and ENISA reporting readiness applies from +September 2026, full obligations from December 2027. A protocol library is in the +CRA *default* class, so conformity is self-assessment — no notified body. + +## 2. Goals + +1. Ship the four deliverables for the `iolinki` device stack, beating the H2 2026 + timeline of competing stack vendors. +2. Make the recurring part (SBOM) fully automatic in the existing release pipeline. +3. Keep the package honest: every claim in the threat model maps to code that exists; + gaps (e.g. no BLOB Transfer & Firmware Update profile) are stated as integrator + responsibilities, not glossed over. +4. Respect the paid-only stance: credibility artifacts public, contract-grade + artifacts commercial. + +## 3. Non-goals + +- IO-Link master stack and udslib coverage (follow-up, same template). +- Implementing new stack features (e.g. BLOB FW-update profile) — the package + documents what exists. +- Legal sign-off. The CRA statement and contract clause ship marked DRAFT — PENDING + LEGAL REVIEW; they are not offered to customers before that review. +- CE marking / EU Declaration of Conformity for iolinki itself (needs the legal + review first; the statement template prepares for it). + +## 4. Placement decision (public vs commercial) + +| Artifact | Home | Rationale | +|---|---|---| +| SBOM per release (CycloneDX + SPDX) | public — release assets on w1ne/iolinki | Zero-dependency C stack; SBOM is cheap credibility and required due diligence input for every evaluator | +| `SECURITY.md` (disclosure policy, CRA vulnerability-handling readiness) | public repo root | Standard OSS location; CRA Annex I Part II expects a public disclosure channel | +| STRIDE threat model (10.512-aligned) | public — `docs/security/THREAT_MODEL.md` | The credibility play: evaluators can check rigor before contact; forces claims to stay tied to code | +| CRA overview for integrators | public — `docs/security/CRA.md` | Explains manufacturer-stays-responsible split and what the commercial package adds; the lead-generation surface | +| CRA compliance statement (Annex I mapping) | commercial — `iolinki-private/docs/cra/` | Contract-grade document, per-customer/per-release; TEConcept-equivalent deliverable | +| Security-update terms (support period, response targets) | commercial — `iolinki-private/docs/cra/` | Contract clause template attached to LICENSE.COMMERCIAL deals | + +Default support period in the terms template: **5 years** from release, matching the +CRA's "at least five years" expectation (10.512 Annex A.2 summarizes the same), +negotiable per contract. + +## 5. Deliverables in detail + +### 5.1 SBOM generator — `tools/generate_sbom.py` (public) + +- Plain-Python (stdlib only, ruff-clean per repo `pyproject.toml`), following the + pattern of `tools/generate_release_notes.py`. +- Inputs: `--version` (from the release tag), `--format cyclonedx|spdx`, `--output`. +- Emits CycloneDX 1.6 JSON and SPDX 2.3 JSON describing one root component: + the iolinki library (supplier, dual license + `GPL-3.0-only OR LicenseRef-iolinki-Commercial`, purl + `pkg:github/w1ne/iolinki@v`), with explicit zero third-party + runtime dependencies. Optional components section lists build/test-only tooling + (cmocka, CMake) marked as excluded-from-runtime so the "no dependencies" claim is + auditable rather than implied. +- Deterministic apart from the required timestamp fields (CI provides the clock). +- Tests: a Python unit test (`tests/test_generate_sbom.py` or `tools/`-adjacent, + matching repo layout) asserting required fields, valid JSON, correct version + propagation, and stable component identity. Runs in CI alongside existing checks. + +### 5.2 Release pipeline — `.github/workflows/release.yml` (public) + +- New step after tests: generate `iolinki-.cdx.json` and + `iolinki-.spdx.json`, attach both to the GitHub release assets. +- CHANGELOG gains an Added entry; from the next tag onward every release carries + its SBOMs. + +### 5.3 `SECURITY.md` upgrade (public) + +Replace the boilerplate with: +- Coordinated disclosure: GitHub private vulnerability reporting as the primary + channel (verify it is enabled on the repo; enable if not), with response targets + (acknowledge ≤ 72 h, triage verdict ≤ 14 days). +- Supported versions: latest release on `develop` free of charge; older versions + under commercial support agreements only (links the paid-only stance). +- CRA readiness note: how vulnerabilities flow into advisories and releases; the + actively-exploited-vulnerability reporting duty (ENISA early warning within 24 h, + notification within 72 h) is acknowledged as an internal process commitment. +- Pointer to `docs/security/THREAT_MODEL.md` and the commercial CRA package. +- No invented contact addresses: use GitHub private reporting + the maintainer + contact that actually exists. + +### 5.4 STRIDE threat model — `docs/security/THREAT_MODEL.md` (public) + +Structure mirrors 10.512 (paraphrased with clause citations only — the guideline's +license forbids reproducing its text/tables): + +1. **Scope and system model** — the stack as an IEC 62443-4-2 *embedded device* + component at SL-C 1; wired point-to-point protocol, no networking, no crypto in + the protocol. Module map with trust boundaries: + PHY byte input (`src/phy_*.c`) → framing + checksum (`src/frame.c`, `src/crc.c`) + → DLL state machine (`src/dll.c`) → ISDU parser (`src/isdu.c`) → Data Storage + (`src/data_storage.c`), Events (`src/events.c`), Direct Parameters + (`src/params.c`), Device Info (`src/device_info.c`) → application callbacks. +2. **Assets** — process data integrity, device parameterization, identification + data, availability of the sensor/actuator function. +3. **STRIDE analysis** — per threat category following 10.512 Section 6's threat + catalogue (spoofing of either peer, tampering/replay on the wire, information + disclosure on the wire, DoS by flooding/disconnection), stating for each: what + the *protocol* leaves open (per 10.512: physical protection is the countermeasure + at SL-C 1), what the *stack* guarantees (checksum verification, length/bounds + validation of every received frame and ISDU, state-machine legality checks, + fixed-rate processing without allocation), and what remains with the + *integrator/device maker*. +4. **IEC 62443-4-2 requirement mapping (stack view)** — the 10.512 Table 2 "yes" + rows restated as stack-relevant claims with code anchors, e.g.: + - CR 2.8/2.9/2.10 (auditable events, storage, overflow behavior) → `src/events.c` + event queue + V1.1.5 Annex D event codes. + - CR 3.1 (communication integrity) → per-protocol checksum/CRC enforcement on + receive and transmit paths. + - CR 3.4 (software/information integrity) → Data Storage image Fletcher-16 + checksum + mismatch recovery. + - CR 3.5 (input validation) → frame length/type validation, ISDU bounds checks. + - CR 3.6 (deterministic output) → DLL exposes communication-loss state to the + application for actuator fallback. + - CR 3.7 (error handling) → error paths return protocol-defined codes, no + internal state leakage. + - CR 7.3/7.4 (backup/recovery) → Data Storage parameter server (index 0x0003) + with Access-Lock integration. + - EDR 3.2/3.10/3.14 (updates, boot integrity) → **explicit gap**: the stack does + not implement the BLOB Transfer & Firmware Update profile; firmware update + authenticity and boot integrity are device-maker responsibilities. Named + integration hooks where applicable. +5. **Residual risks and integrator duties** — physical protection of cable/device + (10.512/10.502 stance), user-doc statements the device maker should carry + (verbatim-quote duty from CR 3.1 contextual mapping is *paraphrased*, with a + pointer to the guideline), secure-deployment reference to Order No. 10.502. +6. **Verification** — how each stack claim is exercised by the existing test suite + (cmocka + docker E2E), so claims stay regression-checked. + +Every claim must carry a code or test anchor. If a claim cannot be anchored, it is +moved to the gaps section. Claims are verified against the source at writing time, +not asserted from memory. + +### 5.5 CRA overview — `docs/security/CRA.md` (public) + +Short document for device makers evaluating the stack: +- You remain the manufacturer; a stack cannot discharge CRA duties (mirrors the + supplier-side framing now common in the market). +- What iolinki provides free (SBOM per release, public threat model, coordinated + disclosure) and what the commercial package adds (compliance statement mapped to + your product context, contractual update terms, support period). +- Not legal advice disclaimer. + +### 5.6 CRA compliance statement template — `iolinki-private/docs/cra/CRA_COMPLIANCE_STATEMENT.md` + +- Header block: stack release, customer, date, DRAFT — PENDING LEGAL REVIEW banner. +- Part 1: mapping of CRA Annex I Part I (product security requirements) to stack + properties — secure-by-default configuration, attack-surface minimization, input + validation, integrity protections, no known exploitable vulnerabilities at + release (CI + cppcheck/MISRA evidence), data minimization (stack processes no + personal data). +- Part 2: mapping of CRA Annex I Part II (vulnerability handling) to our process — + SBOM, disclosure policy, advisory channel, update delivery via tagged releases, + support period. +- Part 3: division of responsibility — what the device manufacturer must still do + (their risk assessment, their DoC, their CE marking, their Article 14 reporting). +- Kept in the private repo; a rendered copy is delivered per commercial contract. + +### 5.7 Security-update terms template — `iolinki-private/docs/cra/SECURITY_UPDATE_TERMS.md` + +- Support period: 5 years from release date (default; per-contract override). +- Severity-tiered response targets (aligned with SECURITY.md public targets). +- Delivery: patched tagged releases + advisory; customer notification route. +- Explicitly scoped to security fixes (keeps the paid-only feature stance intact). +- DRAFT — PENDING LEGAL REVIEW banner. + +## 6. Approach considered and rejected + +- **Syft/cyclonedx-cli generated SBOM**: scanners find nothing useful in a + zero-dependency C source tree; a first-party generator is smaller, deterministic, + and testable. Rejected external tooling. +- **Everything commercial (including threat model)**: maximizes short-term gating + but kills the credibility/marketing value; evaluators can't verify rigor before + contact. Rejected. +- **Everything public (including compliance statement)**: gives away the + contract-grade deliverable competitors charge for; conflicts with paid-only + stance. Rejected. + +## 7. Testing / verification + +- SBOM generator: unit tests in CI (structure, version propagation, both formats). +- Release workflow: dry-run the generator step locally; full verification on the + next `v*` tag. +- Threat model: every claim anchored to a file/test that exists at merge time; + anchors checked during implementation, and the Verification section names the + covering tests. +- Docs: repo quality gates (ruff for Python; markdown consistent with existing + docs style). + +## 8. Open items for the user (defaults taken, override at review) + +1. Scope pass 1 = device stack only (master + udslib follow). +2. Threat model public vs commercial → public. +3. Support period default → 5 years. +4. Both SBOM formats (CycloneDX 1.6 + SPDX 2.3) vs one → both. +5. Legal review of 5.6/5.7 before first customer delivery → required, not scheduled + here. diff --git a/tools/generate_sbom.py b/tools/generate_sbom.py new file mode 100644 index 0000000..5535c1e --- /dev/null +++ b/tools/generate_sbom.py @@ -0,0 +1,179 @@ +#!/usr/bin/env python3 +"""Generate a per-release SBOM for the iolinki device stack. + +Emits CycloneDX 1.6 JSON or SPDX 2.3 JSON describing the iolinki library: +a self-contained C stack with zero third-party runtime dependencies. Build- +and test-only tooling (CMake, cmocka) is listed explicitly with a non-runtime +scope so the zero-dependency claim is auditable rather than implied. + +Usage: + python3 tools/generate_sbom.py --version 1.1.3 --format cyclonedx \ + --output iolinki-1.1.3.cdx.json + +Set SOURCE_DATE_EPOCH for a reproducible timestamp. +""" + +import argparse +import json +import os +import sys +import uuid +from datetime import datetime, timezone + +NAME = "iolinki" +SUPPLIER = "Andrii Shylenko" +REPO_URL = "https://github.com/w1ne/iolinki" +DESCRIPTION = ( + "IO-Link device stack in portable C (IO-Link Interface and System " + "Specification V1.1.5): DLL state machine, ISDU, Data Storage, events, " + "process data; no third-party runtime dependencies." +) +LICENSE_EXPRESSION = "GPL-3.0-only OR LicenseRef-iolinki-Commercial" + +# Build/test-time tooling only; never part of the shipped library. +BUILD_TOOLS = ( + ("cmake", "build system"), + ("cmocka", "unit-test framework"), +) + + +def _purl(version): + return f"pkg:github/w1ne/iolinki@v{version}" + + +def _timestamp(): + epoch = os.environ.get("SOURCE_DATE_EPOCH") + when = ( + datetime.fromtimestamp(int(epoch), tz=timezone.utc) + if epoch + else datetime.now(tz=timezone.utc) + ) + return when.strftime("%Y-%m-%dT%H:%M:%SZ") + + +def build_cyclonedx(version): + root_ref = f"{NAME}@{version}" + tool_components = [ + { + "type": "application", + "bom-ref": f"buildtool-{name}", + "name": name, + "description": f"{role} (build/test only, not shipped)", + "scope": "excluded", + } + for name, role in BUILD_TOOLS + ] + return { + "bomFormat": "CycloneDX", + "specVersion": "1.6", + "serialNumber": f"urn:uuid:{uuid.uuid4()}", + "version": 1, + "metadata": { + "timestamp": _timestamp(), + "supplier": {"name": SUPPLIER}, + "component": { + "type": "library", + "bom-ref": root_ref, + "name": NAME, + "version": version, + "description": DESCRIPTION, + "supplier": {"name": SUPPLIER}, + "licenses": [{"expression": LICENSE_EXPRESSION}], + "purl": _purl(version), + "externalReferences": [{"type": "vcs", "url": REPO_URL}], + }, + }, + "components": tool_components, + # The root component depends on nothing: the stack is self-contained C. + "dependencies": [{"ref": root_ref, "dependsOn": []}], + } + + +def build_spdx(version): + root_id = "SPDXRef-Package-iolinki" + tool_packages = [] + tool_relationships = [] + for name, role in BUILD_TOOLS: + pkg_id = f"SPDXRef-Package-{name}" + tool_packages.append( + { + "SPDXID": pkg_id, + "name": name, + "downloadLocation": "NOASSERTION", + "filesAnalyzed": False, + "licenseConcluded": "NOASSERTION", + "licenseDeclared": "NOASSERTION", + "comment": f"{role} (build/test only, not shipped)", + } + ) + tool_relationships.append( + { + "spdxElementId": pkg_id, + "relationshipType": "BUILD_TOOL_OF", + "relatedSpdxElement": root_id, + } + ) + return { + "spdxVersion": "SPDX-2.3", + "dataLicense": "CC0-1.0", + "SPDXID": "SPDXRef-DOCUMENT", + "name": f"{NAME}-{version}", + "documentNamespace": f"{REPO_URL}/spdx/{version}-{uuid.uuid4()}", + "creationInfo": { + "created": _timestamp(), + "creators": [ + f"Person: {SUPPLIER}", + "Tool: iolinki-generate-sbom", + ], + }, + "packages": [ + { + "SPDXID": root_id, + "name": NAME, + "versionInfo": version, + "supplier": f"Person: {SUPPLIER}", + "downloadLocation": f"git+{REPO_URL}@v{version}", + "filesAnalyzed": False, + "licenseConcluded": LICENSE_EXPRESSION, + "licenseDeclared": LICENSE_EXPRESSION, + "description": DESCRIPTION, + "externalRefs": [ + { + "referenceCategory": "PACKAGE-MANAGER", + "referenceType": "purl", + "referenceLocator": _purl(version), + } + ], + }, + *tool_packages, + ], + "relationships": [ + { + "spdxElementId": "SPDXRef-DOCUMENT", + "relationshipType": "DESCRIBES", + "relatedSpdxElement": root_id, + }, + *tool_relationships, + ], + } + + +BUILDERS = {"cyclonedx": build_cyclonedx, "spdx": build_spdx} + + +def main(argv=None): + parser = argparse.ArgumentParser(description=__doc__.splitlines()[0]) + parser.add_argument("--version", required=True, help="release version, no v prefix") + parser.add_argument("--format", required=True, choices=sorted(BUILDERS)) + parser.add_argument("--output", required=True, help="output JSON path") + args = parser.parse_args(argv) + + doc = BUILDERS[args.format](args.version) + with open(args.output, "w", encoding="utf-8") as fh: + json.dump(doc, fh, indent=2, sort_keys=False) + fh.write("\n") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/test_generate_sbom.py b/tools/test_generate_sbom.py new file mode 100644 index 0000000..50055b8 --- /dev/null +++ b/tools/test_generate_sbom.py @@ -0,0 +1,102 @@ +#!/usr/bin/env python3 +"""Tests for generate_sbom.py — per-release CycloneDX/SPDX SBOM generator.""" + +import json +import os +import subprocess +import sys +import tempfile +import unittest + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) + +from generate_sbom import LICENSE_EXPRESSION, build_cyclonedx, build_spdx # noqa: E402 + +TOOL = os.path.join(os.path.dirname(os.path.abspath(__file__)), "generate_sbom.py") +VERSION = "1.1.3" + + +class TestCycloneDX(unittest.TestCase): + def setUp(self): + self.doc = build_cyclonedx(VERSION) + + def test_format_and_spec_version(self): + self.assertEqual(self.doc["bomFormat"], "CycloneDX") + self.assertEqual(self.doc["specVersion"], "1.6") + + def test_root_component_identity(self): + comp = self.doc["metadata"]["component"] + self.assertEqual(comp["name"], "iolinki") + self.assertEqual(comp["version"], VERSION) + self.assertEqual(comp["purl"], f"pkg:github/w1ne/iolinki@v{VERSION}") + self.assertEqual(comp["licenses"][0]["expression"], LICENSE_EXPRESSION) + + def test_zero_runtime_dependencies_is_explicit(self): + root_ref = self.doc["metadata"]["component"]["bom-ref"] + deps = {d["ref"]: d.get("dependsOn", []) for d in self.doc["dependencies"]} + self.assertIn(root_ref, deps) + self.assertEqual(deps[root_ref], []) + + def test_build_tools_are_excluded_scope(self): + names = {c["name"]: c for c in self.doc.get("components", [])} + for tool in ("cmake", "cmocka"): + self.assertIn(tool, names) + self.assertEqual(names[tool]["scope"], "excluded") + + +class TestSPDX(unittest.TestCase): + def setUp(self): + self.doc = build_spdx(VERSION) + + def test_document_identity(self): + self.assertEqual(self.doc["spdxVersion"], "SPDX-2.3") + self.assertEqual(self.doc["SPDXID"], "SPDXRef-DOCUMENT") + self.assertEqual(self.doc["dataLicense"], "CC0-1.0") + + def test_root_package(self): + pkgs = {p["name"]: p for p in self.doc["packages"]} + self.assertIn("iolinki", pkgs) + pkg = pkgs["iolinki"] + self.assertEqual(pkg["versionInfo"], VERSION) + self.assertEqual(pkg["licenseDeclared"], LICENSE_EXPRESSION) + purls = [ + r["referenceLocator"] + for r in pkg.get("externalRefs", []) + if r["referenceType"] == "purl" + ] + self.assertEqual(purls, [f"pkg:github/w1ne/iolinki@v{VERSION}"]) + + def test_describes_relationship(self): + rels = [ + r + for r in self.doc["relationships"] + if r["relationshipType"] == "DESCRIBES" + and r["spdxElementId"] == "SPDXRef-DOCUMENT" + ] + self.assertEqual(len(rels), 1) + + +class TestCLI(unittest.TestCase): + def run_tool(self, *args): + return subprocess.run( + [sys.executable, TOOL, *args], capture_output=True, text=True + ) + + def test_writes_valid_json_for_both_formats(self): + for fmt in ("cyclonedx", "spdx"): + with tempfile.TemporaryDirectory() as tmp: + out = os.path.join(tmp, "sbom.json") + res = self.run_tool( + "--version", VERSION, "--format", fmt, "--output", out + ) + self.assertEqual(res.returncode, 0, res.stderr) + with open(out, encoding="utf-8") as fh: + json.load(fh) + + def test_unknown_format_fails(self): + res = self.run_tool("--version", VERSION, "--format", "xml", "--output", "x") + self.assertNotEqual(res.returncode, 0) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/virtual_master/nucleo_master.py b/tools/virtual_master/nucleo_master.py index 755bf26..e519795 100644 --- a/tools/virtual_master/nucleo_master.py +++ b/tools/virtual_master/nucleo_master.py @@ -40,7 +40,7 @@ def main(): ) args = parser.parse_args() - print(f"=== IO-Link Master - Real Hardware Test ===") + print("=== IO-Link Master - Real Hardware Test ===") print(f"Connecting to {args.port} at {args.baud} baud...") try: diff --git a/zephyr/CMakeLists.txt b/zephyr/CMakeLists.txt index e5a3dcc..9e0337e 100644 --- a/zephyr/CMakeLists.txt +++ b/zephyr/CMakeLists.txt @@ -12,7 +12,7 @@ zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../include) zephyr_include_directories(${CMAKE_CURRENT_SOURCE_DIR}/../src) zephyr_library_sources( - ../src/iolink_core.c + ../src/device.c ../src/crc.c ../src/frame.c ../src/dll.c