diff --git a/.github/workflows/security-gates.yml b/.github/workflows/security-gates.yml new file mode 100644 index 000000000..eaebb10ba --- /dev/null +++ b/.github/workflows/security-gates.yml @@ -0,0 +1,165 @@ +# Security gates — MANDATORY, BLOCKING. +# +# Every job here is a required status check on `main` (see +# scripts/ci/enforce-quality-gates.sh). None of these jobs may use +# `continue-on-error` or `allow_failure` semantics: a finding either gets +# fixed, or gets an explicit reviewed suppression in-repo +# (.trivyignore / .semgrepignore / audit.toml). Silent bypass is not a state. +# +# DOCUMENTED EXCEPTION (ratchet-in): the `sast` (Semgrep) job is REPORT-ONLY for +# now — `semgrep scan` runs WITHOUT `--error`, so findings are surfaced but do +# not block. This is the single sanctioned non-blocking gate while the p/default +# baseline is triaged (owner: QA/TAF). Exit condition: add `--error` to the +# semgrep step to make it hard-fail once the baseline is clean. Every OTHER job +# here blocks. +# +# Rationale: both company reference pipelines (gitlab.constr.dev 931666, +# 929528) carry Trivy scans with allow_failure that have been red for days. +# The Constructor/Virtuozzo CI/CD Master Plan §2 mandates hard-fail gates. +# +# All third-party actions and the SAST container image are pinned to immutable +# commit SHAs / digests (supply-chain hygiene — this gate practises what it preaches). + +name: Security Gates + +on: + pull_request: + branches: [main] + workflow_dispatch: + schedule: + # Nightly deep sweep (full history / full repo), per Master Plan §2: + # PR runs scan only the diff; the cron run scans everything. + - cron: "0 1 * * *" + +permissions: + contents: read + +concurrency: + group: security-gates-${{ github.ref }} + cancel-in-progress: true + +jobs: + secrets-scan: + name: secrets-scan + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + # Full history: TruffleHog diffs base.sha..head.sha on PRs, and a + # shallow clone (e.g. 50) drops the base commit on any PR with more + # commits than the depth — the secrets scan then errors or scans + # incompletely. Correctness of a secrets gate beats the CI minutes. + fetch-depth: 0 + persist-credentials: false + - name: TruffleHog (verified secrets are a hard failure) + uses: trufflesecurity/trufflehog@1aa1871f9ae24a8c8a3a48a9345514acf42beb39 # v3.82.13 + with: + base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || '' }} + head: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || '' }} + extra_args: --results=verified,unknown + + sast: + name: sast + runs-on: ubuntu-latest + timeout-minutes: 20 + container: + image: semgrep/semgrep:1.131.0@sha256:6bd07d7b166b097e1384f41b94a62d8c8a26a4fff8713992c296e053310da01f + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Semgrep scan (report-only — ratchet to blocking once baseline triaged) + # report-only during ratchet-in: p/default surfaces pre-existing findings (JWT ValidateLifetime, + # Dockerfile USER, …) tracked in TAF for owner triage. Re-add --error to block once the baseline is clean. + run: semgrep scan --config p/default --exclude-rule generic.secrets.security.detected-generic-secret + + deps-audit: + name: deps-audit + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Install cargo-audit + uses: taiki-e/install-action@375e0c7f08a66b8c2ba7e7eef31a6f91043a81b0 # v2.44.38 + with: + tool: cargo-audit@0.22.2 # >=0.22 parses CVSS 4.0 advisories (RUSTSEC-2026-0124) + - name: Rust dependency audit (RUSTSEC advisories block) + working-directory: src/backend + # Waivers tracked in constructorfabric/insight#1339 — remove each as it is fixed: + # RUSTSEC-2023-0071 (rsa "Marvin", no upstream fix), RUSTSEC-2024-0436 (paste), + # RUSTSEC-2026-0173 (proc-macro-error2) — last two unmaintained, all transitive. + run: cargo audit --deny warnings --ignore RUSTSEC-2023-0071 --ignore RUSTSEC-2024-0436 --ignore RUSTSEC-2026-0173 + - name: Set up Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: "3.12" + - name: Python dependency audit (known CVEs block) + run: | + pip install --quiet 'pip-audit==2.10.1' + rc=0 + # Transitive advisories we cannot fix from here — same suppression + # policy as the Rust audit above (fix, or tracked in-repo waiver). + # Every connector depends on airbyte-cdk (>=7.23.1,<8.0.0), whose + # latest 7.x (7.23.2) HARD-PINS nltk==3.9.1 and caps cryptography + # <47.0.0, so the upstream fixes (nltk 3.9.4, cryptography 48.0.1) are + # unreachable until airbyte-cdk bumps them. Waivers tracked in + # constructorfabric/insight#1339 — remove each as the CDK fixes it. + IGNORES=( + # nltk 3.9.1 (airbyte-cdk pin: nltk==3.9.1) + --ignore-vuln PYSEC-2026-96 + --ignore-vuln PYSEC-2026-97 + --ignore-vuln PYSEC-2026-98 + --ignore-vuln PYSEC-2026-99 + --ignore-vuln GHSA-rf74-v2fm-23pw + --ignore-vuln CVE-2026-33230 + --ignore-vuln CVE-2026-33231 + # cryptography 46.0.7 (airbyte-cdk cap: cryptography<47.0.0) + --ignore-vuln GHSA-537c-gmf6-5ccf + ) + # Audit every Python project manifest in the ingestion tree. + n=0 + while IFS= read -r f; do + n=$((n + 1)) + echo "::group::pip-audit $f" + case "$f" in + *requirements*.txt) pip-audit "${IGNORES[@]}" -r "$f" || rc=1 ;; + *pyproject.toml) (cd "$(dirname "$f")" && pip-audit "${IGNORES[@]}" .) || rc=1 ;; + esac + echo "::endgroup::" + done < <(find src/ingestion -name 'pyproject.toml' -o -name 'requirements*.txt' | grep -v node_modules) + # A gate that audits nothing is a silent bypass — fail if the discovery + # found no manifests (tree moved / glob broke) rather than passing green. + if [ "$n" -eq 0 ]; then + echo "::error::no Python manifests found under src/ingestion — nothing audited; refusing to pass" + exit 1 + fi + exit $rc + + trivy-config: + name: trivy-config + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 + with: + persist-credentials: false + - name: Trivy IaC & Dockerfile misconfiguration scan (CRITICAL/HIGH block) + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: config + scan-ref: . + severity: CRITICAL,HIGH + exit-code: "1" + trivyignores: .trivyignore + - name: Trivy filesystem vulnerability scan (CRITICAL/HIGH block) + uses: aquasecurity/trivy-action@ed142fd0673e97e23eac54620cfb913e5ce36c25 # v0.36.0 + with: + scan-type: fs + scan-ref: . + severity: CRITICAL,HIGH + exit-code: "1" + ignore-unfixed: true + trivyignores: .trivyignore diff --git a/.trivyignore b/.trivyignore new file mode 100644 index 000000000..00dafec82 --- /dev/null +++ b/.trivyignore @@ -0,0 +1,11 @@ +# Documented trivy-config waivers — tracked in constructorfabric/insight#1340. +# The gate stays blocking for every other misconfiguration class; remove each +# line as the underlying issue is fixed. +# container runs as root — build/test/tooling images +AVD-DS-0002 +# apt-get without --no-install-recommends — toolbox build image +AVD-DS-0029 +# frontend Deployment missing securityContext — prod hardening backlog +AVD-KSV-0118 +# frontend Deployment root FS not read-only — prod hardening backlog +AVD-KSV-0014