Skip to content

Commit 871cc92

Browse files
DavidJBiancoclaude
andcommitted
Fix eval issues from Phase 5 generator changes
- Fix eCAR REGISTRY NDJSON bug: backslash-escape registry_key and registry_value in Jinja2 template (unescaped Windows paths broke json.loads, causing ~2,571 multi-line records) - Add missing fields to Windows 4625 variant (TransmittedServices, LmPackageName, KeyLength, ProcessId, ProcessName) - Fix flaky baseline logon test to accept 4624 or 4625 - Update TODO.md Phase 5.1/5.2/5.5 checkboxes Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent ad50e7c commit 871cc92

6 files changed

Lines changed: 157 additions & 29 deletions

File tree

TODO.md

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -527,65 +527,65 @@
527527

528528
**Goal:** Fix the hard acceptance failure (parsability) and the most obvious per-record tells. Smallest changes, biggest eval score impact.
529529

530-
- [ ] **Fix empty SID fields in Windows events**
530+
- [x] **Fix empty SID fields in Windows events**
531531
- Generate realistic SIDs: `S-1-5-21-{domain_sub_authorities}-{user_rid}`
532532
- Assign per-domain base SID at engine init (e.g., `S-1-5-21-3623811015-3361044348-30300820`)
533533
- Map each user to a unique RID (starting at 1001, incrementing)
534534
- Well-known SIDs for system accounts: `S-1-5-18` (SYSTEM), `S-1-5-19` (LOCAL SERVICE), `S-1-5-20` (NETWORK SERVICE)
535535
- Populate `SubjectUserSid` and `TargetUserSid` in all Windows event data dicts in `activity.py`
536536
- Files: `activity.py`, `engine.py` (SID registry initialization)
537-
- [ ] **Add logoff generation to baseline activity**
537+
- [x] **Add logoff generation to baseline activity**
538538
- Track active sessions per user; probabilistically end sessions (e.g., 30% chance per hour after first hour)
539539
- Emit Windows 4634 (logoff) and eCAR USER_SESSION/LOGOUT
540540
- Ensure logon→activity→logoff ordering within each session
541541
- Files: `activity.py` (`execute_baseline_activity`), `engine.py` (`_generate_baseline`)
542-
- [ ] **Vary Zeek conn_state and history strings**
542+
- [x] **Vary Zeek conn_state and history strings**
543543
- Replace hardcoded `SF`/`ShADadfF` with probabilistic selection
544544
- Connection states: SF (85%), S0 (5%), S1 (3%), REJ (2%), RSTO (3%), RSTR (1%), OTH (1%)
545545
- Generate history strings that match conn_state (e.g., S0→`S`, REJ→`Sr`, RSTO→`ShADaR`)
546546
- Adjust orig_bytes/resp_bytes to be consistent (e.g., S0 = 0 resp_bytes)
547547
- Files: `activity.py` (`generate_connection`)
548-
- [ ] **Expand process template pools**
548+
- [x] **Expand process template pools**
549549
- Windows: add system backbone (svchost.exe, lsass.exe, explorer.exe, services.exe, csrss.exe, RuntimeBroker.exe, SearchIndexer.exe) + user apps (chrome.exe, firefox.exe, outlook.exe, teams.exe, OneDrive.exe)
550550
- Linux: add system processes (systemd, cron, sshd, rsyslogd, NetworkManager) + user apps (firefox, thunderbird, git, docker, python3)
551551
- Per-persona weighting: developers see more compilers/editors, executives see more Office/browser
552552
- Files: `activity.py` (PROCESS_TEMPLATES, PROCESS_TEMPLATES_LINUX)
553-
- [ ] Test: Parsability score ≥ 98% (SIDs valid format)
554-
- [ ] Test: Logoff events present in output, paired with logons
555-
- [ ] Test: conn_state distribution is varied (not 100% SF)
556-
- [ ] Test: Process path count > 30 unique paths
553+
- [x] Test: Parsability score ≥ 98% (SIDs valid format)
554+
- [x] Test: Logoff events present in output, paired with logons
555+
- [x] Test: conn_state distribution is varied (not 100% SF)
556+
- [x] Test: Process path count > 30 unique paths
557557

558558
### 5.2 Event Type Diversity
559559

560560
**Goal:** Expand the vocabulary of events generated. Address "only 2 Event IDs" and "only 2 eCAR object types."
561561

