Skip to content

GA Pre-Launch Penetration Test Checklist

This is the checklist promised by Threat model follow-up item 6 ("Publish a GA-readiness penetration-test checklist document"). It scopes an external or manual penetration test against the BoardReadyOps control plane (apps/web) and release-signing surface ahead of general availability, and separates what tests/integration/security-adversarial.test.ts and other automated suites already exercise from what still needs a human or external tester.

This document does not itself claim a pen test occurred. See Assurance Case claim 4 for current residual risk, which this checklist is meant to close.

Scope

In scope

  • apps/web control plane: bearer-token and session-cookie authentication (apps/web/lib/api-auth.ts, apps/web/lib/user-session.ts), API token scope enforcement, tenant/repository isolation, GitHub OAuth login and webhook handlers, Stripe billing webhook handler.
  • API token lifecycle (packages/db/src/api-token-store.ts): issuance, scope assignment, hashing/storage.
  • Webhook signature verification (packages/cloud-core/src/index.ts: verifyGitHubWebhook, verifyStripeWebhook) as deployed behind the real route handlers, not just as unit-tested functions.
  • Artifact storage path handling (resolveLocalArtifactPath) and evidence ledger integrity (buildEvidenceLedger / verifyEvidenceLedger).
  • Release signing-key trust store (src/release/signing.ts, release verify --trust-store) and its local file distribution.
  • Container/deployment configuration for apps/web/Dockerfile and apps/container/Dockerfile, and the publish workflows' OIDC/Trusted-Publishing configuration.

Out of scope

  • The local CLI's parsing of KiCad/BOM/pinmap project files — covered by Input Validation fuzz/property tests, not a network attack surface.
  • Plugin runtime sandboxing — explicitly documented as a trusted-code model in ADR 0009; not a GA blocker to re-litigate here.
  • GitHub's own infrastructure (Actions runners, GitHub App platform, Marketplace billing internals) — only BoardReadyOps' use of them is in scope.
  • Denial-of-service / load testing — tracked separately, not a pen-test checklist item.
  • Cross-OS/cross-CI-runner reproducible-build divergence — tracked as its own open item in Release Integrity, not a pen-test finding.

Checklist

Each item lists whether it is Automated (already exercised by a test in this repo — re-verifying it manually is low value) or Manual (needs a human/external tester against a real deployment, because it depends on runtime configuration, infrastructure, or logic no test currently asserts).

Authentication / session

# Item Status
A1 Invalid/forged bearer token (bro_live_...) is rejected. Automated — tests/unit/web/api-auth-repository-scope.test.ts and token-hash lookup in packages/db/src/api-token-store.ts.
A2 Session cookie (brops_session) with a tampered payload or invalid HMAC signature is rejected; signature comparison is constant-time. Manual — confirm timingSafeEqual usage in apps/web/lib/user-session.ts actually gates every code path (no early-return short-circuit reintroduced by a future edit) against a live deployment, not just source reading.
A3 SESSION_SECRET is enforced at minimum length and is not a shared default/example value in the deployed environment. Manual — this is an environment/config check, not something a unit test can assert.
A4 Revoking a user's GitHub App installation does not immediately invalidate their existing BoardReadyOps session; confirm the session's remaining lifetime (sessionLifetimeMs, currently 8h) is an acceptable exposure window. Manual — accepted-risk judgment call, not a pass/fail bug in isolation. Flag if a tester finds the window materially different from 8h in production config.
A5 Failed bearer-token attempts are rate-limited (default 20/60s, BOARDREADYOPS_AUTH_RATE_LIMIT_PER_MINUTE); the rate-limit key is not trivially resettable by rotating IPs. Automated (limiter logic) — tests/unit/web/auth-rate-limit.test.ts. Manual — the limiter keys on the first hop of X-Forwarded-For (clientIdentifierFromRequest); confirm the production reverse proxy strips/overwrites client-supplied X-Forwarded-For before it reaches Next.js, otherwise the limit is spoofable per request.
A6 Failed session-cookie authentication attempts are also rate-limited, not just failed bearer-token attempts. Manual/gap — recordFailedAuthAttempt is currently only called from the bearer-token failure path in apps/web/lib/api-auth.ts; confirm whether this is an accepted gap or needs closing before GA.
A7 OAuth state parameter in the GitHub login flow prevents CSRF/session-fixation on callback. Manual — apps/web/lib/oauth-state.ts and the login/callback routes; no automated adversarial test currently targets this flow.

