CLI & CI/CD
Vooki Pro ships a command-line runner, vooki-pro-cli, so a scan can happen without a
person driving the interface — in a nightly job, in a pipeline, or on a machine with no display. Its exit code
tells the pipeline whether to pass or fail.
How It Works
One idea, and it keeps the whole thing simple: the config file is the single source of truth. Everything — target, scope, discovery, authentication, policy, throttling, report formats, the security gate — is a key in a JSON file. There are no dozens of flags to remember, and the same config always produces the same scan.
A run does three things, in this order:
- Checks the licence — this machine must be activated, and the target must sit inside a licensed domain.
- Runs the scan — the same engine the desktop app uses, with the same checks.
- Writes the reports and sets the exit code —
0pass,2the security gate failed,1something went wrong.
Getting the CLI
Every desktop build ships the CLI beside the app, and there is a standalone package for build servers that should not have the full desktop install. The standalone package carries its own runtime — Python and Node.js are not required — and contains no interface, no browser engine and no AI model.
The installer puts vooki-pro-cli.exe next to the app, and offers an optional
Add CLI to system PATH task — it is unchecked by default, so tick it if you want the short
command:
& "C:\Program Files\Vooki Pro\vooki-pro-cli.exe" --version
For a build agent, unzip the standalone package instead and keep the whole folder together:
Expand-Archive vooki-pro-cli-6.0.0-windows-x64.zip -DestinationPath C:\tools
& C:\tools\vooki-pro-cli\vooki-pro-cli.exe --version
The CLI lives inside the app bundle:
"/Applications/Vooki Pro.app/Contents/MacOS/vooki-pro-cli" --version
For a build agent, extract the standalone package and keep the whole folder together:
tar -xzf vooki-pro-cli-6.0.0-macos-arm64.tar.gz -C /opt
/opt/vooki-pro-cli/vooki-pro-cli --version
Extract the standalone package and keep the whole folder together — the executable loads its libraries from beside it:
tar -xzf vooki-pro-cli-6.0.0-linux-x86_64.tar.gz -C /opt
/opt/vooki-pro-cli/vooki-pro-cli --version
The desktop build also contains the CLI, but inside an AppImage it only exists on a temporary mount that disappears when the app closes — on a server, use the standalone package.
Licence & Activation
A CLI scan needs an active licence on the machine it runs on, exactly like the desktop app. Activate the machine once and every later run is silent about it. These commands use the short name — if the CLI is not on your PATH, use the full path shown above instead.
$env:VOOKI_LICENSE_KEY = "XXXX-XXXX-XXXX-XXXX"
vooki-pro-cli license activate
vooki-pro-cli license status
VOOKI_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX vooki-pro-cli license activate
vooki-pro-cli license status
VOOKI_LICENSE_KEY=XXXX-XXXX-XXXX-XXXX vooki-pro-cli license activate
vooki-pro-cli license status
license activate reads the key from the VOOKI_LICENSE_KEY environment variable, or from
standard input if you prefer to pipe it. The key is never printed. Activating in the desktop app on the same
machine works just as well.
license status prints the plan, expiry, domain slots and whether CI/CD export is included — a quick
way to check an agent before you wire up a pipeline.
The activation is stored in that user's home folder. If your CI agent runs as a service account, activate while logged in as that account — activating as yourself will not license the agent.
Licensed Domains
A licence grants a number of domain slots. A slot covers a registrable domain and everything
beneath it — example.com also covers staging.app.example.com — and all local or private
targets share one slot. The scan target must fall under a bound slot.
Assigning a slot is permanent for the life of the subscription, so the CLI never does it on its own — the same reason the desktop app asks you to confirm first:
| Situation | What happens |
|---|---|
| Target is covered by a bound slot | The scan runs. |
Target is not bound, no --bind-domain | Exit 1, naming the domain and how many slots are free. |
Target is not bound, --bind-domain given, a slot is free | The slot is assigned, then the scan runs. |
| No free slot | Exit 1. Your licensed limit is never exceeded. |
So the first pipeline run for a new domain is either done once by hand, from Settings → Licensed Domains, or with the flag:
vooki-pro-cli --config scan.json --bind-domain # first run for a new domain
vooki-pro-cli --config scan.json # every run after that
Anything the scan discovers on a host your licence does not cover is skipped rather than attacked, and the run says so.
Getting a Config File
Export it from the interface. This is by far the easier route, because the correct method, URL, headers, cookies and body are filled in for you:
- Open Scans → New Scan for a website, or the API Builder for a single API request.
- Configure it exactly as you would for a normal scan — target, auth, discovery, policy.
- Step through to the final Review step.
- Click Export Config and choose where to save the
.jsonfile.
That file is ready to run. Add the CI-only keys — the security gate and any extra report formats — by hand, or pass them on the command line.
Config Template
Writing one by hand is also fine. Only target and requests are required; every other key
is optional and shown here with its default:
{
"target": "https://example.com",
"project_name": "My Project",
"report_format": "html",
"report_type": "standard",
"compliance_framework": "owasp",
"fail_on_severity": "none",
"export_scan_data": false,
"export_scan_data_path": "",
"export_scan_data_redact_credentials": false,
"speed": "normal",
"scan_types": ["all"],
"testing_mode": "full",
"concurrency": 5,
"delay_ms": 0,
"requests_per_second": 7,
"max_payloads_per_type": 0,
"skip_timing": false,
"ai_detect": false,
"attack_include": [],
"attack_exclude": [],
"scope": "same_host",
"scope_hosts": [],
"discovery": "quick",
"crawl_depth": 2,
"max_urls": 0,
"crawl_max_duration_sec": 0,
"discover_background_urls": false,
"discovery_include": [],
"discovery_exclude": [],
"test_state_changing": false,
"auth": {
"headers": { "Authorization": "Bearer ${DAST_TOKEN}" },
"cookie": "session=${SESSION_COOKIE}"
},
"requests": [
{
"url": "https://example.com/login",
"method": "POST",
"headers": { "Content-Type": "application/json" },
"body": { "username": "admin", "password": "${APP_PASSWORD}" }
},
{
"url": "https://example.com/search?q=test",
"method": "GET"
}
]
}
The keys that matter most for a pipeline are fail_on_severity (the security gate),
report_format as a list, and the ${...} placeholders in auth — all three
are covered below.
Key Settings
Report output — report_format takes html (default), json,
sarif, docx or pdf, and accepts a list so one run can
produce several: "report_format": ["json", "sarif", "html"]. Set report_type to
compliance and add compliance_framework — one of owasp,
pci_dss, nist, iso27001, hipaa, gdpr or
sox — for a framework-mapped report. JSON and SARIF are machine-readable and always come out as
themselves. Compliance reports are HTML or PDF only: ask for docx with a compliance report and the CLI
writes HTML instead, with a warning.
Production Safe — "testing_mode": "passive" runs the non-intrusive checks only:
responses the scan already had to fetch, and requests a normal browser would make anyway. No payloads, no guessed
paths, no state changes — the mode for production targets (it still sends traffic to the target). full (the default) is the complete active
scan. An unrecognised value stops the run rather than quietly scanning wider than you asked.
Speed — speed is a shortcut for three presets:
stealth— 2 parallel requests, 2 second delay. Avoids WAF alerts.normal— 5 parallel requests, 500 ms delay. A good default.aggressive— 10 parallel requests, no delay. May trip WAF blocks.
Setting concurrency, delay_ms or requests_per_second directly overrides the
preset; the larger effective delay wins.
Which checks run — "scan_types": ["all"] runs every class. To narrow it, list groups
by name:
"scan_types": ["sqli", "xss", "lfi", "ssrf"]
Scope and discovery — scope takes all, same_host,
same_domain or custom (with scope_hosts). discovery takes
quick (only the listed requests), crawl, or hybrid, tuned with
crawl_depth, max_urls, crawl_max_duration_sec,
discover_background_urls and the discovery_include / discovery_exclude
wildcards. requests must list at least one seed URL even when crawling.
Saving the scan data — a report is a document; the scan data is the scan itself, every finding plus the HTTP request and response behind it. Set:
"export_scan_data": true,
"export_scan_data_path": "",
"export_scan_data_redact_credentials": false
An empty path saves to your Downloads folder. Give a folder and the file is named after your config; give an exact
.vpscan path and it is used verbatim. There is no command-line flag for this — the config decides, so
the same config always produces the same files. The scan data is written even when a scan finds nothing, and the
full path is printed when the scan ends.
The archive keeps the credentials the scan sent, because a retest has to replay them. If you publish it as a
build artefact, set export_scan_data_redact_credentials to true — the header names stay,
the values are replaced. Authenticated retests of that imported scan then need fresh credentials.
Secrets & Environment Variables
Never commit a token. Write a placeholder in the config and let the CLI resolve it from the environment when the scan runs:
| Syntax | Meaning |
|---|---|
${NAME} | The value of NAME. Unset or empty is a configuration error, so a missing secret stops the run instead of silently scanning unauthenticated. |
${NAME:-default} | NAME, or default when it is unset or empty. |
$${ | A literal ${. |
Placeholders work in any string value, anywhere in the config. Names must look like [A-Za-z_][A-Za-z0-9_]*,
so template payloads such as ${7*7} in a request body are left alone, and a bare $NAME is
never expanded — passwords containing $ keep working. Add "env_substitution": false at
the top level if a config genuinely needs literal ${NAME} text.
$env:DAST_TOKEN = "eyJhbGciOi..."
vooki-pro-cli --config scan.json
DAST_TOKEN="eyJhbGciOi..." vooki-pro-cli --config scan.json
DAST_TOKEN="eyJhbGciOi..." vooki-pro-cli --config scan.json
Credentials are kept out of everything the run produces. Values in the auth block, credential headers
such as Authorization and Cookie, and tokens minted by API auto-login are replaced with
[REDACTED] in the console output and in every report.
Running a Scan
--config is the only required parameter:
& "C:\Program Files\Vooki Pro\vooki-pro-cli.exe" --config scan.json --output reports\ --fail-on high
"/Applications/Vooki Pro.app/Contents/MacOS/vooki-pro-cli" --config scan.json --output reports/ --fail-on high
/opt/vooki-pro-cli/vooki-pro-cli --config scan.json --output reports/ --fail-on high
Every parameter:
| Parameter | What it does |
|---|---|
--config FILE | The scan config JSON. Required. |
--output PATH | A file, a base name, or a directory (existing, or ending in a separator) for the reports. Missing folders are created. Defaults to next to the config. |
--fail-on LEVEL | The security gate, overriding the config: none, info, low, medium, high or critical. |
--bind-domain | Assign a free licensed-domain slot to the target if it is not licensed yet. |
--verbose | Print all scan progress messages, not just findings. |
--version | Print the CLI version. |
There are also two sub-commands: license status / license activate (above), and
retest, which re-checks findings already stored on that machine.
Findings print to the terminal as they are found, colour-coded by severity, and the reports are saved when the scan finishes.
The Security Gate & Exit Codes
This is what makes a pipeline fail on its own. Set a threshold and the run exits 2 when anything at
that severity or above is found:
"fail_on_severity": "high"
Or on the command line, which wins over the config — handy when one config serves several stages:
vooki-pro-cli --config scan.json --fail-on critical
| Value | The run fails when it finds |
|---|---|
none (default) | Never — the gate is off, and a completed scan always exits 0. |
info | Anything at all. |
low | Low, Medium, High or Critical. |
medium | Medium, High or Critical. |
high | High or Critical. |
critical | Critical only. |
The exit code is the contract your pipeline reads:
| Code | Meaning | Pipeline |
|---|---|---|
0 | The scan completed and the gate passed, or no gate was configured. | Pass |
1 | Something went wrong, so there is no trustworthy result: no licence or an unlicensed target, a bad config or an undefined ${VARIABLE}, an unreachable target, nothing left to scan, or a report that could not be written. | Fail — tool error |
2 | The scan completed and the gate failed. | Fail — vulnerabilities |
If every request fails, or the filters leave nothing to scan, the run exits 1 rather than reporting a
clean bill of health. "No findings" always means the scan actually ran.
Reports & Artefacts
A report is written for every completed scan, including a clean one — so a missing report can only mean the run failed.
| Format | File | Use it for |
|---|---|---|
json | .json | The machine-readable result: severity counts, the gate verdict and every finding. |
sarif | .sarif | SARIF 2.1.0, for security tooling that ingests it. Needs CI/CD Export on your licence. |
html | .html | The readable report to publish or send on. |
docx | .docx | A Word document for report packs. |
pdf | .pdf | Needs the bundled browser; the standalone CLI package writes HTML instead, with a warning. Don't make PDF a pipeline requirement. |
With several formats, --output is a base name and each report adds its own extension; point it at a
directory and the reports are named after the config file. The JSON report carries everything a pipeline needs to
make its own decision:
"metadata": {
"total_findings": 7,
"severity_counts": { "Critical": 0, "High": 2, "Medium": 3, "Low": 1, "Info": 1 },
"scan_status": "completed",
"requests_total": 42,
"requests_failed": 0,
"security_gate": {
"threshold": "high",
"failed": true,
"blocking_findings": 2
}
}
All five severities are always present, so a consumer never has to treat a missing key as zero.
In a Pipeline
The runner is a plain executable that reads a file and writes files, so it drops into Jenkins, GitHub Actions, GitLab CI or Azure Pipelines without special support. There is no plugin to install. A typical arrangement:
- Run it on a persistent self-hosted agent, activated once. Hosted, throwaway agents get a fresh machine every run and have no activation.
- Keep the exported config in the repository, next to the code it tests, with
${...}placeholders instead of credentials. - Map your CI secret into the environment for the scan step only.
- Set a gate that matches the stage —
criticalon a pull request,highon nightly. - Ask for
["json", "html"](plussarifif your licence includes it): JSON for the machine, HTML for the human who has to read it. - Use a tight policy so the scan finishes inside your build timeout — a Deep policy does not belong in a pull-request check. Run the heavier policies nightly against staging.
- Publish the reports whether the gate passed or failed.
Azure Pipelines
A complete nightly pipeline. The step's exit code fails the run on its own — no parsing required:
trigger: none
schedules:
- cron: "0 2 * * *"
displayName: Nightly DAST
branches:
include: [main]
always: true
pool:
name: Self-Hosted-Vooki # an agent activated with `license activate`
variables:
- group: vooki-dast # holds DAST_TOKEN as a secret variable
- name: VOOKI_OUT
value: $(Build.ArtifactStagingDirectory)/vooki
steps:
- checkout: self
# Fails early with a clear message if the agent lost its activation.
- bash: |
set -euo pipefail
mkdir -p "$(VOOKI_OUT)"
vooki-pro-cli --version
vooki-pro-cli license status
displayName: Check Vooki licence
# Secret variables reach a script only when mapped into env:.
# The CLI resolves ${DAST_TOKEN} itself and never prints it.
- bash: |
vooki-pro-cli --config security/scan.json --output "$(VOOKI_OUT)/" --fail-on high
displayName: Vooki DAST scan
env:
DAST_TOKEN: $(DAST_TOKEN)
# Publish the reports whether the gate passed or failed.
- task: PublishPipelineArtifact@1
condition: succeededOrFailed()
inputs:
targetPath: $(VOOKI_OUT)
artifact: vooki-dast
On a Windows agent, swap the scan step for a PowerShell one:
- powershell: |
& "C:\Program Files\Vooki Pro\vooki-pro-cli.exe" `
--config security\scan.json `
--output "$(Build.ArtifactStagingDirectory)\vooki\" --fail-on high
exit $LASTEXITCODE
displayName: Vooki DAST scan
env:
DAST_TOKEN: $(DAST_TOKEN)
When it detects Azure Pipelines, the CLI also writes gate failures and fatal errors as
##vso[task.logissue] lines, so they appear on the run summary. If you would rather mark the run
"partially succeeded" on a gate failure than fail it outright, read the exit code yourself: 2 is the
gate, 1 is a real error.
Bringing Results Back
Set export_scan_data to true and the pipeline produces a .vpscan archive
alongside the reports. Pull that file onto a workstation and open it with Scans → Import: it
appears like any scan you ran locally, with all findings and evidence intact, ready for
triage and retest.
Good to Know
- Persistent agents only. The CLI uses the machine's own activation, so hosted, throwaway build agents are not supported yet.
- SARIF needs CI/CD Export on the licence. Without it the run stops with a clear message instead of quietly skipping the file.
- The gate counts every finding in the run, not just new ones. There is no baseline comparison yet.
retestneeds the local scan database, so it belongs on a workstation or a persistent agent that keeps its Vooki data — not in a throwaway pipeline job.- The standalone package has no browser engine, so PDF output and background XHR discovery
(
discover_background_urls) are unavailable there; the desktop build has both. - A WAF can quietly limit coverage. If it blocks the scanner, the run can finish with few findings — check the scan's coverage, not just the exit code.