Added Cap::DATA_EXFIL and taint fp and fn fixes on real repos (#59)

* feat: Enhance data exfiltration detection with source sensitivity gating for cookies and headers

* feat: Implement cross-file data exfiltration detection with parameter-specific gate filters

* feat: Add calibration tests and refine DATA_EXFIL severity scoring logic

* feat: Introduce per-detector configuration for data exfiltration suppression

* feat: Enhance DATA_EXFIL findings with destination field tracking in diagnostics and SARIF output

* feat: Add tainted body and URL handling for data exfiltration detection

* feat: Add integration tests and fixtures for DATA_EXFIL and SSRF detection in Go

* feat: Add Java integration tests and fixtures for DATA_EXFIL detection across multiple HTTP clients

* feat: Add synthetic externals handling for closure-captured variables in SSA

* feat: Implement closure-based suppression for resource leak findings

* feat: Add regression guards for shell-injection and taint propagation in for-of destructure patterns

* feat: Implement constructor cap narrowing for data exfiltration detection in HTTP request builders

* feat: Add gated sinks for data exfiltration detection in C and C++ using curl_easy_setopt

* feat: Implement DATA_EXFIL cap parity for backwards analysis and add integration tests

* feat: Add data exfiltration sinks for various languages and enhance documentation

* refactor: Simplify formatting and improve readability in various files

* refactor: Improve readability by simplifying conditional statements and adding clippy linting

* docs: Update CHANGELOG and comments for data exfiltration features and configuration

* docs: Clarify configuration instructions for data exfiltration trusted destinations

* docs: Enhance comments for evidence routing logic in data exfiltration
This commit is contained in:
Eli Peter 2026-05-01 10:59:52 -04:00 committed by GitHub
parent a438886217
commit 58f1794a4e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
189 changed files with 8421 additions and 383 deletions

View file

@ -217,6 +217,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
}
}

View file

@ -638,6 +638,8 @@ mod tests {
exception_edges: Vec::new(),
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
}
}

View file

@ -215,6 +215,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let (eliminated, copy_map) = copy_propagate(&mut body, &cfg);
@ -296,6 +298,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let (eliminated, copy_map) = copy_propagate(&mut body, &cfg);
@ -366,6 +370,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
(cfg, body)
}
@ -488,6 +494,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let (eliminated, _map) = copy_propagate(&mut body, &cfg);
assert_eq!(eliminated, 0, "two-operand Assign is not a copy");
@ -567,6 +575,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let (eliminated, _) = copy_propagate(&mut body, &cfg);
assert_eq!(eliminated, 1, "v1 should be eliminated");
@ -664,6 +674,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let (eliminated, _map) = copy_propagate(&mut body, &cfg);
assert_eq!(eliminated, 1);
@ -712,6 +724,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let (eliminated, map) = copy_propagate(&mut body, &cfg);
assert_eq!(eliminated, 0);

View file

