Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 10 additions & 58 deletions .github/audit/application-security.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,42 +4,12 @@

- `docs/specs/security-local.md`
- `docs/specs/security-remote.md`
- `docs/specs/security-hosted.md`

**Output file:** `audit-application.md`

This is a code-and-specs audit of the product's own boundaries — the remote
control stack, and the local application. You need no PAT — do not use one.
The two pgstencil provenance checks below do read the GitHub API, but only a
public repository, which the workflow's default `GITHUB_TOKEN` and the
operator's own `gh` login both reach; if that API is unreachable, report those
two checks as `UNVERIFIABLE`.

For Hosted accounts, read `docs/specs/hosted.md`, `hosted/server/`,
`hosted/src/`, `hosted/scripts/`, `hosted/wrangler.jsonc`, and
`.github/workflows/hosted-preview.yml` and
`.github/workflows/hosted-production.yml` — `docs/specs/security-hosted.md`'s
Deployment boundary quantifies over the preview and production paths, which
live in those scripts and workflows rather than in the Worker. Verify the
vendored packages by their provenance rather than by reading them: hash each
archive in `vendor/` against `vendor/build.json`; read each archive's own claim
with `tar -xOf vendor/<archive>.tgz package/dist/provenance.json` and check
that it names `build.json`'s commit and does not record `dirty`; then check
that commit against pgstencil `main` and its audit:

```sh
gh api repos/diffplug/pgstencil/compare/<commit>...main --jq .status
gh api repos/diffplug/pgstencil/commits/<commit>/check-runs \
--jq '.check_runs[] | select(.name=="security-audit") | .conclusion'
```

The first must be `ahead` or `identical`, the second `success`. The packed code
itself is audited in `diffplug/pgstencil` by that repository's own
`security-audit` workflow against its `SECURITY.md`; do not audit the tarballs'
contents here — audit how `hosted/` configures the adapter. Distinguish tested
code from pending production configuration; do not treat local provider
simulations as live OAuth acceptance, and treat a checked-in placeholder as no
evidence about an external control.
control stack, and the local application. You need no GitHub API access and no
PAT — do not use one.

Read, at minimum: `docs/specs/remote-security-model.md` **and its paired
`docs/specs/remote-security-model.rationale.md`**, `docs/specs/relay.md`,
Expand Down Expand Up @@ -170,23 +140,6 @@ Be adversarial, and go past the `FAIL IF` list. Ask specifically:
reconstruct and relay only the registered pane-level shapes, never relay a
nested document's location, and target only that origin plus the validated
app origin—never a wildcard or foreign origin.
- **Is the Hosted origin the only one that can drive Hosted?** Trace a request
from `hosted/server/worker.ts` through `workerApp`'s origin gate and
`secureHeaders`: a foreign `Host`, a preview hostname, a misconfigured
deployment's error path, and the SPA fallback must each answer without
credentialed CORS, without a cacheable shell, and without inline script.
Check that authentication cookies stay `__Host-`, Secure, HttpOnly, `Path=/`
and Domain-less, and that no session token reaches browser JSON or storage.
- **Can a Hosted login become terminal access, or an account become someone
else's?** `authPolicy` must keep explicit linking and independent logins; a
callback whose initiating login was revoked must fail; an unused or unknown
provider credential must enable nothing. No Hosted endpoint may mint a Burrow
ACL grant or stand in for the encrypted pairing and presence proof.
- **Does anything from the test or preview build reach production?** The
production Worker must not export the captured-email inbox, the deterministic
clock, or the testing injection module; preview must not copy production
routes, bindings, or credentials, must not call real mail or OAuth, and its
cleanup must check out the base branch rather than the closed PR's.
- Does the shipped code still match what the specs and this section claim? Spec
drift is a finding; say which side is wrong. The newest sections are the ones
most likely to have drifted: `remote-security-model.md`'s Presence proofs,
Expand All @@ -199,11 +152,11 @@ Be adversarial, and go past the `FAIL IF` list. Ask specifically:
look for what a *textual* lint cannot see.

You are also the **catch-all** domain, and this is defined by subtraction, not
by a list: you own everything in the repository that `supply-chain.md` and
`ci-and-secrets.md` do not explicitly claim. Run `ls -A` and work out the
remainder rather than trusting any enumeration — an enumeration goes stale the
moment someone adds a directory, which is exactly how `.vscode/` and
`.impeccable/` ended up owned by nobody.
by a list: you own everything in the repository that `supply-chain.md`,
`ci-and-secrets.md`, and `hosted.md` do not explicitly claim. Run `ls -A` and
work out the remainder rather than trusting any enumeration — an enumeration
goes stale the moment someone adds a directory, which is exactly how `.vscode/`
and `.impeccable/` ended up owned by nobody.

