From 5664369cdc29ddfeb69d6155e6fd4c6ed9ccf448 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:19:41 -0400 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=94=A7=20chore(security):=20drop=20Sn?= =?UTF-8?q?yk,=20consolidate=20dep/CVE=20scanning=20on=20Grype?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Snyk's GitHub SCM integration scans the full dependency requirement graph across every package.json/package-lock.json instead of the resolved, shipped dependency set, so it over-reports advisories in transitive packages the lockfile never resolves to — noise on top of a redundant paid integration. Grype matches lockfile-resolved versions and scans the built image's package catalog, so no requirement-graph false positives. - 🗑️ Removed Snyk wiring: .snyk policy, security-snyk-weekly.yml, the setup-snyk composite action, and the scripts/snyk-* gate/quota scripts - ✨ Added security-grype.yml: grype-deps (all six npm lockfiles; on PRs path-filtered to deps/Dockerfile/workflow + weekly cron + dispatch) and grype-image (built container, scheduled/manual only). SHA-pinned, harden-runner, persist-credentials:false, distinct SARIF categories, upload gated on public-repo visibility, fail-build on HIGH/CRITICAL - 📝 Reworded the lefthook comment, dropped the README + apps/web Snyk badges, reworked the CONTRIBUTING scanning section, updated docs/ci-flow.html - 📝 CHANGELOG: Unreleased → Changed entry with the requirement-graph vs shipped-deps rationale - No govulncheck job and no Trivy: drydock is TypeScript/Node (not Go), and Grype already covers the SCA + container surface. CodeQL, dependency review, OpenSSF Scorecard, and zizmor remain as the free gates --- .github/actions/setup-snyk/action.yml | 23 -- .github/workflows/security-grype.yml | 147 ++++++++ .github/workflows/security-snyk-weekly.yml | 399 --------------------- .snyk | 37 -- CHANGELOG.md | 1 + CONTRIBUTING.md | 4 +- README.md | 1 - apps/web/app/page.tsx | 7 - docs/ci-flow.html | 12 +- lefthook.yml | 4 +- scripts/snyk-code-gate.sh | 31 -- scripts/snyk-container-gate.sh | 17 - scripts/snyk-deps-gate.sh | 9 - scripts/snyk-iac-gate.sh | 9 - scripts/snyk-quota-config.json | 15 - scripts/snyk-quota-plan.mjs | 135 ------- scripts/snyk-quota-plan.test.mjs | 82 ----- 17 files changed, 158 insertions(+), 775 deletions(-) delete mode 100644 .github/actions/setup-snyk/action.yml create mode 100644 .github/workflows/security-grype.yml delete mode 100644 .github/workflows/security-snyk-weekly.yml delete mode 100644 .snyk delete mode 100755 scripts/snyk-code-gate.sh delete mode 100755 scripts/snyk-container-gate.sh delete mode 100755 scripts/snyk-deps-gate.sh delete mode 100755 scripts/snyk-iac-gate.sh delete mode 100644 scripts/snyk-quota-config.json delete mode 100644 scripts/snyk-quota-plan.mjs delete mode 100644 scripts/snyk-quota-plan.test.mjs diff --git a/.github/actions/setup-snyk/action.yml b/.github/actions/setup-snyk/action.yml deleted file mode 100644 index ce2ba6bfc..000000000 --- a/.github/actions/setup-snyk/action.yml +++ /dev/null @@ -1,23 +0,0 @@ -name: Setup Snyk -description: Install Node.js and the pinned Snyk CLI for workflow jobs - -inputs: - node-version: - description: Node.js version to install - required: true - snyk-version: - description: Snyk CLI version to install - required: true - -runs: - using: composite - steps: - - name: Setup Node.js - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 - with: - node-version: ${{ inputs.node-version }} - - - name: Install Snyk CLI - uses: snyk/actions/setup@9adf32b1121593767fc3c057af55b55db032dc04 # v1.0.0 - with: - snyk-version: ${{ inputs.snyk-version }} diff --git a/.github/workflows/security-grype.yml b/.github/workflows/security-grype.yml new file mode 100644 index 000000000..50c4d717e --- /dev/null +++ b/.github/workflows/security-grype.yml @@ -0,0 +1,147 @@ +name: "🛡️ Security: Grype" +run-name: >- + ${{ + github.event_name == 'schedule' && '🛡️ Security: Grype — Weekly run' || + github.event_name == 'pull_request' && format('🛡️ Security: Grype — PR #{0}', github.event.pull_request.number) || + format('🛡️ Security: Grype — Manual by {0}', github.actor) + }} + +on: + workflow_dispatch: + pull_request: + branches: [main] + # Grype matches lockfile-resolved versions, so only dependency / image + # surface changes can move its findings — scope PR runs to those so + # source- or doc-only PRs stay fast. Source SAST is CodeQL's job + # (ci-verify.yml); new-dependency CVEs are dependency-review's. + paths: + - '**/package.json' + - '**/package-lock.json' + - 'Dockerfile' + - '.github/workflows/security-grype.yml' + schedule: + - cron: '15 7 * * 1' # Weekly on Monday at 07:15 UTC + +permissions: + contents: read + +concurrency: + group: security-grype-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + grype-deps: + name: "📦 Security: Grype Dependency Scan (npm lockfiles)" + runs-on: ubuntu-latest + timeout-minutes: 15 + permissions: + contents: read + security-events: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Run Grype across the repository (npm lockfiles) + # Scanning the repo root catalogs every package-lock.json + # (root, app, ui, e2e, apps/demo, apps/web) in a single pass. + # Grype matches the lockfile-resolved versions — not the manifest + # requirement graph — so it does not emit the phantom-dependency + # findings the SCM manifest-graph scanners do. + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + id: grype-deps + with: + path: . + severity-cutoff: high + fail-build: true + output-format: sarif + + - name: Upload Grype dependency SARIF + # Code scanning uploads need a public repo or GHAS. Drydock is public, + # so this runs; the guard keeps the job green on a fork/private mirror. + if: always() && github.event.repository.visibility == 'public' + uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + with: + sarif_file: ${{ steps.grype-deps.outputs.sarif }} + category: grype-deps + + - name: Summarize + if: always() + run: | + { + echo "### Grype Dependency Scan (npm lockfiles)" + echo "- Scanned every package-lock.json across the repo for HIGH/CRITICAL CVEs" + echo "- Matches lockfile-resolved versions (no requirement-graph false positives)" + } >> "$GITHUB_STEP_SUMMARY" + + grype-image: + name: "🐳 Security: Grype Container Scan" + # The image build is the heavy step; keep it on scheduled/manual runs. + # PRs get fast, accurate dependency coverage from grype-deps without + # paying for a Docker build on every push. + if: github.event_name != 'pull_request' + runs-on: ubuntu-latest + timeout-minutes: 20 + permissions: + contents: read + security-events: write + steps: + - name: Harden Runner + uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 + with: + egress-policy: audit + + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + + - name: Build image for scanning + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 + with: + context: . + push: false + load: true + tags: drydock:grype-scan + build-args: DD_VERSION=grype + # load: true uses the docker exporter, which can't accept the OCI + # manifest list produced by provenance attestations. The scan just + # needs the local image; real attestations live on release-cut.yml. + provenance: false + cache-from: type=gha,scope=drydock + cache-to: type=gha,mode=max,scope=drydock,ignore-error=true + + - name: Run Grype vulnerability scanner + # The built image's package catalog is the dependency set actually + # shipped — so this is the authoritative view of the runtime CVE surface. + uses: anchore/scan-action@e1165082ffb1fe366ebaf02d8526e7c4989ea9d2 # v7.4.0 + id: grype + with: + image: drydock:grype-scan + severity-cutoff: high + fail-build: true + output-format: sarif + + - name: Upload Grype SARIF to code scanning + if: always() && github.event.repository.visibility == 'public' + uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 + with: + sarif_file: ${{ steps.grype.outputs.sarif }} + category: grype-image + + - name: Summarize + if: always() + run: | + { + echo "### Grype Container Scan" + echo "- Scanned the built image (shipped dependency set) for HIGH/CRITICAL CVEs" + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/security-snyk-weekly.yml b/.github/workflows/security-snyk-weekly.yml deleted file mode 100644 index cd62207bc..000000000 --- a/.github/workflows/security-snyk-weekly.yml +++ /dev/null @@ -1,399 +0,0 @@ -name: "🛡️ Security: Snyk Paid Scans" -run-name: >- - ${{ - github.event_name == 'schedule' && '🛡️ Security: Snyk Paid Scans — Weekly run' || - format('🛡️ Security: Snyk Paid Scans — Manual by {0}', github.actor) - }} - -on: - workflow_dispatch: - schedule: - - cron: '15 7 * * 1' # Weekly on Monday at 07:15 UTC - -permissions: - contents: read - -concurrency: - group: snyk-paid-${{ github.workflow }} - cancel-in-progress: true - -env: - SNYK_CLI_VERSION: '1.1305.0' - SNYK_QUOTA_CONFIG_PATH: scripts/snyk-quota-config.json - SNYK_CONTAINER_IMAGE: drydock:snyk - -jobs: - prepare: - name: "🛠️ Security: Prepare Snyk Context" - runs-on: ubuntu-latest - timeout-minutes: 5 - environment: ci-security - outputs: - has_token: ${{ steps.token.outputs.has_token }} - is_default_branch: ${{ steps.token.outputs.is_default_branch }} - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Check Snyk token availability - id: token - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} - CURRENT_REF: ${{ github.ref }} - run: | - if [ -n "${SNYK_TOKEN:-}" ]; then - echo "has_token=true" >> "$GITHUB_OUTPUT" - else - echo "has_token=false" >> "$GITHUB_OUTPUT" - fi - - if [ "$CURRENT_REF" = "refs/heads/$DEFAULT_BRANCH" ]; then - echo "is_default_branch=true" >> "$GITHUB_OUTPUT" - else - echo "is_default_branch=false" >> "$GITHUB_OUTPUT" - fi - - quota-plan: - name: "📊 Security: Snyk Quota Plan" - runs-on: ubuntu-latest - timeout-minutes: 5 - needs: [prepare] - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Validate monthly quota plan - id: quota - run: | - set -euo pipefail - node scripts/snyk-quota-plan.mjs --config "${SNYK_QUOTA_CONFIG_PATH}" > quota.json - cat quota.json - - - name: Attach quota summary - run: | - { - echo "### Snyk Quota Plan" - cat quota.json - } >> "$GITHUB_STEP_SUMMARY" - - open-source: - name: "📦 Security: Snyk Open Source" - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - actions: read - contents: read - security-events: write - environment: ci-security - needs: [prepare, quota-plan] - if: ${{ needs.quota-plan.result == 'success' && needs.prepare.outputs.has_token == 'true' && needs.prepare.outputs.is_default_branch == 'true' }} - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Snyk toolchain - uses: ./.github/actions/setup-snyk - with: - node-version: '24' - snyk-version: v${{ env.SNYK_CLI_VERSION }} - - - name: Run Snyk Open Source scans - id: snyk_open_source - continue-on-error: true - run: | - set -euo pipefail - mkdir -p artifacts/snyk/open-source - ./scripts/snyk-deps-gate.sh \ - --file=package-lock.json \ - --package-manager=npm \ - --sarif-file-output=artifacts/snyk/open-source/root.sarif - ./scripts/snyk-deps-gate.sh \ - --file=app/package-lock.json \ - --package-manager=npm \ - --sarif-file-output=artifacts/snyk/open-source/app.sarif - ./scripts/snyk-deps-gate.sh \ - --file=ui/package-lock.json \ - --package-manager=npm \ - --sarif-file-output=artifacts/snyk/open-source/ui.sarif - ./scripts/snyk-deps-gate.sh \ - --file=e2e/package-lock.json \ - --package-manager=npm \ - --sarif-file-output=artifacts/snyk/open-source/e2e.sarif - - - name: Upload root Open Source SARIF - if: ${{ always() && hashFiles('artifacts/snyk/open-source/root.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/open-source/root.sarif - category: snyk-open-source-root - - - name: Upload app Open Source SARIF - if: ${{ always() && hashFiles('artifacts/snyk/open-source/app.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/open-source/app.sarif - category: snyk-open-source-app - - - name: Upload UI Open Source SARIF - if: ${{ always() && hashFiles('artifacts/snyk/open-source/ui.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/open-source/ui.sarif - category: snyk-open-source-ui - - - name: Upload e2e Open Source SARIF - if: ${{ always() && hashFiles('artifacts/snyk/open-source/e2e.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/open-source/e2e.sarif - category: snyk-open-source-e2e - - - name: Fail if any Snyk scan errored - if: always() - env: - OUTCOME: ${{ steps.snyk_open_source.outcome }} - run: | - if [ "$OUTCOME" = "failure" ]; then - echo "::error::Snyk Open Source scan failed" - exit 1 - fi - - code: - name: "🧠 Security: Snyk Code" - runs-on: ubuntu-latest - timeout-minutes: 20 - permissions: - actions: read - contents: read - security-events: write - environment: ci-security - needs: [prepare, quota-plan] - if: ${{ needs.quota-plan.result == 'success' && needs.prepare.outputs.has_token == 'true' && needs.prepare.outputs.is_default_branch == 'true' }} - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - SNYK_CODE_ENFORCE: "true" - - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Snyk toolchain - uses: ./.github/actions/setup-snyk - with: - node-version: '24' - snyk-version: v${{ env.SNYK_CLI_VERSION }} - - - name: Run Snyk Code scan - id: snyk_code - continue-on-error: true - run: | - set -euo pipefail - mkdir -p artifacts/snyk - ./scripts/snyk-code-gate.sh --sarif-file-output=artifacts/snyk/code.sarif - - - name: Upload Code SARIF - if: ${{ always() && hashFiles('artifacts/snyk/code.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/code.sarif - category: snyk-code - - - name: Fail if any Snyk scan errored - if: always() - env: - OUTCOME: ${{ steps.snyk_code.outcome }} - run: | - if [ "$OUTCOME" = "failure" ]; then - echo "::error::Snyk Code scan failed" - exit 1 - fi - - container: - name: "🐳 Security: Snyk Container" - runs-on: ubuntu-latest - timeout-minutes: 30 # Includes Docker image build before scan - permissions: - actions: read - contents: read - security-events: write - environment: ci-security - needs: [prepare, quota-plan] - if: ${{ needs.quota-plan.result == 'success' && needs.prepare.outputs.has_token == 'true' && needs.prepare.outputs.is_default_branch == 'true' }} - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Snyk toolchain - uses: ./.github/actions/setup-snyk - with: - node-version: '24' - snyk-version: v${{ env.SNYK_CLI_VERSION }} - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - - - name: Build image for container scan (cached) - uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 - with: - context: . - push: false - load: true - tags: ${{ env.SNYK_CONTAINER_IMAGE }} - build-args: DD_VERSION=snyk - # load: true uses the docker exporter, which can't accept the OCI - # manifest list produced by provenance attestations. Snyk just needs - # the local image; real attestations live on release-cut.yml. - provenance: false - cache-from: type=gha,scope=drydock - cache-to: type=gha,mode=max,scope=drydock,ignore-error=true - - - name: Run Snyk Container scan - id: snyk_container - continue-on-error: true - run: | - set -euo pipefail - mkdir -p artifacts/snyk - ./scripts/snyk-container-gate.sh "${SNYK_CONTAINER_IMAGE}" \ - --file=Dockerfile \ - --sarif-file-output=artifacts/snyk/container.sarif - - - name: Upload Container SARIF - if: ${{ always() && hashFiles('artifacts/snyk/container.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/container.sarif - category: snyk-container - - - name: Fail if any Snyk scan errored - if: always() - env: - OUTCOME: ${{ steps.snyk_container.outcome }} - run: | - if [ "$OUTCOME" = "failure" ]; then - echo "::error::Snyk Container scan failed" - exit 1 - fi - - iac: - name: "🏗️ Security: Snyk IaC" - runs-on: ubuntu-latest - timeout-minutes: 15 - permissions: - actions: read - contents: read - security-events: write - environment: ci-security - needs: [prepare, quota-plan] - if: ${{ needs.quota-plan.result == 'success' && needs.prepare.outputs.has_token == 'true' && needs.prepare.outputs.is_default_branch == 'true' }} - env: - SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }} - - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Checkout - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - with: - persist-credentials: false - - - name: Setup Snyk toolchain - uses: ./.github/actions/setup-snyk - with: - node-version: '24' - snyk-version: v${{ env.SNYK_CLI_VERSION }} - - - name: Run Snyk IaC scan - id: snyk_iac - continue-on-error: true - run: | - set -euo pipefail - mkdir -p artifacts/snyk - ./scripts/snyk-iac-gate.sh . --sarif-file-output=artifacts/snyk/iac.sarif - - - name: Upload IaC SARIF - if: ${{ always() && hashFiles('artifacts/snyk/iac.sarif') != '' }} - uses: github/codeql-action/upload-sarif@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4.36.0 - with: - sarif_file: artifacts/snyk/iac.sarif - category: snyk-iac - - - name: Fail if any Snyk scan errored - if: always() - env: - OUTCOME: ${{ steps.snyk_iac.outcome }} - run: | - if [ "$OUTCOME" = "failure" ]; then - echo "::error::Snyk IaC scan failed" - exit 1 - fi - - skipped: - name: "⏭️ Security: Snyk Scans Skipped" - runs-on: ubuntu-latest - timeout-minutes: 5 - needs: [prepare, quota-plan] - if: ${{ always() && (needs.quota-plan.result != 'success' || needs.prepare.outputs.has_token != 'true' || needs.prepare.outputs.is_default_branch != 'true') }} - steps: - - name: Harden Runner - uses: step-security/harden-runner@9af89fc71515a100421586dfdb3dc9c984fbf411 # v2.19.4 - with: - egress-policy: audit - - - name: Explain skip reason(s) - env: - HAS_TOKEN: ${{ needs.prepare.outputs.has_token }} - IS_DEFAULT_BRANCH: ${{ needs.prepare.outputs.is_default_branch }} - QUOTA_PLAN_RESULT: ${{ needs.quota-plan.result }} - run: | - { - echo "### Snyk scans skipped" - if [ "$HAS_TOKEN" != "true" ]; then - echo "- Reason: \`SNYK_TOKEN\` secret is not configured." - fi - if [ "$IS_DEFAULT_BRANCH" != "true" ]; then - echo "- Reason: this workflow only runs paid scans on the default branch to protect quotas." - fi - if [ "$QUOTA_PLAN_RESULT" != "success" ]; then - echo "- Reason: quota plan validation failed (\`quota-plan\` job result: \`$QUOTA_PLAN_RESULT\`)." - fi - } >> "$GITHUB_STEP_SUMMARY" diff --git a/.snyk b/.snyk deleted file mode 100644 index e169c231f..000000000 --- a/.snyk +++ /dev/null @@ -1,37 +0,0 @@ -# Snyk policy file — https://docs.snyk.io/manage-risk/policies/the-.snyk-file -# -# SCOPE — this file is ONLY enforced by `snyk test` (Open Source) and -# `snyk iac test`. It is NOT enforced by `snyk code test` (SAST). -# Per Snyk docs: -# "To ignore code vulnerabilities in Snyk Code scans, use the ignore -# button within the Snyk UI. The .snyk file cannot be used for -# ignoring issues in Code scans." -# -# Snyk Code suppressions live server-side in the Snyk platform (web UI -# or Policies REST API). They show up in SARIF as `suppressions` with -# `kind: external`, not from this file. The audit trail for Code -# suppressions is documented in: -# -# .planning/snyk-scans/-code-triage.md -# -# Every OSS/IaC entry here MUST include a load-bearing reason and an -# expires date so the ignore is re-evaluated over time instead of -# silently living forever. -# -# Format: -# : -# - '': -# reason: >- -# Concrete explanation — what the scanner saw, why it's wrong, -# and any guardrails that make the code safe in practice. -# expires: YYYY-MM-DDTHH:MM:SS.fffZ -# created: YYYY-MM-DDTHH:MM:SS.fffZ -# -# Adding entries: prefer `snyk ignore --id= --reason='...' -# --expiry=YYYY-MM-DD` so the rule id and policy version stay in sync. - -version: v1.25.1 - -ignore: {} - -patch: {} diff --git a/CHANGELOG.md b/CHANGELOG.md index 16bebb919..e9a0c8cb6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ scheme restriction) live in `UPGRADE-NOTES.md` and are auto-appended to every ### Changed +- **Consolidated dependency/CVE scanning on Grype; dropped Snyk.** Snyk's GitHub SCM integration scans the full dependency *requirement graph* across every `package.json`/`package-lock.json` in the repo rather than the resolved, shipped dependency set, so it over-reports advisories in transitive packages the lockfile never actually resolves to — noise on top of a redundant paid integration. Grype replaces it on both axes: it scans the built container image (the image's package catalog is the dependency set actually shipped) and the six npm lockfiles (root, `app`, `ui`, `e2e`, `apps/demo`, `apps/web`), matching the lockfile-resolved versions instead of the manifest graph, so it does not emit the requirement-graph false positives. The free gates already in CI cover the rest — CodeQL (SAST), `dependency-review` (new-dependency CVEs on PRs), OpenSSF Scorecard, and zizmor — so nothing else was needed (Trivy intentionally not added; drydock is TypeScript/Node, so the Go call-graph scanner govulncheck used on sibling repos does not apply here). The new `security-grype.yml` runs the dependency scan on pull requests (path-filtered to dependency/Dockerfile/workflow changes) plus a weekly cron and manual dispatch, builds and scans the container image on scheduled/manual runs, and uploads distinct-category SARIF to the GitHub Security tab. Removed the `.snyk` policy file, the `security-snyk-weekly.yml` workflow, the `setup-snyk` composite action, and the `scripts/snyk-*` gate/quota scripts. - **Refreshed the drydock whale logo across the app, website, demo, and docs.** A new master render replaces the brand mark everywhere — the in-app logo and favicons, the website/demo favicons, PWA icons, and OpenGraph cards, and the README/docs logos (including the dark-mode variant). All brand assets are now regenerated from a single master (`drydock.png`) via `scripts/regenerate-brand-assets.sh`. Filenames are unchanged, so the Home Assistant `entity_picture` URL contract is preserved. ### Security diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f9d5f195..ba591d76c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -220,8 +220,8 @@ The `pre-commit` hook runs a scoped `vitest --changed` on staged workspaces for Stryker runs monthly (`.github/workflows/quality-mutation-monthly.yml`), advisory only. Use it as a quality signal, not a score target. -### Paid security scans +### Dependency & container scanning -Snyk (Open Source, Code, Container, IaC) runs weekly via `.github/workflows/security-snyk-weekly.yml` to preserve monthly quotas. +Grype runs via `.github/workflows/security-grype.yml`: the dependency scan (every `package-lock.json`) on pull requests touching dependency/Dockerfile surfaces, plus a weekly cron and manual dispatch; the container-image scan runs on the scheduled/manual runs. Both fail on HIGH/CRITICAL and upload SARIF to the Security tab. SAST is CodeQL and new-dependency CVEs are flagged by `dependency-review` (both in `ci-verify.yml`); OpenSSF Scorecard runs in `security-scorecard.yml`. diff --git a/README.md b/README.md index 278fc66aa..accba44bc 100644 --- a/README.md +++ b/README.md @@ -45,7 +45,6 @@ Codecov Mutation testing Maintainability - Monitored by Snyk


diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx index 72f91831f..180ce1611 100644 --- a/apps/web/app/page.tsx +++ b/apps/web/app/page.tsx @@ -662,13 +662,6 @@ export default function Home() { alt="Codecov" /> - - Snyk - Drydock CI Pipeline Fuzz TestingSun 06:00PRsBlocking OpenSSF ScorecardMon 06:00branch protectionAdvisory Mutation (Stryker)Tue 06:15manual dispatchAdvisory - Snyk Open SourceMon 07:15manual (main)Blocking (quota-gated) - Snyk Code SASTMon 07:15manual (main)Blocking (quota-gated) - Snyk ContainerMon 07:15manual (main)Blocking (quota-gated) - Snyk IaCMon 07:15manual (main)Blocking (quota-gated) + Grype Dependency (npm lockfiles)Mon 07:15PRs (deps/Dockerfile)Blocking + Grype ContainerMon 07:15manual dispatchBlocking @@ -325,13 +323,13 @@

Gate Layering

  • Merge + enforced load test (committed baseline) + auto-tag
  • Release Version assert → CI verify → build → sign → CHANGELOG gate → publish
  • Manual Cut CI verify → bump → CHANGELOG gate → tag
  • -
  • Weekly Paid security scans + advisory mutation review
  • +
  • Weekly Grype dependency + container scan, advisory mutation review
  • Design Decisions

      -
    • Free / Paid Free tools gate PRs, paid (Snyk) weekly + quota-gated
    • +
    • Scanning Grype (deps on PRs + image weekly), CodeQL, Scorecard — all free, no paid scanners
    • QA Image Built once in CI, shared as artifact
    • Release CI Dynamic workflow lookup, polls for prior success
    • Version Tag base version asserted against all package.json files
    • @@ -373,7 +371,7 @@

      Workflow Files

    • CI Verify ci-verify.yml (includes 🛡️ CodeQL and 🎯 Fuzz Testing jobs)
    • Release Cut release-cut.yml (manual dispatch; builds, signs, attests, and tags as the final step)
    • Scorecard security-scorecard.yml
    • -
    • Snyk security-snyk-weekly.yml (weekly + manual)
    • +
    • Grype security-grype.yml (deps on PRs + weekly/manual; container weekly/manual)
    • Stryker quality-mutation-monthly.yml (monthly + manual)
    diff --git a/lefthook.yml b/lefthook.yml index 3e56d13c6..90670313d 100644 --- a/lefthook.yml +++ b/lefthook.yml @@ -21,7 +21,9 @@ # tests run exactly once per push instead of once embedded in build-and-test # plus again if we ever add a standalone coverage gate. # -# Snyk scans are CI-only (release workflow) to preserve the 200/month quota. +# Dependency / container CVE scanning (Grype) is CI-only — see +# security-grype.yml (PR + weekly). govulncheck-style call-graph scanning +# does not apply here; drydock is TypeScript/Node, not Go. # # Biome runs directly (not via qlty) because qlty's biome integration # does not reliably apply fixes. Qlty handles all other linters. diff --git a/scripts/snyk-code-gate.sh b/scripts/snyk-code-gate.sh deleted file mode 100755 index 3495c404c..000000000 --- a/scripts/snyk-code-gate.sh +++ /dev/null @@ -1,31 +0,0 @@ -#!/usr/bin/env bash -# Run snyk code test. -# Default mode is informational to avoid noisy false positives during local use. -# Set SNYK_CODE_ENFORCE=true to fail on findings in CI. -set -uo pipefail - -export CI=1 -export TERM=dumb -export NO_COLOR=1 -SNYK_CODE_ENFORCE="${SNYK_CODE_ENFORCE:-false}" - -echo "Running Snyk Code SAST scan..." -set +e -snyk code test --severity-threshold=high "$@" 2>&1 | perl -pe 's/\e\[[0-9;?]*[ -\/]*[@-~]//g' -status=$? -set -e - -if [ "$status" -eq 0 ]; then - echo "Snyk Code: no high-severity findings." -elif [ "$status" -eq 1 ]; then - if [ "$SNYK_CODE_ENFORCE" = "true" ]; then - echo "Snyk Code: enforcement enabled, failing on findings." - exit 1 - fi - echo "Snyk Code: informational mode, findings reported but not enforced." -else - echo "Snyk Code: scan failed unexpectedly (exit code $status)." - exit "$status" -fi - -echo "Snyk Code: scan complete" diff --git a/scripts/snyk-container-gate.sh b/scripts/snyk-container-gate.sh deleted file mode 100755 index b9a8e764e..000000000 --- a/scripts/snyk-container-gate.sh +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -if [ $# -lt 1 ]; then - echo "Usage: $0 [additional snyk args...]" - exit 1 -fi - -export CI=1 -export TERM=dumb -export NO_COLOR=1 - -image="$1" -shift - -snyk container test "$image" --severity-threshold=high "$@" 2>&1 | - perl -pe 's/\e\[[0-9;?]*[ -\/]*[@-~]//g' diff --git a/scripts/snyk-deps-gate.sh b/scripts/snyk-deps-gate.sh deleted file mode 100755 index b7eaf8e72..000000000 --- a/scripts/snyk-deps-gate.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export CI=1 -export TERM=dumb -export NO_COLOR=1 - -snyk test --severity-threshold=high "$@" 2>&1 | - perl -pe 's/\e\[[0-9;?]*[ -\/]*[@-~]//g' diff --git a/scripts/snyk-iac-gate.sh b/scripts/snyk-iac-gate.sh deleted file mode 100755 index 0814e70a9..000000000 --- a/scripts/snyk-iac-gate.sh +++ /dev/null @@ -1,9 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -export CI=1 -export TERM=dumb -export NO_COLOR=1 - -snyk iac test --severity-threshold=high "$@" 2>&1 | - perl -pe 's/\e\[[0-9;?]*[ -\/]*[@-~]//g' diff --git a/scripts/snyk-quota-config.json b/scripts/snyk-quota-config.json deleted file mode 100644 index 7325acb72..000000000 --- a/scripts/snyk-quota-config.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "runsPerMonth": 4, - "testsPerRun": { - "openSource": 4, - "code": 1, - "container": 1, - "iac": 1 - }, - "quotas": { - "openSource": 200, - "code": 100, - "container": 100, - "iac": 300 - } -} diff --git a/scripts/snyk-quota-plan.mjs b/scripts/snyk-quota-plan.mjs deleted file mode 100644 index c2e56fa1b..000000000 --- a/scripts/snyk-quota-plan.mjs +++ /dev/null @@ -1,135 +0,0 @@ -#!/usr/bin/env node - -import fs from 'node:fs'; -import { parseArgs } from './lib/parse-args.mjs'; - -const PRODUCT_KEYS = ['openSource', 'code', 'container', 'iac']; -export const DEFAULT_CONFIG_PATH = new URL('./snyk-quota-config.json', import.meta.url); - -function toPositiveInt(value, name) { - const numeric = Number(value); - if (!Number.isInteger(numeric) || numeric < 0) { - throw new Error(`${name} must be a non-negative integer`); - } - return numeric; -} - -function normalizeQuotas(quotas) { - return { - openSource: toPositiveInt(quotas?.openSource, 'quotas.openSource'), - code: toPositiveInt(quotas?.code, 'quotas.code'), - container: toPositiveInt(quotas?.container, 'quotas.container'), - iac: toPositiveInt(quotas?.iac, 'quotas.iac'), - }; -} - -function normalizeTestsPerRun(testsPerRun) { - return { - openSource: toPositiveInt(testsPerRun?.openSource, 'testsPerRun.openSource'), - code: toPositiveInt(testsPerRun?.code, 'testsPerRun.code'), - container: toPositiveInt(testsPerRun?.container, 'testsPerRun.container'), - iac: toPositiveInt(testsPerRun?.iac, 'testsPerRun.iac'), - }; -} - -function normalizeQuotaConfig(config) { - return { - runsPerMonth: toPositiveInt(config?.runsPerMonth, 'runsPerMonth'), - testsPerRun: normalizeTestsPerRun(config?.testsPerRun), - quotas: normalizeQuotas(config?.quotas), - }; -} - -export function loadQuotaConfig(configPath = DEFAULT_CONFIG_PATH) { - let raw; - try { - raw = fs.readFileSync(configPath, 'utf8'); - } catch (error) { - throw new Error( - `Unable to read quota config at ${String(configPath)}: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - - let parsed; - try { - parsed = JSON.parse(raw); - } catch (error) { - throw new Error( - `Quota config is not valid JSON at ${String(configPath)}: ${ - error instanceof Error ? error.message : String(error) - }`, - ); - } - - return normalizeQuotaConfig(parsed); -} - -export function evaluateQuotaPlan({ - runsPerMonth, - openSourceTestsPerRun, - codeTestsPerRun, - containerTestsPerRun, - iacTestsPerRun, - quotas, -}) { - const normalizedQuotas = normalizeQuotas(quotas); - const normalizedRunsPerMonth = toPositiveInt(runsPerMonth, 'runsPerMonth'); - const monthly = { - openSource: - normalizedRunsPerMonth * toPositiveInt(openSourceTestsPerRun, 'openSourceTestsPerRun'), - code: normalizedRunsPerMonth * toPositiveInt(codeTestsPerRun, 'codeTestsPerRun'), - container: normalizedRunsPerMonth * toPositiveInt(containerTestsPerRun, 'containerTestsPerRun'), - iac: normalizedRunsPerMonth * toPositiveInt(iacTestsPerRun, 'iacTestsPerRun'), - }; - - const violations = []; - for (const product of PRODUCT_KEYS) { - const monthlyTests = monthly[product]; - const quota = normalizedQuotas[product]; - if (monthlyTests > quota) { - violations.push(`${product} exceeds monthly quota: ${monthlyTests}/${quota}`); - } - } - - return { - ok: violations.length === 0, - monthly, - violations, - }; -} - -function main() { - const args = parseArgs(process.argv.slice(2)); - const config = loadQuotaConfig(args.config); - const plan = evaluateQuotaPlan({ - runsPerMonth: args.runsPerMonth ?? config.runsPerMonth, - openSourceTestsPerRun: args.openSourceTestsPerRun ?? config.testsPerRun.openSource, - codeTestsPerRun: args.codeTestsPerRun ?? config.testsPerRun.code, - containerTestsPerRun: args.containerTestsPerRun ?? config.testsPerRun.container, - iacTestsPerRun: args.iacTestsPerRun ?? config.testsPerRun.iac, - quotas: config.quotas, - }); - - const payload = { - ok: plan.ok, - monthly: plan.monthly, - quotas: config.quotas, - violations: plan.violations, - }; - - console.log(JSON.stringify(payload, null, 2)); - if (!plan.ok) { - process.exit(1); - } -} - -if (import.meta.url === `file://${process.argv[1]}`) { - try { - main(); - } catch (error) { - console.error(error instanceof Error ? error.message : String(error)); - process.exit(1); - } -} diff --git a/scripts/snyk-quota-plan.test.mjs b/scripts/snyk-quota-plan.test.mjs deleted file mode 100644 index 3d0c8cfe8..000000000 --- a/scripts/snyk-quota-plan.test.mjs +++ /dev/null @@ -1,82 +0,0 @@ -import assert from 'node:assert/strict'; -import fs from 'node:fs'; -import os from 'node:os'; -import path from 'node:path'; -import test from 'node:test'; -import { evaluateQuotaPlan, loadQuotaConfig } from './snyk-quota-plan.mjs'; - -test('default config plan stays within configured Snyk quotas', () => { - const config = loadQuotaConfig(); - const result = evaluateQuotaPlan({ - runsPerMonth: config.runsPerMonth, - openSourceTestsPerRun: config.testsPerRun.openSource, - codeTestsPerRun: config.testsPerRun.code, - containerTestsPerRun: config.testsPerRun.container, - iacTestsPerRun: config.testsPerRun.iac, - quotas: config.quotas, - }); - - assert.equal(result.ok, true); - assert.equal(result.monthly.openSource, 16); - assert.equal(result.monthly.code, 4); - assert.equal(result.monthly.container, 4); - assert.equal(result.monthly.iac, 4); - assert.equal(config.quotas.code, 100); -}); - -test('fails plan when code scans exceed monthly quota', () => { - const config = loadQuotaConfig(); - const result = evaluateQuotaPlan({ - runsPerMonth: 40, - openSourceTestsPerRun: 1, - codeTestsPerRun: 3, - containerTestsPerRun: 1, - iacTestsPerRun: 1, - quotas: config.quotas, - }); - - assert.equal(result.ok, false); - assert.match(result.violations.join(' '), /code/i); -}); - -test('loads quota and cadence values from a custom config file', () => { - const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'snyk-quota-plan-')); - const configPath = path.join(tmpDir, 'config.json'); - - fs.writeFileSync( - configPath, - JSON.stringify({ - runsPerMonth: 2, - testsPerRun: { - openSource: 5, - code: 1, - container: 2, - iac: 3, - }, - quotas: { - openSource: 10, - code: 2, - container: 4, - iac: 6, - }, - }), - ); - - const config = loadQuotaConfig(configPath); - const result = evaluateQuotaPlan({ - runsPerMonth: config.runsPerMonth, - openSourceTestsPerRun: config.testsPerRun.openSource, - codeTestsPerRun: config.testsPerRun.code, - containerTestsPerRun: config.testsPerRun.container, - iacTestsPerRun: config.testsPerRun.iac, - quotas: config.quotas, - }); - - assert.equal(result.ok, true); - assert.deepEqual(result.monthly, { - openSource: 10, - code: 2, - container: 4, - iac: 6, - }); -}); From d25756116a3be1a805d32b5247eb339113895555 Mon Sep 17 00:00:00 2001 From: scttbnsn <80784472+scttbnsn@users.noreply.github.com> Date: Tue, 16 Jun 2026 15:32:23 -0400 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=93=9D=20docs(security):=20drop=20the?= =?UTF-8?q?=20Snyk=20cards=20from=20the=20audit-findings=20dashboard?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The six closed/won't-fix cards tracked findings in security-snyk-weekly.yml and the scripts/snyk-* gate scripts, all now removed. Dropped the cards, deleted the now-empty hardening + decisions sections (and their dead CSS), and corrected the summary chips (26->21 closed, removed the 0 won't-fix chip). --- docs/audit-findings.html | 83 +--------------------------------------- 1 file changed, 1 insertion(+), 82 deletions(-) diff --git a/docs/audit-findings.html b/docs/audit-findings.html index 780eeccab..5e7fcca95 100644 --- a/docs/audit-findings.html +++ b/docs/audit-findings.html @@ -51,8 +51,6 @@ .concern-section[data-concern="gates"] .concern-header { color: var(--accent-orange); } .concern-section[data-concern="efficiency"] .concern-header { color: var(--accent-yellow); } .concern-section[data-concern="observability"] .concern-header { color: var(--accent-green); } - .concern-section[data-concern="hardening"] .concern-header { color: var(--accent-purple); } - .concern-section[data-concern="decisions"] .concern-header { color: var(--text-dim); } .card { background: var(--surface); border: 1px solid var(--border); border-radius: 8px; padding: 0.75rem 1rem; margin-bottom: 0.5rem; transition: border-color 0.15s; } .card:hover { border-color: #3a5080; } @@ -104,9 +102,8 @@

    Drydock CI Pipeline Audit

    March 2026 — lefthook + GitHub Actions — Round 1 + Round 2

    - 26 closed (round 1) + 21 closed (round 1) 4 new open (round 2) - 1 won't fix
    @@ -200,18 +197,6 @@

    Drydock CI Pipeline Audit

    Closed
    - -
    -
    -
    -
    Snyk container scan rebuilt image from scratch
    -
    Weekly scan built without cache. ~5-10 min redundant.
    -
    Fix: Buildx + GHA cache.
    - -
    -
    Closed
    -
    -
    @@ -308,17 +293,6 @@

    Drydock CI Pipeline Audit

    -
    -
    -
    -
    Snyk CLI version pinned inline in 4 places
    -
    Fix: Single workflow-level env var.
    - -
    -
    Closed
    -
    -
    -
    @@ -434,28 +408,6 @@

    Drydock CI Pipeline Audit

    -
    -
    -
    -
    Snyk quota gate didn't block scan jobs
    -
    Fix: if: needs.quota-plan.result == 'success'.
    - -
    -
    Closed
    -
    -
    - -
    -
    -
    -
    Snyk Code had continue-on-error
    -
    Fix: Removed. Weekly run is blocking.
    - -
    -
    Closed
    -
    -
    -
    @@ -501,39 +453,6 @@

    Drydock CI Pipeline Audit

    -
    -
    - Hardening & Config - -
    - -
    -
    -
    -
    Snyk quota limits hardcoded in script
    -
    Fix: Single config file snyk-quota-config.json.
    - -
    -
    Closed
    -
    -
    -
    - -
    -
    Decisions
    - -
    -
    -
    -
    Snyk as release-cut prerequisite
    -
    Weekly scan sufficient. Hard-gating would block emergency hotfixes.
    - -
    -
    Won't fix
    -
    -
    -
    -