Skip to content

[security-audit] FAIL on 2026-07-13 #249

Description

@github-actions

Audit failed at 2026-07-13T07:51Z. Run

Security Audit Report

Audited commit: 81712a7 (main). Repo: diffplug/dormouse. Audit run at 2026-07-13T07:43Z.

FAIL IF results

Dependency Supply Chain

  1. PASSnode website/scripts/generate-deps.js produced no diff against a clean checkout. git status --porcelain and git diff --stat -- website/src/data/dependencies-{npm,cargo,runtime}.json were empty after the script ran (Wrote 26 dependencies..., Wrote 12 direct and 489 transitive Cargo dependencies..., Wrote 1 bundled runtime...).
  2. PASSpackage.json devEngines.runtime.version = "24.18.0", an exact MAJOR.MINOR.PATCH.
  3. PASSstandalone/src-tauri/build.rs:42-43 calls read_pinned_node_version + verify_node_version before bundling, hard-failing the build on mismatch (build.rs:149-227).
  4. PASSrelease.yml:39-41 (build-standalone) uses actions/setup-node@48b55a0... (v6.4.0) with node-version-file: package.json. Verified against upstream actions/setup-node docs at that exact pinned SHA (docs/advanced-usage.md@v6.4.0): node-version-file: package.json resolution order is volta.nodedevEngines.runtime (entry named "node") → engines.node; since this repo's package.json has no volta.node, it resolves the pinned devEngines.runtime.version.
  5. PASSpnpm-workspace.yaml:23 has minimumReleaseAge: 1440.
  6. PASS.github/renovate.json enabledManagers: ["github-actions", "npm", "cargo"].
  7. PASS.github/renovate.json package rules include minimumReleaseAge for npm/cargo at patch (1 day), minor (3 days), major (14 days) update types.

GitHub Actions Policies

  1. PASSgrep -rln pull_request_target .github/workflows/ returns only .github/workflows/tend-review.yaml.
  2. PASS — Non-agent-managed workflows (ci.yml, chromatic.yml, release.yml) grant only contents: read at the top level (or job-level contents: read). release.yml's build-standalone/build-vscode jobs add only id-token: write + attestations: write; the security-audit job adds only actions: write (release.yml:225-234), matching the documented exception; publish-vscode has no job-level permissions: override and inherits the top-level contents: read.

Automated Maintainer (tend)

  1. PASS — Ruleset Merge access (id 16757376): target: branch, conditions.ref_name.include: ["~DEFAULT_BRANCH"], rules: [{"type":"update"}] only, bypass_actors: [{"actor_id":5,"actor_type":"RepositoryRole","bypass_mode":"exempt"}] (role 5 = Admin), sole bypass actor.
  2. PASS — Ruleset Tag operations (id 16757382): target: tag, conditions.ref_name.include: ["~ALL"], rules: [{"type":"creation"},{"type":"update"}], same admin-only sole bypass actor.
  3. PASSGH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/collaborators/dormouse-bot/permission{"permission":"write","permissions":{"admin":false,...,"push":true,...}}. Not higher than push/write.
  4. PASSGH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/secrets → exactly CHROMATIC_PROJECT_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, TEND_BOT_TOKEN. No OVSX_PAT/VSCE_PAT at repo level.
  5. PASS — Environment deployment-branch-policies: vscode-extension-publish[{"name":"v*","type":"tag"}] only (admin-gated by Tag operations). security-audit[{"name":"main","type":"branch"},{"name":"v*","type":"tag"}], both admin-gated (Merge access / Tag operations respectively).
  6. PASSAUDIT_PAT is present in the security-audit environment (gh api .../environments/security-audit/secrets{"total_count":1,"secrets":[{"name":"AUDIT_PAT",...}]}) and absent from the repo-level secret list (check 13).
  7. PASS.config/tend.yaml:4-5secrets: allowed: [CHROMATIC_PROJECT_TOKEN].
  8. PASSworkflow-audit.yaml state: "active"; latest successful run created_at: 2026-07-12T09:11:11Z, current time 2026-07-13T07:43Z → ~22.5h old, within 48h.
  9. PASS — All 8 tend-*.yaml files pin actions/checkout@v7 and max-sixty/tend/claude@0.1.10 — version-tag pins, which the spec explicitly permits for this file class (none use @main or an unpinned ref).
  10. FAILtend-mention.yaml's verify job (lines 34-180) declares no permissions: block at either workflow or job level (only the handle job at line 189 does). The repository's default Actions permission is write: GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/permissions/workflow{"default_workflow_permissions":"write","can_approve_pull_request_reviews":true} — confirmed live, re-checked twice. This means the job's ambient GITHUB_TOKEN is granted write access to scopes beyond the allowed set (checks, deployments, packages, security-events, statuses, and actions:write rather than the permitted actions:read) — exceeding contents:write, pull-requests:write, issues:write, id-token:write, actions:read, or any read permission. In the current file, every run: step in this job explicitly overrides GITHUB_TOKEN to secrets.TEND_BOT_TOKEN (lines 156, 180), so the ambient elevated token is not exercised by any step today — but the grant itself violates the letter of the rule, and the job is reachable by any GitHub user via issue_comment, pull_request_review, pull_request_review_comment, or an issues: edited event (no collaborator gate on the trigger itself). See qualitative findings for two related issues: the systemic root cause, and evidence that this exact violation has already been reported once and silently regressed.