Subtraction is **recursive, not top-level**. Where another domain claims a
subdirectory rather than a whole tree, the rest of that tree is yours — so
Expand All @@ -215,10 +168,9 @@ as a subtraction rather than as two named subdirectories, which is the shape
to prefer when you find the next one.

Today the remainder is `lib/`, `relay/`, `remote-lib-common/`, `standalone/`,
`vscode-ext/`, `dor/`, `dor-lib-common/`, `hosted/`, `vendor/`, `canopy/`,
`deploy/`, `docs/`, `.impeccable/`, and the root files — but treat that as a
description of the current tree, not as your scope. Your scope is the
remainder.
`vscode-ext/`, `dor/`, `dor-lib-common/`, `canopy/`, `deploy/`, `docs/`,
`.impeccable/`, and the root files — but treat that as a description of the
current tree, not as your scope. Your scope is the remainder.

Remote control is where the depth goes; the rest is a sweep for anything that
would be a security hole in a terminal that runs local shells — command
Expand Down
72 changes: 72 additions & 0 deletions .github/audit/hosted.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Domain: hosted

**Scope — these specs, and no others:**

- `docs/specs/security-hosted.md`

**Output file:** `audit-hosted.md`

This is a code-and-specs audit of the Hosted account application. You need no
PAT — do not use one. The two pgstencil provenance checks below do read the
GitHub API, but only a public repository, which the workflow's default
`GITHUB_TOKEN` and the operator's own `gh` login both reach; if that API is
unreachable, report those two checks as `UNVERIFIABLE`.

Read `docs/specs/hosted.md`, `hosted/server/`, `hosted/src/`, `hosted/scripts/`,
`hosted/wrangler.jsonc`, and `.github/workflows/hosted-preview.yml` and
`.github/workflows/hosted-production.yml` — `docs/specs/security-hosted.md`'s
Deployment boundary quantifies over the preview and production paths, which
live in those scripts and workflows rather than in the Worker.

Verify the vendored packages by their provenance rather than by reading them:
hash each archive in `vendor/` against `vendor/build.json`; read each archive's
own claim with `tar -xOf vendor/<archive>.tgz package/dist/provenance.json` and
check that it names `build.json`'s commit and does not record `dirty`; then
check that commit against pgstencil `main` and its audit:

```sh
gh api repos/diffplug/pgstencil/compare/<commit>...main --jq .status
gh api repos/diffplug/pgstencil/commits/<commit>/check-runs \
--jq '.check_runs[] | select(.name=="security-audit") | .conclusion'
```

The first must be `ahead` or `identical`, the second `success`. The packed code
itself is audited in `diffplug/pgstencil` by that repository's own
`security-audit` workflow against its `SECURITY.md`; do not audit the tarballs'
contents here — audit how `hosted/` configures the adapter. Distinguish tested
code from pending production configuration; do not treat local provider
simulations as live OAuth acceptance, and treat a checked-in placeholder as no
evidence about an external control.

## Qualitative pass

You own `hosted/` and `vendor/`. You **read** `.github/workflows/hosted-preview.yml`
and `.github/workflows/hosted-production.yml` for the Deployment boundary above,
but you do not own them: `ci-and-secrets` owns those workflows' credentials,
environments, reviewers, and token placement
(`docs/specs/security-ci.md` -> "Hosted Deployments"). Report what the
deployment *path* does and leave that half to it, so the two domains do not
report the same finding twice.

Be adversarial, and go past the `FAIL IF` list. Ask specifically:

