Skip to content

Commit 368fefc

Browse files
DavidJBiancoclaude
andcommitted
Complete Phase 2.4: Enhanced Scenario Schema with timezone tests and test fixes
Phase 2.4 additions: - parse_work_hours() utility for temporal activity distribution - Persona model: optional expanded_activities, work_hours_parsed, activity_intensity - StorylineEvent model: optional event_sequence, duration, retry_on_failure, success_probability - Validation for expanded_activities and event_sequence structures - Timezone unit tests (14 tests) and integration tests (4 tests) - Scenario schema reference documentation (docs/scenario-reference.md) Bug fix: - Fix emitter _flush_unlocked() truncating output on every flush when no header template exists (mode was always "w" instead of "a") Test fixes (21 previously failing tests): - test_engine.py: mock all 7 emitter classes (was only mocking 2 from Phase 1) - test_format_definitions.py: expect 7 formats instead of 2 - test_emitter_threading.py: fix header test using format without headers - test_parallel_generation.py: fix timezone-naive datetime comparison Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent f89eedc commit 368fefc

14 files changed

Lines changed: 1308 additions & 45 deletions

TODO.md

Lines changed: 44 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
# EvidenceForge Implementation Plan
22

3-
**Status:** Phase 2 - Scalability (In Progress - 2.4-2.9 remaining)
3+
**Status:** Phase 2 - Scalability (In Progress - 2.5-2.9 remaining)
44
**Started:** 2026-03-11
5-
**Last Updated:** 2026-03-12
5+
**Last Updated:** 2026-03-12 (Phase 2.4 complete)
66
**Target MVP Completion:** 7-10 weeks from start
77

88
**Recent Completions:**
99
- ✅ Phase 2.1: Parallel Generation with Threaded Emitters
1010
- ✅ Phase 2.2: 5 New Log Formats (eCAR, syslog, bash_history, snort_alert, web_access)
1111
- ✅ Phase 2.3: Progress Reporting
1212
- ✅ Phase 2.10: OS-Aware Activity Generation (Windows + Linux support)
13+
- ✅ Phase 2.4: Enhanced Scenario Schema (work hours parsing, model expansion, timezone tests, validation, docs)
1314

1415
---
1516

@@ -219,15 +220,45 @@
219220
- [x] Show ETA based on moving average
220221
- [x] Test: Progress updates correctly during generation
221222

222-
### 2.4 Enhanced Scenario Schema
223-
224-
- [ ] Expand `models/scenario.py` with full MVP schema
225-
- [ ] Timezone configuration (default + per-system patterns)
226-
- [ ] Persona structure with expanded_activities (prepare for LLM expansion)
227-
- [ ] Storyline with event_sequence (prepare for LLM expansion)
228-
- [ ] Complete environment specification
229-
- [ ] Timezone handling in utilities
230-
- [ ] Test: Timezone conversions (UTC internal → system timezone output)
223+
### 2.4 Enhanced Scenario Schema ✅ COMPLETE
224+
225+
**Goal:** Add optional schema fields for LLM expansion (Phase 3.1) and persona generation (Phase 2.6)
226+
227+
- [x] Add `parse_work_hours()` utility to `utils/time.py`
228+
- [x] Parse "9am-5pm" format to hour ranges
229+
- [x] Support lunch breaks: "9am-5pm (lunch 12pm-1pm)"
230+
- [x] Support half-hours: "8:30am-5:30pm"
231+
- [x] Calculate peak hours (mid-morning, mid-afternoon)
232+
- [x] Add 10+ test cases in `tests/unit/test_time_parsing.py` (17 tests, all passing)
233+
- [x] Expand Persona model in `models/scenario.py` (optional fields, backward compatible)
234+
- [x] Timezone configuration (default + per-system patterns) - Already exists!
235+
- [x] Add optional `expanded_activities` field (for Phase 3.1 LLM to populate)
236+
- [x] Add optional `work_hours_parsed` field (auto-populated from work_hours)
237+
- [x] Add optional `activity_intensity` field (per-activity overrides)
238+
- [x] Add `@model_validator` to auto-populate work_hours_parsed
239+
- [x] Expand StorylineEvent model in `models/scenario.py` (optional fields, backward compatible)
240+
- [x] Add optional `event_sequence` field (sub-events for complex attacks)
241+
- [x] Add optional `duration` field (event duration like "30m")
242+
- [x] Add optional `retry_on_failure` field
243+
- [x] Add optional `success_probability` field (0.0-1.0)
244+
- [x] Add timezone tests
245+
- [x] Create `tests/unit/test_timezone_handling.py` (14 tests: pattern matching, DST, conversions)
246+
- [x] Create `tests/integration/test_scenario_timezone.py` (4 tests: multi-timezone scenarios)
247+
- [x] Test UTC → local timezone conversions
248+
- [x] Test pattern-based timezone overrides
249+
- [x] Test work hours parsing
250+
- [x] Update validation in `validation/schema.py`
251+
- [x] Validate expanded_activities structure if present
252+
- [x] Validate event_sequence structure if present
253+
- [x] 6 new validation tests added to `tests/unit/test_validation.py`
254+
- [x] Documentation
255+
- [x] Create `docs/scenario-reference.md` with full schema reference
256+
- [x] Document optional fields for Phase 3.1 LLM expansion
257+
- [x] Document backward compatibility guarantees
258+
- [x] Verification
259+
- [x] Test backward compatibility with existing scenarios (minimal.yaml, attack.yaml)
260+
- [x] Verify work_hours_parsed auto-population works
261+
- [x] All 105 Phase 2.4 tests pass
231262

232263
### 2.5 Network Visibility Architecture
233264

@@ -321,8 +352,8 @@
321352
**Architecture Note:** Native logs (Windows Event Security, syslog) are ALWAYS present per OS type. eCAR is OPTIONAL and may be present on all, some, or no systems (EDR/XDR is not universally deployed).
322353

323354
**Phase 2 Status:**
324-
- ✅ Complete: 2.1 (Parallel Generation), 2.2 (7 Log Formats), 2.3 (Progress Reporting), 2.10 (OS-Aware Generation)
325-
- 🚧 In Progress: 2.4-2.9 (Enhanced Schema, Network Visibility, Persona Generation, LLM Integration, Medium Datasets)
355+
- ✅ Complete: 2.1 (Parallel Generation), 2.2 (7 Log Formats), 2.3 (Progress Reporting), 2.4 (Enhanced Schema), 2.10 (OS-Aware Generation)
356+
- 🚧 Pending: 2.5-2.9 (Network Visibility, Persona Generation, LLM Integration, Medium Datasets, Phase 2 Testing)
326357

327358
**Phase 2 Milestone (Partial):** Can generate datasets across 7 formats (Windows Event Security, Zeek, eCAR, syslog, bash_history, snort_alert, web_access) in parallel with threaded emitters. Windows and Linux systems generate appropriate OS-specific logs. Native logs (Windows Event/syslog) always present; eCAR optional EDR/XDR layer.
328359

docs/scenario-reference.md

