parlov docs

CLI Reference

Complete reference for the parlov binary — global flags, subcommands, output formats, invocation patterns, and exit codes.

Implemented

parlov is invoked as a single binary with one subcommand per mode of operation. All flags use the --long-name form. There are no short flags.

parlov [--format <table|json|sarif>] <command> [command-specific flags]

Global Flags

FlagTypeDefaultDescription
--formattable, json, sariftableOutput format for all results. Applies to every subcommand.

The --format flag is global — it can appear before or after the subcommand name.


Subcommands

parlov has two subcommands:

  • existence — Run a single status-code differential probe against one endpoint with one HTTP method.
  • scan — Run all applicable elicitation strategies against one endpoint, across multiple methods and vectors.

Both subcommands target the same endpoint pattern and use the same baseline/probe differential model. The difference is scope: existence sends one probe pair and reports one result. scan builds a plan from all registered strategies, filters by risk and vector, and reports an array of findings.


parlov existence

Run a single differential probe. Sends one baseline request (known-existing ID) and one probe request (known-nonexistent ID) using the specified HTTP method, then compares the responses.

Synopsis

parlov existence \
  --target <url-template> \
  --baseline-id <id> \
  [--probe-id <id>] \
  [--method <METHOD>] \
  [--header <Name: Value>]... \
  [--body <body-template>]

Flags

FlagTypeRequiredDefaultDescription
--targetstringyesURL template. {id} is replaced with the resource identifier.
--baseline-idstringyesKnown-existing resource ID. Used as the control input.
--probe-idstringnorandom UUIDv4Known-nonexistent resource ID. Defaults to a random UUID, which is nonexistent by design.
--methodstringnoGETHTTP method to use for both requests.
--headerstringnoAdditional request header in Name: Value format. Repeatable — pass --header once per header.
--bodystringnoRequest body template. {id} is replaced with the resource identifier. Required when the API identifies resources via the request body rather than the URL path.

Examples

Basic GET probe:

parlov existence \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001

Sends GET /users/1001 (baseline) and GET /users/<random-uuid> (probe). Compares the two responses.

POST with body-based identifier:

parlov existence \
  --target "https://api.example.com/auth/login" \
  --baseline-id "alice@example.com" \
  --probe-id "nonexistent@example.com" \
  --method POST \
  --body '{"email": "{id}", "password": "wrong"}'

The {id} placeholder in both --target and --body is replaced with the baseline and probe IDs respectively.

With authentication headers:

parlov existence \
  --target "https://api.example.com/documents/{id}" \
  --baseline-id doc-42 \
  --method GET \
  --header "Authorization: Bearer eyJ..." \
  --header "Accept: application/json"

parlov scan

Run all applicable elicitation strategies against a target endpoint. Builds a plan from registered strategies, filters by risk ceiling or vector selection, executes each strategy, and reports an array of findings.

Synopsis

parlov scan \
  --target <url-template> \
  --baseline-id <id> \
  [--probe-id <id>] \
  [--risk <level>] \
  [--vector <filter>]... \
  [--header <Name: Value>]... \
  [--body <body-template>] \
  [--alt-credential <Header>] \
  [--known-duplicate <field=value>] \
  [--state-field <field=value>]

Flags

FlagTypeRequiredDefaultDescription
--targetstringyesURL template. {id} is replaced with the resource identifier.
--baseline-idstringyesKnown-existing resource ID (baseline).
--probe-idstringnorandom UUIDv4Known-nonexistent resource ID (probe).
--risksafe, method-destructive, operation-destructivenosafeMaximum risk level of strategies to execute. Only strategies at or below this ceiling are included in the plan. Mutually exclusive with --vector.
--vectorstringnoFilter strategies by detection vector. Repeatable. Format: name or name:risk-level. Mutually exclusive with --risk. See Vector Filtering below.
--headerstringnoAdditional request header in Name: Value format. Repeatable.
--bodystringnoRequest body template. {id} is replaced with the resource identifier.
--alt-credentialstringnoUnder-scoped credential header for the scope-manipulation strategy, e.g. Authorization: Bearer <limited-token>.
--known-duplicatestringnoKnown-duplicate value for the uniqueness strategy, in field=value format, e.g. email=alice@example.com.
--state-fieldstringnoInvalid state transition value for the state-transition strategy, in field=value format, e.g. status=invalid_state.

--risk and --vector Are Mutually Exclusive

--risk sets a global risk ceiling — all strategies at or below that level are included.

--vector selects specific detection vectors and optionally overrides the risk level per vector. When --vector is provided, the risk ceiling is derived from the vector filters, not from --risk.

