|
1 | 1 | # EvidenceForge Implementation Plan |
2 | 2 |
|
3 | | -**Status:** Phase 4 - Data Quality Evaluation ✅ SCORING COMPLETE (5/5 dimensions). LLM spot-check deferred. |
| 3 | +**Status:** Phase 6 - Expert-Identified Realism Fixes (44 findings from blind expert panel, 6 resolved) |
4 | 4 | **Started:** 2026-03-11 |
5 | | -**Last Updated:** 2026-03-16 (Phase 4 eval framework complete: 5 dimensions, 23 sub-scores, 653+ tests) |
| 5 | +**Last Updated:** 2026-03-18 (Phase 6 added: 44 expert findings tracked as P0/P1/P2/P3) |
6 | 6 | **Target MVP Completion:** 7-10 weeks from start |
7 | 7 |
|
8 | 8 | **Recent Completions:** |
|
685 | 685 |
|
686 | 686 | --- |
687 | 687 |
|
| 688 | +## Phase 6: Expert-Identified Realism Fixes |
| 689 | + |
| 690 | +**Goal:** Address all findings from blind expert panel review (REALISM_ASSESSMENT-2.md). Four domain experts (threat hunter, detection engineer, Windows sysadmin, Linux/network admin) identified the data as synthetic within seconds. This phase fixes the specific tells, organized by severity. Cross-references are to finding numbers in the assessment. |
| 691 | + |
| 692 | +**Source:** `../DataGenTest/scenarios/insider-exfiltration/REALISM_ASSESSMENT-2.md` |
| 693 | +**Baseline:** Run 8 (overall eval score 82, 6 of 44 original findings resolved) |
| 694 | + |
| 695 | +### 6.0 Fixes Already Implemented (Run 8) |
| 696 | + |
| 697 | +- [x] **LogonType diversity** — Types 2,3,4,5,7,8,9,10,11 with weighted distribution by system type (#1 original, N1 new) |
| 698 | +- [x] **PID multiples of 4** — OS-aware allocation: Windows multiples of 4 from realistic range, Linux sequential (#2 partial) |
| 699 | +- [x] **UDP/TCP history separation** — UDP uses Dd/D/DdDd, ICMP uses OTH, protocol-aware conn_state (#3) |
| 700 | +- [x] **NXDOMAIN responses** — ~20% of DNS lookups emit NXDOMAIN for suffix search failures, WPAD probes (#4) |
| 701 | +- [x] **Syslog volume and diversity** — 12-80 events/hr per Linux server, 10 programs, kernel format, systemd PID 1 (#5) |
| 702 | +- [x] **SYSTEM domain** — NT AUTHORITY for SYSTEM account (#14 original) |
| 703 | +- [x] **explorer.exe in process tree** — winlogon → userinit → explorer.exe chain seeded (#2 partial) |
| 704 | +- [x] **Dynamic ParentProcessName** — looked up from StateManager instead of hardcoded (#2 partial) |
| 705 | + |
| 706 | +### 6.1 P0: Critical (Instant Giveaways) |
| 707 | + |
| 708 | +- [ ] **Fix DNS query type semantics** (Assessment #1) |
| 709 | + - AAAA queries must return IPv6 addresses (e.g., `2607:f8b0:4004:800::201b`), not IPv4 |
| 710 | + - PTR queries must use `in-addr.arpa` format (e.g., `40.246.107.13.in-addr.arpa` → hostname) |
| 711 | + - Remove CNAME as explicit qtype (CNAMEs are returned in answer chains, not queried directly) |
| 712 | + - Add SRV queries (qtype 33) for AD: `_ldap._tcp.dc._msdcs.corp.local`, `_kerberos._tcp.corp.local` |
| 713 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 714 | +- [ ] **Fix parent PID still always 0x4** (Assessment #2) |
| 715 | + - ParentProcessName says explorer.exe but numeric ProcessId (parent) says 0x4 — contradictory |
| 716 | + - Baseline activity must pass `parent_pid=system_pids['explorer']` to `generate_process()` |
| 717 | + - eCAR ppid field also always 4 — must use actual parent PID |
| 718 | + - Files: `activity.py` (`execute_baseline_activity`, `generate_process`), `engine.py` |
| 719 | +- [ ] **Fix duplicate fields in 4624 XML template** (Assessment #3) |
| 720 | + - TargetUserName, TargetDomainName, TargetLogonId, LogonType, LogonGuid appear twice in EventData |
| 721 | + - Template concatenation bug in `windows_event_security.yaml` |
| 722 | + - Files: `formats/definitions/windows_event_security.yaml` |
| 723 | +- [ ] **Add Kerberos/LDAP/SQL traffic to Zeek** (Assessment #4) |
| 724 | + - Zero connections to port 88 (Kerberos), 389 (LDAP), 1433 (SQL Server) |
| 725 | + - Database traffic incorrectly goes to MySQL port 3306 instead of scenario-documented SQL Servers |
| 726 | + - Generate Kerberos/LDAP as system traffic from domain-joined machines to DCs |
| 727 | + - Files: `engine.py` (`_generate_system_traffic`), `activity.py` |
| 728 | +- [ ] **Correlate Zeek DNS and conn UIDs** (Assessment #5) |
| 729 | + - DNS conn.log UID does not appear in dns.log and vice versa |
| 730 | + - `_emit_dns_lookup` must share the same UID between conn.log and dns.log emissions |
| 731 | + - Files: `activity.py` (`_emit_dns_lookup`, `generate_connection`) |
| 732 | + |
| 733 | +### 6.2 P1: Major (Would Fool Casual Observers, Not Experts) |
| 734 | + |
| 735 | +- [ ] **Add missing Windows Event IDs** (Assessment #6, #8) |
| 736 | + - 4768/4769 (Kerberos TGT/service tickets), 4776 (NTLM validation) |
| 737 | + - 5156 (WFP connection allowed), 4648 (explicit creds) |
| 738 | + - 5140/5145 (share access), 4720-4740 (account management) |
| 739 | + - 4103/4104 (PowerShell logging) |
| 740 | + - DCs should have massive Kerberos event volume |
| 741 | + - Files: `windows_event_security.yaml`, `activity.py`, `engine.py` |
| 742 | +- [ ] **Add machine account ($) activity** (Assessment #7) |
| 743 | + - Zero COMPUTERNAME$ events; real AD has constant machine account auth (GPO, Kerberos, LDAP) |
| 744 | + - 118 domain-joined systems should generate significant machine account volume |
| 745 | + - Files: `engine.py` (`_generate_system_traffic`), `activity.py` |
| 746 | +- [ ] **Set `local_resp: true` for internal servers** (Assessment #9) |
| 747 | + - All 122K Zeek records show `local_resp: false`, even for connections to internal 10.10.100.x servers |
| 748 | + - Should be `true` when responder IP is in `Site::local_nets` |
| 749 | + - Files: `activity.py` (`generate_connection`) |
| 750 | +- [ ] **Fix Zeek packet/byte IP+TCP overhead** (Assessment #10) |
| 751 | + - Consistent 40-byte overhead per packet; real TCP needs 52-72 bytes (IP + TCP + options) |
| 752 | + - Files: `activity.py` (packet count calculation) |
| 753 | +- [ ] **Per-computer EventRecordIDs** (Assessment #11) |
| 754 | + - Global counter 10001→185448 across all computers; real Windows logs have per-machine sequences |
| 755 | + - Files: `activity.py` (`_get_next_event_record_id`), `engine.py` |
| 756 | +- [ ] **Fix LogonType distribution** (Assessment #12, N1) |
| 757 | + - Type 7 (unlock) too high at 43K; Type 3 (network) should dominate (60-80%) |
| 758 | + - Type 5 (service) incorrectly assigned to regular user accounts — restrict to service accounts |
| 759 | + - Recalibrate weights: servers need Type 3 at 60-80%, workstations need Type 3 higher than current |
| 760 | + - Files: `activity.py` (`execute_baseline_activity`) |
| 761 | +- [ ] **Massively increase DC event volume** (Assessment #13, N2) |
| 762 | + - Only 147 events from both DCs over 72 hours; real DCs generate tens of thousands per hour |
| 763 | + - DCs contribute <0.1% of total events — should be among noisiest machines |
| 764 | + - Files: `engine.py` (`_generate_system_traffic` or new DC-specific generator) |
| 765 | +- [ ] **Break mechanical traffic pattern** (Assessment #14, N3) |
| 766 | + - Every workstation generates rigid DNS→web→DNS→SMTP 4-tuple pattern |
| 767 | + - Real traffic is bursty with persistent connections, keep-alives, concurrent connections |
| 768 | + - Files: `activity.py` (`execute_baseline_activity`), `engine.py` |
| 769 | +- [ ] **Route SMTP through internal Exchange** (Assessment #15) |
| 770 | + - Workstations connect directly to Gmail/O365 on port 587; should route through SRV-EXCH-01 |
| 771 | + - Files: `activity.py` (connection templates), `engine.py` |
| 772 | +- [ ] **Route DNS through documented DCs** (Assessment #16) |
| 773 | + - DNS queries go to 10.0.0.1 (undocumented); should use DC-01/DC-02 from scenario |
| 774 | + - Files: `engine.py` (`_detect_infrastructure_ips`), `activity.py` (`_dns_server_ip`) |
| 775 | + |
| 776 | +### 6.3 P2: Moderate (Polish & Realism) |
| 777 | + |
| 778 | +- [ ] **Add jitter to storyline timestamps** (Assessment #17) |
| 779 | + - Attack timestamps are exact multiples of 900s/3600s with .000 microseconds |
| 780 | + - Background events have realistic jitter; the contrast is obvious |
| 781 | + - Files: `engine.py` (`_execute_storyline_event`) |
| 782 | +- [ ] **Realistic LogonIDs** (Assessment #18) |
| 783 | + - TargetLogonId values 0x3e7, 0x3e8, 0x3e9... incrementing by 1 |
| 784 | + - Real LSASS generates high-entropy 64-bit values like 0x1A2B3C4D |
| 785 | + - 0x3e7 is SYSTEM's well-known LogonID but assigned to regular users |
| 786 | + - Files: `state_manager.py` (logon ID generation) |
| 787 | +- [ ] **Populate real base64 in encoded commands** (Assessment #19) |
| 788 | + - Literal `<base64_encoded_command>` placeholder never replaced with actual base64 |
| 789 | + - Files: `engine.py` (storyline execution) |
| 790 | +- [ ] **Use realistic public IPs for exfiltration** (Assessment #20) |
| 791 | + - RFC 5737 documentation IPs (203.0.113.x, 198.51.100.x) are not internet-routable |
| 792 | + - Files: scenario YAML, `engine.py` |
| 793 | +- [ ] **Add Kerberos auth package + LogonGuids** (Assessment #21) |
| 794 | + - LogonGuid always null; Kerberos-enabled domains populate this for cross-machine correlation |
| 795 | + - AuthenticationPackageName never "Kerberos" — should dominate in AD |
| 796 | + - Files: `activity.py` (`generate_logon`) |
| 797 | +- [ ] **Fix internal DNS IPs and DB ports** (Assessment #22) |
| 798 | + - db-primary.corp.local resolves to 10.0.100.x but docs say 10.10.100.x; MySQL 3306 instead of SQL 1433 |
| 799 | + - Files: `activity.py` (REVERSE_DNS, EXTERNAL_IPS), `engine.py` |
| 800 | +- [ ] **Fix Zeek `ts` type consistency** (Assessment #23) |
| 801 | + - String in zeek_conn.json, bare number in zeek_dns.json; should be consistent |
| 802 | + - Files: `emitters/zeek.py`, `emitters/zeek_dns.py` |
| 803 | +- [ ] **Interleave scenario events chronologically** (Assessment #24) |
| 804 | + - Attack events appended as separate block at end of ecar.json/zeek_conn.json |
| 805 | + - Files: `emitters/base.py` or `engine.py` (output ordering) |
| 806 | +- [ ] **Add FQDN to Computer names** (Assessment #25) |
| 807 | + - Short names like EXEC-WS-04 instead of EXEC-WS-04.corp.meridiancapital.com |
| 808 | + - Files: `activity.py` (Computer field in Windows events) |
| 809 | +- [ ] **Add RID gaps and computer account SIDs** (Assessment #26) |
| 810 | + - RIDs 1001-1105 with zero gaps; no computer account RIDs or deleted-object gaps |
| 811 | + - Files: `engine.py` (SID registry) |
| 812 | +- [ ] **Use SSH source IPs from documented subnets** (Assessment #27, N4) |
| 813 | + - SSH from 10.0.x.x instead of documented 10.10.x.x topology |
| 814 | + - Files: `engine.py` (syslog generation), `activity.py` |
| 815 | +- [ ] **Sort syslog chronologically** (Assessment #28) |
| 816 | + - Within each hour, timestamps jump randomly; real syslog is append-only/ordered |
| 817 | + - Files: `engine.py` (`_generate_system_traffic` syslog section), `emitters/syslog.py` |
| 818 | +- [ ] **Limit systemd-timesyncd message** (Assessment #29, N5) |
| 819 | + - "Synchronized for the first time" repeats dozens of times; should appear once per boot |
| 820 | + - Files: `engine.py` (syslog templates) |
| 821 | +- [ ] **Diversify Zeek history strings** (Assessment #30) |
| 822 | + - Only ~9 patterns; real Zeek has dozens including retransmission markers (T/t), varied FIN ordering |
| 823 | + - Files: `activity.py` (TCP_CONN_STATE_DISTRIBUTION) |
| 824 | +- [ ] **Correlate eCAR/Zeek source ports** (Assessment #31) |
| 825 | + - eCAR FLOW and Zeek conn at same timestamp show different source ports |
| 826 | + - Files: `activity.py` (`generate_connection`, `_emit_ecar_flow_event`) |
| 827 | +- [ ] **Add SERVFAIL responses** (Assessment #32) |
| 828 | + - Zero SERVFAIL in 39K DNS records; real environments have ~0.1-0.5% |
| 829 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 830 | + |
| 831 | +### 6.4 P3: Minor (Nice-to-Have Improvements) |
| 832 | + |
| 833 | +- [ ] **Fix eCAR DNS FLOW protocol to UDP** (Assessment #33) |
| 834 | + - eCAR shows `protocol: "tcp"` for DNS while Zeek correctly shows UDP |
| 835 | + - Files: `activity.py` (`_emit_ecar_flow_event`) |
| 836 | +- [ ] **Add process attribution to eCAR FLOWs** (Assessment #34) |
| 837 | + - `pid: -1` on all FLOW events; real EDR tracks socket-to-process mapping |
| 838 | + - Files: `activity.py` (`_emit_ecar_flow_event`) |
| 839 | +- [ ] **Reduce eCAR LOGIN frequency** (Assessment #35) |
| 840 | + - LOGIN events paired with every activity burst; users generate new LOGIN every 1-2 seconds |
| 841 | + - Files: `activity.py` (`execute_baseline_activity`) |
| 842 | +- [ ] **Vary filenames in eCAR file operations** (Assessment #36) |
| 843 | + - Cycles through 5 generic filenames: spreadsheet.xlsx, presentation.pptx, notes.txt, etc. |
| 844 | + - Files: `activity.py` (_ECAR_FILE_PATHS_WIN, _ECAR_FILE_PATHS_LINUX) |
| 845 | +- [ ] **Decrement DNS TTLs for cached responses** (Assessment #37) |
| 846 | + - Always round values; cached responses should show decremented TTLs |
| 847 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 848 | +- [ ] **Fix 4625 event Version to 0** (Assessment #38) |
| 849 | + - Uses Version 2 instead of standard Version 0 for 4625 |
| 850 | + - Files: `formats/definitions/windows_event_security.yaml` |
| 851 | +- [ ] **Vary command-line arguments** (Assessment #39) |
| 852 | + - Same exact PowerShell/sqlcmd commands repeated dozens of times |
| 853 | + - Files: `activity.py` (PROCESS_TEMPLATES) |
| 854 | +- [ ] **Add PID interleaving for CRON** (Assessment #40, N6) |
| 855 | + - CRON PIDs too sequential (1660, 1661, 1662...); needs interleaving from other processes |
| 856 | + - Files: `engine.py` (syslog CRON generation) |
| 857 | +- [ ] **Include resp_ip_bytes: 0 on zero-packet records** (Assessment #41) |
| 858 | + - Some records missing resp_ip_bytes instead of having it set to 0 |
| 859 | + - Files: `activity.py` (`generate_connection`) |
| 860 | +- [ ] **Multiple answers for popular DNS domains** (Assessment #42) |
| 861 | + - Every query returns single answer; CDN domains return multiple A records |
| 862 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 863 | +- [ ] **Add `<Events>` root XML wrapper** (Assessment #43) |
| 864 | + - Standalone `<Event>` elements without containing root element |
| 865 | + - Files: `emitters/windows.py` |
| 866 | +- [ ] **Set AA flag for internal DNS** (Assessment #44) |
| 867 | + - Internal zone queries (corp.local) show AA: false; should be authoritative |
| 868 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 869 | + |
| 870 | +**Phase 6 Milestone:** Expert panel re-review finds no P0 instant giveaways. P1 findings reduced by 50%+. Eval score ≥ 90. |
| 871 | + |
| 872 | +--- |
| 873 | + |
688 | 874 | ## Post-MVP Enhancements (Future) |
689 | 875 |
|
690 | 876 | **Not part of MVP, but tracked here for future reference.** |
|
0 commit comments