562-
- [ ] **Add Windows Event IDs to format definition and emitters**
562+
- [x] **Add Windows Event IDs to format definition and emitters**
563563
- 4625: Failed logon (account does not exist, bad password, account locked)
564564
- 4672: Special privileges assigned to new logon (admin logons)
565565
- 4689: Process termination (pair with 4688)
566566
- 4648: Explicit credential logon (RunAs, scheduled tasks)
567567
- 5156: Windows Filtering Platform connection allowed (host firewall)
568568
- Update `windows_event_security.yaml` schema, Jinja2 templates, and validation rules
569569
- Files: `formats/definitions/windows_event_security.yaml`, `generation/emitters/windows.py`
570-
- [ ] **Generate failed logons in baseline**
570+
- [x] **Generate failed logons in baseline**
571571
- 5-15% of logon attempts fail (configurable via persona risk_profile)
572572
- Failure reasons: bad password (most common), account locked, expired password
573573
- Emit Windows 4625 + eCAR USER_SESSION/LOGON_FAILURE
574574
- Files: `activity.py` (`generate_logon` or new `generate_failed_logon`)
575-
- [ ] **Add eCAR object type diversity**
575+
- [x] **Add eCAR object type diversity**
576576
- FILE/CREATE, FILE/MODIFY, FILE/DELETE — generated alongside process activity
577577
- REGISTRY/MODIFY — Windows system processes and app installs
578578
- FLOW/CONNECT — parallel to Zeek connections for eCAR-equipped hosts
579579
- MODULE/LOAD — DLL loads for Windows processes
580580
- Files: `activity.py` (new methods), `generation/emitters/ecar.py`
581-
- [ ] **Add process termination events**
581+
- [x] **Add process termination events**
582582
- Pair 4689 with 4688: terminate processes after realistic duration (seconds to hours)
583583
- Track running processes in StateManager, probabilistically terminate
584584
- Files: `activity.py`, `state_manager.py`
585-
- [ ] Test: ≥ 6 unique Windows Event IDs in output
586-
- [ ] Test: ≥ 5 unique eCAR object types in output
587-
- [ ] Test: Failed logon rate between 5-15% of total logon events
588-
- [ ] Test: Process termination events present, paired with creation
585+
- [x] Test: ≥ 6 unique Windows Event IDs in output
586+
- [x] Test: ≥ 5 unique eCAR object types in output
587+
- [x] Test: Failed logon rate between 5-15% of total logon events
588+
- [x] Test: Process termination events present, paired with creation
589589

590590
### 5.3 Protocol & Network Diversity
591591

@@ -654,34 +654,34 @@
654654

655655
**Goal:** Replace uniform event distribution with realistic human timing patterns. Address "metronomic spacing" and "hard rectangular work hours."
656656

657-
- [ ] **Soft ramp-up/ramp-down for work hours**
657+
- [x] **Soft ramp-up/ramp-down for work hours**
658658
- Replace binary on/off with sigmoid curve: 10% activity at work_start-1h, ramp to 100% by work_start+1h
659659
- Soft lunch dip (50% reduction, not 0%)
660660
- Evening tail: 20% activity for 1-2 hours after work_end
661661
- Occasional late-night activity (1-3% probability per user per night)
662662
- Files: `engine.py` (`_calculate_events_for_hour`)
663-
- [ ] **Activity cluster model**
663+
- [x] **Activity cluster model**
664664
- Replace `_distribute_events_in_hour()` uniform distribution with cluster generation
665665
- Each "activity" becomes a burst of 3-15 correlated events over 5-30 seconds
666666
- Cluster types per persona: developer (editor→compile→test→git), executive (email→calendar→browser), analyst (query→export→review)
667667
- Inter-cluster gaps: 2-15 minutes (exponential distribution)
668668
- Files: `engine.py` (`_distribute_events_in_hour``_generate_activity_clusters`), `activity.py` (cluster templates)
669-
- [ ] **Per-user work hour jitter**
669+
- [x] **Per-user work hour jitter**
670670
- Randomize each user's actual start/end/lunch times ±30min from persona defaults
671671
- Applied once at engine init, consistent throughout scenario
672672
- Early arrivals, late starters, short/long lunches
673673
- Files: `engine.py` (init), `engine.py` (`_calculate_events_for_hour`)
674-
- [ ] **Per-persona behavioral differentiation**
674+
- [x] **Per-persona behavioral differentiation**
675675
- Developers: longer clusters (sustained coding sessions), more process events, fewer web connections
676676
- Executives: short frequent clusters (meetings → quick email checks), more web/email, fewer processes
677677
- Analysts: medium clusters with heavy DB/query activity
678678
- Each persona type gets distinct cluster templates and inter-cluster timing
679679
- Files: `activity.py` (persona-specific cluster definitions)
680-
- [ ] Test: Events cluster with sub-second intra-cluster timing
681-
- [ ] Test: Inter-cluster gaps follow non-uniform distribution
682-
- [ ] Test: Work hour profile shows gradual ramp (not step function)
683-
- [ ] Test: Per-user timing varies (different arrival times)
684-
- [ ] Test: Human burstiness CV > 1.0 (eval dimension)
680+
- [x] Test: Events cluster with sub-second intra-cluster timing
681+
- [x] Test: Inter-cluster gaps follow non-uniform distribution
682+
- [x] Test: Work hour profile shows gradual ramp (not step function)
683+
- [x] Test: Per-user timing varies (different arrival times)
684+
- [x] Test: Human burstiness CV > 1.0 (eval dimension)
685685

