DFIR 案件目录与证据清单初始化
用于建立标准化的数字取证案件目录和基础上下文。它会以案件目录名生成 CASE_ID,创建 analysis、reports、context、audit 以及按资产划分的 sources、export 目录,并从模板生成 CLAUDE.md 与 case_context.md;可按 ASSETS 参数一次创建多台主机的证据目录和 Sources Inventory、Asset Inventory 初始行。重复执行时不会覆盖已有登记,而是扫描新增资产和证据文件,按源路径去重后补充清单,适合在正式解析前统一案件结构、资产编号和证据来源。
在 AI 中使用此 Skill将本页链接复制给 AI,即可让 AI 获取完整 Skill 内容并按此执行
安全提示: 本站 Skill 均经 ChatGPT 最新模型扫描,未发现恶意脚本及危险指令、未检出已知恶意行为特征,但不保证绝对安全,使用即表示接受此风险
Skill 文件
版本 20260601 · 5f1f3e2e85a7a3ff9dd1de04b47ba0fc
SKILL.md
---
name: case-init
description: Create the directory scaffold for a new DFIR case. Run once from inside the case directory (cd /cases/CASE-ID then /case-init) or with explicit args (/case-init CLIENT="Acme Corp" ASSETS="dc01 rd01"). Creates analysis/, reports/, context/, tmp/, audit/, and per-asset sources/export/ dirs; stamps CASE_ID and CLIENT into CLAUDE.md and case_context.md; seeds the Sources Inventory. Re-run to add new assets (creates per-asset dirs and seeds Asset Inventory rows). To update the Sources Inventory after copying evidence, run /case-scan-sources.
---
## XingLo Standalone Package Notes
The original repository installs shared case templates globally. This XingLo package includes the two required templates under `templates/` and the commands below use that package-local installed path. References to other `/case-*` or `/tools-*` skills are follow-on workflow suggestions, not required to create the case scaffold.
# Skill: Case Init — Create Multi-Asset Case Directory Structure
## Overview
Creates the directory scaffold for a new DFIR case, stamps the case ID and client into the
`CLAUDE.md` / `context/case_context.md` templates, and seeds starter Sources Inventory rows
for each asset (a disk-image and a memory-image row, which the examiner edits once evidence is
copied in — one row per source). Does **not** create mount point directories (created by
`/tools-mount-e01` and `/tools-mount-ntfs` at mount time) or export subdirectories (created by
each parsing skill at run time).
On re-run, it also scans every `sources/<asset>/` directory and auto-adds any evidence files
not yet listed in the Sources Inventory — deduped by source path. Hostname is inferred from
the filename; the examiner fills in `{role}`. Existing rows are never modified.
---
## Inputs
> **How to pass inputs:** These are shell variables resolved during execution — they are **NOT**
> parameters to the `Skill` tool. Always call this skill as `Skill(skill="case-init")` or, when
> the user supplies values inline (e.g. `/case-init CLIENT="Acme Corp" ASSETS="dc01"`), as
> `Skill(skill="case-init", args="CLIENT='Acme Corp' ASSETS='dc01'")`. Passing any other keyword
> argument to the Skill tool will fail with "Invalid tool parameters".
>
> **Inputs:** Pass `CLIENT` and `ASSETS` inline (e.g. `/case-init CLIENT="Acme Corp" ASSETS="dc01 rd01"`)
> or omit them — the skill will ask interactively before proceeding. Never infer either value from
> conversation history or memory; always get them from the user.
>
> **If a Bash command is denied:** log the denial to `./audit/decisions.log`, stop, and report
> what was blocked. Do not retry silently.
| Variable | Resolved from | Example | Description |
|----------|--------------|---------|-------------|
| `CASE_ID` | Current folder name (primary) or `args` | `ACME-IR-2026-001` | Auto-derived from `basename "$PWD"` when inside `/cases/<id>`; taken from `args` otherwise. |
| `CLIENT` | `args`, then interactive prompt | `Acme Corp` | Client or organisation name. Ask the user if not in `args`. |
| `ASSETS` | `args`, then interactive prompt | `dc01 rd01 wkst01` | Space-separated asset IDs. Ask the user if not in `args`; leave blank to add later. |
| `CASE_BASE` | Hardcoded default | `/cases` | Root directory. Only used on the fallback path (see step 0). |
---
## Commands
### Interactive Prompt (when CLIENT or ASSETS not in args)
If either value was not passed inline, collect both in a single `AskUserQuestion` call
structured exactly as shown below before running any shell commands.
```json
{
"questions": [
{
"question": "What is the client or organisation name for this case?",
"header": "Client name",
"multiSelect": false,
"options": [
{ "label": "Acme Corp", "description": "Example — type the real name in Other" },
{ "label": "Client Corp", "description": "Example — type the real name in Other" }
]
},
{
"question": "Which asset IDs should be scaffolded? (space-separated, e.g. dc01 rd01)",
"header": "Asset IDs",
"multiSelect": false,
"options": [
{ "label": "dc01", "description": "Single domain controller" },
{ "label": "dc01 rd01", "description": "DC + RDP/jump server" },
{ "label": "dc01 rd01 wkst01", "description": "DC + RDP + workstation" }
]
}
]
}
```
Take the user's selections (or their "Other" text) as `CLIENT` and `ASSETS` and proceed to
Step 0. Never infer either value from memory or conversation history.
### Step 0 — Locate / confirm the case root
The installer's recommended flow puts you **inside** the case directory before launching
Claude (`cd /cases` → `mkdir <caseID>` → `cd <caseID>` → `claude`). Detect that and scaffold
in place; otherwise fall back to creating the case under `$CASE_BASE`.
```bash
CASE_BASE="/cases"
PARENT="$(dirname "$PWD")"
if [[ "$PARENT" == "$CASE_BASE" && "$PWD" != "$CASE_BASE" ]]; then
# Primary path: already inside a case directory (the install.sh flow).
CASE_ROOT="$PWD"
CASE_ID="$(basename "$PWD")"
echo "Detected case directory: $CASE_ROOT (CASE_ID=$CASE_ID)"
# → CONFIRM with the user before proceeding: "Initialize this directory as the case root?"
# → Do NOT mkdir the case root; it already exists.
if [[ -f "$CASE_ROOT/context/case_context.md" ]]; then
echo "Already initialized — this is a RE-RUN (see 'Re-running to add assets' below)."
fi
else
# Fallback path: run from /cases root or elsewhere — legacy behavior.
CASE_ID="ACME-IR-2026-001" # ← prompt the user for this
CASE_ROOT="$CASE_BASE/$CASE_ID"
mkdir -p "$CASE_ROOT"
fi
```
If `CLIENT` or `ASSETS` were not passed in `args`, ask the user for each value now before continuing.
```bash
CLIENT="Acme Corp" # ← from args, or ask the user
ASSETS="dc01 rd01" # ← from args, or ask the user (leave blank to add assets later)
```
### First-run scaffold
On a re-run (when `context/case_context.md` already exists) **skip steps 2-3** — do not
re-copy the templates or re-run the placeholder sed, or investigator-filled content is lost.
See "Re-running to add assets" below.
```bash
TEMPLATES="$HOME/.claude/skills/case-init/templates" # canonical templates installed by install.sh
# 1. Create the always-present directory structure
mkdir -p "$CASE_ROOT/analysis" "$CASE_ROOT/reports" "$CASE_ROOT/context" "$CASE_ROOT/audit" "$CASE_ROOT/tmp"
# 2. Copy the CANONICAL templates — never maintain a second copy of them in this skill
cp "$TEMPLATES/CLAUDE.md" "$CASE_ROOT/CLAUDE.md"
cp "$TEMPLATES/context/case_context.md" "$CASE_ROOT/context/case_context.md"
# 3. Substitute case-level placeholders in both copied files
for f in "$CASE_ROOT/CLAUDE.md" "$CASE_ROOT/context/case_context.md"; do
sed -i -e "s#{CASE_ID}#$CASE_ID#g" -e "s#{CLIENT_NAME}#$CLIENT#g" "$f"
done
# 4. Assets (OPTIONAL) — create per-asset dirs and seed starter inventory rows.
if [[ -n "$ASSETS" ]]; then
for ASSET in $ASSETS; do
mkdir -p "$CASE_ROOT/sources/$ASSET" "$CASE_ROOT/export/$ASSET" "$CASE_ROOT/audit/$ASSET"
done
# Seed Asset Inventory (one row per asset) and Sources Inventory (disk01/memory01 placeholders).
# Replaces {asset_id} placeholder rows in each section with per-asset rows.
python3 - "$CASE_ROOT/context/case_context.md" "$CASE_ID" $ASSETS <<'PY'
import sys, re
ctx, case_id, *assets = sys.argv[1:]
def asset_row(a):
return f"| `{a}` | {{hostname}} | {{role}} |"
def source_rows(a):
# 4 columns: SourceID | AssetID | Type | SourcePath (Type matches the template; case-parse
# and case-analyze Tier-2 source ranking read it instead of guessing from the extension).
return [
f"| `{a}-disk01` | `{a}` | `disk` | `/cases/{case_id}/sources/{a}/{{hostname}}.E01` |",
f"| `{a}-memory01` | `{a}` | `memory` | `/cases/{case_id}/sources/{a}/{{hostname}}.img` |",
]
# A placeholder row is any inventory row whose first cell starts with `{asset_id}` —
# this covers the Asset Inventory row (`{asset_id}`) AND the Sources Inventory rows
# (`{asset_id}-disk01` / `{asset_id}-memory01`), which a bare `startswith` on the
# closing backtick would miss. Seed once per section on the first placeholder seen,
# then drop every placeholder row in that section (Sources has two).
ph = re.compile(r'^\|\s*`\{asset_id\}')
lines = open(ctx).read().splitlines()
out, section, seeded = [], None, set()
for ln in lines:
s = ln.strip()
if s == '## Asset Inventory': section = 'asset'
elif s == '## Sources Inventory': section = 'sources'
elif s.startswith('## '): section = None
if section in ('asset', 'sources') and ph.match(ln):
if section not in seeded:
if section == 'asset': out.extend(asset_row(a) for a in assets)
else: out.extend(r for a in assets for r in source_rows(a))
seeded.add(section)
continue # drop placeholder row(s)
out.append(ln)
open(ctx, "w").write("\n".join(out) + "\n")
PY
echo "Seeded Asset Inventory and Sources Inventory for: $ASSETS"
echo " Fill in hostname/role in Asset Inventory; run /case-scan-sources after copying evidence to auto-populate source paths."
else
echo "No assets provided — add them later by re-running /case-init, or"
echo " mkdir sources/<asset>/ export/<asset>/ audit/<asset>/ manually."
fi
echo "Case directory ready: $CASE_ROOT"
echo ""
echo "If you have not yet verified tool availability on this workstation, run /tools-preflight"
echo "before /case-investigate — it is a one-time check per workstation, not per case."
echo ""
echo "Next steps:"
echo " 1. Drop evidence into sources/<asset_id>/"
echo " 2. Run /case-scan-sources (updates Sources Inventory with the files you just copied)"
echo " 3. Run /tools-preflight (once per workstation)"
echo " 4. If evidence includes disk images (.E01, .dd, .img): run /tools-mount first"
echo " 5. Run /case-investigate"
```
### Re-running to add assets
If `context/case_context.md` already exists, **do not re-copy the templates and do not re-run
the placeholder sed** — that would clobber investigator-filled content.
```bash
# 1. Create directories for any newly specified assets
if [[ -n "$ASSETS" ]]; then
for ASSET in $ASSETS; do
mkdir -p "$CASE_ROOT/sources/$ASSET" "$CASE_ROOT/export/$ASSET" "$CASE_ROOT/audit/$ASSET"
done
fi
# 2. Scan all sources/<asset>/ dirs and upsert Sources Inventory rows for new files
# and currently-mounted directories. Also adds missing Asset Inventory rows for
# any new assets found. Dedup key: source path (last backtick-quoted column).
python3 - "$CASE_ROOT/context/case_context.md" "$CASE_ROOT" <<'PY'
import sys, os, re
from collections import defaultdict
TYPE_SLUG = {
'.e01': 'disk', '.dd': 'disk',
'.vmem': 'memory', '.mem': 'memory', '.raw': 'memory', '.dmp': 'memory', '.img': 'memory',
}
ctx, case_root = sys.argv[1], sys.argv[2]
sources_root = os.path.join(case_root, "sources")
lines = open(ctx).read().splitlines()
def has_placeholder_path(ln):
ticks = re.findall(r'`([^`]+)`', ln)
return bool(ticks) and '{' in ticks[-1]
# Collect existing source paths and max SourceID sequence per (asset, slug)
existing_paths = set()
max_seq = defaultdict(int)
for ln in lines:
ticks = re.findall(r'`([^`]+)`', ln)
if not ticks or not ticks[-1].startswith('/'):
continue
existing_paths.add(ticks[-1])
if len(ticks) >= 2 and not has_placeholder_path(ln):
m = re.match(r'^(.+)-(disk|memory|mount)(\d{2})$', ticks[0])
if m:
max_seq[(m.group(1), m.group(2))] = max(max_seq[(m.group(1), m.group(2))], int(m.group(3)))
# Collect new evidence candidates
candidates = [] # (asset, fpath, slug)
if os.path.isdir(sources_root):
for asset in sorted(os.listdir(sources_root)):
src_dir = os.path.join(sources_root, asset)
if not os.path.isdir(src_dir):
continue
for name in sorted(os.listdir(src_dir)):
fpath = os.path.join(src_dir, name)
if os.path.isfile(fpath):
slug = TYPE_SLUG.get(os.path.splitext(name)[1].lower())
if slug and fpath not in existing_paths:
candidates.append((asset, fpath, slug))
elif os.path.isdir(fpath) and os.path.ismount(fpath) and fpath not in existing_paths:
candidates.append((asset, fpath, 'mount'))
# Assign SourceIDs — always zero-padded two-digit to stay stable when a second image arrives
seq = defaultdict(int)
new_rows = []
for asset, fpath, slug in candidates:
seq[(asset, slug)] += 1
n = max_seq[(asset, slug)] + seq[(asset, slug)]
source_id = f"{asset}-{slug}{n:02d}"
# 4 columns incl. Type (= slug: disk|memory|mount) so Tier-2 source ranking is deterministic.
new_rows.append(f"| `{source_id}` | `{asset}` | `{slug}` | `{fpath}` |")
# Locate Sources Inventory section
in_s, src_start, src_end, last_src_idx = False, -1, len(lines), -1
for i, ln in enumerate(lines):
if ln.strip() == '## Sources Inventory':
in_s = True; src_start = i
elif in_s and re.match(r'^##\s', ln):
src_end = i; in_s = False
if in_s and ln.startswith('|'):
last_src_idx = i
src_placeholder_count = sum(
1 for i, ln in enumerate(lines)
if src_start < i < src_end and ln.startswith('|') and has_placeholder_path(ln)
)
# Locate Asset Inventory section and existing asset IDs
in_a, ast_start, ast_end, last_ast_idx = False, -1, len(lines), -1
existing_assets = set()
for i, ln in enumerate(lines):
if ln.strip() == '## Asset Inventory':
in_a = True; ast_start = i
elif in_a and re.match(r'^##\s', ln):
ast_end = i; in_a = False
if in_a and ln.startswith('| `') and not has_placeholder_path(ln):
ticks = re.findall(r'`([^`]+)`', ln)
if ticks: existing_assets.add(ticks[0])
if in_a and ln.startswith('|'):
last_ast_idx = i
new_asset_rows = [
f"| `{asset}` | {{hostname}} | {{role}} |"
for asset in (sorted(os.listdir(sources_root)) if os.path.isdir(sources_root) else [])
if os.path.isdir(os.path.join(sources_root, asset)) and asset not in existing_assets
]
if not new_rows and not src_placeholder_count and not new_asset_rows:
print("Inventories: nothing to update.")
sys.exit(0)
# Build output: drop placeholder rows in both sections, insert real rows at section ends
out = []
for i, ln in enumerate(lines):
drop = (
(src_start < i < src_end and ln.startswith('|') and has_placeholder_path(ln)) or
(new_asset_rows and ast_start < i < ast_end and ln.startswith('|') and has_placeholder_path(ln))
)
if not drop:
out.append(ln)
if last_src_idx >= 0 and i == last_src_idx:
out.extend(new_rows)
if last_ast_idx >= 0 and i == last_ast_idx:
out.extend(new_asset_rows)
open(ctx, 'w').write('\n'.join(out) + '\n')
parts = []
if src_placeholder_count: parts.append(f"removed {src_placeholder_count} source placeholder(s)")
if new_rows: parts.append(f"added {len(new_rows)} source row(s)")
if new_asset_rows: parts.append(f"added {len(new_asset_rows)} asset row(s)")
print(f"Inventories: {', '.join(parts)}.")
PY
```
---
## Templates
The case `CLAUDE.md` and `context/case_context.md` are **copied verbatim** from the canonical
templates bundled in `~/.claude/skills/case-init/templates/` by this standalone package — this skill carries its required
copy (a second copy would drift out of sync). The only customization is the placeholder
substitution (step 3) and the per-asset Sources Inventory seeding (step 4). To change what a
new case looks like, edit the files under this package's `templates/` directory.
Placeholder convention (used across all templates): `{CASE_ID}`, `{CLIENT_NAME}`, `{CASE_ROOT}`,
`{asset_id}`, `{hostname}`.
---
## Post-Init Checklist
After running case-init:
1. If you skipped assets at init, add them now — re-run `/case-init` from the case
directory, or create `sources/<asset>/ export/<asset>/ audit/<asset>/` manually.
2. Copy evidence into `sources/<asset_id>/` — one entry per source (disk image, memory image,
or a directory of logs); any number per asset.
3. Run `/case-scan-sources` after dropping evidence into `sources/<asset_id>/` — it auto-adds
`.E01` / `.dd` / `.img` / `.vmem` / `.mem` / `.raw` / `.dmp` files not yet in the inventory
(hostname inferred from filename; fill in `{role}` afterwards). Or edit the Sources Inventory
in `context/case_context.md` manually — one row per source, one row per file.
4. Fill in the rest of `context/case_context.md`:
- Examiner & Sign-off: `Lead Examiner` (becomes the report's author of record at sign-off)
- Network Topology (if known)
- Domain Accounts (from initial brief)
- Known IOCs (from threat intel or initial triage)
5. Open the case directory in Claude Code — `CLAUDE.md` loads automatically.
6. If you have not yet run `/tools-preflight` on this workstation, do it now — it is a
one-time tool-availability check, not per-case. Run it before `/case-investigate` so a missing
tool is caught up front rather than mid-run.
7. If evidence includes disk images (`.E01`, `.dd`, `.img`): run `/tools-mount` to mount them
before starting the pipeline — `/case-investigate` parses from mounted filesystems, not raw images.
8. Run `/case-investigate` to start the full parsing → analysis → report pipeline.
---
## Resulting Structure
```
/cases/{CASE_ID}/
CLAUDE.md ← reference-only; loads case_context.md + case-investigate skill
context/
case_context.md ← investigator-maintained intel
sources/
{asset_id}/ ← drop evidence files here (E01, img, vmem)
← mount dirs created here by tools-mount-e01 / tools-mount-ntfs
export/
{asset_id}/ ← parsed evidence ONLY (tool output; subdirs created by each skill)
analysis/ ← per-asset reports + global correlation report
reports/ ← final report (MD + PDF)
tmp/ ← parse-phase working artifacts (dirty-hive copies, staged inputs)
← never cited as evidence; safe to discard after parse phase
audit/ ← operational/control plane (NOT evidence, NOT analysis output)
.dfir_phase ← pipeline phase marker (gates ./export writes)
artifact_failures.log ← parse failures / unparseable artifacts
decisions.log ← autonomous AI decisions when blocked
mount.log ← /tools-mount log: the sudo mount commands it emitted for the operator
forensic_actions.jsonl ← per-action audit trail (PostToolUse hook)
forensic_audit.log ← per-session record (Stop hook)
{asset_id}/ ← per-asset parse_state.txt + parse.log
```
此文件不提供内嵌文本预览
请使用左侧文件行末尾的外链图标打开原始文件。
