Initial commit: chainsaw EVTX hunter
WithSecure Labs' chainsaw — fast Sigma-based EVTX hunter, complementary
to hayabusa/zircolite (different rule engine + format).
- ubuntu:24.04 base, multi-stage (fetcher + runtime).
- Pulls latest chainsaw release tarball from GitHub at build time
(greps the API JSON because release notes contain control chars
that break jq).
- Clones SigmaHQ rules at build (chainsaw v2 dropped bundled rules).
- start.sh: chainsaw hunt /data --csv --output (CSV is mutually
exclusive with --json/--jsonl in v2.x; pick CSV for grep-ability).
- Output: /output/chainsaw_<ts>/{csv/, hunt.txt}.
- test_smoke.sh: fetch Yamato sample-evtx, scan, count detections.
- fetch-test-data.sh + .gitignore.
Validated end-to-end on amd64 Linux: 6/6 PASS, 3970 detections on
DeepBlueCLI subset.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
test-data/
|
||||||
+48
@@ -0,0 +1,48 @@
|
|||||||
|
FROM ubuntu:24.04 AS fetcher
|
||||||
|
LABEL maintainer="tabledevil"
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends \
|
||||||
|
curl ca-certificates git \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
# Pull the latest chainsaw release (Linux amd64) and the SigmaHQ rules
|
||||||
|
# repo at build time. No version pin on the engine — image stays current.
|
||||||
|
# (Plain grep instead of jq because release notes contain control chars
|
||||||
|
# that break jq's JSON parser.)
|
||||||
|
RUN set -eux; \
|
||||||
|
cd /tmp; \
|
||||||
|
url=$(curl -sL https://api.github.com/repos/WithSecureLabs/chainsaw/releases/latest \
|
||||||
|
| grep -oE 'https://[^"]*chainsaw_x86_64-unknown-linux-gnu\.tar\.gz' \
|
||||||
|
| head -1); \
|
||||||
|
echo "downloading $url"; \
|
||||||
|
curl -sL "$url" -o chainsaw.tar.gz; \
|
||||||
|
mkdir -p /opt/chainsaw; \
|
||||||
|
tar -xzf chainsaw.tar.gz -C /opt/chainsaw --strip-components=1; \
|
||||||
|
rm chainsaw.tar.gz; \
|
||||||
|
ls /opt/chainsaw
|
||||||
|
|
||||||
|
# WithSecure dropped the bundled sigma rules in v2 — clone fresh from
|
||||||
|
# SigmaHQ each build so we have current detections.
|
||||||
|
RUN git clone --depth=1 https://github.com/SigmaHQ/sigma /opt/sigma
|
||||||
|
|
||||||
|
# Chainsaw also ships its own mapping/rule files in chainsaw/{mappings,rules}
|
||||||
|
# inside the tarball — those are already at /opt/chainsaw.
|
||||||
|
|
||||||
|
FROM ubuntu:24.04
|
||||||
|
ENV DEBIAN_FRONTEND=noninteractive
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends bash ca-certificates \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
COPY --from=fetcher /opt/chainsaw /opt/chainsaw
|
||||||
|
COPY --from=fetcher /opt/sigma /opt/sigma
|
||||||
|
|
||||||
|
ENV PATH=/opt/chainsaw:$PATH
|
||||||
|
RUN mkdir -p /output && touch /output/notmounted && chmod +x /opt/chainsaw/chainsaw
|
||||||
|
|
||||||
|
ADD start.sh /root/start.sh
|
||||||
|
RUN chmod +x /root/start.sh
|
||||||
|
WORKDIR /data
|
||||||
|
CMD ["/bin/bash","/root/start.sh"]
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
# docker_chainsaw
|
||||||
|
|
||||||
|
WithSecure Labs' [Chainsaw](https://github.com/WithSecureLabs/chainsaw) — fast
|
||||||
|
Sigma-based EVTX hunter — wrapped in a container.
|
||||||
|
|
||||||
|
## Build
|
||||||
|
|
||||||
|
```
|
||||||
|
docker build -t tabledevil/chainsaw .
|
||||||
|
```
|
||||||
|
|
||||||
|
The build always pulls the latest chainsaw release tarball + the current
|
||||||
|
SigmaHQ rule corpus, so every rebuild ships with up-to-date detections.
|
||||||
|
|
||||||
|
## Run
|
||||||
|
|
||||||
|
```
|
||||||
|
docker run --rm --network=none \
|
||||||
|
-v /path/to/evtx:/data:ro \
|
||||||
|
-v /path/for/output:/output \
|
||||||
|
tabledevil/chainsaw
|
||||||
|
```
|
||||||
|
|
||||||
|
Output lands in `/output/chainsaw_<timestamp>/`:
|
||||||
|
|
||||||
|
- `hunt.txt` — chainsaw stdout summary (counts, table)
|
||||||
|
- `csv/` — per-rule CSV detections
|
||||||
|
|
||||||
|
## Test
|
||||||
|
|
||||||
|
```
|
||||||
|
./test_smoke.sh # DeepBlueCLI subset (~21 EVTX, fast)
|
||||||
|
SUBSET=YamatoSecurity ./test_smoke.sh
|
||||||
|
KEEP_DATA=1 ./test_smoke.sh # keep cloned sample-evtx for re-runs
|
||||||
|
```
|
||||||
|
|
||||||
|
The test script clones [Yamato-Security/hayabusa-sample-evtx](https://github.com/Yamato-Security/hayabusa-sample-evtx)
|
||||||
|
on demand into `test-data/` (gitignored).
|
||||||
Executable
+8
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Pull the upstream EVTX sample bundle.
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
mkdir -p test-data
|
||||||
|
[ -d test-data/sample-evtx ] || \
|
||||||
|
git clone --depth=1 https://github.com/Yamato-Security/hayabusa-sample-evtx.git test-data/sample-evtx
|
||||||
|
echo "ready: test-data/sample-evtx"
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# chainsaw on-demand EVTX hunter. Same /data input + /output output pattern
|
||||||
|
# as docker_hayabusa.
|
||||||
|
set -e
|
||||||
|
|
||||||
|
if [ ! -d /data ]; then
|
||||||
|
echo "[!] No folder mounted to /data"
|
||||||
|
echo "[>] docker run -it --rm --network=none -v /path/to/evtx:/data:ro -v /path/for/report:/output tabledevil/chainsaw"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Pick a writable output target.
|
||||||
|
if [ ! -f /output/notmounted ] && [ -w /output ]; then
|
||||||
|
output="/output"
|
||||||
|
elif [ -w /data ]; then
|
||||||
|
output="/data"
|
||||||
|
else
|
||||||
|
echo "[!] No writable output folder available"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
ts="$(date +%s)"
|
||||||
|
out_base="${output}/chainsaw_${ts}"
|
||||||
|
mkdir -p "${out_base}"
|
||||||
|
|
||||||
|
echo "[>] Hunt with built-in chainsaw rules + Sigma core rules"
|
||||||
|
# Chainsaw v2.x makes --csv, --json and --jsonl mutually exclusive — pick CSV
|
||||||
|
# (one file per rule, easy to grep). For JSON later, run with --json.
|
||||||
|
chainsaw hunt /data \
|
||||||
|
--sigma /opt/sigma/rules \
|
||||||
|
--mapping /opt/chainsaw/mappings/sigma-event-logs-all.yml \
|
||||||
|
--rule /opt/chainsaw/rules \
|
||||||
|
--csv \
|
||||||
|
--output "${out_base}/csv" \
|
||||||
|
--skip-errors \
|
||||||
|
2>&1 | tee "${out_base}/hunt.txt"
|
||||||
|
|
||||||
|
echo "[>] Output: ${out_base}/"
|
||||||
|
ls -lh "${out_base}" 2>/dev/null
|
||||||
Executable
+58
@@ -0,0 +1,58 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# Smoke test for chainsaw.
|
||||||
|
#
|
||||||
|
# Available SUBSET values: DeepBlueCLI | YamatoSecurity | EVTX-ATTACK-SAMPLES |
|
||||||
|
# EVTX-to-MITRE-Attack | "" (full bundle).
|
||||||
|
#
|
||||||
|
# Env vars: TAG=ls-chainsaw:test SUBSET=DeepBlueCLI KEEP_DATA=1
|
||||||
|
set -u
|
||||||
|
TAG="${TAG:-ls-chainsaw:test}"
|
||||||
|
SUBSET="${SUBSET:-DeepBlueCLI}"
|
||||||
|
KEEP_DATA="${KEEP_DATA:-0}"
|
||||||
|
|
||||||
|
cd "$(dirname "$0")"
|
||||||
|
ROOT="$(pwd)"
|
||||||
|
DATA="$ROOT/test-data/sample-evtx"
|
||||||
|
OUT="$(mktemp -d)"
|
||||||
|
trap 'rm -rf "$OUT"; [ "$KEEP_DATA" = 0 ] && rm -rf "$ROOT/test-data"' EXIT
|
||||||
|
|
||||||
|
pass=0; fail=0
|
||||||
|
ok() { echo "PASS $1"; pass=$((pass+1)); }
|
||||||
|
bad() { echo "FAIL $1"; fail=$((fail+1)); }
|
||||||
|
|
||||||
|
if docker image inspect "$TAG" >/dev/null 2>&1; then
|
||||||
|
ok "image $TAG present"
|
||||||
|
else
|
||||||
|
bad "image $TAG not present"; exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ ! -d "$DATA" ]; then
|
||||||
|
echo "Fetching sample EVTX..."
|
||||||
|
./fetch-test-data.sh >/dev/null
|
||||||
|
fi
|
||||||
|
SCAN="$DATA/$SUBSET"; [ -z "$SUBSET" ] && SCAN="$DATA"
|
||||||
|
n=$(find "$SCAN" -name "*.evtx" | wc -l | tr -d ' ')
|
||||||
|
[ "$n" -gt 0 ] && ok "found $n EVTX in ${SUBSET:-<all>}" || { bad "no EVTX"; exit 1; }
|
||||||
|
|
||||||
|
echo "Running chainsaw hunt..."
|
||||||
|
if docker run --rm --network=none -v "$SCAN:/data:ro" -v "$OUT:/output" "$TAG" >"$OUT/.run.log" 2>&1; then
|
||||||
|
ok "container exited cleanly"
|
||||||
|
else
|
||||||
|
bad "container non-zero"; tail -25 "$OUT/.run.log"
|
||||||
|
fi
|
||||||
|
|
||||||
|
run_dir=$(ls -d "$OUT"/chainsaw_* 2>/dev/null | head -1)
|
||||||
|
[ -d "$run_dir" ] && ok "output dir created" || { bad "no chainsaw_<ts>/ dir"; }
|
||||||
|
[ -d "$run_dir/csv" ] && ok "csv subdir present" || bad "csv/ missing"
|
||||||
|
[ -s "$run_dir/hunt.txt" ] && ok "hunt summary saved" || bad "hunt.txt missing"
|
||||||
|
|
||||||
|
# chainsaw prints "[+] X Detections found" — pull that.
|
||||||
|
if [ -s "$run_dir/hunt.txt" ]; then
|
||||||
|
hits=$(grep -oE '[0-9]+ Detections found' "$run_dir/hunt.txt" | grep -oE '[0-9]+' | head -1)
|
||||||
|
echo
|
||||||
|
echo "Detections: ${hits:-0}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo
|
||||||
|
echo "Summary: $pass pass, $fail fail"
|
||||||
|
[ "$fail" -eq 0 ]
|
||||||
Reference in New Issue
Block a user