Lines changed: 219 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
# Scenario Schema Reference
2+
3+
This document describes the EvidenceForge scenario file schema, including Phase 2.4 enhanced fields.
4+
5+
## Overview
6+
7+
Scenario files are YAML documents that define the environment, users, systems, personas, and storyline for log generation. All fields marked "Phase 2.4+" are optional and backward compatible with Phase 1 scenarios.
8+
9+
## Top-Level Structure
10+
11+
```yaml
12+
version: "1.0"
13+
name: scenario-name # Alphanumeric, dash, underscore
14+
description: |
15+
Multi-line scenario description
16+
environment: ...
17+
personas: [...] # Optional
18+
time_window: ...
19+
baseline_activity: ...
20+
storyline: [...] # Optional
21+
output: ...
22+
```
23+
24+
## Environment
25+
26+
```yaml
27+
environment:
28+
description: "Corporate office network"
29+
timezone:
30+
default: "America/New_York"
31+
systems: # Optional pattern-based overrides
32+
"EU-*": "Europe/London"
33+
"AP-*": "Asia/Tokyo"
34+
users: [...]
35+
systems: [...]
36+
groups: [...] # Optional
37+
```
38+
39+
### Timezone Configuration
40+
41+
All internal timestamps are stored in UTC. The timezone configuration controls output formatting.
42+
43+
- **default**: Applied to all systems unless overridden (default: `"UTC"`)
44+
- **systems**: Pattern-based overrides using fnmatch glob syntax (`*`, `?`, `[seq]`)
45+
- First matching pattern wins
46+
- Unmatched hostnames use the default
47+
48+
Valid timezone names are any [pytz timezone](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones) (e.g., `America/New_York`, `Europe/London`, `Asia/Tokyo`, `UTC`).
49+
50+
### Users
51+
52+
```yaml
53+
users:
54+
- username: jsmith # Required: alphanumeric, dash, underscore
55+
full_name: "Jane Smith" # Required
56+
email: jane@example.com # Required
57+
groups: ["developers"] # Optional
58+
enabled: true # Optional (default: true)
59+
persona: developer # Optional: reference to persona name
60+
primary_system: WS-01 # Optional: reference to system hostname
61+
```
62+
63+
### Systems
64+
65+
```yaml
66+
systems:
67+
- hostname: WS-01 # Required: RFC 1123 compliant
68+
ip: "10.0.1.10" # Required: IPv4 or IPv6
69+
os: "Windows 10" # Required
70+
type: workstation # Required: workstation|server|domain_controller
71+
assigned_user: jsmith # Optional: reference to username
72+
services: ["IIS"] # Optional
73+
```
74+
75+
## Personas
76+
77+
Personas define user behavior patterns for activity generation.
78+
79+
```yaml
80+
personas:
81+
- name: developer # Required: unique identifier
82+
description: "Software developer who codes and browses" # Required
83+
typical_activities: # Optional list of activity strings
84+
- coding
85+
- web_browsing
86+
work_hours: "9am-5pm" # Optional (default: "9am-5pm")
87+
application_usage: # Optional
88+
- vscode
89+
- chrome
90+
risk_profile: low # Optional: low|medium|high (default: "medium")
91+
```
92+
93+
### Work Hours Format
94+
95+
The `work_hours` field supports these formats:
96+
- `"9am-5pm"` - Basic range
97+
- `"8:30am-5:30pm"` - Half-hour precision
98+
- `"9am-5pm (lunch 12pm-1pm)"` - With lunch break
99+
- `"8:30am-5:30pm (lunch 12:30pm-1:30pm)"` - Both combined
100+
101+
Work hours are automatically parsed into a `work_hours_parsed` dict containing:
102+
- `start`: Start hour as float (e.g., 9.0, 8.5)
103+
- `end`: End hour as float (e.g., 17.0, 17.5)
104+
- `lunch`: Tuple of (start, end) if specified, else null
105+
- `hours`: List of active integer hours (excluding lunch)
106+
- `peak_hours`: Mid-morning and mid-afternoon hours
107+
108+
### Phase 2.4+ Optional Fields
109+
110+
These fields are for future LLM expansion (Phase 3.1) and are not required:
111+
112+
```yaml
113+
personas:
114+
- name: developer
115+
# ... Phase 1 fields above ...
116+
117+
expanded_activities: # Phase 2.4+: LLM-populated activity sequences
118+
- activity_type: process_code
119+
sequence:
120+
- action: open_ide
121+
app: VS Code
122+
- action: edit_files
123+
duration_minutes: 30
124+
temporal_pattern: morning_focus
125+
frequency: daily
126+
127+
activity_intensity: # Phase 2.4+: Per-activity events/hour overrides
128+
process_code: 20
129+
connection_web: 5
130+
```
131+
132+
**expanded_activities** items must have:
133+
- `activity_type` (required): Maps to baseline activity types
134+
- `sequence` (optional): List of action steps
135+
- `temporal_pattern` (optional): When this activity typically occurs
136+
- `frequency` (optional): How often (hourly, daily, weekly)
137+
138+
## Time Window
139+
140+
```yaml
141+
time_window:
142+
start: "2024-01-15T10:00:00Z" # Required: ISO 8601 UTC
143+
end: "2024-01-15T18:00:00Z" # Either end OR duration required
144+
duration: "8h" # Supports: "10h", "3d", "2h30m"
145+
```
146+
147+
## Baseline Activity
148+
149+
```yaml
150+
baseline_activity:
151+
description: "Normal office activity"
152+
intensity: medium # low|medium|high (events/user/hour)
153+
variation: low # low|medium|high (timing variation)
154+
```
155+
156+
Intensity mapping: low=5, medium=15, high=40 events/user/hour.
157+
158+
## Storyline
159+
160+
Storyline events define specific actions at specific times.
161+
162+
```yaml
163+
storyline:
164+
- time: "+2h30m" # Required: ISO 8601, relative offset, or seconds
165+
actor: attacker # Required: username or "attacker"
166+
system: WS-01 # Required: system hostname
167+
activity: "lateral movement" # Required: activity description
168+
details: # Optional: activity-specific details
169+
target_ip: "10.0.1.20"
170+
method: "pass-the-hash"
171+
```
172+
173+
### Phase 2.4+ Optional Fields
174+
175+
```yaml
176+
storyline:
177+
- time: "+2h30m"
178+
# ... Phase 1 fields above ...
179+
180+
event_sequence: # Phase 2.4+: Multi-step sub-events
181+
- sub_event_type: process
182+
delay_seconds: 5
183+
details:
184+
process_name: powershell.exe
185+
- sub_event_type: file
186+
delay_seconds: 10
187+
details:
188+
file_path: C:\temp\payload.exe
189+
190+
duration: "30m" # Phase 2.4+: Event duration
191+
success_probability: 0.8 # Phase 2.4+: 0.0-1.0
192+
retry_on_failure: true # Phase 2.4+: Retry flag
193+
```
194+
195+
**event_sequence** items must have:
196+
- `sub_event_type` (required): Type of sub-event (e.g., process, file, network)
197+
- `delay_seconds` (optional): Delay before this sub-event
198+
- `details` (optional): Sub-event-specific details
199+
200+
## Output
201+
202+
```yaml
203+
output:
204+
logs:
205+
- format: windows_event_security
206+
- format: zeek_conn
207+
- format: ecar
208+
destination: ./output
209+
compression: false # Optional (default: false)
210+
```
211+
212+
Supported formats: `windows_event_security`, `zeek_conn`, `ecar`, `syslog`, `bash_history`, `snort_alert`, `web_access`.
213+
214+
## Backward Compatibility
215+
216+
All Phase 2.4+ fields are optional with null defaults. Existing Phase 1 scenarios work without modification:
217+
- `expanded_activities`, `work_hours_parsed`, `activity_intensity` default to null
218+
- `event_sequence`, `duration`, `retry_on_failure`, `success_probability` default to null
219+
- `work_hours_parsed` is auto-populated from the `work_hours` string if not explicitly provided

src/log_generator/generation/emitters/base.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,9 +238,12 @@ def _flush_unlocked(self) -> None:
238238
if not self.buffer:
239239
return
240240

241-
# Ensure header is written first
241+
# Ensure header is written first (or mark as done if no header)
242242
if not self._header_written:
243243
self._write_header_unlocked()
244+
# Mark header as written even if no header template exists,
245+
# so subsequent flushes use append mode instead of truncating
246+
self._header_written = True
244247

245248
# Ensure output directory exists
246249
self.output_path.parent.mkdir(parents=True, exist_ok=True)

0 commit comments

Comments
 (0)