VS Code Extension Releases

  1. PASSrelease.yml:290-291publish-vscode job has environment: name: vscode-extension-publish.
  2. PASSgrep -rn "VSCE_PAT\|OVSX_PAT" .github/workflows/ shows both only inside publish-vscode (release.yml:320, 332), which runs under the vscode-extension-publish environment. Confirmed the environment's own secrets store holds exactly OVSX_PAT and VSCE_PAT and neither appears at repo level.
  3. PASS — No production desktop signing secrets (APPLE_SIGN_PASS, EV_SIGN_PIN, non-ephemeral TAURI_SIGNING_PRIVATE_KEY) appear anywhere in release.yml; grep -n "secrets\." release.yml shows only GITHUB_TOKEN, VSCE_PAT, OVSX_PAT.
  4. PASSrelease.yml:63-74 ("Generate ephemeral Tauri updater key") runs tauri signer generate --ci --write-keys ... --force and exports it as TAURI_SIGNING_PRIVATE_KEY for the unsigned CI build.

Desktop Releases

  1. PASSscripts/sign-and-deploy.sh verify_downloaded_artifact() (lines 372-404) calls gh attestation verify with --cert-identity/--cert-oidc-issuer/--source-ref/--source-digest, invoked from both download_artifacts_from_run() and the cached-artifact path.
  2. PASSscripts/sign-and-deploy.sh check_sha256_manifest() (lines 359-370), called from verify_downloaded_artifact() (line 402), runs sha256sum -c / shasum -a 256 -c against the manifest.
  3. PASSscripts/sign-and-deploy.sh sign_windows() (lines 683-718) uses jsign --storetype PIV --storepass "$EV_SIGN_PIN" --alias "$JSIGN_ALIAS" ....

CI Validation Contract

  1. PASSsecurity-audit.yaml exists, state: "active", and is dispatched from release.yml's security-audit job (release.yml:222-281), which publish-vscode depends on (needs: [build-standalone, build-vscode, security-audit], release.yml:285-288).
  2. PASS — The audit prompt still requires the qualitative pass (security-audit.yaml:90-93), no FAIL IF is marked ignorable, the failure-reporting step that opens/updates a security-audit-failure issue and exit 1s is intact (security-audit.yaml:123-172), and the AUDIT_PAT pre-check (security-audit.yaml:47-55) is present and unbypassed (writes FAIL/error report and exit 1 on a missing secret).

