forked from Crockan/MercuryToolbox
chore(release): prepare public source release
This commit is contained in:
@@ -0,0 +1,57 @@
|
||||
[package]
|
||||
name = "petools"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
rust-version.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
readme.workspace = true
|
||||
publish.workspace = true
|
||||
description = "PowerShell-first PE import, export, callsite, string, driver, and IOCTL analysis."
|
||||
keywords.workspace = true
|
||||
categories.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[[bin]]
|
||||
name = "peexports"
|
||||
path = "src/main_peexports.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "peimports"
|
||||
path = "src/main_peimports.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pecalls"
|
||||
path = "src/main_pecalls.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pesig"
|
||||
path = "src/main_pesig.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "pestrrefs"
|
||||
path = "src/main_pestrrefs.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "drvshape"
|
||||
path = "src/main_drvshape.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "ioctlscan"
|
||||
path = "src/main_ioctlscan.rs"
|
||||
|
||||
[dependencies]
|
||||
common = { path = "../common", default-features = false }
|
||||
goblin.workspace = true
|
||||
lexopt.workspace = true
|
||||
llvmtools = { path = "../llvmtools" }
|
||||
regex-lite.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assert_cmd.workspace = true
|
||||
predicates.workspace = true
|
||||
tempfile.workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `drvshape`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::DriverShape));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `ioctlscan`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::IoctlScan));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `pecalls`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::Calls));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `peexports`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::Exports));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `peimports`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::Imports));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `pesig`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::Signatures));
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
//! Binary entry point for `pestrrefs`.
|
||||
|
||||
fn main() {
|
||||
std::process::exit(petools::main_entry(petools::ToolKind::StringRefs));
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//! CLI contract tests for PE analysis commands.
|
||||
|
||||
use assert_cmd::Command;
|
||||
use predicates::prelude::*;
|
||||
use serde_json::Value;
|
||||
|
||||
const COMMANDS: &[&str] = &[
|
||||
"peexports",
|
||||
"peimports",
|
||||
"pecalls",
|
||||
"pesig",
|
||||
"pestrrefs",
|
||||
"drvshape",
|
||||
"ioctlscan",
|
||||
];
|
||||
|
||||
fn peimports_command() -> Command {
|
||||
Command::cargo_bin("peimports").expect("binary")
|
||||
}
|
||||
|
||||
fn peexports_command() -> Command {
|
||||
Command::cargo_bin("peexports").expect("binary")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn help_mentions_shared_flags_and_examples() {
|
||||
for command_name in COMMANDS {
|
||||
let mut command = Command::cargo_bin(command_name).expect("binary");
|
||||
command
|
||||
.arg("--help")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Examples"))
|
||||
.stdout(predicate::str::contains("--json"))
|
||||
.stdout(predicate::str::contains("--toon"))
|
||||
.stdout(predicate::str::contains("--input-format"));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_pe_input_is_runtime_error() {
|
||||
let temp = tempfile::NamedTempFile::new().expect("temp file");
|
||||
std::fs::write(temp.path(), b"not a pe").expect("write fixture");
|
||||
|
||||
let mut command = peimports_command();
|
||||
command
|
||||
.arg(temp.path())
|
||||
.arg("--json")
|
||||
.assert()
|
||||
.code(3)
|
||||
.stderr(predicate::str::contains("not a PE"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_json_includes_guided_triage_metadata() {
|
||||
let sample = assert_cmd::cargo::cargo_bin("peimports");
|
||||
let mut command = peimports_command();
|
||||
let output = command
|
||||
.arg(sample)
|
||||
.arg("--json")
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let json = serde_json::from_slice::<Value>(&output).expect("json output");
|
||||
assert_eq!(json["report_quality"]["confidence"], "high");
|
||||
assert_eq!(json["report_quality"]["completeness"], "complete");
|
||||
assert!(
|
||||
json["report_quality"]["evidence"]
|
||||
.as_array()
|
||||
.is_some_and(|items| {
|
||||
items
|
||||
.iter()
|
||||
.any(|item| item == "PE headers and import table parsed")
|
||||
})
|
||||
);
|
||||
assert!(json["next_actions"].as_array().is_some_and(|items| {
|
||||
items.iter().any(|item| {
|
||||
item["label"] == "Hunt imported API callsites"
|
||||
&& item["command"].as_str().is_some_and(|command| {
|
||||
command.contains("pecalls") && command.contains("--category")
|
||||
})
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn imports_text_is_answer_first_and_actionable() {
|
||||
let sample = assert_cmd::cargo::cargo_bin("peimports");
|
||||
let mut command = peimports_command();
|
||||
command
|
||||
.arg(sample)
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("answer="))
|
||||
.stdout(predicate::str::contains(
|
||||
"trust confidence=high completeness=complete",
|
||||
))
|
||||
.stdout(predicate::str::contains(
|
||||
"next_action label=\"Hunt imported API callsites\" command=\"pecalls",
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn exports_ordinal_filter_matches_the_real_export_ordinal() {
|
||||
let mut command = peexports_command();
|
||||
let output = command
|
||||
.arg(r"C:\Windows\System32\kernel32.dll")
|
||||
.arg("--ordinal")
|
||||
.arg("126")
|
||||
.arg("--json")
|
||||
.assert()
|
||||
.success()
|
||||
.get_output()
|
||||
.stdout
|
||||
.clone();
|
||||
|
||||
let json = serde_json::from_slice::<Value>(&output).expect("json output");
|
||||
let export = json["files"][0]["exports"][0].clone();
|
||||
assert_eq!(export["ordinal"], 126);
|
||||
assert_eq!(export["name"], "BuildIoRingCancelRequest");
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
//! Core behavior tests for PE analysis helpers.
|
||||
|
||||
use petools::{
|
||||
ApiCategory, CallsiteQuery, IoctlMethod, IoctlScanOptions, analyze_callsites,
|
||||
categorize_import, decode_ioctl, scan_strings,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn categorizes_common_windows_imports() {
|
||||
assert_eq!(
|
||||
categorize_import("kernel32.dll", "CreateFileW"),
|
||||
ApiCategory::Filesystem
|
||||
);
|
||||
assert_eq!(
|
||||
categorize_import("advapi32.dll", "RegOpenKeyExW"),
|
||||
ApiCategory::Registry
|
||||
);
|
||||
assert_eq!(
|
||||
categorize_import("ntdll.dll", "NtDeviceIoControlFile"),
|
||||
ApiCategory::DeviceIo
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_iat_call_targets_from_objdump_comments() {
|
||||
let query = CallsiteQuery::apis(["CreateFileW"]);
|
||||
let imports = [("0x140072010", "kernel32.dll", "CreateFileW")];
|
||||
let disassembly = "140001098:\tcallq\t*0x70f72(%rip) # 0x140072010\n";
|
||||
let report = analyze_callsites(disassembly, &imports, &query);
|
||||
assert_eq!(report.callsites.len(), 1);
|
||||
assert_eq!(report.callsites[0].api, "CreateFileW");
|
||||
assert_eq!(report.callsites[0].address, 0x1400_01098);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn decodes_ctl_code_fields() {
|
||||
let decoded = decode_ioctl(0x0022_2003).expect("ioctl");
|
||||
assert_eq!(decoded.device_type, 0x22);
|
||||
assert_eq!(decoded.function, 0x800);
|
||||
assert_eq!(decoded.method, IoctlMethod::Neither);
|
||||
assert_eq!(decoded.access_name, "FILE_ANY_ACCESS");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scans_ascii_and_utf16_strings() {
|
||||
let mut bytes = b"\0DeviceIoControl\0\0".to_vec();
|
||||
bytes.extend("IoctlName".encode_utf16().flat_map(u16::to_le_bytes));
|
||||
let strings = scan_strings(&bytes, 5);
|
||||
assert!(strings.iter().any(|item| item.value == "DeviceIoControl"));
|
||||
assert!(strings.iter().any(|item| item.value == "IoctlName"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn utf16le_scan_reports_only_maximal_non_overlapping_runs() {
|
||||
let bytes: Vec<u8> = "LongDeviceName"
|
||||
.encode_utf16()
|
||||
.flat_map(u16::to_le_bytes)
|
||||
.collect();
|
||||
let strings = scan_strings(&bytes, 5);
|
||||
let utf16_hits: Vec<_> = strings
|
||||
.iter()
|
||||
.filter(|item| item.encoding == "utf16le")
|
||||
.collect();
|
||||
|
||||
assert_eq!(utf16_hits.len(), 1);
|
||||
assert_eq!(utf16_hits[0].offset, 0);
|
||||
assert_eq!(utf16_hits[0].value, "LongDeviceName");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn filters_probable_ioctl_candidates() {
|
||||
let options = IoctlScanOptions::default();
|
||||
assert!(options.is_probable_code(0x0022_2000));
|
||||
assert!(!options.is_probable_code(0x0000_0001));
|
||||
}
|
||||
Reference in New Issue
Block a user