diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2adf1bb..a73f375 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,5 +1,11 @@ { "name": "Node.js", "image": "mcr.microsoft.com/devcontainers/javascript-node:5.2.1-24-trixie@sha256:7a81958053c4e3b5b20fa122f7a422d32dd0eebe87a6726f7b14483585ea60fd", + "features": { + "ghcr.io/devcontainers/features/github-cli:1": {}, + "ghcr.io/devcontainers/features/docker-in-docker:4": { + "moby": false + } + }, "postCreateCommand": "bash ./scripts/setup-cloud-environment.sh" } diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8339057..3741824 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,6 +1,7 @@ # High-risk paths. Last matching pattern wins. # Root-anchored so nested copies (e.g. skills/**/scripts/) are not owned here. /.github/ @jaredwray +/.vscode/ @jaredwray /.cursor/ @jaredwray /.devcontainer/ @jaredwray /scripts/ @jaredwray diff --git a/.github/workflows/check-workflows.yaml b/.github/workflows/check-workflows.yaml index f2d5150..c811618 100644 --- a/.github/workflows/check-workflows.yaml +++ b/.github/workflows/check-workflows.yaml @@ -1,4 +1,4 @@ -name: Check Workflows +name: check-workflows on: push: @@ -13,7 +13,10 @@ jobs: permissions: contents: read actions: read - security-events: write # SARIF upload to code scanning + # SARIF upload. GitHub grants read-only on fork PRs regardless; permission + # values cannot be expressions, so the write grant stays and the zizmor + # step below skips Advanced Security on forks. + security-events: write steps: - name: Checkout uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -26,3 +29,7 @@ jobs: firewall-version: "1.15.2" - name: Run zizmor uses: zizmorcore/zizmor-action@cc914d7f3750a2d13d75c7f184a1060aa0e9d482 # v0.6.4 + with: + # Fork PRs cannot upload SARIF (read-only token). Lint with annotations instead. + advanced-security: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository }} + annotations: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name != github.repository }} diff --git a/.github/workflows/codeql.yaml b/.github/workflows/codeql.yaml index 03b22c6..f72ac08 100644 --- a/.github/workflows/codeql.yaml +++ b/.github/workflows/codeql.yaml @@ -23,7 +23,7 @@ permissions: jobs: analyze: - name: Analyze + name: analyze runs-on: ubuntu-latest permissions: actions: read diff --git a/.github/workflows/deploy-site.yaml b/.github/workflows/deploy-site.yaml index 708934c..d51b41d 100644 --- a/.github/workflows/deploy-site.yaml +++ b/.github/workflows/deploy-site.yaml @@ -10,7 +10,7 @@ permissions: jobs: setup-build-deploy: - name: Deploy Website + name: deploy-website runs-on: ubuntu-latest steps: diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 009cdf3..23f9745 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -10,7 +10,7 @@ permissions: jobs: aikido-gate: - name: Aikido release gate + name: aikido-gate runs-on: ubuntu-latest permissions: contents: read diff --git a/DEFENSE_IN_DEPTH.md b/DEFENSE_IN_DEPTH.md index 1b3abf2..d8c6402 100644 --- a/DEFENSE_IN_DEPTH.md +++ b/DEFENSE_IN_DEPTH.md @@ -11,15 +11,16 @@ Profile: npm library · public ## 2. CODEOWNERS and cloud bootstrap -- [x] `.github/CODEOWNERS` covers `/.github/`, `/.cursor/`, `/.devcontainer/`, `/scripts/` with owners the maintainer names — PR #184 +- [ ] `.github/CODEOWNERS` covers `/.github/`, `/.vscode/`, `/.cursor/`, `/.devcontainer/`, `/scripts/` with owners the maintainer names (PR #212 pending) - [x] Codespaces and Cursor Cloud Agents bootstrap Aikido Safe Chain via scripts/setup-cloud-environment.sh (--ci shims, frozen lockfile) — PR #185 +- [x] Dev Container `image` pinned by digest (`name:@sha256:`; not a floating tag) — PR #208 ## 3. Dependencies (pnpm) - [x] `packageManager: pnpm@11.3+` pinned in `package.json` — verified `pnpm@12.4.1+sha512.2e81e399d73fe8390dab25e06aa788ab7a5908248d2f5a370f82b481147a6a7a367bf8048f9a6fdb6460f21a66f0542dedb8b94ca2c8723596741920b1656d4c` - [x] 7-day cooldown: `minimumReleaseAge: 10080`, `minimumReleaseAgeStrict: true`, `minimumReleaseAgeIgnoreMissingTime: false`; no first-party `minimumReleaseAgeExclude` — PR #186 - [x] `trustPolicy: no-downgrade`; no first-party `trustPolicyExclude` — PR #187 -- [x] Lifecycle scripts blocked: `strictDepBuilds: true`, `dangerouslyAllowAllBuilds: false`, `allowBuilds: {}` baseline — verified (third-party `allowBuilds` exceptions: esbuild, sharp, unrs-resolver, workerd) +- [x] Lifecycle scripts blocked: `strictDepBuilds: true`, `dangerouslyAllowAllBuilds: false`, `allowBuilds: {}` baseline — verified (third-party `allowBuilds` exceptions: esbuild, sharp, workerd) - [x] `blockExoticSubdeps: true` — verified - [x] Lockfile committed; CI installs with `pnpm install --frozen-lockfile` — verified - [x] No `.github/dependabot.yml`; other dependency-update tools (if any) open PRs only — never auto-merge — verified @@ -31,6 +32,7 @@ Profile: npm library · public - [x] Every action pinned to a full commit SHA (`npx actions-up`) — PR #188 - [x] Every job installs Socket Firewall (`SocketDev/action` SHA-pinned, `firewall-version` pinned); `pnpm install` / `npm install` run as `sfw pnpm install` / `sfw npm install` — PR #189 - [x] `.github/workflows/check-workflows.yaml` lints workflows with zizmor on every PR — PR #190 +- [ ] Workflow `name:` and job `name:` contain no spaces (kebab-case) so they can be set as required status checks (PR #212 pending) - [x] `persist-credentials: false` on checkouts that don't push — PR #191 - [x] No `pull_request_target` on workflows that run untrusted PR code — verified - [x] Artifact-publishing workflows disable `actions/setup-node` default caching (`package-manager-cache: false`) to prevent cache poisoning — PR #192 @@ -38,11 +40,11 @@ Profile: npm library · public ## 5. npm publishing — npm libraries only -- [x] OIDC trusted publishing configured **stage-only** on npmjs.com for the publish workflow — it can stage, never publish live — verified (maintainer) +- [x] OIDC trusted publishing configured **stage-only** on npmjs.com for the publish workflow — it can stage, never publish live (manual) — verified (maintainer) - [x] `.github/workflows/release.yaml` packs then stages with `pnpm stage publish ./packed/*.tgz --no-git-checks` — PR #193 -- [x] Maintainer promotes staged versions with 2FA — verified (maintainer) -- [x] Drydock connected — staged releases reviewed before promotion — verified (maintainer) -- [x] No direct publish rights: package requires 2FA and disallows tokens — verified (maintainer) +- [x] Maintainer promotes staged versions with 2FA (manual) — verified (maintainer) +- [x] Drydock connected — staged releases reviewed before promotion (manual) — verified (maintainer) +- [x] No direct publish rights: package requires 2FA and disallows tokens (manual) — verified (maintainer) - [x] `package.json` `repository.url` accurate so provenance maps to this repo — verified ## 6. Security tooling @@ -53,6 +55,6 @@ Profile: npm library · public ## 7. Repository lockdown -- [x] `lockdown-repo.sh` applied; `--check` with `--required-checks "test,zizmor"` and `--allowed-actions "codecov/*,cloudflare/*"` passes (PRs required on the default branch, merges blocked unless required status checks pass, tag ruleset, immutable releases, fork-PR approval, read-only workflow tokens, Actions allowlist, secret scanning, Dependabot disabled, private vulnerability reporting as applicable) — PR #195 -- [x] Phishing-resistant 2FA (passkeys / hardware keys) on the GitHub and npm accounts — verified (maintainer) -- [x] Recovery codes stored offline in a password manager — verified (maintainer) +- [x] Phishing-resistant 2FA (passkeys / hardware keys) on the GitHub and npm accounts (manual) — verified (maintainer) +- [x] Recovery codes stored offline in a password manager (manual) — verified (maintainer) +- [x] `lockdown-repo.sh` applied by a repo admin (never committed to this repo) — PR #195. Latest `--check` (`--required-checks "test,zizmor"`, `--allowed-actions "codecov/*,cloudflare/*"`) still fails the branch ruleset: it has no owner `pull_request` bypass. The tag ruleset has no repository-admin bypass. Remaining settings were unreadable here (non-admin token, HTTP 403). Admin re-apply is still required. diff --git a/SECURITY.md b/SECURITY.md index 7bcfd5f..6f35f77 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -27,5 +27,6 @@ This repository follows the [defense-in-depth](https://github.com/jaredwray/agen - Workflow runs from outside collaborators always require maintainer approval, and only allowlisted GitHub Actions can run. - CI runs with read-only permissions (only jobs whose purpose is mutating the repo get `contents: write`); generated output is an artifact, never committed back; every action is pinned to a full commit SHA; Socket Firewall (`sfw`) wraps `pnpm install` / `npm install`; workflows are security-linted with zizmor on every PR. - Codespaces and Cursor Cloud Agents install through Aikido Safe Chain; package-manager shims must not be bypassed. +- The Codespaces Dev Container image is pinned by digest (`name:@sha256:`), not a floating tag. - Dependencies install through pnpm with a 7-day cooldown on new versions, lifecycle scripts blocked by default, and `trustPolicy: no-downgrade`. Socket reviews every dependency change; Aikido scans every build. -- npm releases are staged, never published directly: CI publishes via stage-only OIDC trusted publishing, Drydock reviews the exact staged artifact, and a maintainer promotes it with 2FA. There are no npm tokens. +- npm releases are staged, never published directly: CI publishes via stage-only OIDC trusted publishing, Drydock reviews the exact staged artifact, and a maintainer promotes it with 2FA. There are no npm publish tokens. diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 8b04372..03d0f46 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -9,5 +9,4 @@ trustPolicy: no-downgrade allowBuilds: esbuild: true sharp: true - unrs-resolver: true workerd: true diff --git a/scripts/setup-cloud-environment.sh b/scripts/setup-cloud-environment.sh old mode 100644 new mode 100755 index 0c052e6..23b1ee4 --- a/scripts/setup-cloud-environment.sh +++ b/scripts/setup-cloud-environment.sh @@ -21,8 +21,13 @@ if [[ ! -f pnpm-lock.yaml ]]; then exit 1 fi -if [[ -f package.json ]] && grep -q '"packageManager"' package.json && command -v corepack >/dev/null; then - corepack enable +if ! command -v pnpm >/dev/null \ + && [[ -f package.json ]] \ + && grep -q '"packageManager"' package.json \ + && command -v corepack >/dev/null; then + mkdir -p "$SAFE_CHAIN_BIN" + corepack enable --install-directory "$SAFE_CHAIN_BIN" pnpm + export PATH="${SAFE_CHAIN_BIN}:${PATH}" fi if ! command -v pnpm >/dev/null; then @@ -36,7 +41,12 @@ trap 'rm -f "$installer"' EXIT curl -fsSL "$SAFE_CHAIN_INSTALLER_URL" -o "$installer" echo "${SAFE_CHAIN_INSTALLER_SHA256} ${installer}" | sha256sum -c - -sh "$installer" --ci +# NVM auto-selects from the current directory when sourced. Run outside the +# repository so .nvmrc cannot break the installer's optional legacy scan. +( + cd / + sh "$installer" --ci +) export PATH="${SAFE_CHAIN_SHIMS}:${SAFE_CHAIN_BIN}:${PATH}"