Qualitative findings

  • BLOCKER — repo-wide default_workflow_permissions is write, not read. GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/permissions/workflow returns {"default_workflow_permissions":"write",...}. This is the root cause of FAIL-IF Improve alert icons (and rename from alarm to alert) #19: any workflow or job — present or future, human-authored or tend-authored — that omits an explicit permissions: block silently receives broad write scopes (contents, issues, pull-requests, checks, deployments, packages, security-events, statuses) instead of the least-privilege default the rest of SECURITY.md's ruleset/environment model assumes. Every other reviewed workflow (ci.yml, chromatic.yml, release.yml, security-audit.yaml, workflow-audit.yaml, and every tend-*.yaml job except tend-mention.yaml's verify) happens to declare an explicit permissions: block, so today's actual exposure is limited to that one job — but the repo setting itself is the hole, not just that job, since it fails open by default for anything added later. Fix: flip the repo setting to read (gh api -X PUT repos/diffplug/dormouse/actions/permissions/workflow -f default_workflow_permissions=read -F can_approve_pull_request_reviews=true, admin-only) and add an explicit least-privilege permissions: block to tend-mention.yaml's verify job.

  • BLOCKER — the nightly audit has already reported this exact BLOCKER once and silently stopped reporting it without any code change fixing it, undermining the audit's reliability as a release gate. Issue #236 ("[security-audit] FAIL on 2026-07-09") reported this identical finding — same file, same job, same root cause, same evidence — verbatim down to the API response shape. It was auto-closed the next day ("Audit passed at 2026-07-10T07:59Z") by the workflow's own closure logic, which trusts a single subsequent PASS verdict with no diff against the specific prior finding. git log --follow -- .github/workflows/tend-mention.yaml shows the file's only change since 2026-07-01 is the unrelated 0.1.9 → 0.1.10 tend regeneration (de4ef1ff), which did not touch the verify job's permissions; default_workflow_permissions is still write. Yet the workflow runs on 2026-07-10, 2026-07-11, and 2026-07-12 (all conclusion: success, confirmed via gh run list --workflow security-audit.yaml) all passed. Because security-audit.yaml gates the VS Code publish job, a real, unremediated permission-scope gap has been silently waved through four consecutive nightly runs and would not have blocked a release. Recommendation: don't rely solely on the LLM audit's self-reported PASS/FAIL for issue-closure — cross-check that the specific file/finding named in an open security-audit-failure issue was actually touched by an intervening commit before auto-closing, or have the workflow diff the new report against the previous FAIL report before closing.

  • WARNING — ANTHROPIC_API_KEY is referenced by every tend-*.yaml workflow but is absent from SECURITY.md's threat enumeration and appears unset. grep -rn ANTHROPIC_API_KEY .github/workflows/tend-*.yaml shows it passed as anthropic_api_key: to max-sixty/tend/claude@0.1.10 in all 8 tend workflows, yet SECURITY.md's "Automated Maintainer" section enumerates exactly three reachable secrets (TEND_BOT_TOKEN, CLAUDE_CODE_OAUTH_TOKEN, CHROMATIC_PROJECT_TOKEN). It is not a repo-level secret and does not appear in the org-secrets-visible-to-this-repo list (BUILDCACHE_USER, NEXUS_USER only), so it most likely resolves to an empty string at runtime — no live exposure — but SECURITY.md's tend threat model should either account for it explicitly or the dead reference should be dropped so the documented secret list stays exhaustive.

  • INFO — targeted scan for hardcoded credentials and unsafe fetch-and-execute patterns found nothing. No ghp_/github_pat_/sk-ant-/AWS-key/PEM-private-key patterns and no curl | sh-style pipelines anywhere in workflows, scripts, or lib/dor source (excluding node_modules/dist).

  • INFO — no script-injection pattern found in run: blocks. A structural scan of every .github/workflows/*.y*ml file for ${{ github.event.* }}, ${{ github.head_ref }}, or ${{ inputs.* }} interpolated directly inside a run: string (the classic GitHub Actions script-injection vector) found zero matches; every workflow that handles attacker-influenceable text (tend-mention.yaml, tend-triage.yaml, tend-ci-fix.yaml, workflow-audit.yaml) routes it through env: first.

Summary

Overall: FAIL. One concrete FAIL IF violation was confirmed with direct, reproducible evidence: tend-mention.yaml's verify job has no explicit permissions: block and therefore inherits the repository's default_workflow_permissions: write setting, granting it write scopes beyond what SECURITY.md permits for agent-managed workflows. The systemic root cause (the repo-wide default itself) is flagged as its own BLOCKER, and — more seriously — this exact violation was already caught and reported by a prior audit run (issue #236, 2026-07-09) and then silently stopped being reported for three consecutive nightly runs despite no code change remediating it, which is flagged as a second BLOCKER because it means the audit's PASS verdict cannot currently be trusted as proof that a previously-identified gap was fixed. All 27 other FAIL IF checks passed with direct evidence, and no hardcoded secrets, unsafe curl | sh patterns, or script-injection vectors were found in the qualitative pass. Remediation is narrow: set the repo's default workflow permission to read and add an explicit least-privilege permissions: block to tend-mention.yaml's verify job (patch upstream in the tend generator, or expect a manual override to be reverted on next regeneration), and harden the audit's auto-close logic so a stale PASS run can't silently supersede an unfixed FAIL.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions