Skip to content

Commit 46207c2

Browse files
DavidJBiancoclaude
andcommitted
Restructure output into unified scenario directories
Each scenario now lives in one directory with all related files: scenarios/<name>/ scenario.yaml # scenario definition ENVIRONMENT.md # student context (from /eforge scenario) GROUND_TRUTH.md # answer key (from eforge generate) data/ # generated log files windows_event_security.xml zeek_conn.json ... Key changes: - CLI generate derives data/ path from scenario file location - Re-generation clears and overwrites data/ (no timestamped dirs) - GROUND_TRUTH.md written to scenario root, not inside data/ - Engine accepts separate ground_truth_dir parameter - --output flag still works as full override for backward compat - All 4 skills updated for new path conventions Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ff2d668 commit 46207c2

5 files changed

Lines changed: 74 additions & 31 deletions

File tree

commands/eforge/evaluate.md

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,24 +27,34 @@ If they don't have generated output yet, suggest using `/eforge generate` first.
2727

2828
### Step 1: Locate the Output
2929

30-
The user needs to provide:
31-
1. **Output directory** — the directory containing generated log files (e.g., `output/retail-store-ftp-attack-20260316-140908/`)
32-
2. **Scenario file** — the YAML scenario used for generation
30+
The user needs to provide (or you can infer) the scenario directory. The standard layout is:
3331

34-
If the user doesn't specify, look for the most recent output directory under `output/` or wherever they typically generate. Ask if you can't find it.
32+
```
33+
scenarios/<scenario-name>/
34+
scenario.yaml
35+
ENVIRONMENT.md
36+
GROUND_TRUTH.md
37+
data/ ← this is the output_dir for eforge eval
38+
```
39+
40+
If the user provides the scenario directory (e.g., `scenarios/retail-store-ftp-attack/`), derive:
41+
- Data directory: `scenarios/<name>/data/`
42+
- Scenario file: `scenarios/<name>/scenario.yaml`
43+
44+
If they don't specify, look for scenario directories under `scenarios/`. Ask if you can't find it.
3545

3646
### Step 2: Run the Evaluation
3747

3848
Run both text and JSON output:
3949

4050
```bash
41-
uv run eforge eval <output_dir> --scenario <scenario.yaml> --verbose
51+
uv run eforge eval scenarios/<name>/data/ --scenario scenarios/<name>/scenario.yaml --verbose
4252
```
4353

4454
Also capture the JSON for programmatic analysis:
4555

4656
```bash
47-
uv run eforge eval <output_dir> --scenario <scenario.yaml> --format json 2>/dev/null
57+
uv run eforge eval scenarios/<name>/data/ --scenario scenarios/<name>/scenario.yaml --format json 2>/dev/null
4858
```
4959

5060
### Step 3: Interpret Results

