[security-audit] FAIL on 2026-09-16
- Lenguaje dominante
- TypeScript
- Estrellas
- 5
- Forks
- 0
- Merge medio
- 14 h 13 min
- PR fusionados (30 d)
- 199
Descripción
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))._
Guía de contribución
No hay ninguna guía de contribución indexada para este repositorio
Línea de trabajo
Comienza con la ejecución y la transcripción vinculadas de GitHub Actions y, después, localiza la sección de dominio marcada con `VERDICT: FAIL`; las secciones visibles Supply chain y CI/secrets tienen el estado PASS. Revisa el workflow nombrado y los archivos o tests de auditoría y dependencias relacionados con esa comprobación fallida. Se considera terminado cuando se resuelva el fallo subyacente y la auditoría de seguridad pase sin introducir nuevos hallazgos.
Escrito por el modelo de indexación a partir del texto del issue.
Evaluación
- Stack tecnológico
- github-actions, javascript, rust, typescript
- Área
- ci-cd, devops, security
- Tipo de issue
- Error
- Dificultad
- 4/5
- Tiempo estimado
- 3-5 días
- Estado de actividad
- Activo
- Claridad
- Necesita aclaración
- Aptitud para principiantes
- 25/100