Integrate SAST, DAST, SCA, and secrets detection into a complete GitHub Actions security pipeline with stage-appropriate gates, unified PR reporting, and Slack alerting.
DevSecOps integrates security into every stage of the development lifecycle, shifting security left from a final QA step to an always-on discipline. The goal is not to slow development down — it is to make security findings actionable at the moment code is written, not weeks later in a pentest report.
This lesson builds the complete DevSecOps pipeline: from pre-commit hooks to production monitoring.
Stage 1: Developer Machine (milliseconds-seconds):
Stage 2: Pull Request (minutes):
Stage 3: Staging Deployment (minutes-tens of minutes):
Stage 4: Pre-Production (annual):
1Gates:
2- CRITICAL findings: block at any stage
3- HIGH findings: require explicit approval to proceed
4- MEDIUM/LOW: warn, do not blockCodeQL performs deep semantic code analysis, finding taint-based vulnerabilities that syntactic pattern matching misses.
1# .github/workflows/codeql.yml
2name: CodeQL Analysis
3on:
4 push:
5 branches: [main]
6 pull_request:
7 branches: [main]
8 schedule:
9 - cron: '0 4 * * 1'
10jobs:
11 analyze:
12 runs-on: ubuntu-latest
13 permissions:
14 security-events: write
15 contents: read
16 strategy:
17 matrix:
18 language: ['javascript', 'typescript']
19 steps:
20 - uses: actions/checkout@v4
21 - name: Initialize CodeQL
22 uses: github/codeql-action/init@v3
23 with:
24 languages: javascript
25 queries: +security-and-quality
26 - uses: github/codeql-action/autobuild@v3
27 - uses: github/codeql-action/analyze@v3
28 with:
29 category: /language:javascriptCodeQL findings appear in GitHub Security tab. CodeQL finds: path injection, SQL injection (taint analysis across files), ReDoS, and prototype pollution — with full data-flow analysis, not just pattern matching.
Aggregate all findings into a single PR comment:
1// scripts/generate-security-report.cjs
2const fs = require('fs');
3
4function parseSemgrep(file) {
5 if (!fs.existsSync(file)) return { errors: 0, warnings: 0 };
6 const d = JSON.parse(fs.readFileSync(file));
7 const results = d.results || [];
8 return {
9 errors: results.filter(r => r.extra?.severity === 'ERROR').length,
10 warnings: results.filter(r => r.extra?.severity === 'WARNING').length,
11 };
12}
13
14function parseNpmAudit(file) {
15 if (!fs.existsSync(file)) return { critical: 0, high: 0, moderate: 0 };
16 return JSON.parse(fs.readFileSync(file)).metadata?.vulnerabilities || {};
17}
18
19function parseZap(file) {
20 if (!fs.existsSync(file)) return { high: 0, medium: 0 };
21 const alerts = JSON.parse(fs.readFileSync(file)).site?.[0]?.alerts || [];
22 return {
23 high: alerts.filter(a => a.riskcode === '3').length,
24 medium: alerts.filter(a => a.riskcode === '2').length,
25 };
26}
27
28const semgrep = parseSemgrep('reports/security/semgrep.json');
29const npm = parseNpmAudit('reports/security/npm-audit.json');
30const zap = parseZap('reports/security/zap-baseline.json');
31
32const totalCritical = npm.critical + zap.high + semgrep.errors;
33const status = totalCritical > 0 ? 'FAIL' : 'PASS';
34
35const report = [
36 '<!-- security-report -->',
37 '## Security Scan Results (' + status + ')',
38 '',
39 '| Tool | Critical/High | Medium/Warning |',
40 '|------|--------------|----------------|',
41 '| Semgrep SAST | ' + semgrep.errors + ' errors | ' + semgrep.warnings + ' warnings |',
42 '| npm audit SCA | ' + npm.critical + ' critical, ' + npm.high + ' high | ' + (npm.moderate || 0) + ' moderate |',
43 '| OWASP ZAP DAST | ' + zap.high + ' high | ' + zap.medium + ' medium |',
44 '',
45 status === 'FAIL'
46 ? '**Action required**: Fix ' + totalCritical + ' critical/high finding(s) before merge.'
47 : '**All security checks passed.**',
48].join('\n');
49
50fs.writeFileSync('security-report.md', report);
51console.log(report);
52process.exit(totalCritical > 0 ? 1 : 0);1# .github/workflows/security-gate.yml
2jobs:
3 security-gate:
4 needs: [sast-semgrep, sca-scan, secrets-scan]
5 if: always()
6 runs-on: ubuntu-latest
7 steps:
8 - uses: actions/checkout@v4
9 - uses: actions/setup-node@v4
10 with: { node-version: '20' }
11 - uses: actions/download-artifact@v4
12 with: { pattern: 'security-*', merge-multiple: true, path: reports/ }
13 continue-on-error: true
14
15 - name: Generate unified report
16 run: node scripts/generate-security-report.cjs
17 continue-on-error: true
18
19 - name: Post PR comment
20 if: github.event_name == 'pull_request'
21 uses: actions/github-script@v7
22 with:
23 script: |
24 const fs = require('fs');
25 const report = fs.existsSync('security-report.md')
26 ? fs.readFileSync('security-report.md', 'utf8')
27 : '<!-- security-report -->\n## Security\nNo reports generated.';
28 const { data: comments } = await github.rest.issues.listComments(
29 { ...context.repo, issue_number: context.issue.number });
30 const existing = comments.find(c => c.body.includes('<!-- security-report -->'));
31 if (existing) {
32 await github.rest.issues.updateComment(
33 { ...context.repo, comment_id: existing.id, body: report });
34 } else {
35 await github.rest.issues.createComment(
36 { ...context.repo, issue_number: context.issue.number, body: report });
37 }
38
39 - name: Enforce gate
40 run: |
41 if node scripts/generate-security-report.cjs; then
42 echo 'Security gate passed'
43 else
44 echo 'Security gate FAILED'
45 exit 1
46 fiSecurity KPIs to track weekly:
1const SECURITY_KPIS = {
2 meanTimeToRemediate: {
3 critical: '4 hours', // fix within 4h of discovery
4 high: '72 hours', // fix within 3 days
5 medium: '30 days',
6 },
7 openVulnerabilities: {
8 critical: 0, // always 0
9 high: 0, // always 0
10 medium: '<10',
11 },
12 testCoverage: {
13 sast: 'all PRs',
14 sca: 'all PRs + weekly schedule',
15 dast: 'every staging deploy',
16 secrets: 'every commit (pre-commit)',
17 manualPentest: 'quarterly',
18 },
19} as const;Snyk continuous monitoring:
1# After each staging deployment:
2snyk monitor \
3 --project-name="developerbootstrap-$(git rev-parse --short HEAD)" \
4 --tags="env=staging"
5# Snyk emails/Slacks when new CVEs affect monitored projectsGitHub Security tab: Upload SARIF from Semgrep and CodeQL for persistent, filterable vulnerability tracking:
1- uses: github/codeql-action/upload-sarif@v3
2 with:
3 sarif_file: semgrep.sarif
4 category: semgrep
5# Results appear in GitHub → Security → Code scanning alertsProduction GitHub Actions workflow implementing the full security pipeline: SAST, SCA, secrets detection, DAST, unified reporting, and security gate.
1# .github/workflows/devsecops.yml
2name: DevSecOps Security Pipeline
3
4on:
5 pull_request:
6 branches: [main]
7 push:
8 branches: [main, 'release/**']
9
10jobs:
11 # SAST with Semgrep
12 sast:
13 name: SAST - Semgrep
14 runs-on: ubuntu-latest
15 container: { image: returntocorp/semgrep }
16 steps:
17 - uses: actions/checkout@v4
18 - name: Semgrep scan
19 run: |
20 semgrep \
21 --config=p/nodejs --config=p/secrets --config=p/owasp-top-ten \
22 --config=.semgrep/ \
23 --json --output=semgrep.json \
24 --sarif --output=semgrep.sarif \
25 --exclude='*.test.ts' --exclude='*.spec.ts' \
26 apps/ packages/ || true
27 continue-on-error: true
28 - uses: github/codeql-action/upload-sarif@v3
29 if: always()
30 with: { sarif_file: semgrep.sarif, category: semgrep }
31 continue-on-error: true
32 - uses: actions/upload-artifact@v4
33 if: always()
34 with: { name: security-semgrep, path: semgrep.json }
35
36 # SCA with npm audit and Snyk
37 sca:
38 name: SCA - Dependencies
39 runs-on: ubuntu-latest
40 steps:
41 - uses: actions/checkout@v4
42 - uses: actions/setup-node@v4
43 with: { node-version: '20', cache: 'npm' }
44 - run: npm ci
45 - name: npm audit
46 run: npm audit --json > npm-audit.json 2>/dev/null || true
47 continue-on-error: true
48 - name: Snyk test
49 uses: snyk/actions/node@master
50 env: { SNYK_TOKEN: SNYK_TOKEN_VALUE }
51 with: { args: --severity-threshold=high --json-file-output=snyk.json }
52 continue-on-error: true
53 - uses: actions/upload-artifact@v4
54 if: always()
55 with:
56 name: security-sca
57 path: |
58 npm-audit.json
59 snyk.json
60
61 # Secrets Detection
62 secrets:
63 name: Secrets Detection
64 runs-on: ubuntu-latest
65 steps:
66 - uses: actions/checkout@v4
67 with: { fetch-depth: 0 } # full history for truffleHog
68 - name: truffleHog
69 run: |
70 pip install trufflehog -q
71 trufflehog git file://. --only-verified --json > truffle.json 2>/dev/null || true
72 FOUND=$(wc -l < truffle.json | tr -d ' ')
73 if [ "$FOUND" -gt 0 ]; then
74 echo "CRITICAL: $FOUND verified secrets in history"
75 exit 1
76 fi
77
78 # DAST with ZAP (only on push to main)
79 dast:
80 name: DAST - ZAP Baseline
81 needs: [sca, secrets]
82 runs-on: ubuntu-latest
83 if: github.event_name == 'push'
84 steps:
85 - uses: actions/checkout@v4
86 - name: Wait for staging
87 run: timeout 120 bash -c 'until curl -sf https://staging.api.example.com/api/health; do sleep 5; done'
88 - name: ZAP Baseline
89 uses: zaproxy/action-[email protected]
90 with:
91 target: https://staging.api.example.com
92 token: ZAP_TOKEN
93 cmd_options: -J zap.json -I
94 fail_action: false
95 continue-on-error: true
96 - uses: actions/upload-artifact@v4
97 if: always()
98 with: { name: security-zap, path: zap.json }
99
100 # Security Gate — unified report and deployment block
101 gate:
102 name: Security Gate
103 needs: [sast, sca, secrets]
104 if: always()
105 runs-on: ubuntu-latest
106 steps:
107 - uses: actions/checkout@v4
108 - uses: actions/setup-node@v4
109 with: { node-version: '20' }
110 - uses: actions/download-artifact@v4
111 with: { pattern: security-*, merge-multiple: true, path: reports/ }
112 continue-on-error: true
113
114 - name: Generate unified report
115 run: node scripts/generate-security-report.cjs
116 continue-on-error: true
117
118 - name: Post PR comment
119 if: github.event_name == 'pull_request'
120 uses: actions/github-script@v7
121 with:
122 script: |
123 const fs = require('fs');
124 const report = fs.existsSync('security-report.md')
125 ? fs.readFileSync('security-report.md', 'utf8')
126 : '<!-- security-report -->\n## Security\nNo reports generated.';
127 const { data: comments } = await github.rest.issues.listComments(
128 { ...context.repo, issue_number: context.issue.number });
129 const existing = comments.find(c => c.body.includes('<!-- security-report -->'));
130 if (existing) {
131 await github.rest.issues.updateComment({ ...context.repo, comment_id: existing.id, body: report });
132 } else {
133 await github.rest.issues.createComment({ ...context.repo, issue_number: context.issue.number, body: report });
134 }
135
136 - name: Enforce security gate
137 run: |
138 if [ -f security-report.md ] && grep -q 'FAIL' security-report.md; then
139 echo 'Security gate FAILED — blocking deployment'
140 exit 1
141 fi
142 echo 'Security gate PASSED'This pipeline runs SAST (Semgrep), SCA (npm audit + Snyk), secrets detection (truffleHog), and DAST (ZAP, only on main pushes) as separate jobs in parallel where possible. The security gate job collects all artifacts, generates a unified report, posts a PR comment (updating existing to avoid comment spam), and exits non-zero if the report contains FAIL. DAST runs only on pushes to main to avoid 10-minute scan times on every PR. Semgrep results are also uploaded as SARIF to the GitHub Security tab.
You have completed the Security Testing module. The next module covers API Testing Mastery — starting with REST API testing fundamentals and advancing to contract testing, GraphQL, and production API testing patterns.
Your progress is saved in this browser, on this device only.