diff --git a/doc/conf.py b/doc/conf.py index 300f45b4..61215077 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -117,11 +117,14 @@ # -- Options for autosectionlabel extension ------------------------------- autosectionlabel_maxdepth = 3 -# Suppress warnings about duplicate labels from argparse directive +# Suppress warnings about duplicate labels from argparse directive and +# from the generated threat-model pages (which share section names). suppress_warnings = [ "autosectionlabel.reference/commands", "autosectionlabel.reference/manifest", "autosectionlabel.howto/updating-projects", + "autosectionlabel.explanation/threat_model_supply_chain", + "autosectionlabel.explanation/threat_model_usage", ] # Options for sphinx-autoissues diff --git a/doc/explanation/security.rst b/doc/explanation/security.rst index b40ab932..77e4ed37 100644 --- a/doc/explanation/security.rst +++ b/doc/explanation/security.rst @@ -95,3 +95,16 @@ to reproduce a deterministic dependency state. - Execution in CI environments with insufficient network or secret isolation may allow exfiltration risks if upstream sources are compromised or intentionally malicious. + +Threat Models +------------- + +The following pages document the two threat models in detail. Each page is +generated from the corresponding Python module in ``security/`` — see +``security/README.md`` for instructions on regenerating them. + +.. toctree:: + :maxdepth: 1 + + threat_model_supply_chain + threat_model_usage diff --git a/doc/explanation/threat_model_supply_chain.rst b/doc/explanation/threat_model_supply_chain.rst new file mode 100644 index 00000000..2629b25d --- /dev/null +++ b/doc/explanation/threat_model_supply_chain.rst @@ -0,0 +1,372 @@ +.. ============================================================ +.. Auto-generated file — do not edit manually. +.. Regenerate with (see security/README.md for exact commands): +.. +.. python -m security.tm_ \ +.. --report security/report_template.rst \ +.. > doc/explanation/threat_model_.rst +.. ============================================================ + +System Description +------------------ + +Threat model for dfetch. Covers the pre-install lifecycle: code contribution, CI/CD, build (wheel / sdist), PyPI distribution, and consumer installation. The installed dfetch package is the handoff point to tm_usage.py. + +Assumptions +----------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Name + - Description + + * - Trusted workstation + - Developer workstations are trusted at development and commit time. A compromised workstation is outside the scope of this model. + + * - 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. + + +Dataflows +--------- + +.. list-table:: + :header-rows: 1 + :widths: 35 20 20 25 + + * - Name + - From + - To + - Protocol + + * - DF-11: Submit pull request + - Contributor / Attacker + - A-01: GitHub Repository + - HTTPS + + * - DF-14: pip install dfetch + - Consumer / End User + - A-03: PyPI / TestPyPI + - HTTPS + + * - DF-12: CI checkout and build + - A-01: GitHub Repository + - A-02: GitHub Actions Infrastructure + - + + * - DF-13: Publish to PyPI (OIDC) + - A-02: GitHub Actions Infrastructure + - A-03: PyPI / TestPyPI + - HTTPS + + * - 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 + - A-02: GitHub Actions Infrastructure + - HTTPS + + * - DF-17: CI write-back (SARIF / artifacts / cache) + - A-02: GitHub Actions Infrastructure + - A-01: GitHub Repository + - HTTPS + + +Data Dictionary +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Name + - Description + - Classification + + * - A-05: PyPI OIDC Identity + - GitHub OIDC token exchanged for a short-lived PyPI publish credential. No long-lived API token stored. The token is scoped to the GitHub Actions environment named ``pypi``. Risk: if the OIDC issuer or the PyPI trusted-publisher mapping is misconfigured, an attacker could mint a valid publish token. + - SECRET + + +Actors +------ + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - 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. + + * - 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 four complementary attestation types using ``gh attestation verify`` as documented in the release-integrity guide (see C-026, C-039, C-040): SBOM attestation on the PyPI wheel; SBOM, SLSA build provenance, and VSA on binary installers; SLSA build provenance and in-toto test result attestation on the source archive. + + +Boundaries +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Name + - Description + + * - 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``). + + * - 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 +------ + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Name + - 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. + - ExternalEntity + + * - A-02: GitHub Actions Infrastructure + - Microsoft-operated ephemeral runner executing CI/CD workflows. Egress policy is ``block`` with an explicit allowlist of permitted endpoints - non-allowlisted outbound connections are blocked at the kernel level by ``step-security/harden-runner``. + - 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. + - ExternalEntity + + * - Release Gate / Code Review + - Branch-protection rules and mandatory peer code review enforced before merging to the default branch. Controls privileged operations: PR merge, direct push to main, and release-workflow trigger. A compromised maintainer account with merge rights bypasses peer review and can trigger a malicious release without any automated block. No hardware-token MFA or mandatory second-maintainer approval for release operations is currently enforced. + - Process + + * - GitHub Actions Workflow + - CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, dependency-review, docs, release. All actions pinned by commit SHA. harden-runner used in every workflow that executes steps on a runner (egress: block with endpoint allowlist); ci.yml is a dispatcher-only workflow with no runner steps and does not include harden-runner. + - Process + + * - Python Build (wheel / sdist) + - 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 + + + + + +Controls +-------- + +.. list-table:: + :header-rows: 1 + :widths: 8 20 8 14 15 35 + + * - ID + - Name + - Risk + - STRIDE + - Threats + - Description + * - C-009 + - Actions commit-SHA pinning + - High + - Tampering + - DFT-07 + - Mitigates: Every third-party GitHub Action is pinned to a full commit SHA, preventing tag-mutable supply-chain substitution. ``.github/workflows/*.yml`` + * - C-010 + - OIDC trusted publishing + - High + - Spoofing, Elevation of Privilege + - DFT-07 + - Mitigates: PyPI publishes via ``pypa/gh-action-pypi-publish`` with ``id-token: write`` and no stored long-lived API token. ``.github/workflows/python-publish.yml`` + * - C-011 + - Minimal workflow permissions + - Medium + - Elevation of Privilege + - DFT-07 + - Mitigates: Each workflow declares only the permissions it requires (default ``contents: read``). ``.github/workflows/*.yml`` + * - C-012 + - persist-credentials: false + - Medium + - Information Disclosure + - DFT-07 + - Mitigates: ``persist-credentials: false`` is set on all checkout steps across all workflows that run on a runner. The GitHub token is not persisted in the working tree after checkout. ``.github/workflows/*.yml`` + * - C-013 + - Harden-runner (egress block) + - High + - 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 + - Tampering, Elevation of Privilege + - DFT-03, DFT-06 + - Mitigates: CodeQL scans the Python codebase for security vulnerabilities on pushes and pull requests targeting ``main``, and on a weekly cron schedule. ``.github/workflows/codeql-analysis.yml`` + * - C-016 + - Dependency review + - Medium + - Tampering + - DFT-10 + - Mitigates: ``actions/dependency-review-action`` checks for known vulnerabilities in newly added dependencies on every pull request. ``.github/workflows/dependency-review.yml`` + * - C-017 + - bandit security linter + - Low + - Tampering, Elevation of Privilege + - DFT-03, DFT-06 + - Mitigates: ``bandit -r dfetch`` runs in CI to detect common Python security issues. ``pyproject.toml`` + * - C-021 + - Sigstore SBOM attestation + - Medium + - Spoofing, Tampering + - DFT-05 + - Mitigates: 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``. + * - C-022 + - CycloneDX SBOM on PyPI + - Low + - Repudiation + - DFT-02 + - Mitigates: A CycloneDX SBOM is generated during the build and published alongside the PyPI release, satisfying CRA Article 13 requirements. + * - C-024 + - ``secrets: inherit`` scope + - Medium + - Information Disclosure + - DFT-07 + - Mitigates: ``ci.yml`` only passes required repository secrets to the test and docs workflows, preventing malicious PR steps from exfiltrating unrelated secrets. + * - C-026 + - Consumer-side package provenance verification + - Medium + - Spoofing, Tampering + - DFT-17, DFT-25 + - Mitigates: 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. ``doc/tutorials/installation.rst#verifying-release-integrity`` + * - C-032 + - Consumer attestation verification pins to release tag ref + - Medium + - Tampering, Spoofing + - DFT-27 + - Mitigates: 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. ``doc/tutorials/installation.rst`` + * - C-033 + - Ref-scoped build cache keys isolate PR and release builds + - High + - Tampering + - DFT-28 + - Mitigates: 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. ``.github/workflows/build.yml`` + * - C-038 + - Ancestry enforcement on dfetch main branch + - Low + - Tampering + - DFT-33 + - Mitigates: 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. ``.github/workflows/`` + * - C-039 + - Source build provenance and VSA attestations + - High + - Spoofing, Tampering, Repudiation + - DFT-31, DFT-25 + - Mitigates: 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 remaining gap (no formal SLSA Source Level attestation of governance controls) is tracked in C-037. ``doc/howto/verify-integrity.rst`` + * - C-040 + - Test result attestation on source archive + - Medium + - Repudiation, Tampering + - DFT-31 + - Mitigates: 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. ``.github/workflows/test.yml`` + + +Gaps +---- + +.. list-table:: + :header-rows: 1 + :widths: 8 20 8 14 15 35 + + * - ID + - Name + - Risk + - STRIDE + - Threats + - Description + * - C-023 + - Build deps without hash pinning + - High + - Tampering + - DFT-10 + - Affects: ``pip install .`` and ``pip install --upgrade pip build`` in CI do not use ``--require-hashes``. A compromised PyPI mirror can substitute malicious build tooling. + * - C-037 + - No formal SLSA Source Level attestation of repository governance controls + - Low + - Repudiation, Spoofing + - DFT-31 + - Affects: dfetch now publishes SLSA build provenance for source archives, VSAs for binary installers (C-039), and in-toto test result attestations (C-040). These close the 'no attestation to verify against' concern: consumers can cryptographically verify the artifact chain. The remaining, narrower gap is that dfetch does not publish formal SLSA Source Provenance Attestations generated by a SLSA Source Generator — attestations that would prove the specific source-level governance controls applied on each commit (branch protection, mandatory code review, ancestry enforcement). C-038 establishes that ancestry enforcement is in place and C-026 documents what consumers can verify; the gap is in machine-readable, verifiable proof of those governance controls rather than the controls themselves. Risk is Low: the missing piece is a formal SLSA Source Level certificate (per the SLSA Source Track spec) rather than the absence of any assurance. Fix: publish Source Provenance Attestations via ``slsa-framework/slsa-source-corroborator`` or equivalent on each push to main. + * - C-025 + - No hardware-token MFA for release operations + - High + - 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 new file mode 100644 index 00000000..2341ee3c --- /dev/null +++ b/doc/explanation/threat_model_usage.rst @@ -0,0 +1,477 @@ +.. ============================================================ +.. Auto-generated file — do not edit manually. +.. Regenerate with (see security/README.md for exact commands): +.. +.. python -m security.tm_ \ +.. --report security/report_template.rst \ +.. > doc/explanation/threat_model_.rst +.. ============================================================ + +System Description +------------------ + +Threat model for dfetch. Covers the post-install lifecycle: reading the manifest, fetching dependencies from VCS and archive sources, applying patches, writing vendored files, and generating reports (SBOM, SARIF, check output). The installed dfetch package - produced by the supply chain in tm_supply_chain.py - is the entry point. + +Assumptions +----------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Name + - Description + + * - Trusted workstation + - Developer workstations are trusted at dfetch invocation time. A compromised workstation is outside the scope of this threat model. + + * - TLS delegated to client + - TLS certificate validation is delegated to the OS trust store and the git / svn / urllib clients. dfetch does not independently validate certificates. + + * - No persisted secrets + - No runtime secrets are persisted to disk by dfetch itself. VCS credentials are managed by the OS keychain, SSH agent, or CI secret store. + + * - Optional integrity hash + - The ``integrity.hash`` field in the manifest is optional. Archive dependencies without it have no content-authenticity guarantee beyond TLS transport, which is itself absent for plain ``http://`` URLs. + + * - Mutable VCS references + - Branch- and tag-pinned Git dependencies are mutable references. Upstream force-pushes silently change what is fetched without triggering a manifest diff. + + * - Manifest under code review + - The manifest (``dfetch.yaml``) is under version control and subject to code review. An adversary with write access to the manifest can redirect fetches to attacker-controlled sources; this threat is addressed at the code-review boundary, not within dfetch itself. + + * - dfetch scope boundary + - dfetch is responsible only for its own security posture. The security of fetched third-party source code is the responsibility of the manifest author who selects and pins each dependency. + + * - No HTTPS enforcement + - HTTPS enforcement is the responsibility of the manifest author. dfetch accepts ``http://``, ``svn://``, and other non-TLS scheme URLs as written - it does not upgrade or reject them. + + * - 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. + + +Dataflows +--------- + +.. list-table:: + :header-rows: 1 + :widths: 35 20 20 25 + + * - Name + - From + - To + - Protocol + + * - DF-01: Invoke dfetch command + - Developer + - A-22: dfetch Process + - + + * - DF-02: Read manifest + - A-12: dfetch Manifest + - A-22: dfetch Process + - + + * - DF-03a: Fetch VCS content - HTTPS/SSH + - A-22: dfetch Process + - A-09: Remote VCS Server + - HTTPS / SSH + + * - DF-03b: Fetch VCS content - svn:// / http:// + - A-22: dfetch Process + - A-09: Remote VCS Server + - http / svn + + * - DF-04a: VCS content inbound - HTTPS/SSH + - A-09: Remote VCS Server + - A-22: dfetch Process + - HTTPS / SSH + + * - DF-04b: VCS content inbound - svn:// / http:// + - A-09: Remote VCS Server + - A-22: dfetch Process + - http / svn + + * - DF-05a: Archive download request - HTTPS + - A-22: dfetch Process + - A-10: Archive HTTP Server + - HTTPS + + * - DF-05b: Archive download request - HTTP + - A-22: dfetch Process + - A-10: Archive HTTP Server + - HTTP + + * - DF-06a: Archive bytes - HTTPS + - A-10: Archive HTTP Server + - A-22: dfetch Process + - HTTPS + + * - DF-06b: Archive bytes - HTTP (plaintext risk) + - A-10: Archive HTTP Server + - A-22: dfetch Process + - HTTP + + * - DF-07: Write vendored files + - A-22: dfetch Process + - A-13: Fetched Source Code + - + + * - DF-08: Write dependency metadata + - A-22: dfetch Process + - A-18: Dependency Metadata + - + + * - DF-09: Write SBOM + - A-22: dfetch Process + - A-15: SBOM Output (CycloneDX) + - + + * - DF-16: Read dependency metadata + - A-18: Dependency Metadata + - A-22: dfetch Process + - + + * - DF-10: Read patch for application + - A-19: Patch Files + - Patch Application (patch-ng) + - + + * - DF-10b: Write patched files to vendor directory + - Patch Application (patch-ng) + - A-13: Fetched Source Code + - + + * - DF-15: Vendored source to build + - A-13: Fetched Source Code + - A-11: Consumer Build System + - + + * - DF-11: Dispatch archive bytes to extraction + - A-22: dfetch Process + - Archive Extraction (tarfile / zipfile) + - + + * - DF-12: Write extracted archive to temp dir + - Archive Extraction (tarfile / zipfile) + - A-20: Local VCS Cache (temp) + - + + * - DF-13: Dispatch SVN export subprocess + - A-22: dfetch Process + - SVN Export (svn export) + - + + * - DF-14: Write SVN export to temp dir + - SVN Export (svn export) + - A-20: Local VCS Cache (temp) + - + + * - DF-17: Write audit / check reports + - A-22: dfetch Process + - A-21: Audit / Check Reports + - + + +Data Dictionary +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Name + - Description + - Classification + + * - A-16: VCS Credentials + - SSH private keys, HTTPS Personal Access Tokens, SVN passwords. Used to authenticate to private upstream repositories. dfetch never persists these - managed by OS keychain, SSH agent, or CI secret store. + - SECRET + + * - A-17: Embedded Credential in Remote URL + - A VCS or archive URL that encodes a credential in the userinfo component (e.g. ``https://user:TOKEN@github.com/org/repo.git``). dfetch writes ``remote_url`` verbatim to ``.dfetch_data.yaml`` after each successful fetch. If the URL contains a Personal Access Token or password, that credential is persisted in plaintext and typically committed to VCS, where it becomes readable from every clone and CI checkout indefinitely. + - SECRET + + +Actors +------ + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Name + - Description + + * - Developer + - Writes and reviews ``dfetch.yaml``; selects upstream sources, pins revisions, and optionally enables ``integrity.hash`` for archive dependencies. Trusted at workstation invocation time. Responsible for choosing trustworthy upstream sources and keeping pins current. + + +Boundaries +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Name + - Description + + * - 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. + + * - Internet + - All traffic crossing the local/remote boundary. TLS enforcement is the responsibility of the OS and VCS clients; dfetch does not enforce HTTPS on manifest URLs. + + * - Remote VCS Infrastructure + - Upstream Git and SVN servers (GitHub, GitLab, Gitea, self-hosted). Not controlled by the dfetch project; content is untrusted until verified. + + * - Archive Content Space + - Downloaded archive bytes before extraction and validation. Decompression-bomb and path-traversal checks enforce this boundary during extraction. + + +Assets +------ + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Name + - Description + - Type + + * - A-09: Remote VCS Server + - Upstream Git or SVN host: GitHub, GitLab, Gitea, self-hosted Git/SVN. 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). + - ExternalEntity + + * - A-10: Archive HTTP Server + - 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. + - ExternalEntity + + * - A-23: Upstream Source Attestation (VSA) + - SLSA Source Provenance Attestation or Verification Summary Attestation (VSA) that an upstream VCS host can publish for a specific revision, attesting that the source-level controls required by a given SLSA source level - branch protection, mandatory review, and ancestry enforcement - were applied. CRITICAL: dfetch has no mechanism to request or verify source-level attestations and the manifest schema has no field to declare an expected SLSA source level. In the absence of a VSA the consumer cannot cryptographically distinguish a well-governed upstream from one with no controls at all. + - Datastore + + * - A-11: Consumer Build System + - Build system that compiles fetched source code (A-13). Not controlled by dfetch - it receives untrusted third-party source. + - ExternalEntity + + * - A-22: dfetch Process + - Python CLI entry point dispatching to: update, check, diff, add, remove, update-patch, format-patch, freeze, import, init, report, validate, environment. Invokes Git and SVN as subprocesses (``shell=False``, list args). Extracts archives with decompression-bomb limits and path-traversal checks. + - Process + + * - Patch Application (patch-ng) + - Invokes ``patch-ng`` to apply unified-diff files from ``patch:`` references in the manifest. Path safety is delegated entirely to ``patch-ng``'s internal implementation - dfetch does not independently sanitise patch-file destination paths before handing off to the library. + - Process + + * - A-12: dfetch Manifest + - ``dfetch.yaml`` - declares all upstream sources (URL/VCS type), version pins (branch / tag / revision / SHA), dst paths, patch references, and optional 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. + - Datastore + + * - A-13: Fetched Source Code + - Third-party source code written to the ``dst:`` path after extraction / checkout. Becomes a direct build input for the consuming project. A compromised upstream or MITM can inject malicious code that executes in the consumer's build system, test runner, or production binary. + - Datastore + + * - A-14: Integrity Hash Record + - ``integrity.hash:`` field in ``dfetch.yaml`` (sha256/sha384/sha512:). Primary trust anchor for archive-type dependencies when present. Verified via ``hmac.compare_digest`` (constant-time). Field is optional - absence disables content verification for archives (C-018). Git and SVN have no equivalent; they rely entirely on transport security (C-019). + - Datastore + + * - A-15: SBOM Output (CycloneDX) + - CycloneDX JSON/XML produced by ``dfetch report -t sbom``. Enumerates vendored components with PURL, license, and hash. Falsification hides actual dependencies from downstream CVE scanners. NOTE: this SBOM covers vendored deps only - dfetch itself has a separate machine-readable SBOM published on PyPI (see A-04 in tm_supply_chain.py). + - Datastore + + * - A-18: Dependency Metadata + - ``.dfetch_data.yaml`` files written after each successful fetch. Contains: remote_url, revision/branch/tag, hash, last-fetch timestamp. Read by ``dfetch check`` to detect outdated deps. Tampering can suppress update notifications - an attacker who controls the local filesystem can silently mask a compromised vendored dep. + - Datastore + + * - A-19: Patch Files + - Unified-diff ``.patch`` files referenced by ``patch:`` in ``dfetch.yaml``. Applied by ``patch-ng`` after fetch. A malicious patch can write to arbitrary destination paths - dfetch's path-traversal guards apply to archive extraction but ``patch-ng``'s own path safety depends on its internal implementation. Patch files are not integrity-verified (no hash in manifest schema). + - Datastore + + * - Archive Extraction (tarfile / zipfile) + - Decompresses and extracts TAR (.tar.gz/.tgz/.tar.bz2/.tar.xz) and ZIP archives to a temporary directory. Pre-extraction checks validate decompression-bomb limits, path traversal, symlinks, hardlinks, device files, and FIFOs. On Python ≥ 3.11.4: ``filter='tar'`` strips setuid/setgid bits during extraction. On Python < 3.11.4: ``extractall()`` is called without a filter - setuid, setgid, and sticky bits from TAR member headers are preserved on the extracted files, allowing a malicious archive to introduce setuid-root binaries into the vendor directory. + - Process + + * - SVN Export (svn export) + - Runs ``svn export --non-interactive --force`` to check out SVN dependencies. The ``--ignore-externals`` flag is NOT passed. SVN repositories with ``svn:externals`` properties will trigger additional fetches from third-party SVN servers not declared in ``dfetch.yaml``. These undeclared fetches bypass dfetch's manifest controls: no integrity hash, no metadata record, and no code review of the external URL. + - Process + + * - A-20: Local VCS Cache (temp) + - Temporary directory used during git-clone / svn-checkout / archive extraction. Deleted after content is copied to dst. Path-traversal attacks targeting this space are mitigated by ``check_no_path_traversal()`` and post-extraction symlink walks. + - Datastore + + * - A-21: Audit / Check Reports + - SARIF, Jenkins warnings-ng, Code Climate JSON produced by ``dfetch check``. Falsification hides vulnerabilities from downstream security dashboards. + - Datastore + + + + + +Controls +-------- + +.. list-table:: + :header-rows: 1 + :widths: 8 20 8 14 15 35 + + * - ID + - Name + - Risk + - STRIDE + - Threats + - Description + * - C-001 + - Path-traversal prevention + - High + - Tampering, Elevation of Privilege + - DFT-03 + - Mitigates: ``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. ``dfetch/util/util.py`` + * - C-002 + - Decompression-bomb protection + - Medium + - Denial of Service + - DFT-09 + - Mitigates: Archives are rejected if the uncompressed size exceeds 500 MB or the member count exceeds 10 000. ``dfetch/vcs/archive.py`` + * - C-003 + - Archive symlink validation + - High + - Tampering, Elevation of Privilege + - DFT-03 + - Mitigates: Absolute and escaping (``..``) symlink targets are rejected for both TAR and ZIP. A post-extraction walk validates all symlinks against the manifest root. ``dfetch/vcs/archive.py`` + * - C-004 + - Archive member type checks + - Medium + - Tampering, Elevation of Privilege + - DFT-03 + - Mitigates: TAR and ZIP members of type device file or FIFO are rejected outright. ``dfetch/vcs/archive.py`` + * - C-005 + - Integrity hash verification + - Critical + - Tampering, Spoofing + - DFT-01, DFT-02, DFT-05, DFT-30 + - Mitigates: 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 - see C-018. ``dfetch/vcs/integrity_hash.py`` + * - C-006 + - Non-interactive VCS + - Low + - Spoofing + - DFT-06 + - Mitigates: ``GIT_TERMINAL_PROMPT=0``, ``BatchMode=yes`` for Git; ``--non-interactive`` for SVN. Credential prompts are suppressed to prevent interactive hijacking in CI. ``dfetch/vcs/git.py, dfetch/vcs/svn.py`` + * - C-007 + - Subprocess safety + - High + - Tampering, Elevation of Privilege + - DFT-06 + - Mitigates: All external commands invoked with ``shell=False`` and list-form arguments - no shell-injection vector. ``dfetch/util/cmdline.py`` + * - C-008 + - Manifest input validation + - High + - Tampering + - DFT-04, DFT-08 + - Mitigates: StrictYAML schema with ``SAFE_STR = Regex(r"^[^\x00-\x1F\x7F-\x9F]*$")`` rejects control characters in all string fields. ``dfetch/manifest/schema.py`` + * - C-034 + - Hash algorithm allowlist (SHA-256/384/512 only) + - High + - Tampering, Spoofing + - DFT-30 + - Mitigates: ``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. ``dfetch/vcs/integrity_hash.py`` + * - C-029 + - Nested dependency references in vendored source are out of dfetch scope + - Low + - Tampering + - DFT-22 + - Mitigates: DFT-22 (vendored content containing submodule or nested external references) describes threats that arise when the consuming build system processes build manifests embedded in vendored source (CMakeLists.txt, package.json, Cargo.toml, etc.). Per the 'dfetch scope boundary' assumption, dfetch exports source files - it does not execute or resolve build-system instructions within them. The responsibility for nested dependency resolution belongs to the manifest author and the consuming build system. Consumers should audit vendored repositories for nested package manifests and treat all fetched source as untrusted. + + +Gaps +---- + +.. list-table:: + :header-rows: 1 + :widths: 8 20 8 14 15 35 + + * - ID + - Name + - Risk + - STRIDE + - Threats + - Description + * - C-018 + - Optional integrity hash + - Critical + - Tampering, Spoofing + - DFT-01, DFT-02 + - Affects: ``integrity.hash`` in the manifest is optional. Archive dependencies without it have no content-authenticity guarantee. Plain ``http://`` URLs receive no protection at all - neither transport nor content integrity is enforced. + * - C-019 + - No integrity mechanism for Git/SVN + - High + - Tampering, Spoofing + - DFT-02, DFT-05 + - Affects: Git and SVN dependencies carry no equivalent to ``integrity.hash``. Transport security (TLS or SSH) authenticates the server and channel but cannot detect content legitimately served by a compromised upstream. Mutable references (branch, tag) can silently fetch different content after a force-push. Pinning to an immutable commit SHA is the strongest available mitigation but is not currently enforced by dfetch. + * - C-020 + - No patch-file integrity + - Critical + - Tampering, Elevation of Privilege + - DFT-08, DFT-03 + - Affects: Patch files referenced in the manifest carry no integrity hash. A tampered or attacker-controlled patch file can write to arbitrary paths; path-safety is delegated entirely to ``patch-ng``'s internal implementation and is not independently verified by dfetch before application. + * - C-027 + - No redirect destination validation on archive downloads + - High + - Information Disclosure + - DFT-12 + - Affects: Archive downloads follow up to 10 HTTP 3xx redirects without validating the destination host against an allowlist. A compromised or malicious archive server can redirect to an internal metadata endpoint (e.g. ``http://169.254.169.254/``) and dfetch will follow the redirect, potentially retrieving and writing internal credentials to disk. Plain ``http://`` URLs amplify the risk - both the original request and the redirect are unencrypted. Fix: reject redirects resolving to RFC-1918, loopback, or link-local ranges before following. + * - C-028 + - No denylist for security-sensitive destination paths + - High + - Tampering, Elevation of Privilege + - DFT-16 + - Affects: dfetch's path-traversal check (C-001) prevents writes outside the project root but does not maintain a denylist of security-sensitive within-root paths. A manifest entry with ``dst: .github/workflows/`` would pass all current validation and silently overwrite CI pipeline definitions on the next ``dfetch update``. Should warn or error when ``dst:`` resolves under known-sensitive prefixes (``.github/``, ``.circleci/``, ``Makefile``, etc.), or require all destinations to fall under an explicit vendor root. + * - C-030 + - No timeout or resource cap on VCS operations + - Low + - Denial of Service + - DFT-09 + - Affects: Git clone and SVN export operations have no configurable timeout or maximum-transfer-size limit. A pathological or compromised upstream can deliver arbitrarily large packfiles or working trees, causing disk exhaustion or prolonged CI runner occupation. The 500 MB / 10k-member archive limits (C-002) do not apply to raw VCS checkouts. Shallow clones (``--depth=1``) mitigate history size but do not bound working-tree size. + * - C-031 + - No verification of signed Git tags or Sigstore attestations + - Medium + - Spoofing, Tampering + - DFT-05, DFT-21 + - Affects: dfetch does not verify GPG-signed Git tags or Sigstore tag attestations when resolving VCS dependencies. Upstreams that publish signed releases offer a stronger authenticity guarantee than transport security alone, but dfetch cannot take advantage of it. This is distinct from C-019: signing would authenticate which commit a tag names, not the content of the working tree. For repositories that do publish signed releases, users should pin to the commit SHA recorded after manually running ``git tag -v``. + * - C-035 + - No upstream SLSA source level verification + - Medium + - Spoofing, Tampering + - DFT-31, DFT-32 + - Affects: dfetch has no mechanism to check whether an upstream VCS source publishes or meets any SLSA source level. The manifest schema has no field for declaring the expected SLSA source level of a dependency, and no tooling exists to fetch or verify Source Provenance Attestations or VSAs (A-23). Consumers must manually assess the upstream's review, branch-protection, and ancestry-enforcement posture before trusting a dependency pin - this is undocumented and unenforced by dfetch. DFT-31 (no VSA) and DFT-32 (no two-party review) both stem from this gap. Fix: add an optional ``slsa_source_level:`` field to the manifest schema and implement VSA fetch-and-verify during ``dfetch update``. + * - C-036 + - No ancestry reachability check after VCS fetch + - Low + - Tampering + - DFT-33 + - Affects: dfetch does not verify whether a pinned commit SHA remains reachable from the upstream's current default branch after fetching. An upstream that rewrites its history - interactive rebase, filter-branch, or force-push to main - can orphan a previously-audited SHA without triggering any alert. SLSA Source Level 2 requires ancestry enforcement: the upstream must prevent such rewrites. dfetch cannot enforce this on the upstream, but could detect lineage breaks post-fetch. Fix: after fetching a commit SHA, run ``git merge-base --is-ancestor /`` and warn when the SHA is unreachable. + * - C-041 + - SVN externals not suppressed (--ignore-externals flag unused) + - High + - Tampering + - DFT-15 + - Affects: ``svn export`` is invoked without the ``--ignore-externals`` flag. SVN repositories with ``svn:externals`` properties trigger undeclared fetches from third-party servers that bypass dfetch's manifest controls, integrity checks, and code-review audit trail. Each external fetch is unverified and unavailable for inspection before vendoring. ``dfetch/vcs/svn.py`` + * - C-042 + - setuid/setgid bits preserved during TAR extraction on Python < 3.11.4 + - High + - Tampering, Elevation of Privilege + - DFT-14 + - Affects: On Python versions prior to 3.11.4, ``tarfile.extractall()`` does not apply the ``filter='tar'`` parameter and preserves setuid, setgid, and sticky bits encoded in TAR member headers. A malicious archive can introduce setuid-root binaries into the vendor directory; a later build step that invokes the extracted binary executes with elevated privileges. This affects all dfetch users on Python < 3.11.4. ``dfetch/vcs/archive.py`` diff --git a/security/README.md b/security/README.md index c859cea4..31636f33 100644 --- a/security/README.md +++ b/security/README.md @@ -13,11 +13,11 @@ the SHA above. After this you can generate various reports using: ```bash -python -m security.tm_supply_chain --report security/report_template.md > report.md +python -m security.tm_supply_chain --report security/report_template.rst > doc/explanation/threat_model_supply_chain.rst python -m security.tm_supply_chain --dfd python -m security.tm_supply_chain --seq -python -m security.tm_usage --report security/report_template.md > report_usage.md +python -m security.tm_usage --report security/report_template.rst > doc/explanation/threat_model_usage.rst python -m security.tm_usage --dfd python -m security.tm_usage --seq ``` diff --git a/security/report_template.md b/security/report_template.md deleted file mode 100644 index 65a846b8..00000000 --- a/security/report_template.md +++ /dev/null @@ -1,114 +0,0 @@ -## System Description - -{tm.description} - -## Dataflow Diagram - Level 0 DFD - -```dot -{tm.dfd:call:} -``` - -## Dataflows - -Name|From|To |Data|Protocol|Port -|:----:|:----:|:---:|:----:|:--------:|:----:| -{dataflows:repeat:|{{item.display_name:call:}}|{{item.source.name}}|{{item.sink.name}}|{{item.data}}|{{item.protocol}}|{{item.dstPort}}| -} - -## Data Dictionary - -{data:repeat: -Name|{{item.name}} -|:----|:----| -Description|{{item.description}}| -Classification|{{item.classification.name}}| -Carried By|{{item.carriedBy:repeat:{{{{item.name}}}}
}}| -Processed By|{{item.processedBy:repeat:{{{{item.name}}}}
}}| - -{{item:call:getInScopeFindings}} -} - -## Actors - -{actors:repeat: -Name|{{item.name}} -|:----|:----| -Description|{{item.description}}| -Is Admin|{{item.isAdmin}}| -Finding Count|{{item:call:getFindingCount}}| - -{{item:call:getInScopeFindings}} -} - -## Boundaries - -{boundaries:repeat: -Name|{{item.name}} -|:----|:----| -Description|{{item.description}}| -In Scope|{{item.inScope}}| -Immediate Parent|{{item.parents:if:{{item:call:getParentName}}}}{{item.parents:not:N/A, primary boundary}}| -All Parents|{{item.parents:call:{{{{item.display_name:call:}}}}, }}| -Classification|{{item.maxClassification}}| -Finding Count|{{item:call:getFindingCount}}| - -{{item:call:getInScopeFindings}} -} - - -## Assets - -{assets:repeat: -Name|{{item.name}} -|:----|:----| -Description|{{item.description}}| -In Scope|{{item.inScope}}| -Type|{{item:call:getElementType}}| -Finding Count|{{item:call:getFindingCount}}| - -{{item:call:getInScopeFindings}} -} - - -## Data Flows - -{dataflows:repeat: -Name|{{item.name}} -|:----|:----| -Description|{{item.description}}| -Sink|{{item.sink}}| -Source|{{item.source}}| -Is Response|{{item.isResponse}}| -In Scope|{{item.inScope}}| -Finding Count|{{item:call:getFindingCount}}| - -{{item:call:getInScopeFindings}} -} - - -{tm.excluded_findings:if: -# Excluded Threats -} - -{tm.excluded_findings:repeat: -
- - {{item:call:getThreatId}} - {{item:call:getFindingDescription}} - -