Passing both --risk (with a non-default value) and --vector is an error:

error: --vector and --risk are mutually exclusive

If you need per-vector risk control, use --vector with inline risk levels. If you want a blanket risk ceiling across all vectors, use --risk.

Vector Filtering

The --vector flag accepts two formats:

  • name — include all strategies for this vector at their default risk level.
  • name:risk-level — include strategies for this vector up to the specified risk level.

Vector names: status-code-diff, cache-probing, error-message-granularity.

The flag is repeatable — pass it once per vector filter.

# Only cache-probing strategies, safe risk only
parlov scan --target "..." --baseline-id 42 \
  --vector cache-probing

# Both vectors, cache-probing up to method-destructive
parlov scan --target "..." --baseline-id 42 \
  --vector status-code-diff \
  --vector cache-probing:method-destructive

Strategy-Specific Flags

Some scan strategies require additional input to function. These flags enable those strategies — if the flag is omitted, the strategy that depends on it is skipped.

--alt-credential — Enables the scope-manipulation strategy. Provide a credential header with fewer privileges than the primary --header credential. The scan compares the server's response to the full-privilege credential against the under-scoped credential to detect scope-dependent differentials.

parlov scan \
  --target "https://api.example.com/resources/{id}" \
  --baseline-id 42 \
  --header "Authorization: Bearer $FULL_TOKEN" \
  --alt-credential "Authorization: Bearer $LIMITED_TOKEN"

--known-duplicate — Enables the uniqueness strategy. Provide a field and value that already exists in the system. The scan sends a creation request with this duplicate value to observe whether the server returns a conflict (e.g., 409) for existing values but a different response for nonexistent ones.

parlov scan \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 42 \
  --risk method-destructive \
  --known-duplicate "email=alice@example.com"

--state-field — Enables the state-transition strategy. Provide a field and an invalid state value. The scan sends a state-change request with this invalid value to observe whether the server returns a conflict (e.g., 409) for existing resources but 404 for nonexistent ones.

parlov scan \
  --target "https://api.example.com/orders/{id}" \
  --baseline-id 42 \
  --risk method-destructive \
  --state-field "status=invalid_state"

The {id} Template

Both --target and --body use {id} as a placeholder for the resource identifier. On each request, parlov replaces every occurrence of {id} with the appropriate ID — the baseline ID for the control request, the probe ID for the experimental request.

The placeholder works in URLs:

--target "https://api.example.com/users/{id}/profile"

In request bodies:

--body '{"user_id": "{id}", "action": "lookup"}'

And in both simultaneously:

--target "https://api.example.com/users/{id}" \
--body '{"id": "{id}"}'

If the API identifies resources entirely through the request body (e.g., a GraphQL endpoint), the --target URL can be static (no {id}):

--target "https://api.example.com/graphql" \
--body '{"query": "{ user(id: \"{id}\") { name } }"}'

Output Formats

All output goes to stdout. Errors go to stderr.

Table (default)

Human-readable terminal table with ANSI color coding per verdict level. Designed for interactive use.

existence output columns:

ColumnDescription
OracleOracle class (e.g., Existence)
VerdictConfirmed, Likely, Inconclusive, or NotPresent
SeverityHigh, Medium, Low, or
ConfidenceNumeric score with impact class in parentheses (e.g., 85 (Medium)), or when zero
EvidencePrimary signal description (e.g., 403 (baseline) vs 404 (probe))

When a pattern is matched, additional metadata rows appear below the main row:

RowDescription
LabelHuman-readable name for the matched pattern (e.g., Authorization-based differential)
LeaksWhat the oracle reveals (e.g., Resource existence confirmed to low-privilege callers)
RFC BasisSpecification section grounding the behavior (e.g., RFC 9110 §15.5.4)

Example table output for a 403/404 differential:

+-----------+-----------+-----------+--------------------------+-------------------------------+
| Oracle    | Verdict   | Severity  | Confidence               | Evidence                      |
+===========+===========+===========+==========================+===============================+
| Existence | Confirmed | Medium    | 85 (Medium)              | 403 (baseline) vs 404 (probe) |
|-----------+-----------+-----------+--------------------------+-------------------------------|
|           |           | Label     | Authorization-based differential                          |
|-----------+-----------+-----------+--------------------------+-------------------------------|
|           |           | Leaks     | Resource existence confirmed to low-privilege callers     |
|-----------+-----------+-----------+--------------------------+-------------------------------|
|           |           | RFC Basis | RFC 9110 §15.5.4                                          |
+-----------+-----------+-----------+--------------------------+-------------------------------+

