diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7e93356..cd104262 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -138,6 +138,12 @@ jobs: python script/build.py python script/package.py + - name: Create AUR tarball (Linux, versioned releases only) + if: runner.os == 'Linux' && inputs.release_id != 'latest' && inputs.release_id != '' + run: | + tar czf "build/dfetch-package/dfetch-${{ inputs.release_id }}-nix.tar.gz" \ + -C build/dfetch.dist . + - name: Save cache for clcache if: ${{ always() && matrix.platform == 'windows-latest' && steps.clcache-restore.outputs.cache-hit != 'true' }} uses: actions/cache/save@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 @@ -159,6 +165,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz predicate-type: 'https://cyclonedx.org/bom' predicate-path: ${{ steps.find-sbom.outputs.path }} - name: Verify SBOM attestation @@ -166,7 +173,7 @@ jobs: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi; do + for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi build/dfetch-package/*.tar.gz; do [ -f "$f" ] || continue gh attestation verify "$f" \ --repo "${{ github.repository }}" \ @@ -182,12 +189,13 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz - name: Verify build provenance shell: bash env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi; do + for f in build/dfetch-package/*.deb build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi build/dfetch-package/*.tar.gz; do [ -f "$f" ] || continue gh attestation verify "$f" \ --repo "${{ github.repository }}" \ @@ -222,6 +230,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz predicate-type: 'https://slsa.dev/verification_summary/v1' predicate-path: vsa-predicate.json - name: Store the distribution packages @@ -233,6 +242,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz build/dfetch-package/*.cdx.json - name: Upload installer to release @@ -245,6 +255,7 @@ jobs: build/dfetch-package/*.rpm build/dfetch-package/*.pkg build/dfetch-package/*.msi + build/dfetch-package/*.tar.gz build/dfetch-package/*.cdx.json draft: true preserve_order: true diff --git a/.github/workflows/distribution/aur-publish.yml b/.github/workflows/distribution/aur-publish.yml new file mode 100644 index 00000000..e2c05da0 --- /dev/null +++ b/.github/workflows/distribution/aur-publish.yml @@ -0,0 +1,255 @@ +# Publish to AUR (Arch Linux User Repository) +# +# Pushes an updated PKGBUILD for dfetch-bin to AUR on every GitHub release. +# AUR uses a plain git repository — there is no PR or review gate; every push +# is immediately visible to users who run `yay -S dfetch-bin` or equivalent. +# +# ─── ONE-TIME SETUP ────────────────────────────────────────────────────────── +# +# 1. Create an AUR account +# Register at https://aur.archlinux.org/register/ if you do not already have +# one. Choose a bot username (e.g. "dfetch-bot") that clearly signals it is +# a machine account. +# Ref: https://wiki.archlinux.org/title/AUR_submission_guidelines#Creating_a_new_package +# +# 2. Generate a dedicated SSH key pair (do NOT reuse an existing key) +# +# ssh-keygen -t ed25519 -C "dfetch-aur-bot" -f aur_key +# +# This creates two files: aur_key (private) and aur_key.pub (public). +# Never commit either file; delete them from disk once the secrets are stored. +# Ref: https://wiki.archlinux.org/title/AUR_submission_guidelines#Authentication +# +# 3. Register the public key with AUR +# Log in to https://aur.archlinux.org, go to My Account → SSH Public Keys, +# and paste the contents of aur_key.pub. +# +# 4. Create the GitHub 'aur' environment and store the private key as a secret +# a. Go to: Repository → Settings → Environments → New environment +# b. Name it exactly "aur". +# c. Recommended: add "Required reviewers" so every push is approved by a +# human before it reaches AUR users. +# Ref: https://docs.github.com/en/actions/managing-workflow-runs-and-deployments/managing-deployments/managing-environments-for-deployment#required-reviewers +# d. Under "Environment secrets", click "Add secret": +# Name: AUR_SSH_KEY +# Value: paste the full contents of the aur_key *private* key file +# (including the -----BEGIN/END OPENSSH PRIVATE KEY----- lines). +# Ref: https://docs.github.com/en/actions/security-for-github-actions/security-guides/using-secrets-in-github-actions#creating-secrets-for-an-environment +# +# 5. Create or claim the dfetch-bin AUR package (first time only) +# Either submit a new package at https://aur.archlinux.org/submit/ or ask an +# existing maintainer to add the bot account as a co-maintainer. +# If creating fresh, replace the SKIP placeholder with the real checksum +# *before* the first push — AUR users rely on it for integrity verification: +# +# git clone ssh://aur@aur.archlinux.org/dfetch-bin.git +# cp aur/PKGBUILD dfetch-bin/ +# cd dfetch-bin +# +# # Compute the real SHA256 for the existing release tarball: +# VERSION= # e.g. 0.14.3 +# URL="https://github.com/dfetch-org/dfetch/releases/download/${VERSION}/dfetch-${VERSION}-nix.tar.gz" +# SHA256=$(curl -fsSL "$URL" | sha256sum | awk '{print $1}') +# sed -i "s|pkgver=.*|pkgver=${VERSION}|" PKGBUILD +# sed -i "s|sha256sums=.*|sha256sums=('${SHA256}')|" PKGBUILD +# # Regenerate .SRCINFO from the updated PKGBUILD (requires makepkg): +# makepkg --printsrcinfo > .SRCINFO +# +# git add PKGBUILD .SRCINFO +# git commit -m "Initial import" +# git push +# The package page will appear at https://aur.archlinux.org/packages/dfetch-bin +# within seconds of the first push. +# Ref: https://wiki.archlinux.org/title/AUR_submission_guidelines#Submitting_packages +# +# 6. Test locally before triggering CI (optional but recommended) +# On an Arch Linux system (or in the archlinux Docker image): +# cd aur/ +# makepkg -si # builds and installs the package from source +# Verify `dfetch --version` matches the expected release. +# Ref: https://wiki.archlinux.org/title/Makepkg +# +# ───────────────────────────────────────────────────────────────────────────── + +name: Publish to AUR + +on: + release: + types: [published] + workflow_dispatch: + inputs: + release-tag: + description: 'Release tag (e.g. 0.14.2)' + required: true + type: string + push: + branches: + - main + paths: + - 'aur/**' + - '.github/workflows/distribution/aur-publish.yml' + pull_request: + paths: + - 'aur/**' + - '.github/workflows/distribution/aur-publish.yml' + +permissions: + contents: read + +jobs: + build-binary: + name: Build Linux binary from source + if: github.event_name == 'push' || github.event_name == 'pull_request' + runs-on: ubuntu-latest + + steps: + - name: "Harden the runner (Block egress traffic: Only allow calls to allowed endpoints)" + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: block + allowed-endpoints: >+ + github.com:443 + api.github.com:443 + pypi.org:443 + files.pythonhosted.org:443 + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + fetch-depth: 0 + + - uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: '3.13' + cache: 'pip' + + - name: Build binary with Nuitka + run: | + pip install .[build] + python script/build.py + + - name: Create tarball from build output + run: | + VERSION=$(git describe --tags --abbrev=0) + tar czf "dfetch-${VERSION}-nix.tar.gz" -C build/dfetch.dist . + + - uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: dfetch-nix-tarball + path: dfetch-*-nix.tar.gz + + test: + name: Test PKGBUILD + needs: build-binary + if: github.event_name == 'push' || github.event_name == 'pull_request' + runs-on: ubuntu-latest + container: + image: archlinux:latest@sha256:cc54f12fae7c81bd0d1a38f1694b4c28ffc708b358dd5876ab77dde49d37925a + + steps: + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: dfetch-nix-tarball + + - name: Install build dependencies + run: | + pacman-key --init + pacman-key --populate archlinux + pacman -Syu --noconfirm base-devel + + - name: Patch PKGBUILD to use local tarball + run: | + TARBALL=$(ls dfetch-*-nix.tar.gz) + VERSION=${TARBALL#dfetch-} + VERSION=${VERSION%-nix.tar.gz} + SHA256=$(sha256sum "$TARBALL" | awk '{print $1}') + cp "$TARBALL" aur/ + sed -i "s|^pkgver=.*|pkgver=${VERSION}|" aur/PKGBUILD + sed -i "s|^source=.*|source=(\"${TARBALL}\")|" aur/PKGBUILD + sed -i "s|^sha256sums=.*|sha256sums=('${SHA256}')|" aur/PKGBUILD + + - name: Build package with makepkg + run: | + useradd -m builder + chown -R builder:builder aur/ + su builder -c "cd aur && makepkg --noconfirm" + + - name: Install package and verify + run: | + pacman -U --noconfirm aur/dfetch-bin-*.pkg.tar.zst + dfetch --version + + publish: + name: Publish to AUR + # Only on a real versioned release or a manual dispatch; skip the rolling + # 'latest' tag that main pushes create. + if: >- + github.event_name == 'workflow_dispatch' || + (github.event_name == 'release' && github.event.release.tag_name != 'latest') + runs-on: ubuntu-latest + concurrency: + group: aur-publish-${{ github.event.release.tag_name || inputs.release-tag }} + cancel-in-progress: true + + environment: + name: aur + url: https://aur.archlinux.org/packages/dfetch-bin + + steps: + - name: "Harden the runner (Block egress traffic: Only allow calls to allowed endpoints)" + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: block + allowed-endpoints: >+ + github.com:443 + api.github.com:443 + objects.githubusercontent.com:443 + release-assets.githubusercontent.com:443 + uploads.github.com:443 + aur.archlinux.org:443 + aur.archlinux.org:22 + fulcio.sigstore.dev:443 + rekor.sigstore.dev:443 + tuf-repo-cdn.sigstore.dev:443 + + - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Download and verify release tarball + # Verifies Sigstore build-provenance attestation before trusting the tarball. + # Requires AUR_SSH_KEY secret — see one-time setup instructions at the top of this file. + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ github.event.release.tag_name || inputs.release-tag }} + run: | + URL="https://github.com/dfetch-org/dfetch/releases/download/${TAG}/dfetch-${TAG}-nix.tar.gz" + curl -fsSL "$URL" -o dfetch-nix.tar.gz + gh attestation verify dfetch-nix.tar.gz \ + --repo "${{ github.repository }}" \ + --predicate-type https://slsa.dev/provenance/v1 \ + --cert-identity-regex "^https://github\.com/${{ github.repository }}/\.github/workflows/build\.yml@refs/tags/[0-9]+\.[0-9]+\.[0-9]+$" \ + --cert-oidc-issuer https://token.actions.githubusercontent.com + + - name: Update pkgver and checksums + env: + TAG: ${{ github.event.release.tag_name || inputs.release-tag }} + run: | + SHA256=$(sha256sum dfetch-nix.tar.gz | awk '{print $1}') + sed -i "s/^pkgver=.*/pkgver=${TAG}/" aur/PKGBUILD + sed -i "s/^sha256sums=.*/sha256sums=('${SHA256}')/" aur/PKGBUILD + + - name: Publish to AUR + uses: KSXGitHub/github-actions-deploy-aur@a6ca8e3a2da2a9e461d694e80248ba1a3ab1b3e1 # v3.0.1 + with: + pkgname: dfetch-bin + pkgbuild: ./aur/PKGBUILD + commit_username: dfetch-bot + commit_email: bot@dfetch.dev + ssh_private_key: ${{ secrets.AUR_SSH_KEY }} + commit_message: "Update to ${{ github.event.release.tag_name || inputs.release-tag }}" + updpkgsums: false diff --git a/.github/workflows/python-publish.yml b/.github/workflows/distribution/python-publish.yml similarity index 100% rename from .github/workflows/python-publish.yml rename to .github/workflows/distribution/python-publish.yml diff --git a/.github/workflows/winget-publish.yml b/.github/workflows/distribution/winget-publish.yml similarity index 100% rename from .github/workflows/winget-publish.yml rename to .github/workflows/distribution/winget-publish.yml diff --git a/CHANGELOG.rst b/CHANGELOG.rst index e37ceb3b..367ae4c7 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -1,6 +1,7 @@ Release 0.14.3 (released 2026-06-25) ==================================== +* Add AUR (Arch Linux User Repository) package and automated publish workflow (#1319) * Update ``dfetch environment`` to show newer version inline (#1310) Release 0.14.2 (released 2026-06-21) diff --git a/aur/.SRCINFO b/aur/.SRCINFO new file mode 100644 index 00000000..4502e5ed --- /dev/null +++ b/aur/.SRCINFO @@ -0,0 +1,13 @@ +pkgbase = dfetch-bin + pkgdesc = Vendor tool for fetching external dependencies + pkgver = 0.14.3 + pkgrel = 1 + url = https://github.com/dfetch-org/dfetch + arch = x86_64 + license = MIT + provides = dfetch + conflicts = dfetch + source = https://github.com/dfetch-org/dfetch/releases/download/0.14.3/dfetch-0.14.3-nix.tar.gz + sha256sums = SKIP + +pkgname = dfetch-bin diff --git a/aur/PKGBUILD b/aur/PKGBUILD new file mode 100644 index 00000000..1a4cd2dd --- /dev/null +++ b/aur/PKGBUILD @@ -0,0 +1,22 @@ +# Maintainer: dfetch-org +pkgname=dfetch-bin +pkgver=0.14.3 +pkgrel=1 +pkgdesc="Vendor tool for fetching external dependencies" +arch=('x86_64') +url="https://github.com/dfetch-org/dfetch" +license=('MIT') +provides=('dfetch') +conflicts=('dfetch') +source=("https://github.com/dfetch-org/dfetch/releases/download/${pkgver}/dfetch-${pkgver}-nix.tar.gz") +sha256sums=('SKIP') +# ^ CI replaces SKIP with the real SHA256 before pushing to AUR. +# Do NOT run makepkg manually from this repository copy — use the AUR package +# at https://aur.archlinux.org/packages/dfetch-bin where the real checksum is set. + +package() { + install -dm755 "$pkgdir/opt/dfetch" + cp -a . "$pkgdir/opt/dfetch/" + install -dm755 "$pkgdir/usr/bin" + ln -sf /opt/dfetch/dfetch "$pkgdir/usr/bin/dfetch" +} diff --git a/doc/explanation/threat_model_supply_chain.rst b/doc/explanation/threat_model_supply_chain.rst index 7d88c7cc..9cf29e22 100644 --- a/doc/explanation/threat_model_supply_chain.rst +++ b/doc/explanation/threat_model_supply_chain.rst @@ -20,7 +20,7 @@ Chapter 5. The Sev / Risk rating scale and treatment vocabulary (Mitigate / Accept / Transfer) are defined in the :ref:`Risk Rating Methodology ` section of the main security page. -Threat model for dfetch. Covers the pre-install lifecycle: code contribution, CI/CD, build (wheel / sdist), PyPI distribution, Winget manifest submission, and consumer installation. The installed dfetch package is the handoff point to tm_usage.py. +Threat model for dfetch. Covers the pre-install lifecycle: code contribution, CI/CD, build (wheel / sdist), PyPI distribution, Winget manifest submission, AUR PKGBUILD publication, and consumer installation. The installed dfetch package is the handoff point to tm_usage.py. Assumptions ----------- @@ -84,6 +84,9 @@ Boundaries * - Winget Community Repository - The Windows Package Manager Community Repository (https://github.com/microsoft/winget-pkgs) where dfetch's Winget manifest is hosted. Manifest PRs are submitted automatically by the CI release pipeline (winget-publish.yml) using the stored WINGET_TOKEN PAT (A-10). Consumer installations resolve manifests from this repository; winget downloads the MSI installer from the URL declared in the manifest (pointing to GitHub Releases, A-01) and verifies its SHA256 hash. + * - AUR Repository + - The Arch Linux User Repository (https://aur.archlinux.org) where the ``dfetch-bin`` PKGBUILD is hosted. CI pushes updated PKGBUILDs directly to the AUR git repository using ``aur-publish.yml`` and the AUR_SSH_KEY (A-12). Unlike Winget (which goes through a community PR review), AUR accepts direct git pushes from authenticated maintainers — there is no intermediate PR review gate. + Data Flow Diagram ----------------- @@ -264,6 +267,25 @@ Data Flow Diagram } + subgraph cluster_boundary_AURRepository_aur { + graph [ + fontsize = 10; + fontcolor = black; + style = dashed; + color = firebrick2; + label = <AUR\nRepository>; + ] + + externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur [ + shape = square; + color = black; + fontcolor = black; + label = "A-11: AUR\nRepository\n(aur.archlinux.org/\ndfetch-bin)"; + margin = 0.02; + ] + + } + actor_DeveloperContributor_d2006ce1bb -> externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 [ color = black; fontcolor = black; @@ -397,6 +419,27 @@ Data Flow Diagram label = "DF-29: Consumer\ndownloads MSI via\nwinget"; ] + externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur [ + color = black; + fontcolor = black; + dir = forward; + label = "DF-30: AUR\nPKGBUILD push"; + ] + + actor_ConsumerEndUser_f8af758679 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur [ + color = black; + fontcolor = black; + dir = forward; + label = "DF-31: yay install\ndfetch-bin"; + ] + + externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur -> actor_ConsumerEndUser_f8af758679 [ + color = black; + fontcolor = black; + dir = forward; + label = "DF-32: Consumer\ndownloads tarball\nvia AUR"; + ] + } @enddot @@ -459,6 +502,7 @@ Sequence Diagram database datastore_AdfetchBuildDevDependencies_990b886585 as "A-07: dfetch\nBuild / Dev\nDependencies" database datastore_AbGitHubActionsBuildCache_9df04f8dae as "A-08b:\nGitHub\nActions\nBuild Cache" entity externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48 as "A-09: Winget\nCommunity\nRepository\n(microsoft/winget-pkgs)" + entity externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur as "A-11: AUR\nRepository\n(aur.archlinux.org/\ndfetch-bin)" actor_DeveloperContributor_d2006ce1bb -> externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72: DF-11: Push commits / open PR externalentity_AbGitHubRepositoryfeaturebranchesPRs_0291419f72 -> process_AReleaseGateCodeReview_9345ab4c19: DF-22: PR enters code review @@ -479,6 +523,9 @@ Sequence Diagram externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48: DF-27: Winget manifest PR submission actor_ConsumerEndUser_f8af758679 -> externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48: DF-28: winget install dfetch externalentity_AWingetCommunityRepositorymicrosoftwingetpkgs_7113ed0f48 -> actor_ConsumerEndUser_f8af758679: DF-29: Consumer downloads MSI via winget + externalentity_AGitHubActionsInfrastructure_c76a0a7067 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur: DF-30: AUR PKGBUILD push + actor_ConsumerEndUser_f8af758679 -> externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur: DF-31: yay install dfetch-bin + externalentity_AAURRepositoryaurarchlinuxorgdfetchbin_aur -> actor_ConsumerEndUser_f8af758679: DF-32: Consumer downloads tarball via AUR @enduml .. raw:: html @@ -554,7 +601,7 @@ Asset Identification - Data - High / High / High * - A-06: GitHub Actions Workflow - - CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, dependency-review, docs, release, winget-publish. 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. winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to submit manifest PRs to the Winget Community Repository (A-09). + - CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, dependency-review, docs, release, distribution/winget-publish, distribution/python-publish, distribution/aur-publish. 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. distribution/winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to submit manifest PRs to the Winget Community Repository (A-09). distribution/aur-publish.yml uses an SSH key (AUR_SSH_KEY, A-12) to push PKGBUILDs directly to the AUR repository (A-11). - Process - Medium / Medium / Medium * - A-07: dfetch Build / Dev Dependencies @@ -577,6 +624,14 @@ Asset Identification - Long-lived classic GitHub Personal Access Token (fine-grained PATs are not supported by the ``vedantmgoyal9/winget-releaser`` action) with ``public_repo`` scope, stored as a GitHub Actions environment secret in the ``winget`` environment. Used by ``winget-publish.yml`` to push a manifest branch to the pre-existing fork ``dfetch-org/winget-pkgs`` and open a PR against ``microsoft/winget-pkgs``. Unlike the PyPI OIDC token (A-05) which is short-lived and not stored, this PAT persists indefinitely until rotated. If exfiltrated from the CI environment, an attacker could submit fraudulent manifest PRs from outside the project's pipeline. - Data - High / High / — + * - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - The Arch Linux User Repository entry for the ``dfetch-bin`` package (https://aur.archlinux.org/packages/dfetch-bin). CI pushes updated PKGBUILDs directly via git using the AUR_SSH_KEY (A-12); there is no PR review step — the push goes live immediately. PKGBUILDs contain the SHA256 checksum of the release tarball; AUR helpers such as ``yay`` download and verify the tarball before installing. A compromised SSH key or a malicious PKGBUILD push could redirect consumers to a tampered binary (DFT-36). + - ExternalEntity + - High / High / — + * - A-12: AUR_SSH_KEY + - Ed25519 SSH private key used to authenticate git pushes to the dfetch-bin AUR repository. Stored as a GitHub Actions environment secret in the ``aur`` environment. Used by ``aur-publish.yml`` to push updated PKGBUILDs directly to AUR. Unlike the PyPI OIDC token (A-05) which is short-lived and not stored, this key persists indefinitely until rotated. If exfiltrated from the CI environment, an attacker could push a malicious PKGBUILD directly to AUR without any review gate (DFT-36). + - Data + - High / High / — @@ -690,6 +745,21 @@ Dataflows - Consumer / End User - HTTPS + * - DF-30: AUR PKGBUILD push + - A-02: GitHub Actions Infrastructure + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - SSH + + * - DF-31: yay install dfetch-bin + - Consumer / End User + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - HTTPS + + * - DF-32: Consumer downloads tarball via AUR + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - Consumer / End User + - HTTPS + Threats ------- @@ -864,6 +934,14 @@ Threats | **STRIDE:** T S | **Status:** Mitigate - C-041 + * - DFT-36 + - Compromised AUR SSH key enables direct malicious PKGBUILD push without review gate + - A-11: AUR Repository (aur.archlinux.org/dfetch-bin) + - | **Sev:** 🟠H + | **Risk:** 🟠H + | **STRIDE:** T S + | **Status:** Mitigate + - C-044, C-046, C-047 Controls @@ -885,7 +963,7 @@ Controls * - C-010 - OIDC trusted publishing - DFT-07 - - PyPI publishes via ``pypa/gh-action-pypi-publish`` with ``id-token: write`` and no stored long-lived API token. ``.github/workflows/python-publish.yml`` + - PyPI publishes via ``pypa/gh-action-pypi-publish`` with ``id-token: write`` and no stored long-lived API token. ``.github/workflows/distribution/python-publish.yml`` * - C-011 - Minimal workflow permissions - DFT-07 @@ -953,8 +1031,20 @@ Controls * - C-041 - Winget manifest PRs reviewed by community maintainers - DFT-35 - - Manifest update PRs submitted to ``microsoft/winget-pkgs`` by ``winget-publish.yml`` go through the standard Winget community review process before merging. ``microsoft/winget-pkgs`` maintainers verify the publisher identity and inspect manifest changes including installer URLs and hashes. This provides a manual review gate between a fraudulent PR submission and consumer exposure. Residual risk: a reviewer who approves without independently verifying the installer URL origin could merge a fraudulent manifest. ``.github/workflows/winget-publish.yml`` + - Manifest update PRs submitted to ``microsoft/winget-pkgs`` by ``winget-publish.yml`` go through the standard Winget community review process before merging. ``microsoft/winget-pkgs`` maintainers verify the publisher identity and inspect manifest changes including installer URLs and hashes. This provides a manual review gate between a fraudulent PR submission and consumer exposure. Residual risk: a reviewer who approves without independently verifying the installer URL origin could merge a fraudulent manifest. ``.github/workflows/distribution/winget-publish.yml`` * - C-042 - WINGET_TOKEN scoped to dedicated Winget environment - DFT-34 - - ``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment environment, limiting its exposure: the PAT is only injected into workflows that explicitly reference that environment. Only ``winget-publish.yml`` references the ``winget`` environment, so the PAT is not available to other workflows. Residual risk: unlike PyPI which uses OIDC (A-05, no stored long-lived token), Winget does not support OIDC trusted publishing; the PAT must be stored and rotated manually (DFT-34). ``.github/workflows/winget-publish.yml`` + - ``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment environment, limiting its exposure: the PAT is only injected into workflows that explicitly reference that environment. Only ``winget-publish.yml`` references the ``winget`` environment, so the PAT is not available to other workflows. Residual risk: unlike PyPI which uses OIDC (A-05, no stored long-lived token), Winget does not support OIDC trusted publishing; the PAT must be stored and rotated manually (DFT-34). ``.github/workflows/distribution/winget-publish.yml`` + * - C-044 + - AUR_SSH_KEY scoped to dedicated AUR environment + - DFT-36 + - ``AUR_SSH_KEY`` is stored in the ``aur`` GitHub Actions deployment environment, limiting its exposure: the SSH key is only injected into workflows that explicitly reference that environment. Only ``aur-publish.yml`` references the ``aur`` environment, so the key is not available to other workflows. Unlike PyPI which uses OIDC (A-05, no stored long-lived token) or Winget which uses a PAT (A-10), AUR authentication requires an SSH key for git push access; the key must be stored and rotated manually. A compromised key enables direct (no review gate) PKGBUILD pushes to AUR. ``.github/workflows/distribution/aur-publish.yml`` + * - C-046 + - AUR PKGBUILD uses real SHA256 checksum of release tarball + - DFT-36 + - The ``aur-publish.yml`` workflow computes the SHA256 checksum of the release tarball at publish time and embeds it in the PKGBUILD, replacing the placeholder ``SKIP``. AUR helpers (``yay``, ``paru``) verify this checksum when building the package, so a tampered tarball would be rejected before installation. Residual risk: a compromised AUR_SSH_KEY (A-12) allows pushing a PKGBUILD with a fraudulent checksum — the review gate present in Winget (C-041) does not exist for AUR direct pushes. ``.github/workflows/distribution/aur-publish.yml`` + * - C-047 + - AUR release tarball build-provenance verified before PKGBUILD update + - DFT-36 + - Before computing the SHA256 to embed in the PKGBUILD, ``aur-publish.yml`` runs ``gh attestation verify`` against the downloaded release tarball. This confirms the tarball's Sigstore SLSA build-provenance attestation chain: the artifact was produced by the trusted ``build.yml`` workflow from a signed release tag, not tampered with in transit or substituted by a compromised release asset. If attestation fails the workflow aborts before any PKGBUILD update or AUR push. ``.github/workflows/distribution/aur-publish.yml`` diff --git a/doc/tutorials/installation.rst b/doc/tutorials/installation.rst index 3e05efe1..0e11cf96 100644 --- a/doc/tutorials/installation.rst +++ b/doc/tutorials/installation.rst @@ -53,6 +53,22 @@ The version is automatically determined from the project and used to name the in # or $ sudo rpm -i dfetch--nix.rpm + Arch Linux (via AUR): + + ``dfetch-bin`` is a pre-built binary — no Python installation or + compilation required on your machine. + + .. code-block:: bash + + $ yay -S dfetch-bin + # or with any other AUR helper, e.g. + $ paru -S dfetch-bin + + The AUR helper verifies the SHA256 checksum of the release tarball + before installing. You can also verify that tarball's build provenance + independently using the `GitHub attestation tool + `_. + .. tab:: macOS Download the ``.pkg`` package from the releases page and install it. diff --git a/security/threats.json b/security/threats.json index a150bd92..a4a637d7 100644 --- a/security/threats.json +++ b/security/threats.json @@ -523,5 +523,20 @@ "mitigations": "Pin installer URLs in manifests to the project's official release infrastructure and document that URL changes must be scrutinised as carefully as hash changes. Store publish credentials in a deployment environment requiring reviewer approval. Use OIDC-based credentials where available so exfiltrated tokens expire quickly. Monitor PAT usage for activity outside expected CI build windows.", "example": "An attacker exfiltrates the WINGET_TOKEN PAT from a CI run via a backdoored dependency. They fork microsoft/winget-pkgs, update the dfetch manifest to point the installer URL to attacker.example/dfetch-0.14.0-win.msi (a trojaned MSI), compute its correct SHA256 hash, and open a PR. A reviewer sees the hash matches the declared value and approves. Consumers who run winget install DFetch-org.DFetch download and install the malicious binary.", "references": "https://slsa.dev/spec/v1.0/threats#f-artifact-publication, https://capec.mitre.org/data/definitions/186.html, https://cwe.mitre.org/data/definitions/494.html" + }, + { + "SID": "DFT-36", + "target": [ + "ExternalEntity" + ], + "description": "Compromised AUR SSH key enables direct malicious PKGBUILD push without review gate", + "details": "The AUR (Arch Linux User Repository) accepts direct git pushes from authenticated maintainers — unlike Winget (which requires a PR reviewed by community maintainers before merging), a valid AUR SSH push goes live immediately. An attacker who exfiltrates the AUR_SSH_KEY from the CI environment can push a malicious PKGBUILD that: (1) replaces the source URL with an attacker-controlled tarball, (2) computes its correct SHA256 hash. Arch users running 'yay -Syu' or equivalent will receive the tampered PKGBUILD and install the malicious binary. Because there is no review gate between CI push and consumer exposure, the window of opportunity for detection is shorter than for manifest-based registries.", + "Likelihood Of Attack": "Low", + "severity": "High", + "condition": "target.controls.hasAccessControl is True and target.controls.providesIntegrity is True and target.controls.usesCodeSigning is False and target.controls.isHardened is False", + "prerequisites": "The attacker has obtained the AUR_SSH_KEY private key from the CI environment. The AUR repository for dfetch-bin accepts pushes authenticated with this key. The attacker can craft a PKGBUILD with a correct SHA256 checksum for a malicious tarball.", + "mitigations": "Store the AUR_SSH_KEY in a deployment environment (C-044) that requires explicit environment protection rules before the secret is injected into the workflow. Block all non-allowlisted egress from the CI runner so the key cannot be exfiltrated. Use a dedicated bot key with the minimum required AUR permissions. Rotate the key regularly and immediately on suspected compromise. Monitor AUR commit history for pushes outside expected CI release windows.", + "example": "A backdoored build dependency reads AUR_SSH_KEY from the runner environment and posts it to an attacker-controlled server. The attacker pushes a PKGBUILD pointing to a trojanised tarball on attacker.example with a matching SHA256. Arch users updating dfetch-bin via yay receive and install the malicious binary before the compromise is detected.", + "references": "https://slsa.dev/spec/v1.0/threats#f-artifact-publication, https://wiki.archlinux.org/title/AUR_submission_guidelines, https://capec.mitre.org/data/definitions/560.html" } ] diff --git a/security/tm_controls_data.py b/security/tm_controls_data.py index 737ee413..793606d9 100644 --- a/security/tm_controls_data.py +++ b/security/tm_controls_data.py @@ -2,7 +2,7 @@ All implemented security controls for dfetch, split by threat model scope: -* ``SC_CONTROLS`` — supply-chain controls (C-009 … C-042) +* ``SC_CONTROLS`` — supply-chain controls (C-009 … C-047) * ``USAGE_CONTROLS`` — runtime-usage controls (C-001 … C-045) This module is intentionally pytm-free so that ``compliance.py`` and other @@ -30,7 +30,7 @@ def to_pytm(self) -> str: return f"[{self.id}] {self.name}" -# ── Supply-chain controls (C-009 … C-042) ──────────────────────────────────── +# ── Supply-chain controls (C-009 … C-047) ──────────────────────────────────── SC_CONTROLS: list[Control] = [ Control( @@ -49,7 +49,7 @@ def to_pytm(self) -> str: name="OIDC trusted publishing", assets=["A-05", "A-03"], threats=["DFT-07"], - reference=".github/workflows/python-publish.yml", + reference=".github/workflows/distribution/python-publish.yml", description=( "PyPI publishes via ``pypa/gh-action-pypi-publish`` with " "``id-token: write`` and no stored long-lived API token." @@ -303,7 +303,7 @@ def to_pytm(self) -> str: name="Winget manifest PRs reviewed by community maintainers", assets=["A-09"], threats=["DFT-35"], - reference=".github/workflows/winget-publish.yml", + reference=".github/workflows/distribution/winget-publish.yml", description=( "Manifest update PRs submitted to ``microsoft/winget-pkgs`` by " "``winget-publish.yml`` go through the standard Winget community review " @@ -321,7 +321,7 @@ def to_pytm(self) -> str: name="WINGET_TOKEN scoped to dedicated Winget environment", assets=["A-10"], threats=["DFT-34"], - reference=".github/workflows/winget-publish.yml", + reference=".github/workflows/distribution/winget-publish.yml", description=( "``WINGET_TOKEN`` is stored in the ``winget`` GitHub Actions deployment " "environment, limiting its exposure: the PAT is only injected into " @@ -333,6 +333,58 @@ def to_pytm(self) -> str: "be stored and rotated manually (DFT-34)." ), ), + Control( + id="C-044", + name="AUR_SSH_KEY scoped to dedicated AUR environment", + assets=["A-12", "A-11"], + threats=["DFT-36"], + reference=".github/workflows/distribution/aur-publish.yml", + description=( + "``AUR_SSH_KEY`` is stored in the ``aur`` GitHub Actions deployment " + "environment, limiting its exposure: the SSH key is only injected into " + "workflows that explicitly reference that environment. " + "Only ``aur-publish.yml`` references the ``aur`` environment, so the " + "key is not available to other workflows. " + "Unlike PyPI which uses OIDC (A-05, no stored long-lived token) or " + "Winget which uses a PAT (A-10), AUR authentication requires an SSH key " + "for git push access; the key must be stored and rotated manually. " + "A compromised key enables direct (no review gate) PKGBUILD pushes to AUR." + ), + ), + Control( + id="C-046", + name="AUR PKGBUILD uses real SHA256 checksum of release tarball", + assets=["A-11"], + threats=["DFT-36"], + reference=".github/workflows/distribution/aur-publish.yml", + description=( + "The ``aur-publish.yml`` workflow computes the SHA256 checksum of the " + "release tarball at publish time and embeds it in the PKGBUILD, replacing " + "the placeholder ``SKIP``. " + "AUR helpers (``yay``, ``paru``) verify this checksum when building the " + "package, so a tampered tarball would be rejected before installation. " + "Residual risk: a compromised AUR_SSH_KEY (A-12) allows pushing a " + "PKGBUILD with a fraudulent checksum — the review gate present in Winget " + "(C-041) does not exist for AUR direct pushes." + ), + ), + Control( + id="C-047", + name="AUR release tarball build-provenance verified before PKGBUILD update", + assets=["A-11", "A-12"], + threats=["DFT-36"], + reference=".github/workflows/distribution/aur-publish.yml", + description=( + "Before computing the SHA256 to embed in the PKGBUILD, ``aur-publish.yml`` " + "runs ``gh attestation verify`` against the downloaded release tarball. " + "This confirms the tarball's Sigstore SLSA build-provenance attestation " + "chain: the artifact was produced by the trusted " + "``build.yml`` workflow from a signed release tag, not tampered with " + "in transit or substituted by a compromised release asset. " + "If attestation fails the workflow aborts before any PKGBUILD update or " + "AUR push." + ), + ), ] # ── Runtime-usage controls (C-001 … C-045) ─────────────────────────────────── diff --git a/security/tm_supply_chain.py b/security/tm_supply_chain.py index 4c6afa5b..05a4ef0c 100644 --- a/security/tm_supply_chain.py +++ b/security/tm_supply_chain.py @@ -196,13 +196,16 @@ def _make_sc_processes(b_github: Boundary) -> tuple[Process, Process, Process]: gh_actions_workflow.inBoundary = b_github gh_actions_workflow.description = ( "CI/CD pipelines: test, build (wheel/msi/deb/rpm), lint, CodeQL, Scorecard, " - "dependency-review, docs, release, winget-publish. " + "dependency-review, docs, release, distribution/winget-publish, " + "distribution/python-publish, distribution/aur-publish. " "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. " - "winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to submit manifest " - "PRs to the Winget Community Repository (A-09)." + "distribution/winget-publish.yml uses a stored PAT (WINGET_TOKEN, A-10) to " + "submit manifest PRs to the Winget Community Repository (A-09). " + "distribution/aur-publish.yml uses an SSH key (AUR_SSH_KEY, A-12) to push " + "PKGBUILDs directly to the AUR repository (A-11)." ) gh_actions_workflow.controls.isHardened = ( True # SHA-pinned actions, harden-runner egress:block on all runner workflows @@ -557,6 +560,110 @@ def _make_sc_consumer_install_flows( df26.controls.isNetworkFlow = True +def _make_sc_aur_elements_and_flows( + gh_actions_runner: ExternalEntity, + consumer: Actor, +) -> None: + """Create AUR assets and publishing/installation flows (DF-30 through DF-32).""" + b_aur = Boundary("AUR Repository") + b_aur.description = ( + "The Arch Linux User Repository (https://aur.archlinux.org) where the " + "``dfetch-bin`` PKGBUILD is hosted. " + "CI pushes updated PKGBUILDs directly to the AUR git repository using " + "``aur-publish.yml`` and the AUR_SSH_KEY (A-12). " + "Unlike Winget (which goes through a community PR review), AUR accepts " + "direct git pushes from authenticated maintainers — there is no " + "intermediate PR review gate." + ) + + Data( + "A-12: AUR_SSH_KEY", + description=( + "Ed25519 SSH private key used to authenticate git pushes to the dfetch-bin " + "AUR repository. Stored as a GitHub Actions environment secret in the " + "``aur`` environment. " + "Used by ``aur-publish.yml`` to push updated PKGBUILDs directly to AUR. " + "Unlike the PyPI OIDC token (A-05) which is short-lived and not stored, " + "this key persists indefinitely until rotated. " + "If exfiltrated from the CI environment, an attacker could push a malicious " + "PKGBUILD directly to AUR without any review gate (DFT-36)." + ), + classification=Classification.SECRET, + isCredentials=True, + isPII=False, + isStored=True, + isDestEncryptedAtRest=True, + isSourceEncryptedAtRest=True, + ) + + aur_repo = ExternalEntity("A-11: AUR Repository (aur.archlinux.org/dfetch-bin)") + aur_repo.inBoundary = b_aur + aur_repo.classification = Classification.SENSITIVE + aur_repo.description = ( + "The Arch Linux User Repository entry for the ``dfetch-bin`` package " + "(https://aur.archlinux.org/packages/dfetch-bin). " + "CI pushes updated PKGBUILDs directly via git using the AUR_SSH_KEY (A-12); " + "there is no PR review step — the push goes live immediately. " + "PKGBUILDs contain the SHA256 checksum of the release tarball; AUR helpers " + "such as ``yay`` download and verify the tarball before installing. " + "A compromised SSH key or a malicious PKGBUILD push could redirect consumers " + "to a tampered binary (DFT-36)." + ) + aur_repo.controls.hasAccessControl = True + aur_repo.controls.providesIntegrity = True + aur_repo.controls.usesCodeSigning = False + aur_repo.controls.isHardened = False + + df30 = Dataflow( + gh_actions_runner, + aur_repo, + "DF-30: AUR PKGBUILD push", + ) + df30.description = ( + "On release event, ``aur-publish.yml`` updates the PKGBUILD in ``aur/`` with " + "the new version and the real SHA256 checksum of the release tarball, then " + "pushes directly to the AUR git repository using the AUR_SSH_KEY (A-12). " + "Unlike Winget (DF-27), this push goes live immediately without a review gate. " + "Mitigated by: harden-runner egress block (C-013), SHA-pinned action (C-009), " + "AUR environment secret scoping (C-044), and real checksum in PKGBUILD (C-046)." + ) + df30.protocol = "SSH" + df30.controls.isEncrypted = True + df30.controls.isHardened = True + df30.controls.hasAccessControl = True + df30.controls.providesIntegrity = True + df30.controls.isNetworkFlow = True + + df31 = Dataflow(consumer, aur_repo, "DF-31: yay install dfetch-bin") + df31.description = ( + "Consumer runs ``yay -S dfetch-bin`` or equivalent AUR helper command. " + "The AUR helper clones the PKGBUILD from AUR (A-11), builds the package, " + "and installs it. " + "The PKGBUILD downloads the release tarball from GitHub Releases and verifies " + "its SHA256 checksum (C-046) before installing. " + "The consumer can additionally verify the binary attestations using " + "``gh attestation verify`` as documented in C-026 and C-039." + ) + df31.protocol = "HTTPS" + df31.controls.isEncrypted = True + df31.controls.isNetworkFlow = True + df31.controls.providesIntegrity = True + + df32 = Dataflow(aur_repo, consumer, "DF-32: Consumer downloads tarball via AUR") + df32.description = ( + "The AUR helper resolves the source URL from the PKGBUILD in A-11 and " + "downloads the tarball from GitHub Releases (A-01). " + "The SHA256 checksum declared in the PKGBUILD (C-046) is verified against " + "the downloaded archive before the binary is installed. " + "The consumer can additionally verify SLSA build provenance, SBOM, and VSA " + "attestations using ``gh attestation verify`` as documented in C-026 and C-039." + ) + df32.protocol = "HTTPS" + df32.controls.isEncrypted = True + df32.controls.providesIntegrity = True + df32.controls.isNetworkFlow = True + + def _make_sc_winget_elements_and_flows( gh_actions_runner: ExternalEntity, consumer: Actor, @@ -687,6 +794,7 @@ def _make_sc_elements_and_flows( _make_sc_publish_flow(gh_actions_runner, pypi) _make_sc_consumer_install_flows(consumer, pypi) _make_sc_winget_elements_and_flows(gh_actions_runner, consumer) + _make_sc_aur_elements_and_flows(gh_actions_runner, consumer) def build_model() -> TM: @@ -699,6 +807,7 @@ def build_model() -> TM: "Threat model for dfetch. " "Covers the pre-install lifecycle: code contribution, CI/CD, " "build (wheel / sdist), PyPI distribution, Winget manifest submission, " + "AUR PKGBUILD publication, " "and consumer installation. " "The installed dfetch package is the handoff point to tm_usage.py." ), @@ -725,6 +834,8 @@ def build_model() -> TM: "A-08b", "A-09", "A-10", + "A-11", + "A-12", } ASSET_CONTROLS: dict[str, list[Control]] = build_asset_controls_index( CONTROLS, _SUPPLY_CHAIN_ASSET_IDS @@ -930,6 +1041,13 @@ def build_model() -> TM: stride=["Tampering", "Spoofing"], target="A-09: Winget Community Repository (microsoft/winget-pkgs)", ), + ThreatResponse( + "DFT-36", + "mitigate", + risk="High", + stride=["Tampering", "Spoofing"], + target="A-11: AUR Repository (aur.archlinux.org/dfetch-bin)", + ), ] if __name__ == "__main__":