feat(collection): import a Postman/OpenAPI/HAR collection as test surface

Operators often already have the API mapped. Add a collection importer so a
Postman collection, an OpenAPI 3/Swagger 2 spec, or a HAR capture (JSON) is
parsed into a flat endpoint list and injected into every agent as known
in-scope surface — tested directly, alongside what recon finds, so documented
and auth-gated routes recon might miss are still covered.

- new harness module `collection` (format auto-detected by shape; de-duped;
  render capped so a huge collection can't blow the prompt budget) + 6 tests
- RunConfig.api_collection; injected in operator_directives
- CLI: global `--collection <file>` on run/greybox/aitest, validated at launch
- REPL: `/collection <file>` (and `/import-api`), persisted in the session,
  shown in /status, added to help + completion

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
CyberSecurityUPandClaude Opus 4.8 committed 2026-10-15 20:27:56 -03:00
1 parent 0aaac20a0d
commit 7a0b2cb244
6 files changed
+363 -3

No files matched your search

+20
View File
@@ -91,6 +91,11 @@ struct Cli {
/// Falls back to NEO4J_URI; credentials only via NEO4J_USER / NEO4J_PASSWORD.
#[arg(long = "neo4j", global = true)]
neo4j: Option<String>,
/// Import an API collection (Postman / OpenAPI / Swagger / HAR, JSON) as
/// known in-scope surface: its endpoints are injected into every agent and
/// tested directly, alongside what recon discovers.
#[arg(long = "collection", global = true)]
collection: Option<String>,
/// Decision backend for the calibrated System One layer:
/// typesafe (hosted API, needs TYPESAFE_API_KEY) or laya (local, free,
/// open-source — downloads the model on first use and keeps evidence on the
@@ -924,6 +929,7 @@ async fn main() -> anyhow::Result<()> {
cfg.authz_matrix = cli.authz_matrix.clone();
cfg.creds_file = creds.clone();
cfg.neo4j = cli.neo4j.clone();
apply_collection(&mut cfg, cli.collection.as_deref())?;
if !models.is_empty() {
cfg.models = models;
}
@@ -970,6 +976,7 @@ async fn main() -> anyhow::Result<()> {
cfg.instructions = focus;
cfg.pinned = parse_only(&only);
cfg.research = research;
apply_collection(&mut cfg, cli.collection.as_deref())?;
if quick { apply_quick(&mut cfg); }
if !models.is_empty() {
cfg.models = models;
@@ -1047,6 +1054,7 @@ async fn main() -> anyhow::Result<()> {
cfg.verbose = verbose;
cfg.instructions = focus;
cfg.auth = auth;
apply_collection(&mut cfg, cli.collection.as_deref())?;
if !models.is_empty() { cfg.models = models; }
let out = run_mode(&base, cfg, false, Mode::Ai).await?;
print_findings(&out);
@@ -2239,6 +2247,18 @@ fn apply_quick(cfg: &mut RunConfig) {
println!(" \x1b[2mquick: economy preset — 1 voter, 1 chain round, light recon, ≤6 agents, eco budget\x1b[0m");
}
/// Validate and attach an imported API collection (Postman/OpenAPI/HAR) so a
/// bad file or format fails on the command line, not mid-run, and the operator
/// sees how many endpoints were seeded.
fn apply_collection(cfg: &mut RunConfig, path: Option<&str>) -> anyhow::Result<()> {
let Some(path) = path.filter(|p| !p.trim().is_empty()) else { return Ok(()) };
let eps = harness::collection::parse_file(path)
.map_err(|e| anyhow::anyhow!("--collection {path}: {e}"))?;
println!(" \x1b[2mcollection: imported {} endpoint(s) from {} — tested as known surface\x1b[0m", eps.len(), path);
cfg.api_collection = Some(path.to_string());
Ok(())
}
/// Egress route, out-of-band channel and inbound SMS.
///
/// The transport spec is parsed here rather than at run time so a typo fails
+25 -3
View File
@@ -147,7 +147,7 @@ struct LiveCheckpoint {
/// near-neighbour would break working commands. A test keeps the two in sync.
pub(crate) const ACCEPTED: &[&str] = &[
"/?", "/agents", "/attach", "/audit", "/auth", "/burp", "/cap", "/capability", "/chain", "/changed", "/clear", "/config",
"/context", "/continue", "/creds", "/diff", "/exclude", "/exit", "/expand", "/feed",
"/collection", "/import-api", "/context", "/continue", "/creds", "/diff", "/exclude", "/exit", "/expand", "/feed",
"/pocs", "/poc", "/evidence", "/artifacts", "/finding", "/findings", "/focus", "/forget", "/full", "/go", "/goal", "/graph", "/guardrail", "/guardrails", "/help",
"/history", "/idle", "/inscope", "/instructions", "/integration", "/integrations", "/key", "/log",
"/authorize", "/grant", "/inscope-set", "/scope-file", "/scopefile", "/import-scope", "/authorization", "/authz", "/program", "/logs", "/mcp", "/memory", "/model", "/models", "/objective", "/objectives", "/observe",
@@ -162,7 +162,7 @@ pub(crate) const ACCEPTED: &[&str] = &[
/// All slash-commands, for Tab completion.
const COMMANDS: &[&str] = &[
"/help", "/onboard", "/show", "/config", "/providers", "/model", "/key", "/sub", "/target",
"/scope-file", "/authorization", "/class", "/repo", "/auth", "/creds", "/focus", "/objective", "/scope-out", "/attach", "/context", "/mcp", "/offline",
"/scope-file", "/authorization", "/class", "/repo", "/auth", "/creds", "/collection", "/focus", "/objective", "/scope-out", "/attach", "/context", "/mcp", "/offline",
"/class", "/research", "/quick", "/economy", "/eco", "/sandbox", "/votes", "/chain", "/recon", "/tempmail", "/timeout", "/proxy", "/burp", "/ua", "/agents", "/only", "/theme", "/clear", "/run", "/stop", "/pause", "/continue", "/runs", "/results", "/report",
"/status", "/logs", "/diff", "/retest", "/validate", "/finding", "/pocs", "/expand", "/integrations",
"/memory", "/forget", "/graph", "/inscope", "/observe", "/guardrail", "/policy",
@@ -285,6 +285,8 @@ struct Session {
/// Named identities for multi-role access-control testing (name, header line).
roles: Vec<(String, String)>,
creds: Option<String>,
/// Imported API collection (Postman/OpenAPI/HAR) path — tested as surface.
collection: Option<String>,
instructions: Option<String>,
/// Engagement objective / rules-of-engagement context (why + what matters).
objective: Option<String>,
@@ -349,6 +351,7 @@ impl Default for Session {
auth: None,
roles: Vec::new(),
creds: None,
collection: None,
instructions: None,
objective: None,
authorization: None,
@@ -854,6 +857,18 @@ pub async fn repl(base: &Path, auth: SessionAuth) -> anyhow::Result<()> {
else if arg == "clear" { s.creds = None; println!(" creds cleared"); }
else { s.creds = Some(arg.to_string()); println!(" creds file: {arg}"); }
}
"/collection" | "/import-api" => {
if arg.is_empty() {
println!(" api collection: {}", s.collection.clone().unwrap_or_else(|| "(none) — import with /collection <file> (Postman/OpenAPI/HAR, JSON)".into()));
} else if arg == "clear" {
s.collection = None; println!(" api collection cleared");
} else {
match harness::collection::parse_file(arg) {
Ok(eps) => { s.collection = Some(arg.to_string()); println!(" api collection: imported {} endpoint(s) from {arg} — tested as known surface", eps.len()); }
Err(e) => println!(" \x1b[33m⚠ {e}\x1b[0m"),
}
}
}
"/focus" | "/instructions" => {
if arg == "clear" { s.instructions = None; println!(" focus cleared"); continue; }
if arg.is_empty() { println!(" focus: {}", s.instructions.clone().unwrap_or_else(|| "(none)".into())); continue; }
@@ -1858,6 +1873,7 @@ async fn run(base: &Path, s: &Session, history: &mut Vec<RunRecord>) {
if let M::Grey { repo, .. } = &m {
cfg.repo = Some(repo.clone());
}
cfg.api_collection = s.collection.clone();
crate::apply_creds(&mut cfg, s.creds.as_deref()).await;
let mode = match &m { M::Grey { .. } => "greybox", M::White(_) => "white-box", M::Black(_) => "black-box" };
@@ -1938,6 +1954,7 @@ async fn start_background(base: &Path, s: &Session, reader: &mut Reader,
cfg.pinned = s.pinned.clone();
apply_session_budget(&mut cfg, s);
if matches!(mode_e, crate::Mode::Grey) { cfg.repo = s.repo.clone(); }
cfg.api_collection = s.collection.clone();
crate::apply_creds(&mut cfg, s.creds.as_deref()).await;
crate::subscription_preflight(&cfg).await; // warn early if the CLI isn't logged in
@@ -2324,6 +2341,8 @@ struct Snapshot {
repo: Option<String>,
auth: Option<String>,
creds: Option<String>,
#[serde(default)]
collection: Option<String>,
instructions: Option<String>,
#[serde(default)]
objective: Option<String>,
@@ -2346,6 +2365,7 @@ fn save_session(s: &Session) {
models: s.models.clone(), subscription: s.subscription, mcp: s.mcp,
vote_n: s.vote_n, max_agents: s.max_agents, target: s.target.clone(),
repo: s.repo.clone(), auth: s.auth.clone(), creds: s.creds.clone(),
collection: s.collection.clone(),
instructions: s.instructions.clone(),
objective: s.objective.clone(), out_of_scope: s.out_of_scope.clone(),
scope_in: s.policy.hard.iter().map(|p| p.as_text()).collect(),
@@ -2363,7 +2383,7 @@ fn load_session(s: &mut Session) -> bool {
if snap.vote_n > 0 { s.vote_n = snap.vote_n; }
s.max_agents = snap.max_agents;
s.target = snap.target; s.repo = snap.repo; s.auth = snap.auth;
s.creds = snap.creds; s.instructions = snap.instructions;
s.creds = snap.creds; s.collection = snap.collection; s.instructions = snap.instructions;
s.objective = snap.objective; s.out_of_scope = snap.out_of_scope;
if let Some(soft) = snap.soft { s.policy.soft = soft; }
for t in snap.scope_in { s.policy.allow(&t); }
@@ -2639,6 +2659,7 @@ fn show(s: &Session) {
println!(" │ repo : {}", s.repo.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ auth : {}", s.auth.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ creds : {}", s.creds.clone().unwrap_or_else(|| "(none)".into()));
println!(" │ collection: {}", s.collection.clone().unwrap_or_else(|| "(none — /collection to import Postman/OpenAPI/HAR)".into()));
println!(" │ proxy : {}", s.proxy.clone().unwrap_or_else(|| "(none — /proxy for Burp/ZAP)".into()));
println!(" │ user-agent: {}", s.user_agent.clone().unwrap_or_else(|| "NeuroSploit (default)".into()));
println!(" │ focus : {}", s.instructions.clone().unwrap_or_else(|| "(none — tests everything)".into()));
@@ -2703,6 +2724,7 @@ fn help() {
h("/repo <path|url>", "source repo (greybox) OR skill/n8n file/folder to audit (skills scope)");
h("/auth <value>", "auth header (Bearer/cookie/key). Roles: /auth admin <hdr> · /auth user <hdr>");
h("/creds <file.yaml>", "creds: jwt/header/cookie/login + ssh/windows + aws/gcp/azure + roles");
h("/collection <file>", "import an API collection (Postman/OpenAPI/HAR, JSON) as known surface to test");
h("/focus <text>", "steer the tests (or just type the instruction)");
h("/objective <text>", "engagement goal/context — shapes what agents prioritise & count as impact");
h("/scope-out <text>", "out-of-scope exclusions — host-shaped entries become ENFORCED denials");
@@ -0,0 +1,303 @@
//! Import an API collection as a test surface.
//!
//! An operator often already has the API mapped — a Postman collection, an
//! OpenAPI/Swagger spec, or a HAR capture from the browser. Rather than make
//! recon re-discover those endpoints (and maybe miss the undocumented or
//! auth-gated ones), we parse the collection into a flat endpoint list and
//! inject it into the engagement as known surface, so every agent tests exactly
//! those routes.
//!
//! Supported inputs (auto-detected by shape), JSON only:
//! - Postman Collection v2.x (`info` + `item[]`, nested folders allowed)
//! - OpenAPI 3.x / Swagger 2.0 (`openapi`/`swagger` + `paths{}`)
//! - HAR 1.2 (`log.entries[].request`)
//!
//! YAML OpenAPI is not parsed here (no YAML dependency) — convert to JSON first.
use anyhow::{anyhow, Result};
use serde_json::Value;
/// One endpoint recovered from a collection.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SeedEndpoint {
pub method: String,
pub url: String,
/// Short human hint (summary / body content-type / folder), may be empty.
pub note: String,
}
/// Which collection format a JSON document is, by shape.
fn detect(v: &Value) -> &'static str {
if v.get("log").and_then(|l| l.get("entries")).is_some() {
"har"
} else if v.get("openapi").is_some() || v.get("swagger").is_some() {
"openapi"
} else if v.get("item").is_some() || v.get("info").is_some() {
"postman"
} else {
"unknown"
}
}
/// Parse a collection file into a de-duplicated endpoint list.
pub fn parse_file(path: &str) -> Result<Vec<SeedEndpoint>> {
let raw = std::fs::read_to_string(path)
.map_err(|e| anyhow!("cannot read collection '{path}': {e}"))?;
parse_str(&raw)
}
/// Parse a collection from a JSON string.
pub fn parse_str(raw: &str) -> Result<Vec<SeedEndpoint>> {
let v: Value = serde_json::from_str(raw.trim())
.map_err(|e| anyhow!("collection is not valid JSON (YAML specs must be converted to JSON first): {e}"))?;
let mut out = match detect(&v) {
"har" => from_har(&v),
"openapi" => from_openapi(&v),
"postman" => from_postman(&v),
_ => return Err(anyhow!("unrecognised collection format — expected a Postman collection, an OpenAPI/Swagger spec, or a HAR capture (JSON)")),
};
dedup(&mut out);
if out.is_empty() {
return Err(anyhow!("no endpoints found in the collection"));
}
Ok(out)
}
fn dedup(items: &mut Vec<SeedEndpoint>) {
let mut seen = std::collections::HashSet::new();
items.retain(|e| seen.insert((e.method.clone(), e.url.clone())));
}
// ---- Postman v2.x --------------------------------------------------------
fn from_postman(v: &Value) -> Vec<SeedEndpoint> {
// Collection variables (incl. the common {{baseUrl}}) for substitution.
let mut vars = std::collections::HashMap::new();
if let Some(arr) = v.get("variable").and_then(|x| x.as_array()) {
for it in arr {
if let (Some(k), Some(val)) = (it.get("key").and_then(|x| x.as_str()), it.get("value").and_then(|x| x.as_str())) {
vars.insert(k.to_string(), val.to_string());
}
}
}
let mut out = Vec::new();
if let Some(items) = v.get("item").and_then(|x| x.as_array()) {
for it in items {
walk_postman(it, &vars, &mut out);
}
}
out
}
fn walk_postman(item: &Value, vars: &std::collections::HashMap<String, String>, out: &mut Vec<SeedEndpoint>) {
// A folder has its own `item[]`; recurse.
if let Some(children) = item.get("item").and_then(|x| x.as_array()) {
for c in children {
walk_postman(c, vars, out);
}
return;
}
let Some(req) = item.get("request") else { return };
let method = req.get("method").and_then(|x| x.as_str()).unwrap_or("GET").to_uppercase();
let url = postman_url(req.get("url"), vars);
if url.is_empty() {
return;
}
let note = item.get("name").and_then(|x| x.as_str()).unwrap_or("").to_string();
out.push(SeedEndpoint { method, url: subst_vars(&url, vars), note });
}
fn postman_url(url: Option<&Value>, vars: &std::collections::HashMap<String, String>) -> String {
match url {
Some(Value::String(s)) => s.clone(),
Some(o @ Value::Object(_)) => {
if let Some(raw) = o.get("raw").and_then(|x| x.as_str()) {
return raw.to_string();
}
// Reconstruct host + path when `raw` is absent.
let host = join_parts(o.get("host"), ".");
let path = join_parts(o.get("path"), "/");
let scheme = o.get("protocol").and_then(|x| x.as_str()).unwrap_or("https");
if host.is_empty() { return String::new(); }
let _ = vars;
format!("{scheme}://{host}/{path}")
}
_ => String::new(),
}
}
fn join_parts(v: Option<&Value>, sep: &str) -> String {
match v {
Some(Value::String(s)) => s.clone(),
Some(Value::Array(a)) => a.iter().filter_map(|x| x.as_str()).collect::<Vec<_>>().join(sep),
_ => String::new(),
}
}
fn subst_vars(s: &str, vars: &std::collections::HashMap<String, String>) -> String {
let mut out = s.to_string();
for (k, val) in vars {
out = out.replace(&format!("{{{{{k}}}}}"), val);
}
out
}
// ---- OpenAPI 3.x / Swagger 2.0 ------------------------------------------
fn from_openapi(v: &Value) -> Vec<SeedEndpoint> {
// Base URL: OpenAPI 3 `servers[0].url`, or Swagger 2 `schemes`+`host`+`basePath`.
let base = v.get("servers")
.and_then(|s| s.as_array())
.and_then(|a| a.first())
.and_then(|s| s.get("url"))
.and_then(|x| x.as_str())
.map(|s| s.to_string())
.unwrap_or_else(|| {
let scheme = v.get("schemes").and_then(|x| x.as_array()).and_then(|a| a.first()).and_then(|x| x.as_str()).unwrap_or("https");
let host = v.get("host").and_then(|x| x.as_str()).unwrap_or("");
let bp = v.get("basePath").and_then(|x| x.as_str()).unwrap_or("");
if host.is_empty() { String::new() } else { format!("{scheme}://{host}{bp}") }
});
let base = base.trim_end_matches('/').to_string();
const METHODS: [&str; 7] = ["get", "post", "put", "patch", "delete", "head", "options"];
let mut out = Vec::new();
if let Some(paths) = v.get("paths").and_then(|x| x.as_object()) {
for (path, ops) in paths {
let Some(ops) = ops.as_object() else { continue };
for (m, op) in ops {
if !METHODS.contains(&m.to_lowercase().as_str()) {
continue;
}
let note = op.get("summary").or_else(|| op.get("operationId")).and_then(|x| x.as_str()).unwrap_or("").to_string();
let url = if base.is_empty() { path.clone() } else { format!("{base}{path}") };
out.push(SeedEndpoint { method: m.to_uppercase(), url, note });
}
}
}
out
}
// ---- HAR 1.2 -------------------------------------------------------------
fn from_har(v: &Value) -> Vec<SeedEndpoint> {
let mut out = Vec::new();
if let Some(entries) = v.pointer("/log/entries").and_then(|x| x.as_array()) {
for e in entries {
let Some(req) = e.get("request") else { continue };
let method = req.get("method").and_then(|x| x.as_str()).unwrap_or("GET").to_uppercase();
let url = req.get("url").and_then(|x| x.as_str()).unwrap_or("").to_string();
if url.is_empty() {
continue;
}
let note = req.get("postData").and_then(|p| p.get("mimeType")).and_then(|x| x.as_str()).unwrap_or("").to_string();
out.push(SeedEndpoint { method, url, note });
}
}
out
}
// ---- rendering -----------------------------------------------------------
/// Render the endpoint list as a prompt block injected into every agent, so the
/// imported surface is tested directly. Capped so a huge collection can't blow
/// the context budget.
pub fn render_block(endpoints: &[SeedEndpoint], cap: usize) -> String {
if endpoints.is_empty() {
return String::new();
}
let shown = endpoints.len().min(cap);
let mut s = String::new();
s.push_str(&format!(
"IMPORTED API SURFACE — the operator supplied {} known endpoint(s) from an API collection (Postman/OpenAPI/HAR). TEST EVERY ONE of these in addition to what recon finds; they are in scope and often include undocumented or auth-gated routes recon would miss:\n",
endpoints.len()
));
for e in endpoints.iter().take(cap) {
if e.note.trim().is_empty() {
s.push_str(&format!(" - {} {}\n", e.method, e.url));
} else {
s.push_str(&format!(" - {} {} ({})\n", e.method, e.url, e.note.trim()));
}
}
if endpoints.len() > shown {
s.push_str(&format!(" … and {} more (see the collection file)\n", endpoints.len() - shown));
}
s
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_postman_v21_with_folders_and_vars() {
let raw = r#"{
"info": {"name": "api"},
"variable": [{"key": "baseUrl", "value": "https://api.example.com"}],
"item": [
{"name": "Auth", "item": [
{"name": "login", "request": {"method": "POST", "url": {"raw": "{{baseUrl}}/api/auth/login"}}}
]},
{"name": "search", "request": {"method": "GET", "url": {"raw": "{{baseUrl}}/api/search?q=x"}}}
]
}"#;
let eps = parse_str(raw).unwrap();
assert_eq!(eps.len(), 2);
assert!(eps.iter().any(|e| e.method == "POST" && e.url == "https://api.example.com/api/auth/login"));
assert!(eps.iter().any(|e| e.method == "GET" && e.url == "https://api.example.com/api/search?q=x"));
}
#[test]
fn parses_openapi3() {
let raw = r#"{
"openapi": "3.0.0",
"servers": [{"url": "https://api.example.com/v1"}],
"paths": {
"/users": {"get": {"summary": "list"}, "post": {"operationId": "create"}},
"/users/{id}": {"delete": {}}
}
}"#;
let eps = parse_str(raw).unwrap();
assert_eq!(eps.len(), 3);
assert!(eps.iter().any(|e| e.method == "GET" && e.url == "https://api.example.com/v1/users"));
assert!(eps.iter().any(|e| e.method == "DELETE" && e.url == "https://api.example.com/v1/users/{id}"));
}
#[test]
fn parses_swagger2() {
let raw = r#"{
"swagger": "2.0",
"host": "api.example.com",
"basePath": "/v2",
"schemes": ["https"],
"paths": {"/ping": {"get": {}}}
}"#;
let eps = parse_str(raw).unwrap();
assert_eq!(eps, vec![SeedEndpoint { method: "GET".into(), url: "https://api.example.com/v2/ping".into(), note: String::new() }]);
}
#[test]
fn parses_har_and_dedups() {
let raw = r#"{"log": {"entries": [
{"request": {"method": "GET", "url": "https://api.example.com/a"}},
{"request": {"method": "GET", "url": "https://api.example.com/a"}},
{"request": {"method": "POST", "url": "https://api.example.com/b", "postData": {"mimeType": "application/json"}}}
]}}"#;
let eps = parse_str(raw).unwrap();
assert_eq!(eps.len(), 2, "duplicate GET /a collapses");
}
#[test]
fn rejects_unknown_and_empty() {
assert!(parse_str(r#"{"hello": "world"}"#).is_err());
assert!(parse_str(r#"{"openapi": "3.0.0", "paths": {}}"#).is_err());
assert!(parse_str("not json").is_err());
}
#[test]
fn render_caps_long_lists() {
let eps: Vec<SeedEndpoint> = (0..300).map(|i| SeedEndpoint { method: "GET".into(), url: format!("https://x/{i}"), note: String::new() }).collect();
let block = render_block(&eps, 200);
assert!(block.contains("300 known endpoint"));
assert!(block.contains("and 100 more"));
}
}
+1
View File
@@ -17,6 +17,7 @@ pub mod budget;
pub mod capability;
pub mod chain;
pub mod claims;
pub mod collection;
pub mod compliance;
pub mod creds;
pub mod cvss;
@@ -176,6 +176,14 @@ fn operator_directives(cfg: &RunConfig) -> String {
if let Some(auth) = cfg.auth.as_deref().filter(|x| !x.trim().is_empty()) {
s.push_str(&format!("AUTHENTICATION — test as an authenticated user; send this with each request: {auth}\n"));
}
// Operator-supplied API collection (Postman/OpenAPI/HAR): inject the known
// endpoints as surface to test directly. Parse errors are reported once at
// launch (see the CLI), so render silently here.
if let Some(col) = cfg.api_collection.as_deref().filter(|x| !x.trim().is_empty()) {
if let Ok(eps) = crate::collection::parse_file(col) {
s.push_str(&crate::collection::render_block(&eps, 200));
}
}
// What the deterministic engine needs in order to confirm a class. Agents
// hold the target *now*; asking for a baseline/attack pair or a marker
// after the run is asking for something that no longer exists.
@@ -303,6 +303,11 @@ pub struct RunConfig {
/// gets the full run.
#[serde(default)]
pub budget: crate::budget::Budget,
/// Path to an API collection (Postman / OpenAPI / HAR, JSON) whose endpoints
/// are imported as known in-scope surface and injected into every agent so
/// they are tested directly, in addition to what recon discovers.
#[serde(default)]
pub api_collection: Option<String>,
}
fn default_vote() -> usize {
@@ -363,6 +368,7 @@ impl RunConfig {
authz_matrix: None,
creds_file: None,
neo4j: None,
api_collection: None,
}
}
}