Skip to content

Security Scanning

Security Scanning #5

Workflow file for this run

name: Security Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
# Run full scan nightly at 02:00 UTC to catch newly disclosed CVEs
- cron: '0 2 * * *'
workflow_dispatch:
concurrency:
group: security-${{ github.ref }}
cancel-in-progress: true
env:
NODE_VERSION: '20'
jobs:
# Dependency Scan — npm audit + Snyk SCA
dependency-scan:
name: Dependency Scan (SCA)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # required to upload SARIF to GitHub Security tab
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci --prefer-offline
# ── npm audit (built-in, zero config) ──────────────────────────────────
- name: npm audit
run: npm audit --audit-level=high
# Fails the job if any HIGH or CRITICAL vulnerabilities are found.
# Change to --audit-level=moderate to tighten, or =critical to loosen.
# ── Snyk SCA (deeper graph analysis, licence checks) ───────────────────
- name: Snyk — dependency vulnerability scan
uses: snyk/actions/node@master
continue-on-error: true # upload results even when vulnerabilities exist
env:
SNYK_TOKEN: ${{ secrets.SNYK_TOKEN }}
with:
args: >
--severity-threshold=high
--sarif-file-output=snyk-sca.sarif
- name: Upload Snyk SCA results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: snyk-sca.sarif
category: snyk-sca
# Code Scanning — GitHub CodeQL SAST
code-scan:
name: Code Scan (SAST — CodeQL)
runs-on: ubuntu-latest
permissions:
actions: read
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript-typescript
# Built-in security-and-quality query suite — covers OWASP Top 10,
# injection, path traversal, broken auth, and more.
queries: security-and-quality
- uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
- name: Install dependencies
run: npm ci --prefer-offline
- name: Build (required for CodeQL to trace the compilation graph)
run: npm run build
- name: Perform CodeQL analysis
uses: github/codeql-action/analyze@v3
with:
category: codeql-typescript
# Container Scanning — Trivy image scan
container-scan:
name: Container Scan (Trivy)
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write
steps:
- uses: actions/checkout@v4
- name: Build Docker image
run: |
docker build \
--target production \
--tag teachlink-backend:${{ github.sha }} \
.
# ── Trivy — OS packages + application dependencies in the image ─────────
- name: Trivy — image vulnerability scan
uses: aquasecurity/trivy-action@master
with:
image-ref: teachlink-backend:${{ github.sha }}
format: sarif
output: trivy-image.sarif
severity: HIGH,CRITICAL
exit-code: '1' # fail job on HIGH/CRITICAL findings
ignore-unfixed: true # skip vulnerabilities with no upstream fix yet
- name: Upload Trivy image results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-image.sarif
category: trivy-image
# ── Trivy — IaC / config scan (docker-compose, Dockerfiles) ────────────
- name: Trivy — IaC config scan
uses: aquasecurity/trivy-action@master
with:
scan-type: config
scan-ref: .
format: sarif
output: trivy-iac.sarif
severity: HIGH,CRITICAL
exit-code: '0' # advisory only — don't block on misconfiguration
continue-on-error: true
- name: Upload Trivy IaC results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: trivy-iac.sarif
category: trivy-iac
# Aggregate gate — mirrors the "CI Passed" pattern from ci.yml
# Branch protection can optionally require "Security Passed" as a status check
security-passed:
name: Security Passed
runs-on: ubuntu-latest
needs: [dependency-scan, code-scan, container-scan]
if: always()
steps:
- name: Check all security jobs passed
env:
NEEDS_JSON: ${{ toJSON(needs) }}
run: |
node -e '
const needs = JSON.parse(process.env.NEEDS_JSON);
const failed = [];
for (const [name, info] of Object.entries(needs)) {
const result = info.result;
console.log(`${name}: ${result}`);
if (result !== "success") failed.push(`${name}=${result}`);
}
if (failed.length) {
console.error(`❌ Security scan failed: ${failed.join(", ")}`);
process.exit(1);
}
console.log("✅ All security scans passed.");
'