Tenant isolation

# Item Status
T1 A bearer token scoped to repository A cannot read or write resources for repository B, even by supplying B's id directly in the request. Automated (partial) — tests/integration/security-adversarial.test.ts ("rejects cross-tenant cursor tampering", "enforces tenant scope in DB queries"). resolveRepositoryApiContext in apps/web/lib/api-auth.ts rejects a caller-supplied repositoryId that doesn't match the token's bound repository.
T2 Every store under packages/db/src/ that holds repository-scoped data filters by repository_id in its SQL, not just in an in-memory array filter after an unscoped query. Manual — spot-check stores beyond review-store.ts (which the automated test covers): api-token-store.ts, review-comment-store.ts, runner-artifact-store.ts, finding-decision-store.ts, and any added since.
T3 Every apps/web/app/api/** route handler that touches repository- or review-scoped data calls resolveRepositoryApiContext / resolveReviewApiContext rather than trusting a raw repositoryId/reviewId route or query parameter. Manual — this is a per-route audit; no automated test enumerates route handlers to assert this structurally today.
T4 Session-cookie auth (which has no token-bound repositoryId) correctly derives tenant access from session.installationIds via the GitHub installation lookup, and cannot be tricked into acting on a repository outside those installations by URL/parameter manipulation. Manual — exercise resolveReviewApiContext (apps/web/lib/api-auth.ts) against a session with a known, narrow installation set.

API / webhook surface

# Item Status
W1 An API token's scope (runs:write, reviews:read, reviews:write, admin) is enforced on every route that requires it; a reviews:read token cannot perform a write. Manual — tests/unit/web/api-auth-repository-scope.test.ts covers repository scoping, not an exhaustive per-route scope matrix. Build/execute that matrix manually against a running deployment.
W2 Token creation without an explicit scopes argument does not silently grant broader access than the caller intended. Manual — packages/db/src/api-token-store.ts defaults to all three non-admin scopes when scopes are unspecified; audit every token-creation call site to confirm none relies on this default unintentionally.
W3 Invalid Stripe webhook signature is rejected; valid signature with a stale timestamp (replay) is rejected outside the tolerance window. Automated — tests/integration/security-adversarial.test.ts ("rejects invalid Stripe signature", "accepts valid Stripe signature", "rejects Stripe replay outside tolerance"); tests/unit/cloud-core/webhook.test.ts.
W4 GitHub webhook signature (X-Hub-Signature-256, verifyGitHubWebhook) is verified before any event processing, and a missing header is rejected (not just a malformed one). Manual — verifyGitHubWebhook itself has unit coverage, but confirm the route handlers (apps/web/app/api/github/webhook/route.ts, .../marketplace/webhook/route.ts) reject a request with the header absent entirely, not only a request with a bad signature.
W5 Webhook secrets (STRIPE_WEBHOOK_SECRET, GitHub App webhook secret) are distinct per environment (dev/staging/prod) and not reused. Manual — deployment/secret-management check, not testable from the repo.
W6 A notifier webhookEnv value that resolves to an environment variable whose name doesn't look webhook-related is surfaced, not silently used. Automated — covered by notifier tests referenced in Threat model (notifier.webhook.unrecognized-env-name warning); tests/unit/notifiers/notifiers.test.ts.
W7 Stored content rendered back to users (e.g. review discussion comments) is escaped, not executed, including nested payloads. Automated — tests/integration/security-adversarial.test.ts ("renders a stored-XSS comment payload as inert escaped text"). Manual — spot-check any other user-supplied free-text field rendered in apps/web beyond the discussion tab.
W8 accepted_risk finding dispositions require a substantive reason (not a token string) before being accepted. Automated — tests/integration/security-adversarial.test.ts ("requires reason for accepted_risk disposition"), createFindingDecisionRequestSchema in packages/contracts.
W9 Source-code upload requires an explicit source-upload mode; a manifest cannot smuggle source-classified content under a metadata-only upload mode. Automated — tests/integration/security-adversarial.test.ts ("rejects source upload without explicit source mode"), uploadManifestSchema in packages/contracts.

Artifact storage / signing

# Item Status
S1 Artifact keys containing .. traversal segments or absolute paths are rejected when resolved against the artifact storage root. Automated — tests/integration/security-adversarial.test.ts ("rejects path traversal in artifact key"), resolveLocalArtifactPath in packages/cloud-core/src/index.ts.
S2 A single-byte tamper to a persisted evidence ledger is detected on verification. Automated — tests/integration/security-adversarial.test.ts ("detects single-byte tampering in evidence ledger"), buildEvidenceLedger/verifyEvidenceLedger (SHA-256 over canonical JSON in packages/contracts/src/evidence-ledger.ts).
S3 A revoked signing key in the trust store no longer verifies new signatures, without breaking verification of past releases signed by other still-valid keys. Manual — exercise verifyManifestSignatureAgainstTrustStore end-to-end via release verify --trust-store with a trust store containing one revoked and one active key; no automated test in security-adversarial.test.ts currently drives this through the CLI.
S4 The trust-store JSON file's own integrity/distribution is protected at the point it reaches a consumer's machine (file permissions at minimum; a signed trust-store bundle is explicitly not yet designed, per src/release/signing.ts and Assurance Case claim 3). Manual, known gap — confirm this is documented as accepted risk, not silently assumed safe. Do not treat as a "finding" to fix during the pen test itself; it is already tracked.
S5 An incomplete required-check state cannot produce a "ready" (green) release-gating result. Automated — tests/integration/security-adversarial.test.ts ("prevents incomplete check from turning green"), isWdrrReady in packages/cloud-core.

Infra / deployment

# Item Status
I1 The npm publish workflow (.github/workflows/publish-npm.yml) uses OIDC Trusted Publishing and fails closed if a long-lived NPM_TOKEN or basic-auth .npmrc entry is injected. Manual — Release Integrity documents this as passed for the documented flow; confirm no fallback token secret remains configured in the live GitHub environment/repo secrets.
I2 Container images (apps/web/Dockerfile, apps/container/Dockerfile) do not run as root and do not bake in secrets or long-lived credentials. Manual — image build/runtime audit, not something the repo's tests assert.
I3 GitHub Actions workflow permissions are minimum-necessary at the job level, not only declared broadly at the workflow level. Manual — spot-check .github/workflows/*.yml job-level permissions: blocks, especially any workflow that runs on pull_request_target or handles fork PRs, per the "Malicious workflow change" threat in Threat model.
I4 The custom Next.js worker build (scripts/build-control-plane-worker.mjs output) does not expose any endpoint or capability beyond what the documented API routes provide. Manual — review generated .next/worker.mjs behavior against a running deployment.

Pass / fail criteria and exit conditions

  • Automated items are exit conditions of the existing test suite, not of the pen test: a pen tester should confirm they still pass (corepack pnpm vitest run tests/integration/security-adversarial.test.ts) rather than re-deriving them by hand, and should treat a regression in one of them as a release blocker on its own.
  • Manual items are the actual deliverable of a GA pen test. The test passes when every Manual item above has an explicit recorded result (pass, fail, or accepted-risk-with-justification) — "not tested" is not a passing state.
  • Any Manual item that fails is a GA release blocker unless the maintainer explicitly records it as an accepted risk with a reason, consistent with the accepted_risk disposition discipline this codebase already enforces for findings (W8 above).
  • Known, already-tracked gaps (S4; cross-OS reproducibility in Release Integrity; private vulnerability reporting/push-protection settings confirmation in Assurance Case claim 4) are not new findings if rediscovered — cross-reference before filing a duplicate.
  • On completion, record the tester, date, scope covered, and per-item results as release evidence (see Evidence Bundles) rather than only as an external report, so it participates in the same audit trail as other release verification.