Security Scanning #25
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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."); | |
| ' |