DAST Scanner
Table of Contents
Purpose
The DAST Scanner (xy-dast) performs automated dynamic security testing of running web applications and REST APIs. It tests applications from the outside -- simulating real-world attacks against live endpoints -- to identify vulnerabilities that are only exploitable at runtime.
The scanner supports different application types:
Traditional server-rendered web apps (PHP, JSP, ASP.NET)
SPA JavaScript-heavy Single Page Applications (React, Angular, Vue)
REST API with OpenAPI/Swagger specifications or Postman collections
GraphQL APIs (schema import or introspection)
SOAP web services (WSDL import)
CMS platforms (WordPress, Drupal, Joomla)
You choose what the target is and how hard to scan it on two independent axes: a tech-stack profile (--profile) and a scan intensity (--intensity). They compose freely — e.g. a deep scan of an SPA is --profile spa --intensity deep. See Built-in Scan Profiles.
Installation
The DAST scanner is distributed as a Docker image (xygeni/xy-dast). Install a lightweight wrapper directly from the image — no separate download is needed. The wrapper is a small, signed script that delegates to docker compose run, so once installed you invoke xy-dast as if it were a native command.
One-command install (recommended)
From 6.14.0 there is an installer that does all of the below for you: it finds Docker, pulls the image, verifies its signature, creates the install directory, and pins the wrapper to the exact image digest it verified. On a host with only Podman it stops and tells you to install Docker, rather than installing a wrapper that could not run.
Installs into the first writable of $HOME/.local/bin or a $HOME/bin already on your PATH, falling back to $HOME/.local/bin.
Installs into $HOME\.local\bin unless -InstallDir says otherwise.
-d <dir>
-InstallDir <dir>
Install somewhere other than the default
-i <tag|sha256:digest>
-Image <tag|sha256:digest>
Pin a version instead of latest
--add-to-path
-AddToPath
Add the install directory to your shell profile / user PATH
--prune
-Prune
Also delete xy-dast wrappers found elsewhere on PATH
--allow-unverified
-AllowUnverified
Proceed when no verifier is available
To check the installer itself before running it, compare it against the digest published in the xygeni/xygeni repository:
The digest lives in a different place from the script it vouches for: an attacker would have to compromise both get.xygeni.io and the GitHub repository to pass this check.
A signature that fails verification always stops the install. If no verifier is available at all (no cosign, no Docker-run fallback), the install reports that it could not verify and continues only when you pass --allow-unverified.
Keeping it up to date
update verifies the new image exactly as the installer does, so it fails rather than re-pin to something it could not check. --allow-unverified covers the case where no verifier is available; a signature that fails is always fatal.
update moves the wrapper and the image together. That matters: the two speak a versioned calling contract, and an image newer than its wrapper refuses to run rather than mis-resolve your local file paths — which used to surface as a confusing "file not found" for a file that was plainly there.
Manual install
The step-by-step route below works with any released image, and is the one to use where the installer cannot reach the internet.
Requirements
Docker Engine 20.10+ (or Docker Desktop) with Compose v2 — i.e. the
docker compose ...subcommand. The legacydocker-composev1 binary is not supported, and Podman is not a substitute: the wrapper drivesdocker compose, which Podman does not provide.A directory on your
PATHto drop the wrapper into. This guide uses~/.local/bin(Linux/macOS) and%USERPROFILE%\.local\bin(Windows).
Step 1 — Create the install directory and ensure it is on your PATH
This is the most common source of "command not found: xy-dast" issues. The install directory must exist before the install command (Docker creates it as root if it does not, which then fails to write), and it must be on your PATH for the short xy-dast command to work.
On Windows and macOS the ~/.local/bin directory is not on the default PATH. On most Linux distributions it is added by ~/.profile, but only if the directory exists at login — if you create it now in an existing shell, you still need to add it to PATH for the current session (or open a new login shell after creating it).
Verify:
Verify:
Verify (open a new PowerShell window after persisting):
Step 2 — Install the wrapper from the Docker image
The image's install subcommand drops two files into the mounted directory: the xy-dast wrapper script itself and a sidecar xy-dast-compose.yml that holds the image reference, environment forwarding, and runtime parameters.
--powershell produces the signed .ps1 wrapper (and matching sidecar) instead of the bash one.
Verify:
If you get command not found (or not recognized as ... cmdlet), revisit Step 1 — the directory is almost certainly not on your PATH yet.
Quick install vs. secure install
The plain install command above is a quick install: convenient for desktop and ad-hoc use. The wrapper itself is signed at release time, but the image reference written into xy-dast-compose.yml is a mutable tag (e.g. xygeni/xy-dast:6.7.0).
For production environments — and any setting that needs defence-in-depth against registry-side supply-chain attacks — use the secure install flow, which pins the image to its immutable digest and verifies the cosign (Sigstore keyless) signature before installing:
To upgrade later, repeat the four-step flow with the new version — or run xy-dast update, which pulls, verifies and re-pins the wrapper and the image together.
How the wrapper works
It is a pre-built, byte-stable script signed at release time (Authenticode for
.ps1);installcopies it byte-exact so the signature is preserved.It delegates to
docker compose -f xy-dast-compose.yml run --rm xy-dast …. Compose handles env forwarding (XYGENI_TOKEN,XYGENI_URL,XYGENI_DASHBOARD_URL,XYGENI_DAST_DIR),network_mode: host, andshm_size: 2gb.When you pass
-o <file>, the wrapper mounts the output directory into the container (read-write) so the report appears on your host filesystem.Local input files (
--openapi,--postman,--graphql,--wsdl,--url-list,--client-cert,--auth-config,--session-file, and a--profilefile) are read from a base directory — your current directory by default, or set explicitly with--base-dir. It is mounted read-only, so paths must live under it; relative paths resolve against it. This also reaches files referenced inside a profile or auth config. Use a--base-dirthat contains all your inputs, or pass URLs instead.It looks for the sidecar at
<wrapper-dir>/xy-dast-compose.ymlby default. Override the location with theXY_DAST_COMPOSE_FILEenvironment variable. To pin a different image, edit theimage:line in the sidecar or re-runinstall --image <ref>.
Quick Start
Let the scanner write your configuration
From 6.14.0, if you would rather be asked than read the options, run the guided setup:
It asks about your target and writes a profile YAML, a ready-to-run command line, or both. Only relevant questions are asked — answering "REST API" skips the crawling questions — and every question has a default, so pressing Enter throughout still produces a working configuration. It can also probe the target and propose the profile matching what it finds.
Where a question offers a list, move with the arrow keys or Tab, press 1-9 to take an option outright, and Enter to accept the highlighted one. Single keys carry the commands: b back, s skip, d defaults for everything remaining, q quit without writing, ? help. Esc swaps the list for a typed prompt.
At a typed question the same commands are written with a colon — :b, :s, :d, :q — and ? still asks for help. The keys in use are always shown under the question, so there is nothing to memorise.
Scan a web application:
Results are uploaded to the Xygeni platform by default. To save a local report instead, use -o:
Scan a REST API with an OpenAPI specification:
Scan a REST API from a Postman collection (v2.x). Use a local file or a URL, and override collection variables with --postman-vars:
Scan a Single Page Application:
Scan a GraphQL API:
Scan a SOAP web service:
Scan a REST API with OAuth2 (client credentials). The scanner obtains a token from the token endpoint before the scan and sends it as a bearer token on every request:
The password and refresh_token grants and advanced options are configured via a profile.
Seed the scan from traffic you have already recorded with --navigation, so endpoints that crawling alone would miss are scanned too. The format is auto-detected: a recorded browser navigation — a Selenium IDE (.side) or a Chrome DevTools Recorder (.json) recording — is replayed in a real browser to generate the traffic; a recorded HAR (from browser DevTools, a proxy, or a test tool) is imported directly:
Use --navigation-format selenium|chrome-devtools|har to set the format explicitly when the extension is ambiguous (for example, a .json that is neither a DevTools recording nor a HAR).
A .side given to --navigation is always replayed and seeded — it is a navigation input, not a login method, so the profile script.seedNavigation setting does not apply to it (that setting gates a scripted login .side; see Scripted Browser Login). The navigation script and an optional scripted login are separate inputs: when both are given, a .side navigation is replayed in the same authenticated browser so it can reach gated pages. Only requests on the target's host are kept. Add --navigation-only to scan only the recorded endpoints (skipping the crawl).
Usage
The DAST Scanner is launched using the xy-dast scan [options] command.
To view all available options, use the --help flag:
The most important options are:
Target URL (
-uor--url) -- the base URL of the application to scan (required).Tech-stack profile (
-por--profile) -- what the target is:traditional,spa,openapi,graphql,soap, orcms(or a custom profile).Scan intensity (
--intensity) -- how hard to scan:quick,balanced(default),deep, orpassive. Overlays attack strength, durations, and CVE / deep-crawl on top of the tech profile.OpenAPI spec (
--openapi) -- URL or file path to an OpenAPI/Swagger specification (for REST API scans).Postman collection (
-pmor--postman) -- URL or file path to a Postman v2.x collection;--postman-varsoverrides its variables (key=value, comma-separated).Recorded traffic (
--navigation) -- seed the scan from a recorded browser navigation (.side, replayed) or a recorded HAR (imported);--navigation-onlyscans only the recorded endpoints.Output file (
-oor--output) -- path for the JSON report. Use-for stdout.Project name (
-nor--project-name) -- identifies the project in the Xygeni platform.Upload -- reports are uploaded to the Xygeni backend by default. Disable with
--no-upload.Filtering -- use
--exclude-rulesto skip noisy rules, or--risk-thresholdto set a minimum severity.
Custom Scan Settings
Override timing defaults directly from the command line:
Durations accept 30s / 5m / 1h (a bare number is minutes). A duration of 0 means unlimited (no timeout for that phase, or no overall cap for --timeout) — it never means "skip".
To disable a phase, use an explicit flag — --passive-only (alias --skip-active-scan), --skip-spider, or --skip-ajax-spider:
For a production-safe scan packaged as a profile, use --intensity passive (crawl + passive analysis, no active attack payloads) with any tech base: xy-dast scan -u https://app.example.com --profile spa --intensity passive.
Filtering Results
Deep Crawl
The --deep-crawl option runs a headless browser-based crawler before the main scan. It discovers URLs that traditional spiders miss, especially in JavaScript-heavy applications where content is rendered dynamically.
Discovered URLs are fed as seed URLs into the scanner's spider, improving coverage.
You can tune the crawl depth and timeout:
The deep intensity (--intensity deep) enables deep crawl by default.
Vulnerability Check
The --vuln-check option runs a template-based vulnerability scanner after the main scan completes. It checks the discovered endpoints against thousands of known CVEs, misconfigurations, and exposures — complementing the active scanning with signature-based detection.
Both features can be combined for maximum coverage:
Vulnerability check findings appear in the same report as regular scan findings, with detector IDs prefixed by vuln/ (e.g., vuln/CVE-2021-44228). See DAST Detectors for details.
You can filter by severity and control the scan rate:
The deep intensity (--intensity deep) enables vulnerability check by default.
Out-of-Band Detection (OAST)
Some vulnerabilities never surface in the HTTP response — the proof is a side effect: the target opens a connection to a server you control. Blind SSRF, blind XXE, Log4Shell, blind SSTI and out-of-band XSS are detected this way. The scanner injects a payload and, if the target is vulnerable, it calls back to an OAST server (out-of-band application security testing). These rules are inert unless an OAST service is configured, so it is off by default — enable it explicitly:
Choosing a service. An OAST callback only works if the target can reach the server and the scanner can observe the hit:
callback
Internal targets on the same network as the scanner
Target must route back to the scanner container
boast
Public targets (zero-config public server)
Target needs outbound internet egress
interactsh
Public or self-hosted
Public egress, or a self-hosted server both reach
callback is the simplest and most reliable choice for internal targets and detects the HTTP-based out-of-band rules (blind SSRF, blind XXE). For a self-hosted OAST — the option for internal targets with no public egress that cannot route back to the scanner — run an Interactsh server both the target and the scanner can reach, and point --oast at it. Tokens are read from the environment (env:VAR / ${env:VAR}) and never logged. See DAST Scanner Configuration for the profile block, per-rule detection notes, and self-hosting guidance.
Automatic Profile Selection
If you are unsure which profile fits your target, use --auto-profile to let the scanner probe the application and select the most appropriate profile:
This detects the technology stack (e.g., React SPA, REST API with OpenAPI, or a WordPress/Drupal/Joomla site → cms) and selects the corresponding tech base. If --profile is also specified, it takes precedence. --intensity still applies on top of the auto-detected base — e.g. --auto-profile --intensity deep.
To see the available profiles:
Incremental Scans
Re-test only the endpoints that changed since a baseline — driven by Xygeni's API Security scan — for a large wall-time saving on pull-request pipelines, then merge with the previous report so the output is still a complete snapshot. Off by default; enable with --incremental. See Incremental DAST Scanning for the full workflow, the --baseline-report merge, and the full-scan fallback.
Rate Limiting
By default a scan runs as fast as the target accepts. Against production or rate-sensitive targets that can degrade the service, trip a WAF, or get the scanner's address banned — so a scan can be told how hard to push:
The limit applies to every phase — active scan, crawl, deep crawl and vulnerability check alike. --rate-limit and --request-delay are two spellings of one setting (--rate-limit 5 is --request-delay 200); set whichever fits, not both, and each phase uses the form it can honour exactly.
Every request is paced on a single shared schedule, so the figure you set is a ceiling for the scan as a whole rather than a per-phase or per-thread one. Sustained throughput sits below the ceiling — a scan spends time waiting on responses and between phases — while the peak in any one second stays at the rate you configured, which is what matters for a fragile target.
Two things worth knowing:
--scan-threadsis the only lever the crawl has besides pacing. The crawler exposes no per-request delay of its own, so concurrency bounds how many requests it can have in flight. Setting it also lowers the active scan's threads per host.Pacing applies to browser-driven crawling too. Under AJAX spidering, page resources are fetched through the same schedule, so a low rate slows page loads as well as navigation. On a JS-heavy application prefer bounding
--scan-threadsover setting a very low rate.
The vulnerability check can be exempted so a careful overall pace does not hold back the CVE sweep:
Only that flag exempts it. A profile's vulnCheck.rateLimit sets the sweep's speed when nothing was asked for on the command line, but never overrides an explicit --rate-limit, --request-delay, or a limit discovered adaptively — a file's default must not hold the CVE sweep at full speed against a target the rest of the scan is treading carefully around.
Letting the target set the pace. Where the target states its own limit, --adaptive-rate-limit uses that instead of a number you had to guess:
Probes first for advertised rate-limit headers (
RateLimit-*,X-RateLimit-*) and adopts what it finds, keeping a safety margin below the stated ceiling so a shared counter or a misaligned window does not push the scan over it.Backs off during the scan when the target answers
429, or503withRetry-After, waiting as long as it asked and easing back up once it stops complaining.Slows before being blocked, stretching the remaining quota to last until the limit resets rather than sprinting into a wall of rejections.
An explicit --rate-limit or --request-delay always wins: adaptive mode fills in a limit you did not state, it does not override one you did.
A throttled request tests nothing. Without a signal, a scan that was rate-limited into near-silence looks exactly like a scan of an application with nothing wrong with it. A throttled scan therefore warns when the target pushed back, and records three report properties:
dast.ratelimit.paced matters most in CI: those responses are all successes, so a target advertising a nearly-exhausted quota can hold a scan to a crawl while refusing nothing at all. Gate on non-zero, not on the precise value — the counts are lower bounds, since reporting is batched. On a scan with no limit at all the properties are absent rather than zero; absence means "not measured", not "never throttled".
dast.ratelimit.throttled
Responses the target refused (429, or 503 with Retry-After)
dast.ratelimit.paced
Successful responses whose rate-limit headers slowed the scan down
dast.ratelimit.enforced
false when throttling was requested but never took effect
Two intensity overlays set pacing by default: deep throttles (10 req/s, 2 threads) because it is long-running and trades throughput for politeness, and passive turns on adaptive mode without imposing a fixed limit, so a target that never pushes back is still scanned at full speed. quick and balanced leave pacing unset. The equivalent profile block is rateLimit — see Rate Limiting in the configuration reference.
Identifying the Scan's Traffic
Every scan stamps one header on every request it sends, so the team that owns the application under test can tell the scanner's traffic apart in their own access logs:
The presence of the header identifies DAST traffic; the value identifies one particular scan and states when it started (scan-<yyyyMMdd>-<HHmmss>, in UTC). The value is stamped once per run, so every request belonging to a scan carries the same one, and it is recorded in the report as the dast.request.marker property — a report and the target's logs can therefore be lined up from either side.
Every phase that talks to the target sends it: the crawl, the active scan, the deep crawl, the vulnerability check, the pre-flight probes, and the browser that replays a recorded login or navigation.
To pick the traffic out of an access log:
Your log format has to record the header for this to work — most servers omit unknown request headers by default. In nginx it is available as $http_x_xygeni_dast, and in Apache as %{X-Xygeni-DAST}i.
Filtering by source IP is not an alternative the scanner can offer: the requests come from the host running the scanner, every phase shares that address, and in CI it is usually a shared NAT pool.
The marker identifies traffic, it does not authenticate it. The header name is published and its value is a plain timestamp, so anyone can send it. Use it to recognise traffic — correlating a scan with your logs, tagging entries in a SIEM, explaining a traffic spike after the fact — and never to decide what happens to traffic. Suppressing alerts, dropping requests, allowlisting through a WAF, or skipping authorization for requests that carry it all build a filter an attacker can put themselves inside.
Where a scan genuinely needs different treatment, gate it on something the sender cannot choose — the scanner's source address, allowlisted for the duration of the engagement. If a header is also wanted, give the marker a value only you and the target know, with --request-header "X-Xygeni-DAST: env:SCAN_SECRET", and rotate it as you would any credential.
Custom Request Headers
--request-header adds any other header to the same set — a partner id, a tracing header, or a token an API gateway in front of the target requires:
Repeatable. Both
"Name: Value"andName=Valueare accepted.env:VAR_NAMEreads the value from an environment variable, keeping a token out of the command line and the process list.Naming
X-Xygeni-DASTchanges the marker's value. Giving it an empty value —--request-header "X-Xygeni-DAST:"— suppresses the header entirely, for a target that rejects unknown headers.These are not authentication headers. They combine with every authentication method, including form login, and a header that collides with the one the authentication injects is ignored rather than overriding it.
Authorization,Proxy-AuthorizationandCookieare rejected outright: setting one would replace the scan's own credentials or session rather than add to them, leaving the scan running unauthenticated behind a report that looks clean. Use the authentication options for those.
The equivalent profile block is requestHeaders — see Identifying Scan Traffic in the configuration reference.
Built-in Scan Profiles
Profiles have two independent axes — pick a tech base with --profile and a scan intensity with --intensity, and combine them freely.
Tech base (--profile) — what the target is. Controls the crawl, endpoint import, and scan policy. Auto-selectable with --auto-profile.
traditional
Server-rendered apps (PHP, JSP, ASP.NET)
Spider + moderate browser crawl
—
spa
JS-heavy SPAs (React, Angular, Vue)
Heavy browser (AJAX) crawl
—
openapi
REST APIs with an OpenAPI spec
Minimal spider, no browser
OpenAPI spec (API-Scan policy)
graphql
GraphQL APIs
Minimal spider, no browser
Schema import / introspection
soap
SOAP web services
Minimal spider, no browser
WSDL
cms
WordPress / Drupal / Joomla
Server-rendered crawl
— (CVE checking on, CMS templates)
Scan intensity (--intensity) — how hard. Overlays attack strength, thresholds, phase durations, and CVE / deep-crawl on top of the tech base.
quick
Low
off
off
on (fast, no browser crawl)
unset
balanced (default)
Medium
off
off
on
unset
deep
Insane (extended durations)
on
on
on
10 req/s, 2 threads
passive
—
off
off
skipped (production-safe, no attack payloads)
adaptive, no fixed limit
Composition recipes:
Deep scan of a REST API
--profile openapi --intensity deep
Fast CI gate on an SPA
--profile spa --intensity quick
Production-safe scan (no attack payloads)
--profile <tech> --intensity passive
Auto-detect the stack, deep intensity
--auto-profile --intensity deep
List all available profiles (including custom ones):
Authentication
The DAST scanner supports authenticated scanning to test areas of the application behind login.
Form-Based Login
Bearer Token
Read the token from an environment variable to avoid exposing secrets in command history:
API Key / Custom Header
For APIs that authenticate via a custom header (e.g., X-API-Key):
HTTP Basic Authentication
For targets protected with HTTP Basic auth (RFC 7617):
Client Certificate (mTLS)
For targets that require mutual TLS, supply a PKCS#12 (.p12 / .pfx) certificate. The password is read from an environment variable and is redacted from any log output:
mTLS is orthogonal to the other authentication methods — combine it with --bearer-token, --api-key-*, --basic-*, or form login when the target requires both transport-level and application-level auth. The certificate path can also be set via the clientCertificate block in a profile YAML.
OAuth2
The scanner obtains a token from the OAuth2 token endpoint before the scan and injects it as a bearer token on every request:
The password and refresh_token grants (and provider-specific options) are configured via the authentication block in a profile YAML — see DAST Scanner Configuration.
For long scans where a short-lived token would expire before the scan finishes, add --token-refresh so the scanner renews the token during the scan and keeps authenticated coverage:
The refresh_token grant and finer token-renewal settings are configured via the profile tokenLifecycle block — see DAST Scanner Configuration.
Pre-authenticated Session Import (SSO)
For OIDC/SAML SSO targets (Okta, Microsoft Entra ID, …), authenticate out-of-band and supply the resulting session cookies/headers — the scanner sends them on every request:
--session-file also accepts a Playwright storageState.json directly — the artifact produced by context.storage_state() in an existing Playwright login script. Its cookies are imported, and a JWT found in local storage is turned into an Authorization: Bearer header, so a session captured by a Playwright test can drive an authenticated scan with no conversion:
Opaque (non-JWT) local-storage tokens are not auto-mapped — the target header is ambiguous — so inject those explicitly with --session-cookie or a header.
Scripted Browser Login (multi-step / advanced)
For logins that simple form/bearer auth cannot express (identifier-first or multi-page flows, JavaScript-gated logins, SSO), provide a recorded Selenium IDE (.side) script via an auth-config file. The scanner drives a real browser through the login and reuses the captured session for the scan:
See DAST Scanner Configuration for capture/extraction options.
Always use env:VAR_NAME syntax for secrets (tokens, passwords, API keys, certificate passwords) rather than passing values directly on the command line, to prevent accidental exposure in shell history and process listings.
CI/CD Integration
Use --quiet for minimal output and --fail-on to gate builds on vulnerability severity:
The --quiet flag outputs a single summary line:
Pipe JSON to stdout for downstream processing:
SARIF output for GitHub Code Scanning
--format sarif produces SARIF v2.1.0 output that GitHub Code Scanning, Azure DevOps, the VS Code SARIF Viewer, and most CI/CD security dashboards ingest natively. Findings appear in the repo's Security → Code scanning tab alongside SAST/SCA results.
The --format flag only affects the file written by -o. The Xygeni backend upload payload (when --no-upload is omitted) is always Xygeni JSON regardless of --format.
Saving raw scan artifacts
Use --keep-details to save the underlying scanner output and the generated automation plan alongside the report. This is the easiest way to debug a scan that does not produce expected findings:
GitHub Actions Example
GitLab CI Example
Command Reference
Exit Codes
0
Scan completed successfully
1
General error
2
Scanner engine not found
3
Scanner engine execution failed
4
Invalid input arguments
128
Alert threshold exceeded (see --fail-on)
Environment Variables
XYGENI_TOKEN
API access token (required for report upload)
--
XYGENI_URL
Xygeni API endpoint
https://api.xygeni.io
XYGENI_DASHBOARD_URL
Xygeni dashboard URL
https://in.xygeni.io/dashboard
XYGENI_DIR
Base directory for logs
Current directory
XYGENI_DAST_DIR
Configuration directory (containing conf/)
Script directory
XY_DAST_COMPOSE_FILE
Override the location of the wrapper's xy-dast-compose.yml sidecar (which holds the image reference, environment forwarding, and runtime parameters)
<wrapper-dir>/xy-dast-compose.yml
PROXY_HOST
Proxy hostname
--
PROXY_PORT
Proxy port
3128
Last updated

