Dynamic Analysis in Practice
By chapter 6 you've already read your own code (SAST) and audited what you pulled in from someone else (SCA). This chapter attacks the thing you actually shipped — a live, running target — the way an external adversary would: no source access, just HTTP traffic and a browser. You'll stand up an OWASP ZAP baseline scan against a target you control, wire in the authentication and spidering setup that's the real difference between a scan that covers the app and one that quietly stalls at the login form, run a full active scan and a Burp Suite Dastardly pass, and learn to read an alert's risk and confidence before you file it. Every command here is one you can run today against a target you're authorized to test.
Testing a house's security from outside means walking around and rattling every door and window without ever seeing the blueprints. But if nobody hands you a key to get past the front door first, you'll spend the whole afternoon rattling the one door you can reach and call it "tested" — when the real house has a kitchen, three bedrooms, and a garage you never got close to. DAST is that outside test. Handing it a working key before it starts rattling doors is what turns "I checked the porch" into "I checked the house."
What this chapter tests, and where DAST sits in the blueprint
☺ Like you're 10: This chapter isn't a quiz about definitions — it's the part where you actually point a scanner at something and read what comes back.
Practical DevSecOps doesn't publish a percentage-weighted domain breakdown for the CDP the way a multiple-choice exam publishes "Domain 3 is 20% of the test" — its 5 live challenges blend these skills together against a running environment, a point the certifications page already makes plainly. What this course's own blueprint does instead is organize the underlying practice into 9 chapters that map onto that hands-on scope, and dynamic analysis is reliably one full challenge-equivalent of work on its own: standing up a scan correctly, authenticating it, and separating a real finding from noise — against a target that's actually running, not a diff sitting in a pull request.
That's also the structural reason DAST comes after static analysis and secrets detection in this blueprint rather than before it. Chapter 5 only needed a source tree — it could run in seconds, on every commit, before anything was deployed. This chapter needs a deployed target, because it isn't reading code at all; it's attacking behavior, and behavior only exists once something is running. If you haven't already read the conceptual split between SAST, DAST, and SCA, that page is the fifteen-minute primer this chapter assumes. What follows is the part that primer only summarizes: the actual commands, the actual YAML, and the actual judgment calls.
Stand up a baseline scan against a running target
☺ Like you're 10: A baseline scan is the burglar walking around and looking — it never actually tries a door, so it's safe to run against a house that's still occupied.
OWASP ZAP ships an official Docker image, and the fastest way to get a first result is its baseline scan script — a thin wrapper that spiders the target for a short, bounded window and then runs ZAP's passive scan rules against whatever traffic that spidering generated. "Passive" is the load-bearing word: baseline never sends an attack payload, it only inspects the requests and responses that ordinary crawling produced. That's exactly why it's the scan most teams wire in as a required PR or merge-gate check — it finishes in minutes and it's safe to point at almost anything, staging or otherwise.
docker run --rm -v $(pwd):/zap/wrk/:rw \ -t zaproxy/zap-stable zap-baseline.py \ -t https://staging.internal.example.com \ -r zap-baseline-report.html \ -J zap-baseline-report.json \ -I
The -I flag tells the script not to fail the job on a WARN-level result — useful while you're first tuning a rule set, so a pipeline doesn't hard-block on day one before anyone has triaged anything. Once the rule set is tuned, drop -I and instead reclassify individual rules with a config file, so the exit code reflects a decision your team actually made rather than ZAP's untouched defaults:
# zap-rules.conf — one line per plugin ID, overriding the default WARN # format: <plugin id> <IGNORE|WARN|FAIL> # optional comment 10202 IGNORE # Absence of Anti-CSRF Tokens — this route is a public, read-only API 10096 FAIL # Timestamp Disclosure — leaks internal clock skew we specifically track
Pass it with -c zap-rules.conf. Exact exit-code semantics have shifted a little across ZAP releases, so confirm the current mapping in ZAP's own docs before you wire a hard pipeline gate on a specific number — but the mental model (WARN by default, promote to FAIL for anything that should block, IGNORE for a confirmed false positive with a reason attached) has stayed stable for years.
A green baseline run tells you the passive rules found nothing in the traffic the spider happened to generate — it says nothing about whether an attack payload against those same routes would have succeeded, because baseline never sent one. Treat it as the fast, cheap, always-on gate it is, not as proof the app has been attacked. The full active scan later in this chapter is what actually attacks it.
Authentication: the setup that decides how much of the app gets tested
☺ Like you're 10: Without a key, the spider only ever finds the porch — the dashboard, the admin panel, and everything behind login stay invisible no matter how long it crawls.
Run either command above against a typical app with no authentication configured, and the spider finds the login page, maybe a marketing home page, maybe a health-check endpoint — three or four routes, total. The scan finishes fast, the report looks clean, and none of that means anything, because the spider never got past the front door. This is the single most common reason a DAST run "passes" and the app still ships a real vulnerability two clicks past login: nobody told the scanner how to log in.
ZAP's Automation Framework is the current, YAML-driven way to wire this up — it replaces hand-clicking through the desktop UI's authentication wizard with something you can commit to version control and diff. A context block defines the target, how to authenticate into it, and how ZAP verifies whether a given response came back logged in or logged out:
env:
contexts:
- name: "webapp"
urls: ["https://staging.internal.example.com"]
includePaths: ["https://staging.internal.example.com.*"]
excludePaths: ["https://staging.internal.example.com/logout.*"]
authentication:
method: "form"
parameters:
loginPageUrl: "https://staging.internal.example.com/login"
loginRequestUrl: "https://staging.internal.example.com/login"
loginRequestBody: "username={%username%}&password={%password%}"
verification:
method: "response"
loggedInRegex: "\\QWelcome back\\E"
loggedOutRegex: "\\QPlease log in\\E"
users:
- name: "scanner"
credentials:
username: "dast-scanner@example.com"
password: "$ZAP_SCANNER_PASSWORD"
jobs:
- type: spider
parameters: { context: "webapp", user: "scanner", maxDuration: 10 }
- type: spiderAjax
parameters: { context: "webapp", user: "scanner", maxDuration: 10 }
- type: passiveScan-wait
parameters: { maxDuration: 5 }
- type: report
parameters: { template: "traditional-html", reportDir: "/zap/wrk", reportFile: "zap-auth-report" }Two details here do the actual work, and both are easy to skip under deadline pressure. First, verification — the loggedInRegex/loggedOutRegex pair — exists because a session can silently die mid-scan: a CSRF token mismatch, an idle timeout, a WAF rule that flags the scanner's traffic pattern. Without a verification check, ZAP has no way to notice that it fell back to an unauthenticated session, and everything scanned after that point quietly becomes "the same three public routes" again, just later in the run. Second, excludePaths keeps the spider away from /logout — a link-crawling spider treats a logout link exactly like any other link, and clicking it ends the session it just spent ten minutes building.
Not every app authenticates with a form POST. A token-based SPA or an API needs a script-based or HTTP-header authentication method instead — attaching a bearer token or JWT to every outgoing request via a ZAP script or the Automation Framework's script-based auth method. The exact configuration surface for that has moved around across ZAP add-on versions, so treat the shape here as the pattern to look for rather than something to copy verbatim without checking your installed version's current job list.
Teams that add authentication but skip verification often don't notice for months — the scan runs, produces a plausible-looking report with dozens of alerts, and nobody checks whether those alerts came from behind login or from the marketing site. Deliberately break the login credentials once after setting this up and confirm the scan visibly fails or flags zero authenticated coverage. If it doesn't notice, neither will your pipeline.
Spidering: traditional, AJAX, and OpenAPI-seeded coverage
☺ Like you're 10: One spider reads street signs; the other one actually walks into every store and opens every drawer — a modern app usually needs both, plus a map somebody already drew.
ZAP ships three distinct ways to discover what's there to attack, and picking only one is the second-most-common reason a DAST run under-covers a real application:
- Traditional spider — parses raw HTML for
<a href>links and forms. Fast, and it's what the baseline scan uses by default. It's also blind to anything rendered client-side, which describes most of the interactive surface of a modern React, Vue, or Angular app. - AJAX Spider — drives an actual headless browser (via Selenium, typically headless Firefox or Chrome) to click through the rendered UI and execute JavaScript, the same way a real user would. This is the one that actually discovers a single-page app's routes, because it's the only mode that runs the client-side router at all.
- OpenAPI / GraphQL import — for an API with no HTML to crawl in the first place, ZAP can import the spec directly and seed the sitemap from its documented operations, skipping discovery entirely.
zap-api-scan.py -t https://staging.internal.example.com/openapi.json -f openapi -r zap-api-report.htmlruns an API-tuned scan against every operation in the file.
A pipeline that only ever runs the default traditional spider against a modern SPA backend will report a route count in the single digits against an app with dozens of real screens, and it will look exactly as clean as a thorough scan — clean reports and thin coverage are visually identical until you check the route count against what you know the app actually has.
Reading alert severity: risk × confidence, not just risk
☺ Like you're 10: One number says how bad it'd be if it's real; a second number says how sure the scanner is that it's real — you need both before you decide what to do.
Every ZAP alert carries two independent ratings, and treating them as one number is how teams end up either crying wolf or missing a real finding. Risk — Informational, Low, Medium, or High — describes how bad the underlying issue would be if it's genuinely exploitable. Confidence — Low, Medium, High, or Confirmed — describes how sure the scanner is that what it saw is actually that issue, as opposed to a heuristic false alarm. A few concrete alerts, and where they typically land:
| Alert | Typical risk | Typical confidence | Why |
|---|---|---|---|
| SQL Injection | High | Varies — Low for a timing-based heuristic, High/Confirmed once a payload provably alters query behavior | Easy to suspect from response-time drift, hard to prove without a confirmed behavioral change |
| Cross Site Scripting (Reflected) | High | Usually High | The scanner can directly verify its own payload came back unescaped in the response |
| Absence of Anti-CSRF Tokens | Medium | Medium | Structurally suspicious, but some routes are legitimately token-free (public GETs, API-only endpoints) |
| Content Security Policy Header Not Set | Medium | High | A missing header is a simple, unambiguous check — no room for the scanner to be wrong about whether it's present |
| Cookie No HttpOnly Flag | Low | High | Same — directly observable from the response headers, nothing to infer |
| Information Disclosure – Suspicious Comments | Informational | Low | Pattern-matches source comments left in shipped HTML/JS; frequently benign |
The triage rule that keeps a team from either type of failure: a High risk, Low confidence alert gets manually verified before it blocks anything — reproduce it by hand, or it doesn't go in the release-blocking queue. A High risk, High/Confirmed confidence alert blocks the merge immediately; there's no ambiguity left to resolve. When you do confirm something is a false positive, suppress it narrowly — a ZAP Alert Filter scoped to that specific plugin ID and URL regex, with a written reason — never the whole plugin globally, or you've just blinded yourself to the next real instance of that same alert on a different route.
Risk tells you how bad it would be if it's real. Confidence tells you how sure the scanner is that it is real. A severity label that collapses both into one number is throwing away exactly the information a human triager needs to decide what happens next.
Going active: full scans, and where Burp Suite Dastardly fits in CI
☺ Like you're 10: This is the part where the burglar actually tries the doors — which is exactly why you only ever do it to a house you own.
Where baseline only listens, ZAP's full/active scan actually attacks: it takes every parameter the spider discovered and throws SQL injection payloads, XSS strings, path traversal sequences, and command-injection attempts at each one, then inspects the real response for evidence something worked. zap-full-scan.py -t https://staging.internal.example.com -r zap-full-report.html runs it end to end (spider, then active scan, then report) against whatever context and auth you've wired in the same way as the baseline run.
An active scan sends real, malicious-shaped payloads. It can create test data, trip alerting your on-call didn't expect, and in rare cases knock over a fragile endpoint. Only ever point it at a target you're explicitly authorized to attack — staging, a dedicated test environment, or a deliberately vulnerable practice app like OWASP Juice Shop or DVWA. Never a production system without a documented, agreed authorization window, and never someone else's system at all.
Burp Suite Dastardly, PortSwigger's free scanner built specifically for CI, trades thoroughness for speed and zero configuration. It runs a fixed, non-tunable scan — a fast crawl plus a curated set of high-confidence checks — and emits JUnit XML instead of a standalone report, so a DAST finding shows up next to your unit test failures in the same CI test-results UI instead of living in a separate silo nobody opens:
docker run --rm \ -e BURP_START_URL="https://staging.internal.example.com" \ -e BURP_REPORT_FILE_PATH="/dastardly/dastardly-report.xml" \ -v "$(pwd)":/dastardly \ public.ecr.aws/portswigger/dastardly:latest
Verify that image path on PortSwigger's own Dastardly docs before wiring it into a pipeline — registry locations move. What doesn't move is the positioning: Dastardly is explicitly a lightweight complement to a full ZAP or Burp Suite Professional scan, not a replacement for one. It has no authentication-configuration surface of its own, so it's best used on public-facing surfaces or paired with an already-authenticated ZAP run, not as your only DAST pass against an app that's mostly behind login.
| Tool / mode | What it does | Needs auth wired in | Safe against prod | Output |
|---|---|---|---|---|
| ZAP Baseline | Spider + passive scan only, no attack payloads | Recommended, not required | Yes | HTML/JSON/XML report, CI exit code |
| ZAP Full / Active Scan | Full spidering + real attack payloads against every parameter | Yes — or coverage collapses to the login shell | No — authorized targets only | Detailed HTML/JSON report |
| ZAP API Scan | Seeds from an OpenAPI/GraphQL spec, active-scans every operation | Yes, via token/header | No — same active payloads | HTML/JSON report |
| Burp Suite Dastardly | Fixed, fast crawl + curated check-set, tuned for CI | No built-in config | Test/staging only | JUnit XML — plugs into CI test reporting |
Run a ZAP baseline scan against a local, deliberately vulnerable target you're allowed to attack — OWASP Juice Shop or DVWA in a container — unauthenticated first, then again with a login context wired in through the Automation Framework. Compare the route count and alert count between the two runs; that gap is this chapter. Then run Burp Suite Dastardly against the same target and diff its JUnit output against ZAP's report. Same target, three different levels of thoroughness, in about the time it takes to make coffee twice.
Common exam traps in the DAST chapter
☺ Like you're 10: Most of the ways to lose points here aren't about not knowing DAST — they're about a scan that ran, looked fine, and quietly tested almost nothing.
| Trap | What actually happens | What the challenge wants instead |
|---|---|---|
| Treating "I ran ZAP" as "I ran DAST" | Baseline never sends an attack payload — a green baseline proves nothing about exploitability | An active scan (full scan, or Automation Framework activeScan job) against an authorized target |
| Scanning without an auth context | Spider stalls at 3–4 public routes; the report looks complete because there's nothing left to find | A form or script-based auth context with a working verification check |
| Spider clicks the logout link mid-crawl | Session dies partway through; everything scanned after that point silently reverts to unauthenticated coverage | excludePaths on the logout route, plus a verification check that would actually catch this if it happened anyway |
| Only running the traditional spider on a SPA | A React/Vue/Angular app's client-rendered routes are structurally invisible to link-crawling | The AJAX Spider (and an OpenAPI import for any API surface with no HTML at all) |
| Filing every High-risk alert with equal urgency | A High-risk, Low-confidence heuristic gets treated the same as a confirmed exploit, burying the real finding in noise | Risk × confidence triage — verify low-confidence hits by hand before they block anything |
| Active-scanning a target without authorization | Real attack payloads against a system you don't have explicit permission to test | Authorized targets only — staging, a documented test window, or a deliberately vulnerable practice app |
Benny: Baseline scan's green, zero fails. Merging to staging.
Timmy: Green against what, though? Did it ever actually log in?
Benny: ...it hit the login page. That counts, right?
Rocky the Raccoon: That counts as testing your porch. I could still walk straight into the dashboard through three routes it never even saw.
Foxy: So the scan report and the actual app disagree about how big the app even is?
Timmy: Exactly why the auth context and the AJAX spider aren't optional extras. Skip them and you're not scanning less of the app — you're scanning almost none of it.
Once ZAP and Dastardly hand you a stack of alerts, this chapter's job is done and the next one starts: turning risk-and-confidence-triaged noise into a fixed, tracked, closed ticket is exactly what vulnerability management & triage covers. For the API-only surfaces this chapter only touched briefly, API security in depth goes further, and if the hands-on itch isn't scratched yet, Part 6 of the capstone lab is this exact workflow against the pipeline you've been building across the course. Candidates who want to go past what any CDP challenge requires — real exploitation technique, not just scan-and-report — should look at offensive security for DevSecOps next.
1. What's the practical difference between a ZAP baseline scan and a full/active scan, and why does that difference determine which environments each one is allowed to run against? 2. What does an authentication verification check (loggedInRegex/loggedOutRegex) actually protect a DAST run from, and what happens if you skip it? 3. Name two ZAP spidering modes beyond the traditional link-crawling spider, and say what each one exists to cover. 4. A ZAP alert comes back High risk, Low confidence. What's the correct next step, and why is filing it straight into the release-blocking queue the wrong one? 5. What does Burp Suite Dastardly trade away compared to a full ZAP scan, and what does it get in return that makes that trade worth it in CI?
Check your answers
- Baseline only spiders and runs passive scan rules — it never sends an attack payload, so it's safe against almost any environment, including something production-adjacent. A full/active scan sends real attack payloads (SQLi, XSS, path traversal) against every discovered parameter, so it may only run against a target you're explicitly authorized to attack, never an environment you merely have read access to.
- It protects the run from silently continuing on an unauthenticated session after the real one dies mid-scan (idle timeout, CSRF mismatch, a WAF flag). Skip it and ZAP has no way to notice the fallback happened — everything scanned afterward looks like public, unauthenticated coverage even though the report doesn't say so.
- The AJAX Spider, which drives a real headless browser to execute JavaScript and discover client-rendered routes (needed for SPAs the traditional spider can't see), and OpenAPI/GraphQL import, which seeds the sitemap directly from an API spec for surfaces with no HTML to crawl at all.
- Verify it by hand — reproduce it manually — before it goes anywhere near a release-blocking queue. Filing it straight through is wrong because Low confidence means the scanner itself isn't sure it's real; treating an unverified heuristic as equivalent to a confirmed exploit is exactly the kind of noise that trains a team to stop reading DAST reports.
- It trades away configurability — no authentication setup, a fixed non-tunable check-set, less thorough than a full ZAP or Burp Suite Professional scan. In return it gets speed (minutes, not hours) and JUnit XML output that plugs directly into existing CI test reporting, making it a fast, zero-friction complement rather than a full DAST replacement.