686686
**Phase 5 Milestone:** Generated data passes qualitative review — no instant tells. Eval score ≥ 85, all hard acceptance criteria pass. Background noise has protocol diversity (TCP+UDP+ICMP), event type depth (≥ 6 Windows Event IDs, ≥ 5 eCAR objects), realistic timing patterns, and hundreds of unique destination IPs.
687687

src/evidenceforge/formats/definitions/ecar.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -196,11 +196,11 @@ output:
196196
{%- endif %}
197197
{%- if registry_key %}
198198
{%- if command_line or image_path or file_path or src_ip or src_port or dst_ip or dst_port or protocol or md5 or sha256 %},{% endif %}
199-
"registry_key": "{{ registry_key }}"
199+
"registry_key": "{{ registry_key | replace('\\', '\\\\') | replace('"', '\\"') }}"
200200
{%- endif %}
201201
{%- if registry_value %}
202202
{%- if command_line or image_path or file_path or src_ip or src_port or dst_ip or dst_port or protocol or md5 or sha256 or registry_key %},{% endif %}
203-
"registry_value": "{{ registry_value }}"
203+
"registry_value": "{{ registry_value | replace('\\', '\\\\') | replace('"', '\\"') }}"
204204
{%- endif %}
205205
}
206206
}

src/evidenceforge/formats/definitions/windows_event_security.yaml

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,31 @@ variants:
401401
min_value: 0
402402
max_value: 65535
403403

404+
- name: TransmittedServices
405+
type: string
406+
required: false
407+
description: "List of transmitted services"
408+
409+
- name: LmPackageName
410+
type: string
411+
required: false
412+
description: "LAN Manager package name"
413+
414+
- name: KeyLength
415+
type: integer
416+
required: false
417+
description: "Length of NTLM session security key"
418+
419+
- name: ProcessId
420+
type: hex_string
421+
required: false
422+
description: "Process ID of the logon process"
423+
424+
- name: ProcessName
425+
type: string
426+
required: false
427+
description: "Full path of the logon process executable"
428+
404429
# EventID 4672: Special privileges assigned to new logon
405430
- name: special_privileges
406431
event_id: "4672"

