SAST Reads Your Code. DAST Pokes Your Running App. You Need Both.
SAST (Static Application Security Testing) analyzes source code without executing it. DAST (Dynamic Application Security Testing) attacks the running app from the outside, like a human pentester would. Each catches a different class of bug. Picking one is leaving half the attack surface uncovered.
- Your “security scan” is a single Snyk run that nobody looks at
- SAST findings pile up because the tool has 800 false positives a week
- DAST runs once a quarter against a staging env with no real data
- Security signs off on PRs by reading the title
The 2026 baseline is SAST + secret scanning + SCA in pre-merge CI, DAST nightly against a representative env, and IAST/RASP in production for the highest-risk apps.
Where Each Tool Lives
flowchart LR
DEV[Dev IDE] -->|SAST inline| PR[Pull Request]
PR -->|SAST + SCA + secret scan| CI[CI gate]
CI -->|build| ART[Artifact]
ART -->|deploy| STAGE[Staging]
STAGE -->|DAST nightly| DASH[Security backlog]
STAGE -->|promote| PROD[Production]
PROD -.->|IAST / RASP| DASH
SAST in Practice
The dominant 2026 stack: Semgrep for cross-language pattern rules, CodeQL for deep dataflow on supported languages, language-specific linters (ESLint security plugin, Bandit for Python,gosec, Brakeman) for ecosystem-aware checks.
# .github/workflows/security.yml — SAST on every PR
name: security
on: [pull_request]
jobs:
semgrep:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: returntocorp/semgrep-action@v1
with:
config: >-
p/owasp-top-ten
p/javascript
p/typescript
p/secrets
codeql:
runs-on: ubuntu-latest
permissions: { security-events: write, actions: read, contents: read }
steps:
- uses: actions/checkout@v4
- uses: github/codeql-action/init@v3
with: { languages: 'javascript-typescript' }
- uses: github/codeql-action/analyze@v3Taint Analysis — Why SAST Sometimes “Just Knows”
Pattern-matching tools find eval(userInput). Taint-flow tools find eval(JSON.parse(decode(req.body.x))) three files away. Sources (user-controlled inputs), sanitizers (parsers, validators, escapers), and sinks (eval, query, exec) form a graph; a flow from any source to a sink without a sanitizer is a finding.
// Semgrep taint rule (simplified)
rules:
- id: dom-xss-from-querystring
mode: taint
pattern-sources:
- pattern: location.search
- pattern: new URL(...).searchParams.get(...)
pattern-sanitizers:
- pattern: DOMPurify.sanitize(...)
pattern-sinks:
- pattern: $EL.innerHTML = $X
- pattern: document.write($X)
message: Untrusted URL parameter reaches innerHTML/document.write
severity: ERRORDAST in Practice
OWASP ZAP and Burp Suite Enterprise dominate. The setup that actually works:
- Stand up a representative staging env with seeded test data (not prod-like data, not empty — seeded).
- Provision a test user per role; export the auth flow as a ZAP script.
- Feed ZAP your OpenAPI spec so it knows every endpoint, not just what it can crawl.
- Run nightly; gate releases on critical/high findings, not every advisory.
# OWASP ZAP API scan from CI
docker run --rm -t \
-v "$PWD:/zap/wrk:rw" ghcr.io/zaproxy/zaproxy:stable \
zap-api-scan.py \
-t https://staging.example.com/openapi.json \
-f openapi \
-z "-config api.disablekey=true -config replacer.full_list(0).description=auth" \
-r zap-report.html \
-w zap-report.md \
-J zap-report.jsonIAST / RASP — Production Telemetry for Security
Interactive (IAST) and Runtime (RASP) tools instrument the running app: every parsed input, every outgoing query, every loaded class. Findings have call stacks and request context, so triage is faster than DAST's “something was reflected at /search”. Vendors: Contrast Security, Datadog ASM, Imperva.
SCA — Don't Forget the Dependencies
Most of your code is dependencies. SCA (Software Composition Analysis) tools — Snyk, Dependabot, Renovate, GitHub Advanced Security, OSV-Scanner — match your lockfile against vulnerability databases. Pin versions, generate an SBOM in CI (cyclonedx / syft), block builds on known critical CVEs with a known fixed version.
Triage: Stop the Backlog Before It Starts
Day one of any security program: agree on what is “must fix”, “triage”, and “noise”. Without this, the SAST dashboard becomes a graveyard.
| Severity | Action | SLA |
|---|---|---|
| Critical (exploitable, prod) | Page; block merges | 24h |
| High | Auto-create issue; block release of affected module | 7d |
| Medium | Add to backlog; review weekly | 30d |
| Low / Info | Suppress with comment, track noise rate | — |
Where SAST Lies
SAST has no idea what your runtime config looks like. It flags every SQL string concatenation even if you only ever pass it to an in-memory SQLite for tests. It misses framework-level mitigations (CSRF tokens, automatic escaping). False positives are inevitable; the goal is keeping them under ~15% so developers still read the output.
Where DAST Lies
DAST can't see code paths it doesn't reach. Admin-only endpoints, multi-step workflows, and feature-flagged code are invisible unless you specifically script them. DAST + SAST together cover what neither does alone.
Pitfalls
Treating tool output as “the security review”
Auto-tools find known patterns. Business logic flaws, auth-N/Z bypasses, and chained vulnerabilities still need humans. The tools free up review time for those — they don't replace it.
Running scans without an owner
A new finding with no owner becomes nobody's problem. Route every alert to a team (CODEOWNERS or service catalog) and track time-to- triage, not just time-to-fix.
Gating on every finding
Block CI on critical-with-exploit. For high and below, file an issue and don't block — otherwise the immediate response will be “turn the scanner off”, and you're back to zero.

