Most security teams pick one scanner, run it in CI, and call it "DevSecOps." The problem is that each scanning technique finds fundamentally different vulnerability classes — running only one leaves you blind to the others. This article shows how all three work, what each misses on its own, and how to wire them together without drowning in false positives.
What Each Scanner Actually Does
SAST (Static Application Security Testing) analyzes source code without executing it. Tools like Semgrep, Bandit (Python), or CodeQL parse your code into an AST and look for patterns: SQL concatenation, unsafe deserialization, hardcoded secrets. SAST is fast and runs early in the pipeline — on every commit. The tradeoff: it knows nothing about your runtime environment, so it can't catch vulnerabilities that only emerge at execution time.
DAST (Dynamic Application Security Testing) attacks a running application from the outside, just like a real attacker would. OWASP ZAP and Nikto send crafted HTTP requests and analyze responses. DAST finds things SAST never can: authentication bypass, misconfigured CORS headers, server-side injection via HTTP parameters. The tradeoff: it needs a deployed application, so it fits better in staging environments than on every commit.
IAST (Interactive Application Security Testing) instruments the application itself — typically via a runtime agent — and monitors execution as tests run. Tools like Contrast Security observe data flow inside the running process. IAST has low false positive rates because it can confirm whether a taint path is actually reachable. The tradeoff: it requires a supported language runtime and adds overhead.
Why Running Only One Leaves You Blind
Consider a typical Python Flask application. SAST catches this immediately:
# Semgrep flags this — classic SQL injection def get_user(user_id: str): query = f"SELECT * FROM users WHERE id = {user_id}" return db.execute(query) But SAST won't catch this misconfiguration:
# No static analyzer flags this — the vulnerability is in runtime config app.config['SESSION_COOKIE_SECURE'] = os.getenv('SECURE_COOKIES', 'false').lower() == 'true' If the environment variable is missing in production, session cookies are sent over HTTP. DAST finds this because it checks the actual Set-Cookie header in the HTTP response. SAST can only see the code; the vulnerability lives in deployment configuration.
Conversely, DAST can't find stored XSS that only renders in an authenticated admin panel — the scanner likely won't reach it. SAST catches the missing output escaping directly in the template code.
IAST catches what both miss: a path where user input flows into a shell command through three intermediate functions, none of which are individually suspicious.
Building the Integrated Pipeline
The goal is to gate each scanner at the right pipeline stage, collect findings in a unified format, and block only on high-severity issues.
Here's a Python script that parses Semgrep's JSON output into a normalized finding structure:
import json import sys from dataclasses import dataclass from typing import Literal @dataclass class Finding: tool: str severity: Literal["critical", "high", "medium", "low", "info"] title: str file: str line: int def parse_semgrep(path: str) -> list[Finding]: with open(path) as f: data = json.load(f) severity_map = {"ERROR": "high", "WARNING": "medium", "INFO": "low"} return [ Finding( tool="semgrep", severity=severity_map.get(r["extra"]["severity"], "info"), title=r["check_id"], file=r["path"], line=r["start"]["line"], ) for r in data.get("results", []) ] def should_block(findings: list[Finding]) -> bool: return any(f.severity in {"critical", "high"} for f in findings) if __name__ == "__main__": findings = parse_semgrep(sys.argv[1]) blocking = [f for f in findings if f.severity in {"critical", "high"}] if blocking: print(f"[BLOCK] {len(blocking)} high/critical finding(s)") for f in blocking: print(f" [{f.severity.upper()}] {f.title} — {f.file}:{f.line}") sys.exit(1) print(f"[PASS] {len(findings)} finding(s), none blocking") For DAST, the standard approach is OWASP ZAP in headless mode. Here's a wrapper that runs a baseline scan against a staging URL and extracts high-risk alerts:
import subprocess import xml.etree.ElementTree as ET from pathlib import Path RISK_MAP = {"3": "high", "2": "medium", "1": "low", "0": "info"} def run_zap_scan(target_url: str, report_dir: str) -> list[Finding]: Path(report_dir).mkdir(parents=True, exist_ok=True) subprocess.run([ "docker", "run", "--rm", "-v", f"{report_dir}:/zap/wrk/:rw", "ghcr.io/zaproxy/zaproxy:stable", "zap-baseline.py", "-t", target_url, "-x", "report.xml", "-I", ], check=False) tree = ET.parse(f"{report_dir}/report.xml") findings = [] for alert in tree.getroot().findall(".//alertitem"): risk = alert.findtext("riskcode", "0") findings.append(Finding( tool="zap", severity=RISK_MAP.get(risk, "info"), title=alert.findtext("alert", "unknown"), file=alert.findtext("uri", ""), line=0, )) return findings The CI layout that places each scanner at the right stage:
# .github/workflows/security.yml jobs: sast: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: semgrep --config=auto --baseline-commit=origin/main --json > semgrep.json - run: python scripts/parse_findings.py semgrep.json dast: runs-on: ubuntu-latest needs: [deploy-staging] steps: - run: python scripts/run_zap.py ${{ env.STAGING_URL }} /tmp/zap For IAST, the setup is agent-specific. With Contrast Security, you add a language agent to your application before running the test suite. The agent instruments function calls and reports taint flows as tests execute — no separate scan step is needed. The results land directly in your security dashboard alongside SAST and DAST findings.
Reducing Noise Without Losing Signal
The biggest adoption blocker for multi-scanner pipelines is alert fatigue. These rules help:
- SAST: block only on new findings in changed files. The
--baseline-commitflag in Semgrep handles this natively — you don't get paged for debt that was already there. - DAST: run on staging after every merge to main, not on every commit. Block on OWASP Top 10 categories only. Baseline scans complete in under 5 minutes.
- IAST: treat as a signal during QA, not a hard gate. IAST findings should flow into your security backlog rather than blocking a release directly.
Store all findings in a single SQLite database and deduplicate on (tool, rule, file, line). A rule firing on ten different files is a systemic issue — fix it at the framework level rather than patching each file individually.
A practical companion to building this pipeline: the free security hardening checklists at AYI NEDJIMI Consultants cover the Linux, Docker, and web application layers that DevSecOps tooling often doesn't touch.
The Takeaway
SAST, DAST, and IAST are not alternatives — they're layers that cover each other's blind spots. SAST is cheap and runs early. DAST validates your runtime behavior. IAST confirms exploitability with low false positives.
Start with SAST (Semgrep is free, fast, and has a large rule library), add DAST against your staging environment, and adopt IAST when your test coverage is deep enough that the agent observes meaningful execution paths. The integration work is mostly plumbing: normalizing outputs, deduplicating findings, and placing each scanner at the right pipeline stage.
I run AYI NEDJIMI Consultants, a cybersecurity consulting firm. We publish free security hardening checklists — PDF and Excel.