use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct AnalysisProfile {
#[serde(default)]
pub profile: ProfileMeta,
#[serde(default)]
pub sources: Vec<TaintSource>,
#[serde(default)]
pub sinks: Vec<TaintSink>,
#[serde(default)]
pub transforms: Vec<TaintTransform>,
#[serde(default)]
pub sanitizers: Vec<TaintSanitizer>,
}
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
pub struct ProfileMeta {
pub name: String,
#[serde(default)]
pub description: String,
#[serde(default)]
pub version: String,
#[serde(default)]
pub author: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaintSource {
pub api: String,
pub taint_id: u32,
#[serde(default = "default_extract")]
pub extract: String,
#[serde(default)]
pub description: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaintSink {
pub api: String,
#[serde(default)]
pub dangerous_arg: u32,
#[serde(default = "default_severity")]
pub severity: String,
#[serde(default)]
pub cwe: String,
#[serde(default)]
pub description: String,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaintTransform {
pub api: String,
#[serde(default = "default_true")]
pub propagates_taint: bool,
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct TaintSanitizer {
pub api: String,
#[serde(default)]
pub kills_taint: Vec<String>,
}
fn default_extract() -> String {
"args[0]".into()
}
fn default_severity() -> String {
"high".into()
}
fn default_true() -> bool {
true
}
impl AnalysisProfile {
pub fn parse(toml_str: &str) -> Result<Self, String> {
toml::from_str(toml_str).map_err(|e| format!("profile parse error: {e}"))
}
pub fn merge(profiles: Vec<AnalysisProfile>) -> Result<Self, String> {
let mut merged = AnalysisProfile::default();
merged.profile.name = "merged".into();
let mut seen_taint_ids = std::collections::HashSet::new();
for profile in profiles {
for source in &profile.sources {
if !seen_taint_ids.insert(source.taint_id) {
return Err(format!(
"duplicate taint_id {} in source '{}' from profile '{}'",
source.taint_id, source.api, profile.profile.name,
));
}
}
merged.sources.extend(profile.sources);
merged.sinks.extend(profile.sinks);
merged.transforms.extend(profile.transforms);
merged.sanitizers.extend(profile.sanitizers);
}
Ok(merged)
}
pub fn is_sink(&self, api: &str) -> Option<&TaintSink> {
self.sinks.iter().find(|s| s.api == api)
}
pub fn is_source(&self, api: &str) -> Option<&TaintSource> {
self.sources.iter().find(|s| s.api == api)
}
pub fn kills_taint(&self, api: &str, category: &str) -> bool {
self.sanitizers
.iter()
.any(|s| s.api == api && s.kills_taint.iter().any(|c| c == category))
}
pub fn rule_count(&self) -> usize {
self.sources.len() + self.sinks.len() + self.transforms.len() + self.sanitizers.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
const SAMPLE_PROFILE: &str = r#"
[profile]
name = "test-xss"
description = "Test XSS detection"
[[sources]]
api = "chrome.runtime.onMessage"
taint_id = 1
extract = "args[0]"
[[sources]]
api = "window.postMessage"
taint_id = 2
extract = "event.data"
[[sinks]]
api = "chrome.tabs.executeScript"
dangerous_arg = 1
severity = "critical"
cwe = "CWE-94"
[[sinks]]
api = "eval"
dangerous_arg = 0
severity = "critical"
cwe = "CWE-95"
[[sinks]]
api = "element.innerHTML"
dangerous_arg = 0
severity = "high"
cwe = "CWE-79"
[[transforms]]
api = "JSON.parse"
propagates_taint = true
[[sanitizers]]
api = "DOMPurify.sanitize"
kills_taint = ["xss"]
"#;
#[test]
fn parse_profile() {
let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
assert_eq!(profile.profile.name, "test-xss");
assert_eq!(profile.profile.description, "Test XSS detection");
assert_eq!(profile.sources.len(), 2);
assert_eq!(profile.sinks.len(), 3);
assert_eq!(profile.transforms.len(), 1);
assert_eq!(profile.sanitizers.len(), 1);
}
#[test]
fn is_sink_lookup() {
let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
let sink = profile.is_sink("eval").unwrap();
assert_eq!(sink.cwe, "CWE-95");
assert_eq!(sink.severity, "critical");
assert!(profile.is_sink("chrome.tabs.query").is_none());
}
#[test]
fn is_source_lookup() {
let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
let source = profile.is_source("chrome.runtime.onMessage").unwrap();
assert_eq!(source.taint_id, 1);
assert!(profile.is_source("chrome.storage.get").is_none());
}
#[test]
fn kills_taint() {
let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
assert!(profile.kills_taint("DOMPurify.sanitize", "xss"));
assert!(!profile.kills_taint("DOMPurify.sanitize", "sqli"));
assert!(!profile.kills_taint("JSON.parse", "xss"));
}
#[test]
fn merge_profiles() {
let p1 = AnalysisProfile::parse(
r#"
[profile]
name = "p1"
[[sources]]
api = "source1"
taint_id = 1
[[sinks]]
api = "sink1"
"#,
)
.unwrap();
let p2 = AnalysisProfile::parse(
r#"
[profile]
name = "p2"
[[sources]]
api = "source2"
taint_id = 2
[[sinks]]
api = "sink2"
"#,
)
.unwrap();
let merged = AnalysisProfile::merge(vec![p1, p2]).unwrap();
assert_eq!(merged.sources.len(), 2);
assert_eq!(merged.sinks.len(), 2);
}
#[test]
fn merge_rejects_duplicate_taint_ids() {
let p1 = AnalysisProfile::parse(
r#"
[profile]
name = "p1"
[[sources]]
api = "source1"
taint_id = 1
"#,
)
.unwrap();
let p2 = AnalysisProfile::parse(
r#"
[profile]
name = "p2"
[[sources]]
api = "source2"
taint_id = 1
"#,
)
.unwrap();
assert!(AnalysisProfile::merge(vec![p1, p2]).is_err());
}
#[test]
fn rule_count() {
let profile = AnalysisProfile::parse(SAMPLE_PROFILE).unwrap();
assert_eq!(profile.rule_count(), 2 + 3 + 1 + 1); }
#[test]
fn empty_profile() {
let profile = AnalysisProfile::parse("[profile]\nname = \"empty\"").unwrap();
assert_eq!(profile.rule_count(), 0);
assert!(profile.is_sink("anything").is_none());
}
#[test]
fn parse_empty_profile_no_sources() {
let toml = r#"
[profile]
name = "no-sources"
[[sinks]]
api = "sink1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.sources.is_empty());
assert_eq!(profile.sinks.len(), 1);
}
#[test]
fn parse_empty_profile_no_sinks() {
let toml = r#"
[profile]
name = "no-sinks"
[[sources]]
api = "source1"
taint_id = 1
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sources.len(), 1);
assert!(profile.sinks.is_empty());
}
#[test]
fn parse_empty_profile_no_sanitizers() {
let toml = r#"
[profile]
name = "no-sanitizers"
[[sources]]
api = "source1"
taint_id = 1
[[sinks]]
api = "sink1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.sanitizers.is_empty());
}
#[test]
fn parse_empty_profile_only_sources() {
let toml = r#"
[profile]
name = "only-sources"
[[sources]]
api = "source1"
taint_id = 1
[[sources]]
api = "source2"
taint_id = 2
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sources.len(), 2);
assert!(profile.sinks.is_empty());
assert!(profile.transforms.is_empty());
assert!(profile.sanitizers.is_empty());
}
#[test]
fn parse_empty_profile_only_sinks() {
let toml = r#"
[profile]
name = "only-sinks"
[[sinks]]
api = "sink1"
[[sinks]]
api = "sink2"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.sources.is_empty());
assert_eq!(profile.sinks.len(), 2);
}
#[test]
fn parse_empty_profile_only_sanitizers() {
let toml = r#"
[profile]
name = "only-sanitizers"
[[sanitizers]]
api = "sanitizer1"
kills_taint = ["xss"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.sources.is_empty());
assert!(profile.sinks.is_empty());
assert_eq!(profile.sanitizers.len(), 1);
}
#[test]
fn parse_profile_only_transforms() {
let toml = r#"
[profile]
name = "only-transforms"
[[transforms]]
api = "transform1"
propagates_taint = false
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.sources.is_empty());
assert!(profile.sinks.is_empty());
assert_eq!(profile.transforms.len(), 1);
assert!(profile.sanitizers.is_empty());
}
#[test]
fn parse_invalid_toml_syntax() {
let toml = r#"
[profile
name = "broken"
"#;
let result = AnalysisProfile::parse(toml);
assert!(result.is_err());
assert!(result.unwrap_err().contains("parse error"));
}
#[test]
fn parse_toml_missing_bracket() {
let toml = r#"
[profile]
name = "test"
[[sources
api = "source1"
taint_id = 1
"#;
let result = AnalysisProfile::parse(toml);
assert!(result.is_err());
}
#[test]
fn parse_toml_wrong_type_for_taint_id() {
let toml = r#"
[profile]
name = "test"
[[sources]]
api = "source1"
taint_id = "not-a-number"
"#;
let result = AnalysisProfile::parse(toml);
assert!(result.is_err());
}
#[test]
fn parse_toml_wrong_type_for_dangerous_arg() {
let toml = r#"
[profile]
name = "test"
[[sinks]]
api = "sink1"
dangerous_arg = "not-a-number"
"#;
let result = AnalysisProfile::parse(toml);
assert!(result.is_err());
}
#[test]
fn parse_toml_wrong_type_for_propagates_taint() {
let toml = r#"
[profile]
name = "test"
[[transforms]]
api = "transform1"
propagates_taint = "not-a-boolean"
"#;
let result = AnalysisProfile::parse(toml);
assert!(result.is_err());
}
#[test]
fn parse_toml_empty_string() {
let result = AnalysisProfile::parse("");
assert!(result.is_ok());
let profile = result.unwrap();
assert_eq!(profile.rule_count(), 0);
}
#[test]
fn parse_toml_whitespace_only() {
let result = AnalysisProfile::parse(" \n\t ");
assert!(result.is_ok());
let profile = result.unwrap();
assert_eq!(profile.rule_count(), 0);
}
#[test]
fn parse_toml_duplicate_table() {
let toml = r#"
[profile]
name = "test"
[profile]
name = "duplicate"
"#;
let result = AnalysisProfile::parse(toml);
let _ = result;
}
#[test]
fn merge_five_profiles_no_conflict() {
let profiles: Vec<AnalysisProfile> = (1..=5)
.map(|i| {
AnalysisProfile::parse(&format!(
r#"
[profile]
name = "p{0}"
[[sources]]
api = "source{0}"
taint_id = {0}
[[sinks]]
api = "sink{0}"
"#,
i
))
.unwrap()
})
.collect();
let merged = AnalysisProfile::merge(profiles).unwrap();
assert_eq!(merged.sources.len(), 5);
assert_eq!(merged.sinks.len(), 5);
}
#[test]
fn merge_duplicate_taint_id_first_position() {
let p1 = AnalysisProfile::parse(
r#"
[profile]
name = "p1"
[[sources]]
api = "source1"
taint_id = 1
"#,
)
.unwrap();
let p2 = AnalysisProfile::parse(
r#"
[profile]
name = "p2"
[[sources]]
api = "source2"
taint_id = 1
"#,
)
.unwrap();
let result = AnalysisProfile::merge(vec![p1, p2]);
assert!(result.is_err());
let err = result.unwrap_err();
assert!(err.contains("duplicate taint_id 1"));
assert!(err.contains("source2")); assert!(err.contains("p2")); }
#[test]
fn merge_duplicate_taint_id_middle() {
let p1 = AnalysisProfile::parse(
r#"
[profile]
name = "p1"
[[sources]]
api = "source1"
taint_id = 1
"#,
)
.unwrap();
let p2 = AnalysisProfile::parse(
r#"
[profile]
name = "p2"
[[sources]]
api = "source2"
taint_id = 2
"#,
)
.unwrap();
let p3 = AnalysisProfile::parse(
r#"
[profile]
name = "p3"
[[sources]]
api = "source3"
taint_id = 2
"#,
)
.unwrap();
let result = AnalysisProfile::merge(vec![p1, p2, p3]);
assert!(result.is_err());
}
#[test]
fn merge_duplicate_taint_id_last_position() {
let profiles: Vec<AnalysisProfile> = (1..=5)
.map(|i| {
AnalysisProfile::parse(&format!(
r#"
[profile]
name = "p{0}"
[[sources]]
api = "source{0}"
taint_id = {0}
"#,
i
))
.unwrap()
})
.collect();
let p6 = AnalysisProfile::parse(
r#"
[profile]
name = "p6"
[[sources]]
api = "source6"
taint_id = 5
"#,
)
.unwrap();
let mut all_profiles = profiles;
all_profiles.push(p6);
let result = AnalysisProfile::merge(all_profiles);
assert!(result.is_err());
}
#[test]
fn merge_empty_vec() {
let merged = AnalysisProfile::merge(vec![]).unwrap();
assert_eq!(merged.rule_count(), 0);
assert_eq!(merged.profile.name, "merged");
}
#[test]
fn merge_single_profile() {
let p1 = AnalysisProfile::parse(
r#"
[profile]
name = "p1"
[[sources]]
api = "source1"
taint_id = 1
[[sinks]]
api = "sink1"
"#,
)
.unwrap();
let merged = AnalysisProfile::merge(vec![p1]).unwrap();
assert_eq!(merged.sources.len(), 1);
assert_eq!(merged.sinks.len(), 1);
}
#[test]
fn merge_preserves_all_sinks() {
let p1 = AnalysisProfile::parse(
r#"
[[sinks]]
api = "sink1"
[[sinks]]
api = "sink2"
"#,
)
.unwrap();
let p2 = AnalysisProfile::parse(
r#"
[[sinks]]
api = "sink3"
"#,
)
.unwrap();
let merged = AnalysisProfile::merge(vec![p1, p2]).unwrap();
assert_eq!(merged.sinks.len(), 3);
assert!(merged.is_sink("sink1").is_some());
assert!(merged.is_sink("sink2").is_some());
assert!(merged.is_sink("sink3").is_some());
}
#[test]
fn is_sink_exact_match_required() {
let toml = r#"
[[sinks]]
api = "chrome.tabs.executeScript"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_sink("chrome.tabs.executeScript").is_some());
assert!(profile.is_sink("chrome.tabs").is_none());
assert!(profile.is_sink("tabs.executeScript").is_none());
assert!(profile.is_sink("executeScript").is_none());
assert!(
profile
.is_sink("chrome.tabs.executeScript.details")
.is_none()
);
}
#[test]
fn is_source_exact_match_required() {
let toml = r#"
[[sources]]
api = "chrome.runtime.onMessage"
taint_id = 1
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_source("chrome.runtime.onMessage").is_some());
assert!(profile.is_source("chrome.runtime").is_none());
assert!(profile.is_source("runtime.onMessage").is_none());
assert!(profile.is_source("onMessage").is_none());
}
#[test]
fn is_sink_case_sensitive() {
let toml = r#"
[[sinks]]
api = "Eval"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_sink("Eval").is_some());
let result = profile.is_sink("eval");
let _ = result; }
#[test]
fn is_sink_empty_string() {
let toml = r#"
[[sinks]]
api = ""
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_sink("").is_some());
assert!(profile.is_sink("something").is_none());
}
#[test]
fn kills_taint_multiple_categories() {
let toml = r#"
[[sanitizers]]
api = "sanitize"
kills_taint = ["xss", "sqli", "commandi"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.kills_taint("sanitize", "xss"));
assert!(profile.kills_taint("sanitize", "sqli"));
assert!(profile.kills_taint("sanitize", "commandi"));
assert!(!profile.kills_taint("sanitize", "path_traversal"));
}
#[test]
fn kills_taint_empty_categories() {
let toml = r#"
[[sanitizers]]
api = "sanitize"
kills_taint = []
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(!profile.kills_taint("sanitize", "xss"));
}
#[test]
fn kills_taint_no_sanitizer_match() {
let toml = r#"
[[sanitizers]]
api = "DOMPurify.sanitize"
kills_taint = ["xss"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(!profile.kills_taint("OtherSanitizer", "xss"));
}
#[test]
fn kills_taint_case_sensitive_category() {
let toml = r#"
[[sanitizers]]
api = "sanitize"
kills_taint = ["XSS"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.kills_taint("sanitize", "XSS"));
assert!(!profile.kills_taint("sanitize", "xss"));
}
#[test]
fn kills_taint_multiple_sanitizers() {
let toml = r#"
[[sanitizers]]
api = "sanitizer1"
kills_taint = ["xss"]
[[sanitizers]]
api = "sanitizer2"
kills_taint = ["sqli"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.kills_taint("sanitizer1", "xss"));
assert!(profile.kills_taint("sanitizer2", "sqli"));
assert!(!profile.kills_taint("sanitizer1", "sqli"));
assert!(!profile.kills_taint("sanitizer2", "xss"));
}
#[test]
fn profile_with_unicode_api_names() {
let toml = r#"
[profile]
name = "unicode"
[[sources]]
api = "日本語.メッセージ"
taint_id = 1
[[sinks]]
api = "🎉.celebrate"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_source("日本語.メッセージ").is_some());
assert!(profile.is_sink("🎉.celebrate").is_some());
}
#[test]
fn profile_with_unicode_in_description() {
let toml = r#"
[profile]
name = "unicode"
description = "日本語の説明"
[[sources]]
api = "source"
taint_id = 1
description = "這是中文"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.profile.description, "日本語の説明");
assert_eq!(profile.sources[0].description, "這是中文");
}
#[test]
fn profile_with_special_chars_in_api() {
let toml = r#"
[[sources]]
api = "api-name_with.special$chars"
taint_id = 1
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_source("api-name_with.special$chars").is_some());
}
#[test]
fn profile_empty_api_name() {
let toml = r#"
[[sources]]
api = ""
taint_id = 1
[[sinks]]
api = ""
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.is_source("").is_some());
assert!(profile.is_sink("").is_some());
}
#[test]
fn profile_empty_profile_name() {
let toml = r#"
[profile]
name = ""
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.profile.name, "");
}
#[test]
fn profile_empty_cwe() {
let toml = r#"
[[sinks]]
api = "sink1"
cwe = ""
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sinks[0].cwe, "");
}
#[test]
fn profile_empty_severity() {
let toml = r#"
[[sinks]]
api = "sink1"
severity = ""
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sinks[0].severity, "");
}
#[test]
fn rule_count_empty() {
let profile = AnalysisProfile::default();
assert_eq!(profile.rule_count(), 0);
}
#[test]
fn rule_count_only_sources() {
let toml = r#"
[[sources]]
api = "s1"
taint_id = 1
[[sources]]
api = "s2"
taint_id = 2
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.rule_count(), 2);
}
#[test]
fn rule_count_only_sinks() {
let toml = r#"
[[sinks]]
api = "sink1"
[[sinks]]
api = "sink2"
[[sinks]]
api = "sink3"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.rule_count(), 3);
}
#[test]
fn rule_count_only_transforms() {
let toml = r#"
[[transforms]]
api = "t1"
[[transforms]]
api = "t2"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.rule_count(), 2);
}
#[test]
fn rule_count_only_sanitizers() {
let toml = r#"
[[sanitizers]]
api = "s1"
[[sanitizers]]
api = "s2"
[[sanitizers]]
api = "s3"
[[sanitizers]]
api = "s4"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.rule_count(), 4);
}
#[test]
fn rule_count_mixed() {
let toml = r#"
[[sources]]
api = "s1"
taint_id = 1
[[sinks]]
api = "sink1"
[[transforms]]
api = "t1"
[[sanitizers]]
api = "san1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.rule_count(), 4);
}
#[test]
fn rule_count_large_profile() {
let sources: String = (1..=100)
.map(|i| format!("[[sources]]\napi = \"source{}\"\ntaint_id = {}\n", i, i))
.collect();
let profile = AnalysisProfile::parse(&sources).unwrap();
assert_eq!(profile.rule_count(), 100);
}
#[test]
fn default_severity_is_high() {
let toml = r#"
[[sinks]]
api = "sink1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sinks[0].severity, "high");
}
#[test]
fn default_extract_is_args0() {
let toml = r#"
[[sources]]
api = "source1"
taint_id = 1
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sources[0].extract, "args[0]");
}
#[test]
fn default_propagates_taint_is_true() {
let toml = r#"
[[transforms]]
api = "transform1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.transforms[0].propagates_taint);
}
#[test]
fn default_dangerous_arg_is_0() {
let toml = r#"
[[sinks]]
api = "sink1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sinks[0].dangerous_arg, 0);
}
#[test]
fn default_kills_taint_is_empty() {
let toml = r#"
[[sanitizers]]
api = "sanitizer1"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.sanitizers[0].kills_taint.is_empty());
}
#[test]
fn default_profile_fields() {
let toml = r#"
[profile]
name = "test"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.profile.description, "");
assert_eq!(profile.profile.version, "");
assert_eq!(profile.profile.author, "");
}
#[test]
fn explicit_values_override_defaults() {
let toml = r#"
[[sinks]]
api = "sink1"
severity = "critical"
dangerous_arg = 2
[[sources]]
api = "source1"
taint_id = 1
extract = "return"
[[transforms]]
api = "transform1"
propagates_taint = false
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sinks[0].severity, "critical");
assert_eq!(profile.sinks[0].dangerous_arg, 2);
assert_eq!(profile.sources[0].extract, "return");
assert!(!profile.transforms[0].propagates_taint);
}
#[test]
fn source_with_all_fields() {
let toml = r#"
[[sources]]
api = "chrome.runtime.onMessage"
taint_id = 42
extract = "event.data"
description = "Message from runtime"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
let source = &profile.sources[0];
assert_eq!(source.api, "chrome.runtime.onMessage");
assert_eq!(source.taint_id, 42);
assert_eq!(source.extract, "event.data");
assert_eq!(source.description, "Message from runtime");
}
#[test]
fn source_large_taint_id() {
let toml = r#"
[[sources]]
api = "source1"
taint_id = 4294967295
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sources[0].taint_id, 4294967295);
}
#[test]
fn source_taint_id_zero() {
let toml = r#"
[[sources]]
api = "source1"
taint_id = 0
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sources[0].taint_id, 0);
}
#[test]
fn sink_with_all_fields() {
let toml = r#"
[[sinks]]
api = "eval"
dangerous_arg = 0
severity = "critical"
cwe = "CWE-95"
description = "Code execution"
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
let sink = profile.is_sink("eval").unwrap();
assert_eq!(sink.dangerous_arg, 0);
assert_eq!(sink.severity, "critical");
assert_eq!(sink.cwe, "CWE-95");
assert_eq!(sink.description, "Code execution");
}
#[test]
fn sink_large_dangerous_arg() {
let toml = r#"
[[sinks]]
api = "sink1"
dangerous_arg = 999
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sinks[0].dangerous_arg, 999);
}
#[test]
fn transform_propagates_true() {
let toml = r#"
[[transforms]]
api = "JSON.parse"
propagates_taint = true
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(profile.transforms[0].propagates_taint);
}
#[test]
fn transform_propagates_false() {
let toml = r#"
[[transforms]]
api = "toString"
propagates_taint = false
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert!(!profile.transforms[0].propagates_taint);
}
#[test]
fn sanitizer_single_category() {
let toml = r#"
[[sanitizers]]
api = "sanitize"
kills_taint = ["xss"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sanitizers[0].kills_taint.len(), 1);
}
#[test]
fn sanitizer_many_categories() {
let toml = r#"
[[sanitizers]]
api = "superSanitizer"
kills_taint = ["xss", "sqli", "commandi", "path_traversal", "ssrf", "xxe", "ldap_injection"]
"#;
let profile = AnalysisProfile::parse(toml).unwrap();
assert_eq!(profile.sanitizers[0].kills_taint.len(), 7);
}
}