scan output columns:

ColumnDescription
StrategyName of the elicitation strategy that generated the probe pair
MethodHTTP method used
VerdictConfirmed, Likely, Inconclusive, or NotPresent
SeverityHigh, Medium, Low, or
ConfidenceNumeric score with impact class, or when zero
EvidencePrimary signal description

JSON

Pretty-printed structured JSON following the parlov v1.0.0 schema. Designed for scripting, piping, and downstream tooling.

existence output structure:

{
  "schema_version": "1.0.0",
  "target_url": "https://api.example.com/users/{id}",
  "finding": {
    "finding_id": "ee77a4ef7711",
    "strategy": {
      "id": "existence-cli",
      "name": "CLI existence probe",
      "method": "GET"
    },
    "result": {
      "oracle_class": "Existence",
      "verdict": "Confirmed",
      "confidence": 85,
      "severity": "Medium",
      "impact_class": "Medium"
    },
    "technique": {
      "id": "existence-cli",
      "vector": "StatusCodeDiff",
      "normative_strength": "Should"
    },
    "matched_pattern": {
      "label": "Authorization-based differential",
      "leaks": "Resource existence confirmed to low-privilege callers",
      "rfc_basis": "RFC 9110 §15.5.4"
    },
    "evidence": {
      "reasons": [
        {
          "description": "Authorization-based differential (base +403→85)",
          "points": 85,
          "dimension": "Confidence"
        }
      ],
      "signals": [
        {
          "kind": "StatusCodeDiff",
          "evidence": "403 (baseline) vs 404 (probe)"
        }
      ]
    }
  }
}

scan output structure:

{
  "schema_version": "1.0.0",
  "target_url": "https://api.example.com/users/{id}",
  "findings": [
    {
      "finding_id": "...",
      "strategy": { "id": "...", "name": "...", "method": "..." },
      "result": { "oracle_class": "...", "verdict": "...", "confidence": 0, "severity": "..." },
      "technique": { "id": "...", "vector": "...", "normative_strength": "..." },
      "matched_pattern": { },
      "evidence": { "reasons": [], "signals": [] }
    }
  ]
}

Key differences between existence and scan JSON:

  • existence wraps results in a singular finding object.
  • scan wraps results in a findings array containing zero or more finding objects.
  • Both share the same FindingOutput schema per finding.

JSON field reference:

FieldTypeDescription
schema_versionstringAlways "1.0.0".
target_urlstringThe --target URL template as provided.
finding_idstring12-character hex fingerprint. Deterministic — same inputs produce the same ID.
strategy.idstringMachine-readable strategy identifier.
strategy.namestringHuman-readable strategy name.
strategy.methodstringHTTP method used.
result.oracle_classstring"Existence", "Authentication", "Timing", "Verification", "TokenValidation", or "State".
result.verdictstring"Confirmed", "Likely", "Inconclusive", or "NotPresent".
result.confidenceinteger0–100 confidence score.
result.severitystring"High", "Medium", "Low", or "None".
result.impact_classstring or nullImpact class when distinct from severity. Omitted when null.
technique.idstringTechnique identifier (e.g., "get-403-404").
technique.vectorstringDetection vector: "StatusCodeDiff", "CacheProbing", or "ErrorMessageGranularity".
technique.normative_strengthstring"Must", "Should", "May", or "Unknown".
matched_pattern.labelstring or absentPattern name. Omitted when no pattern matched.
matched_pattern.leaksstring or absentWhat the oracle reveals. Omitted when no pattern matched.
matched_pattern.rfc_basisstring or absentRFC section. Omitted when no pattern matched.
evidence.reasonsarrayScoring reasons, each with description, points, and dimension.
evidence.signalsarrayTyped signals, each with kind and evidence.

SARIF

SARIF v2.1.0 output for CI/CD integration. Compatible with GitHub Advanced Security, GitHub Code Scanning, and other SARIF-consuming platforms.

SARIF-specific behavior:

  • Verdict-to-level mapping: Confirmederror, Likelywarning, Inconclusive and NotPresentnote.
  • NotPresent findings are excluded from the results array entirely.
  • Rule IDs are technique-based (e.g., get-403-404).
  • Fingerprints: Each result carries a deterministic oracleFingerprint/v1 (12-character hex) and a partialFingerprints entry for deduplication across runs.
  • Signals map to relatedLocations on each result.
  • logicalLocations is emitted on every result with the HTTP method and target URL: "logicalLocations": [{"kind": "url", "name": "GET https://..."}].
  • security-severity is set to confidence / 10.0 (0.0–10.0 scale) in rule properties.
  • scan runs include target_url in run.properties.

