use anyhow::Result;
use regex::bytes::Regex;
pub use kingfisher_scanner::__cli_internals::parser::*;
pub fn verify_match_in_context(
source: &[u8],
language: &Language,
re: &Regex,
expected_secret: &[u8],
) -> Result<bool> {
use kingfisher_scanner::primitives::find_secret_capture;
let mut verified = false;
stream_context_candidates(source, language, |text| {
verified = re
.captures_iter(text.as_bytes())
.any(|captures| find_secret_capture(re, &captures).as_bytes() == expected_secret);
!verified
})?;
Ok(verified)
}
#[cfg(test)]
mod tests {
use std::{
collections::BTreeMap,
fs,
path::{Path, PathBuf},
};
use super::*;
fn fixture_cases() -> Vec<(Language, &'static str)> {
vec![
(Language::Bash, "testdata/shell_vulnerable.sh"),
(Language::C, "testdata/c_vulnerable.c"),
(Language::CSharp, "testdata/csharp_vulnerable.cs"),
(Language::Cpp, "testdata/cpp_vulnerable.cpp"),
(Language::Css, "testdata/css_vulnerable.css"),
(Language::Go, "testdata/go_vulnerable.go"),
(Language::Html, "testdata/html_embedded_vulnerable.html"),
(Language::Html, "testdata/html_vulnerable.html"),
(Language::Java, "testdata/java_vulnerable.java"),
(Language::JavaScript, "testdata/javascript_vulnerable.js"),
(Language::Php, "testdata/php_vulnerable.php"),
(Language::Python, "testdata/parsers/comment_only_context.py"),
(Language::Python, "testdata/python_vulnerable.py"),
(Language::Ruby, "testdata/ruby_vulnerable.rb"),
(Language::Rust, "testdata/rust_vulnerable.rs"),
(Language::Toml, "testdata/toml_vulnerable.toml"),
(Language::TypeScript, "testdata/typescript_vulnerable.ts"),
(Language::Yaml, "testdata/yaml_vulnerable.yaml"),
]
}
fn current_capture_texts(
root: &Path,
cases: &[(Language, &'static str)],
) -> BTreeMap<String, Vec<String>> {
let mut current = BTreeMap::new();
for (language, rel_path) in cases {
let file_path = root.join(rel_path);
let source = fs::read(&file_path)
.unwrap_or_else(|e| panic!("failed to read fixture {}: {e}", file_path.display()));
let mut texts = Vec::new();
stream_context_candidates(&source, language, |text| {
texts.push(text.to_string());
true
})
.unwrap_or_else(|e| panic!("context verifier failed for {}: {e}", rel_path));
current.insert(format!("{}:{}", language.name(), rel_path), texts);
}
current
}
#[test]
fn context_verifier_outputs_are_superset_of_golden() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let baseline_path = root.join("testdata/parsers/context_verifier_golden.json");
let cases = fixture_cases();
let current = current_capture_texts(&root, &cases);
if std::env::var("UPDATE_CONTEXT_VERIFIER_GOLDEN").as_deref() == Ok("1") {
let payload = serde_json::to_string_pretty(¤t)
.unwrap_or_else(|e| panic!("failed to serialize golden output: {e}"));
fs::write(&baseline_path, format!("{payload}\n")).unwrap_or_else(|e| {
panic!("failed to write golden output {}: {e}", baseline_path.display())
});
return;
}
let baseline_raw = fs::read_to_string(&baseline_path).unwrap_or_else(|e| {
panic!(
"failed to read golden output {}: {e}. Run with UPDATE_CONTEXT_VERIFIER_GOLDEN=1",
baseline_path.display()
)
});
let baseline: BTreeMap<String, Vec<String>> = serde_json::from_str(&baseline_raw)
.unwrap_or_else(|e| panic!("invalid golden JSON {}: {e}", baseline_path.display()));
let mut regressions = Vec::new();
for (key, expected_texts) in &baseline {
let actual_texts = current.get(key).unwrap_or_else(|| {
panic!("missing fixture key {key}. Run with UPDATE_CONTEXT_VERIFIER_GOLDEN=1")
});
for expected in expected_texts {
if !actual_texts.contains(expected) {
regressions.push(format!(" {key}: missing candidate: {expected:?}"));
}
}
}
assert!(
regressions.is_empty(),
"context verifier regression(s) — golden candidates no longer emitted:\n{}",
regressions.join("\n")
);
}
#[test]
fn html_embedded_context_extracts_script_and_style_candidates() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = fs::read(root.join("testdata/html_embedded_vulnerable.html")).unwrap();
let mut texts = Vec::new();
stream_context_candidates(&source, &Language::Html, |text| {
texts.push(text.to_string());
true
})
.unwrap();
assert!(
texts.iter().any(|text| text.contains("auth0_client_secret =")),
"expected script extraction to emit auth0_client_secret candidate"
);
assert!(
texts.iter().any(|text| text.contains("content =")),
"expected style extraction to emit CSS content candidate"
);
}
#[test]
fn html_embedded_context_extracts_uppercase_script_and_style_candidates() {
let source = br#"
<HTML>
<BODY>
<SCRIPT>const auth0_client_secret = "secret-value";</SCRIPT>
<STYLE>.banner::before { content: "hello"; }</STYLE>
</BODY>
</HTML>
"#;
let mut texts = Vec::new();
stream_context_candidates(source, &Language::Html, |text| {
texts.push(text.to_string());
true
})
.unwrap();
assert!(
texts.iter().any(|text| text.contains("auth0_client_secret = secret-value")),
"expected uppercase script tag to be handled like lowercase script"
);
assert!(
texts.iter().any(|text| text.contains("content =")),
"expected uppercase style tag to emit CSS declaration candidates"
);
}
#[test]
fn html_comment_only_script_context_is_ignored() {
let source = br#"
<html>
<body>
<script>// comment-only-secret</script>
<div>visible text</div>
</body>
</html>
"#;
let mut texts = Vec::new();
stream_context_candidates(source, &Language::Html, |text| {
texts.push(text.to_string());
true
})
.unwrap();
assert!(
!texts.iter().any(|text| text.contains("comment-only-secret")),
"expected commented-out script secrets to stay ignored"
);
assert!(
texts.iter().any(|text| text.contains("div = visible text")),
"expected visible non-script HTML text to remain available for verification"
);
}
#[test]
fn html_multiline_script_assignment_extracts_context() {
let source = br#"
<html>
<body>
<script>
const auth0_client_secret =
"abcd1234abcd1234abcd1234abcd1234";
</script>
</body>
</html>
"#;
let mut texts = Vec::new();
stream_context_candidates(source, &Language::Html, |text| {
texts.push(text.to_string());
true
})
.unwrap();
assert!(
texts
.iter()
.any(|text| text == "auth0_client_secret = abcd1234abcd1234abcd1234abcd1234"),
"expected multiline script assignment candidate, got {texts:?}"
);
}
#[test]
fn comment_only_python_context_is_ignored() {
let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let source = fs::read(root.join("testdata/parsers/comment_only_context.py")).unwrap();
let mut texts = Vec::new();
stream_context_candidates(&source, &Language::Python, |text| {
texts.push(text.to_string());
true
})
.unwrap();
assert!(texts.is_empty());
}
}