Skip to content

Architecture

gsjonio edited this page Jul 15, 2026 · 2 revisions

Architecture

hightower is a Cargo workspace laid out as a hexagonal (ports & adapters) architecture, with one crate per ring. The split is not cosmetic: it is enforced by the compiler, not by code review.

hightower/
├── core/        domain + ports (traits). ZERO OS dependencies.
├── adapters/    driven side: Windows implementations of the ports.
└── cli/         driving side: clap + the composition root.

The three rings

core -- the domain (crate hightower-core)

The pure center: the process model (ProcessInfo, ProcessVerdict, RiskLevel, ...) and the ports -- traits that describe what the outer rings must provide (ProcessLister, SignatureVerifier, ProcessKnowledgeRepository, RiskRule).

Nothing here touches the operating system. Crucially, core does not declare the windows crate as a dependency, so a stray use windows::... in the domain simply fails to compile. The boundary that "the domain must not depend on infrastructure" -- usually a rule you keep in your head or a linter -- is here a fact the compiler checks.

Why it matters: the risk rules can be unit-tested against fake, in-memory adapters, with no real Windows machine required.

adapters -- the driven side (crate hightower-adapters)

The only place allowed to call the OS. It holds the real implementations of the core ports:

  • ToolHelpProcessLister -- enumerates processes via the Windows ToolHelp32 API (CreateToolhelp32Snapshot, Process32FirstW/NextW, QueryFullProcessImageNameW).
  • AuthenticodeVerifier -- checks embedded Authenticode signatures via WinVerifyTrust.
  • EmbeddedKnowledgeRepository -- the embedded known-process database (include_str! + serde_json).

All unsafe lives here, and every unsafe block carries a // SAFETY: comment explaining the invariant upheld by hand -- enforced at build time by #![deny(clippy::undocumented_unsafe_blocks)].

The windows crate is a cfg(windows)-gated dependency, so it is only pulled in on the one platform where the binary actually runs.

cli -- the driving side (crate hightower-cli, binary hightower)

Parses the command line (with clap) and acts as the composition root: the one place that constructs the concrete adapters and injects them into the domain. This is dependency injection without a framework -- a main() that builds the pieces and wires them together.

Design patterns in use

Pattern Where Note
Ports & adapters (hexagonal) the crate split compiler-enforced boundary
Strategy each RiskRule one small struct per heuristic
Chain / pipeline the classifier runs each rule, aggregates findings
Repository ProcessKnowledgeRepository hides where known-process data lives
Trait objects (Box<dyn ...>) classifier ports dynamic dispatch, chosen for readability over generics

Why not a second adapter per port?

Classic hexagonal is justified by having multiple real implementations of a port (e.g. a Windows and a Linux scanner). hightower is Windows-only by nature -- ToolHelp32 and Authenticode only exist there -- so there is exactly one real implementation of each port. We keep the ring separation (for testability), but do not add a folder-per-adapter that would mimic the shape of the pattern without its reason.

CI enforcement

  • test-core runs on both Windows and Ubuntu. On Ubuntu the binary does not even build (it needs windows), so a green Ubuntu test-core is a cheap proof that the domain stayed OS-free.
  • build-cli, clippy run on Windows, where the whole thing exists.
  • fmt, audit round out the checks; -D warnings makes every lint fatal.

Clone this wiki locally