Example SARIF structure (abbreviated):

{
  "$schema": "https://schemastore.azurewebsites.net/schemas/json/sarif-2.1.0.json",
  "version": "2.1.0",
  "runs": [{
    "tool": {
      "driver": {
        "name": "parlov",
        "version": "0.6.0",
        "rules": [{
          "id": "get-403-404",
          "name": "ExistenceOracle",
          "shortDescription": { "text": "Authorization-based differential" },
          "properties": {
            "oracle_class": "Existence",
            "vector": "StatusCodeDiff",
            "security-severity": "8.5"
          }
        }]
      }
    },
    "results": [{
      "ruleId": "get-403-404",
      "level": "error",
      "message": { "text": "Resource existence confirmed to low-privilege callers" },
      "locations": [{
        "physicalLocation": {
          "artifactLocation": { "uri": "https://api.example.com/users/{id}" }
        }
      }],
      "fingerprints": { "oracleFingerprint/v1": "ee77a4ef7711" },
      "relatedLocations": [{
        "id": 0,
        "message": { "text": "[StatusCodeDiff] 403 (baseline) vs 404 (probe)" }
      }],
      "properties": {
        "oracle_class": "Existence",
        "verdict": "Confirmed",
        "confidence": 85,
        "method": "GET"
      }
    }]
  }]
}

Invocation Patterns

Common real-world usage patterns.

Quick existence check

The fastest way to test whether an endpoint leaks resource existence:

parlov existence \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001

Uses GET, random probe UUID, table output. If a differential exists, it will appear immediately.

Full safe scan

Run all safe (read-only) strategies against an endpoint:

parlov scan \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001

--risk safe is the default. This runs GET, HEAD, and conditional request strategies — no server state is modified.

Escalating risk levels

Start safe, escalate if needed:

# Step 1: safe only
parlov scan --target "https://api.example.com/users/{id}" --baseline-id 42

# Step 2: include POST/PATCH/PUT strategies
parlov scan --target "https://api.example.com/users/{id}" --baseline-id 42 \
  --risk method-destructive

# Step 3: include DELETE and resource-exhaustion strategies
parlov scan --target "https://api.example.com/users/{id}" --baseline-id 42 \
  --risk operation-destructive

Testing across auth contexts

The same endpoint must be tested from three perspectives. Run parlov once per context:

# Unauthenticated
parlov scan --target "https://api.example.com/documents/{id}" --baseline-id doc-42

# Authenticated, no access (different organization's token)
parlov scan --target "https://api.example.com/documents/{id}" --baseline-id doc-42 \
  --header "Authorization: Bearer $OTHER_ORG_TOKEN"

# Authenticated, has access
parlov scan --target "https://api.example.com/documents/{id}" --baseline-id doc-42 \
  --header "Authorization: Bearer $OWN_ORG_TOKEN"

Body-based APIs

When the identifier is in the request body (common for POST-based lookup APIs, GraphQL, and RPC-style endpoints):

parlov existence \
  --target "https://api.example.com/graphql" \
  --baseline-id "user-42" \
  --method POST \
  --header "Content-Type: application/json" \
  --body '{"query": "{ user(id: \"{id}\") { email } }"}'

JSON output for scripting

Pipe JSON output to jq for extraction:

# Extract verdict from a single probe
parlov existence --format json \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001 \
  | jq '.finding.result.verdict'

# Filter scan findings to confirmed only
parlov scan --format json \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001 \
  | jq '[.findings[] | select(.result.verdict == "Confirmed")]'

SARIF for CI/CD

Generate SARIF output for GitHub Code Scanning:

parlov scan --format sarif \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001 \
  > results.sarif

# Upload to GitHub (using the gh CLI)
gh api \
  -X POST \
  "/repos/{owner}/{repo}/code-scanning/sarifs" \
  -f "sarif=@results.sarif" \
  -f "ref=refs/heads/main"

Targeting specific vectors

Run only cache-probing strategies:

parlov scan \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001 \
  --vector cache-probing

Run cache-probing up to method-destructive and status-code-diff at safe only:

parlov scan \
  --target "https://api.example.com/users/{id}" \
  --baseline-id 1001 \
  --vector cache-probing:method-destructive \
  --vector status-code-diff

Exit Codes

CodeMeaning
0Success. Results were produced (regardless of verdict).
1Error. An unrecoverable error occurred (invalid arguments, network failure, serialization error). The error message is printed to stderr.

On this page