Skip to content

Test packs

File layout

test-packs/
├── juice-shop-detect.yaml
├── http-rich-test.yaml
├── csrf-flow-test.yaml
└── headers-basic.yaml

Each file must define:

FieldRequiredDescription
idyesUnique identifier; used in API test_pack field (filename without .yaml)
nameyesHuman-readable label emitted in events
stepsyesOrdered list of step objects

The control API validates that id and steps exist when a run is created.

Reference a pack by filename stem:

bash
curl -X POST http://localhost:8080/runs \
  -H "Content-Type: application/json" \
  -d '{
    "scope_id": "local-juice-shop",
    "targets": ["http://juice-shop:3000"],
    "test_pack": "juice-shop-detect"
  }'

Step types

Steps are a discriminated union — each list item has exactly one top-level key:

KeyPurpose
httpSend an HTTP request and store the response
assertCheck a stored response
extractCapture a value from a response into a variable
findingRecord a security finding
yaml
steps:
  - http:
      id: root
      method: GET
      path: /

  - assert:
      response: root
      status: 200
      message: Homepage returned 200

  - finding:
      severity: critical
      message: Vulnerable application detected

Execution model

  • Steps run in order for each target.
  • HTTP responses are stored by id and referenced by later assert / extract steps.
  • If exec.failed is set (failed assert or extract), remaining steps emit step.skipped but the run still completes.
  • HTTP request errors emit request.error but do not set exec.failed — later steps still run unless an assert fails.
  • Redirects are disabled; the worker follows the initial response only.

HTTP step

yaml
- http:
    id: api_probe          # required — response handle for later steps
    method: POST           # required — GET, POST, HEAD, etc.
    path: /rest/user/login # required — joined to job target URL
    timeout_ms: 5000       # optional — per-request timeout
    headers:               # optional
      Accept: application/json
    query:                 # optional — URL query parameters
      q: search
    form:                  # optional — application/x-www-form-urlencoded
      email: test@example.com
      password: secret
    json:                  # optional — application/json body
      email: test@example.com
      password: secret
    body: "raw string"     # optional — raw request body

URL construction

path is joined to the job target (e.g. http://juice-shop:3000 + /apihttp://juice-shop:3000/api).

The final URL must match an entry in the worker scope allowed_origins. The HTTP method must be listed in allowed_methods.

Variable interpolation

Strings in method, path, headers, query, form, body, and json support or substitution from prior extract steps:

yaml
- extract:
    response: login_page
    from: body
    regex: 'name="_token" value="([^"]+)"'
    into: csrf_token

- http:
    id: login_attempt
    method: POST
    path: /login
    form:
      _token: "{{ csrf_token }}"
      email: test@example.com

Assert step

yaml
- assert:
    response: root        # required — id from a prior http step
    message: Check passed # required — emitted on pass/fail
    severity: high        # optional — default info; used on failure

All configured checks must pass. Available conditions (combine as needed):

FieldPass condition
statusStatus code equals value
status_ltStatus code is less than value
status_gteStatus code is greater than or equal to value
status_notStatus code is not equal to value
header_presentHeader exists (case-insensitive)
header_absentHeader does not exist
header_containsHeader value contains substring
body_containsResponse body contains substring
body_not_containsResponse body does not contain substring

Header checks example:

yaml
- assert:
    response: home
    header_absent: content-security-policy
    severity: medium
    message: Missing Content-Security-Policy header

- assert:
    response: api_probe
    header_contains:
      name: content-type
      value: application/json
    message: Login API returns JSON

On failure the worker emits assert.failed, sets exec.failed, and skips subsequent steps (except the final target.completed).

Extract step

yaml
- extract:
    response: login_page  # required — id from a prior http step
    from: body            # body | header
    regex: 'token=([^&]+)'  # required — must have capture group 1
    into: session_token   # required — variable name for later steps
  • from: body searches the response body.
  • from: header searches a flattened name: value header dump.
  • The regex must include a capture group (...). Group 1 is stored in into.
  • On failure emits extract.failed and sets exec.failed.

Finding step

yaml
- finding:
    severity: critical   # info | low | medium | high | critical
    message: OWASP Juice Shop detected

Findings emit finding.created with the given severity and message.

Run outcome is affected by severity:

SeverityRun outcome
critical or highpotentially_exploitable
OtherNo outcome change

If no critical/high finding is emitted, the run outcome defaults to not_exploitable on completion.

Complete examples

Detection pack

juice-shop-detect.yaml on GitHub

yaml
id: juice-shop-detect
name: Detect OWASP Juice Shop

steps:
  - http:
      id: root
      method: GET
      path: /

  - assert:
      response: root
      status: 200
      message: Homepage returned 200

  - assert:
      response: root
      body_contains: "OWASP Juice Shop"
      message: Body contains Juice Shop fingerprint

  - finding:
      severity: critical
      message: OWASP Juice Shop detected

JSON API probe

http-rich-test.yaml — POST with JSON body, status and header checks, SQL error leak detection.

CSRF token flow

csrf-flow-test.yaml — GET page, extract token, POST form with interpolated variable.

Security headers

headers-basic.yaml — checks for missing CSP and X-Frame-Options.

Scope constraints

Workers enforce scope rules before sending requests:

yaml
# scopes/local-juice-shop.yaml
scope_id: local-juice-shop
allowed_origins:
  - http://juice-shop:3000
allowed_methods:
  - GET
  - HEAD

A request is blocked with scope.blocked when:

  • The resolved URL origin does not match allowed_origins
  • The method is not in allowed_methods

Ensure your test pack only uses methods and paths reachable within the active scope.

Events emitted

EventWhen
worker.job.claimedJob picked up
target.startedTest pack execution begins
request.sentHTTP request dispatched
request.completedHTTP response received
request.errorRequest failed (timeout, DNS, invalid method)
assert.passedAssertion succeeded
assert.failedAssertion failed
extract.completedVariable extracted
extract.failedExtraction failed
finding.createdFinding recorded
step.skippedStep skipped after prior failure
scope.blockedRequest blocked by scope
target.completedAll steps finished

Native tools, weird experiments, and practical performance work.