parlov docs

parlov-elicit

Elicitation engine — ScanContext, the Strategy trait, 32 strategy implementations across 3 detection vectors, and probe plan generation.

Implemented

Version: 0.3.0 | Files: 42 | Lines: 8,578 | Dependencies: parlov-core, http, bytes

The elicitation engine and the largest crate in the workspace. Maps operator-supplied scan context into a plan of HTTP probe pairs. Contains 32 strategy implementations across 3 detection vectors. Pure synchronous computation — no I/O, no async.


Public API

Scan Context

ScanContext — All operator-supplied parameters that govern a single elicitation scan. Strategies inspect this to decide applicability and construct probes.

FieldTypeDescription
targetStringURL template with {id} placeholder
baseline_idStringID of a known-existing resource (control)
probe_idStringID of a known-nonexistent resource (test)
headersHeaderMapCommon request headers, typically including Authorization
max_riskRiskLevelMaximum permitted risk level
known_duplicateOption<KnownDuplicate>For uniqueness-conflict strategies
state_fieldOption<StateField>For state-transition strategies
alt_credentialOption<HeaderMap>Under-scoped credential for scope-manipulation strategies
body_templateOption<String>Body template with {id} when identifier is in the body

KnownDuplicate — A field value already taken in the target system.

pub struct KnownDuplicate {
    pub field: String,   // e.g. "email"
    pub value: String,   // e.g. "alice@example.com"
}

StateField — A field value that puts the resource into an invalid or rejected state.

pub struct StateField {
    pub field: String,   // e.g. "status"
    pub value: String,   // e.g. "invalid_state"
}

Risk Classification

RiskLevel — Ordered enum controlling strategy eligibility. Strategies above the operator's max_risk ceiling are skipped.

VariantMeaningStrategy Count
SafeRead-only probing. No state mutation.20
MethodDestructiveNon-idempotent methods (POST, PATCH, PUT). May have side-effects but avoids permanent data loss.9
OperationDestructiveMay trigger irreversible state changes (DELETE, resource exhaustion).3

Variants are PartialOrd so <= comparisons enforce the ceiling.

The Strategy Trait

Every elicitation strategy implements this trait. Strategies are pure — no I/O, no async.

pub trait Strategy: Send + Sync {
    fn id(&self) -> &'static str;
    fn name(&self) -> &'static str;
    fn risk(&self) -> RiskLevel;
    fn methods(&self) -> &[Method];
    fn is_applicable(&self, ctx: &ScanContext) -> bool;
    fn generate(&self, ctx: &ScanContext) -> Vec<ProbeSpec>;
}

Lifecycle:

  1. is_applicable() checks whether the strategy can produce useful probes for this context (e.g. auth-strip requires Authorization headers)
  2. generate() produces ProbeSpec values — only called when is_applicable() returns true
  3. The scheduler owns execution; strategies never touch the network

Probe Specifications

ProbeSpec — The unit of work handed to the scheduler.

pub enum ProbeSpec {
    /// Standard adaptive loop: one baseline, one probe.
    Pair(ProbePair),
    /// Burst: N baseline requests, then N probe requests (timing analysis).
    Burst(BurstSpec),
    /// One baseline + one probe; compare full header sets.
    HeaderDiff(ProbePair),
}

ProbePair — A baseline + probe pair with strategy and technique metadata.

pub struct ProbePair {
    pub baseline: ProbeDefinition,
    pub probe: ProbeDefinition,
    pub metadata: StrategyMetadata,
    pub technique: Technique,
}

BurstSpec — For timing oracles requiring statistical significance.

pub struct BurstSpec {
    pub baseline: ProbeDefinition,
    pub probe: ProbeDefinition,
    pub burst_count: usize,
    pub metadata: StrategyMetadata,
    pub technique: Technique,
}

StrategyMetadata — Identity and risk, carried on every probe spec for attribution without holding a reference to the strategy.

pub struct StrategyMetadata {
    pub strategy_id: &'static str,
    pub strategy_name: &'static str,
    pub risk: RiskLevel,
}

Registry Functions

all_strategies() -> Vec<Box<dyn Strategy>>

Returns all 32 registered strategies, grouped by risk in declaration order.

generate_plan(ctx: &ScanContext) -> Vec<ProbeSpec>

The primary entry point. Iterates all strategies, filters by risk <= ctx.max_risk and is_applicable(ctx), and collects all generated ProbeSpec values in declaration order.


Strategy Inventory

Status Code Diff Vector (17 strategies)

Safe (9):

Strategy IDWhat it doesContext Requirements
accept-elicitSends Accept header requesting uncommon media typeNone
if-none-match-elicitSends If-None-Match with fabricated ETagNone
trailing-slash-elicitAppends/removes trailing slashNone
case-normalize-elicitAlters URL path casingNone
long-uri-elicitSends extremely long URINone
auth-strip-elicitRemoves Authorization headerAuthorization header present
low-privilege-elicitSends with under-scoped credentialsalt_credential present
scope-manipulation-elicitSwaps to alt credential setalt_credential present
rate-limit-headers-elicitCompares rate-limit response headersNone

MethodDestructive (5):

