-
Notifications
You must be signed in to change notification settings - Fork 2
09 Workflows
← Reports & Schemas · Home · Next: Security & Threat Model →
End-to-end playbooks. Each one is a sequence you can follow with a host in front of you.
- Before any engagement
- Endpoint triage
- Network investigation
- Analysing a PCAP someone handed you
- Verifying a container you received
- SOAR and scripted response
- Air-gapped analysis
- Validating an EDR rule
- Team key management
- Media disposal
- Core → Recover → Sanitize, end to end
Once per responder, once per kit:
1 · Issue a persistent signing key.
mkdir -p ~/.arachnid && chmod 700 ~/.arachnid
head -c 32 /dev/urandom > ~/.arachnid/analyst-7.key
chmod 600 ~/.arachnid/analyst-7.key2 · Record its fingerprint out-of-band.
arachnid-core collect -o /tmp/keycheck --signing-key ~/.arachnid/analyst-7.key | tail -3
rm -rf /tmp/keycheckSigning key fingerprint: 6e5cbdee…d827c7
Put that in the case management system, the team roster, wherever an adversary
who rewrites a container cannot also reach. Without this step, verify can
never prove origin.
3 · Verify your binary.
sha256sum -c arachnid-core-0.1.0-x86_64-unknown-linux-musl.sha256
gpg --verify arachnid-core-0.1.0-x86_64-unknown-linux-musl.asc \
arachnid-core-0.1.0-x86_64-unknown-linux-musl4 · Get it allowlisted. Hand the SOC
docs/SOC-ALLOWLISTING.md and, if they want it, a
--dry-run demonstration (Workflow 7).
5 · Have somewhere to write. Put the container on a dedicated collection volume or share, and get that path excluded from real-time scanning. A memory image of an infected host will trigger signature hits. That is the image working correctly.
A host is suspected compromised. You have a shell on it.
sudo arachnid-core collect \
-o /mnt/collection/case-4471/host01 \
--operator "analyst-7" \
--signing-key ~/.arachnid/analyst-7.key \
--log /mnt/collection/case-4471/host01.oplogElevate if you can. Unprivileged collection misses processes owned by other
users, cannot map sockets to owners, and cannot read HKLM values — and it says
so in warnings.
echo $?4 means gaps. Read them now, not later:
jq -r '.collection.warnings[]' /mnt/collection/case-4471/host01/artifacts/report.jsonEvery count in the report below a warning is a floor, not a total.
Compare it against the one on file for analyst-7. If it differs, something is
wrong with your kit before it is wrong with the host.
arachnid-core report /mnt/collection/case-4471/host01 --format html \
-o /mnt/collection/case-4471/host01-triage.htmlRead in this order:
- Collection gaps — what you cannot see.
- Connections to routable addresses — what left the network, and which process owned it.
- Persistence entries — what survives a reboot.
- Processes with an unhashable image — deleted or replaced binaries.
- Active sessions — who is on the box right now.
C=/mnt/collection/case-4471/host01
# listening sockets and their owners
jq -r '.[] | select(.state=="LISTEN")
| "\(.protocol)\t\(.local_addr):\(.local_port)\t\(.process_name // "-")"' \
$C/artifacts/connections.json
# anything running from a temp path
jq -r '.[] | select(.exe != null and (.exe | test("/tmp/|/dev/shm/")))
| "\(.pid)\t\(.exe)"' $C/artifacts/processes.json
# processes with no parent still alive — reparented, often after the parent exited
jq -r '.[] | select(.parent_pid == 1 and .pid > 1000) | "\(.pid)\t\(.name)"' \
$C/artifacts/processes.json
# kernel modules with no on-disk file — a real finding
jq -r '.[] | select(.path == null) | .name' $C/artifacts/kernel_modules.json
# every distinct binary hash, for a bulk lookup against your own corpus
jq -r '.[].exe_sha256 | select(. != null)' $C/artifacts/processes.json | sort -uLive enumeration goes through OS APIs, and a kernel-level implant can lie to them. If anything above looks like a rootkit — a module with no file, a process you cannot hash, sockets with no owner — a memory image is the countermeasure:
sha256sum /opt/avml
sudo arachnid-core collect \
-o /mnt/collection/case-4471/host01-mem \
--operator "analyst-7" --signing-key ~/.arachnid/analyst-7.key \
--memory-tool /opt/avml --memory-tool-sha256 <hex>A second container, not an append — containers are never appended to.
arachnid-core verify /mnt/collection/case-4471/host01Verify while you are still standing next to the evidence, not after it has travelled.
In your case notes, state explicitly:
- collection was not atomic — a process could exit between the process-table read and the connection-table read;
- live enumeration is API-mediated and a kernel implant defeats it;
- anything an attacker removed before you arrived (cleared utmp, deleted unit file) is gone, not merely unreported.
You need to see what a host is talking to.
sudo arachnid-core capture --list-devicessudo arachnid-core capture \
-o /mnt/collection/case-4471/net \
--operator "analyst-7" --signing-key ~/.arachnid/analyst-7.key \
-d eth0 \
-f "not port 22" \
--duration 900Exclude your own session. Bound the run — an unbounded capture that fills the volume is worse than a short one.
Leave promiscuous off unless you specifically need traffic not addressed to this host: enabling it changes the interface's receive mode, which is an observable change to the host you are examining.
jq '.capture | {kernel: .packets_dropped_kernel, interface: .packets_dropped_interface, written: .packets_written}' \
/mnt/collection/case-4471/net/artifacts/report.jsonNon-zero means the capture has holes. Tighten the filter, lower --snaplen, or
write to faster storage, and go again.
capture does not analyse. Run parse-pcap on what it wrote:
arachnid-core parse-pcap \
/mnt/collection/case-4471/net/artifacts/capture.pcap \
-o /mnt/collection/case-4471/net-analysis \
--operator "analyst-7" --signing-key ~/.arachnid/analyst-7.keyThat produces a second container whose custody log records the source savefile's digest — so the analysis is bound to the exact bytes captured.
A=/mnt/collection/case-4471/net-analysis/artifacts/pcap_analysis.json
# every hostname seen
jq -r '.indicators[] | select(.kind | test("dns_query|tls_sni|http_host")) | .value' $A \
| sort -u
# top talkers by packet count
jq -r '.indicators[] | select(.kind=="ipv4") | "\(.count)\t\(.value)"' $A | sort -rn | head
# DNS resolutions observed
jq -r '.indicators[] | select(.kind=="dns_answer") | .value' $A
# biggest flows
jq -r '.flows[:10][] | "\(.bytes)\t\(.src_addr):\(.src_port) -> \(.dst_addr):\(.dst_port)"' $A
# anything cut short by the reassembly ceiling
jq -r '.flows[] | select(.truncated) | "\(.src_addr):\(.src_port) -> \(.dst_addr):\(.dst_port)"' $AThe connection table from Workflow 1 and the flow table from here are two views of the same traffic taken at different times. A flow with no matching process, or a process with a socket that appears in no flow, is the interesting case.
# 1 · hash it before you touch it, and record that hash in your notes
sha256sum incoming.pcap
# 2 · analyse; the digest is recorded in the custody log automatically
arachnid-core parse-pcap incoming.pcap \
-o ./ev-incoming \
--operator "analyst-7" --signing-key ~/.arachnid/analyst-7.key
# 3 · confirm the recorded digest matches what you saw
cut -d' ' -f2- ./ev-incoming/custody.log | jq -r 'select(.event=="note") | .detail'invocation: arachnid-core parse-pcap incoming.pcap -o ./ev-incoming …
source pcap incoming.pcap sha256=ce51b95b…7f6e02 size=454
The source file is never modified and never copied into the container. It stays where it is; the container binds to its bytes by digest.
arachnid-core parse-pcap huge.pcap -o ./ev-huge \
-f "not port 445 and not port 139" \
--max-stream-bytes 2097152Then check what the ceiling cost you:
jq '[.flows[] | select(.truncated)] | length' ./ev-huge/artifacts/pcap_analysis.jsonjq '.pcap.decode_errors' ./ev-incoming/artifacts/report.jsonLikely causes: a link type this build does not decode, frames truncated by a low
snaplen at capture time, or genuine corruption. Check datalink in the analysis
against the supported link types.
Anyone can re-check a container without trusting the collecting host.
arachnid-core verify /path/to/container
echo "exit=$?"| Exit | Means |
|---|---|
0 |
every artifact matches the signed custody log |
3 |
one or more problems — the report lists each |
1 |
not a readable container (missing manifest.json or custody.log) |
key fingerprint: 6e5cbdeecd531dc9b69681ac71b890c6e5338b0dd9664823626c6f9c03d827c7
Compare that against the fingerprint recorded out-of-band at collection. If
it does not match a key you have on file for the responder who claims to have
produced it, the container did not come from them — whatever verify says about
its internal consistency.
Verification of a container signed with an ephemeral key proves integrity only. See Concepts § Signing keys.
cd container/artifacts
cut -d' ' -f2- ../custody.log \
| jq -r 'select(.event=="artifact" and .sha256) | "\(.sha256) \(.name)"' \
| sha256sum -c -That checks artifact digests with coreutils alone. Signatures and the chain need an Ed25519 implementation — see Writing a third-party verifier.
cut -d' ' -f2- container/custody.log \
| jq -r '[.seq, .ts_utc, .event, (.name // .detail // "")] | @tsv'Or, interactively, arachnid-tui → Verify (5) → c.
Exit codes are stable across releases. Branch on them.
#!/usr/bin/env bash
# Collect, handle partial results honestly, verify, and fail loudly on tampering.
set -uo pipefail
CASE="${1:?usage: triage.sh <case-id>}"
OUT="/mnt/collection/${CASE}/$(hostname)"
KEY="/etc/arachnid/responder.key"
RESPONDER="${ARACHNID_OPERATOR:-soar-runner}"
arachnid-core --json --log "${OUT}.oplog" collect \
-o "$OUT" --operator "$RESPONDER" --signing-key "$KEY" > "${OUT}.collect.json"
rc=$?
case $rc in
0) echo "collection complete" ;;
4) echo "PARTIAL — the following collectors were degraded:"
jq -r '.collection.warnings[]' "${OUT}.collect.json"
# keep going: you have evidence, it is just incomplete
;;
2) echo "usage error — check the invocation"; exit 2 ;;
*) echo "collection FAILED (rc=$rc)"; exit 1 ;;
esac
arachnid-core --json verify "$OUT" > "${OUT}.verify.json"
vrc=$?
if [ "$vrc" -eq 3 ]; then
echo "INTEGRITY FAILURE — do not use this container"
jq -r '.problems[]' "${OUT}.verify.json"
exit 3
fi
# Record the fingerprint for the case file
jq -r '.key_fingerprint' "${OUT}.verify.json"
arachnid-core report "$OUT" --format html -o "${OUT}.html"
echo "report: ${OUT}.html"Key points:
-
--jsononcollect,capture,parse-pcap,verifyandcertifygives structured stdout. (reportchooses its rendering with--format jsoninstead.) The operational log goes to stderr or--log, so the two never interleave. - Exit 4 is not failure. Handle it, record the gaps, continue.
- Exit 3 is a hard stop. A container that does not verify is not evidence.
-
--signing-keyis not optional in an automated pipeline: an unattended run producing ephemeral-key containers produces containers nobody can attribute. - Capture the fingerprint into the case record on every run.
# a device list a playbook can choose from
arachnid-core --json capture --list-devices | jq -r '.[] | select(.loopback|not) | .name'
# did anything degrade?
jq -e '.collection.warnings | length == 0' report.json >/dev/null \
&& echo clean || echo degraded
# did the capture drop?
jq -e '.capture.packets_dropped_kernel == 0' report.json >/dev/null \
&& echo lossless || echo "GAPS"Collect on the network, analyse off it.
On the host:
sudo arachnid-core collect -o /media/usb/case-4471/host01 \
--operator "analyst-7" --signing-key /media/usb/keys/analyst-7.key
arachnid-core verify /media/usb/case-4471/host01On the analysis workstation:
# verify first, before you read a single field
arachnid-core verify /mnt/evidence/case-4471/host01
# compare the fingerprint against the case record
arachnid-core report /mnt/evidence/case-4471/host01 --format html -o triage.htmlThe HTML report is fully self-contained — no external stylesheets, fonts, scripts or images — so it renders on a machine with no network at all.
And Arachnid itself makes no outbound connections of any kind: no telemetry, no update check, no indicator lookup, no DNS resolution of anything collected. An air-gapped run behaves identically to a connected one, which is not true of most tooling in this space.
Before a real engagement, prove to the SOC what the tool touches — without producing evidence you then have to account for.
arachnid-core --log-level debug collect -o /tmp/rehearsal --dry-run
ls /tmp/rehearsal
# ls: cannot access '/tmp/rehearsal': No such file or directoryEvery collector runs. Every hash is computed. The custody chain advances in memory. Nothing reaches disk, including the container directory.
What the SOC should observe, and nothing else:
| Expected | Not expected |
|---|---|
reads of /proc, /sys, systemd/cron/autostart paths |
any write outside -o
|
KEY_READ registry opens (Windows) |
any registry write |
OpenProcess with PROCESS_QUERY_LIMITED_INFORMATION | PROCESS_VM_READ
|
ptrace, injection, remote threads |
| no child processes | any child except a named --memory-tool
|
| no sockets | any outbound connection, any listener |
The complete list, with every path and API, is
docs/SOC-ALLOWLISTING.md §4 and §5. If the tool does
something not on that page, that is a defect worth reporting.
For a capture rule, expect AF_PACKET socket creation and SO_ATTACH_FILTER
(Linux) or a handle to \Device\NPCAP\<iface> (Windows). Both are inherent to
packet capture, and are why capture is a separate subcommand you can decline
to allow.
One key per responder, not one per team. The fingerprint is the attribution claim; a shared key attributes nothing.
# per responder, on their own kit
mkdir -p ~/.arachnid && chmod 700 ~/.arachnid
head -c 32 /dev/urandom > ~/.arachnid/$(whoami).key
chmod 600 ~/.arachnid/$(whoami).keyBoth raw and hex seed files are accepted, so a key can be transported as text when that is easier:
xxd -p -c 64 ~/.arachnid/analyst-7.key > analyst-7.hex
# both files produce the same fingerprintMaintain a roster the containers can be checked against:
| Responder | Fingerprint | Issued | Retired |
|---|---|---|---|
| analyst-7 | 6e5cbdee…d827c7 |
2026-08-01 | |
| analyst-3 | a1f09b22…4e0c81 |
2026-06-14 | 2026-08-20 |
Treat the key file like any other credential:
- do not copy it onto the host you are examining if you can avoid it;
- rotate it if a kit is lost, and mark the old fingerprint retired rather than deleting the row — containers signed under it still exist;
- back it up somewhere the responder does not carry into the field.
When there is no persistent key, say so explicitly in the case notes: "this container was signed with an ephemeral key; verification establishes integrity, not origin." That sentence is much cheaper to write now than to explain later.
This workflow destroys data. Full chapter: Secure Erasure.
A drive is leaving the organization — resale, return, or scrap — and must be provably erased.
arachnid-sanitize list-devicesPATH MODEL SERIAL SIZE BUS FLAGS
/dev/nvme0n1 SAMSUNG MZVL41T0HBLB-00BH1 S6B7NX0X602424 953.9 GiB NVMe
/dev/sda Elements SE SSD 23315C401334 931.5 GiB USB SYSTEM
└─ backs a filesystem the running OS has mounted
The SERIAL column is what you will type back. Match it against the disposal
ticket before going further — the serial, not the path. Paths get reused
when drives are hot-swapped; serials do not.
arachnid-sanitize wipe /dev/sdb \
--method dod3 --confirm-serial S4EVNF0M123456 --dry-runEvery rail runs, the estimate is produced, zero bytes are written. This is what catches a wrong serial or a wrong path before it costs you a drive.
sudo arachnid-sanitize wipe /dev/sdb \
--method dod3 \
--confirm-serial S4EVNF0M123456 \
--operator "tech-4" \
--signing-key ~/.arachnid/tech-4.key \
--cert-dir /srv/disposal/certsA 3-second countdown precedes the first write. Ctrl-C cancels — leaving the
device partially overwritten and uncertified, which is recorded rather than
hidden.
| Exit | Means | Do |
|---|---|---|
0 |
erased, verified, certified | release the drive |
3 |
refused by a rail — nothing written | resolve and retry; the drive is untouched |
4 |
wipe ran, verification failed | drive still holds data — destroy physically |
5 |
completed with unwritable regions | drive is failing — destroy physically |
Codes 4 and 5 both mean data may survive. Neither is a success, and neither should let a drive into the resale pile.
arachnid-sanitize cert --cert-dir /srv/disposal/certs --verify
arachnid-sanitize cert --cert-dir /srv/disposal/certs --id <ID> \
--format html -o /srv/disposal/certs/<ID>.htmlRead two fields before filing:
-
method_detail— states plainly whether a hardware purge ran or a software overwrite stood in for one. In this build it is always the latter. -
forced_system_volume— whether the operator overrode the system-volume block.
- This build issues no hardware sanitize command. A
nist-purgejob is a 3-pass software overwrite, and the certificate says so — assess against NIST 800-88 Clear, not Purge. - Crypto-erase is refused on every device.
- On SSDs, wear levelling means an overwrite cannot reach every physical cell. For flash leaving the organization, physical destruction or the vendor's own utility remains the defensible path.
Write those caveats into the disposal record. They are much cheaper to state now than to explain to an auditor later.
The suite's three modules are one sequence: acquire, extract, destroy. This is the whole arc for a single drive, from an endpoint to the inventory shelf.
Steps 1–3 are read-only. Step 4 is not, and it is irreversible. Do not start it until the case is closed and someone has signed off that the drive is no longer needed as evidence.
Collect volatile state from the running host, then image the drive. The image lands in the container as an artifact like any other, hashed into the custody log at the moment it is written.
arachnid-core collect -o ./ev-host01 --operator "analyst-7" --signing-key ~/.keys/analyst-7.key
arachnid-core verify ./ev-host01Work from the acquisition, not the live disk. The drive stays untouched, and a scan can be re-run as often as the case needs.
arachnid-recover scan \
--input ./ev-host01/artifacts/disk.img \
--carve-pass --carve-types jpg,png,pdf,docx,zip \
--output ./ev-host01-recoveredRead the summary before anything else. A 4 exit means the scan finished and
left something out, and results.json names each thing:
jq -r '.filesystems[].unsupported[]?, .problems[]?' ./ev-host01-recovered/results.jsonNever export on the label alone. Look at the reasoning for anything you intend to rely on:
arachnid-recover list-results -i ./ev-host01-recovered/results.json --confidence high,medium
arachnid-recover list-results -i ./ev-host01-recovered/results.json --detail ntfs-000018Then export, and verify the export the same way you verified the collection — it is the same container format and the same command:
arachnid-recover export \
-i ./ev-host01-recovered/results.json \
-o ./ev-host01-recovered/exported \
--confidence high,medium
arachnid-core verify ./ev-host01-recovered/exportedTwo things to write into the case notes at this point:
-
The key fingerprint the export printed. Without it,
verifyproves the container is internally consistent and nothing about who produced it. - Which results are carved. A carved file has no original name, path or timestamp. If a finding rests on where a file was, it cannot rest on a carved result.
Only now, and only after the sign-off. From here, follow
Workflow 9 — Media disposal in full: identify the
drive against the ticket, rehearse with --dry-run, erase, read the exit code
as a disposition, and file the certificate.
arachnid-sanitize list-devices
arachnid-sanitize wipe /dev/sdb --method nist-purge --dry-run./ev-host01/ the acquisition (Core)
custody.log signed, hash-chained
artifacts/disk.img
./ev-host01-recovered/ the scan (Recover)
results.json every result + its scoring rationale
summary.txt
exported/ the recovered files (Recover)
custody.log signed, hash-chained
artifacts/recovered/…
artifacts/carved/…
./certs/ the erasure certificate (Sanitize)
Three containers, one custody format, one verify. That is the point of doing
it in this order.
← Reports & Schemas · Home · Next: Security & Threat Model →