parlov-elicit
Elicitation engine — ScanContext, the Strategy trait, 32 strategy implementations across 3 detection vectors, and probe plan generation.
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.
| Field | Type | Description |
|---|---|---|
target | String | URL template with {id} placeholder |
baseline_id | String | ID of a known-existing resource (control) |
probe_id | String | ID of a known-nonexistent resource (test) |
headers | HeaderMap | Common request headers, typically including Authorization |
max_risk | RiskLevel | Maximum permitted risk level |
known_duplicate | Option<KnownDuplicate> | For uniqueness-conflict strategies |
state_field | Option<StateField> | For state-transition strategies |
alt_credential | Option<HeaderMap> | Under-scoped credential for scope-manipulation strategies |
body_template | Option<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.
| Variant | Meaning | Strategy Count |
|---|---|---|
Safe | Read-only probing. No state mutation. | 20 |
MethodDestructive | Non-idempotent methods (POST, PATCH, PUT). May have side-effects but avoids permanent data loss. | 9 |
OperationDestructive | May 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:
is_applicable()checks whether the strategy can produce useful probes for this context (e.g. auth-strip requires Authorization headers)generate()producesProbeSpecvalues — only called whenis_applicable()returnstrue- 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 ID | What it does | Context Requirements |
|---|---|---|
accept-elicit | Sends Accept header requesting uncommon media type | None |
if-none-match-elicit | Sends If-None-Match with fabricated ETag | None |
trailing-slash-elicit | Appends/removes trailing slash | None |
case-normalize-elicit | Alters URL path casing | None |
long-uri-elicit | Sends extremely long URI | None |
auth-strip-elicit | Removes Authorization header | Authorization header present |
low-privilege-elicit | Sends with under-scoped credentials | alt_credential present |
scope-manipulation-elicit | Swaps to alt credential set | alt_credential present |
rate-limit-headers-elicit | Compares rate-limit response headers | None |
MethodDestructive (5):
| Strategy ID | What it does | Context Requirements |
|---|---|---|
content-type-elicit | Sends mismatched Content-Type | None |
if-match-elicit | Sends If-Match with fabricated ETag | None |
empty-body-elicit | Sends POST/PUT/PATCH with empty body | None |
oversized-body-elicit | Sends oversized request body | None |
state-transition-elicit | Sends invalid state value | state_field present |
OperationDestructive (3):
| Strategy ID | What it does | Context Requirements |
|---|---|---|
uniqueness-elicit | Sends known-duplicate field value | known_duplicate present |
dependency-delete-elicit | Sends DELETE request | None |
rate-limit-burst-elicit | Burst of requests to trigger rate limiting | None |
Cache Probing Vector (8 strategies, all Safe)
| Strategy ID | What it does |
|---|---|
cp-if-none-match | If-None-Match with fabricated ETag |
cp-if-modified-since | If-Modified-Since with past date |
cp-if-match | If-Match with fabricated ETag |
cp-if-unmodified-since | If-Unmodified-Since with past date |
cp-range-satisfiable | Range header requesting first byte |
cp-range-unsatisfiable | Range header requesting impossible range |
cp-if-range | If-Range with fabricated ETag |
cp-accept | Accept header with cache-varying media type |
Error Message Granularity Vector (7 strategies)
Safe (3):
| Strategy ID | What it does |
|---|---|
emg-bola | Compares error body for authorization-dependent messages |
emg-query-validation | Compares error body for query parameter validation |
emg-app-vs-server-404 | Distinguishes application-level vs server-level 404 |
MethodDestructive (4):
| Strategy ID | What it does | Context Requirements |
|---|---|---|
emg-schema-validation-patch | PATCH with invalid schema | None |
emg-schema-validation-put | PUT with invalid schema | None |
emg-state-conflict | Triggers 409 Conflict state | body_template present |
emg-fk-violation | Triggers foreign key violation | None |
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.rsUtility Module (util.rs)
Shared helpers used by strategy implementations:
| Function | Purpose |
|---|---|
substitute_url(template, id) -> String | Replaces {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) -> ProbePair | Constructs 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
- Create a new module under the appropriate vector directory (e.g.
existence/status_code_diff/my_strategy.rs) - Define a unit struct (e.g.
pub struct MyStrategyElicitation;) - Implement
Strategyfor it:- Choose a stable
id()(e.g."my-strategy-elicit") - Set the
risk()level - Implement
is_applicable()to check context requirements - Implement
generate()to buildProbeSpecvalues usingutil::build_pair()
- Choose a stable
- Re-export from the vector's
mod.rs - Add a
Box::new(MyStrategyElicitation)line inregistry.rs::all_strategies() - The strategy is automatically included in
generate_plan()filtering
Adding a New Vector
- Add a variant to
Vectorinparlov-core - Create a new directory under
existence/(e.g.existence/my_vector/) - Implement strategies in that directory following the pattern above
- Each strategy's
Techniqueshould declare the new vector - Add a signal extractor in
parlov-analysis::signalsif the vector produces a new signal type
Adding a New Oracle Class
This is a larger change spanning multiple crates:
- Add variant to
OracleClassinparlov-core - Create a new top-level module in
parlov-elicit(sibling toexistence/) - Implement strategies under the new module
- Create a corresponding analyzer in
parlov-analysis - Add a CLI subcommand in the
parlovbinary crate