vervet is a single-binary adversary-emulation instrument for red teams, purple teams, detection engineers, and AI/LLM security orchestrators. The model decides the next step; vervet provides the narrow, typed, auditable primitive that can actually run.
It is Infection Monkey inverted: no central server, no agent fleet, no hidden
control plane. The CLI is the contract. An orchestrator calls describe, fires
one authorized emulate <ATTACK_ID> verb, receives a vq1 evidence envelope,
and can fold receipts into ATT&CK coverage with report.
Authorized use only. Every state-changing technique requires an Ed25519-signed scope manifest. Out-of-scope is a hard typed refusal: vervet cannot act against a target, technique, or time window the manifest does not authorize.
| Surface | What matters |
|---|---|
| Runtime | Rust 2024 workspace, CLI-first, no server required |
| Orchestrator contract | describe exposes verbs, techniques, and required inputs |
| Authorization | Ed25519-signed manifest checked before every state-changing action |
| Evidence | vq1 receipts with content-addressed handles |
| Auditability | blake3-linked audit entries make timeline edits visible |
| Coverage | report folds receipts into MITRE ATT&CK tactics and techniques |
| Safety posture | Out-of-scope requests fail closed with typed denial and exit code 2 |
Modern AI agents can plan a security engagement, but they still need a narrow execution layer with real boundaries. vervet is built around three constraints:
| Constraint | Implementation |
|---|---|
| An LLM should know exactly what it can call | vervet describe emits the machine-readable contract |
| A technique should not run by convention | Gate::authorize mints an unforgeable Grant required by techniques |
| Evidence should survive scrutiny | Receipts carry typed summaries and audit entries linked to predecessor handles |
signed scope manifest
|
v
describe -> emulate <ATTACK_ID> -> receipt.vq1 -> report
| |
| +-- content-addressed evidence
+-- authorize -> engage -> emit
Every state-changing path flows through authorize -> engage -> emit. That is
the core property of the project: the same boundary governs network discovery,
remote-service checks, valid-account assertions, and password-spray emulation.
A manifest is the signed authorization for one engagement: which CIDRs, which techniques, and which time window. The signing key is held by the authorizing party, never by vervet.
{
"engagement_id": "acme-2026-q2",
"operator": "dj@codetestcode.io",
"authorized_cidrs": ["10.10.0.0/24"],
"excluded_cidrs": ["10.10.0.1/32"],
"technique_allowlist": ["T1046", "T1110.003"],
"valid_from": 0,
"valid_until": 4102444800
}cargo run -p vervet-scope --example sign -- <seed-hex-32-bytes> claims.json > manifest.jsonThe command prints the authority public key to stderr. That authority is passed out-of-band to the gate; a key embedded in the manifest would be self-signed and therefore forgeable.
vervet describevervet emulate T1046 \
--manifest manifest.json \
--authority 4cb5abf6ad79fbf5abbccafcc269d85cd2651ed4b885b5869f241aedf0a5ba29 \
--target 10.10.0.5 \
--ports 22,80 \
--store ./runsThe output is a receipt: a vq1 evidence envelope plus the audit chain that records authorization and engagement as linked actions.
{
"envelope": {
"header": { "schema": "vq1", "version": "0.1.0" },
"summary": {
"name": "Network Service Discovery",
"attack_id": "T1046",
"tactic": "discovery",
"engagement_id": "acme-2026-q2",
"observation_count": 0
},
"handles": {}
},
"audit": [
{
"seq": 0,
"engagement_id": "acme-2026-q2",
"action": "authorize T1046 target=10.10.0.5",
"prev": "ev:genesis"
},
{
"seq": 1,
"engagement_id": "acme-2026-q2",
"action": "engage T1046 observations=0",
"prev": "ev:412c0a8637502e7a"
}
]
}Requests outside the manifest fail closed with exit code 2:
denied: target is not within any authorized CIDR
denied: target falls within an excluded CIDR
denied: technique is not in the manifest allowlistvervet report --store ./runs --engagement acme-2026-q2{
"schema": "vq1-coverage",
"tactics": { "discovery": ["T1046"] },
"techniques": [
{
"id": "T1046",
"name": "Network Service Discovery",
"tactic": "discovery",
"engagements": 1,
"observations": 0,
"detection": "unobserved"
}
],
"totals": { "engagements": 1, "techniques": 1, "observations": 0 },
"detection_note": "detection is unobserved — vervet does not see your blue team; feed SIEM evidence to populate"
}The CLI surface is intentionally small enough for an agent to reason about and strict enough for a human operator to audit.
| verb | purpose |
|---|---|
describe |
Emit the machine-readable protocol, verbs, and technique inputs |
schema |
Print the engagement-receipt contract as JSON Schema |
emulate <ATTACK_ID> |
Authorize, fire one technique, and emit an audited receipt |
report <receipts...> |
Fold receipts into ATT&CK coverage, or read from --store |
explain |
Resolve one evidence handle from a receipt |
help |
Human-facing CLI help; -V / --version for version output |
The current registry is deliberately compact. Each entry maps to MITRE ATT&CK and is implemented as one reviewed, in-tree primitive.
| ATT&CK id | name | tactic |
|---|---|---|
T1046 |
Network Service Discovery | discovery |
T1021 |
Remote Services | lateral movement |
T1078 |
Valid Accounts | initial access |
T1110.003 |
Password Spraying | credential access |
Each technique is one self-contained file in vervet-techniques/src/. Adding a
technique means adding that file plus one mod line; describe, schema, and
dispatch read the registry.
Grantis unforgeable. It has no public constructor; onlyvervet_scope::Gate::authorizemints one.- Techniques cannot bypass scope. A technique takes
&Grant, so acting outside an approved manifest is blocked by the type system. - Registration is compile-time only. Techniques register through
inventory; there is no dynamic plugin loading. - The audit chain is tamper-evident. Each entry commits to the previous entry's blake3 handle, so removal or mutation breaks every later link.
Credential-access techniques judge attempts through a pluggable Verifier.
The default build ships protocol-level probes only. The SSH probe performs a
real RFC-4253 version exchange, confirms the service, and captures its banner,
but reaches at most ssh_confirmed; it does not assert credential validity.
The --features ssh-auth build adds SshAuth, a credential-asserting backend
that performs real password authentication through ssh2 and returns
valid / invalid. Password material is never written to evidence.
Default builds stay lean and protocol-level. Credential-asserting SSH is available, but it is an explicit feature flag.
cargo build --release
cargo build --release -p vervet-cli --features ssh-authRequires Rust 1.88+ with edition 2024. The default binary is dependency-light
and ships protocol probes only; ssh-auth is opt-in so the default stays lean.
The default suite is hermetic: no Docker and no external services.
cargo test --workspace
cargo test -p vervet-scopeDocker-gated credential tests exercise the real SSH backend and full
authorize -> engage -> emit pipeline:
cargo test -p vervet-verify --features ssh-auth
cargo test -p vervet-e2e --features ssh-auth
cargo test -p vervet-cli --features ssh-auth| crate | purpose |
|---|---|
vervet-core |
Foundational types: ATT&CK ids, vq1 envelopes, content-addressed evidence |
vervet-scope |
Signed manifests, IPv4 CIDR scope, unforgeable grants, audit chains |
vervet-technique |
The Technique trait plus the inventory registry |
vervet-techniques |
One self-contained implementation file per ATT&CK technique |
vervet-verify |
Pluggable backends that judge an attempt into a Verdict |
vervet-engage |
The one path every technique firing uses: authorize -> engage -> emit |
vervet-report |
Pure JSON coverage aggregation from receipts |
vervet-store |
Content-addressed run store under <root>/<engagement>/<run-id>.json |
vervet-cli |
The verb surface |
vervet-e2e |
Docker-backed end-to-end tests against real services |
- One concept per file.
mod.rsonly re-exports, and CI enforces a 200-line source budget throughscripts/check-line-budget.sh. - Receipts are self-describing.
reportdoes not need registry lookup; the summary carries the technique name, ATT&CK id, and tactic. - Detection is never overstated. vervet reports
unobserved, neverundetected, because it cannot see your blue team.
AGPL-3.0-or-later. See LICENSE.
Topics: adversary-emulation · breach-and-attack-simulation · MITRE ATT&CK · red-team · purple-team · detection-engineering · security-automation · Rust · CLI · AI · LLM-agent · cybersecurity
{ "tool": "vervet", "protocol": { "schema": "vq1", "version": "0.1.0" }, "verbs": ["describe", "schema", "emulate", "report", "explain", "help"], "authorization": "every engagement requires an Ed25519-signed scope manifest", "techniques": [ { "id": "T1046", "name": "Network Service Discovery", "tactic": "discovery", "inputs": ["target", "ports (default 445,22,3389 = SMB,SSH,RDP)"] } ] }