tests/unit/test_activity.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -350,10 +350,10 @@ def test_execute_baseline_activity_logon(self, activity_gen, test_user, test_sys
350350

351351
activity_gen.execute_baseline_activity(test_user, test_system, timestamp, 'logon')
352352

353-
# Verify Windows emitter received 4624 event
353+
# Verify Windows emitter received logon event (4624 success or 4625 failed)
354354
assert mock_emitters['windows_event_security'].emit_event.called
355355
event_data = mock_emitters['windows_event_security'].emit_event.call_args[0][0]
356-
assert event_data['EventID'] == 4624
356+
assert event_data['EventID'] in (4624, 4625)
357357

358358
def test_execute_baseline_activity_process_creates_session(self, activity_gen, test_user, test_system, state_manager, mock_emitters):
359359
"""execute_baseline_activity should create session before process if needed."""

tests/unit/test_phase5_ecar_diversity.py

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,15 @@
11
"""Unit tests for Phase 5.2.4: eCAR object type diversity."""
22

3+
import json
34
import pytest
45
from datetime import datetime, timezone
6+
from pathlib import Path
57
from unittest.mock import Mock
68

79
from evidenceforge.generation.activity import ActivityGenerator
10+
from evidenceforge.generation.emitters.ecar import EcarEmitter
811
from evidenceforge.generation.state_manager import StateManager
12+
from evidenceforge.formats.loader import load_format
913
from evidenceforge.models import User, System
1014

1115

@@ -131,6 +135,62 @@ def test_multiple_object_types_from_processes(self, activity_gen, test_user, win
131135
assert len(object_types) >= 3, f"Only {len(object_types)} object types: {object_types}"
132136

133137

138+
class TestEcarRegistryBackslashEscaping:
139+
"""Test that REGISTRY events with Windows paths produce valid NDJSON."""
140+
141+
def test_registry_key_with_backslashes_produces_valid_json(self, tmp_path):
142+
"""Registry keys with backslashes must be properly escaped in JSON output."""
143+
fmt_def = load_format("ecar")
144+
output_file = tmp_path / "ecar.json"
145+
emitter = EcarEmitter(fmt_def, output_file, threaded=False)
146+
147+
event_data = {
148+
'timestamp': datetime(2024, 3, 15, 10, 0, 0, tzinfo=timezone.utc),
149+
'hostname': 'WKS-01',
150+
'object': 'REGISTRY',
151+
'action': 'MODIFY',
152+
'pid': 1234,
153+
'principal': 'alice.smith',
154+
'registry_key': r'HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings',
155+
'registry_value': 'ProxyEnable',
156+
}
157+
emitter.emit_event(event_data)
158+
emitter.flush()
159+
160+
content = output_file.read_text()
161+
lines = [l for l in content.strip().splitlines() if l.strip()]
162+
assert len(lines) == 1, f"Expected single-line NDJSON, got {len(lines)} lines"
163+
164+
parsed = json.loads(lines[0])
165+
assert parsed['object'] == 'REGISTRY'
166+
assert parsed['action'] == 'MODIFY'
167+
assert parsed['properties']['registry_key'] == r'HKCU\Software\Microsoft\Windows\CurrentVersion\Internet Settings'
168+
assert parsed['properties']['registry_value'] == 'ProxyEnable'
169+
170+
def test_registry_value_with_quotes_escaped(self, tmp_path):
171+
"""Registry values containing quotes must be JSON-escaped."""
172+
fmt_def = load_format("ecar")
173+
output_file = tmp_path / "ecar.json"
174+
emitter = EcarEmitter(fmt_def, output_file, threaded=False)
175+
176+
event_data = {
177+
'timestamp': datetime(2024, 3, 15, 10, 0, 0, tzinfo=timezone.utc),
178+
'hostname': 'WKS-01',
179+
'object': 'REGISTRY',
180+
'action': 'MODIFY',
181+
'pid': 1234,
182+
'principal': 'alice.smith',
183+
'registry_key': r'HKCU\Software\Test',
184+
'registry_value': 'Value with "quotes"',
185+
}
186+
emitter.emit_event(event_data)
187+
emitter.flush()
188+
189+
content = output_file.read_text()
190+
parsed = json.loads(content.strip())
191+
assert parsed['properties']['registry_value'] == 'Value with "quotes"'
192+
193+
134194
class TestEcarFlowFromConnection:
135195
"""Test that connections emit FLOW/CONNECT eCAR events."""
136196

tests/unit/test_phase5_failed_logon.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
from datetime import datetime, timezone
55
from unittest.mock import Mock
66

7+
from evidenceforge.formats.loader import load_format
8+
from evidenceforge.formats.validator import validate_event
79
from evidenceforge.generation.activity import ActivityGenerator
810
from evidenceforge.generation.state_manager import StateManager
911
from evidenceforge.models import User, System
@@ -112,6 +114,47 @@ def test_emits_ecar_with_failure_reason(self, activity_gen, test_user, win_syste
112114
assert event_data['failure_reason'] == 'bad_password'
113115

114116

117+
class TestFailedLogonFormatValidation:
118+
"""Test that 4625 events pass format validation with all fields."""
119+
120+
def test_4625_with_all_fields_validates(self):
121+
"""A 4625 event with TransmittedServices, LmPackageName, KeyLength, ProcessId, ProcessName should validate."""
122+
fmt_def = load_format("windows_event_security")
123+
event = {
124+
'EventID': 4625,
125+
'TimeCreated': '2024-03-15T10:00:00Z',
126+
'Computer': 'WKS-01.corp.local',
127+
'Channel': 'Security',
128+
'Level': 0,
129+
'EventRecordID': 1001,
130+
'ExecutionProcessID': 4,
131+
'ExecutionThreadID': 64,
132+
'SubjectUserSid': 'S-1-5-18',
133+
'SubjectUserName': '-',
134+
'SubjectDomainName': '-',
135+
'SubjectLogonId': '0x0',
136+
'TargetUserSid': 'S-1-0-0',
137+
'TargetUserName': 'alice.smith',
138+
'TargetDomainName': 'CORP',
139+
'Status': '0xc000006d',
140+
'SubStatus': '0xc0000064',
141+
'FailureReason': '%%2313',
142+
'LogonType': 3,
143+
'LogonProcessName': 'NtLmSsp',
144+
'AuthenticationPackageName': 'NTLM',
145+
'WorkstationName': 'WKS-02',
146+
'IpAddress': '10.0.10.2',
147+
'IpPort': 49152,
148+
'TransmittedServices': '-',
149+
'LmPackageName': '-',
150+
'KeyLength': 0,
151+
'ProcessId': '0x0',
152+
'ProcessName': '-',
153+
}
154+
result = validate_event(fmt_def, event, variant_name='failed_logon')
155+
assert result.valid, f"Validation errors: {result.errors}"
156+
157+
115158
class TestFailedLogonRate:
116159
"""Test that baseline activity includes ~10% failed logons."""
117160

0 commit comments

Comments
 (0)