MacNoise generates real macOS telemetry: network connections, file writes, process spawns, plist mutations, TCC probes, and more. Point it at a machine running your EDR, SIEM, or firewall stack and see what actually fires - not what the vendor datasheet claims will fire.
For background on the motivation and design, see the release blog post.
# Build (add build-amd64 / build-arm64 to cross-compile for Darwin, or release for both)
make build
# List available modules
./macnoise list
# Run a single module
./macnoise run net_connect --param target=127.0.0.1 --param port=8080
# Preview without executing
./macnoise run svc_launch_agent --dry-run
# Run all network modules
./macnoise run --category network
# Run a scenario
./macnoise scenario configs/scenarios/edr_validation.yaml
# Emit structured JSONL output
./macnoise scenario configs/scenarios/file_flow.yaml --format jsonl --output /tmp/events.jsonl
| Category | Description |
|---|---|
network | TCP connections, HTTP, listeners, reverse shells, DNS, and TLS |
process | Exact execution, signal delivery, dylib injection, Gatekeeper bypass, and osascript |
file | Bounded discovery, literal reads/copies, creation, modification, archiving, hiding, and decoy encryption |
tcc | TCC permission probes with exact Full Disk Access, Contacts, Accessibility, or Screen Recording requirements |
credential | Native credential-store access |
volume | Disk-image creation and mounted-volume lifecycle |
service | Launchd enumeration, LaunchAgent/Daemon persistence, cron, shell profile, and Login Items |
plist | Plist creation and modification |
evasion | Log clearing, timestomping, history removal, and masquerading |
See the generated module catalog for every module, parameter, output, event type, privilege, and ATT&CK mapping.
macnoise run <module> [--param key=val ...] Run a specific module
macnoise run --category <cat> Run all modules in a category
macnoise run --all Run all modules
macnoise list [--category <cat>] List modules
macnoise info <module> Show module details, params, MITRE
macnoise scenario <file.yaml> [--input key=val] [--report report.json]
Run a YAML scenario
macnoise categories List categories with counts
macnoise version Print version
| Flag | Default | Description |
|---|---|---|
--format | human | Output format: human or jsonl |
--output | (none) | Write output to file (in addition to stdout) |
--verbose | false | Verbose output including cleanup errors |
--dry-run | false | Preview actions without executing |
--no-cleanup | false | Leave module artifacts in place (see below) |
--timeout | 30 | Per-module timeout in seconds |
--audit-log | (none) | Write OCSF 1.7.0 audit records to a JSONL file |
--config | (none) | Load defaults from a YAML config file |
--run-id | generated | Set the correlation identifier for this run |
Scenario files use version: 1. Inputs and module outputs are typed, and a
later step references them with explicit mappings rather than string
interpolation:
version: 1
name: Archive one generated artifact
on_error: stop
inputs:
content:
type: string
required: true
steps:
# Custom modules declare these outputs through OutputSpecs.
- id: create
module: custom_create
params:
content:
input: content
- id: archive
module: custom_archive
params:
source:
output: create.path
outputs:
archive:
output: archive.path
Only outputs declared by a module can be referenced. Local scenarios can be
reused with an include step; includes are relative, cannot traverse above
the root scenario directory, are cycle-checked, and are limited to eight levels.
MacNoise validates the complete graph before execution, gives the run one
private workspace, and cleans invoked modules in reverse order. Use
--input content=value to supply inputs and --report report.json for the
versioned execution report.
By default every module reverses itself when it finishes. That is usually what you want, but it means a detection only ever sees the install event. To validate that your stack detects the persistence itself - a LaunchAgent sitting in ~/Library/LaunchAgents, a cron entry, a modified shell profile - the artifact has to still be there when the scan runs:
./macnoise run svc_launch_agent --no-cleanup
Each module that skips cleanup prints a line naming itself, and the audit log records cleanup_result: skipped rather than ok, so a run that left persistence behind is never mistaken for one that tidied up. Use macnoise info <module> to see what a given module creates.
You are responsible for removing these yourself. Re-running the same module without the flag will clean up only what that run created, not what a previous --no-cleanup run left behind.
MacNoise writes two separate streams. Telemetry events - what your EDR/SIEM actually sees - go to stdout or --output. A second, optional stream records what MacNoise itself did: which modules ran, prereq/cleanup outcomes, and MITRE mappings, in OCSF 1.7.0 JSONL.
./macnoise scenario configs/scenarios/amos_atomic_stealer.yaml --audit-log /tmp/audit.jsonl
Every telemetry event carries one authoritative outcome and one typed subject (schema 2.0). The outcome says what happened to the action MacNoise attempted, while the subject identifies the file, process, network endpoint, service, or resource involved:
outcome | Meaning | Human marker |
|---|---|---|
executed | The action ran and did what the module claims | [+] |
denied | The action ran and the environment refused it | [-] |
indeterminate | The action ran, but nothing can be concluded | [?] |
error | MacNoise itself failed to carry the action out | [!] |
A denied TCC probe or a beacon to a dead C2 is the telemetry this tool exists to generate, so it is distinct from error, which means MacNoise itself failed. The audit log records the same value at unmapped.outcome. Parameters declared sensitive are replaced with [REDACTED] in managed audit records and command-line identity.
The audit log opens in append mode, so records from multiple runs pile up in one file for batch analysis. If you're adding a module and want to know how a new event type gets classified into OCSF, see CONTRIBUTING.md.
The generated module catalog is the authoritative reference for names, parameters, outputs, event types, privileges, and ATT&CK mappings. Category notes explain platform behavior and operational boundaries:
Scenarios chain modules into ordered sequences - a single YAML file that replays a multi-stage intrusion pattern against your detections.
| File | Description |
|---|---|
network_only.yaml | Composed TCP, listener, DNS, HTTP beacon, and HTTP exfiltration operations |
edr_validation.yaml | Comprehensive EDR detection coverage |
full_sweep.yaml | All categories |
lazarus_group.yaml | Lazarus Group: dylib injection, service discovery, reverse shell, LaunchAgent persistence |
amos_atomic_stealer.yaml | AMOS / Atomic Stealer: MaaS infostealer, Gatekeeper bypass, keychain dump, ZIP exfil, backdoor persistence |
clickfix.yaml | ClickFix: obfuscated one-liner pasted into Terminal, base64 decode, second-stage fetch, LaunchAgent persistence |
ransomware.yaml | Ransomware impact: stage plaintext decoys, encrypt them, then drop a ransom note |
discovery.yaml | Composed argv-based system, account, network, and security software discovery recipes |
process_chain.yaml | Three-process shell chain built from an explicit argument vector |
file_flow.yaml | Connected create, modify, bounded discovery, read, copy, and archive flow |
mounted_execution.yaml | Create and execute a payload from an observed disk-image mount point |
The two APT scenarios follow real documented intrusion sequences, technique by technique - each YAML file cites the actual threat intel it's built from and annotates every step with the MITRE technique it exercises, so start there for the full breakdown rather than a retelling here.
Dry-run first:
./macnoise scenario configs/scenarios/<scenario>.yaml --dry-run
Cross-reference with your SIEM/EDR: each step comment names the technique it should trigger. No matching alert after a real run is a gap in your coverage.
Writing your own:
version: 1
name: My Custom Scenario
on_error: stop
steps:
- module: net_connect
params:
target: "192.168.1.1"
port: 443
- module: file_create
params:
base_dir: "/tmp/test"
Parameters are checked against each module's declared string, integer, boolean,
path, or list type before preview or execution. Unknown names and invalid values
are rejected. on_error defaults to stop. Set it to continue only when a
coverage sweep should attempt later module invocations after a failure.
Start from the scenario template for typed inputs, outputs, and connected dataflow.
Version 1.0 defines the supported CLI commands and flags, module names and contracts, scenario schema 1, telemetry schema 2.0, and scenario-report schema 1.0. Future incompatible changes to those interfaces require a new major release.
Existing users should read Migrating from v0.6.0 to v1.0.0. It maps every removed module and describes the scenario, JSONL, and Go API changes.
See CONTRIBUTING.md for the primitive, scenario, and core-change paths.
Releases are automated - release-please cuts a new version straight from your Conventional Commit PR title, so feat: add net_tls module or fix: correct beacon jitter is both your PR title and your changelog entry.
MacNoise is intended for authorized security testing, EDR validation, and detection engineering on systems you own or have explicit written permission to test. The authors assume no liability for misuse.
AI Code contributions are fine, but please keep in mind that code review is currently going to be a human-led process which means there is only so much code we can review. Please limit PRs to a specific fix, or new telemetry module. PRs with extensive changes are likely going to be closed.