diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1858a01..aa8641d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,10 +14,10 @@ jobs: name: Go Build & Test runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.25.10' @@ -120,7 +120,7 @@ jobs: run: bash scripts/check-coverage-thresholds.sh - name: Upload Coverage Report - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: go-coverage path: coverage.out @@ -188,7 +188,7 @@ jobs: runs-on: ubuntu-latest needs: go-build-and-test steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Show Docker versions run: | @@ -328,10 +328,10 @@ jobs: name: Frontend Build runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Node.js - uses: actions/setup-node@v4 + uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: '22' @@ -339,6 +339,17 @@ jobs: working-directory: web run: npm ci + - name: npm audit (production deps, high+critical) + # Phase 1 TEST-L2 closure (2026-05-13): + # Production frontend dependencies must not carry high or + # critical CVEs. Dev-only deps (vitest, vite, eslint, etc.) + # are excluded via --omit=dev since they never ship to + # operators. If this gate fires, triage each finding via npm + # overrides, dep upgrade, or a tracked --ignore with an issue + # link. Do not mass-silence findings. + working-directory: web + run: npm audit --omit=dev --audit-level=high + - name: TypeScript Check working-directory: web run: npx tsc --noEmit @@ -374,10 +385,10 @@ jobs: name: Helm Chart Validation runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Install Helm - uses: azure/setup-helm@v4 + uses: azure/setup-helm@1a275c3b69536ee54be43f2070a358922e12c8d4 # v4 with: version: '3.13.0' @@ -527,10 +538,10 @@ jobs: needs: [go-build-and-test] timeout-minutes: 30 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.25.10' cache: true @@ -624,10 +635,10 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 15 steps: - - uses: actions/checkout@v5 + - uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.25.10' cache: true diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index a7e4164..49f247f 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -53,17 +53,17 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Go if: matrix.language == 'go' - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: # Match ci.yml + release.yml + security-deep-scan.yml. go-version: '1.25.10' - name: Initialize CodeQL - uses: github/codeql-action/init@v3 + uses: github/codeql-action/init@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3 with: languages: ${{ matrix.language }} # Use the security-and-quality query suite — security finds plus @@ -72,10 +72,10 @@ jobs: queries: security-and-quality - name: Autobuild - uses: github/codeql-action/autobuild@v3 + uses: github/codeql-action/autobuild@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@v3 + uses: github/codeql-action/analyze@7fd177fa680c9881b53cdab4d346d32574c9f7f4 # v3 with: category: "/language:${{ matrix.language }}" # SARIF upload is implicit (and is what populates the Security tab). diff --git a/.github/workflows/loadtest.yml b/.github/workflows/loadtest.yml index 0247f13..db2919c 100644 --- a/.github/workflows/loadtest.yml +++ b/.github/workflows/loadtest.yml @@ -49,13 +49,13 @@ jobs: steps: - name: Checkout - uses: actions/checkout@v4 + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Docker Buildx # The compose stack builds the certctl image from the repo # root Dockerfile. Buildx gives the build a usable cache and # works with newer compose versions. - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Run loadtest run: make loadtest @@ -70,7 +70,7 @@ jobs: # authoritative machine-readable form; summary.txt is the # human-readable text the README baseline tracks. if: always() - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: k6-summary-${{ github.run_id }} path: deploy/test/loadtest/results/ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f50126e..338ed1e 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -39,10 +39,10 @@ jobs: os: [linux, darwin] arch: [amd64, arm64] steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Set up Go - uses: actions/setup-go@v5 + uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: ${{ env.GO_VERSION }} @@ -123,7 +123,7 @@ jobs: cat "${OUTPUT_NAME}.sha256" - name: Upload build artefacts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 with: name: binary-${{ steps.build.outputs.output_name }} path: | @@ -151,7 +151,7 @@ jobs: hashes: ${{ steps.hashes.outputs.hashes }} steps: - name: Download binary artefacts - uses: actions/download-artifact@v4 + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 with: pattern: binary-* path: artifacts @@ -191,7 +191,7 @@ jobs: checksums.txt - name: Upload artefacts to GitHub Release - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 if: startsWith(github.ref, 'refs/tags/') with: files: | @@ -212,7 +212,7 @@ jobs: actions: read id-token: write contents: write - uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@v2.1.0 + uses: slsa-framework/slsa-github-generator/.github/workflows/generator_generic_slsa3.yml@f7dd8c54c2067bafc12ca7a55595d5ee9b75204a # v2.1.0 with: base64-subjects: "${{ needs.aggregate-checksums.outputs.hashes }}" upload-assets: true @@ -235,10 +235,10 @@ jobs: id-token: write # Cosign keyless OIDC identity token steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Log in to GitHub Container Registry - uses: docker/login-action@v3 + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: registry: ${{ env.REGISTRY }} username: ${{ github.actor }} @@ -249,14 +249,14 @@ jobs: run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> "$GITHUB_OUTPUT" - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - name: Install Cosign uses: sigstore/cosign-installer@cad07c2e89fa2edd6e2d7bab4c1aa38e53f76003 # v4.1.1 - name: Build and push server image id: server-push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: ./Dockerfile @@ -291,7 +291,7 @@ jobs: - name: Build and push agent image id: agent-push - uses: docker/build-push-action@v6 + uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6 with: context: . file: ./Dockerfile.agent @@ -334,7 +334,7 @@ jobs: contents: write steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - name: Extract version from tag id: version @@ -351,7 +351,7 @@ jobs: # README is the source of truth for those, and inlining them in every # release page produces the kind of "every release looks identical" # noise that gives operators no signal about what actually changed. - uses: softprops/action-gh-release@v2 + uses: softprops/action-gh-release@3bb12739c298aeb8a4eeaf626c5b8d85266b0e65 # v2 with: # Pin the release title to the tag name. softprops/action-gh-release@v2 # falls back to the most recent commit subject when `name:` is omitted, diff --git a/.github/workflows/security-deep-scan.yml b/.github/workflows/security-deep-scan.yml index 8458258..24f7c50 100644 --- a/.github/workflows/security-deep-scan.yml +++ b/.github/workflows/security-deep-scan.yml @@ -36,9 +36,9 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 60 steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 - - uses: actions/setup-go@v5 + - uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5 with: go-version: '1.25' @@ -126,7 +126,7 @@ jobs: continue-on-error: true - name: ZAP baseline - uses: zaproxy/action-baseline@v0.10.0 + uses: zaproxy/action-baseline@1e1871e84428617b969d4a1f981a8255630d54b0 # v0.10.0 with: target: 'https://localhost:8443' continue-on-error: true @@ -175,7 +175,7 @@ jobs: # --- Upload everything as artefacts --- - name: Upload deep-scan receipts - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 if: always() with: name: security-deep-scan-${{ github.run_id }} diff --git a/.gitignore b/.gitignore index 4f2bc0d..f5cc34e 100644 --- a/.gitignore +++ b/.gitignore @@ -88,3 +88,8 @@ Thumbs.db # CERTCTL_TEST_CA_BUNDLE=./certs/ca.crt. Material is regenerated on every # `docker compose up` and never belongs in git. /deploy/test/certs/ + +# Phase 1 RED-1 closure (2026-05-13): the f5-mock-icontrol Dockerfile +# rebuilds from source via multi-stage build (deploy/test/f5-mock-icontrol/ +# Dockerfile line 13). The compiled ELF must not be tracked. +deploy/test/f5-mock-icontrol/f5-mock-icontrol diff --git a/deploy/test/f5-mock-icontrol/f5-mock-icontrol b/deploy/test/f5-mock-icontrol/f5-mock-icontrol deleted file mode 100755 index 04f6f89..0000000 Binary files a/deploy/test/f5-mock-icontrol/f5-mock-icontrol and /dev/null differ diff --git a/scripts/ci-guards/no-precompiled-binary.sh b/scripts/ci-guards/no-precompiled-binary.sh new file mode 100755 index 0000000..01c3082 --- /dev/null +++ b/scripts/ci-guards/no-precompiled-binary.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# scripts/ci-guards/no-precompiled-binary.sh +# +# Phase 1 RED-1 closure (2026-05-13): no precompiled binary (ELF / +# Mach-O / PE) should ever be tracked in the repo. The original +# Phase 1 trigger was `deploy/test/f5-mock-icontrol/f5-mock-icontrol` +# — an 8.6 MB ARM64 ELF that lived in git alongside the Go source +# that builds it. The Dockerfile for that fixture already runs +# `go build` from source inside the container, so the tracked +# binary was vestigial. Deleting it cost nothing; tracking it cost +# 8.6 MB per clone forever. +# +# This guard scans every TRACKED file (i.e. what an external clone +# sees, not what's in the operator's working tree) and uses `file(1)` +# to detect compiled executables. Any hit fails the build with a +# clear pointer to the right fix. +# +# Allowlist: +# - PNG / JPG / PDF / SVG / GIF / WebP — image assets are not +# binaries in the supply-chain sense even though `file` reports +# them as "executable" in some encodings. +# - Specific large fixtures that legitimately need to be tracked +# (e.g. canonical certificate test vectors). Add to the +# allowlist with a rationale comment. +# +# Mirror of the B6-no-private-keys-in-tree.sh pattern. + +set -e + +# What `file(1)` outputs we treat as a binary smell. ELF / Mach-O / +# PE / Java class / WebAssembly all qualify. Image / archive / text +# formats do NOT. +BAD_TYPES='ELF|Mach-O|PE32|PE32\+|compiled Java class|WebAssembly' + +VIOLATIONS=$(git ls-files -z \ + | xargs -0 file --brief --separator='|' --print0 2>/dev/null \ + | tr '\0' '\n' \ + | grep -E "$BAD_TYPES" \ + | grep -vE '^$' \ + || true) + +if [ -n "$VIOLATIONS" ]; then + echo "::error::no-precompiled-binary regression: tracked executable file(s) found:" + echo "" + echo "$VIOLATIONS" + echo "" + echo "Precompiled binaries must not be tracked. If this is a test" + echo "fixture, route the build through a Dockerfile / Makefile target" + echo "that rebuilds from source. If it is a legitimate exception," + echo "add the path to the allowlist in scripts/ci-guards/no-precompiled-binary.sh" + echo "with a rationale comment." + exit 1 +fi + +echo "no-precompiled-binary guard OK: no tracked ELF / Mach-O / PE / class / wasm binaries" diff --git a/scripts/ci-guards/no-tag-pinned-actions.sh b/scripts/ci-guards/no-tag-pinned-actions.sh new file mode 100755 index 0000000..dc88091 --- /dev/null +++ b/scripts/ci-guards/no-tag-pinned-actions.sh @@ -0,0 +1,52 @@ +#!/usr/bin/env bash +# scripts/ci-guards/no-tag-pinned-actions.sh +# +# Phase 1 RED-2 closure (2026-05-13): every GitHub Action invocation +# under .github/workflows/ MUST be SHA-pinned (@<40-char-sha>) rather +# than tag-pinned (@v4 / @v0.10.0 / etc.). Tags are mutable; SHAs +# aren't. Mutable tags are the standard supply-chain attack vector +# against GitHub Actions consumers — a compromised tag silently +# pulls compromised code on every CI run. +# +# Pattern allowance: +# - `uses: org/repo@<40-char-sha> # v4` ← the trailing comment +# documenting the human-readable tag is REQUIRED for operator +# audit purposes ("which version is that SHA?"), but the SHA is +# the load-bearing pin. +# +# How to fix a violation: +# 1. Look up the action's tag → SHA mapping. Either via the GitHub +# web UI (visit the action's tags page), or via: +# curl -sS https://api.github.com/repos///git/refs/tags/ | jq .object.sha +# 2. Rewrite the line as `uses: /@ # `. +# 3. Re-run this guard locally to confirm. +# +# Rationale + history: +# - Phase 1 of the certctl architecture diligence remediation +# (cowork/certctl-architecture-diligence-audit.html#fix-RED-2) +# swept the entire .github/workflows/ tree from 37 tag-pinned / +# 4 SHA-pinned to 0 tag-pinned / 41 SHA-pinned in one PR. +# - This guard catches the regression mode: a future PR adds a new +# `uses: foo/bar@v2` line and the build fails until the +# contributor SHA-pins it. + +set -e + +# Match `uses: @vN` or `@v.` etc. Don't match +# `@<40-char-sha>`. The negative-lookahead-free shell-regex shape: +# anything-not-hex-40-chars after the @. +VIOLATIONS=$(grep -rnE "uses:[^#]*@v[0-9]" .github/workflows/ 2>/dev/null || true) + +if [ -n "$VIOLATIONS" ]; then + echo "::error::no-tag-pinned-actions regression: tag-pinned uses: line found." + echo "" + echo "GitHub Actions MUST be SHA-pinned (@<40-char-sha>) rather than" + echo "tag-pinned (@v4). Tags are mutable; SHAs aren't. See the guard" + echo "header for the fix workflow." + echo "" + echo "Violations:" + echo "$VIOLATIONS" + exit 1 +fi + +echo "no-tag-pinned-actions guard OK: every GitHub Action is SHA-pinned"