- **Is the Hosted origin the only one that can drive Hosted?** Trace a request
from `hosted/server/worker.ts` through `workerApp`'s origin gate and
`secureHeaders`: a foreign `Host`, a preview hostname, a misconfigured
deployment's error path, and the SPA fallback must each answer without
credentialed CORS, without a cacheable shell, and without inline script.
Check that authentication cookies stay `__Host-`, Secure, HttpOnly, `Path=/`
and Domain-less, and that no session token reaches browser JSON or storage.
- **Can a Hosted login become terminal access, or an account become someone
else's?** `authPolicy` must keep explicit linking and independent logins; a
callback whose initiating login was revoked must fail; an unused or unknown
provider credential must enable nothing. No Hosted endpoint may mint a Burrow
ACL grant or stand in for the encrypted pairing and presence proof.
- **Does anything from the test or preview build reach production?** The
production Worker must not export the captured-email inbox, the deterministic
clock, or the testing injection module; preview must not copy production
routes, bindings, or credentials, must not call real mail or OAuth, and its
cleanup must check out the base branch rather than the closed PR's.

Does the shipped code still match what the spec and this section claim? Spec
drift is a finding; say which side is wrong.
27 changes: 15 additions & 12 deletions .github/audit/orchestrator.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,32 @@ The security specs (`docs/specs/security*.md`) are what you audit against:
their `FAIL IF` lines are concrete mechanical checks, and `docs/specs/security-audit.md`
says that list is not exhaustive, so each domain gets a qualitative pass too.

**Audit nothing yourself.** Fan the work out to three subagents with disjoint
**Audit nothing yourself.** Fan the work out to four subagents with disjoint
scopes, then merge what they return. The domains are genuinely different
subject matters with different evidence — dependency provenance is lockfiles,
CI is `gh api` output, application security is reading the pairing code
adversarially — and one context holding all three degrades the third.
adversarially, Hosted accounts are a Worker's origin gate and its deployment
path — and one context holding them all degrades the ones that read code.

## 1. Spawn all three
## 1. Spawn all four

Spawn them with the Task tool **in a single message** so they run
concurrently, using these three `subagent_type` values:
concurrently, using these four `subagent_type` values:

- `supply-chain`
- `ci-and-secrets`
- `application-security`
- `hosted`

Each is already defined with the prompt it needs — pointing at
`.github/audit/_preamble.md` plus its own domain file — and with the model it
should run on. `application-security` is deliberately on a stronger model than
the other two; do not override it, and do not paste prompt text into the Task
call. A one-line instruction such as "begin your audit" is enough, because the
agent definition carries the rest.
should run on. `application-security` and `hosted` are deliberately on a
stronger model than the other two; do not override them, and do not paste
prompt text into the Task call. A one-line instruction such as "begin your
audit" is enough, because the agent definition carries the rest.

Do not read the domain files yourself. They are long, you are not auditing,
and holding all three in your context is the thing this split exists to avoid.
and holding all four in your context is the thing this split exists to avoid.

## 2. Wait without ending your turn

Expand Down Expand Up @@ -74,13 +76,13 @@ finished() { [ -s "$1" ] && [ "$(sed -e '/^[[:space:]]*$/d' "$1" | tail -n1)" =
# 540 leaves a minute of margin under the cap.
CALL_END=$(( $(date +%s) + 540 ))
ANSWER="ALL FINISHED"
until finished audit-supply-chain.md && finished audit-ci-secrets.md && finished audit-application.md; do
until finished audit-supply-chain.md && finished audit-ci-secrets.md && finished audit-application.md && finished audit-hosted.md; do
NOW=$(date +%s)
[ "$NOW" -ge "$DEADLINE" ] && { ANSWER="DEADLINE"; break; }
[ "$NOW" -ge "$CALL_END" ] && { ANSWER="STILL WAITING"; break; }
sleep 10
done
for f in audit-supply-chain.md audit-ci-secrets.md audit-application.md; do
for f in audit-supply-chain.md audit-ci-secrets.md audit-application.md audit-hosted.md; do
if finished "$f"; then echo "$f: finished"
elif [ -s "$f" ]; then echo "$f: still writing"
else echo "$f: not started"; fi
Expand Down Expand Up @@ -151,6 +153,7 @@ emit() {
emit "Supply chain" audit-supply-chain.md
emit "CI and secrets" audit-ci-secrets.md
emit "Application security" audit-application.md
emit "Hosted accounts" audit-hosted.md
} > audit-report.md
```

Expand All @@ -175,7 +178,7 @@ unaudited ones in the one paragraph a reader starts from.
## 4. The verdict

Write `PASS` or `FAIL` — no other text — to `audit-status.txt` according to
the precedence below. PASS requires all three domains to pass.
the precedence below. PASS requires all four domains to pass.

FAIL if any subagent returned FAIL. That is a finding, and it stays a finding
whether or not the other domains reported.
Expand Down
Loading
Loading