Manual security testing does not scale. To catch vulnerabilities continuously, you need to embed security checks into your development workflow. This is the practice of DevSecOps -- shifting security left into the CI/CD pipeline.
The DevSecOps Pipeline
Code ──> Commit ──> Build ──> Test ──> Deploy ──> Monitor
│ │ │ │ │ │
▼ ▼ ▼ ▼ ▼ ▼
SAST Secret SCA/Dep DAST Config Runtime
Lint Scanning Audit Scan Audit ProtectionEach stage adds a security layer:
| Stage | Tool Type | What It Catches |
|---|---|---|
| Code | SAST | Code-level vulnerabilities |
| Commit | Secrets | Hardcoded credentials |
| Build | SCA | Vulnerable dependencies |
| Test | DAST | Runtime vulnerabilities |
| Deploy | Config | Infrastructure misconfigurations |
| Monitor | Runtime | Active attacks, anomalies |
Secret Scanning with Gitleaks
Prevent credentials from entering your repository.
# .github/workflows/security.yml
name: Security Checks
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
secret-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}Custom Gitleaks Configuration
# .gitleaks.toml
title = "Custom Gitleaks Config"
[[rules]]
id = "firebase-api-key"
description = "Firebase API Key"
regex = '''AIza[0-9A-Za-z\-_]{35}'''
tags = ["key", "firebase"]
[[rules]]
id = "generic-api-key"
description = "Generic API Key"
regex = '''(?i)(api[_-]?key|apikey)\s*[:=]\s*['"][a-zA-Z0-9]{20,}['"]'''
tags = ["key", "api"]
[allowlist]
paths = [
'''\.env\.example''',
'''docs/.*\.md'''
]
Static Application Security Testing (SAST)
Analyze source code for vulnerabilities without running the application.
ESLint Security Plugin for JavaScript/TypeScript
# Install security-focused ESLint plugin
pnpm add -D eslint-plugin-security// eslint.config.js
import security from 'eslint-plugin-security';
export default [
{
plugins: { security },
rules: {
'security/detect-object-injection': 'warn',
'security/detect-non-literal-regexp': 'warn',
'security/detect-unsafe-regex': 'error',
'security/detect-buffer-noassert': 'error',
'security/detect-eval-with-expression': 'error',
'security/detect-no-csrf-before-method-override': 'error',
'security/detect-possible-timing-attacks': 'warn',
}
}
];Semgrep for Multi-Language SAST
# In your CI pipeline
sast-scanning:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run Semgrep
uses: returntocorp/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/javascript
p/typescript
p/reactCustom Semgrep Rules
# .semgrep/custom-rules.yml
rules:
- id: no-innerhtml
patterns:
- pattern: $EL.innerHTML = $VALUE
message: "Using innerHTML can lead to XSS. Use textContent or a sanitizer."
languages: [javascript, typescript]
severity: ERROR
- id: no-hardcoded-secrets
patterns:
- pattern: |
const $KEY = "..."
- metavariable-regex:
metavariable: $KEY
regex: ".*(password|secret|api_key|token).*"
message: "Possible hardcoded secret in variable $KEY"
languages: [javascript, typescript]
severity: ERRORDependency Scanning (SCA)
Check your dependencies for known vulnerabilities.
dependency-check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Audit dependencies
run: pnpm audit --audit-level=high
- name: Check for outdated packages
run: pnpm outdated || trueAutomated Dependency Updates with Renovate
// renovate.json
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"extends": ["config:recommended"],
"vulnerabilityAlerts": {
"enabled": true,
"labels": ["security"],
"assignees": ["security-team"]
},
"packageRules": [
{
"matchUpdateTypes": ["patch"],
"automerge": true,
"automergeType": "branch"
}
]
}Dynamic Application Security Testing (DAST)
Scan the running application for vulnerabilities.
dast-scanning:
runs-on: ubuntu-latest
needs: [deploy-staging]
steps:
- name: ZAP Baseline Scan
uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.target-app.com'
rules_file_name: '.zap/rules.tsv'
fail_action: true
- name: Upload ZAP Report
uses: actions/upload-artifact@v4
if: always()
with:
name: zap-report
path: report_html.htmlZAP Rules Configuration
# .zap/rules.tsv
# Rule ID Action Description
10038 IGNORE Content Security Policy (intentional inline scripts)
10063 WARN Feature Policy Header Not Set
10098 FAIL Cross-Domain Misconfiguration
40012 FAIL Cross Site Scripting (Reflected)
40014 FAIL Cross Site Scripting (Persistent)
90033 FAIL Loosely Scoped CookieComplete Security Pipeline
Here is a full pipeline that combines all stages:
name: Security Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
schedule:
- cron: '0 6 * * 1' # Weekly Monday scan
jobs:
secrets:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- uses: gitleaks/gitleaks-action@v2
sast:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: p/owasp-top-ten
dependencies:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pnpm install --frozen-lockfile
- run: pnpm audit --audit-level=high
dast:
runs-on: ubuntu-latest
needs: [secrets, sast, dependencies]
steps:
- uses: zaproxy/action-baseline@v0.12.0
with:
target: 'https://staging.target-app.com'
security-gate:
runs-on: ubuntu-latest
needs: [secrets, sast, dependencies, dast]
steps:
- name: Security Gate Check
run: |
echo "All security checks passed"
echo "Safe to deploy to production"Monitoring and Alerting
Security does not end at deployment.
// Example: Runtime security monitoring middleware (Express.js)
function securityMonitor(req, res, next) {
const suspiciousPatterns = [
/(\%27)|(\')|(\-\-)/i, // SQL injection
/<script[\s>]/i, // XSS attempts
/\.\.\//, // Path traversal
/etc\/passwd/i, // System file access
];
const input = `${req.url} ${JSON.stringify(req.body)}`;
for (const pattern of suspiciousPatterns) {
if (pattern.test(input)) {
console.warn(`[SECURITY] Suspicious request detected`, {
ip: req.ip,
method: req.method,
url: req.url,
pattern: pattern.toString(),
timestamp: new Date().toISOString()
});
// Log but don't block -- WAF handles blocking
break;
}
}
next();
}Key Takeaways
- Shift security left by integrating checks into every pipeline stage
- Secret scanning prevents credentials from entering version control
- SAST catches code vulnerabilities before they reach production
- Dependency scanning flags known vulnerabilities in third-party packages
- DAST tests the running application for exploitable issues
- A security gate ensures nothing deploys without passing all checks
- Runtime monitoring catches attacks that bypass pre-deployment testing
You have completed the Security Testing course. You now have the skills to identify, exploit, and automate the detection of security vulnerabilities in web applications.