@ -217,6 +217,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -265,6 +267,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -314,6 +318,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -359,6 +365,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -396,6 +404,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -460,6 +470,8 @@ mod tests {
exception_edges: vec![],
field_interner: interner,
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -527,6 +539,8 @@ mod tests {
exception_edges: vec![],
field_interner: interner,
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -587,6 +601,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -637,6 +653,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -724,6 +742,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);
@ -801,6 +821,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let removed = eliminate_dead_defs(&mut body, &cfg);

View file

@ -788,6 +788,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
@ -835,6 +837,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
@ -885,6 +889,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(
@ -913,6 +919,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let errs = check_structural_invariants(&body);
assert!(

View file

@ -4,7 +4,7 @@ use crate::ssa::type_facts::TypeKind;
use petgraph::graph::NodeIndex;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
/// Unique identifier for an SSA value (one per definition point).
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
@ -353,6 +353,26 @@ pub struct SsaBody {
/// cleanly with an empty map (no migration needed).
#[serde(default)]
pub field_writes: HashMap<SsaValue, (SsaValue, FieldId)>,
/// SSA values that lowering injected for **free / closure-captured**
/// variables (variables referenced by the body but not declared as
/// formal parameters and not assigned within the body).
///
/// Lowering models every external use as an [`SsaOp::Param`] in block
/// 0 so the rename pass can reference it. Real formal parameters and
/// closure captures end up using the same op variant; this side-table
/// distinguishes the two so downstream analyses (in particular the
/// JS/TS handler-name auto-seed in
/// [`crate::taint::ssa_transfer`]) can avoid treating closure
/// captures as if they were the function's own parameters. Without
/// this distinction, a lambda body that references an out-of-scope
/// `userId` / `cmd` / `payload` would have the synthetic Param
/// auto-seeded as `UserInput`, producing a phantom source on the
/// enclosing function's declaration line.
///
/// `#[serde(default)]` for backward compatibility with summary blobs
/// produced before this field existed.
#[serde(default)]
pub synthetic_externals: HashSet<SsaValue>,
}
impl SsaBody {
@ -560,6 +580,7 @@ mod tests {
exception_edges: vec![],
field_interner: FieldInterner::new(),
field_writes: HashMap::new(),
synthetic_externals: HashSet::new(),
};
let fid = body.intern_field("mu");
body.blocks[0].body.push(SsaInst {

View file

@ -239,18 +239,25 @@ fn lower_to_ssa_inner(
// 6. Rename variables (dominator tree preorder walk)
let dom_tree_children = build_dom_tree_children(num_blocks, &doms, &block_graph);
let (mut ssa_blocks, mut value_defs, cfg_node_map, field_interner, field_writes) =
rename_variables(
cfg,
&blocks_nodes,
&block_succs,
&block_preds,
&phi_placements,
&dom_tree_children,
&filtered_edges,
&external_vars,
&nop_nodes,
);
let (
mut ssa_blocks,
mut value_defs,
cfg_node_map,
field_interner,
field_writes,
synthetic_externals,
) = rename_variables(
cfg,
&blocks_nodes,
&block_succs,
&block_preds,
&phi_placements,
&dom_tree_children,
&filtered_edges,
&external_vars,
formal_params,
&nop_nodes,
);
// 6b. Fill any missing phi operands with a shared Undef sentinel so
// every phi has exactly one operand per predecessor. See
@ -306,6 +313,7 @@ fn lower_to_ssa_inner(
exception_edges,
field_interner,
field_writes,
synthetic_externals,
};
// 9. Catch-block reachability invariant.
@ -927,6 +935,7 @@ fn rename_variables(
dom_tree_children: &[Vec<usize>],
filtered_edges: &[(NodeIndex, NodeIndex, EdgeKind)],
external_vars: &[String],
formal_params: &[String],
nop_nodes: &HashSet<NodeIndex>,
) -> (
Vec<SsaBlock>,
@ -934,6 +943,7 @@ fn rename_variables(
HashMap<NodeIndex, SsaValue>,
crate::ssa::ir::FieldInterner,
HashMap<SsaValue, (SsaValue, crate::ssa::ir::FieldId)>,
HashSet<SsaValue>,
) {
let num_blocks = blocks_nodes.len();
let mut next_value: u32 = 0;
@ -1679,6 +1689,27 @@ fn rename_variables(
// Inject synthetic Param instructions at START of block 0 for external variables.
// These create SSA definitions so the rename pass can reference them.
// Pre-seed var_stacks so process_block sees them.
//
// `external_vars` contains both real formal parameters and free / closure-
// captured variables (variables read by the body but not declared as a
// formal and not assigned anywhere). Both end up emitted as
// [`SsaOp::Param`] in block 0; we record the SSA values that correspond
// to free vars in `synthetic_externals` so downstream analyses (the JS/TS
// handler-name auto-seed in particular) can avoid treating closure
// captures as if they were parameters of the function under analysis.
//
// **Conservative behaviour when `formal_params` is empty.** Several
// call sites (`lower_to_ssa`, `lower_to_ssa_scoped_nop`) don't supply
// formal parameter names; in that case we cannot distinguish formals
// from free vars structurally, so we leave `synthetic_externals` empty
// and the auto-seed pass keeps its pre-fix behaviour of treating every
// `Param` op as a candidate. Only callers that pass a non-empty
// `formal_params` slice (`lower_to_ssa_with_params`, used by the
// findings pipeline's per-function lowering) opt into the
// closure-capture distinction.
let mut synthetic_externals: HashSet<SsaValue> = HashSet::new();
let formal_set: HashSet<&str> = formal_params.iter().map(|s| s.as_str()).collect();
let track_synthetic = !formal_params.is_empty();
if !external_vars.is_empty() {
let entry_cfg_node = blocks_nodes[0][0];
let mut synthetic_body = Vec::with_capacity(external_vars.len());
@ -1691,7 +1722,8 @@ fn rename_variables(
cfg_node: entry_cfg_node,
block: BlockId(0),
});
let op = if is_receiver_name(var) {
let is_receiver = is_receiver_name(var);
let op = if is_receiver {
SsaOp::SelfParam
} else {
let op = SsaOp::Param {
@ -1700,6 +1732,28 @@ fn rename_variables(
positional_idx += 1;
op
};
// A non-receiver var is "synthetic" (a free / closure capture)
// when it is *not* one of the function's declared formals AND
// not a dotted access on a formal (`input.cmd` where `input` is
// a formal — it represents a structural projection of the
// formal, not a free variable; the auto-seed should still treat
// it as part of the formal's own taint surface). Receivers are
// intentionally excluded: `this` / `self` represent the implicit
// receiver, which always belongs to the function.
//
// Only fire when the caller supplied formal-parameter names; see
// the `track_synthetic` rationale above.
let root_is_formal = var
.split_once('.')
.map(|(root, _)| formal_set.contains(root))
.unwrap_or(false);
if track_synthetic
&& !is_receiver
&& !formal_set.contains(var.as_str())
&& !root_is_formal
{
synthetic_externals.insert(v);
}
synthetic_body.push(SsaInst {
value: v,
op,
@ -1784,6 +1838,7 @@ fn rename_variables(
cfg_node_map,
field_interner,
field_writes,
synthetic_externals,
)
}

View file

@ -417,6 +417,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
}
}

View file

@ -440,6 +440,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let cfg: Cfg = Graph::new();
let const_values = HashMap::new();

View file

@ -25,6 +25,15 @@ pub enum TypeKind {
FileHandle,
Url,
HttpClient,
/// A pre-network HTTP request builder produced by `Client::post(url)`,
/// `surf::post(url)`, `Request::builder()`, `ureq::post(url)`, etc.
/// The body-bind methods (`body`, `json`, `form`, `multipart`,
/// `body_string`, `body_json`, `body_bytes`) and terminal verbs
/// (`send`, `send_string`, `send_json`, `send_form`) are sinks for
/// `DATA_EXFIL` when receiver-typed. Distinct from `HttpClient` so
/// type-qualified resolution can attach builder-only rules without
/// over-firing on plain client objects.
RequestBuilder,
/// A local, in-memory collection (HashMap, HashSet, Vec, etc.).
/// The auth sink gate uses this so calls like `map.insert(...)`
/// are treated as bookkeeping rather than cross-tenant sinks. No
@ -76,6 +85,7 @@ impl TypeKind {
Self::DatabaseConnection => Some("DatabaseConnection"),
Self::FileHandle => Some("FileHandle"),
Self::Url => Some("URL"),
Self::RequestBuilder => Some("RequestBuilder"),
_ => None,
}
}
@ -180,9 +190,10 @@ impl TypeFactResult {
///
/// Suppression policy:
/// * [`TypeKind::Int`] (and float, treated as numeric): suppresses
/// `SQL_QUERY`, `FILE_IO`, `SHELL_ESCAPE`, `HTML_ESCAPE`, `SSRF` ,
/// numeric values cannot carry the metacharacters required to drive
/// any of these injection classes.
/// `SQL_QUERY`, `FILE_IO`, `SHELL_ESCAPE`, `HTML_ESCAPE`, `SSRF`,
/// `DATA_EXFIL`, numeric values cannot carry the metacharacters
/// required to drive any of these injection classes, nor can they
/// encode credentials/tokens that meaningfully constitute leakage.
/// * [`TypeKind::Bool`]: suppresses every type-suppressible bit ,
/// `true`/`false` cannot carry a payload of any kind.
pub fn is_type_safe_for_sink(
@ -191,8 +202,12 @@ pub fn is_type_safe_for_sink(
type_facts: &TypeFactResult,
) -> bool {
use crate::labels::Cap;
let type_suppressible =
Cap::SQL_QUERY | Cap::FILE_IO | Cap::SHELL_ESCAPE | Cap::HTML_ESCAPE | Cap::SSRF;
let type_suppressible = Cap::SQL_QUERY
| Cap::FILE_IO
| Cap::SHELL_ESCAPE
| Cap::HTML_ESCAPE
| Cap::SSRF
| Cap::DATA_EXFIL;
if !sink_caps.intersects(type_suppressible) {
return false;
}
@ -224,6 +239,13 @@ pub(crate) fn constructor_type(lang: Lang, callee: &str) -> Option<TypeKind> {
"newHttpClient" | "newBuilder" if callee.contains("HttpClient") => {
Some(TypeKind::HttpClient)
}
// Apache HttpClient idiomatic factory:
// `CloseableHttpClient client = HttpClients.createDefault();`
// `HttpClients` contains the substring `HttpClient` so this
// doesn't widen to unrelated `createDefault` calls.
"createDefault" | "custom" if callee.contains("HttpClient") => {
Some(TypeKind::HttpClient)
}
"OkHttpClient" | "WebClient" | "RestTemplate" => Some(TypeKind::HttpClient),
"getConnection" => Some(TypeKind::DatabaseConnection),
"MongoClient" => Some(TypeKind::DatabaseConnection),
@ -340,6 +362,10 @@ pub(crate) fn constructor_type(lang: Lang, callee: &str) -> Option<TypeKind> {
// so the auth sink gate recognises
// `let x = factory_fn(); x.insert(..)`.
Some(TypeKind::LocalCollection)
} else if is_rust_request_builder_constructor(base) {
// HTTP request-builder constructors across reqwest, surf,
// ureq, hyper. See [`is_rust_request_builder_constructor`].
Some(TypeKind::RequestBuilder)
} else {
None
}
@ -449,6 +475,54 @@ fn is_rust_local_collection_constructor(base: &str) -> bool {
})
}
/// Does the peeled Rust callee correspond to a known HTTP request-builder
/// constructor / factory? Covers:
/// * surf free verbs (`surf::post`, `surf::get`, ...) ,
/// * ureq free verbs (`ureq::post`, ...) ,
/// * hyper `Request::builder` ,
/// * reqwest `Client::post(url)` / `Client::get(url)` etc. (the `Client`
/// instance is itself an `HttpClient` but the verb call on it returns a
/// `RequestBuilder` whose chained methods bind body/json/form/etc.).
///
/// reqwest's `Client::new` keeps its existing `HttpClient` mapping ,
/// it produces the client, not a builder.
fn is_rust_request_builder_constructor(base: &str) -> bool {
// surf free verbs that return Request (acts as a builder).
const SURF_VERBS: &[&str] = &[
"post", "get", "put", "delete", "patch", "head", "connect", "trace",
];
if SURF_VERBS
.iter()
.any(|v| base.ends_with(&format!("surf::{v}")))
{
return true;
}
// ureq free verbs that return Request.
const UREQ_VERBS: &[&str] = &["post", "get", "put", "delete", "patch", "head"];
if UREQ_VERBS
.iter()
.any(|v| base.ends_with(&format!("ureq::{v}")))
{
return true;
}
// hyper request builder.
if base.ends_with("Request::builder") || base.ends_with("hyper::Request::builder") {
return true;
}
// reqwest Client verb-on-instance. `Client::post(url)` /
// `Client::get(url)` chained-form returns a RequestBuilder. We match
// the constructor-style segment used by chain text after CFG receiver
// collapse (`reqwest::Client::new.post`, `Client::post`, etc.).
const REQWEST_CLIENT_VERBS: &[&str] =
&["post", "get", "put", "delete", "patch", "head", "request"];
if REQWEST_CLIENT_VERBS.iter().any(|v| {
base.ends_with(&format!("Client::new.{v}")) || base.ends_with(&format!("Client::{v}"))
}) {
return true;
}
false
}
pub fn is_identity_method(callee: &str) -> bool {
let suffix = callee.rsplit(['.', ':']).next().unwrap_or(callee);
matches!(
@ -1076,6 +1150,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let consts = HashMap::from([
@ -1189,6 +1265,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let consts = HashMap::new();
@ -1220,9 +1298,10 @@ mod tests {
}
/// Int-typed values must suppress every type-suppressible
/// cap, including the freshly-added `SSRF` bit. Numeric IDs
/// cannot rewrite a URL host, cannot form path traversal sequences,
/// cannot carry SQL/HTML/shell metacharacters.
/// cap, including the freshly-added `SSRF` and `DATA_EXFIL` bits.
/// Numeric IDs cannot rewrite a URL host, cannot form path
/// traversal sequences, cannot carry SQL/HTML/shell metacharacters,
/// and do not encode credentials worth exfiltrating.
#[test]
fn int_suppresses_every_type_suppressible_cap() {
use crate::labels::Cap;
@ -1236,6 +1315,7 @@ mod tests {
Cap::SHELL_ESCAPE,
Cap::HTML_ESCAPE,
Cap::SSRF,
Cap::DATA_EXFIL,
] {
assert!(
is_type_safe_for_sink(&[SsaValue(0)], cap, &result),
@ -1271,6 +1351,7 @@ mod tests {
Cap::SHELL_ESCAPE,
Cap::HTML_ESCAPE,
Cap::SSRF,
Cap::DATA_EXFIL,
] {
assert!(
is_type_safe_for_sink(&[SsaValue(0)], cap, &result),
@ -1307,14 +1388,14 @@ mod tests {
/// `is_type_safe_for_sink` requires an intentional matrix edit + a
/// test update. Truth values:
///
/// | TypeKind | SQL | FILE | SHELL | HTML | SSRF | CODE_EXEC | DESERIALIZE |
/// |-----------|-----|------|-------|------|------|-----------|-------------|
/// | Int | Y | Y | Y | Y | Y | N | N |
/// | Bool | Y | Y | Y | Y | Y | N | N |
/// | String | N | N | N | N | N | N | N |
/// | Url | N | N | N | N | N | N | N |
/// | Object | N | N | N | N | N | N | N |
/// | Unknown | N | N | N | N | N | N | N |
/// | TypeKind | SQL | FILE | SHELL | HTML | SSRF | DATA_EXFIL | CODE_EXEC | DESERIALIZE |
/// |-----------|-----|------|-------|------|------|------------|-----------|-------------|
/// | Int | Y | Y | Y | Y | Y | Y | N | N |
/// | Bool | Y | Y | Y | Y | Y | Y | N | N |
/// | String | N | N | N | N | N | N | N | N |
/// | Url | N | N | N | N | N | N | N | N |
/// | Object | N | N | N | N | N | N | N | N |
/// | Unknown | N | N | N | N | N | N | N | N |
#[test]
fn type_kind_cap_suppression_matrix() {
use crate::labels::Cap;
@ -1324,40 +1405,41 @@ mod tests {
("SHELL_ESCAPE", Cap::SHELL_ESCAPE),
("HTML_ESCAPE", Cap::HTML_ESCAPE),
("SSRF", Cap::SSRF),
("DATA_EXFIL", Cap::DATA_EXFIL),
("CODE_EXEC", Cap::CODE_EXEC),
("DESERIALIZE", Cap::DESERIALIZE),
];
// (kind_name, kind, [suppress for each cap in `caps` order])
let rows: &[(&str, TypeKind, [bool; 7])] = &[
let rows: &[(&str, TypeKind, [bool; 8])] = &[
(
"Int",
TypeKind::Int,
[true, true, true, true, true, false, false],
[true, true, true, true, true, true, false, false],
),
(
"Bool",
TypeKind::Bool,
[true, true, true, true, true, false, false],
[true, true, true, true, true, true, false, false],
),
(
"String",
TypeKind::String,
[false, false, false, false, false, false, false],
[false, false, false, false, false, false, false, false],
),
(
"Url",
TypeKind::Url,
[false, false, false, false, false, false, false],
[false, false, false, false, false, false, false, false],
),
(
"Object",
TypeKind::Object,
[false, false, false, false, false, false, false],
[false, false, false, false, false, false, false, false],
),
(
"Unknown",
TypeKind::Unknown,
[false, false, false, false, false, false, false],
[false, false, false, false, false, false, false, false],
),
];
for (kind_name, kind, expected) in rows {
@ -1389,6 +1471,7 @@ mod tests {
Cap::SHELL_ESCAPE,
Cap::HTML_ESCAPE,
Cap::SSRF,
Cap::DATA_EXFIL,
Cap::CODE_EXEC,
Cap::DESERIALIZE,
] {
@ -1487,6 +1570,8 @@ mod tests {
exception_edges: vec![],
field_interner: crate::ssa::ir::FieldInterner::default(),
field_writes: std::collections::HashMap::new(),
synthetic_externals: std::collections::HashSet::new(),
};
let consts = HashMap::new();