For the complete documentation index, see llms.txt. This page is also available as Markdown.

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.

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.

Linux / macOS
Windows
Effect

-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 legacy docker-compose v1 binary is not supported, and Podman is not a substitute: the wrapper drives docker compose, which Podman does not provide.

  • A directory on your PATH to 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.

Verify:

Verify:

Verify (open a new PowerShell window after persisting):

The Windows wrapper is a PowerShell script (xy-dast.ps1) signed with an Authenticode certificate. If your execution policy blocks running scripts, run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned once.

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); install copies 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, and shm_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 --profile file) 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-dir that contains all your inputs, or pass URLs instead.

  • It looks for the sidecar at <wrapper-dir>/xy-dast-compose.yml by default. Override the location with the XY_DAST_COMPOSE_FILE environment variable. To pin a different image, edit the image: line in the sidecar or re-run install --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.

The wizard never asks you to type a password or token. Where a credential is needed it asks for the name of the environment variable holding it and writes ${env:VAR} into the profile, so the generated files are safe to commit.

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 (-u or --url) -- the base URL of the application to scan (required).

  • Tech-stack profile (-p or --profile) -- what the target is: traditional, spa, openapi, graphql, soap, or cms (or a custom profile).

  • Scan intensity (--intensity) -- how hard to scan: quick, balanced (default), deep, or passive. 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 (-pm or --postman) -- URL or file path to a Postman v2.x collection; --postman-vars overrides 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-only scans only the recorded endpoints.

  • Output file (-o or --output) -- path for the JSON report. Use - for stdout.

  • Project name (-n or --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-rules to skip noisy rules, or --risk-threshold to 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:

Service
Best for
Reachability

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-threads is 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-threads over 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, or 503 with Retry-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.

Property
Meaning

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.

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" and Name=Value are accepted.

  • env:VAR_NAME reads the value from an environment variable, keeping a token out of the command line and the process list.

  • Naming X-Xygeni-DAST changes 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-Authorization and Cookie are 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.

Tech base
Best for
Crawl
Endpoint import

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.

Intensity
Attack strength
CVE check
Deep crawl
Active scan
Pacing

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:

Goal
Command

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

If no --intensity is given, balanced is used. Passing an intensity name directly to --profile (e.g. --profile deep) is accepted as shorthand for the traditional base at that intensity.

List all available profiles (including custom ones):

Custom profiles can be placed in $XYGENI_DAST_DIR/profiles/, ./profiles/, or ./conf/profiles/. See DAST Scanner Configuration for the full profile schema and examples.

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.

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.

DAST findings reference HTTP URLs rather than source-tree files, so they appear in the Security tab but do not produce inline PR annotations. This is a known limitation of all DAST tooling that emits SARIF.

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

Code
Description

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

Variable
Description
Default

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