Ares
SkillCloud & infraOperating, debugging, deploying and reasoning about the Ares autonomous red/blue AD attack platform in this repo. Use for launching or stopping red ops (task red:ec2:multi, ec2:launch, red:multi) and blue investigations, reading loot/reports/scorecards, deploying or gating a binary on EC2 kali-ares / k8s attack-simulation / the proxmox attacker VM, inspecting Redis op state and key types, LogQL/Loki, Tempo/OTEL and Grafana-MCP queries against ares, config/ares.yaml or ARES_* env questions, per-role model assignment, benchmark/replay/diversity-sweep/eval work, the tool catalog and tool-failure strings, CI gates and pre-commit hooks, and the banned-lab-token test-data rule. Also use whenever a claim about ares needs verifying before it is stated — the reference set here carries file:line citations and the mistakes this assistant has repeatedly made on this repo.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the Ares skill
What this skill tells your AI
The instructions your AI receives, as published by dreadnode/ares in .claude/skills/ares/SKILL.md and read by ahel’s review.
Router + non-negotiables. The detail lives in references/; every claim there is cited to file:line at HEAD.
Before you touch anything
Sourced from references/hard-won-lessons.md — 30 rules mined from 209 sessions where the operator had to correct this assistant. Read that file first. Each rule below names the check that satisfies it.
-
Test data is a closed set. Only
contoso.local/fabrikam.local,192.168.58.x,dc01/dc02/sql01/web01/ws01/ca01,alice/bob/carol/admin/svc_*,P@ssw0rd!. Gate withscripts/goad-token-sweep.sh— never by eyeballing, and never by widening the Write hook's bypasscaselist. The three enforcers, their divergences and their coverage holes are tabulated once, inreferences/tools-and-gates.md#the-banned-token-sweep; do not restate them elsewhere.-
This skill directory is unswept by both automated layers.
scripts/goad-token-sweep.sh:36exempts all of.claude/, and its whole-tree mode enumeratesgit ls-files(:41-47) —.claude/skills/ares/**is untracked at HEAD (git ls-files .claudelists only the three agents and theares-debug/attack-path-diversity-sweepskills). Only the PreToolUse Write/Edit hook covers it, so a file arriving here bycp/mv/rsyncis never scanned. Passing the paths to the script explicitly does not help — its exempt filter runs on"$@"too (:52), so it exits 0 vacuously. Borrow the regex:bash -c 'eval "$(sed -n "23,29p" scripts/goad-token-sweep.sh)"; grep -rHniE "$banned" .claude/skills/ares/' -
That hook is itself operator-local and untracked —
.gitignore:31-35ignores.claude/*and un-ignores onlyagents/andskills/**. It is wired at.claude/settings.json:10as a bare command path, which requires the exec bit; at HEAD the file is mode 644. Verify before relying on it:test -x .claude/hooks/check-banned-strings.sh.
-
-
Pin the EC2 box to an explicit instance id + region before the first remote command, and pin it twice.
AWS_REGIONalone selects staging (us-west-1, profilelab) vs prod (us-east-1);EC2_NAME=kali-aresis a*kali-ares*glob that matches in both. Default to staging; touch prod only when the user says "prod" in that message.task ec2:launchrunsredis-cli FLUSHDBon whatever it resolves.AWS_PROFILE=lab AWS_REGION=us-west-1 task ec2:resolve EC2_NAME=kali-ares # prints id+IP+Name for EVERY matchSSM-backed tasks honour
EC2_INSTANCE_ID(run-ssm.sh:69-73); CLI-backed ones (ec2:runtime/loot/ops/watch/kill/stop-op/teardown,blue:*) do not — pin those asEC2_NAME=i-…and passAWS_PROFILE=/AWS_REGION=explicitly, because clap hard-defaultslab/us-west-1and ignores your exports.ec2:reportis SSM-backed despite looking like the others (.taskfiles/ec2/Taskfile.yaml:878-900). -
Never attribute an op result to your change until a literal NEW with that change is present in the deployed binary.
task ec2:exec EC2_NAME=<pinned> CMD="grep -ac -- '<literal>' /usr/local/bin/ares"must be ≥ 1. Outer double quotes, inner single — the inverted form dies withCMD required/ exit 201 whenever the literal contains a space (.taskfiles/ec2/Taskfile.yaml:1477-1479; empirically verified inreferences/operations.md), and gate literals are normally log sentences. Pick the literal from acontains("…"),format!/bail!/panic!fragment, or.arg("…")— neverstarts_with/ends_with/==, which thedev-deployprofile folds out. A failed gate means your change did not ship; there is no other reading. -
Progress is a state diff, not liveness. Two Redis snapshots 60 s apart that show objective state advancing. Process up, workers
active, Redis ping, ≥80% cache hit and climbing tokens are all compatible with a wedged op — token churn plus a high cache-hit rate is the signature of the wedge, not evidence of health. -
Never run an interactive or blocking command from an agent. Banned:
task ec2:logs(interactive SSM session, never terminates),task ec2:redis:forward/ec2:nats:forward(foreground, and eachxargs kills whatever holds its local port),task ec2:watch,task ec2:launch(WAITdefaultstrue),task red:multi(FOLLOWdefaultstrue),red:multi:watchwithoutONCE=true,task remote:logs(FOLLOWdefaultstrue),task run WAIT=true|CAPTURE=true,ec2:loot/red:multi:lootwithDIFF=true(promoted to a 10 s watch loop),task blue:multi:operation-status WATCH=…(no timeout),task blue:reports:clean(read -p, hangs undertask -y),ares ops deletewithout--force. Useec2:logs:fetch,ec2:exec CMD='tail -n 200 …',ec2:ops:ids,ec2:runtimeinstead. -
Never read an exit code through a pipe. The Bash tool's zsh inherits
pipefail, so a pipeline can invent or hide a failure:cmd >/tmp/out 2>&1; echo "REAL_EXIT=$?"; rg -n 'pattern' /tmp/out. A trailingrgin a compound call sets the call's exit code. -
Base64-wrap any remote command containing a double quote,
$( ), or a space-in-arg.{{.CMD}}is spliced textually intorun_ssm_cmd;$( )evaluates locally.B64=$(printf '%s' '<script>' | base64 | tr -d '\n'); task ec2:exec EC2_NAME=<pinned> CMD="echo $B64 | base64 -d | bash". Empty output fromec2:execis a broken command, not a real negative, and theCMD required/ exit-201 message is a lie about emptiness. -
Read each Redis key with the verb its TYPE demands. Wrong verb → loud
WRONGTYPE; wrong key name → a silent0that reads exactly like an empty op. There is no:credskey. Table below; full catalog inreferences/state-and-redis.md. -
Zero grep hits prove nothing until the pattern is validated against a line you know exists.
/var/log/ares/*.logis ANSI-painted, so anyfield=valueanchor matches nothing;grep -ais mandatory, strip escapes withs/\x1b\[[0-9;]*[a-zA-Z]//g, scope to the bare op id (notop.id=) plus a timestamp window, and include the rotated/zgrepsibling. -
Generated text is not evidence. An LLM task summary, a timeline
Assistance needed:string, or a subagent's conclusion is confabulation until resolved against raw tool output (ares ops sessions replay <op> <task>), Redis, or the box. The code agrees: agent assertions live in a separatellm_findingsfield, never authoritative state. -
Treat
/Users/l/dreadnode/aresas a checkout other sessions are mutating. Re-readgit -C /Users/l/dreadnode/ares branch --show-current && git status --porcelainafter every pause; work in your own worktree; stage explicit paths; neverrebase/pull/stash/restore/reset --hard/force-push there. -
Score a dreadgoad op on
Domains (n/3 compromised, n/2 forests)plus the per-domain tree line — a domain counts only withDA+ akrbtgt: <types>detail + a matchingdc_secretsdump_<domain>EXPLOITED row. Neitherops runtimenorops lootsubtracts supersede credits; onlyops reportdoes. -
Never cheat the benchmark. No potfile/
--showrecovery, no operator-known wordlists, noredis-cli hsetintoares:op:*. If you hand-patched state, say so in the same breath and re-run clean. Disclose the seededinitial_credentialwhenever you quote a result. -
Resolve AWS auth yourself — never tell the user to run
aws sso loginorassume; for profilelabit fails.unset AWS_ACCESS_KEY_ID AWS_SECRET_ACCESS_KEY AWS_SESSION_TOKEN; AWS_PROFILE=lab AWS_REGION=us-west-1 command aws sts get-caller-identity. -
Do not stall the turn. You have SSM reach into the box; "I'd need the op logs" is never a reason to hand back after the user said "fix it".
Before you say it works — the evidence contract
| Claim | Evidence that settles it |
|---|---|
| The change shipped | Build SHA/Deploy SHA from this run + GATE_STRING found in /usr/local/bin/ares; binary mtime newer than the commit; the op started after the deploy finished |
| Workers run the new code | deploy's restart block printed restarting: ares@…, not no ares@ worker units active — skipping restart; then task ec2:status |
| The op is healthy | task ec2:runtime … OPERATION_ID=op-… read in full + two state snapshots 60 s apart showing advance |
| The op terminated | Status: completed and runtime/tokens/cost stopped climbing. Completion condition met only freezes red dispatch and opens a drain window (300 s red, up to 3300 s blue) |
| The bug is fixed | the originally-failing operation re-run against the deployed binary. cargo test, clippy, --help and green CI are never verification. State-shape or report changes need a fresh live op — ops report --regenerate cannot surface a key that did not exist when that state was written |
| A detection fires | the composed LogQL from the tool result replayed against live Loki stage by stage, with ARES_DEPLOYMENT confirmed equal to the shipper's deployment label; ares blue evidence <inv-id> --json for provenance |
| Any number | which counter, from which command, for which op id — and whether supersede credits were subtracted |
Absence of a warning log is never a success verdict. If the path only logs on failure, add the success-side log.
Route the ask
| Ask | Go to |
|---|---|
| The mistakes we keep making here; is my claim already known-false | references/hard-won-lessons.md — read first, includes a "rules that expired" list |
| Crate boundaries, the (non-)tick, roles→toolsets, dispatch paths, one task end to end, completion/freeze | references/architecture.md |
Launch / loot / report / stop / inject; the ares ops tree; LATEST=true; "I changed code, now prove it"; poll for DA/krbtgt without blocking the turn; which read commands are k8s-only | references/operations.md |
Build + ship to EC2 / k8s / proxmox / local; BUILD_TOOL; systemd units; post-deploy worker bounce | references/deployment.md |
| Redis key catalog, TYPEs, meta/completion fields, timeline shapes, queues, dedup, locks, snapshot recipes | references/state-and-redis.md |
| Loki labels + working LogQL, Tempo/OTEL spans and the env var that silences export, Grafana MCP; why LLM/tool latency is unrecoverable with OTLP off | references/observability.md |
| Detection catalog, deterministic sweep, investigation lifecycle + lock keys, technique-ID join, scorecards | references/blue-team.md |
benchmark: tasks, replay record/playback, eval/scoring, reports/ + logs/ layout, real-vs-vacuous sweeps; building a denominator for "never exploited" | references/benchmarks-and-replay.md |
config/ares.yaml block by block, per-role models and how to prove one activated, the ARES_* env table, secrets, precedence | references/config-and-env.md |
| Which binary a tool spawns, timeout/kill semantics, failure strings, CI gates + pre-commit hooks, the full add-a-tool gate list, the banned-token rule (authoritative) | references/tools-and-gates.md |
| A live op is stuck / slow / crashing / a worker crash-loops | skill ares-debug — it owns wedge signatures and the Step 0..9 probe ladder. Do not re-derive them here |
Run a diversity sweep end to end (knobs on, benchmark:diversity-sweep, read coverage.csv, iterate temperature) | skill attack-path-diversity-sweep for the workflow; knob truth below |
| Execute a multi-step ares workflow (≥3 dependent commands: launch → monitor → deploy → inject → report) | agent ares-operator. One-shot commands run inline — never dispatch an agent for a single task ec2:runtime |
| Trace a code path through the crates, a build error, "where is X implemented" | agent rust-ares-expert |
| Target-lab questions (accounts, ACL chains, ADCS templates, trusts, "what does this credential unlock") | agent dreadgoad-expert fails on every call via model-level safeguards. Read the DreadGOAD docs and docs/goad-checklist.md directly; never reword a prompt to evade a refusal |
ares-debug is stale on two facts — do not copy them forward
Its Redis key/TYPE table (SKILL.md:110-123) and its deploy/restart section (:396-405) are correct — do not "fix" them. The two that are stale:
| Its claim | Truth at HEAD |
|---|---|
SKILL.md:285 — the worker's unavailable_tools is a per-process HashSet<String>, "no TTL, no re-probe", entries "persist across every subsequent op the same worker handles" | It is a HashMap<String, UnavailableEntry> with exponential re-probe backoff 60 s → 300 s → 1800 s → 4 h, final rung a cap (ares-cli/src/worker/tool_executor.rs:351-372), and one successful spawn removes the entry outright (:592-601). Only typed BinaryNotFound poisons it. Detail: references/tools-and-gates.md |
SKILL.md:49, :269, :274 grep Tool binary not found (spawn failed) | That string has zero hits in ares-*/src at HEAD — the grep reports "no cascade" during a live cascade. Current strings: Tool binary not found (ENOENT from worker) — removing from available tools for the rest of this task (ares-llm/src/agent_loop/runner.rs:608), Tool binary not found (ENOENT) — backing off before next re-probe (ares-cli/src/worker/tool_executor.rs:677), Skipping tool cached as ENOENT (:532) |
Both are worked examples of the same rule: every claim needs a file:line.
Diversity knobs ship ON, not off. config/ares.yaml:104-116 sets selection_temperature: 0.7, novelty.enabled: true / scope: per-campaign, randomize_entry_foothold: true, emit_path_records: true (turned on by 72a40f02). The comment block at :97 claiming they "default to today's deterministic behaviour", and the sweep skill's Step 1 "All four default to off", are both stale. Precedence is not the generic env > JSON > YAML for these four: strategy.rs:236-243 assigns novelty.enabled, novelty.scope, randomize_entry_foothold and emit_path_records from YAML unconditionally, clobbering any JSON payload; only selection_temperature (:223), novelty.enabled (:244) and emit_path_records (:247) have env overrides. randomize_entry_foothold and novelty.scope are YAML-only. See references/config-and-env.md.
60-second orientation
One binary, ares, built from a four-crate workspace: ares-core (models, Redis key names, NATS subjects, YAML config, detection catalog, report renderers), ares-tools (one wrapper module per role + executor.rs + the parsers that are the only authoritative discovery source), ares-llm (tool registry schemas, Tera prompts, providers, agent loop), ares-cli (orchestrator and worker are modules inside it — never ares-orchestrator/). The orchestrator is one process running ~72 independent tokio loops (62 auto_* automations plus ten infra loops) — there is no orchestrator tick; find the loop by its log line. The LLM agent loop runs in-process inside the orchestrator, so every "the agent decided X" line is in orchestrator.log; workers only execute individual tool calls off NATS ares.tools.exec.{role}. Seven roles (recon, credential_access, cracker, acl, privesc, lateral, coercion), each with a code-defined toolset — the agents.<role>.tools YAML list is decorative. Three dispatch paths: automation→LLM, automation→direct tool (25 call sites that never touch an LLM), and the generic vuln queue. Redis is the sole authority for state; logs and traces are derived. Blue runs inside the same orchestrator process when ARES_BLUE_ENABLED=1 and is scored as a MITRE-ID join against red's own record.
Establish where things stand:
git -C /Users/l/dreadnode/ares branch --show-current && git -C /Users/l/dreadnode/ares status --porcelain
AWS_PROFILE=lab AWS_REGION=us-west-1 task ec2:resolve EC2_NAME=kali-ares # pin the box FIRST
task ec2:ops:ids EC2_NAME=<pinned> # STARTED_AT | STATUS | OP_ID; no local binary needed
task ec2:runtime EC2_NAME=<pinned> OPERATION_ID=op-… # domains, split vuln counters, tokens
task ec2:status EC2_NAME=<pinned> # 7 ares@ units, redis, nats, disk, hashcat
task ec2:exec EC2_NAME=<pinned> CMD='redis-cli hmget "ares:op:<id>:meta" has_domain_admin has_golden_ticket red_completed_at red_completion_reason red_blocked_on_blue'
task ec2:report, ec2:ops:ids, ec2:status and ec2:exec need no local binary. The other ec2:* CLI tasks gate on ./target/release/ares (.taskfiles/ec2/Taskfile.yaml:587); BUILD_TOOL=remote, the deploy default, never produces it — build it with cargo build --release -p ares-cli, or route around it with task ec2:exec EC2_NAME=<pinned> CMD='ares ops loot --latest --json' (the box's own binary against the box's own Redis).
Healthy: DA lands at a median 4.1 min, p90 8.8; total duration median 18.8 min. No DA by ~15 min is outside the whole observed distribution — escalate to ares-debug. Those numbers were measured 2026-07-30 over n=47 DA ops in the local, gitignored reports/red/ corpus (.gitignore:10) — they are not reproducible from a clean clone and drift with every op. Re-derive before quoting; the recipe is in references/operations.md.
Fresh clone, first five minutes
cargo build --release -p ares-cli # the CLI-backed ec2:* tasks need this; no task builds it
set -a; . ./.env; set +a # S3_BUCKET (.env.example:33); `task setup-env` is `cp -n` (Taskfile.yaml:238) — never overwrites, never says it skipped
AWS_PROFILE=lab AWS_REGION=us-west-1 task ec2:resolve EC2_NAME=kali-ares # pin the box
Skip step 1 only if you stay on ec2:exec / ec2:ops:ids / ec2:status / ec2:report. .env.example is incomplete (OTEL_TRACES_ENDPOINT, ALLOY_LOKI_ENDPOINT are missing) — see references/config-and-env.md.
Repo map
| Path | Holds |
|---|---|
ares-core/ | models, state/keys.rs, nats.rs, config deserialization, telemetry, detection catalog, eval + report renderers |
ares-tools/ | per-role tool wrappers, executor.rs, parsers/, scope.rs, mutation.rs, sanitize.rs, blue/ detection + Loki client |
ares-llm/ | tool_registry/ (JSON schemas per role), prompt/ (embedded Tera), provider/, agent_loop/, routing/. Library only |
ares-cli/ | the ares binary: orchestrator/ (automations, dispatcher, result processing, completion, blue), worker/, ops/, blue/, benchmark/, transport.rs |
config/ares.yaml | the only per-role model lever; operation.*, timeouts, vulnerability priorities, diversity knobs. Much of the rest is parsed and never read |
tools.yaml | build-time manifest of expected binaries per role (read by build.rs, panic!s on malformed) |
.taskfiles/{ec2,red,blue,k8s,remote,benchmark,obs,proxmox}/ | every task namespace. A dot-directory — rg/fd need --hidden |
scripts/ | goad-token-sweep.sh (banned-token gate), env-from-secrets.sh (regenerates .env, truncating) |
ansible/ | box provisioning, ares@.service.j2 unit template, vector/Loki shipper config |
docs/ | red.md, blue.md, strategy.md, infrastructure.md, attack-path-diversity.md, benchmark-replay.md, goad-checklist.md (the lab spec). Several are stale — verify against HEAD |
reports/ (gitignored) | red/<op>.md, blue/<op>.md (the only file with the red-vs-blue scorecard), blue/investigations/, diversity/<campaign>/coverage.csv, generalize/ |
logs/ (gitignored) | red-ec2-<op>-<ts>.log, red-multi-…, blue-<ts>.log — launcher-side transcripts, not the box's /var/log/ares/ |
GAPS.md | untracked, operator-local. Holds the ### Claimed work table (only a Verified: op marker closes a row) |
task ec2:e2e | The deploy→gate→launch→watch harness (.taskfiles/ec2/scripts/e2e-op.sh; was the untracked testes.sh until 2026-08-08). Read it before diagnosing an op, and fix it rather than hand-rolling its sequence |
Signals
- GitHub stars
- 73
- Forks
- 14
- Last commit
- Sep 2026
Advanced
- Catalog kind
- skill
- Gateway key
ares- Source
- github.com/dreadnode/ares