Skip to content

Commit 322369c

Browse files
committed
fix: harden cross-source log realism
1 parent 0eba2af commit 322369c

37 files changed

Lines changed: 1398 additions & 108 deletions
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
# Iteration-Test Assessment Loops 60–69
2+
3+
## Scope
4+
5+
Completed ten counted `eforge-assess` loops (60 through 69) against
6+
`scenarios/iteration-test/scenario.yaml`. Each loop used deterministic scenario/config validation,
7+
the default non-slow regression suite before generation, forced generation into a new immutable
8+
loop directory, quantitative evaluation, targeted hard probes, four fresh isolated blind experts,
9+
and a family-level fix selected from the blind findings. Loop 68 and loop 69 exceeded the mandatory
10+
disagreement/spread threshold and received fresh report-only deliberations.
11+
12+
Blind agents were restricted to each loop's `output/data` plus the expert briefing/persona
13+
references. They could not read the scenario, ground truth, evaluation, probes, source, parent
14+
directories, or prior reports. Dataset integrity manifests confirm the reviewed output did not
15+
change during each panel.
16+
17+
## Results
18+
19+
| Loop | Records | Eval | Threat / Detection / Network / Host | Initial avg | Deliberation |
20+
|---:|---:|---:|---|---:|---:|
21+
| 60 | 80,096 | 97.783 | 66 / 66 / 74 / 94 | 75.00 ||
22+
| 61 | 82,379 | 97.086 | 68 / 89 / 82 / 95 | 83.50 ||
23+
| 62 | 82,690 | 97.241 | 77 / 86 / 85 / 96 | 86.00 ||
24+
| 63 | 82,690 | 97.241 | 77 / 86 / 85 / 96 | 86.00 ||
25+
| 64 | 82,676 | 97.241 | 77 / 86 / 73 / 96 | 83.00 ||
26+
| 65 | 82,817 | 97.261 | 90 / 86 / 73 / 95 | 86.00 ||
27+
| 66 | 80,763 | 97.581 | 74 / 82 / 74 / 91 | 80.25 ||
28+
| 67 | 81,573 | 97.403 | 85 / 93 / 71 / 98 | 86.75 ||
29+
| 68 | 78,872 | 97.006 | 92 / 70 / 24 / 99 | 71.25 | 91.50 |
30+
| 69 | 77,567 | 97.291 | 86 / 88 / 28 / 74 | 69.00 | 82.50 |
31+
32+
Loop 69's initial five-loop rolling mean was 78.65. The falling initial mean in loops 68–69 did
33+
not indicate broad regression: network specialists increasingly judged that subsystem Real, while
34+
host specialists found sharper endpoint identity contradictions. Deliberation explicitly resolved
35+
this as scope weighting.
36+
37+
## Family-Level Changes
38+
39+
- **Loop 60:** repaired Windows background-process lifecycle ownership, including durable
40+
`taskhostw.exe` identity and dependent-aware termination.
41+
- **Loop 61:** made scenario IP ownership the canonical source for internal PTR identity and private
42+
reverse-zone authority.
43+
- **Loop 62:** made retained historical process lifetimes participate in host-local PID reservation.
44+
- **Loop 63:** added data-driven TLS SNI predicates so domain-specific IDS rules attach only to an
45+
exact eligible flow.
46+
- **Loop 64:** made the Linux sudo bundle own the visible `/usr/bin/sudo` lifecycle and PAM PID.
47+
- **Loop 65:** attached sudo to a live user shell/session, modeled its elevated child, and terminated
48+
child then sudo after PAM close.
49+
- **Loop 66:** allowed out-of-order baseline planning to deterministically bootstrap/reuse the sudo
50+
owner session.
51+
- **Loop 67:** leased one stable TTY/session shell per host/user/TTY, serialized foreground sudo
52+
commands, made resolver recovery stateful, and constrained image loads to process lifetime.
53+
- **Loop 68:** reused historical Linux sessions, unified PAM/eCAR login PID ownership, leased TTYs
54+
exclusively, restricted `wsqmcons.exe` to rare workstation-only activity, and improved multipart
55+
curl ownership metadata.
56+
- **Loop 69 post-review:** resolved Security authentication/Kerberos provider PIDs through the
57+
host's canonical LSASS identity; registered pre-window sudo sessions as carried state instead of
58+
visible boundary logins; reused the PAM-owned local-login process for shell ancestry; and made
59+
child creates a termination floor for the eCAR parent process.
60+
61+
## Final Loop Findings
62+
63+
The loop-69 hard probes verified zero duplicate eCAR event IDs, exclusive TTY ownership across 67
64+
successful sudo chains, no module-after-termination rows, and workstation-only `wsqmcons.exe`.
65+
Twenty-seven of 31 successful PAM local-login records matched the exact eCAR `/bin/login` PID,
66+
substantially improving loop 68. The blind panel then isolated the remaining two owner seams:
67+
duplicate login processes/boundary session initialization and literal PID 600 across selected
68+
Windows Security authentication families. It also confirmed one 28 ms eCAR
69+
parent-termination-before-child inversion.
70+
71+
Network evidence is the strongest subsystem. The loop-69 network reviewer found coherent
72+
sensor-local identities for 1,866 shared flows, fully contained protocol/file lifecycles, exact
73+
loss-aware proxy byte reconciliation, realistic DHCP T/2 renewals, and complete ASA connection/NAT
74+
lifecycle behavior. The initial network synthetic-confidence score was 28.
75+
76+
## Verification
77+
78+
- Scenario validation: valid with only expected advisory warnings.
79+
- Config validation: 89 configuration files valid, zero errors.
80+
- Every generated loop: automated evaluation PASS.
81+
- Loop-69 quantitative evaluation: 97.29138173504755 over 77,567 records.
82+
- Focused post-review regression tests: passed.
83+
- Ruff lint and format checks: passed.
84+
- Full post-review regression suite: 5,516 passed, 20 skipped in 359.16 seconds; no generated loop
85+
was mutated after its blind review began.
86+
87+
## Remaining Work
88+
89+
1. Regenerate to verify the post-loop-69 provider-PID and carried-session fixes against output-level
90+
probes and a new blind panel.
91+
2. Add coherent source-process visibility/drop semantics for RDP and PsExec initiating endpoints.
92+
3. Expand public scanner actor and TCP-fingerprint populations with a long one-off tail.
93+
4. Correct the isolated explicit-credential source-context leak and addressless KDC 4771 request.
94+
5. Continue reducing local-console density on Linux server roles.
95+
96+
Artifacts are under `scenarios/iteration-test/blind-test/loop-60` through `loop-69`; the aggregate
97+
report is `scenarios/iteration-test/blind-test/REPORT.md`, and the trend visualization is
98+
`scenarios/iteration-test/blind-test/assessment-effectiveness-dashboard-last-20-loops.svg`.

