mirror of
https://github.com/elicpeter/nyx.git
synced 2026-06-27 20:29:39 +02:00
[pitboss/grind] deferred session-0035 (20260517T044708Z-e058)
This commit is contained in:
parent
f4ef3a8ffc
commit
dc0cff58c7
1 changed files with 122 additions and 8 deletions
|
|
@ -383,6 +383,42 @@ pub fn derive_from_rule_namespace_with(
|
||||||
evidence: &crate::evidence::Evidence,
|
evidence: &crate::evidence::Evidence,
|
||||||
summaries: Option<&GlobalSummaries>,
|
summaries: Option<&GlobalSummaries>,
|
||||||
) -> Option<HarnessSpec> {
|
) -> Option<HarnessSpec> {
|
||||||
|
// Path is required to locate the sink and to extension-check the lang.
|
||||||
|
if diag.path.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Language-agnostic `taint-*` rule ids (e.g. `taint-ldap-injection`,
|
||||||
|
// `taint-sql-injection`, `taint-data-exfiltration`) carry the cap in the
|
||||||
|
// rule slug itself; the language comes from the file extension. Try this
|
||||||
|
// shortcut first so taint findings with no flow_steps can still derive.
|
||||||
|
if let Some(taint_cap) = cap_for_taint_rule_id(&diag.id) {
|
||||||
|
let lang = lang_from_path(&diag.path)?;
|
||||||
|
let expected_cap = {
|
||||||
|
let from_ev = Cap::from_bits_truncate(evidence.sink_caps);
|
||||||
|
if !from_ev.is_empty() {
|
||||||
|
from_ev
|
||||||
|
} else {
|
||||||
|
taint_cap
|
||||||
|
}
|
||||||
|
};
|
||||||
|
if expected_cap.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let entry_function = resolve_enclosing_function(diag, evidence, summaries, lang)
|
||||||
|
.unwrap_or_else(|| "<unknown>".to_owned());
|
||||||
|
return Some(finalize_spec(
|
||||||
|
diag,
|
||||||
|
diag.path.clone(),
|
||||||
|
entry_function,
|
||||||
|
lang,
|
||||||
|
expected_cap,
|
||||||
|
diag.path.clone(),
|
||||||
|
diag.line as u32,
|
||||||
|
SpecDerivationStrategy::FromRuleNamespace,
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let mut iter = diag.id.split('.');
|
let mut iter = diag.id.split('.');
|
||||||
let lang_prefix = iter.next()?;
|
let lang_prefix = iter.next()?;
|
||||||
let category = iter.next()?;
|
let category = iter.next()?;
|
||||||
|
|
@ -405,10 +441,6 @@ pub fn derive_from_rule_namespace_with(
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Path is required to locate the sink and to extension-check the lang.
|
|
||||||
if diag.path.is_empty() {
|
|
||||||
return None;
|
|
||||||
}
|
|
||||||
// Cross-check: the diag's file extension must agree with the rule's
|
// Cross-check: the diag's file extension must agree with the rule's
|
||||||
// language prefix when both are available. Disagreement is a stronger
|
// language prefix when both are available. Disagreement is a stronger
|
||||||
// signal of a mis-rooted finding than a missing extension.
|
// signal of a mis-rooted finding than a missing extension.
|
||||||
|
|
@ -433,6 +465,23 @@ pub fn derive_from_rule_namespace_with(
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Map a language-agnostic `taint-*` rule id (as registered in
|
||||||
|
/// [`crate::labels::CAP_RULE_REGISTRY`]) to its [`Cap`].
|
||||||
|
///
|
||||||
|
/// Returns `None` for rule ids that are not registered as a class entry,
|
||||||
|
/// including the legacy generic `taint-unsanitised-flow` (which is not in
|
||||||
|
/// the registry — its findings carry their actual cap through evidence,
|
||||||
|
/// not the rule slug).
|
||||||
|
fn cap_for_taint_rule_id(rule_id: &str) -> Option<Cap> {
|
||||||
|
if !rule_id.starts_with("taint-") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
crate::labels::CAP_RULE_REGISTRY
|
||||||
|
.iter()
|
||||||
|
.find(|meta| meta.rule_id == rule_id)
|
||||||
|
.map(|meta| meta.cap)
|
||||||
|
}
|
||||||
|
|
||||||
// ── Strategy 3: walk a FuncSummary for the sink's enclosing function ─────────
|
// ── Strategy 3: walk a FuncSummary for the sink's enclosing function ─────────
|
||||||
|
|
||||||
/// Build a spec by walking `summary` (the sink's enclosing function) for any
|
/// Build a spec by walking `summary` (the sink's enclosing function) for any
|
||||||
|
|
@ -1460,11 +1509,13 @@ mod tests {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn rule_namespace_strategy_skips_legacy_taint_ids() {
|
fn rule_namespace_strategy_skips_unknown_taint_ids() {
|
||||||
use crate::labels::Cap;
|
use crate::labels::Cap;
|
||||||
// `taint-...` is *not* a language-namespace prefix; rule-namespace
|
// Unregistered `taint-*` rule slugs (e.g. the legacy generic
|
||||||
// strategy must skip it so the next strategy can try.
|
// `taint-unsanitised-flow`) are not in `CAP_RULE_REGISTRY`; the
|
||||||
let diag = diag_with_rule_id("taint-unsanitised-flow", "app/handler.py", Cap::SHELL_ESCAPE.bits());
|
// shortcut must skip them so downstream strategies can try.
|
||||||
|
let diag =
|
||||||
|
diag_with_rule_id("taint-unsanitised-flow", "app/handler.py", Cap::SHELL_ESCAPE.bits());
|
||||||
// No flow_steps, no http/cli marker → ends in SpecDerivationFailed.
|
// No flow_steps, no http/cli marker → ends in SpecDerivationFailed.
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
HarnessSpec::from_finding(&diag).unwrap_err(),
|
HarnessSpec::from_finding(&diag).unwrap_err(),
|
||||||
|
|
@ -1472,6 +1523,69 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_namespace_strategy_resolves_registered_taint_ldap_injection() {
|
||||||
|
use crate::labels::Cap;
|
||||||
|
// Java OWASP fixtures emit `taint-ldap-injection` with no flow_steps;
|
||||||
|
// the rule slug carries the cap, the file extension carries the lang.
|
||||||
|
let diag = diag_with_rule_id(
|
||||||
|
"taint-ldap-injection",
|
||||||
|
"src/main/java/org/owasp/benchmark/Vuln.java",
|
||||||
|
Cap::LDAP_INJECTION.bits(),
|
||||||
|
);
|
||||||
|
let spec = HarnessSpec::from_finding(&diag).unwrap();
|
||||||
|
assert_eq!(spec.derivation, SpecDerivationStrategy::FromRuleNamespace);
|
||||||
|
assert_eq!(spec.lang, Lang::Java);
|
||||||
|
assert_eq!(spec.expected_cap, Cap::LDAP_INJECTION);
|
||||||
|
assert_eq!(spec.sink_line, 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_namespace_strategy_taint_id_falls_back_to_registry_cap_when_evidence_zero() {
|
||||||
|
use crate::labels::Cap;
|
||||||
|
// sink_caps=0 → use the cap from `CAP_RULE_REGISTRY`.
|
||||||
|
let diag = diag_with_rule_id("taint-sql-injection", "app/handler.py", 0);
|
||||||
|
let spec = HarnessSpec::from_finding(&diag).unwrap();
|
||||||
|
assert_eq!(spec.derivation, SpecDerivationStrategy::FromRuleNamespace);
|
||||||
|
assert_eq!(spec.lang, Lang::Python);
|
||||||
|
assert_eq!(spec.expected_cap, Cap::SQL_QUERY);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_namespace_strategy_taint_id_lang_follows_path_extension() {
|
||||||
|
use crate::labels::Cap;
|
||||||
|
// Same rule slug, different file extension → derives a Go spec.
|
||||||
|
let diag =
|
||||||
|
diag_with_rule_id("taint-data-exfiltration", "cmd/leak.go", Cap::DATA_EXFIL.bits());
|
||||||
|
let spec = HarnessSpec::from_finding(&diag).unwrap();
|
||||||
|
assert_eq!(spec.derivation, SpecDerivationStrategy::FromRuleNamespace);
|
||||||
|
assert_eq!(spec.lang, Lang::Go);
|
||||||
|
assert_eq!(spec.expected_cap, Cap::DATA_EXFIL);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn rule_namespace_strategy_taint_id_requires_path() {
|
||||||
|
use crate::labels::Cap;
|
||||||
|
// Path empty → cannot infer lang; strategy bails so callgraph-entry
|
||||||
|
// can try.
|
||||||
|
let diag = crate::commands::scan::Diag {
|
||||||
|
id: "taint-ldap-injection".into(),
|
||||||
|
path: String::new(),
|
||||||
|
line: 12,
|
||||||
|
col: 4,
|
||||||
|
confidence: Some(Confidence::Medium),
|
||||||
|
evidence: Some(Evidence {
|
||||||
|
sink_caps: Cap::LDAP_INJECTION.bits(),
|
||||||
|
..Default::default()
|
||||||
|
}),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
assert_eq!(
|
||||||
|
HarnessSpec::from_finding(&diag).unwrap_err(),
|
||||||
|
UnsupportedReason::SpecDerivationFailed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn func_summary_strategy_picks_first_tainted_param() {
|
fn func_summary_strategy_picks_first_tainted_param() {
|
||||||
use crate::labels::Cap;
|
use crate::labels::Cap;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue