From ee86714879202af208f587f783d4239f3e1a9471 Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 11:37:34 -0700 Subject: [PATCH 1/4] chore: add Dependabot security-only update config Add Dependabot configuration for cargo and github-actions ecosystems with weekly schedule. Include automerge workflow for patch/minor updates and dependency-audit workflow for vulnerability scanning. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/dependabot.yml | 19 +++ .github/workflows/dependabot-automerge.yml | 57 +++++++++ .github/workflows/dependency-audit.yml | 136 +++++++++++++++++++++ 3 files changed, 212 insertions(+) create mode 100644 .github/dependabot.yml create mode 100644 .github/workflows/dependabot-automerge.yml create mode 100644 .github/workflows/dependency-audit.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..dc72f04 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,19 @@ +version: 2 +updates: + - package-ecosystem: "cargo" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "security" + - "dependencies" + + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 + labels: + - "security" + - "dependencies" diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml new file mode 100644 index 0000000..4561a0e --- /dev/null +++ b/.github/workflows/dependabot-automerge.yml @@ -0,0 +1,57 @@ +# Dependabot auto-merge workflow +# Copy to .github/workflows/dependabot-automerge.yml +# +# Requires repository secrets: +# APP_ID — GitHub App ID with contents:write and pull-requests:write +# APP_PRIVATE_KEY — GitHub App private key +# +# Auto-approves and squash-merges Dependabot PRs for: +# - Patch version updates +# - Minor version updates +# - Indirect (transitive) dependency updates +# Major version updates are left for human review. +name: Dependabot auto-merge + +on: + pull_request_target: + branches: + - main + +permissions: {} + +jobs: + dependabot: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: read + if: github.event.pull_request.user.login == 'dependabot[bot]' + steps: + - name: Dependabot metadata + id: metadata + uses: dependabot/fetch-metadata@ffa630c65fa7e0ecfa0625b5ceda64399aea1b36 # v2 + with: + github-token: "${{ secrets.GITHUB_TOKEN }}" + + - name: Generate app token + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-patch' || + steps.metadata.outputs.update-type == 'version-update:semver-minor' || + steps.metadata.outputs.dependency-type == 'indirect' + id: app-token + uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 + with: + app-id: ${{ secrets.APP_ID }} + private-key: ${{ secrets.APP_PRIVATE_KEY }} + + - name: Approve and auto-merge + if: >- + steps.metadata.outputs.update-type == 'version-update:semver-patch' || + steps.metadata.outputs.update-type == 'version-update:semver-minor' || + steps.metadata.outputs.dependency-type == 'indirect' + run: | + gh pr review --approve "$PR_URL" + gh pr merge --squash --admin "$PR_URL" + env: + PR_URL: ${{ github.event.pull_request.html_url }} + GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml new file mode 100644 index 0000000..d84556e --- /dev/null +++ b/.github/workflows/dependency-audit.yml @@ -0,0 +1,136 @@ +# Dependency vulnerability audit +# Copy to .github/workflows/dependency-audit.yml +# +# Auto-detects ecosystems present in the repository and runs the appropriate +# audit tool. Fails the build if any dependency has a known security advisory. +# +# Add "dependency-audit" as a required status check in branch protection. +name: Dependency audit + +on: + pull_request: + branches: [main] + push: + branches: [main] + +permissions: + contents: read + +jobs: + detect: + name: Detect ecosystems + runs-on: ubuntu-latest + outputs: + npm: ${{ steps.check.outputs.npm }} + gomod: ${{ steps.check.outputs.gomod }} + cargo: ${{ steps.check.outputs.cargo }} + pip: ${{ steps.check.outputs.pip }} + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Detect package ecosystems + id: check + run: | + # npm + if find . -name 'package-lock.json' -not -path '*/node_modules/*' | grep -q .; then + echo "npm=true" >> "$GITHUB_OUTPUT" + else + echo "npm=false" >> "$GITHUB_OUTPUT" + fi + + # Go modules + if find . -name 'go.sum' | grep -q .; then + echo "gomod=true" >> "$GITHUB_OUTPUT" + else + echo "gomod=false" >> "$GITHUB_OUTPUT" + fi + + # Cargo + if [ -f "Cargo.lock" ]; then + echo "cargo=true" >> "$GITHUB_OUTPUT" + else + echo "cargo=false" >> "$GITHUB_OUTPUT" + fi + + # Python + if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then + echo "pip=true" >> "$GITHUB_OUTPUT" + else + echo "pip=false" >> "$GITHUB_OUTPUT" + fi + + audit-npm: + name: npm audit + needs: detect + if: needs.detect.outputs.npm == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "lts/*" + + - name: Audit npm dependencies + run: npm audit --audit-level=moderate + + audit-go: + name: govulncheck + needs: detect + if: needs.detect.outputs.gomod == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: actions/setup-go@d35c59abb061a4a6fb18e82ac0862c26744d6ab5 # v5 + with: + go-version: "stable" + + - name: Install govulncheck + run: go install golang.org/x/vuln/cmd/govulncheck@latest + + - name: Audit Go dependencies + run: | + # Run govulncheck in each module directory + find . -name 'go.mod' -exec dirname {} \; | while read -r dir; do + echo "::group::govulncheck $dir" + (cd "$dir" && govulncheck ./...) + echo "::endgroup::" + done + + audit-cargo: + name: cargo audit + needs: detect + if: needs.detect.outputs.cargo == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - name: Install cargo-audit + run: cargo install cargo-audit + + - name: Audit Cargo dependencies + run: cargo audit + + audit-pip: + name: pip-audit + needs: detect + if: needs.detect.outputs.pip == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 + with: + python-version: "3.x" + + - name: Install pip-audit + run: pip install pip-audit + + - name: Audit Python dependencies + run: | + if [ -f "pyproject.toml" ]; then + pip-audit --require-hashes=false + elif [ -f "requirements.txt" ]; then + pip-audit -r requirements.txt + fi From c304a568e9c40a45007c5c4afce53610270c713d Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 11:46:35 -0700 Subject: [PATCH 2/4] fix: update to latest security-only standards (limit:0, improved audit) Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/dependabot.yml | 2 +- .github/workflows/dependabot-automerge.yml | 49 ++++++++---- .github/workflows/dependency-audit.yml | 88 ++++++++++++++++------ 3 files changed, 103 insertions(+), 36 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index dc72f04..4012cb0 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -4,7 +4,7 @@ updates: directory: "/" schedule: interval: "weekly" - open-pull-requests-limit: 10 + open-pull-requests-limit: 0 labels: - "security" - "dependencies" diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 4561a0e..6080c9a 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -5,11 +5,17 @@ # APP_ID — GitHub App ID with contents:write and pull-requests:write # APP_PRIVATE_KEY — GitHub App private key # -# Auto-approves and squash-merges Dependabot PRs for: -# - Patch version updates -# - Minor version updates +# Auto-approves and enables auto-merge for Dependabot PRs that are: +# - GitHub Actions updates (patch or minor version bumps) +# - Security updates for any ecosystem (patch or minor) # - Indirect (transitive) dependency updates -# Major version updates are left for human review. +# Major version updates are always left for human review. +# Uses --auto so the merge waits for all required CI checks to pass. +# +# Safety model: application ecosystems use open-pull-requests-limit: 0 in +# dependabot.yml, so the only app-ecosystem PRs Dependabot can create are +# security updates. This workflow adds defense-in-depth by also checking +# the package ecosystem. name: Dependabot auto-merge on: @@ -33,25 +39,40 @@ jobs: with: github-token: "${{ secrets.GITHUB_TOKEN }}" + - name: Determine if auto-merge eligible + id: eligible + run: | + UPDATE_TYPE="${{ steps.metadata.outputs.update-type }}" + DEP_TYPE="${{ steps.metadata.outputs.dependency-type }}" + ECOSYSTEM="${{ steps.metadata.outputs.package-ecosystem }}" + + # Must be patch, minor, or indirect + if [[ "$UPDATE_TYPE" != "version-update:semver-patch" && \ + "$UPDATE_TYPE" != "version-update:semver-minor" && \ + "$DEP_TYPE" != "indirect" ]]; then + echo "eligible=false" >> "$GITHUB_OUTPUT" + echo "Skipping: major update requires human review" + exit 0 + fi + + # GitHub Actions version updates are always eligible + # App ecosystem PRs can only exist as security updates (limit: 0) + echo "eligible=true" >> "$GITHUB_OUTPUT" + echo "Auto-merge eligible: ecosystem=$ECOSYSTEM update=$UPDATE_TYPE" + - name: Generate app token - if: >- - steps.metadata.outputs.update-type == 'version-update:semver-patch' || - steps.metadata.outputs.update-type == 'version-update:semver-minor' || - steps.metadata.outputs.dependency-type == 'indirect' + if: steps.eligible.outputs.eligible == 'true' id: app-token uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3 with: app-id: ${{ secrets.APP_ID }} private-key: ${{ secrets.APP_PRIVATE_KEY }} - - name: Approve and auto-merge - if: >- - steps.metadata.outputs.update-type == 'version-update:semver-patch' || - steps.metadata.outputs.update-type == 'version-update:semver-minor' || - steps.metadata.outputs.dependency-type == 'indirect' + - name: Approve and enable auto-merge + if: steps.eligible.outputs.eligible == 'true' run: | gh pr review --approve "$PR_URL" - gh pr merge --squash --admin "$PR_URL" + gh pr merge --auto --squash "$PR_URL" env: PR_URL: ${{ github.event.pull_request.html_url }} GH_TOKEN: ${{ steps.app-token.outputs.token }} diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index d84556e..36a8f6a 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -5,6 +5,9 @@ # audit tool. Fails the build if any dependency has a known security advisory. # # Add "dependency-audit" as a required status check in branch protection. +# +# Pinned tool versions (update deliberately): +# govulncheck v1.1.4 | cargo-audit 0.21.1 | pip-audit 2.9.0 name: Dependency audit on: @@ -31,29 +34,30 @@ jobs: - name: Detect package ecosystems id: check run: | - # npm + # npm — look for package-lock.json anywhere (excluding node_modules) if find . -name 'package-lock.json' -not -path '*/node_modules/*' | grep -q .; then echo "npm=true" >> "$GITHUB_OUTPUT" else echo "npm=false" >> "$GITHUB_OUTPUT" fi - # Go modules - if find . -name 'go.sum' | grep -q .; then + # Go modules — detect via go.mod (not go.sum, which may not exist) + if find . -name 'go.mod' -not -path '*/vendor/*' | grep -q .; then echo "gomod=true" >> "$GITHUB_OUTPUT" else echo "gomod=false" >> "$GITHUB_OUTPUT" fi - # Cargo - if [ -f "Cargo.lock" ]; then + # Cargo — detect via Cargo.toml anywhere (lockfile may not exist for libraries) + if find . -name 'Cargo.toml' -not -path '*/target/*' | grep -q .; then echo "cargo=true" >> "$GITHUB_OUTPUT" else echo "cargo=false" >> "$GITHUB_OUTPUT" fi - # Python - if [ -f "pyproject.toml" ] || [ -f "requirements.txt" ]; then + # Python — detect pyproject.toml or requirements.txt anywhere + if find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q . || \ + find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' | grep -q .; then echo "pip=true" >> "$GITHUB_OUTPUT" else echo "pip=false" >> "$GITHUB_OUTPUT" @@ -72,7 +76,17 @@ jobs: node-version: "lts/*" - name: Audit npm dependencies - run: npm audit --audit-level=moderate + run: | + # Audit each package-lock.json found in the repo + status=0 + while IFS= read -r dir; do + echo "::group::npm audit $dir" + if ! (cd "$dir" && npm audit --audit-level=low); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'package-lock.json' -not -path '*/node_modules/*' -exec dirname {} \;) + exit $status audit-go: name: govulncheck @@ -87,16 +101,19 @@ jobs: go-version: "stable" - name: Install govulncheck - run: go install golang.org/x/vuln/cmd/govulncheck@latest + run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4 - name: Audit Go dependencies run: | - # Run govulncheck in each module directory - find . -name 'go.mod' -exec dirname {} \; | while read -r dir; do + status=0 + while IFS= read -r dir; do echo "::group::govulncheck $dir" - (cd "$dir" && govulncheck ./...) + if ! (cd "$dir" && govulncheck ./...); then + status=1 + fi echo "::endgroup::" - done + done < <(find . -name 'go.mod' -not -path '*/vendor/*' -exec dirname {} \;) + exit $status audit-cargo: name: cargo audit @@ -106,11 +123,24 @@ jobs: steps: - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + - uses: dtolnay/rust-toolchain@stable + - name: Install cargo-audit - run: cargo install cargo-audit + run: cargo install cargo-audit@0.21.1 --locked - name: Audit Cargo dependencies - run: cargo audit + run: | + # cargo audit operates on Cargo.lock at workspace root + # For workspaces, a single audit at root covers all crates + status=0 + while IFS= read -r dir; do + echo "::group::cargo audit $dir" + if ! (cd "$dir" && cargo generate-lockfile 2>/dev/null; cargo audit); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'Cargo.toml' -not -path '*/target/*' -exec dirname {} \; | sort -u) + exit $status audit-pip: name: pip-audit @@ -125,12 +155,28 @@ jobs: python-version: "3.x" - name: Install pip-audit - run: pip install pip-audit + run: pip install pip-audit==2.9.0 - name: Audit Python dependencies run: | - if [ -f "pyproject.toml" ]; then - pip-audit --require-hashes=false - elif [ -f "requirements.txt" ]; then - pip-audit -r requirements.txt - fi + status=0 + # Audit each Python project found in the repo + while IFS= read -r dir; do + echo "::group::pip-audit $dir" + if [ -f "$dir/pyproject.toml" ]; then + if ! pip-audit "$dir"; then + status=1 + fi + elif [ -f "$dir/requirements.txt" ]; then + if ! pip-audit -r "$dir/requirements.txt"; then + status=1 + fi + fi + echo "::endgroup::" + done < <( + { + find . -name 'pyproject.toml' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \; + find . -name 'requirements.txt' -not -path '*/.venv/*' -not -path '*/venv/*' -exec dirname {} \; + } | sort -u + ) + exit $status From 763a4a80345c3d3e7b97ef6d34cce48807260c34 Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 11:52:13 -0700 Subject: [PATCH 3/4] fix: bump cargo-audit to 0.22.1 for CVSS 4.0 support Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/dependency-audit.yml | 40 ++++++++++++++++++++++++-- 1 file changed, 38 insertions(+), 2 deletions(-) diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index 36a8f6a..cdd7351 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -7,7 +7,7 @@ # Add "dependency-audit" as a required status check in branch protection. # # Pinned tool versions (update deliberately): -# govulncheck v1.1.4 | cargo-audit 0.21.1 | pip-audit 2.9.0 +# govulncheck v1.1.4 | cargo-audit 0.22.1 | pip-audit 2.9.0 name: Dependency audit on: @@ -25,6 +25,7 @@ jobs: runs-on: ubuntu-latest outputs: npm: ${{ steps.check.outputs.npm }} + pnpm: ${{ steps.check.outputs.pnpm }} gomod: ${{ steps.check.outputs.gomod }} cargo: ${{ steps.check.outputs.cargo }} pip: ${{ steps.check.outputs.pip }} @@ -41,6 +42,13 @@ jobs: echo "npm=false" >> "$GITHUB_OUTPUT" fi + # pnpm — look for pnpm-lock.yaml anywhere + if find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' | grep -q .; then + echo "pnpm=true" >> "$GITHUB_OUTPUT" + else + echo "pnpm=false" >> "$GITHUB_OUTPUT" + fi + # Go modules — detect via go.mod (not go.sum, which may not exist) if find . -name 'go.mod' -not -path '*/vendor/*' | grep -q .; then echo "gomod=true" >> "$GITHUB_OUTPUT" @@ -88,6 +96,34 @@ jobs: done < <(find . -name 'package-lock.json' -not -path '*/node_modules/*' -exec dirname {} \;) exit $status + audit-pnpm: + name: pnpm audit + needs: detect + if: needs.detect.outputs.pnpm == 'true' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4 + + - uses: pnpm/action-setup@a7487c7e89a18df4991f7f222e4898a00d66ddda # v4 + + - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 + with: + node-version: "lts/*" + cache: "pnpm" + + - name: Audit pnpm dependencies + run: | + # Audit each pnpm-lock.yaml found in the repo + status=0 + while IFS= read -r dir; do + echo "::group::pnpm audit $dir" + if ! (cd "$dir" && pnpm audit --audit-level low); then + status=1 + fi + echo "::endgroup::" + done < <(find . -name 'pnpm-lock.yaml' -not -path '*/node_modules/*' -exec dirname {} \;) + exit $status + audit-go: name: govulncheck needs: detect @@ -126,7 +162,7 @@ jobs: - uses: dtolnay/rust-toolchain@stable - name: Install cargo-audit - run: cargo install cargo-audit@0.21.1 --locked + run: cargo install cargo-audit@0.22.1 --locked - name: Audit Cargo dependencies run: | From 14f99d3b356da65475c704b89f0dfa5ab3de7290 Mon Sep 17 00:00:00 2001 From: DJ Date: Fri, 3 Apr 2026 20:08:04 -0700 Subject: [PATCH 4/4] chore: strip template header comments from deployed workflows Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/dependabot-automerge.yml | 1 - .github/workflows/dependency-audit.yml | 1 - 2 files changed, 2 deletions(-) diff --git a/.github/workflows/dependabot-automerge.yml b/.github/workflows/dependabot-automerge.yml index 6080c9a..aa27ff0 100644 --- a/.github/workflows/dependabot-automerge.yml +++ b/.github/workflows/dependabot-automerge.yml @@ -1,5 +1,4 @@ # Dependabot auto-merge workflow -# Copy to .github/workflows/dependabot-automerge.yml # # Requires repository secrets: # APP_ID — GitHub App ID with contents:write and pull-requests:write diff --git a/.github/workflows/dependency-audit.yml b/.github/workflows/dependency-audit.yml index cdd7351..943ba65 100644 --- a/.github/workflows/dependency-audit.yml +++ b/.github/workflows/dependency-audit.yml @@ -1,5 +1,4 @@ # Dependency vulnerability audit -# Copy to .github/workflows/dependency-audit.yml # # Auto-detects ecosystems present in the repository and runs the appropriate # audit tool. Fails the build if any dependency has a known security advisory.