← THE INDEX  ·  DETECTION ENG

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.

Secure CI/CD Pipeline

How it's structured

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
.semgrep/rules.yml: four custom rules for cases the packs miss
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", ...)

SARIF and the Security tab

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.

SOC integration

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.