Static Analysis, SAST, and Dependency Scanning as Hard Gates
Static Analysis, SAST, and Dependency Scanning as Hard Gates
Security scanning that runs but does not block is decoration. The scan finds 47 vulnerabilities. The developer glances at the output, sees it passed, and merges. Three weeks later, a critical CVE in a transitive dependency makes it to production.
Hard gates block the merge. No exceptions for “we’ll fix it later.” The pipeline fails, the PR cannot be merged, and the vulnerability does not reach production.
The Failure
The team added Trivy to their pipeline as an informational step. The scan ran, produced a JSON report, and uploaded it as an artifact. No one read the artifacts. Over six months, 23 high-severity CVEs accumulated in production containers. When the security audit happened, the team spent two weeks remediating vulnerabilities that could have been caught at merge time.
The fix: make Trivy a required status check. If Trivy finds a HIGH or CRITICAL vulnerability, the pipeline fails and the PR cannot be merged.
The Mechanism
Scanning Layers
| Layer | Tool | What It Scans | When |
|---|---|---|---|
| Source code | CodeQL | Logic bugs, SQL injection, XSS | PR, push to main |
| Dependencies | Trivy, OWASP DC | Known CVEs in libraries | PR, scheduled |
| Container image | Trivy | OS packages, app dependencies | After build |
| IaC | Trivy, Checkov | Kubernetes manifests, Dockerfiles | PR |
| Secrets | Gitleaks | Hardcoded credentials, API keys | PR, pre-commit |
Gate vs Advisory
A gate blocks the pipeline. A PR cannot merge until the gate passes. An advisory produces a report but does not block. Used for informational findings or new rules being evaluated.
Start with advisories. Measure false positive rates. When the false positive rate is acceptable, promote to a gate.
The Implementation
Trivy Container Scanning
# .github/workflows/security.yml
# HARDENED: Trivy as a hard gate
name: Security Scan
on:
pull_request:
branches: [main]
jobs:
trivy-image:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build image
run: docker build -t ${{ github.repository }}:${{ github.sha }} .
- name: Trivy image scan
uses: aquasecurity/trivy-action@master
with:
image-ref: ${{ github.repository }}:${{ github.sha }}
format: "sarif"
output: "trivy-results.sarif"
severity: "CRITICAL,HIGH"
exit-code: "1" # Fail on findings
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: "trivy-results.sarif"
CodeQL for Source Analysis
# .github/workflows/codeql.yml
# HARDENED: CodeQL as a required check
name: CodeQL Analysis
on:
pull_request:
branches: [main]
schedule:
- cron: "0 6 * * 1" # Weekly full scan
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
strategy:
matrix:
language: ["javascript", "go", "java"]
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: ${{ matrix.language }}
queries: +security-extended
- name: Build
uses: github/codeql-action/autobuild@v3
- name: Perform analysis
uses: github/codeql-action/analyze@v3
with:
category: "/language:${{ matrix.language }}"
Gitleaks Pre-Commit and CI
# .github/workflows/secrets.yml
# HARDENED: Block commits with secrets
name: Secret Detection
on:
pull_request:
branches: [main]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Gitleaks scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
Trivy IaC Scanning
- name: Trivy IaC scan
uses: aquasecurity/trivy-action@master
with:
scan-type: "config"
scan-ref: "k8s/"
format: "sarif"
output: "trivy-iac.sarif"
exit-code: "1"
severity: "CRITICAL,HIGH"
The Gate
Security scans are required status checks in GitHub branch protection:
Settings → Branches → main → Require status checks:
✓ trivy-image
✓ codeql-analysis
✓ gitleaks
No PR merges to main without all three passing.
The Recovery
Trivy blocks a PR for a vulnerability in a base image: Update the base image. If no fix is available, add the CVE to .trivyignore with an expiration date and a linked tracking issue.
CodeQL produces false positives: Suppress with // codeql[query-id] inline comments. Track suppressions. Review them quarterly.
Gitleaks flags a test fixture as a secret: Add the pattern to .gitleaks.toml allowlist. Never add real secrets to allowlists.