commands/eforge/generate.md

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -76,15 +76,28 @@ cd /Users/dabianco/projects/SURGe/data-gen-test
7676
uv run eforge generate <scenario-file> --verbose
7777
```
7878
79-
Always use `--verbose` so you can see progress and diagnose issues. Generation creates a timestamped output directory like `output/scenario-name-20240115-100000/`.
79+
Always use `--verbose` so you can see progress and diagnose issues. Generation writes log files to a `data/` subdirectory alongside the scenario file:
80+
81+
```
82+
scenarios/<scenario-name>/
83+
scenario.yaml ← input
84+
ENVIRONMENT.md ← created by /eforge scenario
85+
GROUND_TRUTH.md ← generated (answer key)
86+
data/ ← generated log files
87+
windows_event_security.xml
88+
zeek_conn.json
89+
...
90+
```
91+
92+
Re-running generation overwrites the previous `data/` directory.
8093
8194
### 3. Post-Generation
8295
8396
After successful generation:
8497
- List the generated files and their sizes
8598
- Check that expected formats were produced
86-
- If the scenario had a storyline, note that `GROUND_TRUTH.md` was generated in the output directory — this is the answer key containing the full attack timeline and IOCs
87-
- If an `ENVIRONMENT.md` exists alongside the scenario file (created by `/eforge scenario`), copy it into the output directory so it sits alongside the generated logs and GROUND_TRUTH.md
99+
- If the scenario had a storyline, note that `GROUND_TRUTH.md` was generated alongside the scenario file — this is the answer key containing the full attack timeline and IOCs
100+
- `ENVIRONMENT.md` (created by `/eforge scenario`) is already in the same directory — no copying needed
88101
- Summarize the output for the user
89102
90103
### 4. Diagnose Errors

commands/eforge/scenario.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -411,8 +411,8 @@ After generating the scenario YAML, also create an `ENVIRONMENT.md` file in the
411411

412412
After the interview, generate both files:
413413

414-
1. **Scenario YAML** — Write to the user's chosen path (default: `scenarios/<scenario-name>.yaml`)
415-
2. **ENVIRONMENT.md** — Write alongside the scenario YAML
414+
1. **Scenario YAML** — Write to the user's chosen path (default: `scenarios/<scenario-name>/scenario.yaml`)
415+
2. **ENVIRONMENT.md** — Write alongside the scenario YAML (default: `scenarios/<scenario-name>/ENVIRONMENT.md`)
416416
3. **Realism Review** — Before validating, review the entire scenario as a tough-but-fair devil's advocate. Check:
417417
- **Attack realism**: Does the attack chain make sense? Would a real attacker do this in this order? Are there missing steps (e.g., no reconnaissance before lateral movement, no persistence after initial access)?
418418
- **Technical accuracy**: Are command lines correct for the target OS? Are process paths right? Do the MITRE ATT&CK technique IDs match what's actually happening?

src/evidenceforge/cli/commands.py

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -259,18 +259,24 @@ def generate(
259259

260260
# Determine output directory
261261
if output:
262-
output_dir = output
263-
elif scenario.output.destination:
264-
output_dir = Path(scenario.output.destination)
262+
# Explicit --output flag: use as data directory directly
263+
data_dir = output
264+
ground_truth_dir = output
265265
else:
266-
output_dir = Path("./output")
266+
# Default: derive from scenario file location
267+
# scenarios/<name>/scenario.yaml → data goes to scenarios/<name>/data/
268+
scenario_dir = scenario_file.parent
269+
data_dir = scenario_dir / "data"
270+
ground_truth_dir = scenario_dir
267271

268-
# Create timestamped subdirectory
269-
from datetime import datetime
270-
timestamp = datetime.now().strftime("%Y%m%d-%H%M%S")
271-
output_dir = output_dir / f"{scenario.name}-{timestamp}"
272+
console.print(f"\n[bold]Data directory:[/bold] {data_dir}")
273+
console.print(f"[bold]Ground truth:[/bold] {ground_truth_dir / 'GROUND_TRUTH.md'}")
272274

273-
console.print(f"\n[bold]Output directory:[/bold] {output_dir}")
275+
# Clear previous data on re-generation
276+
if data_dir.exists():
277+
import shutil
278+
shutil.rmtree(data_dir)
279+
console.print("[dim]Cleared previous data[/dim]")
274280

275281
# Generate logs
276282
try:
@@ -333,23 +339,33 @@ def progress_callback(event_type: str, data: dict) -> None:
333339
# Generate logs with progress reporting
334340
engine = GenerationEngine(
335341
scenario=scenario,
336-
output_dir=output_dir,
337-
progress_callback=progress_callback
342+
output_dir=data_dir,
343+
progress_callback=progress_callback,
344+
ground_truth_dir=ground_truth_dir,
338345
)
339346
engine.generate()
340347

341348
console.print("\n[bold green]✓ Generation complete![/bold green]")
342-
console.print(f"\nGenerated logs:")
343-
console.print(f" Directory: {output_dir}")
349+
console.print(f"\nGenerated files:")
350+
console.print(f" Scenario directory: {ground_truth_dir}")
344351

345-
# List generated files
346-
if output_dir.exists():
347-
for file in sorted(output_dir.iterdir()):
348-
if file.is_file():
352+
# List files in scenario root (GROUND_TRUTH.md)
353+
if ground_truth_dir.exists():
354+
for file in sorted(ground_truth_dir.iterdir()):
355+
if file.is_file() and file.name == "GROUND_TRUTH.md":
349356
size = file.stat().st_size
350357
size_str = f"{size:,} bytes" if size < 1024 else f"{size / 1024:.1f} KB"
351358
console.print(f" • {file.name} ({size_str})")
352359

360+
# List generated log files in data/
361+
if data_dir.exists():
362+
console.print(f" Data: {data_dir}")
363+
for file in sorted(data_dir.iterdir()):
364+
if file.is_file():
365+
size = file.stat().st_size
366+
size_str = f"{size:,} bytes" if size < 1024 else f"{size / 1024:.1f} KB"
367+
console.print(f" • {file.name} ({size_str})")
368+
353369
# Success - exit normally
354370
return
355371

src/evidenceforge/generation/engine.py

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,18 +59,21 @@ def __init__(
5959
self,
6060
scenario: Scenario,
6161
output_dir: Path,
62-
progress_callback: Optional[Callable[[str, dict], None]] = None
62+
progress_callback: Optional[Callable[[str, dict], None]] = None,
63+
ground_truth_dir: Optional[Path] = None,
6364
):
6465
"""Initialize generation engine.
6566
6667
Args:
6768
scenario: Validated scenario object
68-
output_dir: Output directory path
69+
output_dir: Output directory path for generated log files
6970
progress_callback: Optional callback for progress reporting.
7071
Called with (event_type: str, data: dict) at key milestones.
72+
ground_truth_dir: Directory for GROUND_TRUTH.md. Defaults to output_dir.
7173
"""
7274
self.scenario = scenario
7375
self.output_dir = output_dir
76+
self.ground_truth_dir = ground_truth_dir or output_dir
7477
self.progress_callback = progress_callback
7578
self.state_manager = StateManager()
7679
self.emitters: dict[str, WindowsEventEmitter | ZeekEmitter | EcarEmitter | SyslogEmitter | BashHistoryEmitter | SnortEmitter | WebEmitter] = {}
@@ -724,7 +727,8 @@ def _generate_ground_truth(self) -> None:
724727
Creates comprehensive attack documentation including narrative,
725728
timeline, and IOCs for threat hunting training.
726729
"""
727-
output_path = self.output_dir / "GROUND_TRUTH.md"
730+
self.ground_truth_dir.mkdir(parents=True, exist_ok=True)
731+
output_path = self.ground_truth_dir / "GROUND_TRUTH.md"
728732

729733
generator = GroundTruthGenerator(
730734
scenario=self.scenario,

0 commit comments

Comments
 (0)