src/evidenceforge/config/activity/edr_pools.yaml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,13 @@
1111
# {hex} = 8-character hexadecimal token, {process_prefetch_name} = owning
1212
# process basename uppercased for Windows Prefetch artifacts.
1313

14+
# Additional application/service principals that must never receive generic interactive-user
15+
# file churn. Core operating-system accounts remain protected in code for compatibility.
16+
linux_service_users:
17+
- "dovecot"
18+
- "meridian-app"
19+
- "postfix"
20+
1421
group_policy_extension_guids:
1522
- "35378EAC-683F-11D2-A89A-00C04FBBCFA2"
1623
- "42B5FAAE-6536-11D2-AE5A-0000F87571E3"
@@ -133,7 +140,7 @@ file_side_effect_profiles:
133140

134141
- name: linux_dbus_state
135142
executables: ["dbus-daemon"]
136-
actions: ["modify"]
143+
actions: ["read"]
137144
paths_linux:
138145
- "/var/lib/dbus/machine-id"
139146
- "/run/dbus/system_bus_socket"

src/evidenceforge/config/activity/extra_syslog_messages.yaml

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -479,6 +479,7 @@ programs:
479479
- app: systemd-resolved
480480
distro: ubuntu
481481
weight: 1
482+
max_per_host_window: 4
482483
params:
483484
feature_set:
484485
- UDP
@@ -501,10 +502,8 @@ programs:
501502
- "1232"
502503
- "1472"
503504
messages:
504-
- "Using degraded feature set {degraded_feature_set} instead of UDP+EDNS0 for DNS server {dns_server} after transaction {0}."
505-
- "Grace period over, resuming full feature set UDP+EDNS0 for DNS server {dns_server} after probe {0}."
506-
- "Flushed {cache_bucket} cache scope {scope} after DNS server {dns_server} changed features."
507-
- "Transaction {0} switched to {feature_set} with advertised UDP packet size {packet_size}."
505+
- "Using degraded feature set {degraded_feature_set} instead of UDP+EDNS0 for DNS server {dns_server}."
506+
- "Grace period over, resuming full feature set (UDP+EDNS0) for DNS server {dns_server}."
508507

