[security-audit] FAIL on 2026-09-16
- Vorherrschende Sprache
- TypeScript
- Sterne
- 5
- Forks
- 0
- Ø Merge
- 14 Std. 13 Min.
- Gemergte PRs (30 T.)
- 199
Beschreibung
Audit failed at 2026-09-09T09:18Z. [Run](https://github.com/diffplug/dormouse/actions/runs/34332049244) · [Transcript](https://github.com/diffplug/dormouse/actions/runs/34332049244/artifacts/10096921421)
- **A domain returned `FAIL`.** audit-application.md opened with `VERDICT: FAIL` — read that domain's section first. A domain's own verdict outranks the merged one.
# Security audit
## Supply chain
VERDICT: PASS
### FAIL IF results
**Disclosure**
- `node website/scripts/generate-deps.js` after `pnpm install --frozen-lockfile` produces no diff in `website/src/data/` — **PASS**. Ran `pnpm install --frozen-lockfile` (already satisfied, "Already up to date"), then the generator: "Wrote 55 dependencies to src/data/dependencies-npm.json", "Wrote 13 direct and 478 transitive Cargo dependencies to src/data/dependencies-cargo.json", "Wrote 1 bundled runtime to src/data/dependencies-runtime.json". `git status --porcelain -- website/src/data/` and `git diff -- website/src/data/` were both empty. Tree left clean, no revert needed.
- `.github/workflows/ci.yml` keeps running the generator under the frozen-lockfile precondition and fails on a diff — **PASS**. Job `build-and-test` runs `pnpm install --frozen-lockfile` (line 25), then step "Dependency disclosure is current" (lines 30–44) runs `node website/scripts/generate-deps.js` and does `git diff --quiet -- website/src/data/ || exit 1`.
- The disclosure does not omit a shipped workspace's graph or exclude a shipped package — **PASS**. Derived from `pnpm-workspace.yaml`'s 10 packages against `productDependencyFilters`/`excludedWorkspacePackages` in `website/scripts/generate-deps.js`: `dor`→root, `dormouse-lib` (`lib`)→root, `relay`→root, `dormouse-standalone` (`standalone`)→root, `dormouse-sidecar` (`standalone/sidecar`)→root, `dormouse` (`vscode-ext`)→root, `canopy`→excluded, `dormouse-website` (`website`)→excluded, `dor-lib-common` and `remote-lib-common`→reachable workspace edges (not named directly, per the spec's own note). `assertWorkspaceCoverage` (`website/scripts/dependency-workspaces.js`) enforces this and did not throw during the generator run (an "Unclassified workspace packages" or "Excluded workspace ... is reachable" error would have aborted it with exit code 1). `npx vitest run scripts/dependency-workspaces.test.js` (run from `website/`): 1 file passed, 6/6 tests passed.
**Bundled runtime**
- Root `package.json` has `devEngines.runtime.version` as an exact `MAJOR.MINOR.PATCH` — **PASS**. `{"runtime":{"name":"node","version":"24.18.0","onFail":"download"}}`.
- `standalone/src-tauri/build.rs` runs `--version` on the binary and fails the build on mismatch, with the one permitted cross-arch skip — **PASS**. `verify_node_version` (lines 191–227) runs `node_source --version`, compares to `read_pinned_node_version`'s parse of `package.json`, and returns `Err` on mismatch; it `Ok`-returns with a `cargo:warning` only when `host != target` (lines 197–205), matching the allowed skip.
- `release.yml`'s standalone matrix is entirely host-native (no skip is exercised in the release build) — **PASS**. All three `build-standalone` matrix entries pair native host/target: `ubuntu-22.04`→`x86_64-unknown-linux-gnu`, `macos-latest`→`aarch64-apple-darwin`, `windows-latest`→`x86_64-pc-windows-msvc` (`.github/workflows/release.yml` lines 25–36).
- `build-standalone` installs the pinned runtime via `node-version-file: package.json`, and `package.json` has no `volta.node` or `engines.node` — **PASS**. `.github/workflows/release.yml` line 48 (and lines 193, 324 for the other jobs) use `node-version-file: package.json`; `grep -n "volta\|\"engines\"" package.json` returned nothing.
**Cooldown and alerts**
- `pnpm-workspace.yaml` has `minimumReleaseAge: 1440` — **PASS** (confirmed present at end of file).
- `.github/renovate.json` has `npm` and `cargo` in `enabledManagers`, with `minimumReleaseAge` package rules for both — **PASS**. `"enabledManagers": ["github-actions", "npm", "cargo"]`; `packageRules` include `matchUpdateTypes: patch/minor/major` entries scoped to `["npm", "cargo"]` at 1/3/14 days respectively.
- `.github/renovate.json` has a `vulnerabilityAlerts` block with `minimumReleaseAge` set explicitly — **PASS**. `"vulnerabilityAlerts": {"enabled": true, "schedule": [], "minimumReleaseAge": "1 day", "draftPR": false, "labels": [...]}`, with an explanatory comment array reiterating why omission would silently drop the cooldown.
- Secret scanning, push protection, and Dependabot alerts are enabled — **PASS** (all three clauses individually verified via `gh api` with `GH_TOKEN=$AUDIT_PAT`, since the default token got 403): `repos/diffplug/dormouse --jq .security_and_analysis` → `"secret_scanning":{"status":"enabled"}`, `"secret_scanning_push_protection":{"status":"enabled"}`; `GET /repos/diffplug/dormouse/vulnerability-alerts` → `HTTP/2.0 204 No Content` (204, not 404, meaning Dependabot alerts are on).
### Qualitative findings
**INFO** — Newly added/upgraded runtime dependencies since the last successful audit (2026-09-08T08:52 UTC, run 34206861498; the next scheduled run started in parallel with this one). Six Renovate PRs merged same-day: `tailwind-variants` v3.3.1 (#594), `playwright-core` v1.63.0 (#593), `@tauri-apps/plugin-updater` v2.11.0 (#592), `hono` v4.13.7 (#591), `@types/react-dom` v19.2.7 (#590), `@tauri-apps/plugin-shell` v2.3.6 (#589). Checked each: `tailwind-variants`, `@tauri-apps/plugin-updater`, `hono`, `@tauri-apps/plugin-shell` are production dependencies and each commit carries a matching `website/src/data/dependencies-npm.json` update (verified via `git show --stat`); `playwright-core` (`vscode-ext/package.json` `devDependencies`) and `@types/react-dom` (four packages, all `devDependencies`) correctly carry no disclosure-file change since they never reach a user's machine. All six are ordinary version bumps (no dependency additions/removals), all pnpm-managed and already reflected in the clean disclosure regeneration above. No BLOCKER or WARNING.
**INFO** — Lockfile resolutions outside the registry: exactly one, `@diffplug/xterm-addon-webgl-sdf` at `canopy/package.json`, resolved as a GitHub Releases tarball (`https://github.com/diffplug/xterm.js/releases/download/sdf-v0.20.0-sdf304.0/...tgz`) with an `integrity: sha512-...` pin in `pnpm-lock.yaml`. `canopy` is one of the two workspaces the spec explicitly excludes as a root ("Storybook-only rendering lab no shipped build imports"), and this is exactly the fork dependency `AGENTS.md`'s Architecture section documents by name. Confirmed no such non-registry resolution is reachable from any of the six product roots: all 55 disclosed npm entries have ordinary semver-looking `version` fields (no `http`/`github` substrings), and `standalone/src-tauri/Cargo.lock` shows every one of its 491 non-root packages sourced from `registry+https://github.com/rust-lang/crates.io-index` (the only package with no `source` line is the local `dormouse` crate itself). No BLOCKER or WARNING.
**INFO** — Install scripts in production dependencies: `pnpm-workspace.yaml`'s `allowBuilds` allowlist (`@swc/core: true`, `esbuild: true`, `node-pty: true`, `sharp: true`, and explicit denials `@vscode/vsce-sign: false`, `keytar: false`) is being enforced, not just declared. `node_modules/.modules.yaml` after a frozen-lockfile install shows `"ignoredBuilds": []` and `"pendingBuilds": []` — no package with a build/install script is sitting untriaged, which is what would happen if a newly bumped dependency introduced one Renovate hadn't been taught about. Both `keytar@7.9.0` and `@vscode/vsce-sign@2.0.9` are present in the lockfile (transitive, packaging-tool-only) and correctly build-denied rather than absent. No BLOCKER or WARNING.
**INFO** — Reachable-but-undisclosed check: no gap found. `dor-lib-common` and `remote-lib-common` (workspace edges, not named roots) are exercised by the coverage assertion and its pinned test; `pnpm-workspace.yaml` was not touched since the last audit, so no new workspace package needs classification.
No BLOCKER or WARNING findings. All `FAIL IF` checks in scope were determined (none `UNVERIFIABLE`).
## CI and secrets
VERDICT: PASS
### FAIL IF results
**docs/specs/security.md**
- Private vulnerability reporting enabled — PASS. `gh api repos/diffplug/dormouse/private-vulnerability-reporting` → `{"enabled":true}`.
**docs/specs/security-ci.md — GitHub Actions Policies**
- `pull_request_target` appears only in `tend-*.yaml` — PASS. Repo-wide grep found exactly one hit: `.github/workflows/tend-review.yaml:11`. No other `.github/workflows/**` file uses it.
- Non-agent-managed workflow effective write permissions restricted to the named exceptions — PASS. `release.yml`: top-level `contents: read`; `build-standalone`/`build-vscode` jobs add only `id-token: write` + `attestations: write` (the named exception); `security-audit` job adds only `actions: write` (the named exception); `publish-vscode` has no job-level block, inheriting `contents: read`. `ci.yml` and `chromatic.yml` each declare only `contents: read`, no job overrides.
**Automated Maintainer (tend)**
- `scripts/workflow-audit.test.mjs` pins the tend-regen materialization boundary (mode-100644/100755-only, symlink rejection, no unrelated generated files) — PASS. `node scripts/workflow-audit.test.mjs`: 6/6 tests pass, including "never materializes an unrelated symlink" and "rejects symlink workflow inputs before running the generator."
- `workflow-audit.yaml`'s lower bound is the previous successful run's server-set `created_at`, never pusher-controlled — PASS. Read `.github/workflows/workflow-audit.yaml`: `SINCE=$(gh api .../workflows/workflow-audit.yaml/runs?status=success...--jq '.workflow_runs[0].created_at // ""')`, falling back only to a fixed `25 hours ago` if no prior run exists. No `--since` derived from commit data.
- Admin-gating rulesets — PASS, both clauses independently verified via `GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/rulesets/16757376` and `/16757382`:
- `Merge access` (16757376): `target: branch`, `conditions.ref_name.include: ["~DEFAULT_BRANCH"]`, `rules: [{type: update}]`, `bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}]` — exact match.
- `Tag operations` (16757382): `target: tag`, `conditions.ref_name.include: ["~ALL"]`, `rules: [{type: creation}, {type: update}]`, `bypass_actors: [{actor_id: 5, actor_type: RepositoryRole}]` — exact match.
- `dormouse-bot` holds neither `maintain` nor `admin` — PASS. `GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/collaborators/dormouse-bot/permission` → `permission: "write"`, `role_name: "write"`, `permissions: {admin: false, maintain: false, push: true, ...}`.
- Every environment's deployment-branch-policy admits only refs admin-gated by the two rulesets — PASS, checked all four environments individually:
- `release-attest`: `v*` tag only (Tag operations).
- `security-audit`: `main` branch + `v*` tag (Merge access + Tag operations).
- `tend`: `main` only (Merge access).
- `vscode-extension-publish`: `v*` tag only (Tag operations).
All match the spec's "Today:" list exactly.
- Secret inventory placement — PASS, verified with one `gh api` call per surface:
- Repo-level (`actions/secrets`): only `CHROMATIC_PROJECT_TOKEN`.
- Org-level (`actions/organization-secrets`): `total_count: 0`.
- `security-audit` env secrets: `AUDIT_PAT`, `CLAUDE_CODE_OAUTH_TOKEN` — exact match.
- `tend` env secrets: `TEND_BOT_TOKEN`, `CLAUDE_CODE_OAUTH_TOKEN` — exact match.
- `vscode-extension-publish` env secrets: `OVSX_PAT`, `VSCE_PAT` — exact match.
- `release-attest` env: `secrets: []`, `variables: []` — empty as required.
- No `ANTHROPIC_API_KEY` visible at repo or org level.
- `CHROMATIC_PROJECT_TOKEN` present in `.config/tend.yaml` `secrets.allowed` — PASS. File contents: `secrets:\n allowed:\n - CHROMATIC_PROJECT_TOKEN`.
- `workflow-audit.yaml` not missing/disabled, successful run within 48h — PASS. `gh api .../actions/workflows` shows it `active`; last 5 runs all `success`, most recent `2026-09-08T12:06:14Z` (~21h before this audit at `2026-09-09T09:00Z`).
- Every `tend-*.yaml` pins `max-sixty/tend` at ≥0.1.19 — PASS. All 8 files pin `max-sixty/tend/claude@0.2.0` and carry header `# Generated by tend 0.2.0`.
- Unpinned action refs confined to `tend-*.yaml` — PASS. Repo-wide grep for unpinned (`@vN`/tag-style) `uses:` lines returned matches only inside `tend-ci-fix.yaml`, `tend-mention.yaml`, `tend-nightly.yaml`, `tend-notifications.yaml`, `tend-review-runs.yaml`, `tend-review.yaml`, `tend-triage.yaml`, `tend-weekly.yaml` (`actions/checkout@v7`, `astral-sh/setup-uv@v10.0.1`). Every other workflow's `uses:` lines carry a commit-SHA pin with a version comment (spot-checked `release.yml`, `ci.yml`, `chromatic.yml`, `security-audit.yaml`, `workflow-audit.yaml`).
- Agent-managed workflows' effective `GITHUB_TOKEN` permissions stay within the allowed set — PASS. Read every job-level (and workflow-level, where no job override exists) `permissions:` block in `security-audit.yaml`, `workflow-audit.yaml`, and all 8 `tend-*.yaml` files: observed scopes are exactly `{contents: write|read, pull-requests: write|read, actions: read, issues: write, id-token: write}` — no scope outside the allowed list on any job.
- `default_workflow_permissions` is `read` and `can_approve_pull_request_reviews` is `false` — PASS. `GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/actions/permissions/workflow` → `{"default_workflow_permissions":"read","can_approve_pull_request_reviews":false}`.
**VS Code Extension Releases**
- `vscode-extension-publish` environment protections — PASS. `GH_TOKEN=$AUDIT_PAT gh api repos/diffplug/dormouse/environments` shows nonempty `required_reviewers` (2 named users), `prevent_self_review: true` on that protection rule, and top-level `can_admins_bypass: false`.
- `release.yml`'s VS Code publish job is bound to `vscode-extension-publish`, and `VSCE_PAT`/`OVSX_PAT` appear nowhere else — PASS. `publish-vscode` job declares `environment: {name: vscode-extension-publish}`; repo-wide grep for `VSCE_PAT|OVSX_PAT` under `.github/workflows/` found both references only inside that job (lines 347, 359 of `release.yml`).
- `release.yml` does not use production desktop signing secrets in CI and generates an ephemeral Tauri updater key — PASS. `build-standalone` runs `tauri signer generate --ci --write-keys "$RUNNER_TEMP/tauri-ci-updater.key" --force` and exports it as `TAURI_SIGNING_PRIVATE_KEY` for that job only; repo-wide grep for `EV_SIGN_PIN`/`APPLE_SIGN_PASS` under `.github/workflows/` returned no hits, and the only `TAURI_SIGNING_PRIVATE_KEY` hit in workflows is this ephemeral-key assignment.
**Desktop Releases**
- `scripts/sign-and-deploy.sh` verifies GitHub artifact attestations, verifies SHA-256 manifests, and uses PIV-backed Windows signing — PASS. `verify_downloaded_artifact()` runs `gh attestation verify` with `--cert-identity`/`--cert-oidc-issuer`/`--source-ref`/`--source-digest`, and `check_sha256_manifest()` runs `sha256sum -c`/`shasum -a 256 -c`; `sign_windows()` calls `jsign --storetype PIV --storepass env:EV_SIGN_PIN`.
- `TAURI_SIGNING_PRIVATE_KEY` is env-only and `EV_SIGN_PIN` is passed to `jsign --storepass` by environment reference, not literally — PASS. `sign_updates()` invokes `TAURI_SIGNING_PRIVATE_KEY="$TAURI_SIGNING_PRIVATE_KEY" ... tauri signer sign "$bundle"` (env prefix, never on the `tauri` argv), and both `jsign` calls use the literal string `env:EV_SIGN_PIN` as the `--storepass` argument (jsign resolves it from the environment itself), never the PIN's value. `APPLE_SIGN_PASS` does travel on `notarytool`'s argv — this is the spec's documented, already-accepted known gap, not a new finding.
**docs/specs/security-audit.md**
- Every `docs/specs/security*.md` spec claimed by exactly one domain, no scope names a nonexistent file — PASS. `node scripts/spec-lint.mjs` → `spec-lint: OK (33 specs, 65 files checked)`; its check 16 is exactly this rule, and it passed. Manually cross-checked the 6 non-rationale `security*.md` files against the three domain `**Scope` blocks: each appears exactly once.
- Dedicated `application-security` subagent, not merged into another domain's context — PASS. `.github/audit/application-security.md` exists standalone, scoped only to `security-local.md`/`security-remote.md`; `.github/workflows/security-audit.yaml`'s `--agents` JSON defines it as a separate agent from `ci-and-secrets`/`supply-chain`.
- `application-security` on a stronger model than the mechanical domains, in both CI and local — PASS. `security-audit.yaml`: top-level `claude_args` sets `--model sonnet`, and the `--agents` JSON overrides `application-security` alone with `"model":"opus"`. `scripts/security-audit-local.sh`: `run_domain()` sets `model_args="--model sonnet"` unconditionally then overrides to `--model opus` only when `domain = application-security`.
- `.github/audit/` has every prompt file the workflow names, and the local runner uses the same files — PASS. All five files (`_preamble.md`, `orchestrator.md`, `supply-chain.md`, `ci-and-secrets.md`, `application-security.md`) exist; `security-audit-local.sh` loops over exactly that filename set at startup and fails closed if any is missing; both CI and local prompts are `Read ".github/audit/_preamble.md" and ".github/audit/.md"` pointers, not inlined copies.
- Union of qualitative scopes covers every top-level path — PASS. `ci-and-secrets` owns `.github/`, `.config/`, `.claude/`, `.vscode/`, `scripts/`, `website/public/`, plus any secret-touching code; `supply-chain` owns the dependency graph, lockfile, and all of `website/` except `website/public/`; `application-security` is defined as the recursive subtraction remainder, covering everything else including `.impeccable/` and root files. `ls -A` of the repo root shows nothing outside this union (`node_modules/` is gitignored and untracked, not a scope gap).
- `.github/audit/` and `.vscode/` both inside `workflow-audit.yaml`'s diff window — PASS. `WINDOW=(.github/workflows/ .config/tend.yaml .github/audit/ .vscode/)`, and `WINDOW_NON_WORKFLOW=("${WINDOW[@]:1}")` is derived from it (not hand-duplicated), consumed identically by the commit list, `own_changes()`, and both classifiers' refusal checks.
**Orchestration**
- `orchestrator.md` requires a non-turn-ending wait via a persisted-deadline Bash `until` loop, re-issued past the 10-minute cap, under a 25-minute deadline — PASS. §2 of `.github/audit/orchestrator.md` specifies exactly this, with `DEADLINE_FILE="$RUNNER_TEMP/audit-deadline"` persisted across re-issues.
- `security-audit.yaml`'s `timeout-minutes: 40` exceeds the orchestrator's 25-minute wait deadline — PASS. Confirmed in the workflow file with an explanatory comment tying the two together.
- Orchestrator cannot report PASS while a subagent left no fragment — PASS. §4 of `orchestrator.md`: "If no subagent returned FAIL but any domain returned INCONCLUSIVE, or a fragment is missing, empty, or has no exact verdict line, write no status file at all," landing on MISSING → INCONCLUSIVE, not PASS. `security-audit.yaml`'s reporting step independently re-checks fragment presence and verdict-line exactness regardless of what the orchestrator wrote (defense in depth), and both paths exit non-zero.
**Outcomes and reporting**
- Only literal `PASS`/`FAIL` honored, three real outcomes — PASS. `case "${STATUS:-}" in PASS|FAIL) ;; *) STATUS=MISSING ;; esac` in `security-audit.yaml`.
- `audit-report.md` written before `audit-status.txt` — PASS. `orchestrator.md` §4 states this explicitly and orders the instructions accordingly.
- Redact-secrets step covers every published sink and fails closed — PASS. Read the step: it processes `$TRANSCRIPT`, `audit-report.md`, and `$AUDIT_FRAGMENTS` (the three per-domain files, sourced from the job-level `AUDIT_FRAGMENTS` env var so the redactor and the reporting-step guard loop can't drift), and on any Node error runs `rm -f` on that exact same file set before exiting 1. `node scripts/security-audit.test.mjs` → 19/19 pass, including "redaction covers every published sink" and "redactor failure removes every published sink."
- Reporting step writes one note per condition, not per combination — PASS. Five independent `if [ -n "${X:-}" ]; then echo "..." >> "$NOTES"; fi` blocks (`DISSENTING`, `MISSING_FRAGMENTS`, `UNREADABLE_VERDICTS`, `INCONCLUSIVE_DOMAINS`, `STATUS_FILE_VERDICT`), each appending independently.
- Both fragment guards run unconditionally, not gated on status — PASS. Both `for f in $AUDIT_FRAGMENTS` loops run unconditionally before the single escalation block; comments in the file explicitly flag this ("Both loops now run UNCONDITIONALLY").
- Reporting step accepts only exact `VERDICT: PASS`, recognizes `VERDICT: FAIL` as dissent (with or without suffix), flags `VERDICT: INCONCLUSIVE`, and treats anything else as unreadable; `STATUS` only literal `PASS`/`FAIL` — PASS, confirmed by direct read of the `case` statements plus `node scripts/security-audit.test.mjs`'s "reporting" test group (10/10 pass, covering exact-match, prefix, whitespace, and combination cases).
- Redaction/reporting regression suite — PASS. `node scripts/security-audit.test.mjs`: 19/19 tests pass (0 failing).
**Environment and AUDIT_PAT**
- `security-audit.yaml` dispatch is a full release gate (dispatch + watch + `needs:`) — PASS. `release.yml`'s `security-audit` job runs `gh workflow run "$workflow" ...`, then `gh run watch "$run_id" ... --exit-status`; `publish-vscode` declares `needs: [build-standalone, build-vscode, security-audit]`.
- `Verify AUDIT_PAT is provisioned` step present, after checkout/install, before the audit step — PASS. Confirmed at that exact position in `security-audit.yaml`, checking `[ -n "$AUDIT_PAT" ]` and failing closed (writes `FAIL` status, exits 1) otherwise.
- `security-audit.yaml` itself active and producing successful runs — PASS. `gh api .../actions/workflows` shows `state: active`; last several scheduled runs all `conclusion: success` (one `in_progress` at check time, which is this run).
### Qualitative findings
No BLOCKER or WARNING findings. Scope swept: `.github/` (all workflows, `.github/audit/`), `.config/tend.yaml`, `.claude/` (`settings.json`, skills, output-styles, commands), `.vscode/` (`tasks.json`, `launch.json`), `scripts/` (spot-checked every script that touches `process.env`/`secrets.`), and `website/public/` (the Tauri updater manifest and static assets).
- `.vscode/tasks.json` has one task (`build-dormouse-vscode`, a plain `pnpm build:vscode` shell command) and carries no `"runOn": "folderOpen"` or any other checkout-triggered automation — matches `docs/specs/security-ci.md`'s statement that no such task exists today. INFO: worth re-checking on every future edit to this file, since adding `runOn` here is exactly the persistence class this domain watches for and it is not caught by any lint — only by this qualitative pass.
- `.claude/settings.json` permission allowlist is narrow and read/build-oriented (`agent-browser`, `npx tsc`, `magick identify`, `pnpm test`/filtered variants, a preview MCP tool) — nothing that reaches a secret or performs a network write.
- `.claude/skills/*` and `.claude/commands/release-notes.md` are prose/process guidance for the tend bot and release workflow; nothing executes with elevated privilege from these files themselves, and none references a secret value.
- `website/public/standalone-latest.json` (the Tauri updater manifest) contains only public update-signature material (Tauri's public-key-verifiable minisign-style signatures, not secrets) and public download URLs — consistent with `docs/specs/security-ci.md` -> "Desktop Releases": the manifest is generated locally by `sign-and-deploy.sh`'s `sign_updates()` from the (locally-held) private key, and committing the *public* signature is expected.
- Scripts touching `process.env`/`secrets.` outside `.github/workflows/` (`scripts/csp-defaults.mjs`, `scripts/dogfood-vscode.mjs`, `scripts/free-dev-port.mjs`, `scripts/pairing-walkthrough/*.mjs`) read only non-secret config/dev-environment variables (ports, CSP policy toggles, dev-harness flags) — no credential material observed.
- No org-level secret is shared with this repo (confirmed via API, not just spec text), so the "Org-level secrets" re-evaluation obligation in `docs/specs/security-ci.md` has nothing pending.
- All redaction/reporting/local-runner/workflow-audit regression suites (`scripts/security-audit.test.mjs`, `scripts/workflow-audit.test.mjs`, `scripts/clamp-issue-body-selftest.mjs`, `scripts/spec-lint.mjs`) ran clean in this session, giving mechanical (not just textual) confirmation for the checks they pin.
UNVERIFIABLE: none. Every `FAIL IF` in scope was independently determined via `gh api` (using `AUDIT_PAT` where admin scope was required), direct file reads, or running the pinned test scripts.
## Application security
VERDICT: FAIL
Domain: `application-security`. Scope: `docs/specs/security-local.md` (17 `FAIL IF`
bullets) and `docs/specs/security-remote.md` (50 bullets), plus the catch-all
remainder. Run at `bb5ff06` on `main`.
**One `FAIL IF` clause is violated** — `docs/specs/security-local.md` -> "Terminal
output", the bounded-and-control-stripped rule, for `CwdState.host`. Every other
clause in both specs PASSES. No BLOCKER-rated qualitative finding.
### FAIL IF results
Lint gates, run at HEAD: `spec-lint` OK (33 specs), `loopback-lint` OK (3
listeners, 1 allowlisted), `e2e-lint` OK (13 rules, 1366 checks),
`e2e-lint-selftest` OK (20 load-bearing checks), `deploy-lint` OK (3 installers,
31 rules, 81 checks), `deploy-lint-selftest` OK (112 load-bearing checks),
`installer-verify-test` OK (62 checks), `ps1-cmdlet-lint` OK (674 calls),
`ps1-cmdlet-lint-selftest` OK (4 checks). Test suites: `remote-lib-common` 241
pass, `relay` 275 pass, `lib` remote subtree 585 pass (31 files), `vscode-ext`
171 pass.
#### `docs/specs/security-local.md` -> Terminal output (3 bullets)
- L37 `isKnownUnsupportedIterm2Osc` still consumes `OSC 52` — **PASS**.
`lib/src/lib/terminal-protocol.ts:708-717` returns true for `52` / `52;` (and
`50` / `50;`); reached from `parseOsc` at `:294`, which returns `[]` so the
sequence never re-enters `visibleData` (`:223-228`). `52` and `50` are also in
`OSC_CONSUMED_IDS` (`:93`), so an unterminated one is buffered, not forwarded.
- L37 every parse site runs `TerminalProtocolParser` before `pty:data` leaves it
— **PASS**, 4 emitters, all covered. VS Code:
`vscode-ext/src/message-router.ts:231-234` routes every raw `onData` into
`getOwnerPtyStream(id).write(data)`; the only `pty:data` post is `:532`, fed
from `createProcessedPtyStream.onChunk` (`:288-291`). Standalone:
`standalone/sidecar/main.js:46` withholds the `data` event, which reaches the
wire only via `lib/src/host/remote/sidecar-entry.ts:256-266` ->
`ownerStream(id).parsed.write(chunk)` -> `:167`; a throw in the tap drops the
chunk rather than emitting it raw (`:41-45`). `lib/src/lib/platform/fake-adapter.ts:456-470`
parses before `dataHandlers`. `vscode-ext/src/processed-pty-streams.ts`
refuses a second parser. The three webview-side `new TerminalProtocolParser`
are the one-shot replay parsers the spec sanctions.
- L38 `TITLE_LIMIT` bounds every retained title — **PASS**.
`lib/src/lib/terminal-protocol.ts:79` (256), applied `:333`, `:387`, `:696`, `:793`.
- L38 `BODY_LIMIT` bounds every retained body — **PASS**. `:80` (4096), applied
`:321`, `:334`, `:388`.
- L38 `sanitizeText` strips controls before storage — **PASS**. `:821-828`,
`[\x00-\x1f\x7f-\x9f]+` -> space, then `truncateText` (`:834-837`) counting
code points, so no split surrogate; empty reduces to `null`.
- L38 `COMMAND_LINE_LIMIT` binds *after* the `\xNN` unescape, 4x before it —
**PASS**. `:614-617`
`sanitizeText(decodeOsc633Value(truncateText(rawCommand, COMMAND_LINE_LIMIT * 4)), COMMAND_LINE_LIMIT)`;
`COMMAND_LINE_LIMIT = 2048` at `:86`.
- L38 `MAX_CWD_LENGTH` / `boundedCwdValue` bound and strip every CWD source —
**PASS for `path` and `uri`**. `lib/src/lib/terminal-state.ts:722` (4096) and
`:738-743`; applied in `cwdFromOsc7` (`:266`, `:275`), `cwdFromOsc9_9` (`:290`),
`cwdFromDecodedPath` (`:679`, serving `osc633` / `osc1337` / `process` / `manual`).
- L38 no retained value stops being bounded **and** control-stripped —
**FAIL**, one value: `CwdState.host`. See BLOCKER-adjacent detail below.
`cwdFromOsc7` (`lib/src/lib/terminal-state.ts:263-286`) bounds and strips
`rawUri` at `:265` and re-bounds the *path* after its percent-decode at `:274`
— but `:275` computes `host = extractFileUriHost(rawUri) || parsed.hostname`,
and `extractFileUriHost` (`:704-708`) regex-slices `[^/]*` out of the URI and
then `safeDecodeURIComponent`s it. The percent-decode re-introduces control
characters *after* the only strip, and nothing re-sanitizes. Reproduced
against the shipped predicates:
`file://ok.example?a%1Bb%5D0;PWNED%07` yields
`host: "ok.example?ab]0;PWNED"`, `isRemote: true`. The URL parser
rejects controls inside the host, but the `?`/`#` tail is outside the host and
the regex swallows it to end-of-string.
Retained and rendered: `cwd.host` is spliced into pane-header labels at
`terminal-state.ts:340` and `:369` and into grouping labels at `:850`, and is
a component of `cwdIdentity` (`:326`). It is also persisted — the whole
`CwdState` is written to the notepad archive on Surface closure and read back
with a type check only (`lib/src/lib/notepad/archive-model.ts:137-140`, no
length bound, no control strip).
Blast radius is bounded: React renders these as text nodes, so there is no
markup execution and no escape re-injection into a PTY, and the value does not
cross a boundary — `dor ls` sends `state.cwd?.path` (`lib/src/components/Wall.tsx:1085`)
and the remote directory sends `pane.cwd?.path`
(`lib/src/remote/burrow/directory.ts:33`), neither `host`. So this is header
and archive-record spoofing/corruption, not code execution. It is nonetheless
a violation of the rule as written, and the code is internally inconsistent:
the sibling `path` on the same line *is* re-bounded after its decode. One-line
fix: wrap `extractFileUriHost`'s return in `boundedCwdValue`.
- L39 `OSC 8` activation cannot reach `openExternal` without the dialog —
**PASS**. `lib/src/lib/terminal-lifecycle.ts:210-217` calls
`event.preventDefault()` then `requestExternalLinkConfirmation`; the only
consumer of that store is `lib/src/components/ExternalLinkModalHost.tsx:24-28`.
The other two `openExternal` callers are app chrome
(`lib/src/components/Wall.tsx:1623`, `lib/src/components/ExternalTextLink.tsx:19`).
- L39 the dialog renders no open action for a **deceptive** verdict — **PASS**,
all three render branches. `lib/src/components/ExternalLinkModal.tsx:109-156`:
`isDeceptive` (`:61`) is tested first, so an openable-and-deceptive target
takes the deceptive arm (`Close` + copy only, `:111-126`); branch 2
(`:128-146`) is the sole `onConfirm` wiring; branch 3 (`:147-156`) is `Close`
alone. Initial focus is the copy button when deceptive (`:81`).
- L39 the host also rejects a deceptive confirmation — **PASS**.
`ExternalLinkModalHost.tsx:25-27` requires
`status === 'openable' && verdict !== 'deceptive'`.
- L39 second pass through `normalizeExternalUri` at every adapter — **PASS**.
`vscode-ext/src/message-router.ts:636-638`, `standalone/src/tauri-adapter.ts:419-421`,
`standalone/src/browser-sidecar-adapter.ts:229-231`.
#### `docs/specs/security-local.md` -> Browser panes (2 bullets)
---
_Truncated to fit: the full body is 100009 characters. The untruncated `audit-report.md` is in this run's `audit-transcript` artifact ([download](https://github.com/diffplug/dormouse/actions/runs/34332049244/artifacts/10096921421))._
Beitragsleitfaden
Für dieses Repository ist kein Beitragsleitfaden indexiert
Rechercherichtung
Start with the linked GitHub Actions run and transcript, then locate the domain section marked `VERDICT: FAIL`; the visible Supply chain and CI/secrets sections are PASS. Review the named workflow, audit, and dependency files or tests for that failing check. Done means the underlying failure is resolved and the security audit passes without introducing new findings.
Vom Indexierungsmodell aus dem Issue-Text verfasst.
Bewertung
- Tech-Stack
- github-actions, javascript, rust, typescript
- Bereich
- ci-cd, devops, security
- Issue-Typ
- Bug
- Schwierigkeit
- 4/5
- Geschätzter Aufwand
- 3-5 Tage
- Aktivitätsstatus
- Aktiv
- Klarheit
- Muss geklärt werden
- Anfängerfreundlichkeit
- 25/100