diff --git a/CHANGELOG.rst b/CHANGELOG.rst index af370e343..759ff840f 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -3,10 +3,19 @@ Release 0.15.0 (unreleased) * Implement C-043: add ``pip-audit`` OSV gate to the release workflow; publishing is blocked if any known vulnerability is found in dfetch's runtime dependencies -* Add CRA Compliance Track B: OSCAL 1.1.2 Component Definition mapping all CRA Annex I Part I +* Add CRA Compliance Track B: OSCAL 1.2.2 Component Definition mapping all CRA Annex I Part I essential requirements (ECR-a–m) through prEN 40000-1-4 Security Objectives to dfetch controls; covers Part II via prEN 40000-1-3; introduces controls C-043 (release-gate CVE check), C-044 (data minimisation policy), and C-046 (exploit mitigation inventory) +* Upgrade OSCAL artifacts to 1.2.2: both the prEN 40000-1-4 catalog and the Component Definition + now declare ``oscal-version: 1.2.2``; the catalog gains ``parties``, ``roles``, and + ``responsible-parties`` in its metadata; the Component Definition adds a ``purpose`` field on + the component, a ``supplier`` party, ``document-ids`` for stable cross-referencing, + ``responsible-roles``, and ``evidence`` links on every implemented-requirement pointing to the + concrete code or CI workflow file that implements the control — turning the compliance mapping + into a machine-verifiable security-as-code artifact; the back-matter is enriched with references + to the OpenSSF Scorecard, SLSA Source Provenance, Sigstore attestation, in-toto test results, + CodeQL, and dependency-review workflows Release 0.14.0 (released 2026-06-14) ==================================== diff --git a/doc/explanation/compliance_track.rst b/doc/explanation/compliance_track.rst index 90a453c22..ec58d4b03 100644 --- a/doc/explanation/compliance_track.rst +++ b/doc/explanation/compliance_track.rst @@ -1,3 +1,6 @@ +.. This file is auto-generated by ``python -m security.compliance --rst``. + Do not edit manually — edit security/compliance_data.py instead. + .. _compliance_track: CRA Compliance @@ -6,11 +9,11 @@ CRA Compliance .. note:: dfetch is **non-commercial open-source software** and falls outside the - mandatory scope of Regulation (EU) 2024/2847 (CRA): it is not placed on the - market in the context of a commercial activity (CRA Article 3(1); Recital 18 - provides interpretive context). This document is produced voluntarily to support - downstream integrators who must account for open-source components in - their own Article 13 conformity assessments. + mandatory scope of Regulation (EU) 2024/2847 (CRA): it is not placed on + the market in the context of a commercial activity (CRA Article 3(1); + Recital 18 provides interpretive context). This document is produced + voluntarily to support downstream integrators who must account for + open-source components in their own Article 13 conformity assessments. This page provides three-tier traceability from the CRA Annex I essential requirements through the prEN 40000-1-4 Security Objectives to the @@ -22,10 +25,10 @@ concrete dfetch controls or documented gaps:: ↓ dfetch control (C-001 … C-046) or documented gap -Machine-readable artifacts are kept alongside the source, encoded in OSCAL 1.1.2 (pinned version; NIST released 1.2.2 in April 2026 — migration not yet performed): +Machine-readable artifacts are kept alongside the source, encoded in OSCAL 1.2.2: -- `security/cra_pren_4000014_oscal_catalog.json `_ — prEN 40000-1-4 catalog -- `security/dfetch.component-definition.json `_ — dfetch Component Definition +- `security/cra_pren_4000014_oscal_catalog.json `_ — prEN 40000-1-4 catalog (includes parties, roles, and responsible-parties) +- `security/dfetch.component-definition.json `_ — dfetch Component Definition (includes supplier party, purpose, evidence links per implemented-requirement) The full list of all controls is available on the :doc:`control_register` page. @@ -66,14 +69,10 @@ Classification Decision ---- CRA Annex V — Technical Documentation Map ------------------------------------------- +----------------------------------------- -Annex V of Regulation (EU) 2024/2847 specifies the minimum content for -a manufacturer's technical documentation. The table below maps each -Annex V element to the corresponding dfetch artifact or section. -Because dfetch is outside mandatory CRA scope, this mapping is provided -as a convenience for downstream integrators conducting their own -Article 13 conformity assessments. +Annex V of Regulation (EU) 2024/2847 specifies the minimum content for a manufacturer's technical documentation. The table below maps each Annex V element to the corresponding dfetch artifact or section. +Because dfetch is outside mandatory CRA scope, this mapping is provided as a convenience for downstream integrators conducting their own Article 13 conformity assessments. .. list-table:: :header-rows: 1 @@ -81,41 +80,18 @@ Article 13 conformity assessments. * - Annex V element - dfetch artifact / section - - * - **1. General description** — intended purpose, product name and version, - manufacturer address - - :doc:`security` § *Product and manufacturer identification*; - :doc:`../reference/manifest` (manifest schema and version field) - - * - **2. Design and development** — software architecture; how components - build on or feed into each other - - :doc:`../explanation/architecture` (layer diagram and module overview); - :doc:`security_pipeline` § *Threat model pipeline* (security-relevant - component relationships) - - * - **3. Production and monitoring** — build pipeline, dependency - management, CI/CD monitoring - - :doc:`security_pipeline` § *Compliance pipeline* and *Release - attestations*; CI workflows in - `.github/workflows/ `_ - - * - **4. Cybersecurity risk assessment** (Article 13(2)) — asset - identification, threat analysis, risk treatment - - :doc:`threat_model_supply_chain` (pre-install lifecycle); - :doc:`threat_model_usage` (runtime invocation); - see also :doc:`security` § *Risk Rating Methodology* - - * - **5. Implemented security solutions and applied standards** — - list of harmonised standards applied; where not applied, - description of how each Annex I requirement is met - - This page (§§ *Applicable Standards*, *Part I*, *Part II*); - :doc:`control_register` (all 46 controls with references); - OSCAL Component Definition - `security/dfetch.component-definition.json `_ - + * - **1. General description** — intended purpose, product name and version, manufacturer address + - :doc:`security` § *Product and manufacturer identification*; :doc:`../reference/manifest` (manifest schema and version field) + * - **2. Design and development** — software architecture; how components build on or feed into each other + - :doc:`../explanation/architecture` (layer diagram and module overview); :doc:`security_pipeline` § *Threat model pipeline* (security-relevant component relationships) + * - **3. Production and monitoring** — build pipeline, dependency management, CI/CD monitoring + - :doc:`security_pipeline` § *Compliance pipeline* and *Release attestations*; CI workflows in `\.github/workflows/ `_ + * - **4. Cybersecurity risk assessment** (Article 13(2)) — asset identification, threat analysis, risk treatment + - :doc:`threat_model_supply_chain` (pre-install lifecycle); :doc:`threat_model_usage` (runtime invocation); see also :doc:`security` § *Risk Rating Methodology* + * - **5. Implemented security solutions and applied standards** — list of harmonised standards applied; where not applied, description of how each Annex I requirement is met + - This page (§§ *Applicable Standards*, *Part I*, *Part II*); :doc:`control_register` (all 46 controls with references); OSCAL Component Definition `security/dfetch.component-definition.json `_ * - **6. EU Declaration of Conformity** (Annex IV) - - Not required. dfetch is outside mandatory CRA scope (see - *Classification Decision* above). No CE marking is affixed. + - Not required. dfetch is outside mandatory CRA scope (see *Classification Decision* above). No CE marking is affixed. ---- @@ -134,7 +110,7 @@ Applicable Standards * - prEN 40000-1-2 - Cyber Resilience Principles and Secure Development Lifecycle (working title; subject to change on publication) - Yes - - Process standard covering risk-based product security across the lifecycle. The Product Security Context (§6.2) is documented in :doc:`security`. The threat models (`tm_supply_chain.py `_, `tm_usage.py `_) implement §6.3–§6.6. + - Process standard covering risk-based product security across the lifecycle. The Product Security Context (§6.2) is documented in :doc:`security`. Track A threat models (`tm_supply_chain.py `_, `tm_usage.py `_) implement §6.3–§6.6. - — * - prEN 40000-1-3 - Vulnerability Handling Requirements @@ -144,12 +120,12 @@ Applicable Standards * - prEN 40000-1-4 - Generic Security Requirements (draft, indicative publication October 2027) - Yes - - Primary standard for this document. Maps CRA Annex I Part I requirements (a)–(m) to Security Objectives (SO.\*) and Technical Controls (GEC-\*, SUM-\*, etc.). The catalog is included as `security/cra_pren_4000014_oscal_catalog.json `_. + - Primary standard for this document. Maps CRA Annex I Part I Art. 2(a)–(m) to Security Objectives (SO.*) and Technical Controls (GEC-*, SUM-*, etc.). The catalog is included as `security/cra_pren_4000014_oscal_catalog.json `_. - Standard is in draft; final clause numbering may change. * - EN 18031-1/2:2024 - Common security requirements for radio equipment (basis of prEN 40000-1-4) - Yes - - prEN 40000-1-4 builds on EN 18031. Many technical controls (GEC-\*, SUM-\*, AUM-\*, SSM-\*, SCM-\*) originate from EN 18031. dfetch's applicability is assessed at the prEN 40000-1-4 SO level. + - prEN 40000-1-4 builds on EN 18031. Many technical controls (GEC-*, SUM-*, AUM-*, SSM-*, SCM-*) originate from EN 18031. dfetch's applicability is assessed at the prEN 40000-1-4 SO level. - — * - ETSI EN 303 645 V3.1.3 - Cyber Security for Consumer Internet of Things @@ -160,7 +136,7 @@ Applicable Standards ---- Part I — Product Security Requirements (ECR-a to ECR-m) --------------------------------------------------------- +------------------------------------------------------- The table below summarises dfetch's implementation of each prEN 40000-1-4 Security Objective per CRA essential requirement. @@ -183,32 +159,32 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi - :ref:`C-001 `, :ref:`C-002 ` - Integrity hash verification (:ref:`C-005 `) is opt-in; manifest entries without an ``integrity`` field are fetched without hash verification by default - ⚠ Partial - * - + * - - SO.SecureStartupConfig - — - — - — N/A - * - + * - - SO.FactoryReset - — - — - — N/A * - **ECR-C** — Ensure that vulnerabilities can be addressed through security updates, including automatic updates enabled by default, with an opt-out mechanism, user notification, and the option to postpone updates. - SO.Updateability + - :ref:`C-010 `, :ref:`C-039 `, :ref:`C-043 ` - — - - Satisfied by pip distribution (``pip install --upgrade dfetch``); no dfetch-specific control required — see note below table. - ✓ Implemented - * - + * - - SO.AutomaticUpdates - — - — - — N/A - * - + * - - SO.UserUpdateNotification - - `dfetch/util/github_version_check.py `_ - - ``dfetch check`` suppresses the call when ``CI`` is set (`check.py `_ line 102); ``dfetch environment`` calls it unconditionally + - — + - — - ✓ Implemented - * - + * - - SO.PostponeUpdates - — - — @@ -216,34 +192,34 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi * - **ECR-D** — Ensure protection from unauthorised access by appropriate control mechanisms including authentication, identity or access management systems, and report on possible unauthorised access. - SO.AccessControl - :ref:`C-006 `, :ref:`C-036 ` - - dfetch has no native authentication or authorisation layer; access control is fully delegated to the underlying VCS server and host OS. C-006 prevents interactive credential prompts (mitigating credential interception), and C-036 strips credentials from persisted metadata — both are confidentiality controls, not access control mechanisms in the authentication/authorisation sense. + - dfetch has no native authentication or authorisation layer; access control is fully delegated to the underlying VCS server and host OS. C-006 prevents interactive credential prompts, and C-036 strips credentials from persisted metadata — both are confidentiality controls, not access-control mechanisms in the authentication/authorisation sense - ⚠ Partial - * - + * - - SO.AccessControlReport - :ref:`C-045 ` - - No persistent log of access attempts; C-045 detects and warns on plaintext transport but does not log events + - No persistent log of unauthorised access attempts - ⚠ Partial * - **ECR-E** — Protect the confidentiality of stored, transmitted or otherwise processed data by state-of-the-art mechanisms such as encryption at rest and in transit. - SO.DataStoredConfidentiality - :ref:`C-036 ` - — - ✓ Implemented - * - + * - - SO.DataProcessedConfidentiality - :ref:`C-005 `, :ref:`C-034 ` - — - ✓ Implemented - * - + * - - SO.DataTransmittedConfidentiality - :ref:`C-045 ` - C-045 warns on plaintext-scheme URLs but does not refuse to proceed; TLS/SSH confidentiality is provided by the underlying VCS client, not enforced by dfetch itself - ⚠ Partial - * - + * - - SO.ComAuth - :ref:`C-045 ` - Server authentication (TLS certificate verification, SSH host-key checking) is delegated to the OS trust store and VCS client; dfetch does not independently authenticate remote endpoints and cannot enforce authenticated channels when C-045's warning is overridden by the user - ⚠ Partial - * - + * - - SO.SecureProvisioning - :ref:`C-005 ` - — @@ -253,17 +229,17 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi - :ref:`C-005 ` - Integrity hash opt-in only; not enforced by default for git/svn - ⚠ Partial - * - + * - - SO.DataProcessedIntegrity - :ref:`C-005 `, :ref:`C-034 ` - — - ✓ Implemented - * - + * - - SO.DataTransmittedIntegrity - :ref:`C-005 ` - C-005 provides end-to-end hash verification for archive sources only (opt-in); git and svn sources rely solely on VCS object integrity (SHA-1/SHA-256 object model) and TLS/SSH channel integrity — no dfetch-level hash verification - ⚠ Partial - * - + * - - SO.IntegrityReport - :ref:`C-045 ` - No persistent integrity-violation log @@ -278,7 +254,7 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi - — - — - — N/A - * - + * - - SO.IncidentResilience - :ref:`C-002 `, :ref:`C-007 ` - No timeout on VCS operations (potential resource exhaustion) @@ -288,12 +264,12 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi - :ref:`C-001 `, :ref:`C-007 ` - Archive HTTP operations time out at 15 s (reachability) and 60 s (download) via ``archive.py``; git and svn subprocess calls have no timeout and can stall indefinitely - ⚠ Partial - * - + * - - SO.PreventAttackPropagation - :ref:`C-001 `, :ref:`C-008 ` - — - ✓ Implemented - * - + * - - SO.MonitorExternalImpact - — - — @@ -311,19 +287,19 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi * - **ECR-L** — Provide security related information by recording and monitoring relevant internal activity, including the access to or modification of data, services or functions, with an opt-out mechanism for the user. - SO.LogSecurityRelevantActivities - — - - No persistent structured security event log (LGM-1/2/3/4 gap). dfetch prints operational output to stderr but does not retain it, does not record which credentials were used, which files were modified, or when remote access occurred. C-036 ensures credentials are excluded from the operational output but is not a logging control. + - No persistent structured security event log (LGM-1/2/3/4 gap). dfetch prints operational output to stderr but does not retain it, does not record which credentials were used, which files were modified, or when remote access occurred. C-036 ensures credentials are excluded from operational output but is not a logging control - ⚠ Partial - * - + * - - SO.MonitorSecurityRelevantActivities - :ref:`C-045 ` - — - ⚠ Partial - * - + * - - SO.OptionDisableDataLogging - — - — - — N/A - * - + * - - SO.OptionDisableDataMonitoring - — - — @@ -331,54 +307,38 @@ The table below summarises dfetch's implementation of each prEN 40000-1-4 Securi * - **ECR-M** — Provide the possibility for users to securely and easily remove on a permanent basis all data and settings and, where such data can be transferred to other products or systems, ensure that this is done in a secure manner. - SO.SecureDataDeletion - — - - No dfetch-specific control required — satisfied by design (no personal data stored); standard OS file deletion suffices — see note below table. + - — - ✓ Implemented - * - + * - - SO.DataTransmittedConfidentiality - — - — - — N/A - * - + * - - SO.DataTransmittedIntegrity - — - — - — N/A - * - + * - - SO.ComAuth - — - — - — N/A -.. rubric:: Notes on "Implemented" rows with no control listed - -**ECR-C SO.Updateability** — SUM-1/SUM-2 require the manufacturer to make security -updates available through a secure channel. dfetch publishes every release to PyPI -(TLS-protected, OIDC-authenticated via :ref:`C-010 `) and GitHub Releases -(with release attestations per :ref:`C-039 `). The CVE gate (:ref:`C-043 `) -blocks release if known vulnerabilities are present in runtime dependencies. Providing -the update *mechanism* is the manufacturer's obligation under SUM-1/SUM-2; delivery -to the end user is the responsibility of the user's package manager. - -**ECR-C SO.UserUpdateNotification** — ``dfetch check`` and ``dfetch environment`` -both call ``newer_version_available()`` (``dfetch/util/github_version_check.py``), -which polls the GitHub releases API and prints a notice if a newer dfetch release -exists. ``dfetch check`` suppresses the call when the ``CI`` environment variable -is set (``check.py`` line 102: ``if not os.environ.get("CI")``); ``dfetch environment`` -does not apply this guard and always performs the check. - -**ECR-M SO.SecureDataDeletion** — No dfetch-specific control is needed. DLM-1 is -satisfied by design: dfetch stores no personal data, credentials, or cryptographic -keying material on disk. The only on-disk state is ``.dfetch_data.yaml`` (non-sensitive -dependency metadata — credentials stripped by :ref:`C-036 `) and vendored -source files (third-party code). Standard OS file deletion (``rm`` / ``del``) is -sufficient to remove all dfetch data; no secure-wipe facility is warranted. +.. rubric:: Notes on "Implemented" rows + +**ECR-C SO.Updateability** — SUM-1/SUM-2 require the manufacturer to make security updates available through a secure channel. dfetch publishes every release to PyPI (TLS-protected, OIDC-authenticated via :ref:`C-010 `) and GitHub Releases (with release attestations per :ref:`C-039 `). The CVE gate (:ref:`C-043 `) blocks release if known vulnerabilities are present in runtime dependencies. Providing the update *mechanism* is the manufacturer's obligation under SUM-1/SUM-2; delivery to the end user is the responsibility of the user's package manager. + +**ECR-C SO.UserUpdateNotification** — ``dfetch check`` and ``dfetch environment`` both call ``newer_version_available()`` (``dfetch/util/github_version_check.py``), which polls the GitHub releases API and prints a notice if a newer dfetch release exists. ``dfetch check`` suppresses the call when the ``CI`` environment variable is set (``check.py`` line 102: ``if not os.environ.get("CI")``); ``dfetch environment`` does not apply this guard and always performs the check. + +**ECR-M SO.SecureDataDeletion** — No dfetch-specific control is needed. DLM-1 is satisfied by design: dfetch stores no personal data, credentials, or cryptographic keying material on disk. The only on-disk state is ``.dfetch_data.yaml`` (non-sensitive dependency metadata — credentials stripped by :ref:`C-036 `) and vendored source files (third-party code). Standard OS file deletion (``rm`` / ``del``) is sufficient to remove all dfetch data; no secure-wipe facility is warranted. ---- Part II — Vulnerability Handling (prEN 40000-1-3) ------------------------------------------------- -Part II requirements are addressed via prEN 40000-1-3. Part II §4 (active vulnerability reporting to national CSIRTs and ENISA) is not applicable: this obligation falls on commercial manufacturers placing products on the market, not on non-commercial open-source software outside mandatory CRA scope. +Part II requirements are addressed via prEN 40000-1-3. pii-04 is not applicable under Recital 18. .. list-table:: :header-rows: 1 @@ -397,7 +357,7 @@ Part II requirements are addressed via prEN 40000-1-3. Part II §4 (active vulne * - Part II §2 - Address vulnerabilities without delay; provide free security updates. - :ref:`C-015 `, :ref:`C-016 `, `SECURITY.md `_ - - No LTS backport policy (latest release only — documented in `SECURITY.md `_) + - No LTS backport policy (latest release only — documented in SECURITY.md) - ⚠ Partial * - Part II §3 - Apply effective coordinated vulnerability disclosure (CVD) policy. @@ -428,9 +388,9 @@ Part II requirements are addressed via prEN 40000-1-3. Part II §4 (active vulne ---- Gap Analysis — Compliance-Only Controls ----------------------------------------- +--------------------------------------- -Three compliance-only controls address CRA requirements not independently covered by the risk models. +3 compliance-only controls address CRA requirements not independently covered by the Track A risk models. **:ref:`C-043 ` — Release-gate CVE check (ECR-a, SO.VulnerabilityManagementProcess → GEC-1)** @@ -446,22 +406,24 @@ prEN 40000-1-4 ECR-k requires documenting applicable exploit mitigation techniqu - **ASLR / DEP / stack canaries**: provided by CPython and the OS; not in dfetch's control but inherited. - **No eval/exec of remote content**: dfetch never evaluates fetched content as code. -- **Constant-time comparison** (:ref:`C-005 `): HMAC-based integrity hash uses ``hmac.compare_digest``. -- **No shell injection** (:ref:`C-007 `): all subprocess calls use ``shell=False``. -- **Input validation** (:ref:`C-008 `): URL scheme, path, and revision inputs are validated. -- **Static analysis** (:ref:`C-015 `, :ref:`C-017 `): CodeQL and bandit gate every commit. -- CFI, sandboxing, and signed-execution policies are not applicable to a pure-Python tool. +- **Constant-time comparison** (:ref:`C-005 `): HMAC-based integrity hash uses ``hmac.compare_digest``. +- **No shell injection** (:ref:`C-007 `): all subprocess calls use ``shell=False``. +- **Input validation** (:ref:`C-008 `): URL scheme, path, and revision inputs are validated. +- **Static analysis** (:ref:`C-015 `, :ref:`C-017 `): CodeQL and bandit gate every commit. +- CFI, sandboxing, and signed-execution policies are not applicable to a pure-Python tool. ---- OSCAL Artifacts --------------- -The OSCAL 1.1.2 Component Definition references the catalog file and can be +The OSCAL 1.2.2 Component Definition references the catalog file and can be regenerated with: .. code-block:: bash python -m security.compliance \\ --component security/dfetch.component-definition.json \\ + --version 0.15.0 \\ --rst > doc/explanation/compliance_track.rst + diff --git a/doc/explanation/control_register.rst b/doc/explanation/control_register.rst index 67ce020d3..9594c28e3 100644 --- a/doc/explanation/control_register.rst +++ b/doc/explanation/control_register.rst @@ -1,11 +1,12 @@ +.. This file is auto-generated by ``python -m security.compliance --control-register``. + Do not edit manually — edit security/compliance_data.py or the threat model files instead. + .. _control_register: Control Register ================ -All controls implemented by dfetch, sorted by ID. Risk-driven controls emerge -from the :doc:`threat models `; compliance-only controls address CRA -requirements not independently surfaced by the risk analysis. +All controls implemented by dfetch, sorted by ID. Risk-driven controls emerge from the :doc:`threat models `; compliance-only controls address CRA requirements not independently surfaced by the risk analysis. .. list-table:: :header-rows: 1 @@ -69,7 +70,7 @@ requirements not independently surfaced by the risk analysis. C-009 - Actions commit-SHA pinning - Risk-driven - - `.github/workflows/ `_ + - `.github/workflows/*.yml `_ * - .. _c-010: C-010 @@ -81,19 +82,19 @@ requirements not independently surfaced by the risk analysis. C-011 - Minimal workflow permissions - Risk-driven - - `.github/workflows/ `_ + - `.github/workflows/*.yml `_ * - .. _c-012: C-012 - persist-credentials: false - Risk-driven - - `.github/workflows/ `_ + - `.github/workflows/*.yml `_ * - .. _c-013: C-013 - Harden-runner (egress block) - Risk-driven - - `.github/workflows/ `_ + - `.github/workflows/*.yml `_ * - .. _c-015: C-015 @@ -221,3 +222,4 @@ requirements not independently surfaced by the risk analysis. - Exploit mitigation inventory - Compliance-only - :doc:`compliance_track` + diff --git a/doc/explanation/security_pipeline.rst b/doc/explanation/security_pipeline.rst index ec0e66ed7..8d671bcba 100644 --- a/doc/explanation/security_pipeline.rst +++ b/doc/explanation/security_pipeline.rst @@ -38,17 +38,19 @@ to produce the two RST threat-model pages (:doc:`threat_model_supply_chain` and sequence diagram, and tables for assets, threats, and controls. **Compliance pipeline** — -`security/compliance_data.py `_ -defines the 46 dfetch controls and their mapping to CRA essential requirements +`security/tm_controls_data.py `_ +defines all dfetch controls (Track A: risk-driven; Track B: compliance-only controls +in ``compliance_data.py``) and their mapping to CRA essential requirements and prEN 40000-1-4 security objectives. `security/compliance.py `_ reads those definitions together with the static OSCAL catalog and generates -:doc:`compliance_track` (human-readable RST mapping tables) and +:doc:`compliance_track` (human-readable RST mapping tables), +:doc:`control_register` (the full control register with GitHub references), and `security/dfetch.component-definition.json `_ -(machine-readable OSCAL 1.1.2 Component Definition; 1.1.2 is the pinned version — -NIST released 1.2.2 in April 2026, migration not yet performed). The -:doc:`control_register` page is maintained manually and references controls -defined in ``compliance_data.py``. +(machine-readable OSCAL 1.2.2 Component Definition). The Component Definition +includes the supplier party, component purpose, and ``evidence`` links on each +implemented-requirement pointing to the concrete code or CI file that implements +the control — making the mapping machine-verifiable. **Release attestations** — GitHub Actions generates five cryptographic attestation types *about dfetch itself* during every release, signed by Sigstore and verifiable @@ -90,16 +92,17 @@ of their choice: - CRA Annex I → prEN 40000-1-4 SO.* → dfetch control traceability tables * - :doc:`control_register` - - RST (maintained) - - All 46 dfetch controls (C-001 to C-046) with references and status + - RST (generated) + - All dfetch controls with type, references, and status * - `security/dfetch.component-definition.json `_ - - OSCAL 1.1.2 JSON (pinned) - - Machine-readable Component Definition; maps dfetch controls to CRA ECRs + - OSCAL 1.2.2 JSON (generated) + - Machine-readable Component Definition; maps dfetch controls to CRA ECRs; + includes supplier party, component purpose, and evidence links to code * - `security/cra_pren_4000014_oscal_catalog.json `_ - - OSCAL 1.1.2 JSON (pinned) - - Static prEN 40000-1-4 catalog (input to compliance pipeline, not generated) + - OSCAL 1.2.2 JSON (static) + - Static prEN 40000-1-4 catalog; includes parties, roles, and responsible-parties * - :ref:`Release attestations ` - Sigstore-signed (GitHub Actions) diff --git a/security/compliance.py b/security/compliance.py index 4b55a762e..1191dd785 100644 --- a/security/compliance.py +++ b/security/compliance.py @@ -1,6 +1,6 @@ """CRA Compliance Track B for dfetch. -Produces an OSCAL 1.1.2 Component Definition and a human-readable RST document +Produces an OSCAL 1.2.2 Component Definition and a human-readable RST document that map the CRA Annex I essential requirements through prEN 40000-1-4 Security Objectives to dfetch's implemented controls. @@ -15,7 +15,6 @@ """ import argparse -import importlib import json import os import re @@ -25,14 +24,15 @@ from typing import Any from security.compliance_data import ( + ANNEX_V_MAP, CLASSIFICATION_DECISION, PART_II_REQUIREMENTS, SO_IMPLEMENTATIONS, STANDARDS, TRACK_B_CONTROLS, - Control, SOImplementation, ) +from security.tm_controls_data import SC_CONTROLS, USAGE_CONTROLS, Control CATALOG_PATH = os.path.join( os.path.dirname(__file__), "cra_pren_4000014_oscal_catalog.json" @@ -62,32 +62,14 @@ def _so_title(so_id: str) -> str: return "SO." + "".join(p.capitalize() for p in parts) -def _load_track_a_controls(track_b_only: bool = False) -> list[Control]: - """Load Track A controls from threat models if pytm is available. +_GITHUB_BASE = "https://github.com/dfetch-org/dfetch" - Raises RuntimeError when pytm is unavailable and track_b_only is False, - so callers must explicitly opt in to Track-B-only degraded mode. - """ - try: - tm_sc = importlib.import_module("security.tm_supply_chain") - tm_u = importlib.import_module("security.tm_usage") - except ModuleNotFoundError as exc: - if not track_b_only: - raise RuntimeError( - "Track A controls unavailable (pytm not installed). " - "Pass --track-b-only to generate a Track B only artifact." - ) from exc - print( - "Note: pytm not available — Track A controls omitted (--track-b-only).", - file=sys.stderr, - ) + +def _load_track_a_controls(track_b_only: bool = False) -> list[Control]: + """Return all Track A controls, or an empty list when track_b_only is set.""" + if track_b_only: return [] - sc_controls: list[Any] = getattr(tm_sc, "CONTROLS", []) - u_controls: list[Any] = getattr(tm_u, "CONTROLS", []) - return [ - Control(id=c.id, name=c.name, description=c.description, reference=c.reference) - for c in sc_controls + u_controls - ] + return list(SC_CONTROLS) + list(USAGE_CONTROLS) def get_all_controls(track_b_only: bool = False) -> list[Control]: @@ -111,13 +93,25 @@ def load_catalog() -> Any: # ── OSCAL Component Definition builder ──────────────────────────────────────── +_PARTY_UUID_DFETCH_ORG = _u("party-dfetch-org") + + def _build_metadata(version: str) -> dict[str, Any]: - """Return the OSCAL metadata block.""" + """Return the OSCAL 1.2.2 metadata block with parties and roles.""" return { "title": "dfetch CRA Compliance Component Definition", "last-modified": f"{date.today().isoformat()}T00:00:00Z", "version": version, - "oscal-version": "1.1.2", + "oscal-version": "1.2.2", + "document-ids": [ + { + "scheme": "uri", + "identifier": ( + "https://github.com/dfetch-org/dfetch/blob/main/" + "security/dfetch.component-definition.json" + ), + } + ], "props": [ { "name": "cra-class", @@ -136,6 +130,49 @@ def _build_metadata(version: str) -> dict[str, Any]: "prEN 40000-1-4 (draft, indicative publication October 2027)" ), }, + { + "name": "openssf-scorecard", + "value": "https://api.securityscorecards.dev/projects/github.com/dfetch-org/dfetch", + }, + ], + "roles": [ + { + "id": "supplier", + "title": "Software Supplier", + "description": "Organisation that develops and distributes dfetch.", + }, + { + "id": "maintainer", + "title": "Maintainer", + "description": "Primary maintainer of the dfetch project and this compliance document.", + }, + ], + "parties": [ + { + "uuid": _PARTY_UUID_DFETCH_ORG, + "type": "organization", + "name": "dfetch-org", + "links": [ + { + "href": "https://github.com/dfetch-org", + "rel": "homepage", + } + ], + "remarks": ( + "Non-commercial open-source organisation. " + "dfetch is not placed on the market in the context of a commercial activity." + ), + } + ], + "responsible-parties": [ + { + "role-id": "supplier", + "party-uuids": [_PARTY_UUID_DFETCH_ORG], + }, + { + "role-id": "maintainer", + "party-uuids": [_PARTY_UUID_DFETCH_ORG], + }, ], "remarks": ( "Produced voluntarily under Article 13(5) to support downstream integrators. " @@ -173,10 +210,19 @@ def _build_so_description(so_impl: SOImplementation) -> str: return " ".join(parts) if parts else _so_title(so_impl.so_id) +def _build_evidence_links(so_impl: SOImplementation) -> list[dict[str, str]]: + """Return OSCAL links pointing to code or CI evidence for one SO.""" + return [ + {"href": href, "rel": "evidence", "text": text} + for href, text in so_impl.evidence_hrefs + ] + + def _build_implemented_requirements() -> list[dict[str, Any]]: """Return one implemented-requirement dict per SO.""" - return [ - { + reqs = [] + for so_impl in SO_IMPLEMENTATIONS: + req: dict[str, Any] = { "uuid": _u(f"req-{so_impl.so_id}"), "control-id": so_impl.so_id, "props": _build_so_props(so_impl), @@ -188,8 +234,11 @@ def _build_implemented_requirements() -> list[dict[str, Any]]: } ], } - for so_impl in SO_IMPLEMENTATIONS - ] + links = _build_evidence_links(so_impl) + if links: + req["links"] = links + reqs.append(req) + return reqs def _build_component(version: str) -> dict[str, Any]: @@ -202,7 +251,18 @@ def _build_component(version: str) -> dict[str, Any]: "Python CLI tool for vendoring source-code dependencies from Git, SVN, " "or archive files into a project as plain files." ), - "props": [{"name": "software-version", "value": version}], + "purpose": ( + "Vendor source-code dependencies from Git, SVN, or archive files into a " + "project as plain files, enabling reproducible builds without submodules or " + "externals. Supports integrity hashing, plaintext-transport detection, and " + "credential redaction to reduce supply-chain risk." + ), + "props": [ + {"name": "software-version", "value": version}, + {"name": "asset-type", "value": "software.application"}, + {"name": "vendor-name", "value": "dfetch-org"}, + {"name": "license", "value": "MIT"}, + ], "links": [ {"href": "https://github.com/dfetch-org/dfetch", "rel": "homepage"}, {"href": "https://pypi.org/project/dfetch/", "rel": "distribution"}, @@ -211,6 +271,17 @@ def _build_component(version: str) -> dict[str, Any]: "rel": "reference", "text": "prEN 40000-1-4 OSCAL Catalog", }, + { + "href": "SECURITY.md", + "rel": "reference", + "text": "Vulnerability Disclosure Policy", + }, + ], + "responsible-roles": [ + { + "role-id": "supplier", + "party-uuids": [_PARTY_UUID_DFETCH_ORG], + } ], "control-implementations": [ { @@ -251,7 +322,7 @@ def _build_back_matter() -> dict[str, Any]: }, { "uuid": _u("res-security-md"), - "title": "dfetch Security Policy (SECURITY.md)", + "title": "dfetch Vulnerability Disclosure Policy (SECURITY.md)", "rlinks": [ { "href": ( @@ -260,12 +331,122 @@ def _build_back_matter() -> dict[str, Any]: } ], }, + { + "uuid": _u("res-scorecard"), + "title": "OpenSSF Scorecard — dfetch supply-chain security score", + "props": [ + {"name": "type", "value": "assessment-report"}, + {"name": "tool", "value": "OpenSSF Scorecard"}, + ], + "rlinks": [ + { + "href": ( + "https://api.securityscorecards.dev/projects/" + "github.com/dfetch-org/dfetch" + ) + } + ], + }, + { + "uuid": _u("res-scorecard-workflow"), + "title": "OpenSSF Scorecard CI workflow", + "rlinks": [ + { + "href": ".github/workflows/scorecard.yml", + } + ], + }, + { + "uuid": _u("res-slsa-provenance"), + "title": "SLSA Source Provenance Attestation workflow", + "props": [ + {"name": "type", "value": "attestation"}, + {"name": "framework", "value": "SLSA"}, + ], + "rlinks": [ + { + "href": ".github/workflows/source-provenance.yml", + } + ], + }, + { + "uuid": _u("res-sigstore-attestation"), + "title": "Sigstore build provenance and SBOM attestation workflow", + "props": [ + {"name": "type", "value": "attestation"}, + {"name": "framework", "value": "Sigstore"}, + ], + "rlinks": [ + { + "href": ".github/workflows/python-publish.yml", + } + ], + }, + { + "uuid": _u("res-in-toto-attestation"), + "title": "in-toto test-results attestation workflow", + "props": [ + {"name": "type", "value": "attestation"}, + {"name": "framework", "value": "in-toto"}, + ], + "rlinks": [ + { + "href": ".github/workflows/test.yml", + } + ], + }, + { + "uuid": _u("res-codeql"), + "title": "CodeQL static analysis workflow (C-015)", + "props": [ + {"name": "type", "value": "tool"}, + {"name": "tool", "value": "CodeQL"}, + ], + "rlinks": [ + { + "href": ".github/workflows/codeql-analysis.yml", + } + ], + }, + { + "uuid": _u("res-dep-review"), + "title": "Dependency review workflow (C-016)", + "props": [ + {"name": "type", "value": "tool"}, + {"name": "tool", "value": "dependency-review"}, + ], + "rlinks": [ + { + "href": ".github/workflows/dependency-review.yml", + } + ], + }, + { + "uuid": _u("res-releases"), + "title": "dfetch GitHub Releases (SBOM and attestation download)", + "rlinks": [ + { + "href": "https://github.com/dfetch-org/dfetch/releases", + } + ], + }, + { + "uuid": _u("res-verify-integrity"), + "title": "How to verify dfetch integrity and attestations", + "rlinks": [ + { + "href": ( + "https://dfetch.readthedocs.io/en/latest/howto/verify-integrity.html" + ) + } + ], + }, ] } def build_oscal_component_definition(version: str = "0.14.0") -> dict[str, Any]: - """Return a complete OSCAL 1.1.2 Component Definition for dfetch.""" + """Return a complete OSCAL 1.2.2 Component Definition for dfetch.""" return { "component-definition": { "uuid": _u("component-definition"), @@ -301,13 +482,66 @@ def _rst_list_table( lines.append("") lines.append(" * - " + "\n - ".join(_rst_escape_star(h) for h in headers)) for row in rows: - lines.append( - " * - " + "\n - ".join(_rst_escape_star(str(c)) for c in row) - ) + lines.append(" * - " + "\n - ".join(str(c) for c in row)) lines.append("") return "\n".join(lines) +def _rst_ctrl_ref(ctrl_id: str) -> str: + """Return an RST cross-reference for a control ID.""" + return f":ref:`{ctrl_id} <{ctrl_id.lower()}>`" + + +def _format_ref_as_rst(ref: str) -> str: + """Convert a control reference string to RST markup. + + Handles: empty/dash, doc/ paths, glob patterns, directories, comma-separated, + and regular file paths — all converted to appropriate GitHub links or RST refs. + """ + ref = ref.strip() + if not ref or ref == "—": + return "—" + # Already RST markup + if ref.startswith(":doc:") or ref.startswith(":ref:"): + return ref + # Handle parenthetical suffix like "path (note about it)" + paren_suffix = "" + if " (" in ref and ref.endswith(")"): + paren_idx = ref.rfind(" (") + paren_suffix = " " + ref[paren_idx + 1 :] + ref = ref[:paren_idx] + # doc/ prefix → RST :doc: reference (relative to doc/explanation/) + if ref.startswith("doc/howto/"): + slug = ref.removeprefix("doc/howto/").removesuffix(".rst") + return f":doc:`../howto/{slug}`" + if ref.startswith("doc/explanation/"): + slug = ref.removeprefix("doc/explanation/").removesuffix(".rst") + return f":doc:`{slug}`" + # Comma-separated → format each individually + if "," in ref: + parts = [_format_single_ref(p.strip()) for p in ref.split(",")] + return ",\n ".join(parts) + return _format_single_ref(ref) + paren_suffix + + +def _format_single_ref(ref: str) -> str: + """Format a single (non-comma-separated) file/dir reference as RST.""" + ref = ref.strip() + if not ref: + return "—" + # Glob or directory → tree link; strip any glob portion from the URL path + if ref.endswith("/") or "*" in ref: + if "*" in ref: + dir_part = ref[: ref.index("*")].rstrip("/") + else: + dir_part = ref.rstrip("/") + tree_url = f"{_GITHUB_BASE}/tree/main/{dir_part}" + return f"`{ref} <{tree_url}>`_" + # Regular file → blob link + blob_url = f"{_GITHUB_BASE}/blob/main/{ref}" + return f"`{ref} <{blob_url}>`_" + + def _render_classification() -> None: """Print the Classification Decision section.""" print(_rst_title("Classification Decision", "-")) @@ -362,7 +596,7 @@ def _part_i_rows(ecr_map: dict[str, str]) -> list[list[str]]: if so.ecr_id != current_ecr: current_ecr = so.ecr_id ecr_cell = f"**{so.ecr_id.upper()}** — {ecr_map.get(so.ecr_id, '')}" - ctrls = ", ".join(so.controls) if so.controls else "—" + ctrls = ", ".join(_rst_ctrl_ref(c) for c in so.controls) if so.controls else "—" gaps = "; ".join(so.gaps) if so.gaps else "—" rows.append( [ @@ -399,11 +633,19 @@ def _render_part_ii_table() -> None: "Part II requirements are addressed via prEN 40000-1-3. " "pii-04 is not applicable under Recital 18.\n" ) + + def _fmt_ctrl(c: str) -> str: + if re.match(r"^C-\d+$", c): + return _rst_ctrl_ref(c) + if c == "SECURITY.md": + return f"`SECURITY.md <{_GITHUB_BASE}/blob/main/SECURITY.md>`_" + return c + rows = [ [ req.ref, req.text, - ", ".join(req.controls) if req.controls else "—", + ", ".join(_fmt_ctrl(c) for c in req.controls) if req.controls else "—", "; ".join(req.gaps) if req.gaps else "—", _STATUS_LABEL.get(req.status, req.status), ] @@ -422,45 +664,49 @@ def _gap_entries() -> list[tuple[str, str]]: """Return (title, body) pairs for the gap analysis section.""" return [ ( - "C-043 — Release-gate CVE check" - + " (ECR-a, SO.VulnerabilityManagementProcess → GEC-1)", + ":ref:`C-043 ` — Release-gate CVE check" + " (ECR-a, SO.VulnerabilityManagementProcess → GEC-1)", ( - "dfetch's CI detects vulnerabilities at commit time (C-015, C-016, C-017) " - "but does not gate the release publish on a CVE scan of runtime dependencies. " - "C-043 (planned) adds ``pip-audit`` or ``osv-scanner`` to the publish " - "workflow." + "dfetch's CI detects vulnerabilities at commit time " + "(:ref:`C-015 `, :ref:`C-016 `, :ref:`C-017 `). " + ":ref:`C-043 ` completes the coverage: the publish workflow runs " + "``pip-audit`` against the project's runtime dependencies via the OSV " + "database and blocks the release if any known vulnerability is found." ), ), ( - "C-044 — Data minimisation policy" - + " (ECR-g, SO.DataMinimization → DTM-1)", + ":ref:`C-044 ` — Data minimisation policy" + " (ECR-g, SO.DataMinimization → DTM-1)", ( - "dfetch processes dependency metadata only. The ``.dfetch_data.yaml`` file " - "stores: ``remote_url`` (credentials stripped by C-036), ``revision``, " - "optional ``integrity.hash``, and ``last_fetch`` timestamp. Each field is " - "functionally necessary for ``dfetch check`` and ``dfetch freeze``. " - "No personal data is collected; no telemetry is sent. " - "C-044 formalises this assertion as a documented policy." + "dfetch processes dependency metadata only. The ``.dfetch_data.yaml`` " + "file stores: ``remote_url`` (credentials stripped by " + ":ref:`C-036 `), ``revision``, optional ``integrity.hash``, " + "and ``last_fetch`` timestamp. Each field is functionally necessary " + "for ``dfetch check`` and ``dfetch freeze``. No personal data is " + "collected; no telemetry is sent. :ref:`C-044 ` formalises " + "this assertion as a documented policy." ), ), ( - "C-046 — Exploit mitigation inventory" - + " (ECR-k, SO.ReduceImpactOfIncident → GEC-11)", + ":ref:`C-046 ` — Exploit mitigation inventory" + " (ECR-k, SO.ReduceImpactOfIncident → GEC-11)", ( - "prEN 40000-1-4 ECR-k requires documenting applicable exploit mitigation " - "techniques. For dfetch (pure Python):\n\n" + "prEN 40000-1-4 ECR-k requires documenting applicable exploit " + "mitigation techniques. For dfetch (pure Python):\n\n" "- **ASLR / DEP / stack canaries**: provided by CPython and the OS; " " not in dfetch's control but inherited.\n" "- **No eval/exec of remote content**: dfetch never evaluates fetched " " content as code.\n" - "- **Constant-time comparison** (C-005): HMAC-based integrity hash uses " - " ``hmac.compare_digest``.\n" - "- **No shell injection** (C-007): all subprocess calls use ``shell=False``.\n" - "- **Input validation** (C-008): URL scheme, path, and revision inputs " - " are validated.\n" - "- **Static analysis** (C-015, C-017): CodeQL and bandit gate every commit.\n" - "- CFI, sandboxing, and signed-execution policies are not applicable to " - " a pure-Python tool." + "- **Constant-time comparison** (:ref:`C-005 `): HMAC-based " + " integrity hash uses ``hmac.compare_digest``.\n" + "- **No shell injection** (:ref:`C-007 `): all subprocess " + " calls use ``shell=False``.\n" + "- **Input validation** (:ref:`C-008 `): URL scheme, path, " + " and revision inputs are validated.\n" + "- **Static analysis** (:ref:`C-015 `, :ref:`C-017 `): " + " CodeQL and bandit gate every commit.\n" + "- CFI, sandboxing, and signed-execution policies are not applicable " + " to a pure-Python tool." ), ), ] @@ -479,46 +725,57 @@ def _render_gap_analysis() -> None: print(f"{body}\n") -def _render_control_register(track_b_only: bool = False) -> None: - """Print the final merged control register table.""" - print(_rst_title("Final Control Register", "-")) +def _render_annex_v() -> None: + """Print the CRA Annex V technical documentation map.""" + print(_rst_title("CRA Annex V — Technical Documentation Map", "-")) print( - "All controls from Track A (risk-driven) and Track B (regulatory) merged and " - "sorted. Track B controls (C-043, C-044, and C-046) are marked accordingly.\n" + "Annex V of Regulation (EU) 2024/2847 specifies the minimum content for " + "a manufacturer's technical documentation. The table below maps each " + "Annex V element to the corresponding dfetch artifact or section.\n" + "Because dfetch is outside mandatory CRA scope, this mapping is provided " + "as a convenience for downstream integrators conducting their own " + "Article 13 conformity assessments.\n" ) - track_b_ids = {c.id for c in TRACK_B_CONTROLS} - rows = [ - [ - ctrl.id, - ctrl.name, - "Track B" if ctrl.id in track_b_ids else "Track A", - ctrl.reference or "—", - ] - for ctrl in get_all_controls(track_b_only=track_b_only) - ] print( _rst_list_table( - ["ID", "Name", "Track", "Reference"], - rows, - widths=[8, 40, 10, 42], + ["Annex V element", "dfetch artifact / section"], + [[elem, artifact] for elem, artifact in ANNEX_V_MAP], + widths=[45, 55], ) ) +def _render_impl_notes() -> None: + """Print notes on 'Implemented' rows that have no control assigned.""" + noted = [so for so in SO_IMPLEMENTATIONS if so.note] + if not noted: + return + print('.. rubric:: Notes on "Implemented" rows\n') + for so in noted: + print(so.note + "\n") + + def render_rst(track_b_only: bool = False) -> None: """Print the full compliance track RST document to stdout.""" + print( + ".. This file is auto-generated by ``python -m security.compliance --rst``.\n" + " Do not edit manually — edit security/compliance_data.py instead.\n" + ) print(".. _compliance_track:\n") - print(_rst_title("CRA Compliance Track B")) + print(_rst_title("CRA Compliance")) print( ".. note::\n\n" - " dfetch is **non-commercial open-source software** and is exempt from\n" - " mandatory CRA obligations under Recital 18 of Regulation (EU) 2024/2847.\n" - " This document is produced voluntarily under Article 13(5) to support\n" - " downstream integrators who must account for open-source components in\n" - " their own conformity assessments.\n" + " dfetch is **non-commercial open-source software** and falls outside the\n" + " mandatory scope of Regulation (EU) 2024/2847 (CRA): it is not placed on\n" + " the market in the context of a commercial activity (CRA Article 3(1);\n" + " Recital 18 provides interpretive context). This document is produced\n" + " voluntarily to support downstream integrators who must account for\n" + " open-source components in their own Article 13 conformity assessments.\n" ) print( - "Three-tier traceability::\n\n" + "This page provides three-tier traceability from the CRA Annex I essential\n" + "requirements through the prEN 40000-1-4 Security Objectives to the\n" + "concrete dfetch controls or documented gaps::\n\n" " CRA Annex I Essential Requirement (ECR-a … ECR-m)\n" " ↓\n" " prEN 40000-1-4 Security Objective (SO.*)\n" @@ -526,28 +783,95 @@ def render_rst(track_b_only: bool = False) -> None: " dfetch control (C-001 … C-046) or documented gap\n" ) print( - "Machine-readable OSCAL 1.1.2 artifacts are kept alongside the source:\n\n" - "- ``security/cra_pren_4000014_oscal_catalog.json`` — prEN 40000-1-4 catalog\n" - "- ``security/dfetch.component-definition.json`` — dfetch Component Definition\n" + "Machine-readable artifacts are kept alongside the source, encoded in " + "OSCAL 1.2.2:\n\n" + "- `security/cra_pren_4000014_oscal_catalog.json" + " `_" + " — prEN 40000-1-4 catalog (includes parties, roles, and responsible-parties)\n" + "- `security/dfetch.component-definition.json" + " `_" + " — dfetch Component Definition (includes supplier party, purpose, evidence" + " links per implemented-requirement)\n" ) + print( + "The full list of all controls is available on the :doc:`control_register` page.\n" + ) + print( + ".. rubric:: Status key\n\n" + ".. list-table::\n" + " :widths: 10 90\n\n" + " * - ✓\n" + " - Implemented — control satisfies the objective fully.\n" + " * - ⚠\n" + " - Partial — control exists but a gap remains (see Gaps column).\n" + " * - N/A\n" + " - Not applicable — the objective does not apply to dfetch.\n" + ) + print("----\n") _render_classification() + print("----\n") + _render_annex_v() + print("----\n") _render_standards_table() + print("----\n") _render_part_i_table() + _render_impl_notes() + print("----\n") _render_part_ii_table() + print("----\n") _render_gap_analysis() - _render_control_register(track_b_only=track_b_only) + print("----\n") print(_rst_title("OSCAL Artifacts", "-")) print( - "The OSCAL 1.1.2 Component Definition references the catalog file and can be\n" + "The OSCAL 1.2.2 Component Definition references the catalog file and can be\n" "regenerated with:\n\n" ".. code-block:: bash\n\n" " python -m security.compliance \\\\\n" " --component security/dfetch.component-definition.json \\\\\n" - " --track-b-only \\\\\n" + " --version 0.15.0 \\\\\n" " --rst > doc/explanation/compliance_track.rst\n" ) +def render_control_register_rst(track_b_only: bool = False) -> None: + """Print the control register RST document to stdout.""" + print( + ".. This file is auto-generated by ``python -m security.compliance " + "--control-register``.\n" + " Do not edit manually — edit security/compliance_data.py or the threat " + "model files instead.\n" + ) + print(".. _control_register:\n") + print(_rst_title("Control Register")) + print( + "All controls implemented by dfetch, sorted by ID. Risk-driven controls " + "emerge from the :doc:`threat models `; compliance-only controls " + "address CRA requirements not independently surfaced by the risk analysis.\n" + ) + track_b_ids = {c.id for c in TRACK_B_CONTROLS} + controls = get_all_controls(track_b_only=track_b_only) + + # Build RST list-table rows with anchors inside the ID cell + lines = [".. list-table::", " :header-rows: 1", " :widths: 8 40 16 36", ""] + lines.append(" * - ID\n - Name\n - Type\n - Reference") + for ctrl in controls: + ctrl_type = "Compliance-only" if ctrl.id in track_b_ids else "Risk-driven" + ref_rst = _format_ref_as_rst(ctrl.reference) + anchor = ctrl.id.lower() + # RST anchor in table cell must be followed by blank line then the cell text + id_cell = f".. _{anchor}:\n\n {ctrl.id}" + lines.append( + f" * - {id_cell}\n" + f" - {ctrl.name}\n" + f" - {ctrl_type}\n" + f" - {ref_rst}" + ) + lines.append("") + print("\n".join(lines)) + + # ── Entry point ─────────────────────────────────────────────────────────────── if __name__ == "__main__": @@ -560,6 +884,11 @@ def render_rst(track_b_only: bool = False) -> None: parser.add_argument( "--rst", action="store_true", help="Print RST document to stdout" ) + parser.add_argument( + "--control-register", + action="store_true", + help="Print control register RST document to stdout", + ) parser.add_argument( "--version", default="0.14.0", help="dfetch version (default: 0.14.0)" ) @@ -581,3 +910,6 @@ def render_rst(track_b_only: bool = False) -> None: if args.rst: render_rst(track_b_only=args.track_b_only) + + if args.control_register: + render_control_register_rst(track_b_only=args.track_b_only) diff --git a/security/compliance_data.py b/security/compliance_data.py index 072677d31..75444ac67 100644 --- a/security/compliance_data.py +++ b/security/compliance_data.py @@ -7,17 +7,7 @@ from dataclasses import dataclass, field from typing import Literal - -@dataclass -class Control: - """An implemented security control (mirrors tm_elements.Control).""" - - id: str - name: str - description: str - assets: list[str] = field(default_factory=list) - threats: list[str] = field(default_factory=list) - reference: str = "" +from security.tm_controls_data import Control # noqa: F401 # re-exported @dataclass @@ -44,6 +34,8 @@ class SOImplementation: "implemented", "partially-implemented", "planned", "not-applicable" ] = "partially-implemented" description: str = "" + evidence_hrefs: list[tuple[str, str]] = field(default_factory=list) + note: str = "" @dataclass @@ -65,12 +57,17 @@ class PartIIRequirement: CLASSIFICATION_DECISION: dict[str, str] = { "Product type": "Software tool (CLI) — Python package distributed via PyPI", "CRA classification": "Non-commercial open-source software (Recital 18 exemption)", - "Legal basis": "Article 3(14), Recital 18, Article 13(5) of Regulation (EU) 2024/2847", + "Legal basis": ( + "CRA Article 3(1) (scope — dfetch is not placed on the market in the context " + "of a commercial activity); Article 3(14) (definition of open-source software " + "steward, for reference); Recital 18 (interpretive context for the treatment " + "of non-commercial FOSS)" + ), "Mandatory obligations": "None — not a commercial product; no CE marking required", "Voluntary alignment": ( - "This Track B document is produced voluntarily under Article 13(5) to support " - "downstream integrators who must account for open-source components in their " - "own CRA conformity assessments." + "This compliance document is produced voluntarily — dfetch has no legal " + "obligation under the CRA — to support downstream integrators who must account " + "for open-source components in their own Article 13 conformity assessments." ), } @@ -79,12 +76,13 @@ class PartIIRequirement: STANDARDS: list[ApplicableStandard] = [ ApplicableStandard( name="prEN 40000-1-2", - reference="Cyber Resilience Principles and Risk Management", + reference="Cyber Resilience Principles and Secure Development Lifecycle (working title; subject to change on publication)", applies=True, scope_note=( "Process standard covering risk-based product security across the lifecycle. " "The Product Security Context (§6.2) is documented in :doc:`security`. " - "Track A threat models (tm_supply_chain.py, tm_usage.py) implement §6.3–§6.6." + "Track A threat models (`tm_supply_chain.py `_, " + "`tm_usage.py `_) implement §6.3–§6.6." ), ), ApplicableStandard( @@ -93,8 +91,8 @@ class PartIIRequirement: applies=True, scope_note=( "Covers CRA Annex I Part II vulnerability handling obligations. " - "Addressed in the Part II table below via SECURITY.md, SBOM (C-022), " - "and dependency-review CI (C-016)." + "Addressed in the Part II table below via `SECURITY.md `_, SBOM (:ref:`C-022 `), " + "and dependency-review CI (:ref:`C-016 `)." ), gap_note="No formal patch SLA or LTS backport policy defined.", ), @@ -105,7 +103,7 @@ class PartIIRequirement: scope_note=( "Primary standard for this document. Maps CRA Annex I Part I Art. 2(a)–(m) " "to Security Objectives (SO.*) and Technical Controls (GEC-*, SUM-*, etc.). " - "The catalog is included as security/cra_pren_4000014_oscal_catalog.json." + "The catalog is included as `security/cra_pren_4000014_oscal_catalog.json `_." ), gap_note="Standard is in draft; final clause numbering may change.", ), @@ -171,6 +169,53 @@ class PartIIRequirement: ), ] +# ── Annex V technical documentation map ────────────────────────────────────── + +ANNEX_V_MAP: list[tuple[str, str]] = [ + ( + "**1. General description** — intended purpose, product name and version, " + "manufacturer address", + ":doc:`security` § *Product and manufacturer identification*; " + ":doc:`../reference/manifest` (manifest schema and version field)", + ), + ( + "**2. Design and development** — software architecture; how components " + "build on or feed into each other", + ":doc:`../explanation/architecture` (layer diagram and module overview); " + ":doc:`security_pipeline` § *Threat model pipeline* (security-relevant " + "component relationships)", + ), + ( + "**3. Production and monitoring** — build pipeline, dependency management, " + "CI/CD monitoring", + ":doc:`security_pipeline` § *Compliance pipeline* and *Release attestations*; " + "CI workflows in " + "`\\.github/workflows/ `_", + ), + ( + "**4. Cybersecurity risk assessment** (Article 13(2)) — asset identification, " + "threat analysis, risk treatment", + ":doc:`threat_model_supply_chain` (pre-install lifecycle); " + ":doc:`threat_model_usage` (runtime invocation); " + "see also :doc:`security` § *Risk Rating Methodology*", + ), + ( + "**5. Implemented security solutions and applied standards** — " + "list of harmonised standards applied; where not applied, description of how " + "each Annex I requirement is met", + "This page (§§ *Applicable Standards*, *Part I*, *Part II*); " + ":doc:`control_register` (all 46 controls with references); " + "OSCAL Component Definition " + "`security/dfetch.component-definition.json " + "`_", + ), + ( + "**6. EU Declaration of Conformity** (Annex IV)", + "Not required. dfetch is outside mandatory CRA scope (see " + "*Classification Decision* above). No CE marking is affixed.", + ), +] + # ── Security Objective implementations ─────────────────────────────────────── SO_IMPLEMENTATIONS: list[SOImplementation] = [ @@ -178,7 +223,7 @@ class PartIIRequirement: SOImplementation( so_id="so-vulnerability-management-process", ecr_id="ecr-a", - controls=["C-015", "C-016", "C-017", "C-022", "C-043"], + controls=["C-015", "C-016", "C-017", "C-022", "C-040", "C-043"], status="implemented", description=( "GEC-1: C-015 (CodeQL), C-016 (dependency-review), C-017 (bandit), " @@ -186,6 +231,14 @@ class PartIIRequirement: "C-043 (pip-audit OSV gate) blocks release if runtime dependencies " "carry known vulnerabilities." ), + evidence_hrefs=[ + (".github/workflows/codeql-analysis.yml", "C-015 CodeQL static analysis"), + (".github/workflows/dependency-review.yml", "C-016 Dependency review"), + ( + ".github/workflows/python-publish.yml", + "C-022 SBOM generation; C-043 pip-audit CVE gate", + ), + ], ), # ECR-b: Secure Configuration SOImplementation( @@ -197,11 +250,21 @@ class PartIIRequirement: "GEC-7 (no external sensing capabilities)", "AUM-5 (no password or authentication mechanism)", ], + gaps=[ + "Integrity hash verification (:ref:`C-005 `) is opt-in; manifest entries without an ``integrity`` field are fetched without hash verification by default" + ], status="partially-implemented", description=( "GEC-12 (no unneeded software components): C-001 enforces minimal " "runtime dependencies. dfetch does not expose network services." ), + evidence_hrefs=[ + ("dfetch/util/util.py", "C-001 Path-traversal prevention"), + ( + "dfetch/vcs/archive.py", + "C-002 Decompression-bomb protection; C-003 Symlink validation; C-004 Member-type checks", + ), + ], ), SOImplementation( so_id="so-secure-startup-config", @@ -225,12 +288,7 @@ class PartIIRequirement: SOImplementation( so_id="so-updateability", ecr_id="ecr-c", - not_applicable=[ - "No dfetch-specific control required — updateability is inherent to " - "pip distribution (``pip install --upgrade dfetch``) and GitHub Releases. " - "SUM-1/SUM-2 are satisfied by the distribution mechanism, not by a " - "runtime dfetch feature." - ], + controls=["C-010", "C-039", "C-043"], status="implemented", description=( "SUM-1/SUM-2: Updates distributed via PyPI (``pip install --upgrade dfetch``) " @@ -238,6 +296,16 @@ class PartIIRequirement: "model satisfies SUM-2. No dfetch-specific update mechanism is needed or " "implemented; the package manager is the update vehicle." ), + note=( + "**ECR-C SO.Updateability** — SUM-1/SUM-2 require the manufacturer to make " + "security updates available through a secure channel. dfetch publishes every " + "release to PyPI (TLS-protected, OIDC-authenticated via :ref:`C-010 `) " + "and GitHub Releases (with release attestations per :ref:`C-039 `). " + "The CVE gate (:ref:`C-043 `) blocks release if known vulnerabilities " + "are present in runtime dependencies. Providing the update *mechanism* is the " + "manufacturer's obligation under SUM-1/SUM-2; delivery to the end user is the " + "responsibility of the user's package manager." + ), ), SOImplementation( so_id="so-automatic-updates", @@ -251,11 +319,32 @@ class PartIIRequirement: SOImplementation( so_id="so-user-update-notification", ecr_id="ecr-c", - controls=["C-040"], + controls=[], status="implemented", description=( - "UNM-4: dfetch check and dfetch environment report when a newer dfetch " - "version is available (C-040)." + "UNM-4: ``dfetch check`` and ``dfetch environment`` both call " + "``newer_version_available()`` (``dfetch/util/github_version_check.py``), " + "which polls the GitHub releases API and prints a notice if a newer dfetch " + "release exists." + ), + evidence_hrefs=[ + ( + "dfetch/util/github_version_check.py", + "Version availability check implementation", + ), + ( + "dfetch/commands/check.py", + "Version check in dfetch check (suppressed in CI)", + ), + ("dfetch/commands/environment.py", "Version check in dfetch environment"), + ], + note=( + "**ECR-C SO.UserUpdateNotification** — ``dfetch check`` and ``dfetch environment`` " + "both call ``newer_version_available()`` (``dfetch/util/github_version_check.py``), " + "which polls the GitHub releases API and prints a notice if a newer dfetch release " + "exists. ``dfetch check`` suppresses the call when the ``CI`` environment variable " + 'is set (``check.py`` line 102: ``if not os.environ.get("CI")``); ' + "``dfetch environment`` does not apply this guard and always performs the check." ), ), SOImplementation( @@ -276,12 +365,27 @@ class PartIIRequirement: "ACM-2, AUM-2, AUM-3, AUM-4, AUM-6 " "(dfetch has no user-facing authentication or access control)" ], + gaps=[ + "dfetch has no native authentication or authorisation layer; access control is " + "fully delegated to the underlying VCS server and host OS. C-006 prevents " + "interactive credential prompts, and C-036 strips credentials from persisted " + "metadata — both are confidentiality controls, not access-control mechanisms " + "in the authentication/authorisation sense" + ], status="partially-implemented", description=( "dfetch delegates authentication to the host VCS client (git, svn) and " "the OS credential store. C-006 prevents SSH command injection; " "C-036 strips credentials from stored metadata." ), + evidence_hrefs=[ + ("dfetch/vcs/git.py", "C-006 Non-interactive git (prevents SSH injection)"), + ("dfetch/vcs/svn.py", "C-006 Non-interactive svn"), + ( + "dfetch/project/metadata.py", + "C-036 Credential redaction from stored metadata", + ), + ], ), SOImplementation( so_id="so-access-control-report", @@ -293,6 +397,10 @@ class PartIIRequirement: "GEC-13: C-045 (plaintext transport warning) alerts on unauthenticated " "connections. No persistent security event log." ), + evidence_hrefs=[ + ("dfetch/manifest/project.py", "C-045 Plaintext transport detection"), + ("dfetch/project/subproject.py", "C-045 Plaintext transport warning"), + ], ), # ECR-e: Confidentiality SOImplementation( @@ -309,6 +417,9 @@ class PartIIRequirement: "metadata_store.storesSensitiveData = False and isDestEncryptedAtRest = False " "as a conscious design choice; no encryption-at-rest is required." ), + evidence_hrefs=[ + ("dfetch/project/metadata.py", "C-036 Credential redaction before write"), + ], ), SOImplementation( so_id="so-data-processed-confidentiality", @@ -319,12 +430,23 @@ class PartIIRequirement: "GEC-8: C-005 (constant-time comparison), C-034 (temp-file cleanup) " "protect in-process data." ), + evidence_hrefs=[ + ( + "dfetch/vcs/integrity_hash.py", + "C-005 Constant-time HMAC comparison; C-034 SHA-256/384/512 allowlist", + ), + ], ), SOImplementation( so_id="so-data-transmitted-confidentiality", ecr_id="ecr-e", - controls=["C-005", "C-045"], - status="implemented", + controls=["C-045"], + gaps=[ + "C-045 warns on plaintext-scheme URLs but does not refuse to proceed; " + "TLS/SSH confidentiality is provided by the underlying VCS client, not " + "enforced by dfetch itself" + ], + status="partially-implemented", description=( "SCM-3/SCM-4: Plaintext transport (http://, git://, svn://) is accepted " "by design for legacy source compatibility. C-045 detects and warns the " @@ -335,12 +457,22 @@ class PartIIRequirement: "has not been tampered with in transit; it does not encrypt or conceal the " "content. This is a deliberate design decision, not a residual gap." ), + evidence_hrefs=[ + ("dfetch/manifest/project.py", "C-045 Plaintext transport detection"), + ("dfetch/vcs/integrity_hash.py", "C-005 Archive content integrity hash"), + ], ), SOImplementation( so_id="so-com-auth-e", ecr_id="ecr-e", - controls=["C-003", "C-004", "C-045"], - status="implemented", + controls=["C-045"], + gaps=[ + "Server authentication (TLS certificate verification, SSH host-key checking) " + "is delegated to the OS trust store and VCS client; dfetch does not " + "independently authenticate remote endpoints and cannot enforce authenticated " + "channels when C-045's warning is overridden by the user" + ], + status="partially-implemented", description=( "SCM-2: HTTPS connections authenticate via TLS CA chain (C-003); SSH " "connections authenticate via host-key verification (C-004). Plain git:// " @@ -351,6 +483,13 @@ class PartIIRequirement: "implemented TLS or SSH'; the residual risk for unauthenticated transports " "is an accepted design trade-off." ), + evidence_hrefs=[ + ( + "dfetch/vcs/archive.py", + "C-003 Archive symlink validation; C-004 Archive member-type checks", + ), + ("dfetch/manifest/project.py", "C-045 Plaintext transport detection"), + ], ), SOImplementation( so_id="so-secure-provisioning", @@ -362,6 +501,9 @@ class PartIIRequirement: "CRY-1: C-005 uses Python's hashlib with SHA-256 for integrity hashes. " "No key management is required or performed by dfetch." ), + evidence_hrefs=[ + ("dfetch/vcs/integrity_hash.py", "C-005 SHA-256 integrity hash (hashlib)"), + ], ), # ECR-f: Integrity SOImplementation( @@ -374,6 +516,12 @@ class PartIIRequirement: "SSM-2: C-005 (integrity hash in .dfetch_data.yaml) provides optional " "stored-data integrity verification." ), + evidence_hrefs=[ + ( + "dfetch/vcs/integrity_hash.py", + "C-005 Integrity hash stored in .dfetch_data.yaml", + ), + ], ), SOImplementation( so_id="so-data-processed-integrity", @@ -381,19 +529,33 @@ class PartIIRequirement: controls=["C-005", "C-034"], status="implemented", description="GEC-8: C-005 and C-034 protect data integrity during processing.", + evidence_hrefs=[ + ( + "dfetch/vcs/integrity_hash.py", + "C-005 Integrity hash; C-034 SHA-256/384/512 allowlist", + ), + ], ), SOImplementation( so_id="so-data-transmitted-integrity", ecr_id="ecr-f", - controls=["C-003", "C-004"], + controls=["C-005"], gaps=[ - "No end-to-end hash for git/svn transport beyond TLS/SSH channel integrity" + "C-005 provides end-to-end hash verification for archive sources only (opt-in); " + "git and svn sources rely solely on VCS object integrity (SHA-1/SHA-256 object " + "model) and TLS/SSH channel integrity — no dfetch-level hash verification" ], status="partially-implemented", description=( "SCM-2: TLS (C-003) and SSH (C-004) provide channel-level integrity. " "Commit-hash pinning (rev: ) provides content-level integrity for git." ), + evidence_hrefs=[ + ( + "dfetch/vcs/archive.py", + "C-003 Archive symlink validation; C-004 Archive member-type checks", + ), + ], ), SOImplementation( so_id="so-integrity-report", @@ -405,6 +567,12 @@ class PartIIRequirement: "GEC-13-f: dfetch surfaces transport-integrity warnings (C-045) at runtime " "but does not maintain a persistent security event log." ), + evidence_hrefs=[ + ( + "dfetch/manifest/project.py", + "C-045 Plaintext transport warning at runtime", + ), + ], ), # ECR-g: Data Minimisation SOImplementation( @@ -422,6 +590,12 @@ class PartIIRequirement: "justified by functional necessity. " "DTM-2: met by design — dfetch collects no telemetry or optional data." ), + evidence_hrefs=[ + ( + "dfetch/project/metadata.py", + "Metadata model — only non-sensitive fields stored", + ), + ], ), # ECR-h: Availability SOImplementation( @@ -459,12 +633,21 @@ class PartIIRequirement: "TCM-1 (dfetch makes targeted VCS fetch requests; " "no ambient outbound traffic to throttle)" ], + gaps=[ + "Archive HTTP operations time out at 15 s (reachability) and 60 s (download) " + "via ``archive.py``; git and svn subprocess calls have no timeout and can " + "stall indefinitely" + ], status="partially-implemented", description=( "GEC-8-i: C-001 (minimal deps) and C-007 (subprocess controls) reduce " "the risk of dfetch being weaponised against external services. " "LIM-1: dfetch fetches only what is listed in the manifest." ), + evidence_hrefs=[ + ("dfetch/util/util.py", "C-001 Path-traversal and dependency minimisation"), + ("dfetch/util/cmdline.py", "C-007 Subprocess safety (shell=False)"), + ], ), SOImplementation( so_id="so-prevent-attack-propagation", @@ -479,6 +662,10 @@ class PartIIRequirement: "under the 'Manifest under code review' assumption: dfetch.yaml is " "version-controlled and any such dst: change would be rejected at review." ), + evidence_hrefs=[ + ("dfetch/util/util.py", "C-001 check_no_path_traversal() via realpath"), + ("dfetch/manifest/schema.py", "C-008 Manifest URL and path validation"), + ], ), SOImplementation( so_id="so-monitor-external-impact", @@ -498,11 +685,28 @@ class PartIIRequirement: "GEC-2-j, GEC-3-j, GEC-4-j, GEC-5-j, GEC-7-j " "(dfetch exposes no network services)" ], + gaps=[ + "No domain or URL-scheme allowlist constrains which remote URLs the manifest " + "may reference; git and svn subprocess calls have no timeout (archive HTTP " + "operations time out at 15 s / 60 s)" + ], status="partially-implemented", description=( "GEC-6: C-007 (no shell=True), C-008 (URL/path validation) implement " "input validation. GEC-12-j: C-001 enforces minimal runtime dependencies." ), + evidence_hrefs=[ + ( + "dfetch/util/cmdline.py", + "C-007 Subprocess safety (shell=False everywhere)", + ), + ("dfetch/manifest/schema.py", "C-008 Manifest input validation"), + ("dfetch/util/util.py", "C-001 Minimal dependency footprint"), + ( + "dfetch/vcs/archive.py", + "C-003 Symlink validation; C-004 Member-type checks", + ), + ], ), # ECR-k: Exploit Mitigation SOImplementation( @@ -519,21 +723,37 @@ class PartIIRequirement: "shell=False (C-007); static analysis (C-015, C-017). " "C-046 formalises this inventory in doc/explanation/compliance_track.rst." ), + evidence_hrefs=[ + (".github/workflows/codeql-analysis.yml", "C-015 CodeQL static analysis"), + ("pyproject.toml", "C-017 bandit security linter configuration"), + ("dfetch/vcs/integrity_hash.py", "C-005 Constant-time HMAC comparison"), + ("dfetch/util/cmdline.py", "C-007 No shell=True in subprocess calls"), + ], ), # ECR-l: Monitoring and Logging SOImplementation( so_id="so-log-security-relevant-activities", ecr_id="ecr-l", - controls=["C-036"], + controls=[], gaps=[ - "No persistent security event log (LGM-2/3/4 gap)", - "No opt-out for logging — dfetch does not log by default", + "No persistent structured security event log (LGM-1/2/3/4 gap). dfetch prints " + "operational output to stderr but does not retain it, does not record which " + "credentials were used, which files were modified, or when remote access occurred. " + "C-036 ensures credentials are excluded from operational output but is not a " + "logging control" ], status="partially-implemented", description=( "LGM-1: dfetch logs warnings to stderr during a run but does not persist " "them. LGM-6: C-036 ensures credentials are not logged." ), + evidence_hrefs=[ + ( + "dfetch/project/metadata.py", + "C-036 Credential exclusion from stored metadata and logs", + ), + ("dfetch/log/", "Logging module — transient stderr only"), + ], ), SOImplementation( so_id="so-monitor-security-relevant-activities", @@ -545,6 +765,10 @@ class PartIIRequirement: "MON-1: C-045 (plaintext transport detection) monitors for insecure " "connections at runtime and surfaces warnings to the user." ), + evidence_hrefs=[ + ("dfetch/manifest/project.py", "C-045 Plaintext transport detection"), + ("dfetch/project/subproject.py", "C-045 Runtime transport monitoring"), + ], ), SOImplementation( so_id="so-option-disable-data-logging", @@ -581,6 +805,15 @@ class PartIIRequirement: "to remove all dfetch data. ECR-m is satisfied by design because dfetch " "collects no personal data, credentials, or keying material on disk." ), + note=( + "**ECR-M SO.SecureDataDeletion** — No dfetch-specific control is needed. " + "DLM-1 is satisfied by design: dfetch stores no personal data, credentials, " + "or cryptographic keying material on disk. The only on-disk state is " + "``.dfetch_data.yaml`` (non-sensitive dependency metadata — credentials " + "stripped by :ref:`C-036 `) and vendored source files (third-party " + "code). Standard OS file deletion (``rm`` / ``del``) is sufficient to remove " + "all dfetch data; no secure-wipe facility is warranted." + ), ), SOImplementation( so_id="so-data-transmitted-confidentiality-m", @@ -622,7 +855,9 @@ class PartIIRequirement: ref="Part II §2", text="Address vulnerabilities without delay; provide free security updates.", controls=["C-015", "C-016", "SECURITY.md"], - gaps=["No LTS backport policy (latest release only — documented in SECURITY.md)"], + gaps=[ + "No LTS backport policy (latest release only — documented in SECURITY.md)" + ], status="partially-implemented", ), PartIIRequirement( diff --git a/security/cra_pren_4000014_oscal_catalog.json b/security/cra_pren_4000014_oscal_catalog.json index d848aa0eb..9667faa5f 100644 --- a/security/cra_pren_4000014_oscal_catalog.json +++ b/security/cra_pren_4000014_oscal_catalog.json @@ -5,13 +5,52 @@ "title": "CRA Security Controls Catalog – prEN 40000-1-4 (PT2 Generic Security Requirements)", "last-modified": "2026-06-15T00:00:00Z", "version": "draft-2026-03-05", - "oscal-version": "1.1.2", + "oscal-version": "1.2.2", "remarks": "Derived from the CEN/CLC/JTC 13/WG 9 PT2 deep-dive session by Angelo D'Amato (Vulnir B.V.), 5 March 2026, under STAN4CR Grant Agreement No. 101196779. Maps CRA Annex I Part I Essential Requirements (a)–(m) to security objectives and technical controls from prEN 40000-1-4, building on EN 18031:2024 with new controls for CRA scope.", "props": [ { "name": "regulation", "value": "Regulation (EU) 2024/2847 – Cyber Resilience Act" }, { "name": "standard-reference", "value": "prEN 40000-1-4" }, { "name": "source-presentation", "value": "2026-03-05_CRA_Unlocked_DeepDive_SecurityControls_CEN-CLC-JTC-13-WG-9_PT2.pdf" }, { "name": "status", "value": "draft – indicative publication October 2027" } + ], + "roles": [ + { + "id": "catalog-maintainer", + "title": "Catalog Maintainer", + "description": "Organisation maintaining this OSCAL encoding of the prEN 40000-1-4 control catalog." + }, + { + "id": "content-creator", + "title": "Content Creator", + "description": "Author of the original prEN 40000-1-4 control mapping presented at the CEN/CLC/JTC 13/WG 9 PT2 deep-dive session." + } + ], + "parties": [ + { + "uuid": "d9e4f123-8a01-5b2c-9f3e-6c7d8e9f0a1b", + "type": "organization", + "name": "dfetch-org", + "links": [ + { "href": "https://github.com/dfetch-org", "rel": "homepage" } + ], + "remarks": "OSCAL encoding of the catalog maintained by dfetch-org." + }, + { + "uuid": "a2b3c4d5-e6f7-8901-bcde-f01234567890", + "type": "person", + "name": "Angelo D'Amato, Vulnir B.V.", + "remarks": "Author of the original prEN 40000-1-4 control mapping (STAN4CR Grant Agreement No. 101196779)." + } + ], + "responsible-parties": [ + { + "role-id": "catalog-maintainer", + "party-uuids": ["d9e4f123-8a01-5b2c-9f3e-6c7d8e9f0a1b"] + }, + { + "role-id": "content-creator", + "party-uuids": ["a2b3c4d5-e6f7-8901-bcde-f01234567890"] + } ] }, "groups": [ diff --git a/security/dfetch.component-definition.json b/security/dfetch.component-definition.json index 42576c39d..ebc78b102 100644 --- a/security/dfetch.component-definition.json +++ b/security/dfetch.component-definition.json @@ -3,9 +3,15 @@ "uuid": "eddc2e8c-a2ae-5a42-8d35-88f5c2af55b2", "metadata": { "title": "dfetch CRA Compliance Component Definition", - "last-modified": "2026-06-16T00:00:00Z", + "last-modified": "2026-06-17T00:00:00Z", "version": "0.15.0", - "oscal-version": "1.1.2", + "oscal-version": "1.2.2", + "document-ids": [ + { + "scheme": "uri", + "identifier": "https://github.com/dfetch-org/dfetch/blob/main/security/dfetch.component-definition.json" + } + ], "props": [ { "name": "cra-class", @@ -18,6 +24,50 @@ { "name": "standard", "value": "prEN 40000-1-4 (draft, indicative publication October 2027)" + }, + { + "name": "openssf-scorecard", + "value": "https://api.securityscorecards.dev/projects/github.com/dfetch-org/dfetch" + } + ], + "roles": [ + { + "id": "supplier", + "title": "Software Supplier", + "description": "Organisation that develops and distributes dfetch." + }, + { + "id": "maintainer", + "title": "Maintainer", + "description": "Primary maintainer of the dfetch project and this compliance document." + } + ], + "parties": [ + { + "uuid": "af05d1a0-4751-533e-b0d4-377d76da174b", + "type": "organization", + "name": "dfetch-org", + "links": [ + { + "href": "https://github.com/dfetch-org", + "rel": "homepage" + } + ], + "remarks": "Non-commercial open-source organisation. dfetch is not placed on the market in the context of a commercial activity." + } + ], + "responsible-parties": [ + { + "role-id": "supplier", + "party-uuids": [ + "af05d1a0-4751-533e-b0d4-377d76da174b" + ] + }, + { + "role-id": "maintainer", + "party-uuids": [ + "af05d1a0-4751-533e-b0d4-377d76da174b" + ] } ], "remarks": "Produced voluntarily under Article 13(5) to support downstream integrators. dfetch is non-commercial open-source software and is not subject to mandatory CRA conformity obligations (Recital 18)." @@ -28,10 +78,23 @@ "type": "software", "title": "dfetch", "description": "Python CLI tool for vendoring source-code dependencies from Git, SVN, or archive files into a project as plain files.", + "purpose": "Vendor source-code dependencies from Git, SVN, or archive files into a project as plain files, enabling reproducible builds without submodules or externals. Supports integrity hashing, plaintext-transport detection, and credential redaction to reduce supply-chain risk.", "props": [ { "name": "software-version", "value": "0.15.0" + }, + { + "name": "asset-type", + "value": "software.application" + }, + { + "name": "vendor-name", + "value": "dfetch-org" + }, + { + "name": "license", + "value": "MIT" } ], "links": [ @@ -47,6 +110,19 @@ "href": "security/cra_pren_4000014_oscal_catalog.json", "rel": "reference", "text": "prEN 40000-1-4 OSCAL Catalog" + }, + { + "href": "SECURITY.md", + "rel": "reference", + "text": "Vulnerability Disclosure Policy" + } + ], + "responsible-roles": [ + { + "role-id": "supplier", + "party-uuids": [ + "af05d1a0-4751-533e-b0d4-377d76da174b" + ] } ], "control-implementations": [ @@ -65,7 +141,7 @@ }, { "name": "dfetch-controls", - "value": "C-015, C-016, C-017, C-022, C-043" + "value": "C-015, C-016, C-017, C-022, C-040, C-043" } ], "statements": [ @@ -74,6 +150,23 @@ "uuid": "d07ef2cb-63b5-5c7b-a70e-6530e894f633", "description": "GEC-1: C-015 (CodeQL), C-016 (dependency-review), C-017 (bandit), C-022 (SBOM) address known-vulnerability detection in CI. C-043 (pip-audit OSV gate) blocks release if runtime dependencies carry known vulnerabilities." } + ], + "links": [ + { + "href": ".github/workflows/codeql-analysis.yml", + "rel": "evidence", + "text": "C-015 CodeQL static analysis" + }, + { + "href": ".github/workflows/dependency-review.yml", + "rel": "evidence", + "text": "C-016 Dependency review" + }, + { + "href": ".github/workflows/python-publish.yml", + "rel": "evidence", + "text": "C-022 SBOM generation; C-043 pip-audit CVE gate" + } ] }, { @@ -88,6 +181,10 @@ "name": "not-applicable", "value": "GEC-2, GEC-3, GEC-4, GEC-5 (no exposed network services \u2014 CLI tool); GEC-7 (no external sensing capabilities); AUM-5 (no password or authentication mechanism)" }, + { + "name": "gaps", + "value": "Integrity hash verification (:ref:`C-005 `) is opt-in; manifest entries without an ``integrity`` field are fetched without hash verification by default" + }, { "name": "dfetch-controls", "value": "C-001, C-002" @@ -97,7 +194,19 @@ { "statement-id": "so-secure-default-configuration-stmt", "uuid": "709823d8-4d88-57af-924b-59bde9ed1e31", - "description": "GEC-12 (no unneeded software components): C-001 enforces minimal runtime dependencies. dfetch does not expose network services. Not applicable: GEC-2, GEC-3, GEC-4, GEC-5 (no exposed network services \u2014 CLI tool); GEC-7 (no external sensing capabilities); AUM-5 (no password or authentication mechanism)" + "description": "GEC-12 (no unneeded software components): C-001 enforces minimal runtime dependencies. dfetch does not expose network services. Gaps: Integrity hash verification (:ref:`C-005 `) is opt-in; manifest entries without an ``integrity`` field are fetched without hash verification by default Not applicable: GEC-2, GEC-3, GEC-4, GEC-5 (no exposed network services \u2014 CLI tool); GEC-7 (no external sensing capabilities); AUM-5 (no password or authentication mechanism)" + } + ], + "links": [ + { + "href": "dfetch/util/util.py", + "rel": "evidence", + "text": "C-001 Path-traversal prevention" + }, + { + "href": "dfetch/vcs/archive.py", + "rel": "evidence", + "text": "C-002 Decompression-bomb protection; C-003 Symlink validation; C-004 Member-type checks" } ] }, @@ -150,13 +259,17 @@ { "name": "implementation-status", "value": "implemented" + }, + { + "name": "dfetch-controls", + "value": "C-010, C-039, C-043" } ], "statements": [ { "statement-id": "so-updateability-stmt", "uuid": "4619e5ae-8f33-53a0-ae99-dcb6e045d1fd", - "description": "SUM-1/SUM-2: Updates distributed via PyPI (pip install --upgrade dfetch) and GitHub Releases. pip's TLS-protected download satisfies SUM-2." + "description": "SUM-1/SUM-2: Updates distributed via PyPI (``pip install --upgrade dfetch``) and GitHub Releases. pip's TLS-protected download and version-pinning model satisfies SUM-2. No dfetch-specific update mechanism is needed or implemented; the package manager is the update vehicle." } ] }, @@ -188,17 +301,30 @@ { "name": "implementation-status", "value": "implemented" - }, - { - "name": "dfetch-controls", - "value": "C-040" } ], "statements": [ { "statement-id": "so-user-update-notification-stmt", "uuid": "5ecb3c34-e8a6-5998-95e1-4918aae6993a", - "description": "UNM-4: dfetch check and dfetch environment report when a newer dfetch version is available (C-040)." + "description": "UNM-4: ``dfetch check`` and ``dfetch environment`` both call ``newer_version_available()`` (``dfetch/util/github_version_check.py``), which polls the GitHub releases API and prints a notice if a newer dfetch release exists." + } + ], + "links": [ + { + "href": "dfetch/util/github_version_check.py", + "rel": "evidence", + "text": "Version availability check implementation" + }, + { + "href": "dfetch/commands/check.py", + "rel": "evidence", + "text": "Version check in dfetch check (suppressed in CI)" + }, + { + "href": "dfetch/commands/environment.py", + "rel": "evidence", + "text": "Version check in dfetch environment" } ] }, @@ -235,6 +361,10 @@ "name": "not-applicable", "value": "ACM-2, AUM-2, AUM-3, AUM-4, AUM-6 (dfetch has no user-facing authentication or access control)" }, + { + "name": "gaps", + "value": "dfetch has no native authentication or authorisation layer; access control is fully delegated to the underlying VCS server and host OS. C-006 prevents interactive credential prompts, and C-036 strips credentials from persisted metadata \u2014 both are confidentiality controls, not access-control mechanisms in the authentication/authorisation sense" + }, { "name": "dfetch-controls", "value": "C-006, C-036" @@ -244,7 +374,24 @@ { "statement-id": "so-access-control-stmt", "uuid": "851f2832-c17f-51e2-8fef-28d575f3ea42", - "description": "dfetch delegates authentication to the host VCS client (git, svn) and the OS credential store. C-006 prevents SSH command injection; C-036 strips credentials from stored metadata. Not applicable: ACM-2, AUM-2, AUM-3, AUM-4, AUM-6 (dfetch has no user-facing authentication or access control)" + "description": "dfetch delegates authentication to the host VCS client (git, svn) and the OS credential store. C-006 prevents SSH command injection; C-036 strips credentials from stored metadata. Gaps: dfetch has no native authentication or authorisation layer; access control is fully delegated to the underlying VCS server and host OS. C-006 prevents interactive credential prompts, and C-036 strips credentials from persisted metadata \u2014 both are confidentiality controls, not access-control mechanisms in the authentication/authorisation sense Not applicable: ACM-2, AUM-2, AUM-3, AUM-4, AUM-6 (dfetch has no user-facing authentication or access control)" + } + ], + "links": [ + { + "href": "dfetch/vcs/git.py", + "rel": "evidence", + "text": "C-006 Non-interactive git (prevents SSH injection)" + }, + { + "href": "dfetch/vcs/svn.py", + "rel": "evidence", + "text": "C-006 Non-interactive svn" + }, + { + "href": "dfetch/project/metadata.py", + "rel": "evidence", + "text": "C-036 Credential redaction from stored metadata" } ] }, @@ -262,14 +409,26 @@ }, { "name": "dfetch-controls", - "value": "C-009" + "value": "C-045" } ], "statements": [ { "statement-id": "so-access-control-report-stmt", "uuid": "5fc0e660-93e3-57a5-ae6a-55d9b9365c55", - "description": "GEC-13: C-009 (plaintext transport warning) alerts on unauthenticated connections. No persistent security event log. Gaps: No persistent log of unauthorised access attempts" + "description": "GEC-13: C-045 (plaintext transport warning) alerts on unauthenticated connections. No persistent security event log. Gaps: No persistent log of unauthorised access attempts" + } + ], + "links": [ + { + "href": "dfetch/manifest/project.py", + "rel": "evidence", + "text": "C-045 Plaintext transport detection" + }, + { + "href": "dfetch/project/subproject.py", + "rel": "evidence", + "text": "C-045 Plaintext transport warning" } ] }, @@ -292,6 +451,13 @@ "uuid": "d6a8cfa8-3e30-5318-8ca6-59f99fb7dbff", "description": "SSM-1/SSM-3: The developer workstation is a trusted boundary (as established in the usage threat model: 'Trusted at workstation invocation time'). C-036 strips all credentials from .dfetch_data.yaml before write, so the stored file contains only non-sensitive metadata (remote URL without userinfo, revision, content hash, timestamp). The threat model explicitly marks metadata_store.storesSensitiveData = False and isDestEncryptedAtRest = False as a conscious design choice; no encryption-at-rest is required." } + ], + "links": [ + { + "href": "dfetch/project/metadata.py", + "rel": "evidence", + "text": "C-036 Credential redaction before write" + } ] }, { @@ -313,6 +479,13 @@ "uuid": "38cff185-27ad-55b2-96f7-da88b47eedaa", "description": "GEC-8: C-005 (constant-time comparison), C-034 (temp-file cleanup) protect in-process data." } + ], + "links": [ + { + "href": "dfetch/vcs/integrity_hash.py", + "rel": "evidence", + "text": "C-005 Constant-time HMAC comparison; C-034 SHA-256/384/512 allowlist" + } ] }, { @@ -321,18 +494,34 @@ "props": [ { "name": "implementation-status", - "value": "implemented" + "value": "partially-implemented" + }, + { + "name": "gaps", + "value": "C-045 warns on plaintext-scheme URLs but does not refuse to proceed; TLS/SSH confidentiality is provided by the underlying VCS client, not enforced by dfetch itself" }, { "name": "dfetch-controls", - "value": "C-005, C-009" + "value": "C-045" } ], "statements": [ { "statement-id": "so-data-transmitted-confidentiality-stmt", "uuid": "4440d616-231f-51ca-a514-c5cd42a3851b", - "description": "SCM-3/SCM-4: Plaintext transport (http://, git://, svn://) is accepted by design for legacy source compatibility. C-009 detects and warns the user before proceeding \u2014 'Detection only; dfetch still proceeds with the plaintext connection; the control raises user awareness but does not enforce scheme selection' (usage threat model, C-009 description). For archive URLs over HTTP, C-005 (integrity hash) verifies that content has not been tampered with in transit; it does not encrypt or conceal the content. This is a deliberate design decision, not a residual gap." + "description": "SCM-3/SCM-4: Plaintext transport (http://, git://, svn://) is accepted by design for legacy source compatibility. C-045 detects and warns the user before proceeding \u2014 'Detection only; dfetch still proceeds with the plaintext connection; the control raises user awareness but does not enforce scheme selection' (usage threat model, C-045 description). For archive URLs over HTTP, C-005 (integrity hash) verifies that content has not been tampered with in transit; it does not encrypt or conceal the content. This is a deliberate design decision, not a residual gap. Gaps: C-045 warns on plaintext-scheme URLs but does not refuse to proceed; TLS/SSH confidentiality is provided by the underlying VCS client, not enforced by dfetch itself" + } + ], + "links": [ + { + "href": "dfetch/manifest/project.py", + "rel": "evidence", + "text": "C-045 Plaintext transport detection" + }, + { + "href": "dfetch/vcs/integrity_hash.py", + "rel": "evidence", + "text": "C-005 Archive content integrity hash" } ] }, @@ -342,18 +531,34 @@ "props": [ { "name": "implementation-status", - "value": "implemented" + "value": "partially-implemented" + }, + { + "name": "gaps", + "value": "Server authentication (TLS certificate verification, SSH host-key checking) is delegated to the OS trust store and VCS client; dfetch does not independently authenticate remote endpoints and cannot enforce authenticated channels when C-045's warning is overridden by the user" }, { "name": "dfetch-controls", - "value": "C-003, C-004, C-009" + "value": "C-045" } ], "statements": [ { "statement-id": "so-com-auth-e-stmt", "uuid": "c083999a-6c6c-5185-a7ad-8ee203dc13a4", - "description": "SCM-2: HTTPS connections authenticate via TLS CA chain (C-003); SSH connections authenticate via host-key verification (C-004). Plain git:// and svn:// connections lack channel-level authentication but are accepted by design for legacy compatibility \u2014 the same rationale as DataTransmittedConfidentiality \u2014 and C-009 warns the user. The usage threat model notes the network adversary 'cannot break correctly implemented TLS or SSH'; the residual risk for unauthenticated transports is an accepted design trade-off." + "description": "SCM-2: HTTPS connections authenticate via TLS CA chain (C-003); SSH connections authenticate via host-key verification (C-004). Plain git:// and svn:// connections lack channel-level authentication but are accepted by design for legacy compatibility \u2014 the same rationale as DataTransmittedConfidentiality \u2014 and C-045 warns the user. The usage threat model notes the network adversary 'cannot break correctly implemented TLS or SSH'; the residual risk for unauthenticated transports is an accepted design trade-off. Gaps: Server authentication (TLS certificate verification, SSH host-key checking) is delegated to the OS trust store and VCS client; dfetch does not independently authenticate remote endpoints and cannot enforce authenticated channels when C-045's warning is overridden by the user" + } + ], + "links": [ + { + "href": "dfetch/vcs/archive.py", + "rel": "evidence", + "text": "C-003 Archive symlink validation; C-004 Archive member-type checks" + }, + { + "href": "dfetch/manifest/project.py", + "rel": "evidence", + "text": "C-045 Plaintext transport detection" } ] }, @@ -380,6 +585,13 @@ "uuid": "f337f022-b1e9-58f6-9b96-87434a022998", "description": "CRY-1: C-005 uses Python's hashlib with SHA-256 for integrity hashes. No key management is required or performed by dfetch. Not applicable: CCK-1, CCK-2, CCK-3 (dfetch manages no cryptographic keys)" } + ], + "links": [ + { + "href": "dfetch/vcs/integrity_hash.py", + "rel": "evidence", + "text": "C-005 SHA-256 integrity hash (hashlib)" + } ] }, { @@ -405,6 +617,13 @@ "uuid": "8a522c77-b369-527f-a9ee-ef2b66e95dd4", "description": "SSM-2: C-005 (integrity hash in .dfetch_data.yaml) provides optional stored-data integrity verification. Gaps: Integrity hash opt-in only; not enforced by default for git/svn" } + ], + "links": [ + { + "href": "dfetch/vcs/integrity_hash.py", + "rel": "evidence", + "text": "C-005 Integrity hash stored in .dfetch_data.yaml" + } ] }, { @@ -426,6 +645,13 @@ "uuid": "5de5eb69-9db8-5b2c-9d56-cd7dc067c9e1", "description": "GEC-8: C-005 and C-034 protect data integrity during processing." } + ], + "links": [ + { + "href": "dfetch/vcs/integrity_hash.py", + "rel": "evidence", + "text": "C-005 Integrity hash; C-034 SHA-256/384/512 allowlist" + } ] }, { @@ -438,18 +664,25 @@ }, { "name": "gaps", - "value": "No end-to-end hash for git/svn transport beyond TLS/SSH channel integrity" + "value": "C-005 provides end-to-end hash verification for archive sources only (opt-in); git and svn sources rely solely on VCS object integrity (SHA-1/SHA-256 object model) and TLS/SSH channel integrity \u2014 no dfetch-level hash verification" }, { "name": "dfetch-controls", - "value": "C-003, C-004" + "value": "C-005" } ], "statements": [ { "statement-id": "so-data-transmitted-integrity-stmt", "uuid": "615addd1-b43e-5585-9bea-2a806c4f6f0e", - "description": "SCM-2: TLS (C-003) and SSH (C-004) provide channel-level integrity. Commit-hash pinning (rev: ) provides content-level integrity for git. Gaps: No end-to-end hash for git/svn transport beyond TLS/SSH channel integrity" + "description": "SCM-2: TLS (C-003) and SSH (C-004) provide channel-level integrity. Commit-hash pinning (rev: ) provides content-level integrity for git. Gaps: C-005 provides end-to-end hash verification for archive sources only (opt-in); git and svn sources rely solely on VCS object integrity (SHA-1/SHA-256 object model) and TLS/SSH channel integrity \u2014 no dfetch-level hash verification" + } + ], + "links": [ + { + "href": "dfetch/vcs/archive.py", + "rel": "evidence", + "text": "C-003 Archive symlink validation; C-004 Archive member-type checks" } ] }, @@ -467,14 +700,21 @@ }, { "name": "dfetch-controls", - "value": "C-009" + "value": "C-045" } ], "statements": [ { "statement-id": "so-integrity-report-stmt", "uuid": "99d5808a-c006-5839-a3f3-acbfe9e93974", - "description": "GEC-13-f: dfetch surfaces transport-integrity warnings (C-009) at runtime but does not maintain a persistent security event log. Gaps: No persistent integrity-violation log" + "description": "GEC-13-f: dfetch surfaces transport-integrity warnings (C-045) at runtime but does not maintain a persistent security event log. Gaps: No persistent integrity-violation log" + } + ], + "links": [ + { + "href": "dfetch/manifest/project.py", + "rel": "evidence", + "text": "C-045 Plaintext transport warning at runtime" } ] }, @@ -501,6 +741,13 @@ "uuid": "dda2d3c4-5ef4-56bd-a2a2-923a894ae320", "description": "DTM-1: C-044 documents that .dfetch_data.yaml is limited to remote_url (stripped), revision, optional hash, and last_fetch \u2014 each justified by functional necessity. DTM-2: met by design \u2014 dfetch collects no telemetry or optional data. Not applicable: UNM-1, UNM-2 (dfetch processes no personal data requiring user notification); DTM-3 (no optional data processing to configure)" } + ], + "links": [ + { + "href": "dfetch/project/metadata.py", + "rel": "evidence", + "text": "Metadata model \u2014 only non-sensitive fields stored" + } ] }, { @@ -565,6 +812,10 @@ "name": "not-applicable", "value": "TCM-1 (dfetch makes targeted VCS fetch requests; no ambient outbound traffic to throttle)" }, + { + "name": "gaps", + "value": "Archive HTTP operations time out at 15 s (reachability) and 60 s (download) via ``archive.py``; git and svn subprocess calls have no timeout and can stall indefinitely" + }, { "name": "dfetch-controls", "value": "C-001, C-007" @@ -574,7 +825,19 @@ { "statement-id": "so-limit-external-impact-stmt", "uuid": "27745cea-042a-5b68-85d4-a3fc05e25645", - "description": "GEC-8-i: C-001 (minimal deps) and C-007 (subprocess controls) reduce the risk of dfetch being weaponised against external services. LIM-1: dfetch fetches only what is listed in the manifest. Not applicable: TCM-1 (dfetch makes targeted VCS fetch requests; no ambient outbound traffic to throttle)" + "description": "GEC-8-i: C-001 (minimal deps) and C-007 (subprocess controls) reduce the risk of dfetch being weaponised against external services. LIM-1: dfetch fetches only what is listed in the manifest. Gaps: Archive HTTP operations time out at 15 s (reachability) and 60 s (download) via ``archive.py``; git and svn subprocess calls have no timeout and can stall indefinitely Not applicable: TCM-1 (dfetch makes targeted VCS fetch requests; no ambient outbound traffic to throttle)" + } + ], + "links": [ + { + "href": "dfetch/util/util.py", + "rel": "evidence", + "text": "C-001 Path-traversal and dependency minimisation" + }, + { + "href": "dfetch/util/cmdline.py", + "rel": "evidence", + "text": "C-007 Subprocess safety (shell=False)" } ] }, @@ -597,6 +860,18 @@ "uuid": "0841a803-efcb-56d5-9498-c0a6d2d1a49d", "description": "LIM-2: Path traversal to destinations outside the project tree is prevented by C-001 (check_no_path_traversal() via realpath). The residual risk of a manifest declaring a legitimate but sensitive dst: (e.g. .github/workflows/) is accepted in the usage threat model under the 'Manifest under code review' assumption: dfetch.yaml is version-controlled and any such dst: change would be rejected at review." } + ], + "links": [ + { + "href": "dfetch/util/util.py", + "rel": "evidence", + "text": "C-001 check_no_path_traversal() via realpath" + }, + { + "href": "dfetch/manifest/schema.py", + "rel": "evidence", + "text": "C-008 Manifest URL and path validation" + } ] }, { @@ -632,6 +907,10 @@ "name": "not-applicable", "value": "GEC-2-j, GEC-3-j, GEC-4-j, GEC-5-j, GEC-7-j (dfetch exposes no network services)" }, + { + "name": "gaps", + "value": "No domain or URL-scheme allowlist constrains which remote URLs the manifest may reference; git and svn subprocess calls have no timeout (archive HTTP operations time out at 15 s / 60 s)" + }, { "name": "dfetch-controls", "value": "C-001, C-003, C-004, C-007, C-008" @@ -641,7 +920,29 @@ { "statement-id": "so-reduce-attack-surface-stmt", "uuid": "d05109ab-eb71-5788-86f4-77ee6a6005f7", - "description": "GEC-6: C-007 (no shell=True), C-008 (URL/path validation) implement input validation. GEC-12-j: C-001 enforces minimal runtime dependencies. Not applicable: GEC-2-j, GEC-3-j, GEC-4-j, GEC-5-j, GEC-7-j (dfetch exposes no network services)" + "description": "GEC-6: C-007 (no shell=True), C-008 (URL/path validation) implement input validation. GEC-12-j: C-001 enforces minimal runtime dependencies. Gaps: No domain or URL-scheme allowlist constrains which remote URLs the manifest may reference; git and svn subprocess calls have no timeout (archive HTTP operations time out at 15 s / 60 s) Not applicable: GEC-2-j, GEC-3-j, GEC-4-j, GEC-5-j, GEC-7-j (dfetch exposes no network services)" + } + ], + "links": [ + { + "href": "dfetch/util/cmdline.py", + "rel": "evidence", + "text": "C-007 Subprocess safety (shell=False everywhere)" + }, + { + "href": "dfetch/manifest/schema.py", + "rel": "evidence", + "text": "C-008 Manifest input validation" + }, + { + "href": "dfetch/util/util.py", + "rel": "evidence", + "text": "C-001 Minimal dependency footprint" + }, + { + "href": "dfetch/vcs/archive.py", + "rel": "evidence", + "text": "C-003 Symlink validation; C-004 Member-type checks" } ] }, @@ -668,6 +969,28 @@ "uuid": "c6e07d34-53fe-5d62-8e20-14848fc721e1", "description": "GEC-11: Python interpreter provides ASLR/DEP/stack-canaries (OS-level). dfetch: no eval/exec of remote content; constant-time comparison (C-005); shell=False (C-007); static analysis (C-015, C-017). C-046 formalises this inventory in doc/explanation/compliance_track.rst. Not applicable: Compile-time mitigations (CFI, sandboxing) \u2014 not applicable to pure Python" } + ], + "links": [ + { + "href": ".github/workflows/codeql-analysis.yml", + "rel": "evidence", + "text": "C-015 CodeQL static analysis" + }, + { + "href": "pyproject.toml", + "rel": "evidence", + "text": "C-017 bandit security linter configuration" + }, + { + "href": "dfetch/vcs/integrity_hash.py", + "rel": "evidence", + "text": "C-005 Constant-time HMAC comparison" + }, + { + "href": "dfetch/util/cmdline.py", + "rel": "evidence", + "text": "C-007 No shell=True in subprocess calls" + } ] }, { @@ -680,18 +1003,26 @@ }, { "name": "gaps", - "value": "No persistent security event log (LGM-2/3/4 gap); No opt-out for logging \u2014 dfetch does not log by default" - }, - { - "name": "dfetch-controls", - "value": "C-036" + "value": "No persistent structured security event log (LGM-1/2/3/4 gap). dfetch prints operational output to stderr but does not retain it, does not record which credentials were used, which files were modified, or when remote access occurred. C-036 ensures credentials are excluded from operational output but is not a logging control" } ], "statements": [ { "statement-id": "so-log-security-relevant-activities-stmt", "uuid": "25624d61-2d09-5f3f-b16c-9ab4f44755bf", - "description": "LGM-1: dfetch logs warnings to stderr during a run but does not persist them. LGM-6: C-036 ensures credentials are not logged. Gaps: No persistent security event log (LGM-2/3/4 gap); No opt-out for logging \u2014 dfetch does not log by default" + "description": "LGM-1: dfetch logs warnings to stderr during a run but does not persist them. LGM-6: C-036 ensures credentials are not logged. Gaps: No persistent structured security event log (LGM-1/2/3/4 gap). dfetch prints operational output to stderr but does not retain it, does not record which credentials were used, which files were modified, or when remote access occurred. C-036 ensures credentials are excluded from operational output but is not a logging control" + } + ], + "links": [ + { + "href": "dfetch/project/metadata.py", + "rel": "evidence", + "text": "C-036 Credential exclusion from stored metadata and logs" + }, + { + "href": "dfetch/log/", + "rel": "evidence", + "text": "Logging module \u2014 transient stderr only" } ] }, @@ -709,14 +1040,26 @@ }, { "name": "dfetch-controls", - "value": "C-009" + "value": "C-045" } ], "statements": [ { "statement-id": "so-monitor-security-relevant-activities-stmt", "uuid": "70de7f3c-825a-520e-b8c9-e7e40d847c4a", - "description": "MON-1: C-009 (plaintext transport detection) monitors for insecure connections at runtime and surfaces warnings to the user. Not applicable: NMM-1 (no ambient network monitoring)" + "description": "MON-1: C-045 (plaintext transport detection) monitors for insecure connections at runtime and surfaces warnings to the user. Not applicable: NMM-1 (no ambient network monitoring)" + } + ], + "links": [ + { + "href": "dfetch/manifest/project.py", + "rel": "evidence", + "text": "C-045 Plaintext transport detection" + }, + { + "href": "dfetch/project/subproject.py", + "rel": "evidence", + "text": "C-045 Runtime transport monitoring" } ] }, @@ -769,13 +1112,17 @@ { "name": "implementation-status", "value": "implemented" + }, + { + "name": "not-applicable", + "value": "No dfetch-specific secure-deletion control is required: dfetch stores no personal data and no keying material. The only on-disk state is .dfetch_data.yaml (non-sensitive metadata) and vendored source files (third-party code, not user data). Standard OS file deletion (rm / del) is sufficient; cryptographic wipe is not warranted. DLM-1 is satisfied by design (no sensitive data to wipe), not by a dedicated dfetch control." } ], "statements": [ { "statement-id": "so-secure-data-deletion-stmt", "uuid": "6fe16078-41e4-5fc5-9002-f26643b1c3ac", - "description": "DLM-1: .dfetch_data.yaml contains only non-sensitive metadata \u2014 remote URL (credentials stripped by C-036), revision, optional content hash, and last-fetch timestamp. Standard OS file deletion (rm / del) is sufficient; no secure-wipe is required. Users delete the file and vendored directories to remove all dfetch data." + "description": "DLM-1: .dfetch_data.yaml contains only non-sensitive metadata \u2014 remote URL (credentials stripped by C-036), revision, optional content hash, and last-fetch timestamp. Standard OS file deletion is sufficient; no secure-wipe is required. Users delete the file and vendored directories to remove all dfetch data. ECR-m is satisfied by design because dfetch collects no personal data, credentials, or keying material on disk. Not applicable: No dfetch-specific secure-deletion control is required: dfetch stores no personal data and no keying material. The only on-disk state is .dfetch_data.yaml (non-sensitive metadata) and vendored source files (third-party code, not user data). Standard OS file deletion (rm / del) is sufficient; cryptographic wipe is not warranted. DLM-1 is satisfied by design (no sensitive data to wipe), not by a dedicated dfetch control." } ] }, @@ -869,12 +1216,153 @@ }, { "uuid": "ba6fc625-58ea-5b9b-afc6-9358d247f19a", - "title": "dfetch Security Policy (SECURITY.md)", + "title": "dfetch Vulnerability Disclosure Policy (SECURITY.md)", "rlinks": [ { "href": "https://github.com/dfetch-org/dfetch/blob/main/SECURITY.md" } ] + }, + { + "uuid": "711fb72a-ef71-5747-802e-29d85afa20f9", + "title": "OpenSSF Scorecard \u2014 dfetch supply-chain security score", + "props": [ + { + "name": "type", + "value": "assessment-report" + }, + { + "name": "tool", + "value": "OpenSSF Scorecard" + } + ], + "rlinks": [ + { + "href": "https://api.securityscorecards.dev/projects/github.com/dfetch-org/dfetch" + } + ] + }, + { + "uuid": "0e57263f-9e36-59a0-a601-9e7e4d9f9993", + "title": "OpenSSF Scorecard CI workflow", + "rlinks": [ + { + "href": ".github/workflows/scorecard.yml" + } + ] + }, + { + "uuid": "1f24319e-2e49-5420-be93-05b1b412d53a", + "title": "SLSA Source Provenance Attestation workflow", + "props": [ + { + "name": "type", + "value": "attestation" + }, + { + "name": "framework", + "value": "SLSA" + } + ], + "rlinks": [ + { + "href": ".github/workflows/source-provenance.yml" + } + ] + }, + { + "uuid": "34c6113f-d968-58e0-91f4-6f5ea8ef3a82", + "title": "Sigstore build provenance and SBOM attestation workflow", + "props": [ + { + "name": "type", + "value": "attestation" + }, + { + "name": "framework", + "value": "Sigstore" + } + ], + "rlinks": [ + { + "href": ".github/workflows/python-publish.yml" + } + ] + }, + { + "uuid": "81831f2f-9ed7-5346-ac54-a64ebef56f71", + "title": "in-toto test-results attestation workflow", + "props": [ + { + "name": "type", + "value": "attestation" + }, + { + "name": "framework", + "value": "in-toto" + } + ], + "rlinks": [ + { + "href": ".github/workflows/test.yml" + } + ] + }, + { + "uuid": "d9097ed2-8e79-5d0d-ac7d-737f5ec67303", + "title": "CodeQL static analysis workflow (C-015)", + "props": [ + { + "name": "type", + "value": "tool" + }, + { + "name": "tool", + "value": "CodeQL" + } + ], + "rlinks": [ + { + "href": ".github/workflows/codeql-analysis.yml" + } + ] + }, + { + "uuid": "26a9d69f-f30d-513d-9489-57af4be48e46", + "title": "Dependency review workflow (C-016)", + "props": [ + { + "name": "type", + "value": "tool" + }, + { + "name": "tool", + "value": "dependency-review" + } + ], + "rlinks": [ + { + "href": ".github/workflows/dependency-review.yml" + } + ] + }, + { + "uuid": "39f27364-2b2a-5933-a99c-8d9edc3fe324", + "title": "dfetch GitHub Releases (SBOM and attestation download)", + "rlinks": [ + { + "href": "https://github.com/dfetch-org/dfetch/releases" + } + ] + }, + { + "uuid": "faa6b6be-8130-5409-8845-b8aeb6660c03", + "title": "How to verify dfetch integrity and attestations", + "rlinks": [ + { + "href": "https://dfetch.readthedocs.io/en/latest/howto/verify-integrity.html" + } + ] } ] } diff --git a/security/tm_controls_data.py b/security/tm_controls_data.py new file mode 100644 index 000000000..737ee4132 --- /dev/null +++ b/security/tm_controls_data.py @@ -0,0 +1,485 @@ +"""Control register data — no pytm dependency. + +All implemented security controls for dfetch, split by threat model scope: + +* ``SC_CONTROLS`` — supply-chain controls (C-009 … C-042) +* ``USAGE_CONTROLS`` — runtime-usage controls (C-001 … C-045) + +This module is intentionally pytm-free so that ``compliance.py`` and other +tooling can import the full control register without requiring pytm to be +installed. The threat-model files (``tm_supply_chain.py``, ``tm_usage.py``) +re-export the same lists via ``CONTROLS = SC_CONTROLS / USAGE_CONTROLS``. +""" + +from dataclasses import dataclass, field + + +@dataclass +class Control: + """An implemented security control.""" + + id: str + name: str + description: str + assets: list[str] = field(default_factory=list) + threats: list[str] = field(default_factory=list) + reference: str = "" + + def to_pytm(self) -> str: + """Return a human-readable label used in traceability comments.""" + return f"[{self.id}] {self.name}" + + +# ── Supply-chain controls (C-009 … C-042) ──────────────────────────────────── + +SC_CONTROLS: list[Control] = [ + Control( + id="C-009", + name="Actions commit-SHA pinning", + assets=["A-01", "A-02"], + threats=["DFT-07"], + reference=".github/workflows/*.yml", + description=( + "All third-party GitHub Actions are pinned to a full commit SHA " + "(``@<40-hex>``), preventing tag-mutable supply-chain substitution." + ), + ), + Control( + id="C-010", + name="OIDC trusted publishing", + assets=["A-05", "A-03"], + threats=["DFT-07"], + reference=".github/workflows/python-publish.yml", + description=( + "PyPI publishes via ``pypa/gh-action-pypi-publish`` with " + "``id-token: write`` and no stored long-lived API token." + ), + ), + Control( + id="C-011", + name="Minimal workflow permissions", + assets=["A-01"], + threats=["DFT-07"], + reference=".github/workflows/*.yml", + description=( + "Each workflow declares only the permissions it requires " + "(default ``contents: read``)." + ), + ), + Control( + id="C-012", + name="persist-credentials: false", + assets=["A-01"], + threats=["DFT-07"], + reference=".github/workflows/*.yml", + description=( + "``persist-credentials: false`` is set on almost all checkout steps across " + "all workflows that run on a runner. " + "Two exceptions are intentional: ``release.yml`` (needs write access to push " + "release assets) and the ``attest-source-governance`` job in " + "``source-provenance.yml`` (the ``slsa_with_provenance`` action requires " + "repository write access). " + "All other checkout steps suppress credential persistence." + ), + ), + Control( + id="C-013", + name="Harden-runner (egress block)", + assets=["A-01", "A-02"], + threats=["DFT-07", "DFT-29"], + reference=".github/workflows/*.yml", + description=( + "``step-security/harden-runner`` is used in every workflow with " + "``egress-policy: block`` and an allowlist of permitted endpoints. " + "All non-allowlisted outbound connections are blocked." + ), + ), + Control( + id="C-015", + name="CodeQL static analysis", + assets=["A-01"], + threats=["DFT-03", "DFT-06"], + reference=".github/workflows/codeql-analysis.yml", + description=( + "CodeQL scans the Python codebase for security vulnerabilities on " + "pushes and pull requests targeting ``main``, and on a weekly cron schedule." + ), + ), + Control( + id="C-016", + name="Dependency review", + assets=["A-07"], + threats=["DFT-10"], + reference=".github/workflows/dependency-review.yml", + description=( + "``actions/dependency-review-action`` checks for known vulnerabilities " + "in newly added dependencies on every pull request." + ), + ), + Control( + id="C-017", + name="bandit security linter", + assets=["A-01"], + threats=["DFT-03", "DFT-06"], + reference="pyproject.toml", + description=( + "``bandit -r dfetch`` runs in CI to detect common Python security issues." + ), + ), + Control( + id="C-021", + name="Sigstore SBOM attestation", + assets=["A-03"], + threats=["DFT-05"], + description=( + "The release pipeline generates CycloneDX SBOM attestations via " + "``actions/attest`` with Sigstore signatures. These attest the software " + "composition (SBOM) of the published packages using predicate type " + "``https://cyclonedx.org/bom``." + ), + ), + Control( + id="C-022", + name="CycloneDX SBOM on PyPI", + assets=["A-03", "A-07"], + threats=["DFT-02"], + description=( + "A CycloneDX SBOM is generated during the build and published " + "alongside the PyPI release, satisfying CRA Article 13 requirements." + ), + ), + Control( + id="C-024", + name="Explicit secret forwarding", + assets=["A-01", "A-05"], + threats=["DFT-07"], + reference=".github/workflows/ci.yml", + description=( + "``ci.yml`` explicitly names only the secrets each child workflow " + "requires (``CODACY_PROJECT_TOKEN`` → ``test.yml``; " + "``GH_DFETCH_ORG_DEPLOY`` → ``docs.yml``); all other repository " + "secrets are not forwarded. ``secrets: inherit`` is deliberately " + "not used so that malicious PR steps cannot exfiltrate unrelated secrets." + ), + ), + Control( + id="C-026", + name="Consumer-side package provenance verification", + assets=["A-03"], + threats=["DFT-17", "DFT-25"], + reference="doc/howto/verify-integrity.rst", + description=( + "Consumers installing dfetch via ``pip install dfetch`` have access to " + "documented procedures to verify the SBOM (CycloneDX) attestation of the " + "PyPI wheel using the GitHub CLI (``gh attestation verify``). " + "Consumers installing binary packages (deb, rpm, pkg, msi) can verify " + "SLSA build provenance, SBOM, and VSA attestations. " + "Consumers working from source can verify SLSA build provenance and in-toto " + "test result attestations on the source archive. " + "Platform-specific instructions for Linux, macOS, and Windows are provided " + "in ``doc/howto/verify-integrity.rst``. " + "This is an interim mitigation pending PEP 740 built-in support in pip. " + "Without this documentation, a compromised PyPI account, namespace-squatting, " + "or dependency-confusion could serve malicious code undetected (DFT-17). " + "An attacker controlling the release pipeline could publish a plausible " + "attestation alongside a backdoored wheel (DFT-25). " + "By providing clear, copy-paste instructions, we enable security-conscious " + "consumers to verify provenance before installation." + ), + ), + Control( + id="C-032", + name="Consumer attestation verification pins to release tag ref", + assets=["A-01", "A-03"], + threats=["DFT-27"], + reference="doc/howto/verify-integrity.rst", + description=( + "All ``gh attestation verify`` commands in the installation guide use " + "``--cert-identity`` pinned to the release workflow at a specific version " + "tag (e.g. ``...python-publish.yml@refs/tags/v`` for pip packages, " + "``...build.yml@refs/tags/v`` for binary installers) combined " + "with ``--cert-oidc-issuer https://token.actions.githubusercontent.com``. " + "This rejects attestations produced by any workflow or branch other than " + "the expected release workflow on an official version tag. " + "A build from an unofficial fork or unprotected branch would produce an " + "attestation with a different cert-identity and fail verification." + ), + ), + Control( + id="C-033", + name="Ref-scoped build cache keys isolate PR and release builds", + assets=["A-08b"], + threats=["DFT-28"], + reference=".github/workflows/build.yml", + description=( + "ccache and clcache keys in ``build.yml`` include ``${{ github.ref_name }}`` " + "so cache entries written by a pull-request build are scoped to the PR's " + "branch name and cannot be restored by a release-tag build. " + "A malicious fork PR step cannot pre-populate a cache slot that the release " + "workflow will restore, because the release tag name is not reachable from " + "the PR's branch ref." + ), + ), + Control( + id="C-037", + name="SLSA Source Provenance Attestation of repository governance controls", + assets=["A-01"], + threats=["DFT-31"], + reference=".github/workflows/source-provenance.yml", + description=( + "Source Provenance Attestations are published via " + "``slsa-framework/source-actions/slsa_with_provenance`` on every push to ``main``. " + "These attestations prove the specific source-level governance controls " + "applied on each commit: branch protection, mandatory code review, and " + "ancestry enforcement (C-038). " + "Predicate type ``https://slsa.dev/source_provenance/v1`` is signed by " + "GitHub Actions via Sigstore and stored in the GitHub Attestation registry. " + "Consumers can verify using ``gh attestation verify`` with " + "``--predicate-type https://slsa.dev/source_provenance/v1`` and " + "``--cert-identity`` pinned to ``source-provenance.yml@refs/heads/main``." + ), + ), + Control( + id="C-038", + name="Ancestry enforcement on dfetch main branch", + assets=["A-01"], + threats=["DFT-33"], + reference=".github/workflows/", + description=( + "GitHub branch-protection rules on the dfetch ``main`` branch prohibit " + "force-pushes, satisfying the SLSA Source Level 2 ancestry-enforcement " + "requirement. The immutable revision lineage of the main branch is " + "preserved: no contributor can rewrite history and orphan a " + "previously-audited commit SHA. Consumers who pin to a dfetch commit SHA " + "can rely on that SHA remaining reachable indefinitely." + ), + ), + Control( + id="C-039", + name="Source build provenance and VSA attestations", + assets=["A-01", "A-02", "A-03"], + threats=["DFT-31", "DFT-25"], + reference="doc/howto/verify-integrity.rst", + description=( + "Every dfetch release ships two complementary Sigstore-signed attestations " + "that together let consumers trace the full source → binary chain. " + "SLSA build provenance (``source-provenance.yml``) on the source archive proves " + "the archive was produced from the official tagged commit by the official CI " + "workflow, recording the exact inputs used at build time. " + "A Verification Summary Attestation (VSA, ``build.yml``) on binary installers " + "records that the source archive was itself attested and verified before the " + "binary was produced, linking source-level trust to the installed package. " + "Both are signed by GitHub Actions via Sigstore and can be verified using " + "``gh attestation verify`` with ``--predicate-type https://slsa.dev/provenance/v1`` " + "or ``--predicate-type https://slsa.dev/verification_summary/v1`` respectively. " + "This substantially mitigates DFT-31 (consumers now have attestations to verify " + "against) and DFT-25 (forged provenance would fail Sigstore verification). " + "The formal SLSA Source Level attestation of governance controls is addressed by C-037." + ), + ), + Control( + id="C-040", + name="Test result attestation on source archive", + assets=["A-01", "A-02"], + threats=["DFT-31"], + reference=".github/workflows/test.yml", + description=( + "The CI test workflow (``test.yml``) generates an in-toto test result " + "attestation (predicate type ``https://in-toto.io/attestation/test-result/v0.1``) " + "for every release and main-branch commit. " + "The attestation proves the full CI test suite ran against the exact source " + "archive and every check passed, before any binary was produced from that source. " + "Consumers can verify it using " + "``gh attestation verify dfetch-source.tar.gz`` with " + "``--predicate-type https://in-toto.io/attestation/test-result/v0.1`` and " + "``--cert-identity`` pinned to ``test.yml`` at the release tag ref. " + "This provides an additional layer of assurance beyond build provenance: " + "not only was the artifact produced from the official commit, but the test " + "suite demonstrably passed on that exact source before any binary was built." + ), + ), + Control( + id="C-041", + name="Winget manifest PRs reviewed by community maintainers", + assets=["A-09"], + threats=["DFT-35"], + reference=".github/workflows/winget-publish.yml", + description=( + "Manifest update PRs submitted to ``microsoft/winget-pkgs`` by " + "``winget-publish.yml`` go through the standard Winget community review " + "process before merging. " + "``microsoft/winget-pkgs`` maintainers verify the publisher identity and " + "inspect manifest changes including installer URLs and hashes. " + "This provides a manual review gate between a fraudulent PR submission " + "and consumer exposure. " + "Residual risk: a reviewer who approves without independently verifying " + "the installer URL origin could merge a fraudulent manifest." + ), + ), + Control( + id="C-042", + name="WINGET_TOKEN scoped to dedicated Winget environment", + assets=["A-10"], + threats=["DFT-34"], + reference=".github/workflows/winget-publish.yml", + description=( + "``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment " + "environment, limiting its exposure: the PAT is only injected into " + "workflows that explicitly reference that environment. " + "Only ``winget-publish.yml`` references the ``winget`` environment, " + "so the PAT is not available to other workflows. " + "Residual risk: unlike PyPI which uses OIDC (A-05, no stored long-lived " + "token), Winget does not support OIDC trusted publishing; the PAT must " + "be stored and rotated manually (DFT-34)." + ), + ), +] + +# ── Runtime-usage controls (C-001 … C-045) ─────────────────────────────────── + +USAGE_CONTROLS: list[Control] = [ + Control( + id="C-001", + name="Path-traversal prevention", + assets=["A-13", "A-20"], + threats=["DFT-03"], + reference="dfetch/util/util.py", + description=( + "``check_no_path_traversal()`` resolves both the candidate path and the " + "destination root via ``os.path.realpath`` (symlink-aware), then rejects " + "any path whose resolved prefix does not start with the resolved root. " + "Applied to every file copy and post-extraction symlink." + ), + ), + Control( + id="C-002", + name="Decompression-bomb protection", + assets=["A-20", "A-13"], + threats=["DFT-09"], + reference="dfetch/vcs/archive.py", + description=( + "Archives are rejected if the uncompressed size exceeds 500 MB or the " + "member count exceeds 10 000." + ), + ), + Control( + id="C-003", + name="Archive symlink validation", + assets=["A-13"], + threats=["DFT-03"], + reference="dfetch/vcs/archive.py", + description=( + "Absolute and escaping (``..``) symlink targets are rejected for both " + "TAR and ZIP. A post-extraction walk validates all symlinks against " + "the manifest root." + ), + ), + Control( + id="C-004", + name="Archive member type checks", + assets=["A-13", "A-20"], + threats=["DFT-03"], + reference="dfetch/vcs/archive.py", + description=( + "TAR and ZIP members of type device file or FIFO are rejected outright." + ), + ), + Control( + id="C-005", + name="Integrity hash verification", + assets=["A-12", "A-13"], + threats=["DFT-01", "DFT-02", "DFT-05", "DFT-30"], + reference="dfetch/vcs/integrity_hash.py", + description=( + "SHA-256, SHA-384, and SHA-512 verified via ``hmac.compare_digest`` " + "(constant-time comparison, resistant to timing attacks). " + "Primary defence against content substitution for archive dependencies. " + "Effectiveness is conditional on the hash field being present." + ), + ), + Control( + id="C-006", + name="Non-interactive VCS", + assets=["A-16", "A-09"], + threats=["DFT-06"], + reference="dfetch/vcs/git.py, dfetch/vcs/svn.py", + description=( + "``GIT_TERMINAL_PROMPT=0``, ``BatchMode=yes`` for Git; " + "``--non-interactive`` for SVN. Credential prompts are suppressed to " + "prevent interactive hijacking in CI." + ), + ), + Control( + id="C-007", + name="Subprocess safety", + assets=["A-22"], + threats=["DFT-06"], + reference="dfetch/util/cmdline.py", + description=( + "All external commands invoked with ``shell=False`` and list-form " + "arguments - no shell-injection vector." + ), + ), + Control( + id="C-008", + name="Manifest input validation", + assets=["A-12"], + threats=["DFT-04", "DFT-08"], + reference="dfetch/manifest/schema.py", + description=( + r'StrictYAML schema with ``SAFE_STR = Regex(r"^[^\x00-\x1F\x7F-\x9F]*$")`` ' + "rejects control characters in all string fields." + ), + ), + Control( + id="C-034", + name="Hash algorithm allowlist (SHA-256/384/512 only)", + assets=["A-12"], + threats=["DFT-30"], + reference="dfetch/vcs/integrity_hash.py", + description=( + "``integrity_hash.py`` accepts only ``sha256:``, ``sha384:``, and " + "``sha512:`` prefixes; any other algorithm prefix is rejected at parse " + "time. MD5 and SHA-1 are not accepted. " + "This directly mitigates DFT-30 (SLSA M2: exploit cryptographic hash " + "collisions) by ensuring that integrity hashes, when present, use " + "algorithms with no known practical collision attacks." + ), + ), + Control( + id="C-036", + name="Persisted-metadata credential redaction", + assets=["A-17", "A-18"], + threats=["DFT-13"], + reference="dfetch/project/metadata.py", + description=( + "``Metadata.dump()`` rebuilds the netloc of every persisted URL from " + "``parsed.hostname`` and ``parsed.port`` via ``urllib.parse.urlsplit`` / " + "``urlunsplit``, dropping any ``user:password@`` userinfo before writing " + "``.dfetch_data.yaml``. The same stripper is applied to each " + "``dependencies[].remote_url`` entry (git submodule, svn:external) so a " + "credential in a nested upstream URL also never reaches disk. The " + "in-memory ``Metadata`` object held by the running command keeps the " + "original URL — only the on-disk representation is redacted, so an " + "in-flight authenticated fetch is unaffected." + ), + ), + Control( + id="C-045", + name="Plaintext transport detection", + assets=["A-09", "A-22"], + threats=["DFT-26"], + reference="dfetch/manifest/project.py, dfetch/project/subproject.py", + description=( + "``plaintext_warning()`` (``dfetch/manifest/project.py``) inspects the " + "resolved remote URL immediately before each VCS command is issued " + "(inside the ``check_for_update`` and ``update`` spinners in " + "``subproject.py``). If the scheme is ``http://``, ``git://``, or " + "``svn://``, a visible warning is emitted naming the redacted URL " + "(credentials stripped from the userinfo component) and recommending " + "``https://`` or ``svn+ssh://``. " + "Detection only — dfetch still proceeds with the plaintext connection; " + "the control raises user awareness but does not enforce scheme selection." + ), + ), +] diff --git a/security/tm_elements.py b/security/tm_elements.py index c1f2713d2..3b09bf7cf 100644 --- a/security/tm_elements.py +++ b/security/tm_elements.py @@ -11,6 +11,8 @@ from pytm import Assumption, Boundary +from security.tm_controls_data import Control # noqa: F401 # re-exported + THREATS_FILE = os.path.join(os.path.dirname(__file__), "threats.json") @@ -33,22 +35,6 @@ class ThreatResponse: target: str = "" -@dataclass -class Control: - """An implemented security control.""" - - id: str - name: str - description: str - assets: list[str] = field(default_factory=list) - threats: list[str] = field(default_factory=list) - reference: str = "" - - def to_pytm(self) -> str: - """Return a human-readable label used in traceability comments.""" - return f"[{self.id}] {self.name}" - - def build_asset_controls_index( controls: list[Control], valid_asset_ids: set[str], diff --git a/security/tm_supply_chain.py b/security/tm_supply_chain.py index 0fac73b24..1482e4c9f 100644 --- a/security/tm_supply_chain.py +++ b/security/tm_supply_chain.py @@ -32,6 +32,7 @@ Process, ) +from security.tm_controls_data import SC_CONTROLS as CONTROLS # noqa: E402 # pylint: disable=wrong-import-position from security.tm_elements import ( # noqa: E402 # pylint: disable=wrong-import-position THREATS_FILE, Control, @@ -707,310 +708,6 @@ def build_model() -> TM: return model -# ── CONTROLS AND GAPS ──────────────────────────────────────────────────────── - -CONTROLS: list[Control] = [ - Control( - id="C-009", - name="Actions commit-SHA pinning", - assets=["A-01", "A-02"], - threats=["DFT-07"], - reference=".github/workflows/*.yml", - description=( - "All third-party GitHub Actions are pinned to a full commit SHA " - "(``@<40-hex>``), preventing tag-mutable supply-chain substitution." - ), - ), - Control( - id="C-010", - name="OIDC trusted publishing", - assets=["A-05", "A-03"], - threats=["DFT-07"], - reference=".github/workflows/python-publish.yml", - description=( - "PyPI publishes via ``pypa/gh-action-pypi-publish`` with " - "``id-token: write`` and no stored long-lived API token." - ), - ), - Control( - id="C-011", - name="Minimal workflow permissions", - assets=["A-01"], - threats=["DFT-07"], - reference=".github/workflows/*.yml", - description=( - "Each workflow declares only the permissions it requires " - "(default ``contents: read``)." - ), - ), - Control( - id="C-012", - name="persist-credentials: false", - assets=["A-01"], - threats=["DFT-07"], - reference=".github/workflows/*.yml", - description=( - "``persist-credentials: false`` is set on almost all checkout steps across " - "all workflows that run on a runner. " - "Two exceptions are intentional: ``release.yml`` (needs write access to push " - "release assets) and the ``attest-source-governance`` job in " - "``source-provenance.yml`` (the ``slsa_with_provenance`` action requires " - "repository write access). " - "All other checkout steps suppress credential persistence." - ), - ), - Control( - id="C-013", - name="Harden-runner (egress block)", - assets=["A-01", "A-02"], - threats=["DFT-07", "DFT-29"], - reference=".github/workflows/*.yml", - description=( - "``step-security/harden-runner`` is used in every workflow with " - "``egress-policy: block`` and an allowlist of permitted endpoints. " - "All non-allowlisted outbound connections are blocked." - ), - ), - Control( - id="C-015", - name="CodeQL static analysis", - assets=["A-01"], - threats=["DFT-03", "DFT-06"], - reference=".github/workflows/codeql-analysis.yml", - description=( - "CodeQL scans the Python codebase for security vulnerabilities on " - "pushes and pull requests targeting ``main``, and on a weekly cron schedule." - ), - ), - Control( - id="C-016", - name="Dependency review", - assets=["A-07"], - threats=["DFT-10"], - reference=".github/workflows/dependency-review.yml", - description=( - "``actions/dependency-review-action`` checks for known vulnerabilities " - "in newly added dependencies on every pull request." - ), - ), - Control( - id="C-017", - name="bandit security linter", - assets=["A-01"], - threats=["DFT-03", "DFT-06"], - reference="pyproject.toml", - description=( - "``bandit -r dfetch`` runs in CI to detect common Python security issues." - ), - ), - Control( - id="C-021", - name="Sigstore SBOM attestation", - assets=["A-03"], - threats=["DFT-05"], - description=( - "The release pipeline generates CycloneDX SBOM attestations via " - "``actions/attest`` with Sigstore signatures. These attest the software " - "composition (SBOM) of the published packages using predicate type " - "``https://cyclonedx.org/bom``." - ), - ), - Control( - id="C-022", - name="CycloneDX SBOM on PyPI", - assets=["A-03", "A-07"], - threats=["DFT-02"], - description=( - "A CycloneDX SBOM is generated during the build and published " - "alongside the PyPI release, satisfying CRA Article 13 requirements." - ), - ), - Control( - id="C-024", - name="Explicit secret forwarding", - assets=["A-01", "A-05"], - threats=["DFT-07"], - description=( - "``ci.yml`` explicitly names only the secrets each child workflow " - "requires (``CODACY_PROJECT_TOKEN`` → ``test.yml``; " - "``GH_DFETCH_ORG_DEPLOY`` → ``docs.yml``); all other repository " - "secrets are not forwarded. ``secrets: inherit`` is deliberately " - "not used so that malicious PR steps cannot exfiltrate unrelated secrets." - ), - ), - Control( - id="C-026", - name="Consumer-side package provenance verification", - assets=["A-03"], - threats=["DFT-17", "DFT-25"], - reference="doc/howto/verify-integrity.rst", - description=( - "Consumers installing dfetch via ``pip install dfetch`` have access to " - "documented procedures to verify the SBOM (CycloneDX) attestation of the " - "PyPI wheel using the GitHub CLI (``gh attestation verify``). " - "Consumers installing binary packages (deb, rpm, pkg, msi) can verify " - "SLSA build provenance, SBOM, and VSA attestations. " - "Consumers working from source can verify SLSA build provenance and in-toto " - "test result attestations on the source archive. " - "Platform-specific instructions for Linux, macOS, and Windows are provided " - "in ``doc/howto/verify-integrity.rst``. " - "This is an interim mitigation pending PEP 740 built-in support in pip. " - "Without this documentation, a compromised PyPI account, namespace-squatting, " - "or dependency-confusion could serve malicious code undetected (DFT-17). " - "An attacker controlling the release pipeline could publish a plausible " - "attestation alongside a backdoored wheel (DFT-25). " - "By providing clear, copy-paste instructions, we enable security-conscious " - "consumers to verify provenance before installation." - ), - ), - Control( - id="C-032", - name="Consumer attestation verification pins to release tag ref", - assets=["A-01", "A-03"], - threats=["DFT-27"], - reference="doc/howto/verify-integrity.rst", - description=( - "All ``gh attestation verify`` commands in the installation guide use " - "``--cert-identity`` pinned to the release workflow at a specific version " - "tag (e.g. ``...python-publish.yml@refs/tags/v`` for pip packages, " - "``...build.yml@refs/tags/v`` for binary installers) combined " - "with ``--cert-oidc-issuer https://token.actions.githubusercontent.com``. " - "This rejects attestations produced by any workflow or branch other than " - "the expected release workflow on an official version tag. " - "A build from an unofficial fork or unprotected branch would produce an " - "attestation with a different cert-identity and fail verification." - ), - ), - Control( - id="C-033", - name="Ref-scoped build cache keys isolate PR and release builds", - assets=["A-08b"], - threats=["DFT-28"], - reference=".github/workflows/build.yml", - description=( - "ccache and clcache keys in ``build.yml`` include ``${{ github.ref_name }}`` " - "so cache entries written by a pull-request build are scoped to the PR's " - "branch name and cannot be restored by a release-tag build. " - "A malicious fork PR step cannot pre-populate a cache slot that the release " - "workflow will restore, because the release tag name is not reachable from " - "the PR's branch ref." - ), - ), - Control( - id="C-037", - name="SLSA Source Provenance Attestation of repository governance controls", - assets=["A-01"], - threats=["DFT-31"], - reference=".github/workflows/source-provenance.yml", - description=( - "Source Provenance Attestations are published via " - "``slsa-framework/source-actions/slsa_with_provenance`` on every push to ``main``. " - "These attestations prove the specific source-level governance controls " - "applied on each commit: branch protection, mandatory code review, and " - "ancestry enforcement (C-038). " - "Predicate type ``https://slsa.dev/source_provenance/v1`` is signed by " - "GitHub Actions via Sigstore and stored in the GitHub Attestation registry. " - "Consumers can verify using ``gh attestation verify`` with " - "``--predicate-type https://slsa.dev/source_provenance/v1`` and " - "``--cert-identity`` pinned to ``source-provenance.yml@refs/heads/main``." - ), - ), - Control( - id="C-038", - name="Ancestry enforcement on dfetch main branch", - assets=["A-01"], - threats=["DFT-33"], - reference=".github/workflows/", - description=( - "GitHub branch-protection rules on the dfetch ``main`` branch prohibit " - "force-pushes, satisfying the SLSA Source Level 2 ancestry-enforcement " - "requirement. The immutable revision lineage of the main branch is " - "preserved: no contributor can rewrite history and orphan a " - "previously-audited commit SHA. Consumers who pin to a dfetch commit SHA " - "can rely on that SHA remaining reachable indefinitely." - ), - ), - Control( - id="C-039", - name="Source build provenance and VSA attestations", - assets=["A-01", "A-02", "A-03"], - threats=["DFT-31", "DFT-25"], - reference="doc/howto/verify-integrity.rst", - description=( - "Every dfetch release ships two complementary Sigstore-signed attestations " - "that together let consumers trace the full source → binary chain. " - "SLSA build provenance (``source-provenance.yml``) on the source archive proves " - "the archive was produced from the official tagged commit by the official CI " - "workflow, recording the exact inputs used at build time. " - "A Verification Summary Attestation (VSA, ``build.yml``) on binary installers " - "records that the source archive was itself attested and verified before the " - "binary was produced, linking source-level trust to the installed package. " - "Both are signed by GitHub Actions via Sigstore and can be verified using " - "``gh attestation verify`` with ``--predicate-type https://slsa.dev/provenance/v1`` " - "or ``--predicate-type https://slsa.dev/verification_summary/v1`` respectively. " - "This substantially mitigates DFT-31 (consumers now have attestations to verify " - "against) and DFT-25 (forged provenance would fail Sigstore verification). " - "The formal SLSA Source Level attestation of governance controls is addressed by C-037." - ), - ), - Control( - id="C-040", - name="Test result attestation on source archive", - assets=["A-01", "A-02"], - threats=["DFT-31"], - reference=".github/workflows/test.yml", - description=( - "The CI test workflow (``test.yml``) generates an in-toto test result " - "attestation (predicate type ``https://in-toto.io/attestation/test-result/v0.1``) " - "for every release and main-branch commit. " - "The attestation proves the full CI test suite ran against the exact source " - "archive and every check passed, before any binary was produced from that source. " - "Consumers can verify it using " - "``gh attestation verify dfetch-source.tar.gz`` with " - "``--predicate-type https://in-toto.io/attestation/test-result/v0.1`` and " - "``--cert-identity`` pinned to ``test.yml`` at the release tag ref. " - "This provides an additional layer of assurance beyond build provenance: " - "not only was the artifact produced from the official commit, but the test " - "suite demonstrably passed on that exact source before any binary was built." - ), - ), - Control( - id="C-041", - name="Winget manifest PRs reviewed by community maintainers", - assets=["A-09"], - threats=["DFT-35"], - reference=".github/workflows/winget-publish.yml", - description=( - "Manifest update PRs submitted to ``microsoft/winget-pkgs`` by " - "``winget-publish.yml`` go through the standard Winget community review " - "process before merging. " - "``microsoft/winget-pkgs`` maintainers verify the publisher identity and " - "inspect manifest changes including installer URLs and hashes. " - "This provides a manual review gate between a fraudulent PR submission " - "and consumer exposure. " - "Residual risk: a reviewer who approves without independently verifying " - "the installer URL origin could merge a fraudulent manifest." - ), - ), - Control( - id="C-042", - name="WINGET_TOKEN scoped to dedicated Winget environment", - assets=["A-10"], - threats=["DFT-34"], - reference=".github/workflows/winget-publish.yml", - description=( - "``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment " - "environment, limiting its exposure: the PAT is only injected into " - "workflows that explicitly reference that environment. " - "Only ``winget-publish.yml`` references the ``winget`` environment, " - "so the PAT is not available to other workflows. " - "Residual risk: unlike PyPI which uses OIDC (A-05, no stored long-lived " - "token), Winget does not support OIDC trusted publishing; the PAT must " - "be stored and rotated manually (DFT-34)." - ), - ), -] - _SUPPLY_CHAIN_ASSET_IDS = { "A-01", "A-01b", diff --git a/security/tm_usage.py b/security/tm_usage.py index d5142c57b..53f884ed0 100644 --- a/security/tm_usage.py +++ b/security/tm_usage.py @@ -44,6 +44,7 @@ Process, ) +from security.tm_controls_data import USAGE_CONTROLS as CONTROLS # noqa: E402 # pylint: disable=wrong-import-position from security.tm_elements import ( # noqa: E402 # pylint: disable=wrong-import-position THREATS_FILE, Control, @@ -869,155 +870,6 @@ def build_model() -> TM: return model -# ── CONTROLS AND GAPS ──────────────────────────────────────────────────────── - -CONTROLS: list[Control] = [ - Control( - id="C-001", - name="Path-traversal prevention", - assets=["A-13", "A-20"], - threats=["DFT-03"], - reference="dfetch/util/util.py", - description=( - "``check_no_path_traversal()`` resolves both the candidate path and the " - "destination root via ``os.path.realpath`` (symlink-aware), then rejects " - "any path whose resolved prefix does not start with the resolved root. " - "Applied to every file copy and post-extraction symlink." - ), - ), - Control( - id="C-002", - name="Decompression-bomb protection", - assets=["A-20", "A-13"], - threats=["DFT-09"], - reference="dfetch/vcs/archive.py", - description=( - "Archives are rejected if the uncompressed size exceeds 500 MB or the " - "member count exceeds 10 000." - ), - ), - Control( - id="C-003", - name="Archive symlink validation", - assets=["A-13"], - threats=["DFT-03"], - reference="dfetch/vcs/archive.py", - description=( - "Absolute and escaping (``..``) symlink targets are rejected for both " - "TAR and ZIP. A post-extraction walk validates all symlinks against " - "the manifest root." - ), - ), - Control( - id="C-004", - name="Archive member type checks", - assets=["A-13", "A-20"], - threats=["DFT-03"], - reference="dfetch/vcs/archive.py", - description=( - "TAR and ZIP members of type device file or FIFO are rejected outright." - ), - ), - Control( - id="C-005", - name="Integrity hash verification", - assets=["A-12", "A-13"], - threats=["DFT-01", "DFT-02", "DFT-05", "DFT-30"], - reference="dfetch/vcs/integrity_hash.py", - description=( - "SHA-256, SHA-384, and SHA-512 verified via ``hmac.compare_digest`` " - "(constant-time comparison, resistant to timing attacks). " - "Primary defence against content substitution for archive dependencies. " - "Effectiveness is conditional on the hash field being present." - ), - ), - Control( - id="C-006", - name="Non-interactive VCS", - assets=["A-16", "A-09"], - threats=["DFT-06"], - reference="dfetch/vcs/git.py, dfetch/vcs/svn.py", - description=( - "``GIT_TERMINAL_PROMPT=0``, ``BatchMode=yes`` for Git; " - "``--non-interactive`` for SVN. Credential prompts are suppressed to " - "prevent interactive hijacking in CI." - ), - ), - Control( - id="C-007", - name="Subprocess safety", - assets=["A-22"], - threats=["DFT-06"], - reference="dfetch/util/cmdline.py", - description=( - "All external commands invoked with ``shell=False`` and list-form " - "arguments - no shell-injection vector." - ), - ), - Control( - id="C-008", - name="Manifest input validation", - assets=["A-12"], - threats=["DFT-04", "DFT-08"], - reference="dfetch/manifest/schema.py", - description=( - r'StrictYAML schema with ``SAFE_STR = Regex(r"^[^\x00-\x1F\x7F-\x9F]*$")`` ' - "rejects control characters in all string fields." - ), - ), - Control( - id="C-045", - name="Plaintext transport detection", - assets=["A-09", "A-22"], - threats=["DFT-26"], - reference="dfetch/manifest/project.py, dfetch/project/subproject.py", - description=( - "``plaintext_warning()`` (``dfetch/manifest/project.py``) inspects the " - "resolved remote URL immediately before each VCS command is issued " - "(inside the ``check_for_update`` and ``update`` spinners in " - "``subproject.py``). If the scheme is ``http://``, ``git://``, or " - "``svn://``, a visible warning is emitted naming the redacted URL " - "(credentials stripped from the userinfo component) and recommending " - "``https://`` or ``svn+ssh://``. " - "Detection only — dfetch still proceeds with the plaintext connection; " - "the control raises user awareness but does not enforce scheme selection." - ), - ), - Control( - id="C-034", - name="Hash algorithm allowlist (SHA-256/384/512 only)", - assets=["A-12"], - threats=["DFT-30"], - reference="dfetch/vcs/integrity_hash.py", - description=( - "``integrity_hash.py`` accepts only ``sha256:``, ``sha384:``, and " - "``sha512:`` prefixes; any other algorithm prefix is rejected at parse " - "time. MD5 and SHA-1 are not accepted. " - "This directly mitigates DFT-30 (SLSA M2: exploit cryptographic hash " - "collisions) by ensuring that integrity hashes, when present, use " - "algorithms with no known practical collision attacks." - ), - ), - Control( - id="C-036", - name="Persisted-metadata credential redaction", - assets=["A-17", "A-18"], - threats=["DFT-13"], - reference="dfetch/project/metadata.py", - description=( - "``Metadata.dump()`` rebuilds the netloc of every persisted URL from " - "``parsed.hostname`` and ``parsed.port`` via ``urllib.parse.urlsplit`` / " - "``urlunsplit``, dropping any ``user:password@`` userinfo before writing " - "``.dfetch_data.yaml``. The same stripper is applied to each " - "``dependencies[].remote_url`` entry (git submodule, svn:external) so a " - "credential in a nested upstream URL also never reaches disk. The " - "in-memory ``Metadata`` object held by the running command keeps the " - "original URL — only the on-disk representation is redacted, so an " - "in-flight authenticated fetch is unaffected." - ), - ), -] - RESPONSES: list[ThreatResponse] = [ ThreatResponse( "DFT-01",