From 8fe7a4312d5091f962aeb2256f9911e7f486de84 Mon Sep 17 00:00:00 2001 From: tabledevil Date: Thu, 7 May 2026 19:20:32 +0200 Subject: [PATCH] Initial commit: chainsaw EVTX hunter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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_/{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) --- .gitignore | 1 + Dockerfile | 48 ++++++++++++++++++++++++++++++++++++++ README.md | 38 ++++++++++++++++++++++++++++++ fetch-test-data.sh | 8 +++++++ start.sh | 39 +++++++++++++++++++++++++++++++ test_smoke.sh | 58 ++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 192 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 README.md create mode 100755 fetch-test-data.sh create mode 100755 start.sh create mode 100755 test_smoke.sh diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..6350f0f --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +test-data/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..deb7728 --- /dev/null +++ b/Dockerfile @@ -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"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..734cd40 --- /dev/null +++ b/README.md @@ -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_/`: + +- `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). diff --git a/fetch-test-data.sh b/fetch-test-data.sh new file mode 100755 index 0000000..609f6a9 --- /dev/null +++ b/fetch-test-data.sh @@ -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" diff --git a/start.sh b/start.sh new file mode 100755 index 0000000..2e54529 --- /dev/null +++ b/start.sh @@ -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 diff --git a/test_smoke.sh b/test_smoke.sh new file mode 100755 index 0000000..239542b --- /dev/null +++ b/test_smoke.sh @@ -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:-}" || { 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_/ 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 ]