diff --git a/.claude/settings.json b/.claude/settings.json
index 530d55384..1bc349884 100644
--- a/.claude/settings.json
+++ b/.claude/settings.json
@@ -25,7 +25,9 @@
"Bash(make -C doc latexpdf)",
"Bash(make -C doc clean)",
"Bash(dfetch add:*)",
- "Bash(dfetch update:*)"
+ "Bash(dfetch update:*)",
+ "Bash(python -m security.tm_supply_chain:*)",
+ "Bash(python -m security.tm_usage:*)"
],
"additionalDirectories": [
"/workspaces/dfetch/.claude"
diff --git a/doc/explanation/threat_model_supply_chain.rst b/doc/explanation/threat_model_supply_chain.rst
index dad2e28d9..598a9b472 100644
--- a/doc/explanation/threat_model_supply_chain.rst
+++ b/doc/explanation/threat_model_supply_chain.rst
@@ -1,3 +1,6 @@
+dfetch Supply Chain
+===================
+
.. ============================================================
.. Auto-generated file — do not edit manually.
.. Regenerate with (see security/README.md for exact commands):
@@ -28,27 +31,384 @@ Assumptions
* - CI runner posture
- GitHub Actions environments inherit the security posture of the GitHub-hosted runner. Ephemeral runner isolation is provided by GitHub.
- * - Harden-runner in block mode
- - The ``harden-runner`` egress policy is set to ``block`` with an allowlist of permitted endpoints. Outbound network connections from CI runners are blocked unless explicitly permitted.
-
- * - Build deps without hash pinning
- - dfetch's own build and dev dependencies are installed without ``--require-hashes``, so a compromised PyPI mirror can substitute malicious build tools.
-
- * - Attacker: Network-adjacent
- - Positioned on the same network segment as a CI runner or developer workstation - shared cloud tenant, BGP hijack, compromised DNS resolver, or corporate proxy. Can intercept and modify unencrypted traffic (http://, svn://) and inject HTTP redirects. Cannot break correctly implemented TLS or SSH.
-
- * - Attacker: Compromised upstream
- - A dependency maintainer account taken over via phishing, credential stuffing, or MFA bypass - or a maintainer acting maliciously. Delivers attacker-controlled content over an authenticated channel; transport security provides no protection. Mitigated only by commit-SHA pinning and human review before accepting any update.
-
- * - Attacker: Compromised registry or CDN
- - Holds write access to a public package registry (PyPI) or an archive CDN node, or is BGP-adjacent to one. Serves malicious content under a valid TLS certificate - transport integrity does not detect server-side substitution. Only cryptographic content hashes or signed attestations provide a defence.
-
- * - Attacker: Local filesystem
- - Holds write access to the developer workstation or CI runner working tree - gained via a compromised dev dependency, malicious post-install hook, or lateral movement. Can tamper with ``.dfetch_data.yaml``, patch files, and vendored source after dfetch writes them to disk.
-
- * - Attacker: Malicious manifest contributor
- - A repository contributor who introduces a malicious ``dfetch.yaml`` change: redirecting a dep to an attacker-controlled URL, pointing ``dst:`` at a sensitive path, or embedding a credential-bearing URL. dfetch is not the control point for this threat; code review is the intended mitigating boundary.
+.. raw:: html
+
+
+
+Data Flow Diagram
+-----------------
+
+.. uml::
+
+ @startdot
+ digraph tm {
+ graph [
+ fontname = Arial;
+ fontsize = 14;
+ ]
+ node [
+ fontname = Arial;
+ fontsize = 14;
+ rankdir = lr;
+ ]
+ edge [
+ shape = none;
+ arrowtail = onormal;
+ fontname = Arial;
+ fontsize = 12;
+ ]
+ labelloc = "t";
+ fontsize = 20;
+ nodesep = 1;
+
+ subgraph cluster_boundary_ConsumerEnvironment_88f2d9c06f {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <Consumer\nEnvironment>;
+ ]
+
+ actor_ConsumerEndUser_f8af758679 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "Consumer / End\nUser";
+ margin = 0.02;
+ ]
+
+ }
+
+ subgraph cluster_boundary_GitHubPlatform_579e9aae81 {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <GitHub Platform>;
+ ]
+
+ externalentity_AGitHubRepositorymainprotected_2c440ebe53 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-01: GitHub\nRepository (main /\nprotected)";
+ margin = 0.02;
+ ]
+
+ externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-01b: GitHub\nRepository\n(feature branches\n/ PRs)";
+ margin = 0.02;
+ ]
+
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-02: GitHub\nActions\nInfrastructure";
+ margin = 0.02;
+ ]
+
+ process_ReleaseGateCodeReview_9345ab4c19 [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "Release Gate /\nCode Review";
+ margin = 0.02;
+ ]
+
+ process_GitHubActionsWorkflow_86e4604564 [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "GitHub Actions\nWorkflow";
+ margin = 0.02;
+ ]
+
+ process_PythonBuildwheelsdist_b2e5892d06 [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "Python Build\n(wheel / sdist)";
+ margin = 0.02;
+ ]
+
+ datastore_AdfetchBuildDevDependencies_990b886585 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-07: dfetch Build\n/ Dev Dependencies";
+ ]
+
+ datastore_AbGitHubActionsBuildCache_9df04f8dae [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-08b: GitHub\nActions Build\nCache";
+ ]
+
+ }
+
+ subgraph cluster_boundary_LocalDeveloperEnvironment_acf3059e70 {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <Local Developer\nEnvironment>;
+ ]
+
+ actor_DeveloperContributor_d2006ce1bb [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "Developer /\nContributor";
+ margin = 0.02;
+ ]
+
+ }
+
+ subgraph cluster_boundary_PyPITestPyPI_f2eb7a3ff7 {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <PyPI / TestPyPI>;
+ ]
+
+ externalentity_APyPITestPyPI_c6f87088c2 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-03: PyPI /\nTestPyPI";
+ margin = 0.02;
+ ]
+
+ }
+
+ actor_DeveloperContributor_d2006ce1bb -> externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-11: Push\ncommits / open PR";
+ ]
+
+ externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 -> process_ReleaseGateCodeReview_9345ab4c19 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-22: PR enters\ncode review";
+ ]
+
+ externalentity_AGitHubRepositorymainprotected_2c440ebe53 -> process_GitHubActionsWorkflow_86e4604564 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-12: Main branch\nworkflows drive CI\nexecution";
+ ]
+
+ externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 -> externalentity_AGitHubActionsInfrastructure_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-13a: PR CI\ncheckout";
+ ]
+
+ externalentity_AGitHubRepositorymainprotected_2c440ebe53 -> externalentity_AGitHubActionsInfrastructure_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-13b: Release CI\ncheckout";
+ ]
+
+ datastore_AbGitHubActionsBuildCache_9df04f8dae -> externalentity_AGitHubActionsInfrastructure_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-14: CI cache\nrestore";
+ ]
+
+ process_GitHubActionsWorkflow_86e4604564 -> process_PythonBuildwheelsdist_b2e5892d06 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-15: Workflow\ntriggers build\nstep";
+ ]
+
+ process_PythonBuildwheelsdist_b2e5892d06 -> externalentity_AGitHubActionsInfrastructure_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-15b: Built\nwheel/sdist\nartifacts";
+ ]
+
+ externalentity_APyPITestPyPI_c6f87088c2 -> datastore_AdfetchBuildDevDependencies_990b886585 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-16: CI fetches\nbuild/dev deps\nfrom PyPI";
+ ]
+
+ datastore_AdfetchBuildDevDependencies_990b886585 -> process_PythonBuildwheelsdist_b2e5892d06 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-17: Build tools\nconsumed by build\nstep";
+ ]
+
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> datastore_AbGitHubActionsBuildCache_9df04f8dae [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-18: CI cache\nwrite";
+ ]
+
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AGitHubRepositorymainprotected_2c440ebe53 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-19: CI write-\nback (SARIF /\nartifacts)";
+ ]
+
+ process_ReleaseGateCodeReview_9345ab4c19 -> externalentity_AGitHubRepositorymainprotected_2c440ebe53 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-23: Approved\nmerge to main";
+ ]
+
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_APyPITestPyPI_c6f87088c2 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-24: Publish\nwheel to PyPI\n(OIDC)";
+ ]
+
+ actor_ConsumerEndUser_f8af758679 -> externalentity_APyPITestPyPI_c6f87088c2 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-25: pip install\ndfetch";
+ ]
+
+ externalentity_APyPITestPyPI_c6f87088c2 -> actor_ConsumerEndUser_f8af758679 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-26: Consumer\ndownloads dfetch\nfrom PyPI";
+ ]
+
+ }
+ @enddot
+
+.. raw:: html
+
+
+
+
+
+.. raw:: html
+
+
+
+Sequence Diagram
+----------------
+
+.. uml::
+
+ @startuml
+ skinparam defaultFontSize 16
+ actor actor_DeveloperContributor_d2006ce1bb as "Developer /\nContributor"
+ actor actor_ConsumerEndUser_f8af758679 as "Consumer /\nEnd User"
+ entity externalentity_AGitHubRepositorymainprotected_2c440ebe53 as "A-01: GitHub\nRepository\n(main /\nprotected)"
+ entity externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 as "A-01b:\nGitHub\nRepository\n(feature\nbranches /\nPRs)"
+ entity externalentity_AGitHubActionsInfrastructure_c76a0a7067 as "A-02: GitHub\nActions\nInfrastructure"
+ entity externalentity_APyPITestPyPI_c6f87088c2 as "A-03: PyPI /\nTestPyPI"
+ entity process_ReleaseGateCodeReview_9345ab4c19 as "Release Gate\n/ Code\nReview"
+ entity process_GitHubActionsWorkflow_86e4604564 as "GitHub\nActions\nWorkflow"
+ entity process_PythonBuildwheelsdist_b2e5892d06 as "Python Build\n(wheel /\nsdist)"
+ database datastore_AdfetchBuildDevDependencies_990b886585 as "A-07: dfetch\nBuild / Dev\nDependencies"
+ database datastore_AbGitHubActionsBuildCache_9df04f8dae as "A-08b:\nGitHub\nActions\nBuild Cache"
+
+ actor_DeveloperContributor_d2006ce1bb -> externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72: DF-11: Push commits / open PR
+ externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 -> process_ReleaseGateCodeReview_9345ab4c19: DF-22: PR enters code review
+ externalentity_AGitHubRepositorymainprotected_2c440ebe53 -> process_GitHubActionsWorkflow_86e4604564: DF-12: Main branch workflows drive CI execution
+ externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 -> externalentity_AGitHubActionsInfrastructure_c76a0a7067: DF-13a: PR CI checkout
+ externalentity_AGitHubRepositorymainprotected_2c440ebe53 -> externalentity_AGitHubActionsInfrastructure_c76a0a7067: DF-13b: Release CI checkout
+ datastore_AbGitHubActionsBuildCache_9df04f8dae -> externalentity_AGitHubActionsInfrastructure_c76a0a7067: DF-14: CI cache restore
+ process_GitHubActionsWorkflow_86e4604564 -> process_PythonBuildwheelsdist_b2e5892d06: DF-15: Workflow triggers build step
+ process_PythonBuildwheelsdist_b2e5892d06 -> externalentity_AGitHubActionsInfrastructure_c76a0a7067: DF-15b: Built wheel/sdist artifacts
+ externalentity_APyPITestPyPI_c6f87088c2 -> datastore_AdfetchBuildDevDependencies_990b886585: DF-16: CI fetches build/dev deps from PyPI
+ datastore_AdfetchBuildDevDependencies_990b886585 -> process_PythonBuildwheelsdist_b2e5892d06: DF-17: Build tools consumed by build step
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> datastore_AbGitHubActionsBuildCache_9df04f8dae: DF-18: CI cache write
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AGitHubRepositorymainprotected_2c440ebe53: DF-19: CI write-back (SARIF / artifacts)
+ process_ReleaseGateCodeReview_9345ab4c19 -> externalentity_AGitHubRepositorymainprotected_2c440ebe53: DF-23: Approved merge to main
+ externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_APyPITestPyPI_c6f87088c2: DF-24: Publish wheel to PyPI (OIDC)
+ actor_ConsumerEndUser_f8af758679 -> externalentity_APyPITestPyPI_c6f87088c2: DF-25: pip install dfetch
+ externalentity_APyPITestPyPI_c6f87088c2 -> actor_ConsumerEndUser_f8af758679: DF-26: Consumer downloads dfetch from PyPI
+ @enduml
+
+.. raw:: html
+
+
+
+
Dataflows
---------
@@ -62,39 +422,84 @@ Dataflows
- To
- Protocol
- * - DF-11: Submit pull request
- - Contributor / Attacker
- - A-01: GitHub Repository
+ * - DF-11: Push commits / open PR
+ - Developer / Contributor
+ - A-01b: GitHub Repository (feature branches / PRs)
- HTTPS
- * - DF-14: pip install dfetch
- - Consumer / End User
- - A-03: PyPI / TestPyPI
- - HTTPS
+ * - DF-22: PR enters code review
+ - A-01b: GitHub Repository (feature branches / PRs)
+ - Release Gate / Code Review
+ -
+
+ * - DF-12: Main branch workflows drive CI execution
+ - A-01: GitHub Repository (main / protected)
+ - GitHub Actions Workflow
+ -
- * - DF-12: CI checkout and build
- - A-01: GitHub Repository
+ * - DF-13a: PR CI checkout
+ - A-01b: GitHub Repository (feature branches / PRs)
- A-02: GitHub Actions Infrastructure
- -
+ -
- * - DF-13: Publish to PyPI (OIDC)
+ * - DF-13b: Release CI checkout
+ - A-01: GitHub Repository (main / protected)
- A-02: GitHub Actions Infrastructure
+ -
+
+ * - DF-14: CI cache restore
+ - A-08b: GitHub Actions Build Cache
+ - A-02: GitHub Actions Infrastructure
+ - HTTPS
+
+ * - DF-15: Workflow triggers build step
+ - GitHub Actions Workflow
+ - Python Build (wheel / sdist)
+ -
+
+ * - DF-15b: Built wheel/sdist artifacts
+ - Python Build (wheel / sdist)
+ - A-02: GitHub Actions Infrastructure
+ -
+
+ * - DF-16: CI fetches build/dev deps from PyPI
- A-03: PyPI / TestPyPI
+ - A-07: dfetch Build / Dev Dependencies
- HTTPS
+ * - DF-17: Build tools consumed by build step
+ - A-07: dfetch Build / Dev Dependencies
+ - Python Build (wheel / sdist)
+ -
+
* - DF-18: CI cache write
- A-02: GitHub Actions Infrastructure
- A-08b: GitHub Actions Build Cache
- HTTPS
- * - DF-19: CI cache restore
- - A-08b: GitHub Actions Build Cache
+ * - DF-19: CI write-back (SARIF / artifacts)
- A-02: GitHub Actions Infrastructure
+ - A-01: GitHub Repository (main / protected)
- HTTPS
- * - DF-17: CI write-back (SARIF / artifacts / cache)
+ * - DF-23: Approved merge to main
+ - Release Gate / Code Review
+ - A-01: GitHub Repository (main / protected)
+ -
+
+ * - DF-24: Publish wheel to PyPI (OIDC)
- A-02: GitHub Actions Infrastructure
- - A-01: GitHub Repository
+ - A-03: PyPI / TestPyPI
+ - HTTPS
+
+ * - DF-25: pip install dfetch
+ - Consumer / End User
+ - A-03: PyPI / TestPyPI
+ - HTTPS
+
+ * - DF-26: Consumer downloads dfetch from PyPI
+ - A-03: PyPI / TestPyPI
+ - Consumer / End User
- HTTPS
@@ -124,11 +529,8 @@ Actors
* - Name
- Description
- * - Developer
- - dfetch project contributor: writes code, reviews PRs, cuts releases. Trusted at workstation time; responsible for correct branch-protection and release workflow configuration.
-
- * - Contributor / Attacker
- - External contributor submitting pull requests, or an adversary attempting supply-chain manipulation (malicious PR, action-poisoning, or MITM on CI network traffic). Code review, branch protection, and SHA-pinned Actions are the primary controls at this boundary.
+ * - Developer / Contributor
+ - Anyone who writes code for dfetch: core maintainers who push directly and cut releases, and external contributors who submit pull requests. Maintainers are trusted at workstation time and are responsible for correct branch-protection and release workflow configuration. External contributors are untrusted until their PR passes code review and CI.
* - Consumer / End User
- Installs dfetch from PyPI (``pip install dfetch``) or from binary installer, then invokes it on a developer workstation or in a CI pipeline. Can verify five complementary attestation types using ``gh attestation verify`` as documented in the release-integrity guide (see C-026, C-037, C-039, C-040): SBOM attestation on the PyPI wheel; SBOM, SLSA build provenance, and VSA on binary installers; SLSA build provenance, in-toto test result attestation, and SLSA Source Provenance Attestation on the source archive and main-branch commits.
@@ -147,15 +549,15 @@ Boundaries
* - Local Developer Environment
- Developer workstation or local CI runner. Assumed trusted at invocation time. Hosts the manifest (``dfetch.yaml``), vendor directory, dependency metadata (``.dfetch_data.yaml``), and patch files.
- * - GitHub Actions Infrastructure
- - Microsoft-operated ephemeral runners executing the CI/CD workflows. Egress traffic is blocked (``harden-runner`` with ``egress-policy: block``) with an allowlist of permitted endpoints; ``ci.yml`` forwards only explicitly named secrets to child workflows (``CODACY_PROJECT_TOKEN`` to ``test.yml``, ``GH_DFETCH_ORG_DEPLOY`` to ``docs.yml``).
+ * - Consumer Environment
+ - End-user workstation or downstream CI pipeline where dfetch is installed and invoked. Distinct from the developer environment: no source checkout, no signing keys, no deploy access. The consumer is trusted at invocation time but has no special relationship with the dfetch release infrastructure.
+
+ * - GitHub Platform
+ - GitHub-hosted infrastructure: repository, CI/CD runners, Actions workflows, build cache, and code-scanning results. Egress traffic on runners is blocked (``harden-runner`` with ``egress-policy: block``) with an allowlist of permitted endpoints; ``ci.yml`` forwards only explicitly named secrets to child workflows (``CODACY_PROJECT_TOKEN`` to ``test.yml``, ``GH_DFETCH_ORG_DEPLOY`` to ``docs.yml``).
* - PyPI / TestPyPI
- Python Package Index and its staging registry. dfetch publishes via OIDC trusted publishing - no long-lived API token stored.
- * - Internet
- - Public internet - upstream package registries, PyPI, GitHub, CDN nodes, and other external endpoints reachable by the CI/CD infrastructure.
-
Assets
------
@@ -168,8 +570,12 @@ Assets
- Description
- Type
- * - A-01: GitHub Repository
- - Source code, PRs, releases, and workflow definitions. GitHub Actions workflows (``.github/workflows/``) with ``contents:write`` permission can modify repository state and trigger releases.
+ * - A-01: GitHub Repository (main / protected)
+ - The protected ``main`` branch: force-push disabled, merges require passing CI and at least one approving review. Contains the authoritative workflow definitions (``.github/workflows/``), release tags, and published release assets. Workflow files on main are what GitHub Actions actually executes — a PR cannot override them for its own CI run. ``contents:write`` permission allows CI to upload SARIF results and release assets.
+ - ExternalEntity
+
+ * - A-01b: GitHub Repository (feature branches / PRs)
+ - Unprotected feature branches and fork PRs: no mandatory review, no CI-gate requirement to push. Any authenticated GitHub user can open a PR modifying ``.github/workflows/`` files; those changes are reviewed before merging to main but execute with restricted permissions during CI (no access to production secrets). A malicious PR modifying workflow files could attempt to exfiltrate secrets during the PR CI run, mitigated by ``ci.yml`` secret scoping (C-024) and harden-runner egress block (C-013).
- ExternalEntity
* - A-02: GitHub Actions Infrastructure
@@ -177,7 +583,7 @@ Assets
- ExternalEntity
* - A-03: PyPI / TestPyPI
- - Python Package Index. dfetch is published via OIDC trusted publishing (no long-lived API token). Account takeover or registry compromise would affect every consumer installing dfetch.
+ - Python Package Index - both the registry service and the published dfetch wheel/sdist (https://pypi.org/project/dfetch/). Published via OIDC trusted publishing; no long-lived API token stored. A machine-readable CycloneDX SBOM is generated during the build and published alongside the release. Account takeover, registry compromise, or namespace-squatting would affect every consumer installing dfetch.
- ExternalEntity
* - Release Gate / Code Review
@@ -192,22 +598,10 @@ Assets
- Runs ``python -m build`` to produce wheel and sdist. Build deps (setuptools, build, fpm, gem) fetched from PyPI/RubyGems without hash pinning. SLSA provenance attestations are generated by the release workflow.
- Process
- * - A-04: dfetch PyPI Package
- - Published wheel and sdist on PyPI (https://pypi.org/project/dfetch/). Published via OIDC trusted publishing - no long-lived API token stored. A machine-readable CycloneDX SBOM is generated during the build and published alongside the release. Compromise of the PyPI account or registry affects every consumer.
- - Datastore
-
- * - A-06: OpenSSF Scorecard Results
- - Weekly OSSF Scorecard SARIF results uploaded to GitHub Code Scanning. Covers: branch-protection, CI-tests, code-review, maintained, packaging, pinned-dependencies, SAST, signed-releases, token-permissions, vulnerabilities, dangerous-workflow, binary-artifacts, fuzzing, license, CII-best-practices, security-policy, webhooks. Suppression or forgery hides supply-chain regressions.
- - Datastore
-
* - A-07: dfetch Build / Dev Dependencies
- Python packages installed during CI: setuptools, build, pylint, bandit, mypy, pytest, etc. Ruby gem ``fpm`` for platform builds. Installed via ``pip install .`` and ``pip install --upgrade pip build`` without ``--require-hashes`` - a compromised PyPI mirror or BGP hijack can substitute malicious build tools. ``gem install fpm`` and ``choco install svn/zig`` are also not hash-verified.
- Datastore
- * - A-08: GitHub Actions Workflows
- - ``.github/workflows/*.yml`` - CI/CD configuration checked into the repository. 11 workflows: ci, build, run, test, docs, release, python-publish, dependency-review, codeql-analysis, scorecard, devcontainer. A malicious PR that modifies workflows can exfiltrate secrets or publish a backdoored release. Mitigated by: SHA-pinned actions, persist-credentials:false, minimal permissions.
- - Datastore
-
* - A-08b: GitHub Actions Build Cache
- GitHub Actions cache entries written and restored across pipeline runs. Used to speed up dependency installation (pip, gem) and incremental builds. Cache-poisoning from forked PRs (DFT-28, SLSA E6: poison the build cache) is mitigated by ref-scoped cache keys: build.yml includes ``${{ github.ref_name }}`` in both ``key`` and ``restore-keys`` (C-033), which isolates PR and release caches per branch so a fork cannot write into the release cache namespace.
- Datastore
@@ -259,12 +653,6 @@ Controls
- Information Disclosure, Tampering
- DFT-07, DFT-29
- Mitigates: ``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. ``.github/workflows/*.yml``
- * - C-014
- - OpenSSF Scorecard
- - Low
- - Tampering
- - DFT-07, DFT-10
- - Mitigates: Weekly OSSF Scorecard analysis uploaded to GitHub Code Scanning covers the full set of OpenSSF Scorecard checks. ``.github/workflows/scorecard.yml``
* - C-015
- CodeQL static analysis
- Medium
@@ -370,3 +758,4 @@ Gaps
- Spoofing, Elevation of Privilege
- DFT-11
- Affects: No hardware-token (FIDO2/WebAuthn) MFA or mandatory second-approver sign-off is required for accounts with merge or release-trigger rights. A compromised maintainer account - via phishing, credential stuffing, or SMS-TOTP bypass - can merge a backdoored PR and trigger the release workflow without any automated block. Enforce FIDO2 MFA on all accounts with merge rights and add a required reviewer to the ``pypi`` deployment environment.
+
diff --git a/doc/explanation/threat_model_usage.rst b/doc/explanation/threat_model_usage.rst
index 2341ee3c8..1033d9c2c 100644
--- a/doc/explanation/threat_model_usage.rst
+++ b/doc/explanation/threat_model_usage.rst
@@ -1,3 +1,6 @@
+dfetch Runtime Usage
+====================
+
.. ============================================================
.. Auto-generated file — do not edit manually.
.. Regenerate with (see security/README.md for exact commands):
@@ -184,6 +187,36 @@ Dataflows
- A-21: Audit / Check Reports
-
+ * - DF-22: Copy extracted content to vendor directory
+ - A-20: Local VCS Cache (temp)
+ - A-22: dfetch Process
+ -
+
+ * - DF-18: Read integrity hash for archive verification
+ - A-14: Integrity Hash Record
+ - A-22: dfetch Process
+ -
+
+ * - DF-18b: Write computed hash to manifest (dfetch freeze)
+ - A-22: dfetch Process
+ - A-14: Integrity Hash Record
+ -
+
+ * - DF-20: Author / maintain dfetch.yaml
+ - Developer
+ - A-12: dfetch Manifest
+ -
+
+ * - DF-19: VCS server publishes source attestation (not consumed by dfetch)
+ - A-09: Remote VCS Server
+ - A-23: Upstream Source Attestation (VSA)
+ -
+
+ * - DF-21: Create / maintain patch files
+ - Developer
+ - A-19: Patch Files
+ -
+
Data Dictionary
---------------
@@ -321,6 +354,457 @@ Assets
+Data Flow Diagram
+-----------------
+
+.. uml::
+
+ @startdot
+ digraph tm {
+ graph [
+ fontname = Arial;
+ fontsize = 14;
+ ]
+ node [
+ fontname = Arial;
+ fontsize = 14;
+ rankdir = lr;
+ ]
+ edge [
+ shape = none;
+ arrowtail = onormal;
+ fontname = Arial;
+ fontsize = 12;
+ ]
+ labelloc = "t";
+ fontsize = 20;
+ nodesep = 1;
+
+ subgraph cluster_boundary_ArchiveContentSpace_b8773cb4e7 {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <Archive Content\nSpace>;
+ ]
+
+ process_ArchiveExtractiontarfilezipfile_da43120000 [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "Archive Extraction\n(tarfile /\nzipfile)";
+ margin = 0.02;
+ ]
+
+ }
+
+ subgraph cluster_boundary_Internet_88f2d9c06f {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <Internet>;
+ ]
+
+
+ }
+
+ subgraph cluster_boundary_LocalDeveloperEnvironment_acf3059e70 {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <Local Developer\nEnvironment>;
+ ]
+
+ actor_Developer_f2eb7a3ff7 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "Developer";
+ margin = 0.02;
+ ]
+
+ externalentity_AConsumerBuildSystem_0291419f72 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-11: Consumer\nBuild System";
+ margin = 0.02;
+ ]
+
+ process_AdfetchProcess_c76a0a7067 [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "A-22: dfetch\nProcess";
+ margin = 0.02;
+ ]
+
+ process_PatchApplicationpatchng_c6f87088c2 [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "Patch Application\n(patch-ng)";
+ margin = 0.02;
+ ]
+
+ datastore_AdfetchManifest_9345ab4c19 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-12: dfetch\nManifest";
+ ]
+
+ datastore_AFetchedSourceCode_86e4604564 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-13: Fetched\nSource Code";
+ ]
+
+ datastore_AIntegrityHashRecord_b2e5892d06 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-14: Integrity\nHash Record";
+ ]
+
+ datastore_ASBOMOutputCycloneDX_990b886585 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-15: SBOM Output\n(CycloneDX)";
+ ]
+
+ datastore_ADependencyMetadata_9df04f8dae [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-18: Dependency\nMetadata";
+ ]
+
+ datastore_APatchFiles_65d0d57a00 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-19: Patch Files";
+ ]
+
+ process_SVNExportsvnexport_7eb89910ee [
+ shape = circle;
+ color = black;
+ fontcolor = black;
+ label = "SVN Export (svn\nexport)";
+ margin = 0.02;
+ ]
+
+ datastore_ALocalVCSCachetemp_ae6e32d09a [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-20: Local VCS\nCache (temp)";
+ ]
+
+ datastore_AAuditCheckReports_d0c0ca0a3b [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-21: Audit /\nCheck Reports";
+ ]
+
+ }
+
+ subgraph cluster_boundary_RemoteVCSInfrastructure_579e9aae81 {
+ graph [
+ fontsize = 10;
+ fontcolor = black;
+ style = dashed;
+ color = firebrick2;
+ label = <Remote VCS\nInfrastructure>;
+ ]
+
+ externalentity_ARemoteVCSServer_d2006ce1bb [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-09: Remote VCS\nServer";
+ margin = 0.02;
+ ]
+
+ externalentity_AArchiveHTTPServer_f8af758679 [
+ shape = square;
+ color = black;
+ fontcolor = black;
+ label = "A-10: Archive HTTP\nServer";
+ margin = 0.02;
+ ]
+
+ datastore_AUpstreamSourceAttestationVSA_2c440ebe53 [
+ shape = cylinder;
+ color = black;
+ fontcolor = black;
+ label = "A-23: Upstream\nSource Attestation\n(VSA)";
+ ]
+
+ }
+
+ actor_Developer_f2eb7a3ff7 -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-01: Invoke\ndfetch command";
+ ]
+
+ datastore_AdfetchManifest_9345ab4c19 -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-02: Read\nmanifest";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> externalentity_ARemoteVCSServer_d2006ce1bb [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-03a: Fetch VCS\ncontent -\nHTTPS/SSH";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> externalentity_ARemoteVCSServer_d2006ce1bb [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-03b: Fetch VCS\ncontent - svn:// /\nhttp://";
+ ]
+
+ externalentity_ARemoteVCSServer_d2006ce1bb -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-04a: VCS\ncontent inbound -\nHTTPS/SSH";
+ ]
+
+ externalentity_ARemoteVCSServer_d2006ce1bb -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-04b: VCS\ncontent inbound -\nsvn:// / http://";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> externalentity_AArchiveHTTPServer_f8af758679 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-05a: Archive\ndownload request -\nHTTPS";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> externalentity_AArchiveHTTPServer_f8af758679 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-05b: Archive\ndownload request -\nHTTP";
+ ]
+
+ externalentity_AArchiveHTTPServer_f8af758679 -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-06a: Archive\nbytes - HTTPS";
+ ]
+
+ externalentity_AArchiveHTTPServer_f8af758679 -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-06b: Archive\nbytes - HTTP\n(plaintext risk)";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> datastore_AFetchedSourceCode_86e4604564 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-07: Write\nvendored files";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> datastore_ADependencyMetadata_9df04f8dae [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-08: Write\ndependency\nmetadata";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> datastore_ASBOMOutputCycloneDX_990b886585 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-09: Write SBOM";
+ ]
+
+ datastore_ADependencyMetadata_9df04f8dae -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-16: Read\ndependency\nmetadata";
+ ]
+
+ datastore_APatchFiles_65d0d57a00 -> process_PatchApplicationpatchng_c6f87088c2 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-10: Read patch\nfor application";
+ ]
+
+ process_PatchApplicationpatchng_c6f87088c2 -> datastore_AFetchedSourceCode_86e4604564 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-10b: Write\npatched files to\nvendor directory";
+ ]
+
+ datastore_AFetchedSourceCode_86e4604564 -> externalentity_AConsumerBuildSystem_0291419f72 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-15: Vendored\nsource to build";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> process_ArchiveExtractiontarfilezipfile_da43120000 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-11: Dispatch\narchive bytes to\nextraction";
+ ]
+
+ process_ArchiveExtractiontarfilezipfile_da43120000 -> datastore_ALocalVCSCachetemp_ae6e32d09a [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-12: Write\nextracted archive\nto temp dir";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> process_SVNExportsvnexport_7eb89910ee [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-13: Dispatch\nSVN export\nsubprocess";
+ ]
+
+ process_SVNExportsvnexport_7eb89910ee -> datastore_ALocalVCSCachetemp_ae6e32d09a [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-14: Write SVN\nexport to temp dir";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> datastore_AAuditCheckReports_d0c0ca0a3b [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-17: Write audit\n/ check reports";
+ ]
+
+ datastore_ALocalVCSCachetemp_ae6e32d09a -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-22: Copy\nextracted content\nto vendor\ndirectory";
+ ]
+
+ datastore_AIntegrityHashRecord_b2e5892d06 -> process_AdfetchProcess_c76a0a7067 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-18: Read\nintegrity hash for\narchive\nverification";
+ ]
+
+ process_AdfetchProcess_c76a0a7067 -> datastore_AIntegrityHashRecord_b2e5892d06 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-18b: Write\ncomputed hash to\nmanifest (dfetch\nfreeze)";
+ ]
+
+ actor_Developer_f2eb7a3ff7 -> datastore_AdfetchManifest_9345ab4c19 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-20: Author /\nmaintain\ndfetch.yaml";
+ ]
+
+ externalentity_ARemoteVCSServer_d2006ce1bb -> datastore_AUpstreamSourceAttestationVSA_2c440ebe53 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-19: VCS server\npublishes source\nattestation (not\nconsumed by\ndfetch)";
+ ]
+
+ actor_Developer_f2eb7a3ff7 -> datastore_APatchFiles_65d0d57a00 [
+ color = black;
+ fontcolor = black;
+ dir = forward;
+ label = "DF-21: Create /\nmaintain patch\nfiles";
+ ]
+
+ }
+ @enddot
+
+Sequence Diagram
+----------------
+
+.. uml::
+
+ @startuml
+ actor actor_Developer_f2eb7a3ff7 as "Developer"
+ entity externalentity_ARemoteVCSServer_d2006ce1bb as "A-09: Remote VCS Server"
+ entity externalentity_AArchiveHTTPServer_f8af758679 as "A-10: Archive HTTP Server"
+ database datastore_AUpstreamSourceAttestationVSA_2c440ebe53 as "A-23: Upstream Source Attestation (VSA)"
+ entity externalentity_AConsumerBuildSystem_0291419f72 as "A-11: Consumer Build System"
+ entity process_AdfetchProcess_c76a0a7067 as "A-22: dfetch Process"
+ entity process_PatchApplicationpatchng_c6f87088c2 as "Patch Application (patch-ng)"
+ database datastore_AdfetchManifest_9345ab4c19 as "A-12: dfetch Manifest"
+ database datastore_AFetchedSourceCode_86e4604564 as "A-13: Fetched Source Code"
+ database datastore_AIntegrityHashRecord_b2e5892d06 as "A-14: Integrity Hash Record"
+ database datastore_ASBOMOutputCycloneDX_990b886585 as "A-15: SBOM Output (CycloneDX)"
+ database datastore_ADependencyMetadata_9df04f8dae as "A-18: Dependency Metadata"
+ database datastore_APatchFiles_65d0d57a00 as "A-19: Patch Files"
+ entity process_ArchiveExtractiontarfilezipfile_da43120000 as "Archive Extraction (tarfile / zipfile)"
+ entity process_SVNExportsvnexport_7eb89910ee as "SVN Export (svn export)"
+ database datastore_ALocalVCSCachetemp_ae6e32d09a as "A-20: Local VCS Cache (temp)"
+ database datastore_AAuditCheckReports_d0c0ca0a3b as "A-21: Audit / Check Reports"
+
+ actor_Developer_f2eb7a3ff7 -> process_AdfetchProcess_c76a0a7067: DF-01: Invoke dfetch command
+ datastore_AdfetchManifest_9345ab4c19 -> process_AdfetchProcess_c76a0a7067: DF-02: Read manifest
+ process_AdfetchProcess_c76a0a7067 -> externalentity_ARemoteVCSServer_d2006ce1bb: DF-03a: Fetch VCS content - HTTPS/SSH
+ process_AdfetchProcess_c76a0a7067 -> externalentity_ARemoteVCSServer_d2006ce1bb: DF-03b: Fetch VCS content - svn:// / http://
+ externalentity_ARemoteVCSServer_d2006ce1bb -> process_AdfetchProcess_c76a0a7067: DF-04a: VCS content inbound - HTTPS/SSH
+ externalentity_ARemoteVCSServer_d2006ce1bb -> process_AdfetchProcess_c76a0a7067: DF-04b: VCS content inbound - svn:// / http://
+ process_AdfetchProcess_c76a0a7067 -> externalentity_AArchiveHTTPServer_f8af758679: DF-05a: Archive download request - HTTPS
+ process_AdfetchProcess_c76a0a7067 -> externalentity_AArchiveHTTPServer_f8af758679: DF-05b: Archive download request - HTTP
+ externalentity_AArchiveHTTPServer_f8af758679 -> process_AdfetchProcess_c76a0a7067: DF-06a: Archive bytes - HTTPS
+ externalentity_AArchiveHTTPServer_f8af758679 -> process_AdfetchProcess_c76a0a7067: DF-06b: Archive bytes - HTTP (plaintext risk)
+ process_AdfetchProcess_c76a0a7067 -> datastore_AFetchedSourceCode_86e4604564: DF-07: Write vendored files
+ process_AdfetchProcess_c76a0a7067 -> datastore_ADependencyMetadata_9df04f8dae: DF-08: Write dependency metadata
+ process_AdfetchProcess_c76a0a7067 -> datastore_ASBOMOutputCycloneDX_990b886585: DF-09: Write SBOM
+ datastore_ADependencyMetadata_9df04f8dae -> process_AdfetchProcess_c76a0a7067: DF-16: Read dependency metadata
+ datastore_APatchFiles_65d0d57a00 -> process_PatchApplicationpatchng_c6f87088c2: DF-10: Read patch for application
+ process_PatchApplicationpatchng_c6f87088c2 -> datastore_AFetchedSourceCode_86e4604564: DF-10b: Write patched files to vendor directory
+ datastore_AFetchedSourceCode_86e4604564 -> externalentity_AConsumerBuildSystem_0291419f72: DF-15: Vendored source to build
+ process_AdfetchProcess_c76a0a7067 -> process_ArchiveExtractiontarfilezipfile_da43120000: DF-11: Dispatch archive bytes to extraction
+ process_ArchiveExtractiontarfilezipfile_da43120000 -> datastore_ALocalVCSCachetemp_ae6e32d09a: DF-12: Write extracted archive to temp dir
+ process_AdfetchProcess_c76a0a7067 -> process_SVNExportsvnexport_7eb89910ee: DF-13: Dispatch SVN export subprocess
+ process_SVNExportsvnexport_7eb89910ee -> datastore_ALocalVCSCachetemp_ae6e32d09a: DF-14: Write SVN export to temp dir
+ process_AdfetchProcess_c76a0a7067 -> datastore_AAuditCheckReports_d0c0ca0a3b: DF-17: Write audit / check reports
+ datastore_ALocalVCSCachetemp_ae6e32d09a -> process_AdfetchProcess_c76a0a7067: DF-22: Copy extracted content to vendor directory
+ datastore_AIntegrityHashRecord_b2e5892d06 -> process_AdfetchProcess_c76a0a7067: DF-18: Read integrity hash for archive verification
+ process_AdfetchProcess_c76a0a7067 -> datastore_AIntegrityHashRecord_b2e5892d06: DF-18b: Write computed hash to manifest (dfetch freeze)
+ actor_Developer_f2eb7a3ff7 -> datastore_AdfetchManifest_9345ab4c19: DF-20: Author / maintain dfetch.yaml
+ externalentity_ARemoteVCSServer_d2006ce1bb -> datastore_AUpstreamSourceAttestationVSA_2c440ebe53: DF-19: VCS server publishes source attestation (not consumed by dfetch)
+ actor_Developer_f2eb7a3ff7 -> datastore_APatchFiles_65d0d57a00: DF-21: Create / maintain patch files
+ @enduml
+
Controls
--------
diff --git a/security/tm_common.py b/security/tm_common.py
index 05e8f0fc2..01f55c274 100644
--- a/security/tm_common.py
+++ b/security/tm_common.py
@@ -5,6 +5,7 @@
"""
import os
+import re
import sys
from collections.abc import Callable
from dataclasses import dataclass, field
@@ -111,22 +112,6 @@ def make_supply_chain_assumptions() -> list[Assumption]:
"GitHub-hosted runner. Ephemeral runner isolation is provided by GitHub."
),
),
- Assumption(
- "Harden-runner in block mode",
- description=(
- "The ``harden-runner`` egress policy is set to ``block`` with an "
- "allowlist of permitted endpoints. Outbound network connections from "
- "CI runners are blocked unless explicitly permitted."
- ),
- ),
- Assumption(
- "Build deps without hash pinning",
- description=(
- "dfetch's own build and dev dependencies are installed without "
- "``--require-hashes``, so a compromised PyPI mirror can substitute "
- "malicious build tools."
- ),
- ),
]
@@ -309,6 +294,136 @@ def _fixed(element: Any) -> str:
ReportUtils.getInScopeFindings = staticmethod(_fixed)
+def _fix_pytm_dot_output(dot: str) -> str:
+ """Replace non-portable pytm image references with Graphviz-native shapes.
+
+ pytm emits absolute filesystem paths like
+ ``image = "/home/user/.../pytm/images/datastore_black.png"``
+ which break on any machine other than the one that generated the diagram.
+ Replace the four-attribute image block with a single portable cylinder shape,
+ then rename ``xlabel`` to ``label`` and drop the now-redundant ``label = ""``.
+ """
+ dot = re.sub(
+ r"([ \t]*)shape = none;\n[ \t]*fixedsize = shape;\n"
+ r'[ \t]*image = "[^"]*pytm/images/[^"]*";\n[ \t]*imagescale = true;',
+ r"\1shape = cylinder;",
+ dot,
+ )
+ dot = re.sub(r"\bxlabel\b", "label", dot)
+ dot = re.sub(r'[ \t]+label = "";\n', "", dot)
+ return dot
+
+
+_FULLSCREEN_HTML = """\
+.. raw:: html
+
+
+
+{uml_block}
+
+.. raw:: html
+
+
+
+
+"""
+
+
+def _fullscreen_wrap(uml_rst: str) -> str:
+ """Wrap a ``.. uml::`` RST block with click-to-fullscreen HTML (HTML output only)."""
+ return _FULLSCREEN_HTML.format(uml_block=uml_rst.rstrip("\n"))
+
+
+def _render_dfd_section(tm: Any) -> str:
+ """Render the DFD as an RST ``.. uml::`` block using PlantUML @startdot.
+
+ Returns an empty string when ``tm.dfd()`` produces no content.
+ """
+ dfd = tm.dfd()
+ if not dfd or not dfd.strip():
+ return ""
+ dfd = _fix_pytm_dot_output(dfd)
+ wrapped = "@startdot\n" + dfd + "\n@enddot"
+ indented = "\n".join(" " + line if line else "" for line in wrapped.splitlines())
+ uml_rst = (
+ "Data Flow Diagram\n" "-----------------\n\n" ".. uml::\n\n" f"{indented}\n"
+ )
+ return _fullscreen_wrap(uml_rst)
+
+
+def _wrap_seq_participant_labels(seq: str, max_chars: int = 12) -> str:
+ """Wrap long participant labels at word boundaries to narrow each column.
+
+ PlantUML uses a literal ``\\n`` inside quoted strings as a line break.
+ Narrower participant boxes shrink the natural diagram width, so when Sphinx
+ scales the image to page width the font displays proportionally larger.
+ """
+
+ def _wrap(label: str) -> str:
+ words = label.split()
+ lines: list[str] = []
+ current: list[str] = []
+ width = 0
+ for word in words:
+ gap = 1 if current else 0
+ if width + gap + len(word) > max_chars and current:
+ lines.append(" ".join(current))
+ current = [word]
+ width = len(word)
+ else:
+ current.append(word)
+ width += gap + len(word)
+ if current:
+ lines.append(" ".join(current))
+ return r"\n".join(lines)
+
+ return re.sub(r'as "([^"]+)"', lambda m: f'as "{_wrap(m.group(1))}"', seq)
+
+
+def _render_seq_section(tm: Any) -> str:
+ """Render the sequence diagram as an RST ``.. uml::`` block.
+
+ Returns an empty string when ``tm.seq()`` produces no content.
+ """
+ seq = tm.seq()
+ if not seq or not seq.strip():
+ return ""
+ seq = _wrap_seq_participant_labels(seq)
+ seq = seq.replace(
+ "@startuml\n",
+ "@startuml\nskinparam defaultFontSize 16\n",
+ 1,
+ )
+ indented = "\n".join(" " + line if line else "" for line in seq.splitlines())
+ uml_rst = "Sequence Diagram\n" "----------------\n\n" ".. uml::\n\n" f"{indented}\n"
+ return _fullscreen_wrap(uml_rst)
+
+
def run_model(build_model_fn: Callable[[], Any], controls: list[Control]) -> None:
"""Run a threat model from a ``__main__`` entry point.
@@ -323,62 +438,18 @@ def run_model(build_model_fn: Callable[[], Any], controls: list[Control]) -> Non
print(f"Usage: python {script} --report ", file=sys.stderr)
raise SystemExit(1)
tm.resolve()
- print(tm.report(sys.argv[_idx + 1]))
+ title = tm.name
+ print(title)
+ print("=" * len(title))
+ print()
+ rendered = tm.report(sys.argv[_idx + 1])
+ dfd = _render_dfd_section(tm)
+ seq = _render_seq_section(tm)
+ rendered = rendered.replace(
+ "\nDataflows\n---------",
+ f"\n{dfd}\n{seq}\nDataflows\n---------",
+ )
+ print(rendered)
print(render_controls_section(controls))
else:
tm.process()
-
-
-def make_attacker_profiles() -> list[Assumption]:
- """Return Assumption objects describing the in-scope adversary classes."""
- return [
- Assumption(
- "Attacker: Network-adjacent",
- description=(
- "Positioned on the same network segment as a CI runner or developer "
- "workstation - shared cloud tenant, BGP hijack, compromised DNS resolver, "
- "or corporate proxy. Can intercept and modify unencrypted traffic "
- "(http://, svn://) and inject HTTP redirects. Cannot break correctly "
- "implemented TLS or SSH."
- ),
- ),
- Assumption(
- "Attacker: Compromised upstream",
- description=(
- "A dependency maintainer account taken over via phishing, credential "
- "stuffing, or MFA bypass - or a maintainer acting maliciously. "
- "Delivers attacker-controlled content over an authenticated channel; "
- "transport security provides no protection. Mitigated only by "
- "commit-SHA pinning and human review before accepting any update."
- ),
- ),
- Assumption(
- "Attacker: Compromised registry or CDN",
- description=(
- "Holds write access to a public package registry (PyPI) or an archive "
- "CDN node, or is BGP-adjacent to one. Serves malicious content under "
- "a valid TLS certificate - transport integrity does not detect "
- "server-side substitution. Only cryptographic content hashes or "
- "signed attestations provide a defence."
- ),
- ),
- Assumption(
- "Attacker: Local filesystem",
- description=(
- "Holds write access to the developer workstation or CI runner working "
- "tree - gained via a compromised dev dependency, malicious post-install "
- "hook, or lateral movement. Can tamper with ``.dfetch_data.yaml``, "
- "patch files, and vendored source after dfetch writes them to disk."
- ),
- ),
- Assumption(
- "Attacker: Malicious manifest contributor",
- description=(
- "A repository contributor who introduces a malicious ``dfetch.yaml`` "
- "change: redirecting a dep to an attacker-controlled URL, pointing "
- "``dst:`` at a sensitive path, or embedding a credential-bearing URL. "
- "dfetch is not the control point for this threat; code review is the "
- "intended mitigating boundary."
- ),
- ),
- ]
diff --git a/security/tm_supply_chain.py b/security/tm_supply_chain.py
index 7ea7a9ba9..2cb8cad63 100644
--- a/security/tm_supply_chain.py
+++ b/security/tm_supply_chain.py
@@ -38,9 +38,7 @@
ControlAssessment,
apply_report_utils_patch,
build_asset_controls_index,
- make_attacker_profiles,
make_dev_env_boundary,
- make_network_boundary,
make_supply_chain_assumptions,
run_model,
)
@@ -49,52 +47,51 @@
def _make_sc_boundaries() -> tuple[Boundary, Boundary, Boundary, Boundary]:
"""Create and return supply-chain trust boundaries."""
b_dev = make_dev_env_boundary()
- b_github = Boundary("GitHub Actions Infrastructure")
+ b_consumer = Boundary("Consumer Environment")
+ b_consumer.description = (
+ "End-user workstation or downstream CI pipeline where dfetch is installed "
+ "and invoked. Distinct from the developer environment: no source checkout, "
+ "no signing keys, no deploy access. The consumer is trusted at invocation "
+ "time but has no special relationship with the dfetch release infrastructure."
+ )
+ b_github = Boundary("GitHub Platform")
b_github.description = (
- "Microsoft-operated ephemeral runners executing the CI/CD workflows. "
- "Egress traffic is blocked (``harden-runner`` with ``egress-policy: block``) "
- "with an allowlist of permitted endpoints; ``ci.yml`` forwards only "
- "explicitly named secrets to child workflows (``CODACY_PROJECT_TOKEN`` "
- "to ``test.yml``, ``GH_DFETCH_ORG_DEPLOY`` to ``docs.yml``)."
+ "GitHub-hosted infrastructure: repository, CI/CD runners, Actions workflows, "
+ "build cache, and code-scanning results. "
+ "Egress traffic on runners is blocked (``harden-runner`` with "
+ "``egress-policy: block``) with an allowlist of permitted endpoints; "
+ "``ci.yml`` forwards only explicitly named secrets to child workflows "
+ "(``CODACY_PROJECT_TOKEN`` to ``test.yml``, ``GH_DFETCH_ORG_DEPLOY`` "
+ "to ``docs.yml``)."
)
b_pypi = Boundary("PyPI / TestPyPI")
b_pypi.description = (
"Python Package Index and its staging registry. dfetch publishes via "
"OIDC trusted publishing - no long-lived API token stored."
)
- b_net = make_network_boundary(
- description=(
- "Public internet - upstream package registries, PyPI, GitHub, CDN nodes, "
- "and other external endpoints reachable by the CI/CD infrastructure."
- )
- )
- return b_dev, b_github, b_pypi, b_net
+ return b_dev, b_consumer, b_github, b_pypi
def _make_sc_actors_and_entities(
b_dev: Boundary,
+ b_consumer: Boundary,
b_github: Boundary,
b_pypi: Boundary,
- b_net: Boundary,
-) -> tuple[Actor, Actor, ExternalEntity, ExternalEntity, ExternalEntity]:
- """Create actors and external entities; return those referenced by dataflows."""
- developer = Actor("Developer")
+) -> tuple[
+ Actor, Actor, ExternalEntity, ExternalEntity, ExternalEntity, ExternalEntity
+]:
+ """Create actors and external entities; return all for use in dataflows."""
+ developer = Actor("Developer / Contributor")
developer.inBoundary = b_dev
developer.description = (
- "dfetch project contributor: writes code, reviews PRs, cuts releases. "
- "Trusted at workstation time; responsible for correct branch-protection "
- "and release workflow configuration."
- )
- contributor = Actor("Contributor / Attacker")
- contributor.inBoundary = b_net
- contributor.description = (
- "External contributor submitting pull requests, or an adversary attempting "
- "supply-chain manipulation (malicious PR, action-poisoning, or MITM on CI "
- "network traffic). Code review, branch protection, and SHA-pinned Actions "
- "are the primary controls at this boundary."
+ "Anyone who writes code for dfetch: core maintainers who push directly and "
+ "cut releases, and external contributors who submit pull requests. "
+ "Maintainers are trusted at workstation time and are responsible for correct "
+ "branch-protection and release workflow configuration. "
+ "External contributors are untrusted until their PR passes code review and CI."
)
consumer = Actor("Consumer / End User")
- consumer.inBoundary = b_dev
+ consumer.inBoundary = b_consumer
consumer.description = (
"Installs dfetch from PyPI (``pip install dfetch``) or from binary installer, "
"then invokes it on a developer workstation or in a CI pipeline. "
@@ -104,13 +101,32 @@ def _make_sc_actors_and_entities(
"installers; SLSA build provenance, in-toto test result attestation, and SLSA Source "
"Provenance Attestation on the source archive and main-branch commits."
)
- gh_repository = ExternalEntity("A-01: GitHub Repository")
+ gh_repository = ExternalEntity("A-01: GitHub Repository (main / protected)")
gh_repository.inBoundary = b_github
gh_repository.classification = Classification.RESTRICTED
gh_repository.description = (
- "Source code, PRs, releases, and workflow definitions. "
- "GitHub Actions workflows (``.github/workflows/``) with "
- "``contents:write`` permission can modify repository state and trigger releases."
+ "The protected ``main`` branch: force-push disabled, merges require passing CI "
+ "and at least one approving review. Contains the authoritative workflow "
+ "definitions (``.github/workflows/``), release tags, and published release assets. "
+ "Workflow files on main are what GitHub Actions actually executes — a PR cannot "
+ "override them for its own CI run. "
+ "``contents:write`` permission allows CI to upload SARIF results and release assets."
+ )
+ gh_repository.controls.hasAccessControl = True
+ gh_repository.controls.providesIntegrity = True
+ gh_repository_branches = ExternalEntity(
+ "A-01b: GitHub Repository (feature branches / PRs)"
+ )
+ gh_repository_branches.inBoundary = b_github
+ gh_repository_branches.classification = Classification.RESTRICTED
+ gh_repository_branches.description = (
+ "Unprotected feature branches and fork PRs: no mandatory review, no CI-gate "
+ "requirement to push. Any authenticated GitHub user can open a PR modifying "
+ "``.github/workflows/`` files; those changes are reviewed before merging to main "
+ "but execute with restricted permissions during CI (no access to production secrets). "
+ "A malicious PR modifying workflow files could attempt to exfiltrate secrets "
+ "during the PR CI run, mitigated by ``ci.yml`` secret scoping (C-024) and "
+ "harden-runner egress block (C-013)."
)
gh_actions_runner = ExternalEntity("A-02: GitHub Actions Infrastructure")
gh_actions_runner.inBoundary = b_github
@@ -123,17 +139,34 @@ def _make_sc_actors_and_entities(
)
pypi = ExternalEntity("A-03: PyPI / TestPyPI")
pypi.inBoundary = b_pypi
- pypi.classification = Classification.PUBLIC
+ pypi.classification = Classification.SENSITIVE
pypi.description = (
- "Python Package Index. dfetch is published via OIDC trusted publishing "
- "(no long-lived API token). Account takeover or registry compromise "
- "would affect every consumer installing dfetch."
+ "Python Package Index - both the registry service and the published dfetch "
+ "wheel/sdist (https://pypi.org/project/dfetch/). "
+ "Published via OIDC trusted publishing; no long-lived API token stored. "
+ "A machine-readable CycloneDX SBOM is generated during the build and "
+ "published alongside the release. "
+ "Account takeover, registry compromise, or namespace-squatting would affect "
+ "every consumer installing dfetch."
+ )
+ pypi.controls.usesCodeSigning = (
+ True # Sigstore SBOM attestation (actions/attest; predicate cyclonedx.org/bom)
+ )
+ pypi.controls.isHardened = (
+ True # OIDC trusted publishing; no stored long-lived token
+ )
+ return (
+ developer,
+ consumer,
+ gh_repository,
+ gh_repository_branches,
+ gh_actions_runner,
+ pypi,
)
- return contributor, consumer, gh_repository, gh_actions_runner, pypi
-def _make_sc_processes(b_github: Boundary) -> None:
- """Create CI/CD process elements; all auto-register with TM."""
+def _make_sc_processes(b_github: Boundary) -> tuple[Process, Process, Process]:
+ """Create CI/CD process elements; return all for use in dataflows."""
release_gate = Process("Release Gate / Code Review")
release_gate.inBoundary = b_github
release_gate.description = (
@@ -184,29 +217,11 @@ def _make_sc_processes(b_github: Boundary) -> None:
False # build deps installed without --require-hashes; see gap C-023
)
python_build.controls.isCiPipeline = True # CI/release pipeline build step
+ return release_gate, gh_actions_workflow, python_build
-def _make_sc_datastores(b_github: Boundary, b_pypi: Boundary) -> Datastore:
- """Create data assets; return gh_actions_cache (referenced by dataflows)."""
- pypi_package = Datastore("A-04: dfetch PyPI Package")
- pypi_package.inBoundary = b_pypi
- pypi_package.description = (
- "Published wheel and sdist on PyPI (https://pypi.org/project/dfetch/). "
- "Published via OIDC trusted publishing - no long-lived API token stored. "
- "A machine-readable CycloneDX SBOM is generated during the build and "
- "published alongside the release. "
- "Compromise of the PyPI account or registry affects every consumer."
- )
- pypi_package.storesSensitiveData = False
- pypi_package.hasWriteAccess = True # publish pipeline writes new releases
- pypi_package.isSQL = False
- pypi_package.classification = Classification.SENSITIVE
- pypi_package.controls.usesCodeSigning = (
- True # Sigstore SBOM attestation (actions/attest; predicate cyclonedx.org/bom)
- )
- pypi_package.controls.isHardened = (
- True # OIDC trusted publishing; no stored long-lived token
- )
+def _make_sc_datastores(b_github: Boundary) -> tuple[Datastore, Datastore]:
+ """Create data assets; return all for use in dataflows."""
Data(
"A-05: PyPI OIDC Identity",
description=(
@@ -223,25 +238,6 @@ def _make_sc_datastores(b_github: Boundary, b_pypi: Boundary) -> Datastore:
isDestEncryptedAtRest=False,
isSourceEncryptedAtRest=True,
)
- scorecard_results = Datastore("A-06: OpenSSF Scorecard Results")
- scorecard_results.inBoundary = b_github
- scorecard_results.description = (
- "Weekly OSSF Scorecard SARIF results uploaded to GitHub Code Scanning. "
- "Covers: branch-protection, CI-tests, code-review, maintained, packaging, "
- "pinned-dependencies, SAST, signed-releases, token-permissions, vulnerabilities, "
- "dangerous-workflow, binary-artifacts, fuzzing, license, CII-best-practices, "
- "security-policy, webhooks. "
- "Suppression or forgery hides supply-chain regressions."
- )
- scorecard_results.storesSensitiveData = False
- scorecard_results.hasWriteAccess = (
- True # CI runner writes SARIF uploads here (DF-17)
- )
- scorecard_results.isSQL = False
- scorecard_results.classification = Classification.RESTRICTED
- scorecard_results.controls.providesIntegrity = (
- True # authenticated output from OSSF Scorecard action
- )
dfetch_dev_deps = Datastore("A-07: dfetch Build / Dev Dependencies")
dfetch_dev_deps.inBoundary = b_github
dfetch_dev_deps.description = (
@@ -256,24 +252,6 @@ def _make_sc_datastores(b_github: Boundary, b_pypi: Boundary) -> Datastore:
dfetch_dev_deps.hasWriteAccess = False
dfetch_dev_deps.isSQL = False
dfetch_dev_deps.classification = Classification.RESTRICTED
- gh_workflows = Datastore("A-08: GitHub Actions Workflows")
- gh_workflows.inBoundary = b_github
- gh_workflows.description = (
- "``.github/workflows/*.yml`` - CI/CD configuration checked into the repository. "
- "11 workflows: ci, build, run, test, docs, release, python-publish, "
- "dependency-review, codeql-analysis, scorecard, devcontainer. "
- "A malicious PR that modifies workflows can exfiltrate secrets or publish "
- "a backdoored release. "
- "Mitigated by: SHA-pinned actions, persist-credentials:false, minimal permissions."
- )
- gh_workflows.storesSensitiveData = False
- gh_workflows.hasWriteAccess = True # PRs can modify .github/workflows/ definitions
- gh_workflows.isSQL = False
- gh_workflows.classification = Classification.RESTRICTED
- gh_workflows.controls.isHardened = True
- gh_workflows.controls.providesIntegrity = (
- True # branch protection + mandatory code review
- )
gh_actions_cache = Datastore("A-08b: GitHub Actions Build Cache")
gh_actions_cache.inBoundary = b_github
gh_actions_cache.description = (
@@ -291,75 +269,155 @@ def _make_sc_datastores(b_github: Boundary, b_pypi: Boundary) -> Datastore:
)
gh_actions_cache.isSQL = False
gh_actions_cache.classification = Classification.RESTRICTED
- return gh_actions_cache
+ return dfetch_dev_deps, gh_actions_cache
-def _make_sc_contrib_flows(
- contributor: Actor,
- consumer: Actor,
- gh_repository: ExternalEntity,
- pypi: ExternalEntity,
+def _make_sc_code_contribution_flows(
+ developer: Actor,
+ gh_repository_branches: ExternalEntity,
+ release_gate: Process,
) -> None:
- """Create contributor- and consumer-facing dataflows; all auto-register with TM."""
- df11 = Dataflow(contributor, gh_repository, "DF-11: Submit pull request")
+ """Create code-contribution and review dataflows (DF-11, DF-22); auto-register with TM."""
+ df11 = Dataflow(developer, gh_repository_branches, "DF-11: Push commits / open PR")
df11.description = (
- "External contributor opens a PR against the dfetch repository. "
- "Workflow files in ``.github/workflows/`` can be modified by PRs. "
- "``ci.yml`` only passes required repository secrets to the test and docs "
- "workflows, preventing malicious PR steps from exfiltrating secrets."
+ "Developer or contributor pushes commits or opens a pull request targeting a "
+ "feature branch (A-01b). The entry point for all code changes. "
+ "Workflow files in ``.github/workflows/`` can be modified by PRs but are "
+ "reviewed before merging; ``ci.yml`` only passes required repository secrets "
+ "to child workflows, preventing PR CI steps from exfiltrating unrelated secrets."
)
df11.protocol = "HTTPS"
+ df11.controls.isEncrypted = True
df11.controls.hasAccessControl = True
- df11.controls.isHardened = (
- True # git commit SHAs provide content-addressable integrity
- )
df11.controls.providesIntegrity = True
- df14 = Dataflow(consumer, pypi, "DF-14: pip install dfetch")
- df14.description = (
- "Consumer installs dfetch from PyPI. The installed wheel contains the dfetch "
- "CLI; the handoff to the runtime-usage model occurs when the consumer invokes "
- "dfetch with a manifest. "
- "The consumer can verify the SBOM (CycloneDX) attestation of the wheel using "
- "``gh attestation verify`` with ``--predicate-type https://cyclonedx.org/bom`` "
- "and cert-identity pinned to ``python-publish.yml`` (see C-026)."
+
+ df22 = Dataflow(
+ gh_repository_branches, release_gate, "DF-22: PR enters code review"
)
- df14.protocol = "HTTPS"
- df14.controls.isEncrypted = True
- df14.controls.isNetworkFlow = True
+ df22.description = (
+ "Pull request from a feature branch (A-01b) flows into the Release Gate / "
+ "Code Review process. Branch-protection rules require passing CI status "
+ "checks and at least one approving review before any merge to main is permitted."
+ )
+ df22.controls.hasAccessControl = True
-def _make_sc_ci_flows(
+def _make_sc_workflow_checkout_flows(
gh_repository: ExternalEntity,
+ gh_actions_workflow: Process,
+ gh_repository_branches: ExternalEntity,
gh_actions_runner: ExternalEntity,
- pypi: ExternalEntity,
- gh_actions_cache: Datastore,
) -> None:
- """Create CI infrastructure dataflows; all auto-register with TM."""
- df12 = Dataflow(gh_repository, gh_actions_runner, "DF-12: CI checkout and build")
+ """Create workflow-definition and CI-checkout dataflows (DF-12, DF-13a/b); auto-register with TM."""
+ df12 = Dataflow(
+ gh_repository,
+ gh_actions_workflow,
+ "DF-12: Main branch workflows drive CI execution",
+ )
df12.description = (
- "GitHub Actions checks out source, installs deps, runs tests, lints, builds. "
- "``persist-credentials:false`` on all checkout steps. "
- "All third-party actions pinned by commit SHA."
+ "The ``.github/workflows/*.yml`` YAML files on the protected main branch (A-01) "
+ "define what GitHub Actions executes. GitHub enforces that PR CI runs use the "
+ "workflow files from the base branch (main), not from the PR branch — a malicious "
+ "PR cannot override the workflow for its own CI run. "
+ "Mitigated by SHA-pinned actions, minimal permissions, and mandatory code review "
+ "before any workflow change reaches main (C-009, C-011)."
)
df12.controls.isHardened = True
- df12.controls.providesIntegrity = True
- df13 = Dataflow(gh_actions_runner, pypi, "DF-13: Publish to PyPI (OIDC)")
- df13.description = (
- "On release event, wheel/sdist uploaded to PyPI via "
- "``pypa/gh-action-pypi-publish``. OIDC trusted publishing: no stored API token. "
- "Four attestation types are generated by the release pipeline and anchored in Sigstore: "
- "SLSA build provenance, SBOM (CycloneDX), VSA (Verification Summary Attestation) on "
- "binary installers, and in-toto test result attestation on the source archive."
- )
- df13.protocol = "HTTPS"
- df13.controls.isEncrypted = True
- df13.controls.isHardened = (
- True # OIDC token exchange; no long-lived credential in transit
+
+ df13a = Dataflow(
+ gh_repository_branches, gh_actions_runner, "DF-13a: PR CI checkout"
)
- df13.controls.providesIntegrity = (
- True # Sigstore SBOM attestation covers published artifacts
+ df13a.description = (
+ "GitHub Actions checks out source from the feature branch / PR (A-01b) for "
+ "CI runs triggered by pull requests. ``persist-credentials:false`` on all "
+ "checkout steps. Runs with restricted permissions — no access to production "
+ "secrets (see C-024)."
+ )
+ df13a.controls.isHardened = True
+
+ df13b = Dataflow(gh_repository, gh_actions_runner, "DF-13b: Release CI checkout")
+ df13b.description = (
+ "GitHub Actions checks out the tagged commit from main (A-01) for release "
+ "and build workflows. ``persist-credentials:false`` on all checkout steps. "
+ "All third-party actions pinned by commit SHA."
)
- df13.controls.usesCodeSigning = True # SBOM attestation via actions/attest
+ df13b.controls.isHardened = True
+ df13b.controls.providesIntegrity = True
+
+
+def _make_sc_cache_restore_flow(
+ gh_actions_cache: Datastore,
+ gh_actions_runner: ExternalEntity,
+) -> None:
+ """Create CI cache-restore dataflow (DF-14); auto-registers with TM."""
+ df14 = Dataflow(gh_actions_cache, gh_actions_runner, "DF-14: CI cache restore")
+ df14.description = (
+ "GitHub Actions runner restores a previously written cache entry before "
+ "running build steps. Ref-scoped cache keys (C-033) ensure a release-tag build "
+ "restores only entries written under the same ref, not those written by a less-privileged "
+ "PR workflow. Residual risk: if ref-scoped keys were removed or misconfigured, the release "
+ "build could consume attacker-controlled artifacts."
+ )
+ df14.protocol = "HTTPS"
+ df14.controls.isEncrypted = True
+
+
+def _make_sc_build_step_flows(
+ gh_actions_workflow: Process,
+ python_build: Process,
+ pypi: ExternalEntity,
+ dfetch_dev_deps: Datastore,
+ gh_actions_runner: ExternalEntity,
+) -> None:
+ """Create build-trigger, dep-fetch, build-tool-consume, and artifact-output flows (DF-15 to DF-17b)."""
+ df15 = Dataflow(
+ gh_actions_workflow, python_build, "DF-15: Workflow triggers build step"
+ )
+ df15.description = (
+ "The executing workflow process invokes the Python Build step "
+ "(``python -m build``) to produce wheel and sdist artefacts. "
+ "Build tools (setuptools, fpm, etc.) are installed without hash-pinning at this "
+ "point; a compromised package from PyPI could execute arbitrary code during the "
+ "build (gap C-023)."
+ )
+
+ df15b = Dataflow(
+ python_build, gh_actions_runner, "DF-15b: Built wheel/sdist artifacts"
+ )
+ df15b.description = (
+ "The Python Build step produces wheel and sdist artefacts, which are written to "
+ "the runner filesystem and later consumed by the publish step (DF-24). "
+ "A compromised build tool (DF-17 gap) can tamper with the artefact content at "
+ "this point before attestations are generated."
+ )
+
+ df16 = Dataflow(pypi, dfetch_dev_deps, "DF-16: CI fetches build/dev deps from PyPI")
+ df16.description = (
+ "The CI runner installs build and dev dependencies from public PyPI: "
+ "setuptools, build, pylint, mypy, pytest, etc. "
+ "No ``--require-hashes`` flag is used, so a compromised PyPI mirror or BGP "
+ "hijack can substitute malicious build tooling without triggering any checksum "
+ "failure (gap C-023)."
+ )
+ df16.protocol = "HTTPS"
+ df16.controls.isEncrypted = True
+ df16.controls.isNetworkFlow = True
+
+ df17 = Dataflow(
+ dfetch_dev_deps, python_build, "DF-17: Build tools consumed by build step"
+ )
+ df17.description = (
+ "Installed build/dev dependencies (A-07) are consumed by the Python Build "
+ "process. If any dependency was substituted by an attacker (DF-16 gap), the "
+ "malicious code runs here with runner-level privileges."
+ )
+
+
+def _make_sc_cache_write_flow(
+ gh_actions_runner: ExternalEntity,
+ gh_actions_cache: Datastore,
+) -> None:
+ """Create CI cache-write dataflow (DF-18); auto-registers with TM."""
df18 = Dataflow(gh_actions_runner, gh_actions_cache, "DF-18: CI cache write")
df18.description = (
"GitHub Actions runner writes build cache entries (pip dependencies, gem packages, "
@@ -371,38 +429,132 @@ def _make_sc_ci_flows(
)
df18.protocol = "HTTPS"
df18.controls.isEncrypted = True
- df19 = Dataflow(gh_actions_cache, gh_actions_runner, "DF-19: CI cache restore")
- df19.description = (
- "GitHub Actions runner restores a previously written cache entry before "
- "running build steps. Ref-scoped cache keys (C-033) ensure a release-tag build "
- "restores only entries written under the same ref, not those written by a less-privileged "
- "PR workflow. Residual risk: if ref-scoped keys were removed or misconfigured, the release "
- "build could consume attacker-controlled artifacts."
- )
- df19.protocol = "HTTPS"
- df19.controls.isEncrypted = True
- df17 = Dataflow(
+
+
+def _make_sc_ci_reporting_flows(
+ gh_actions_runner: ExternalEntity,
+ gh_repository: ExternalEntity,
+) -> None:
+ """Create CI write-back dataflow (DF-19); auto-registers with TM."""
+ df19 = Dataflow(
gh_actions_runner,
gh_repository,
- "DF-17: CI write-back (SARIF / artifacts / cache)",
+ "DF-19: CI write-back (SARIF / artifacts)",
)
- df17.description = (
+ df19.description = (
"CI runner uploads artifacts back to the repository: SARIF results to GitHub "
- "Code Scanning (CodeQL, Scorecard), release assets, and build cache. "
- "Suppression or falsification of SARIF uploads (A-06) would hide supply-chain "
- "regressions from the security dashboard. "
+ "Code Scanning (CodeQL, Scorecard) and release assets. "
"Mitigated by: authenticated GitHub API (GITHUB_TOKEN scoped to ``security-events: write``), "
"SHA-pinned upload actions."
)
- df17.protocol = "HTTPS"
- df17.controls.isEncrypted = True
- df17.controls.isHardened = (
- True # GITHUB_TOKEN is scoped; harden-runner blocks exfiltration
+ df19.protocol = "HTTPS"
+ df19.controls.isEncrypted = True
+ df19.controls.isHardened = True
+ df19.controls.providesIntegrity = True
+ df19.controls.hasAccessControl = True
+
+
+def _make_sc_merge_flow(
+ release_gate: Process,
+ gh_repository: ExternalEntity,
+) -> None:
+ """Create approved-merge dataflow (DF-23); auto-registers with TM."""
+ df23 = Dataflow(release_gate, gh_repository, "DF-23: Approved merge to main")
+ df23.description = (
+ "After peer review approval and all required CI checks pass, the Release Gate "
+ "permits the merge. The resulting commit on main may trigger downstream "
+ "CI workflows including the release pipeline."
+ )
+ df23.controls.hasAccessControl = True
+ df23.controls.providesIntegrity = True
+
+
+def _make_sc_publish_flow(
+ gh_actions_runner: ExternalEntity,
+ pypi: ExternalEntity,
+) -> None:
+ """Create the PyPI publish dataflow (DF-24); auto-registers with TM."""
+ df24 = Dataflow(gh_actions_runner, pypi, "DF-24: Publish wheel to PyPI (OIDC)")
+ df24.description = (
+ "On release event, the GitHub Actions runner uses ``pypa/gh-action-pypi-publish`` "
+ "with OIDC trusted publishing to upload the wheel and sdist produced by the Python "
+ "Build step to PyPI (A-03). No long-lived API token "
+ "is stored; the OIDC token is scoped to the ``pypi`` deployment environment. "
+ "Four attestation types are generated and anchored in Sigstore: "
+ "SLSA build provenance, SBOM (CycloneDX), VSA on binary installers, and in-toto "
+ "test result attestation on the source archive (C-021, C-039, C-040)."
+ )
+ df24.protocol = "HTTPS"
+ df24.controls.isEncrypted = True
+ df24.controls.isHardened = (
+ True # OIDC token exchange; no long-lived credential in transit
)
- df17.controls.providesIntegrity = True
- df17.controls.hasAccessControl = (
- True # GITHUB_TOKEN permission scoping per workflow
+ df24.controls.providesIntegrity = True
+ df24.controls.usesCodeSigning = True # Sigstore attestations via actions/attest
+
+
+def _make_sc_consumer_install_flows(
+ consumer: Actor,
+ pypi: ExternalEntity,
+) -> None:
+ """Create consumer pip-install and download dataflows (DF-25, DF-26); auto-register with TM."""
+ df25 = Dataflow(consumer, pypi, "DF-25: pip install dfetch")
+ df25.description = (
+ "Consumer installs dfetch from PyPI. The installed wheel contains the dfetch "
+ "CLI; the handoff to the runtime-usage model occurs when the consumer invokes "
+ "dfetch with a manifest. "
+ "The consumer can verify the SBOM (CycloneDX) attestation of the wheel using "
+ "``gh attestation verify`` with ``--predicate-type https://cyclonedx.org/bom`` "
+ "and cert-identity pinned to ``python-publish.yml`` (see C-026)."
)
+ df25.protocol = "HTTPS"
+ df25.controls.isEncrypted = True
+ df25.controls.isNetworkFlow = True
+
+ df26 = Dataflow(pypi, consumer, "DF-26: Consumer downloads dfetch from PyPI")
+ df26.description = (
+ "The published dfetch wheel (A-03) is downloaded to the consumer workstation "
+ "during ``pip install dfetch``. The consumer can verify the SBOM (CycloneDX) "
+ "attestation using ``gh attestation verify`` as documented in C-026. "
+ "Without verification, a compromised PyPI account or namespace-squatting "
+ "could serve a malicious package undetected (DFT-17)."
+ )
+ df26.protocol = "HTTPS"
+ df26.controls.isEncrypted = True
+ df26.controls.isNetworkFlow = True
+
+
+def _make_sc_elements_and_flows(
+ b_dev: Boundary,
+ b_consumer: Boundary,
+ b_github: Boundary,
+ b_pypi: Boundary,
+) -> None:
+ """Create all supply-chain model elements and wire dataflows DF-11 through DF-26."""
+ (
+ developer,
+ consumer,
+ gh_repository,
+ gh_repository_branches,
+ gh_actions_runner,
+ pypi,
+ ) = _make_sc_actors_and_entities(b_dev, b_consumer, b_github, b_pypi)
+ release_gate, gh_actions_workflow, python_build = _make_sc_processes(b_github)
+ dfetch_dev_deps, gh_actions_cache = _make_sc_datastores(b_github)
+ # Lifecycle: commit/PR → CI → review → merge → publish → consumer install
+ _make_sc_code_contribution_flows(developer, gh_repository_branches, release_gate)
+ _make_sc_workflow_checkout_flows(
+ gh_repository, gh_actions_workflow, gh_repository_branches, gh_actions_runner
+ )
+ _make_sc_cache_restore_flow(gh_actions_cache, gh_actions_runner)
+ _make_sc_build_step_flows(
+ gh_actions_workflow, python_build, pypi, dfetch_dev_deps, gh_actions_runner
+ )
+ _make_sc_cache_write_flow(gh_actions_runner, gh_actions_cache)
+ _make_sc_ci_reporting_flows(gh_actions_runner, gh_repository)
+ _make_sc_merge_flow(release_gate, gh_repository)
+ _make_sc_publish_flow(gh_actions_runner, pypi)
+ _make_sc_consumer_install_flows(consumer, pypi)
def build_model() -> TM:
@@ -421,15 +573,9 @@ def build_model() -> TM:
mergeResponses=True,
threatsFile=THREATS_FILE,
)
- model.assumptions = make_supply_chain_assumptions() + make_attacker_profiles()
- b_dev, b_github, b_pypi, b_net = _make_sc_boundaries()
- contributor, consumer, gh_repository, gh_actions_runner, pypi = (
- _make_sc_actors_and_entities(b_dev, b_github, b_pypi, b_net)
- )
- _make_sc_processes(b_github)
- gh_actions_cache = _make_sc_datastores(b_github, b_pypi)
- _make_sc_contrib_flows(contributor, consumer, gh_repository, pypi)
- _make_sc_ci_flows(gh_repository, gh_actions_runner, pypi, gh_actions_cache)
+ model.assumptions = make_supply_chain_assumptions()
+ b_dev, b_consumer, b_github, b_pypi = _make_sc_boundaries()
+ _make_sc_elements_and_flows(b_dev, b_consumer, b_github, b_pypi)
return model
@@ -440,7 +586,7 @@ def build_model() -> TM:
Control(
id="C-009",
name="Actions commit-SHA pinning",
- assets=["A-08", "A-02"],
+ assets=["A-01", "A-02"],
threats=["DFT-07"],
reference=".github/workflows/*.yml",
assessment=ControlAssessment(risk="High", stride=["Tampering"]),
@@ -452,7 +598,7 @@ def build_model() -> TM:
Control(
id="C-010",
name="OIDC trusted publishing",
- assets=["A-05", "A-04"],
+ assets=["A-05", "A-03"],
threats=["DFT-07"],
reference=".github/workflows/python-publish.yml",
assessment=ControlAssessment(
@@ -466,7 +612,7 @@ def build_model() -> TM:
Control(
id="C-011",
name="Minimal workflow permissions",
- assets=["A-08"],
+ assets=["A-01"],
threats=["DFT-07"],
reference=".github/workflows/*.yml",
assessment=ControlAssessment(risk="Medium", stride=["Elevation of Privilege"]),
@@ -478,7 +624,7 @@ def build_model() -> TM:
Control(
id="C-012",
name="persist-credentials: false",
- assets=["A-08", "A-01"],
+ assets=["A-01"],
threats=["DFT-07"],
reference=".github/workflows/*.yml",
assessment=ControlAssessment(
@@ -493,7 +639,7 @@ def build_model() -> TM:
Control(
id="C-013",
name="Harden-runner (egress block)",
- assets=["A-08", "A-02"],
+ assets=["A-01", "A-02"],
threats=["DFT-07", "DFT-29"],
reference=".github/workflows/*.yml",
assessment=ControlAssessment(
@@ -505,22 +651,10 @@ def build_model() -> TM:
"All non-allowlisted outbound connections are blocked."
),
),
- Control(
- id="C-014",
- name="OpenSSF Scorecard",
- assets=["A-01", "A-06"],
- threats=["DFT-07", "DFT-10"],
- reference=".github/workflows/scorecard.yml",
- assessment=ControlAssessment(risk="Low", stride=["Tampering"]),
- description=(
- "Weekly OSSF Scorecard analysis uploaded to GitHub Code Scanning "
- "covers the full set of OpenSSF Scorecard checks."
- ),
- ),
Control(
id="C-015",
name="CodeQL static analysis",
- assets=["A-01", "A-08"],
+ assets=["A-01"],
threats=["DFT-03", "DFT-06"],
reference=".github/workflows/codeql-analysis.yml",
assessment=ControlAssessment(
@@ -559,7 +693,7 @@ def build_model() -> TM:
Control(
id="C-021",
name="Sigstore SBOM attestation",
- assets=["A-04"],
+ assets=["A-03"],
threats=["DFT-05"],
assessment=ControlAssessment(risk="Medium", stride=["Spoofing", "Tampering"]),
description=(
@@ -572,7 +706,7 @@ def build_model() -> TM:
Control(
id="C-022",
name="CycloneDX SBOM on PyPI",
- assets=["A-04", "A-07"],
+ assets=["A-03", "A-07"],
threats=["DFT-02"],
assessment=ControlAssessment(risk="Low", stride=["Repudiation"]),
description=(
@@ -583,7 +717,7 @@ def build_model() -> TM:
Control(
id="C-024",
name="``secrets: inherit`` scope",
- assets=["A-08", "A-05"],
+ assets=["A-01", "A-05"],
threats=["DFT-07"],
assessment=ControlAssessment(risk="Medium", stride=["Information Disclosure"]),
description=(
@@ -595,7 +729,7 @@ def build_model() -> TM:
Control(
id="C-026",
name="Consumer-side package provenance verification",
- assets=["A-04"],
+ assets=["A-03"],
threats=["DFT-17", "DFT-25"],
assessment=ControlAssessment(
status="implemented", risk="Medium", stride=["Spoofing", "Tampering"]
@@ -623,7 +757,7 @@ def build_model() -> TM:
Control(
id="C-032",
name="Consumer attestation verification pins to release tag ref",
- assets=["A-08", "A-04"],
+ assets=["A-01", "A-03"],
threats=["DFT-27"],
reference="doc/tutorials/installation.rst",
assessment=ControlAssessment(
@@ -702,7 +836,7 @@ def build_model() -> TM:
Control(
id="C-039",
name="Source build provenance and VSA attestations",
- assets=["A-01", "A-02", "A-04"],
+ assets=["A-01", "A-02", "A-03"],
threats=["DFT-31", "DFT-25"],
reference="doc/howto/verify-integrity.rst",
assessment=ControlAssessment(
@@ -767,7 +901,7 @@ def build_model() -> TM:
Control(
id="C-025",
name="No hardware-token MFA for release operations",
- assets=["A-01", "A-04"],
+ assets=["A-01", "A-03"],
threats=["DFT-11"],
assessment=ControlAssessment(
status="gap", risk="High", stride=["Spoofing", "Elevation of Privilege"]
@@ -786,13 +920,11 @@ def build_model() -> TM:
_SUPPLY_CHAIN_ASSET_IDS = {
"A-01",
+ "A-01b",
"A-02",
"A-03",
- "A-04",
"A-05",
- "A-06",
"A-07",
- "A-08",
"A-08b",
}
ASSET_CONTROLS: dict[str, list[Control]] = build_asset_controls_index(
diff --git a/security/tm_usage.py b/security/tm_usage.py
index c7f9675bd..7653dc938 100644
--- a/security/tm_usage.py
+++ b/security/tm_usage.py
@@ -50,7 +50,6 @@
ControlAssessment,
apply_report_utils_patch,
build_asset_controls_index,
- make_attacker_profiles,
make_dev_env_boundary,
make_network_boundary,
make_usage_assumptions,
@@ -73,8 +72,8 @@ def _make_usage_boundaries() -> tuple[Boundary, Boundary]:
def _make_usage_actors_and_external_entities(
b_dev: Boundary,
b_remote_vcs: Boundary,
-) -> tuple[Actor, ExternalEntity, ExternalEntity, ExternalEntity]:
- """Create actors and external entities; return those referenced by dataflows."""
+) -> tuple[Actor, ExternalEntity, ExternalEntity, ExternalEntity, Datastore]:
+ """Create actors and external entities; return all for use in dataflows."""
developer = Actor("Developer")
developer.inBoundary = b_dev
developer.description = (
@@ -92,7 +91,14 @@ def _make_usage_actors_and_external_entities(
"Not controlled by the dfetch project; content is untrusted until verified. "
"The SLSA source level of any upstream is unknown and unverified - dfetch does "
"not check whether the upstream enforces branch protection, mandatory review, or "
- "ancestry enforcement, and no VSA is fetched alongside repository content (A-23)."
+ "ancestry enforcement, and no VSA is fetched alongside repository content (A-23). "
+ "Threat postures: a compromised upstream maintainer account (phishing, credential "
+ "stuffing, or MFA bypass) delivers attacker-controlled commits over an authenticated "
+ "channel where transport security gives no protection — mitigated only by commit-SHA "
+ "pinning and review before accepting any update. "
+ "A network-adjacent attacker (BGP hijack, compromised DNS resolver) can intercept "
+ "unencrypted traffic (svn://, http://) and inject redirects, but cannot break "
+ "correctly implemented TLS or SSH."
)
archive_server = ExternalEntity("A-10: Archive HTTP Server")
archive_server.inBoundary = b_remote_vcs
@@ -100,7 +106,13 @@ def _make_usage_actors_and_external_entities(
archive_server.description = (
"HTTP/HTTPS server serving ``.tar.gz``, ``.tgz``, ``.tar.bz2``, ``.tar.xz``, "
"or ``.zip`` files. CRITICAL: ``http://`` (non-TLS) URLs are accepted without "
- "enforcement of integrity hashes - the ``integrity.hash`` field is optional."
+ "enforcement of integrity hashes - the ``integrity.hash`` field is optional. "
+ "Threat postures: a network-adjacent attacker (BGP hijack, compromised DNS resolver, "
+ "or corporate proxy) can intercept ``http://`` traffic and serve a malicious archive "
+ "transparently — ``integrity.hash`` is the only defence for plain-HTTP URLs. "
+ "A compromised CDN node or registry can serve malicious content under a valid TLS "
+ "certificate; transport integrity gives no protection — only a verified content hash "
+ "or signed attestation detects server-side substitution."
)
upstream_source_attestation = Datastore("A-23: Upstream Source Attestation (VSA)")
upstream_source_attestation.inBoundary = b_remote_vcs
@@ -131,7 +143,13 @@ def _make_usage_actors_and_external_entities(
"Build system that compiles fetched source code (A-13). "
"Not controlled by dfetch - it receives untrusted third-party source."
)
- return developer, remote_git_svn, archive_server, consumer_build
+ return (
+ developer,
+ remote_git_svn,
+ archive_server,
+ consumer_build,
+ upstream_source_attestation,
+ )
def _make_usage_processes(b_dev: Boundary) -> tuple[Process, Process]:
@@ -181,8 +199,8 @@ def _make_usage_processes(b_dev: Boundary) -> tuple[Process, Process]:
def _make_usage_datastores_a(
b_dev: Boundary,
-) -> tuple[Datastore, Datastore, Datastore]:
- """Create primary data stores; return those referenced by dataflows."""
+) -> tuple[Datastore, Datastore, Datastore, Datastore]:
+ """Create primary data stores; return all for use in dataflows."""
manifest_store = Datastore("A-12: dfetch Manifest")
manifest_store.inBoundary = b_dev
manifest_store.description = (
@@ -191,7 +209,15 @@ def _make_usage_datastores_a(
"integrity hashes. "
"Tampering redirects fetches to attacker-controlled sources. "
"RISK: ``integrity.hash`` is Optional in schema - archive deps can be declared "
- "without any content-authenticity guarantee."
+ "without any content-authenticity guarantee. "
+ "Threat postures: a malicious manifest contributor introduces a ``dfetch.yaml`` "
+ "change that redirects a dep to an attacker-controlled URL, points ``dst:`` at a "
+ "sensitive path, or embeds a credential-bearing URL — dfetch is not the control "
+ "point; code review at the PR boundary is the intended mitigating control. "
+ "A local-filesystem attacker with write access to the working tree (gained via a "
+ "compromised dev dependency, malicious post-install hook, or lateral movement) can "
+ "tamper with ``.dfetch_data.yaml``, patch files, and vendored source after dfetch "
+ "writes them to disk."
)
manifest_store.storesSensitiveData = (
False # config only (URLs, pins, hashes), not credentials/PII
@@ -244,7 +270,7 @@ def _make_usage_datastores_a(
sbom_output.hasWriteAccess = True
sbom_output.isSQL = False
sbom_output.classification = Classification.RESTRICTED
- return manifest_store, fetched_source, sbom_output
+ return manifest_store, fetched_source, sbom_output, integrity_hash_record
def _make_usage_datastores_b(b_dev: Boundary) -> tuple[Data, Datastore, Datastore]:
@@ -530,7 +556,7 @@ def _make_usage_patch_and_build_flows(
def _make_usage_extraction_elements_and_flows(
b_dev: Boundary, dfetch_cli: Process
) -> None:
- """Create archive/SVN extraction elements and their dataflows; all auto-register with TM."""
+ """Create archive/SVN extraction elements, their dataflows, and the cache-copy flow."""
b_archive = Boundary("Archive Content Space")
b_archive.description = (
"Downloaded archive bytes before extraction and validation. "
@@ -636,6 +662,150 @@ def _make_usage_extraction_elements_and_flows(
"dfetch check writes SARIF, Jenkins warnings-ng, or Code Climate JSON; "
"falsification hides vulnerabilities from downstream dashboards."
)
+ df22 = Dataflow(
+ local_vcs_cache, dfetch_cli, "DF-22: Copy extracted content to vendor directory"
+ )
+ df22.description = (
+ "dfetch reads the validated content from the temporary directory (A-20) and "
+ "copies it to the ``dst:`` path declared in the manifest. "
+ "``check_no_path_traversal()`` is applied before each file write (C-001); "
+ "symlinks are validated post-extraction."
+ )
+ df22.controls.sanitizesInput = True
+ df22.controls.isHardened = True
+
+
+def _make_usage_integrity_flows(
+ dfetch_cli: Process,
+ manifest_store: Datastore,
+ integrity_hash_record: Datastore,
+ developer: Actor,
+) -> None:
+ """Wire integrity-hash read/write and manifest-authorship flows."""
+ df18 = Dataflow(
+ integrity_hash_record,
+ dfetch_cli,
+ "DF-18: Read integrity hash for archive verification",
+ )
+ df18.description = (
+ "dfetch reads the ``integrity.hash`` field (A-14) from the manifest when "
+ "verifying a downloaded archive. The hash is compared with the content "
+ "digest via ``hmac.compare_digest`` (constant-time). "
+ "When the field is absent no verification occurs (gap C-018)."
+ )
+ df18.controls.providesIntegrity = True
+ df18.controls.validatesInput = True
+
+ df18b = Dataflow(
+ dfetch_cli,
+ integrity_hash_record,
+ "DF-18b: Write computed hash to manifest (dfetch freeze)",
+ )
+ df18b.description = (
+ "``dfetch freeze`` computes the SHA-256/384/512 digest of a downloaded archive "
+ "and writes the ``integrity.hash`` value back into ``dfetch.yaml`` (A-14). "
+ "This is the recommended mechanism for bootstrapping a hash-pinned manifest "
+ "entry; without it the field remains absent and no content verification occurs."
+ )
+ df18b.controls.providesIntegrity = True
+
+ df20 = Dataflow(developer, manifest_store, "DF-20: Author / maintain dfetch.yaml")
+ df20.description = (
+ "Developer writes and updates ``dfetch.yaml`` (A-12): selecting upstream "
+ "sources, pinning revisions, setting ``dst:`` paths, adding ``integrity.hash`` "
+ "fields, and listing patch references. "
+ "The manifest is subject to code review; a malicious change can redirect "
+ "any dependency fetch to an attacker-controlled URL (assumption: Manifest under "
+ "code review)."
+ )
+ df20.controls.hasAccessControl = True
+
+
+def _make_usage_gap_doc_flows(
+ developer: Actor,
+ patch_store: Datastore,
+ remote_git_svn: ExternalEntity,
+ upstream_source_attestation: Datastore,
+) -> None:
+ """Wire patch-file authorship and VSA gap-documentation flows."""
+ df19 = Dataflow(
+ remote_git_svn,
+ upstream_source_attestation,
+ "DF-19: VCS server publishes source attestation (not consumed by dfetch)",
+ )
+ df19.description = (
+ "An upstream VCS host with SLSA Source Level support may publish a "
+ "Source Provenance Attestation or Verification Summary Attestation (VSA) "
+ "alongside repository content. "
+ "CRITICAL: dfetch has no mechanism to request or verify these attestations "
+ "(gap C-035). The flow is modelled to document the gap — dfetch does not "
+ "fetch or validate A-23 at any point in its current implementation."
+ )
+ df19.controls.providesIntegrity = False
+
+ df21 = Dataflow(developer, patch_store, "DF-21: Create / maintain patch files")
+ df21.description = (
+ "Developer authors unified-diff ``.patch`` files referenced by ``patch:`` "
+ "entries in the manifest (A-19). "
+ "Patch files carry no integrity hash and are applied directly by patch-ng; "
+ "a tampered or attacker-supplied patch can write to arbitrary paths (gap C-020)."
+ )
+
+
+def _make_usage_stores_and_flows( # pylint: disable=too-many-arguments,too-many-positional-arguments
+ b_dev: Boundary,
+ developer: Actor,
+ remote_git_svn: ExternalEntity,
+ archive_server: ExternalEntity,
+ consumer_build: ExternalEntity,
+ upstream_source_attestation: Datastore,
+ dfetch_cli: Process,
+ patch_apply: Process,
+) -> None:
+ """Create datastores and wire all usage-model dataflows."""
+ manifest_store, fetched_source, sbom_output, integrity_hash_record = (
+ _make_usage_datastores_a(b_dev)
+ )
+ embedded_url_credential, metadata_store, patch_store = _make_usage_datastores_b(
+ b_dev
+ )
+ _make_usage_vcs_dataflows(developer, dfetch_cli, manifest_store, remote_git_svn)
+ _make_usage_archive_dataflows(dfetch_cli, archive_server)
+ _make_usage_output_flows(
+ dfetch_cli, fetched_source, metadata_store, sbom_output, embedded_url_credential
+ )
+ _make_usage_patch_and_build_flows(
+ patch_apply, patch_store, fetched_source, consumer_build
+ )
+ _make_usage_extraction_elements_and_flows(b_dev, dfetch_cli)
+ _make_usage_integrity_flows(
+ dfetch_cli, manifest_store, integrity_hash_record, developer
+ )
+ _make_usage_gap_doc_flows(
+ developer, patch_store, remote_git_svn, upstream_source_attestation
+ )
+
+
+def _make_usage_elements_and_flows(b_dev: Boundary, b_remote_vcs: Boundary) -> None:
+ """Create all usage-model elements and wire their dataflows."""
+ (
+ developer,
+ remote_git_svn,
+ archive_server,
+ consumer_build,
+ upstream_source_attestation,
+ ) = _make_usage_actors_and_external_entities(b_dev, b_remote_vcs)
+ dfetch_cli, patch_apply = _make_usage_processes(b_dev)
+ _make_usage_stores_and_flows(
+ b_dev,
+ developer,
+ remote_git_svn,
+ archive_server,
+ consumer_build,
+ upstream_source_attestation,
+ dfetch_cli,
+ patch_apply,
+ )
def build_model() -> TM:
@@ -656,25 +826,9 @@ def build_model() -> TM:
mergeResponses=True,
threatsFile=THREATS_FILE,
)
- model.assumptions = make_usage_assumptions() + make_attacker_profiles()
+ model.assumptions = make_usage_assumptions()
b_dev, b_remote_vcs = _make_usage_boundaries()
- developer, remote_git_svn, archive_server, consumer_build = (
- _make_usage_actors_and_external_entities(b_dev, b_remote_vcs)
- )
- dfetch_cli, patch_apply = _make_usage_processes(b_dev)
- manifest_store, fetched_source, sbom_output = _make_usage_datastores_a(b_dev)
- embedded_url_credential, metadata_store, patch_store = _make_usage_datastores_b(
- b_dev
- )
- _make_usage_vcs_dataflows(developer, dfetch_cli, manifest_store, remote_git_svn)
- _make_usage_archive_dataflows(dfetch_cli, archive_server)
- _make_usage_output_flows(
- dfetch_cli, fetched_source, metadata_store, sbom_output, embedded_url_credential
- )
- _make_usage_patch_and_build_flows(
- patch_apply, patch_store, fetched_source, consumer_build
- )
- _make_usage_extraction_elements_and_flows(b_dev, dfetch_cli)
+ _make_usage_elements_and_flows(b_dev, b_remote_vcs)
return model