use tracing::warn;
const DANGEROUS_CHAINS: &[(&[&str], &str)] = &[
(
&["filesystem_write", "shell_execute"],
"write-then-execute: possible code injection",
),
(
&["shell_execute", "web_fetch"],
"execute-then-fetch: possible data exfiltration",
),
(
&["longterm_memory", "shell_execute"],
"memory-then-execute: possible memory poisoning exploitation",
),
];
#[derive(Debug)]
pub struct ChainTracker {
history: Vec<String>,
alerted: Vec<bool>,
}
impl Default for ChainTracker {
fn default() -> Self {
Self::new()
}
}
impl ChainTracker {
pub fn new() -> Self {
Self {
history: Vec::new(),
alerted: vec![false; DANGEROUS_CHAINS.len()],
}
}
pub fn record(&mut self, tool_names: &[String]) {
self.history.extend(tool_names.iter().cloned());
self.check_patterns();
}
fn check_patterns(&mut self) {
for (idx, (pattern, description)) in DANGEROUS_CHAINS.iter().enumerate() {
if self.alerted[idx] {
continue;
}
if Self::contains_subsequence(&self.history, pattern) {
self.alerted[idx] = true;
warn!(
pattern = ?pattern,
tools = ?self.history,
"Tool chain alert: {}",
description,
);
crate::audit::log_audit_event(
crate::audit::AuditCategory::ToolChainAlert,
crate::audit::AuditSeverity::Warning,
"tool_chain_alert",
&format!("{description}: {:?}", self.history),
false,
);
}
}
}
fn contains_subsequence(haystack: &[String], needle: &[&str]) -> bool {
let mut needle_idx = 0;
for item in haystack {
if needle_idx < needle.len() && item == needle[needle_idx] {
needle_idx += 1;
}
}
needle_idx == needle.len()
}
#[cfg(test)]
pub fn history(&self) -> &[String] {
&self.history
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_no_alert_on_clean_sequence() {
let mut tracker = ChainTracker::new();
tracker.record(&["web_fetch".into(), "filesystem_read".into()]);
tracker.record(&["filesystem_read".into()]);
assert!(!tracker.alerted.iter().any(|&a| a));
assert_eq!(tracker.history().len(), 3);
}
#[test]
fn test_detects_write_then_execute() {
let mut tracker = ChainTracker::new();
tracker.record(&["filesystem_write".into()]);
tracker.record(&["shell_execute".into()]);
assert!(tracker.alerted[0]);
assert!(!tracker.alerted[1]);
assert!(!tracker.alerted[2]);
}
#[test]
fn test_detects_execute_then_fetch() {
let mut tracker = ChainTracker::new();
tracker.record(&["shell_execute".into()]);
tracker.record(&["web_fetch".into()]);
assert!(tracker.alerted[1]);
}
#[test]
fn test_detects_memory_then_execute() {
let mut tracker = ChainTracker::new();
tracker.record(&["longterm_memory".into()]);
tracker.record(&["filesystem_read".into()]); tracker.record(&["shell_execute".into()]);
assert!(tracker.alerted[2]);
}
#[test]
fn test_no_false_positive_on_reverse_order() {
let mut tracker = ChainTracker::new();
tracker.record(&["shell_execute".into()]);
tracker.record(&["filesystem_write".into()]);
assert!(!tracker.alerted[0]);
}
#[test]
fn test_subsequence_detection_non_contiguous() {
let mut tracker = ChainTracker::new();
tracker.record(&["filesystem_write".into()]);
tracker.record(&["web_fetch".into()]); tracker.record(&["shell_execute".into()]);
assert!(tracker.alerted[0]); }
#[test]
fn test_no_duplicate_alerts() {
let mut tracker = ChainTracker::new();
tracker.record(&["filesystem_write".into()]);
tracker.record(&["shell_execute".into()]);
assert!(tracker.alerted[0]);
tracker.record(&["filesystem_write".into()]);
tracker.record(&["shell_execute".into()]);
assert!(tracker.alerted[0]);
}
#[test]
fn test_contains_subsequence_basic() {
assert!(ChainTracker::contains_subsequence(
&["a".into(), "b".into(), "c".into()],
&["a", "c"],
));
assert!(!ChainTracker::contains_subsequence(
&["a".into(), "b".into(), "c".into()],
&["c", "a"],
));
}
#[test]
fn test_empty_history_no_match() {
assert!(!ChainTracker::contains_subsequence(&[], &["a", "b"]));
}
#[test]
fn test_empty_pattern_always_matches() {
assert!(ChainTracker::contains_subsequence(&["a".into()], &[]));
}
#[test]
fn test_default_trait() {
let tracker = ChainTracker::default();
assert!(tracker.history.is_empty());
assert_eq!(tracker.alerted.len(), DANGEROUS_CHAINS.len());
}
#[test]
fn test_parallel_tool_batch_recording() {
let mut tracker = ChainTracker::new();
tracker.record(&["filesystem_write".into(), "shell_execute".into()]);
assert!(tracker.alerted[0]);
}
#[test]
fn test_multiple_patterns_detected() {
let mut tracker = ChainTracker::new();
tracker.record(&["filesystem_write".into()]);
tracker.record(&["shell_execute".into()]);
assert!(tracker.alerted[0]);
tracker.record(&["web_fetch".into()]);
assert!(tracker.alerted[1]);
}
}