chore(release): prepare public source release

This commit is contained in:
MercuryToolbox Release
2026-07-18 15:33:01 +08:00
commit 34d6a57f38
510 changed files with 163501 additions and 0 deletions
+57
View File
@@ -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
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `drvshape`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::DriverShape));
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `ioctlscan`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::IoctlScan));
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `pecalls`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::Calls));
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `peexports`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::Exports));
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `peimports`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::Imports));
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `pesig`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::Signatures));
}
+5
View File
@@ -0,0 +1,5 @@
//! Binary entry point for `pestrrefs`.
fn main() {
std::process::exit(petools::main_entry(petools::ToolKind::StringRefs));
}
+124
View File
@@ -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");
}
+75
View File
@@ -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));
}