Secure CI/CD Pipeline
Four security gates before merge: SAST, secret scan, dependency audit, and lint. They fan out in parallel so a failure tells you exactly which check tripped.

Four security gates before merge: SAST, secret scan, dependency audit, and lint. They fan out in parallel so a failure tells you exactly which check tripped.

The checks run as separate jobs so a failure tells you exactly which gate tripped instead of one long log to scroll through. lint runs first as a cheap fail-fast. The three security scans (SAST, secret scan, dependency audit) fan out in parallel after lint passes. test waits on all three, and the SOC notification runs last with if: always() so the SOC hears about failures too, not just green runs.
| Job | Tool | What it stops |
|---|---|---|
lint |
ruff | style + security rule set (S) |
sast |
Semgrep | OWASP/Flask packs + four custom rules |
secrets |
gitleaks | committed credentials, full history on PRs |
dependencies |
pip-audit | known-vulnerable pinned packages |
test |
pytest | regressions, with coverage |
notify-soc |
scripts/notify_soc.py |
posts run outcome to SOC webhook |
rules:
- id: flask-debug-true
languages: [python]
severity: ERROR
message: Running Flask with debug=True exposes the Werkzeug debugger and allows remote code execution.
patterns:
- pattern: $APP.run(..., debug=True, ...)
- id: subprocess-shell-true
languages: [python]
severity: ERROR
message: subprocess call with shell=True and a non-literal argument is a command injection risk.
patterns:
- pattern: subprocess.$FN(..., shell=True, ...)
- pattern-not: subprocess.$FN("...", shell=True, ...)
- id: jwt-decode-without-verification
languages: [python]
severity: ERROR
message: jwt.decode with verify=False or options disabling signature verification accepts forged tokens.
patterns:
- pattern-either:
- pattern: 'jwt.decode(..., verify=False, ...)'
- pattern: 'jwt.decode(..., options={..., "verify_signature": False, ...}, ...)'
- id: hardcoded-bind-all-interfaces
languages: [python]
severity: WARNING
message: Binding to 0.0.0.0 exposes the service on all interfaces; confirm this is intended.
patterns:
- pattern: $APP.run(..., host="0.0.0.0", ...)
Semgrep emits SARIF that gets uploaded with github/codeql-action/upload-sarif, so findings show up under the repo's Security tab and as inline PR annotations rather than only in the job log. That matters for review workflow: an engineer looking at a PR sees the Semgrep finding inline next to the offending line, not buried in a separate job output.
The dependency gate uses pip-audit, which fails the build on any pinned package with a known advisory. The screenshot below shows it catching a deliberately vulnerable dependency in the sample app.

The last job posts the run outcome (repo, commit SHA, actor, status, and a link back to the run) to a Shuffle webhook. In the lab, that webhook feeds the SOC automation lab, so a failed security gate opens a TheHive case the same way a Wazuh alert does. Set the SHUFFLE_WEBHOOK_URL repository secret to wire it up; without it the job no-ops rather than failing the run. That means failed gates are visible to the SOC, not just to the engineering team.