- {{item:call:getThreatId}} was excluded for - {{item:call:getFindingTarget}} - because of the assumption "{{item.assumption.name}}" -

- {{item.assumption.description:if: -
Assumption description
-

{{item.assumption.description}}

- }} -
Severity
-

{{item:call:getFindingSeverity}}

-
Example Instances
-

{{item:call:getFindingExample}}

-
References
-

{{item:call:getFindingReferences}}

-
-} diff --git a/security/report_template.rst b/security/report_template.rst new file mode 100644 index 00000000..f4c10593 --- /dev/null +++ b/security/report_template.rst @@ -0,0 +1,129 @@ +.. ============================================================ +.. Auto-generated file — do not edit manually. +.. Regenerate with (see security/README.md for exact commands): +.. +.. python -m security.tm_ \ +.. --report security/report_template.rst \ +.. > doc/explanation/threat_model_.rst +.. ============================================================ + +System Description +------------------ + +{tm.description} + +Assumptions +----------- + +.. list-table:: + :header-rows: 1 + :widths: 30 70 + + * - Name + - Description +{tm.assumptions:repeat: + * - {{item.name}} + - {{item.description}} +} + +Dataflows +--------- + +.. list-table:: + :header-rows: 1 + :widths: 35 20 20 25 + + * - Name + - From + - To + - Protocol +{dataflows:repeat: + * - {{item.display_name:call:}} + - {{item.source.name}} + - {{item.sink.name}} + - {{item.protocol}} +} + +Data Dictionary +--------------- + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Name + - Description + - Classification +{data:repeat: + * - {{item.name}} + - {{item.description}} + - {{item.classification.name}} +} + +Actors +------ + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Name + - Description +{actors:repeat: + * - {{item.name}} + - {{item.description}} +} + +Boundaries +---------- + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Name + - Description +{boundaries:repeat: + * - {{item.name}} + - {{item.description}} +} + +Assets +------ + +.. list-table:: + :header-rows: 1 + :widths: 25 55 20 + + * - Name + - Description + - Type +{assets:repeat: + * - {{item.name}} + - {{item.description}} + - {{item:call:getElementType}} +} + +{tm.excluded_findings:if: +Excluded Threats +---------------- + +.. list-table:: + :header-rows: 1 + :widths: 12 28 20 20 8 12 + + * - ID + - Description + - Target + - Assumption + - Severity + - References +} +{tm.excluded_findings:repeat: + * - {{item:call:getThreatId}} + - {{item:call:getFindingDescription}} + - {{item:call:getFindingTarget}} + - {{item.assumption.name}} + - {{item:call:getFindingSeverity}} + - {{item:call:getFindingReferences}} +} diff --git a/security/tm_common.py b/security/tm_common.py index 91c9fac3..05e8f0fc 100644 --- a/security/tm_common.py +++ b/security/tm_common.py @@ -4,7 +4,6 @@ module level, so it is safe to import before ``TM.reset()``. """ -import html import os import sys from collections.abc import Callable @@ -202,62 +201,71 @@ def make_usage_assumptions() -> list[Assumption]: def render_controls_section(controls: list[Control]) -> str: - """Render controls and gaps as a markdown section appended to the report.""" + """Render controls and gaps as RST list-table sections appended to the report.""" implemented = [c for c in controls if c.status == "implemented"] gaps = [c for c in controls if c.status == "gap"] - parts: list[str] = [] - - def _render_control(c: Control, *, is_gap: bool) -> str: - meta: list[str] = [f"**Risk:** {c.assessment.risk}"] - if c.assessment.stride: - meta.append(f"**STRIDE:** {', '.join(c.assessment.stride)}") - label = "**Affects threats:**" if is_gap else "**Mitigates:**" - if c.threats: - meta.append(f"{label} {', '.join(c.threats)}") - lines = [f"### {c.id}: {c.name}", " \n".join(meta)] + def _row(c: Control, *, is_gap: bool) -> str: + stride = ", ".join(c.assessment.stride) if c.assessment.stride else "—" + threats = ", ".join(c.threats) if c.threats else "—" + desc = c.description if c.reference: - lines.append(f"**Reference:** `{c.reference}`") - lines.append(f"\n{c.description}") - return "\n".join(lines) + desc += f" ``{c.reference}``" + label = "Affects" if is_gap else "Mitigates" + return ( + f" * - {c.id}\n" + f" - {c.name}\n" + f" - {c.assessment.risk}\n" + f" - {stride}\n" + f" - {threats}\n" + f" - {label}: {desc}\n" + ) + + def _table(items: list[Control], *, is_gap: bool, heading: str) -> str: + rows = "".join(_row(c, is_gap=is_gap) for c in items) + return ( + f"{heading}\n{'-' * len(heading)}\n\n" + ".. list-table::\n" + " :header-rows: 1\n" + " :widths: 8 20 8 14 15 35\n\n" + " * - ID\n" + " - Name\n" + " - Risk\n" + " - STRIDE\n" + " - Threats\n" + " - Description\n" + rows + ) + parts: list[str] = [] if implemented: - parts.append("## Controls\n") - parts.extend(_render_control(c, is_gap=False) for c in implemented) - + parts.append(_table(implemented, is_gap=False, heading="Controls")) if gaps: - parts.append("## Gaps\n") - parts.extend(_render_control(c, is_gap=True) for c in gaps) + parts.append(_table(gaps, is_gap=True, heading="Gaps")) return "\n\n".join(parts) def _render_finding(f: Any) -> str: - """Render one pytm Finding to an HTML ``
`` block string.""" - - def _esc(attr: str) -> str: - return html.escape(str(getattr(f, attr, ""))) - - def _esc_ml(attr: str) -> str: - return html.escape(str(getattr(f, attr, ""))).replace("\n", "
") - - return ( - "
\n \n " - + _esc("threat_id") - + " - " - + _esc("description") - + "\n \n
Targeted Element
\n

" - + _esc("target") - + "

\n
Severity
\n

" - + _esc("severity") - + "

\n
Example Instances
\n

" - + _esc_ml("example") - + "

\n
Mitigations
\n

" - + _esc_ml("mitigations") - + "

\n
References
\n

" - + _esc_ml("references") - + "

\n
\n" - ) + """Render one pytm Finding as an RST definition list entry.""" + + def _val(attr: str) -> str: + return str(getattr(f, attr, "")).strip() + + threat_id = _val("threat_id") + description = _val("description") + lines = [f"{threat_id} — {description}"] + for label, attr in ( + ("Targeted Element", "target"), + ("Severity", "severity"), + ("Example", "example"), + ("Mitigations", "mitigations"), + ("References", "references"), + ): + value = _val(attr) + if value: + lines.append(f" **{label}:** {value}") + lines.append("") + return "\n".join(lines) def apply_report_utils_patch() -> None: @@ -274,7 +282,7 @@ def apply_report_utils_patch() -> None: strings that get baked into the spec before getInScopeFindings can loop. Fix: replace getInScopeFindings with one that renders each in-scope finding - to an HTML string directly, so no sub-template is needed in the report. + to an RST string directly, so no sub-template is needed in the report. element.findings is already scoped to that element, so no target lookup is required - the in-scope check on the element itself is sufficient. """