diff --git a/AGENTS.md b/AGENTS.md index 1fce134cb..bc5c9ecb6 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -15,13 +15,15 @@ Deeper design: [Developer guide](./docs/guides/DEVELOPER_GUIDE.md), [Architectur ```bash mise run install # yarn workspaces + agent Python (uv) mise run build # agent quality + cdk + cli + docs (parallel) -mise run security # secrets, deps, sast, grype, retire, gh-actions, agent +mise run security # secrets, deps, sast, sast:masking, grype, retire, gh-actions, agent mise run hooks:install # prek git hooks (also runs at end of install) mise run hooks:run # pre-commit + pre-push locally ``` Security subtasks: `mise run security:secrets`, `security:sast`, `security:sast:masking`, `security:deps`, `security:retire`, `security:gh-actions`. For `security:sast:masking` allowlist intentional fallbacks with an inline `nosemgrep: -- ` comment on the flagged `return` line (or the line immediately above) — the rule anchors on the `return`, so a token placed higher does not bind. +**Claim the security legs one at a time, never in aggregate.** `mise run security` runs its eight legs in the order listed above and stops at the first failure, so "`mise run security` passes" is only ever a statement about the legs that actually ran. This is not a local-only hazard: the weekly `security.yml` cron — the *only* surface that runs the container image scan — invokes the same aggregate, so a red early leg suppresses the later ones there too, and the issue it auto-files names just the first failure. That is how 14 fixable HIGH/CRITICAL findings in the image-bundled `gh` / `uv` / `npm` binaries sat unseen: the container scan is inside the *last* leg, and a red `security:sast` (3rd) or `security:sast:masking` (4th) stops the chain five legs short of it — see [#897](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/issues/897). Note which leg is red is not stable, so do not memorise one: fixing the earlier one just uncovers the next. PR checks do not compensate: `security-pr.yml` runs only the diff-scoped `security:secrets:range` and `security:sast:masking:range` plus `security:deps` and `security:gh-actions`; whole-repo `security:sast` and `//agent:security` are absent from it, and `build.yml` sets `MISE_DISABLE_TOOLS: "aqua:aquasecurity/trivy,grype,semgrep"` ([#235](https://github.com/aws-samples/sample-autonomous-cloud-coding-agents/issues/235)). So 8/8 green PR checks say nothing about those legs — run them by name before asserting they pass. + Package commands: [cdk/AGENTS.md](./cdk/AGENTS.md), [cli/AGENTS.md](./cli/AGENTS.md), [agent/AGENTS.md](./agent/AGENTS.md), [docs/AGENTS.md](./docs/AGENTS.md). Run `mise tasks --all` with `MISE_EXPERIMENTAL=1` for the full list. Prefer mise tasks over raw `jest`, `tsc`, or `cdk` for full suites; package guides show targeted `npx jest` / `uv run pytest` for single files. diff --git a/agent/Dockerfile b/agent/Dockerfile index cc280daf1..daa4ab2db 100644 --- a/agent/Dockerfile +++ b/agent/Dockerfile @@ -1,5 +1,12 @@ ARG TARGETPLATFORM=linux/arm64 -ARG GH_VERSION=2.93.0 +# ``gh`` is compiled from source below (``go install …@vX``), so its vendored module +# set is whatever this tag pins — there is no separate dep file to patch, and a +# version bump is the only lever. 2.101.0 clears the nine fixable HIGH/CRITICAL +# findings #897 recorded against 2.93.0: sigstore/rekor 1.5.3 (CVE-2026-48702), +# x/crypto 0.57.0, x/mod 0.41.0, x/net 0.58.0, x/text 0.42.0, grpc 1.83.2. Its +# ``go 1.27.0`` directive is satisfied by the pinned golang:1.27.1 builder below. +# When bumping, confirm by re-scanning the built image, not by reading a changelog. +ARG GH_VERSION=2.101.0 # Every external image is pinned by DIGEST, not by tag. # @@ -93,10 +100,65 @@ RUN npm install -g npm@latest && \ node "${CLAUDE_NPM_ROOT}/install.cjs" && \ claude --version +# Patch npm's OWN bundled dependencies (#897). +# +# ``npm install -g npm@latest`` above does not fix these. npm 12.0.2 — the current +# latest — still ships brace-expansion 5.0.7 (CVE-2026-14257, CVE-2026-69152), +# ip-address 10.2.0 (CVE-2026-69192) and tar 7.5.19 (CVE-2026-73566), each with a +# published fix. So ``npm@latest`` is not a security control here; it was measured, +# not assumed. +# +# The approach used just above for the claude-code tree (``npm --prefix +# update …``) CANNOT be reused here. npm's published package.json declares +# ``@npmcli/docs@^1.0.0``, a workspace-internal package that is not on the +# registry, and Arborist resolves the whole declared graph before touching +# anything — so every npm-native operation inside npm's own tree dies with E404 +# (verified for ``update``, ``update --omit=dev`` and ``install --no-save``). +# npm's bundled node_modules is a build artifact, not an installable project. +# +# Hence a direct tarball overlay. ``npm pack`` fetches through the registry's +# integrity check, and each package is pure JS at a patch/minor bump, so the new +# file set supersedes what it replaces. +# +# The pinned version is a FLOOR, not an assignment: ``npm@latest`` floats, so a +# later npm could bundle something newer, and overwriting blindly would silently +# walk it backwards. npm's own bundled ``semver`` does the comparison and the +# overlay is skipped when the shipped version already satisfies the floor. +# +# Both failure modes are hard-verified rather than left to degrade into a no-op: +# a package missing from the tree fails the build (the layout is npm's internal +# detail and may change), and the post-overlay version is asserted. +RUN NPM_TREE="$(npm root -g)/npm" && \ + SEMVER="${NPM_TREE}/node_modules/semver" && \ + mkdir -p /var/tmp/npm-cve && cd /var/tmp/npm-cve && \ + for pv in brace-expansion:5.0.9 ip-address:10.3.1 tar:7.5.21; do \ + name="${pv%:*}"; floor="${pv#*:}"; \ + pkg="${NPM_TREE}/node_modules/${name}/package.json"; \ + [ -f "${pkg}" ] || { echo "FATAL: ${name} absent from npm's bundled tree; layout changed, re-check #897"; exit 1; }; \ + cur="$(node -p "require('${pkg}').version")"; \ + if node -e "process.exit(require('${SEMVER}').gte('${cur}','${floor}')?0:1)"; then \ + echo "npm bundled dep ${name} ${cur} already satisfies floor ${floor}; left alone"; \ + else \ + tarball="$(npm pack "${name}@${floor}" --silent)"; \ + for dir in $(find "${NPM_TREE}/node_modules" -type d -name "${name}"); do \ + tar -xzf "/var/tmp/npm-cve/${tarball}" -C "${dir}" --strip-components=1; \ + done; \ + got="$(node -p "require('${pkg}').version")"; \ + [ "${got}" = "${floor}" ] || { echo "FATAL: ${name} is ${got} after overlay, expected ${floor}"; exit 1; }; \ + echo "npm bundled dep ${name} ${cur} -> ${got}"; \ + fi; \ + done && \ + cd / && rm -rf /var/tmp/npm-cve && \ + npm --version + # Install uv (fast Python package manager) — pinned by digest, not just version: # a published tag can be re-pushed, and this COPY sits above the dependency sync, # so a silent move here invalidates it. See the digest note at the top of the file. -COPY --from=ghcr.io/astral-sh/uv:0.11.14@sha256:1025398289b62de8269e70c45b91ffa37c373f38118d7da036fb8bb8efc85d97 /uv /usr/local/bin/uv +# 0.12.17 vendors quinn-proto 0.11.18, clearing GHSA-4w2j-m93h-cj5j (#897). Read that +# advisory carefully: its "fixed in 0.11.15" is *quinn-proto's* version, not uv's — +# uv 0.11.14 happens to vendor quinn-proto 0.11.14, which makes the two look like one +# number. The floor is therefore a uv release that vendors ≥ 0.11.15, not uv 0.11.15. +COPY --from=ghcr.io/astral-sh/uv:0.12.17@sha256:10787c682e4184e4f290de1171fd4703dc63de99221f10fe1c99002ce7fa9acc /uv /usr/local/bin/uv # Install Python dependencies via uv. Build context is repo root (set in # ``cdk/src/stacks/agent.ts``) so source paths are prefixed with ``agent/``.