509508
- app: thermald
510509
system_types: [workstation]

src/evidenceforge/config/activity/ids_signatures.yaml

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -93,13 +93,13 @@ signatures:
9393
application_protocol: tls
9494
inspection: metadata
9595
semantic_claim: response_content
96-
- {sid: 2016360, rev: 1, message: "ET INFO Observed Discord Domain (discordapp.com)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out}
97-
- {sid: 2023882, rev: 2, message: "ET INFO Observed Telegram Domain (t.me)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out}
98-
- {sid: 2025712, rev: 2, message: "ET INFO External IP Lookup Domain (ipify.org)", classification: "misc-activity", priority: 2, proto: tcp, dst_port: 443, direction: out}
99-
- {sid: 2024897, rev: 3, message: "ET INFO External IP Lookup (ipinfo.io)", classification: "misc-activity", priority: 2, proto: tcp, dst_port: 443, direction: out}
96+
- {sid: 2016360, rev: 1, message: "ET INFO Observed Discord Domain (discordapp.com)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out, predicate: {phase: established, application_protocol: tls, tls_server_names: [discordapp.com, "*.discordapp.com"]}}
97+
- {sid: 2023882, rev: 2, message: "ET INFO Observed Telegram Domain (t.me)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out, predicate: {phase: established, application_protocol: tls, tls_server_names: [t.me, "*.t.me"]}}
98+
- {sid: 2025712, rev: 2, message: "ET INFO External IP Lookup Domain (ipify.org)", classification: "misc-activity", priority: 2, proto: tcp, dst_port: 443, direction: out, predicate: {phase: established, application_protocol: tls, tls_server_names: [ipify.org, "*.ipify.org"]}}
99+
- {sid: 2024897, rev: 3, message: "ET INFO External IP Lookup (ipinfo.io)", classification: "misc-activity", priority: 2, proto: tcp, dst_port: 443, direction: out, predicate: {phase: established, application_protocol: tls, tls_server_names: [ipinfo.io, "*.ipinfo.io"]}}
100100
- {sid: 2028401, rev: 1, message: "ET JA3 Hash - Possible Malware - Various RAT", classification: "potentially-bad-traffic", priority: 1, proto: tcp, dst_port: 443, direction: out}
101-
- {sid: 2024290, rev: 2, message: "ET INFO Observed AWS S3 Domain (s3.amazonaws.com)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out}
102-
- {sid: 2025991, rev: 2, message: "ET INFO Observed Cloudflare DNS Over HTTPS Domain (cloudflare-dns.com)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out}
101+
- {sid: 2024290, rev: 2, message: "ET INFO Observed AWS S3 Domain (s3.amazonaws.com)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out, predicate: {phase: established, application_protocol: tls, tls_server_names: [s3.amazonaws.com, "*.s3.amazonaws.com"]}}
102+
- {sid: 2025991, rev: 2, message: "ET INFO Observed Cloudflare DNS Over HTTPS Domain (cloudflare-dns.com)", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out, predicate: {phase: established, application_protocol: tls, tls_server_names: [cloudflare-dns.com, "*.cloudflare-dns.com"]}}
103103
- {sid: 2023672, rev: 2, message: "ET INFO Python-urllib Outbound Request", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out}
104104
- {sid: 2022476, rev: 1, message: "ET INFO LibreSSL Outbound Connection", classification: "misc-activity", priority: 3, proto: tcp, dst_port: 443, direction: out}
105105

src/evidenceforge/config/activity/system_processes.yaml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,10 @@ scheduled_tasks:
5757
- "cleanmgr.exe /autoclean /d C:"
5858
parent: svchost_local_system
5959
- image: "C:\\Windows\\System32\\wsqmcons.exe"
60-
weight: 4
60+
system_types: [workstation]
61+
weight: 1
62+
max_per_host_window: 1
63+
cooldown_hours: 24
6164
command_templates:
6265
- "wsqmcons.exe"
6366
parent: svchost_local_system

src/evidenceforge/config/schemas.py

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ class IdsSignaturePredicateSpec(BaseModel, extra="forbid", frozen=True):
6161
http_methods: list[str] = Field(default_factory=list)
6262
http_statuses: list[int] = Field(default_factory=list)
6363
requires_http_body: bool = False
64+
tls_server_names: list[str] = Field(default_factory=list)
6465
file_mime_types: list[str] = Field(default_factory=list)
6566
semantic_claim: Literal[
6667
"flow_metadata",
@@ -109,6 +110,19 @@ def normalize_file_mime_types(cls, values: list[str]) -> list[str]:
109110
raise ValueError("file_mime_types must not contain duplicates")
110111
return normalized
111112

113+
@field_validator("tls_server_names")
114+
@classmethod
115+
def normalize_tls_server_names(cls, values: list[str]) -> list[str]:
116+
"""Normalize exact or suffix-wildcard TLS server-name requirements."""
117+
normalized = [value.strip().lower().rstrip(".") for value in values]
118+
if any(
119+
not value or (value.startswith("*.") and value.count("*") > 1) for value in normalized
120+
):
121+
raise ValueError("tls_server_names must contain non-empty exact or *.suffix names")
122+
if len(normalized) != len(set(normalized)):
123+
raise ValueError("tls_server_names must not contain duplicates")
124+
return normalized
125+
112126
@model_validator(mode="after")
113127
def validate_semantic_combination(self) -> Self:
114128
"""Reject predicates that cannot be evaluated coherently."""
@@ -121,6 +135,8 @@ def validate_semantic_combination(self) -> Self:
121135
raise ValueError("HTTP-specific fields require application_protocol='http'")
122136
if self.requires_http_body and self.payload_direction not in {"orig", "either"}:
123137
raise ValueError("requires_http_body needs orig/either payload_direction")
138+
if self.tls_server_names and self.application_protocol != "tls":
139+
raise ValueError("tls_server_names requires application_protocol='tls'")
124140
if self.file_mime_types and self.semantic_claim != "file_content":
125141
raise ValueError("file_mime_types requires semantic_claim='file_content'")
126142
if self.phase == "response" and not self.requires_response:

src/evidenceforge/events/network.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@ class SignaturePredicate:
7272
http_methods: tuple[str, ...] = ()
7373
http_statuses: tuple[int, ...] = ()
7474
requires_http_body: bool = False
75+
tls_server_names: tuple[str, ...] = ()
7576
file_mime_types: tuple[str, ...] = ()
7677
semantic_claim: SemanticClaim = "flow_metadata"
7778

@@ -92,6 +93,8 @@ def __post_init__(self) -> None:
9293
raise ValueError("HTTP-specific IDS predicates require application_protocol='http'")
9394
if self.requires_http_body and self.payload_direction not in {"orig", "either"}:
9495
raise ValueError("HTTP request bodies require orig/either payload direction")
96+
if self.tls_server_names and self.application_protocol != "tls":
97+
raise ValueError("TLS server-name requirements need application_protocol='tls'")
9598
if self.file_mime_types and self.semantic_claim != "file_content":
9699
raise ValueError("IDS file MIME requirements need semantic_claim='file_content'")
97100

src/evidenceforge/generation/actions/ids_alert.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -224,6 +224,7 @@ def _predicate_from_signature(
224224
http_methods=tuple(spec.http_methods),
225225
http_statuses=tuple(spec.http_statuses),
226226
requires_http_body=spec.requires_http_body,
227+
tls_server_names=tuple(spec.tls_server_names),
227228
file_mime_types=tuple(spec.file_mime_types),
228229
semantic_claim=spec.semantic_claim,
229230
)
@@ -301,6 +302,14 @@ def ids_alert_matches_transaction(
301302
return False
302303
if predicate.requires_http_body and (http is None or int(http.request_body_len or 0) <= 0):
303304
return False
305+
if predicate.tls_server_names:
306+
server_name = str(getattr(ssl, "server_name", "") or "").lower().rstrip(".")
307+
if not any(
308+
server_name == expected
309+
or (expected.startswith("*.") and server_name.endswith(expected[1:]))
310+
for expected in predicate.tls_server_names
311+
):
312+
return False
304313
if predicate.semantic_claim == "dns_query" and dns is None:
305314
return False
306315
if predicate.semantic_claim == "dns_response" and (dns is None or not has_response):

0 commit comments

Comments
 (0)