Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 31 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,36 @@
All notable changes to OffsetScan are documented in this file.
The project follows semantic versioning.

## [0.2.0] - 2026-07-21

### Added

- **`offsetscan yara` subcommand** (feature-gated behind `yara-scan`, off by default).
Matches files against YARA rule files and emits one record per matched string —
`File`/`Rule`/`StringId`/`Offset`/`OffsetHex`/`Data` — **verified byte-identical to
OffsetInspect's `Invoke-OffsetYaraScan`** on the same rules and sample. Supports multiple
`--rules` files, `--recurse`, a `--timeout`, and `--ndjson`. A default build (no feature)
exposes the subcommand but returns a clear rebuild instruction. Schema field names are
locked by a unit test that runs even in the default build.

### Changed

- Bumped the `yara` dependency `0.28 -> 0.32` and enabled its `vendored` feature.
`yara-sys 0.28` no longer builds against current `libclang` (bindgen emits a different
anonymous-union layout); 0.32 builds cleanly and vendors `libyara` so no system YARA
install is needed (a C toolchain and `libclang` are still required at build time). The
feature is still excluded from CI, so verify the feature build locally after dep bumps.
- Unified the strict (goblin) and lenient (salvage) PE parse paths onto a single
`assemble_pe_info` builder, removing duplicated struct-assembly and overlay logic. Output
verified unchanged across the 150-file valid corpus and the 34-file malformed corpus.

### Fixed

- The strict PE path computed the overlay boundary **without** filtering out zero-raw-size
sections, unlike OffsetInspect's `Get-OIPEOverlayRange` and the lenient path — a latent
inconsistency that could misplace the overlay for PEs with a zero-raw-size section
(e.g. some packers' virtual sections). Both paths now use the filtered computation.

## [0.1.4] - 2026-07-21

### Fixed
Expand All @@ -16,7 +46,7 @@ The project follows semantic versioning.
reachable imports/imphash. Valid PEs are unaffected — they always take the goblin path,
verified unchanged across a 150-file corpus vs pefile. Across a 34-file malformed corpus
(truncations, bit-flips, corrupted directories, bogus section counts, pure garbage),
`IsPE` now agrees with OffsetInspect on all 34 (was 22/34), and neither engine crashes or
`IsPE` now agrees with OffsetInspect on all 34 (was 22/34), and neither engine crashes nor
hangs on any input. Genuinely unparseable inputs (no MZ/PE signature, or a section count
that cannot fit) are still rejected, matching OffsetInspect.

Expand Down
58 changes: 28 additions & 30 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "offsetscan"
version = "0.1.4"
version = "0.2.0"
edition = "2021"
authors = ["DreadHost Research"]
description = "Standalone native corpus-scale engine for PE parsing, entropy, string extraction, and IOC panels — schema-compatible with OffsetInspect.Result/ThreatScanResult JSON output."
Expand Down Expand Up @@ -37,8 +37,11 @@ csv = "1"
chrono = { version = "0.4", features = ["serde"] }

[dependencies.yara]
version = "0.28"
version = "0.32"
optional = true
# Build a bundled libyara from source so the feature needs no system YARA install
# (still requires a C toolchain + libclang for bindgen at build time).
features = ["vendored"]

[features]
default = []
Expand Down
49 changes: 49 additions & 0 deletions src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,19 @@ enum Commands {
#[arg(long)]
recurse: bool,
},
/// Match files against YARA rules; one record per matched string with its offset.
/// Requires a build with `--features yara-scan`.
Yara {
path: String,
/// One or more YARA rule files.
#[arg(long, required = true)]
rules: Vec<String>,
#[arg(long)]
recurse: bool,
/// Per-file scan timeout in seconds.
#[arg(long, default_value_t = 60)]
timeout: i32,
Comment on lines +81 to +83

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Validate or constrain timeout to avoid negative values being passed into YARA.

The CLI’s i32 timeout is passed straight through to Rules::scan_file, so clap will accept negative values and forward them to YARA. Please either switch to an unsigned type or explicitly reject/normalize negative timeouts before calling the scan to avoid undefined behavior in the YARA API.

},
}

/// Parse a byte offset given as decimal or `0x`-prefixed hexadecimal.
Expand Down Expand Up @@ -185,6 +198,42 @@ fn main() {
});
}
}
Commands::Yara {
path,
rules,
recurse,
timeout,
} => {
let files = expand_paths(&path, recurse);
// Rules are recompiled per file (simple and correct); acceptable for the
// interactive/small-corpus use this subcommand targets.
let mut all_hits = Vec::new();
let mut had_error = false;
for f in &files {
match yara_scan::scan_with_rules(&f.to_string_lossy(), &rules, timeout) {
Ok(hits) => all_hits.extend(hits),
Err(e) => {
eprintln!("offsetscan yara: {}: {e}", f.display());
had_error = true;
}
}
}
if ndjson {
for hit in &all_hits {
if let Ok(line) = serde_json::to_string(hit) {
println!("{line}");
}
}
} else {
match serde_json::to_string_pretty(&all_hits) {
Ok(text) => println!("{text}"),
Err(e) => eprintln!("offsetscan yara: serialization failed: {e}"),
}
}
if had_error && all_hits.is_empty() {
std::process::exit(1);
}
}
}
}

Expand Down
Loading