Strategy IDWhat it doesContext Requirements
content-type-elicitSends mismatched Content-TypeNone
if-match-elicitSends If-Match with fabricated ETagNone
empty-body-elicitSends POST/PUT/PATCH with empty bodyNone
oversized-body-elicitSends oversized request bodyNone
state-transition-elicitSends invalid state valuestate_field present

OperationDestructive (3):

Strategy IDWhat it doesContext Requirements
uniqueness-elicitSends known-duplicate field valueknown_duplicate present
dependency-delete-elicitSends DELETE requestNone
rate-limit-burst-elicitBurst of requests to trigger rate limitingNone

Cache Probing Vector (8 strategies, all Safe)

Strategy IDWhat it does
cp-if-none-matchIf-None-Match with fabricated ETag
cp-if-modified-sinceIf-Modified-Since with past date
cp-if-matchIf-Match with fabricated ETag
cp-if-unmodified-sinceIf-Unmodified-Since with past date
cp-range-satisfiableRange header requesting first byte
cp-range-unsatisfiableRange header requesting impossible range
cp-if-rangeIf-Range with fabricated ETag
cp-acceptAccept header with cache-varying media type

Error Message Granularity Vector (7 strategies)

Safe (3):

Strategy IDWhat it does
emg-bolaCompares error body for authorization-dependent messages
emg-query-validationCompares error body for query parameter validation
emg-app-vs-server-404Distinguishes application-level vs server-level 404

MethodDestructive (4):

Strategy IDWhat it doesContext Requirements
emg-schema-validation-patchPATCH with invalid schemaNone
emg-schema-validation-putPUT with invalid schemaNone
emg-state-conflictTriggers 409 Conflict statebody_template present
emg-fk-violationTriggers foreign key violationNone

Internal Architecture

Module Layout

parlov-elicit/src/
├── lib.rs                # Public re-exports
├── context.rs            # ScanContext, KnownDuplicate, StateField
├── strategy.rs           # Strategy trait
├── types.rs              # RiskLevel, ProbeSpec, ProbePair, BurstSpec, StrategyMetadata
├── registry.rs           # all_strategies(), generate_plan()
├── util.rs               # substitute_url(), substitute_body(), build_pair()
└── existence/
    ├── status_code_diff/   # 17 strategy modules
    │   ├── accept.rs
    │   ├── auth_strip.rs
    │   ├── case_normalize.rs
    │   ├── content_type.rs
    │   ├── dependency_delete.rs
    │   ├── empty_body.rs
    │   ├── if_match.rs
    │   ├── if_none_match.rs
    │   ├── long_uri.rs
    │   ├── low_privilege.rs
    │   ├── oversized_body.rs
    │   ├── rate_limit_burst.rs
    │   ├── rate_limit_headers.rs
    │   ├── scope_manipulation.rs
    │   ├── state_transition.rs
    │   ├── trailing_slash.rs
    │   └── uniqueness.rs
    ├── cache_probing/      # 8 strategy modules
    │   ├── accept.rs
    │   ├── if_match.rs
    │   ├── if_modified_since.rs
    │   ├── if_none_match.rs
    │   ├── if_range.rs
    │   ├── if_unmodified_since.rs
    │   ├── range_satisfiable.rs
    │   └── range_unsatisfiable.rs
    └── error_message_granularity/  # 7 strategy modules
        ├── app_vs_server_404.rs
        ├── bola.rs
        ├── fk_violation.rs
        ├── query_validation.rs
        ├── schema_validation_patch.rs
        ├── schema_validation_put.rs
        └── state_conflict.rs

Utility Module (util.rs)

Shared helpers used by strategy implementations:

FunctionPurpose
substitute_url(template, id) -> StringReplaces {id} in URL template
substitute_body(template, id) -> Option<Bytes>Replaces {id} in body template
build_pair(ctx, method, baseline_headers, probe_headers, body, metadata, technique) -> ProbePairConstructs a complete ProbePair from context and overrides

build_pair() is the workhorse — strategies call it with their specific method, header, and body overrides while it handles URL substitution and body template resolution.


Extension Points

Adding a New Strategy

  1. Create a new module under the appropriate vector directory (e.g. existence/status_code_diff/my_strategy.rs)
  2. Define a unit struct (e.g. pub struct MyStrategyElicitation;)
  3. Implement Strategy for it:
    • Choose a stable id() (e.g. "my-strategy-elicit")
    • Set the risk() level
    • Implement is_applicable() to check context requirements
    • Implement generate() to build ProbeSpec values using util::build_pair()
  4. Re-export from the vector's mod.rs
  5. Add a Box::new(MyStrategyElicitation) line in registry.rs::all_strategies()
  6. The strategy is automatically included in generate_plan() filtering

Adding a New Vector

  1. Add a variant to Vector in parlov-core
  2. Create a new directory under existence/ (e.g. existence/my_vector/)
  3. Implement strategies in that directory following the pattern above
  4. Each strategy's Technique should declare the new vector
  5. Add a signal extractor in parlov-analysis::signals if the vector produces a new signal type

Adding a New Oracle Class

This is a larger change spanning multiple crates:

  1. Add variant to OracleClass in parlov-core
  2. Create a new top-level module in parlov-elicit (sibling to existence/)
  3. Implement strategies under the new module
  4. Create a corresponding analyzer in parlov-analysis
  5. Add a CLI subcommand in the parlov binary crate

On this page