|
1 | 1 | # EvidenceForge Implementation Plan |
2 | 2 |
|
3 | | -**Status:** Phase 6 - Expert-Identified Realism Fixes (44 findings from blind expert panel, 11 resolved) |
| 3 | +**Status:** Phase 6 - Expert-Identified Realism Fixes (44 original + 16 new from improvement loop, 46 resolved) |
4 | 4 | **Started:** 2026-03-11 |
5 | | -**Last Updated:** 2026-03-18 (Phase 6.1 P0 complete: 5 critical fixes, 702 tests passing) |
| 5 | +**Last Updated:** 2026-03-18 (Improvement loop complete: eval 75.4→90.6, 765 tests passing) |
6 | 6 | **Target MVP Completion:** 7-10 weeks from start |
7 | 7 |
|
8 | 8 | **Recent Completions:** |
|
804 | 804 |
|
805 | 805 | ### 6.4 P3: Minor (Nice-to-Have Improvements) |
806 | 806 |
|
807 | | -- [ ] **Fix eCAR DNS FLOW protocol to UDP** (Assessment #33) |
808 | | - - eCAR shows `protocol: "tcp"` for DNS while Zeek correctly shows UDP |
| 807 | +- [x] **Fix eCAR DNS FLOW protocol to UDP** (Assessment #33, Improvement Loop Iter 2) |
| 808 | + - eCAR now uses `"udp"` for DNS (port 53) and NTP (port 123) connections |
809 | 809 | - Files: `activity.py` (`_emit_ecar_flow_event`) |
810 | 810 | - [ ] **Add process attribution to eCAR FLOWs** (Assessment #34) |
811 | 811 | - `pid: -1` on all FLOW events; real EDR tracks socket-to-process mapping |
|
834 | 834 | - [ ] **Multiple answers for popular DNS domains** (Assessment #42) |
835 | 835 | - Every query returns single answer; CDN domains return multiple A records |
836 | 836 | - Files: `activity.py` (`_emit_dns_lookup`) |
837 | | -- [ ] **Add `<Events>` root XML wrapper** (Assessment #43) |
838 | | - - Standalone `<Event>` elements without containing root element |
839 | | - - Files: `emitters/windows.py` |
| 837 | +- [x] **Add `<Events>` root XML wrapper** (Assessment #43, Improvement Loop Iter 2) |
| 838 | + - XML declaration + `<Events>` root + `</Events>` footer via header/footer templates |
| 839 | + - Files: `emitters/base.py`, `formats/format_def.py`, `formats/definitions/windows_event_security.yaml` |
840 | 840 | - [ ] **Set AA flag for internal DNS** (Assessment #44) |
841 | 841 | - Internal zone queries (corp.local) show AA: false; should be authoritative |
842 | 842 | - Files: `activity.py` (`_emit_dns_lookup`) |
843 | 843 |
|
| 844 | +### 6.5 Improvement Loop Findings (2026-03-18) |
| 845 | + |
| 846 | +**Source:** Automated improvement loop with 4-expert blind panel on `insider-exfiltration` scenario. |
| 847 | +**Baseline:** Eval 75.4/100 → **Final: 90.6/100** across 5 iterations. |
| 848 | + |
| 849 | +#### Resolved in improvement loop: |
| 850 | +- [x] **Zeek UID source port mismatch** (P0, Iter 2) |
| 851 | + - conn.log and dns.log had different `id.orig_p` for same UID |
| 852 | + - Fix: pass `src_port` through `generate_connection()` to dns.log emission |
| 853 | + - Files: `activity.py` (`generate_connection`, `_emit_dns_lookup`) |
| 854 | +- [x] **TCP orig_pkts inconsistent with history** (P0, Iter 2) |
| 855 | + - `orig_pkts: 1` with `history: "ShADadfF"` (implies 4+ orig packets) |
| 856 | + - Fix: derive TCP packet counts from history field characters |
| 857 | + - Files: `activity.py` (`generate_connection`) |
| 858 | +- [x] **External CDN hostnames resolve to internal IPs** (P0, Iter 3) |
| 859 | + - `cdn-3986.cloudfront.net` → `10.10.100.16` (impossible in reality) |
| 860 | + - Fix: internal IPs get internal hostnames (`srv-01.corp.local` pattern) |
| 861 | + - Files: `activity.py` (`_emit_dns_lookup`, `_generate_internal_hostname`) |
| 862 | +- [x] **Fabricated CDN/cloud hostnames** (P0, Iter 3) |
| 863 | + - `server-68-352.compute.amazonaws.com`, `cdn-NNNN.cloudfront.net` — wrong format |
| 864 | + - Fix: realistic formats (ec2-x-x-x-x, d{hash}.cloudfront.net, e{id}.dscb.akamaiedge.net) |
| 865 | + - Files: `activity.py` (`_generate_random_hostname`) |
| 866 | +- [x] **Reserved domains in DNS** (P1, Iter 3) |
| 867 | + - `api.example.com`, `blog.example.com`, `cdn.example.com`, `www.example.com` |
| 868 | + - Fix: replaced with real service domains in REVERSE_DNS map |
| 869 | + - Files: `activity.py` (REVERSE_DNS) |
| 870 | +- [x] **Same IPv6 for all AAAA queries** (P1, Iter 3) |
| 871 | + - 87% of AAAA responses used `2607:f8b0:` (Google prefix) regardless of provider |
| 872 | + - Fix: diverse prefixes per provider (Azure, Akamai, AWS, Cloudflare, Fastly) |
| 873 | + - Files: `activity.py` (`_ipv4_to_fake_ipv6`) |
| 874 | +- [x] **4625 SID contradiction with SubStatus** (P0, Iter 4) |
| 875 | + - SubStatus `0xc0000064` (user not found) paired with valid domain SID |
| 876 | + - Fix: NULL SID `S-1-0-0` for unknown-user failures; varied SubStatus types |
| 877 | + - Files: `activity.py` (`generate_failed_logon`) |
| 878 | +- [x] **4768:4769 TGT/TGS 1:1 ratio** (P0, Iter 4) |
| 879 | + - Exact same count (90,344 each) — statistically impossible |
| 880 | + - Fix: 2-5 TGS per TGT; TGS targets member servers (60%) not just DCs |
| 881 | + - Files: `engine.py` (`_generate_system_traffic`) |
| 882 | +- [x] **Zero ICMP traffic** (P0, Iter 5) |
| 883 | + - 250K conn.log entries with no ICMP at all |
| 884 | + - Fix: servers ping each other 0-1 times per hour |
| 885 | + - Files: `engine.py` (`_generate_system_traffic`) |
| 886 | +- [x] **NXDOMAIN `NonExistentSite` literal** (P2, Iter 2) |
| 887 | + - Placeholder string used as AD site name |
| 888 | + - Fix: replaced with `Default-First-Site-Name` |
| 889 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 890 | + |
| 891 | +#### Eval scoring fixes (Iter 1): |
| 892 | +- [x] **FQDN hostname normalization** — eval VisibilityModel and all dimension scorers |
| 893 | + now handle both bare and FQDN hostnames (Source Correctness: 1→100) |
| 894 | +- [x] **PreAuthType integer coercion** — Windows parser now coerces to int (Parsability: 92→99.5) |
| 895 | +- [x] **Timezone-aware work hours** — eval compares in scenario timezone, not UTC |
| 896 | +- [x] **AD ports in common ports list** — Kerberos (88), LDAP (389) no longer flagged as anomalous |
| 897 | +- [x] **Deep off-hours anomaly detection** — only midnight-5am flagged, not all off-hours |
| 898 | + |
| 899 | +#### Remaining tells (not yet addressed): |
| 900 | + |
| 901 | +**P0 — Instant Giveaways:** |
| 902 | +- [ ] **RFC 5737 documentation IP (203.0.113.50) for exfiltration target** |
| 903 | + - TEST-NET-3 range instantly recognizable; also has `local_resp: true` |
| 904 | + - Fix: use realistic cloud storage IPs; fix `_is_private_ip()` for exfil destinations |
| 905 | + - Files: `activity.py` (storyline connection handling), scenario YAML |
| 906 | +- [ ] **No DNS resolution for exfiltration/storyline targets** |
| 907 | + - Connections to 203.0.113.50 (pCloud) have no corresponding DNS queries |
| 908 | + - Fix: emit DNS lookup before each storyline network connection |
| 909 | + - Files: `engine.py` (storyline execution), `activity.py` |
| 910 | +- [ ] **Kernel uptime counters non-monotonic in syslog** |
| 911 | + - Values jump randomly (1061731 → 516344 → 460933) instead of increasing |
| 912 | + - Fix: track per-host uptime state in StateManager, increment monotonically |
| 913 | + - Files: `engine.py` (syslog generation), `state_manager.py` |
| 914 | +- [ ] **EventRecordID timestamps go backward within same computer** |
| 915 | + - Higher RecordID has earlier timestamp (events shuffled then sequentially numbered) |
| 916 | + - Fix: sort events by timestamp before assigning EventRecordIDs per computer |
| 917 | + - Files: `emitters/windows.py` or `engine.py` (event ordering) |
| 918 | +- [ ] **No SSH/RDP connections in Zeek despite syslog SSH sessions** |
| 919 | + - Syslog shows active sshd but conn.log has zero port 22/3389 traffic |
| 920 | + - Fix: generate Zeek conn.log entries for SSH connections to Linux hosts |
| 921 | + - Files: `engine.py` (`_generate_system_traffic`) |
| 922 | + |
| 923 | +**P1 — Expert-Level Tells:** |
| 924 | +- [ ] **No LogonType 5 (Service) events** |
| 925 | + - Zero events; real Windows servers constantly generate type 5 for services |
| 926 | + - Fix: emit 4624 type 5 for service accounts (svc*, $-suffix) in system traffic |
| 927 | + - Files: `engine.py` (`_generate_system_traffic`), `activity.py` |
| 928 | +- [ ] **No ANONYMOUS LOGON (S-1-5-7) events** |
| 929 | + - Real environments have frequent anonymous logons from network discovery |
| 930 | + - Fix: emit periodic 4624 type 3 with ANONYMOUS LOGON on servers/DCs |
| 931 | + - Files: `engine.py` (`_generate_system_traffic`) |
| 932 | +- [ ] **LmPackageName always "-" even for NTLM authentications** |
| 933 | + - Should be "NTLM V2" when AuthenticationPackageName is "NTLM" |
| 934 | + - Files: `activity.py` (`generate_logon`, `generate_failed_logon`) |
| 935 | +- [ ] **Zeek history "SS" is invalid** |
| 936 | + - Repeated SYN is still "S" in real Zeek; "SS" never appears |
| 937 | + - Fix: remove "SS" from `_TCP_CONN_ENTRIES` history patterns |
| 938 | + - Files: `activity.py` (`_TCP_CONN_ENTRIES`) |
| 939 | +- [ ] **Service distribution unrealistic** (too much Kerberos/LDAP, too little HTTPS) |
| 940 | + - Real enterprises have HTTPS as dominant protocol |
| 941 | + - Fix: increase baseline HTTPS connections, reduce Kerberos frequency |
| 942 | + - Files: `engine.py` (`_generate_system_traffic`) |
| 943 | +- [ ] **No 4770 TGT Renewal events** |
| 944 | + - TGTs expire every ~10 hours; 3-day window should have renewals |
| 945 | + - Fix: emit 4770 events periodically on DCs |
| 946 | + - Files: `activity.py` (new `generate_kerberos_renewal`), `engine.py` |
| 947 | +- [ ] **4672 Special Privileges ratio too low** (9% vs expected ~30%+) |
| 948 | + - Every admin/service/machine account logon should generate paired 4672 |
| 949 | + - Fix: emit 4672 for machine accounts, service accounts, admin logons |
| 950 | + - Files: `activity.py` (`generate_logon`, `generate_machine_account_logon`) |
| 951 | + |
| 952 | +**P2 — Polish Issues:** |
| 953 | +- [ ] **Events grouped by computer, not chronologically interleaved** |
| 954 | + - Windows XML has all EXEC-WS-01 events, then all EXEC-WS-02, etc. |
| 955 | + - Fix: sort events globally by timestamp before writing |
| 956 | + - Files: `emitters/windows.py` or `engine.py` |
| 957 | +- [ ] **Syslog only from 2 hosts** (DMZ-WEB-01, SRV-LOG-01) |
| 958 | + - 118-system environment should have more syslog sources |
| 959 | + - Fix: emit syslog for all Linux systems, or add forwarding from Windows |
| 960 | + - Files: `engine.py` (syslog target selection) |
| 961 | +- [ ] **No TXT DNS queries** (SPF/DKIM/DMARC checks) |
| 962 | + - Fix: add periodic TXT lookups for email-related domains |
| 963 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 964 | +- [ ] **Audit serial numbers non-monotonic in syslog** |
| 965 | + - Jump erratically instead of incrementing per boot |
| 966 | + - Fix: track per-host audit serial in StateManager |
| 967 | + - Files: `engine.py`, `state_manager.py` |
| 968 | +- [ ] **DNS-then-service paired pattern too uniform** |
| 969 | + - Every connection preceded by DNS with 10-50ms gap; needs caching, concurrency |
| 970 | + - Fix: skip DNS for cached domains, add temporal jitter |
| 971 | + - Files: `activity.py` (`_emit_dns_lookup`) |
| 972 | +- [ ] **Ground truth mislabels file servers as "C2 Servers"** |
| 973 | + - Internal SMB servers listed under "C2 Server" IOC category |
| 974 | + - Fix: use correct labels in ground truth generation |
| 975 | + - Files: `ground_truth.py` |
| 976 | + |
844 | 977 | **Phase 6 Milestone:** Expert panel re-review finds no P0 instant giveaways. P1 findings reduced by 50%+. Eval score ≥ 90. |
845 | 978 |
|
846 | 979 | --- |
|
0 commit comments