2020 "initial-access" : "初始访问" ,
2121 "identity" : "身份突破" ,
2222 "execution" : "执行" ,
23+ "persistence" : "持久化" ,
24+ "privilege-escalation" : "权限提升" ,
2325 "compromise" : "主机失陷" ,
26+ "remote-access" : "远程访问" ,
2427 "lateral-movement" : "横向移动" ,
2528 "command-control" : "命令控制" ,
2629 "exfiltration" : "数据外传" ,
3639 "初始访问" ,
3740 "身份突破" ,
3841 "执行" ,
42+ "持久化" ,
43+ "权限提升" ,
3944 "主机失陷" ,
45+ "远程访问" ,
4046 "横向移动" ,
4147 "命令控制" ,
4248 "数据外传" ,
@@ -96,6 +102,7 @@ def correlate_incidents(events: Sequence[LogEvent], alerts: Sequence[DetectionAl
96102
97103 incidents .sort (key = lambda item : (_LEVEL_ORDER [item .level ], len (item .source_types ), len (item .affected_events )), reverse = True )
98104 incidents = _drop_subset_incidents (incidents )
105+ incidents = [incident for incident in incidents if not _is_low_value_windows_standalone_incident (incident )]
99106 for idx , incident in enumerate (incidents , 1 ):
100107 incident .id = f"inc-{ idx :03d} "
101108 return incidents [:50 ]
@@ -109,8 +116,13 @@ def _correlation_keys(events: Iterable[LogEvent]) -> List[Tuple[str, str]]:
109116 candidates = [
110117 ("session" , event .details .get ("session_id" , "" )),
111118 ("trace" , event .details .get ("trace_id" , "" )),
112- ("ip" , event .details .get ("src_ip" , "" ) or event .ip or "" ),
113- ("account" , event .details .get ("account" , "" ) or event .user or "" ),
119+ ("ip" , event .details .get ("src_ip" , "" ) or event .details .get ("source_ip" , "" ) or event .ip or "" ),
120+ ("workstation" , event .details .get ("workstation" , "" )),
121+ ("account" , event .details .get ("account" , "" ) or event .details .get ("target_account" , "" ) or event .user or "" ),
122+ ("account" , event .details .get ("account_name" , "" )),
123+ ("account" , event .details .get ("member_account" , "" )),
124+ ("sid" , event .details .get ("target_sid" , "" )),
125+ ("sid" , event .details .get ("member_sid" , "" )),
114126 ("asset" , event .details .get ("asset" , "" ) or event .host or "" ),
115127 ]
116128 for kind , value in candidates :
@@ -173,14 +185,26 @@ def _is_duplicate_incident(candidate: Incident, existing: Incident) -> bool:
173185 )
174186
175187
188+ def _is_low_value_windows_standalone_incident (incident : Incident ) -> bool :
189+ return (
190+ incident .source_types == ["windows-event" ]
191+ and incident .confidence == "low"
192+ and len (incident .affected_events ) <= 1
193+ and not incident .source_ips
194+ )
195+
196+
176197def _build_incident (index : int , events : Sequence [LogEvent ], alerts : Sequence [DetectionAlert ]) -> Incident :
198+ events , alerts = _focus_windows_chain_scope (events , alerts )
177199 max_level = _max_level (
178200 [event .level for event in events ] + [alert .level for alert in alerts ],
179201 default = ThreatLevel .INFO ,
180202 )
181- source_ips = _sorted_values (event .details .get ("src_ip" ) or event .ip for event in events )
203+ source_ips = _sorted_values (event .details .get ("src_ip" ) or event .details . get ( "source_ip" ) or event . ip for event in events )
182204 accounts = _sorted_values (event .details .get ("account" ) or event .user for event in events )
183205 assets = _sorted_values (event .details .get ("asset" ) or event .host for event in events )
206+ operators = _sorted_values (event .details .get ("operator_account" ) or event .details .get ("subject_account" ) for event in events )
207+ workstations = _sorted_values (event .details .get ("source_workstation" ) or event .details .get ("workstation" ) for event in events )
184208 source_types = _sorted_values (event .details .get ("source_type" ) for event in events )
185209 families = _sorted_values (event .details .get ("event_family" ) for event in events )
186210 raw_phases = [_FAMILY_PHASE .get (family , family ) for family in families ]
@@ -190,10 +214,10 @@ def _build_incident(index: int, events: Sequence[LogEvent], alerts: Sequence[Det
190214 key = lambda phase : (_PHASE_INDEX .get (phase , len (_PHASE_INDEX )), phase ),
191215 )
192216 confidence = _confidence (events , alerts , source_types , families )
193- title = _title (source_ips , assets , source_types , phases , max_level )
194- description = _description (source_ips , accounts , assets , source_types , phases , alerts , events )
217+ title = _title (source_ips , accounts , assets , source_types , phases , max_level , alerts )
218+ description = _description (source_ips , accounts , assets , operators , workstations , source_types , phases , alerts , events )
195219 timeline = _timeline (events )
196- evidence = _evidence (events , alerts , source_types , phases )
220+ evidence = _evidence (events , alerts , source_ips , accounts , assets , operators , workstations , source_types , phases )
197221
198222 return Incident (
199223 id = f"inc-{ index :03d} " ,
@@ -223,12 +247,112 @@ def _sorted_values(values: Iterable[object]) -> List[str]:
223247 return sorted ({str (value ) for value in values if value not in (None , "" , "-" , "null" , "None" )})
224248
225249
250+ def _focus_windows_chain_scope (
251+ events : Sequence [LogEvent ],
252+ alerts : Sequence [DetectionAlert ],
253+ ) -> Tuple [List [LogEvent ], List [DetectionAlert ]]:
254+ chain_alerts = [alert for alert in alerts if alert .rule_id == "WIN-CHAIN-001" ]
255+ if not chain_alerts :
256+ return list (events ), list (alerts )
257+
258+ chain_event_ids = {event_id for alert in chain_alerts for event_id in alert .affected_events }
259+ chain_events = [event for event in events if event .id in chain_event_ids ]
260+ if not chain_events :
261+ return list (events ), list (alerts )
262+
263+ account_keys = {
264+ _account_key (value )
265+ for event in chain_events
266+ for value in (
267+ event .details .get ("account" ),
268+ event .details .get ("target_account" ),
269+ event .details .get ("account_name" ),
270+ )
271+ if _account_key (value )
272+ }
273+ sids = {
274+ str (value ).strip ().lower ()
275+ for event in chain_events
276+ for value in (event .details .get ("target_sid" ), event .details .get ("member_sid" ))
277+ if value not in (None , "" , "-" )
278+ }
279+ focused = [
280+ event for event in events
281+ if event .id in chain_event_ids
282+ or _event_matches_account (event , account_keys , sids )
283+ ]
284+ focused_ids = {event .id for event in focused }
285+ focused_alerts = [
286+ alert for alert in alerts
287+ if alert .rule_id == "WIN-CHAIN-001" or set (alert .affected_events ) & focused_ids
288+ ]
289+ return focused , focused_alerts
290+
291+
292+ def _event_matches_account (event : LogEvent , account_keys : Set [str ], sids : Set [str ]) -> bool :
293+ if not account_keys and not sids :
294+ return False
295+ event_keys = {
296+ _account_key (value )
297+ for value in (
298+ event .details .get ("account" ),
299+ event .details .get ("target_account" ),
300+ event .details .get ("target_user" ),
301+ event .details .get ("member_account" ),
302+ event .details .get ("member_name" ),
303+ event .details .get ("account_name" ),
304+ )
305+ if _account_key (value )
306+ }
307+ if event_keys & account_keys :
308+ return True
309+ event_sids = {
310+ str (value ).strip ().lower ()
311+ for value in (event .details .get ("target_sid" ), event .details .get ("member_sid" ))
312+ if value not in (None , "" , "-" )
313+ }
314+ return bool (event_sids & sids )
315+
316+
317+ def _account_key (value : object ) -> str :
318+ text = str (value or "" ).strip ().strip ("\\ /" )
319+ if "\\ " in text :
320+ text = text .rsplit ("\\ " , 1 )[- 1 ]
321+ if "/" in text :
322+ text = text .rsplit ("/" , 1 )[- 1 ]
323+ return text .lower ()
324+
325+
326+ def _first_human (values : Sequence [str ]) -> str :
327+ for value in values :
328+ if not value .startswith ("S-1-" ):
329+ return value
330+ return values [0 ] if values else ""
331+
332+
333+ def _human_accounts (values : Sequence [str ]) -> List [str ]:
334+ human = [value for value in values if not value .startswith ("S-1-" )]
335+ return human or list (values )
336+
337+
338+ def _chain_account (alerts : Sequence [DetectionAlert ]) -> str :
339+ for alert in alerts :
340+ if alert .rule_id != "WIN-CHAIN-001" :
341+ continue
342+ for item in alert .evidence :
343+ if item .startswith ("目标账户:" ):
344+ return item .split (":" , 1 )[1 ].strip ()
345+ return ""
346+
347+
226348def _confidence (
227349 events : Sequence [LogEvent ],
228350 alerts : Sequence [DetectionAlert ],
229351 source_types : Sequence [str ],
230352 families : Sequence [str ],
231353) -> str :
354+ if any (alert .rule_id == "WIN-CHAIN-001" for alert in alerts ):
355+ return "high"
232356 if len (source_types ) >= 3 or (len (source_types ) >= 2 and len (families ) >= 3 ):
233357 return "high"
234358 if len (source_types ) >= 2 or len (alerts ) >= 2 or len (events ) >= 5 :
@@ -238,11 +362,16 @@ def _confidence(
238362
239363def _title (
240364 source_ips : Sequence [str ],
365+ accounts : Sequence [str ],
241366 assets : Sequence [str ],
242367 source_types : Sequence [str ],
243368 phases : Sequence [str ],
244369 level : ThreatLevel ,
370+ alerts : Sequence [DetectionAlert ],
245371) -> str :
372+ if any (alert .rule_id == "WIN-CHAIN-001" for alert in alerts ):
373+ account = _chain_account (alerts ) or _first_human (accounts )
374+ return f"可疑本地管理员账号与远程登录: { account } " if account else "可疑本地管理员账号与远程登录"
246375 subject = source_ips [0 ] if source_ips else (assets [0 ] if assets else "未知实体" )
247376 if len (source_types ) >= 2 :
248377 return f"P0 多源关联案件: { subject } "
@@ -255,12 +384,23 @@ def _description(
255384 source_ips : Sequence [str ],
256385 accounts : Sequence [str ],
257386 assets : Sequence [str ],
387+ operators : Sequence [str ],
388+ workstations : Sequence [str ],
258389 source_types : Sequence [str ],
259390 phases : Sequence [str ],
260391 alerts : Sequence [DetectionAlert ],
261392 events : Sequence [LogEvent ],
262393) -> str :
263- subject = source_ips [0 ] if source_ips else "未知来源"
394+ subject = source_ips [0 ] if source_ips else (workstations [0 ] if workstations else "未知来源" )
395+ if any (alert .rule_id == "WIN-CHAIN-001" for alert in alerts ):
396+ return (
397+ f"{ subject } 关联到 Windows 账号创建、特权组加入和远程登录链路;"
398+ f"核心账号: { _chain_account (alerts ) or _first_human (accounts ) or '?' } ;"
399+ f"操作者: { ', ' .join (operators [:3 ]) or '?' } ;"
400+ f"来源工作站: { ', ' .join (workstations [:3 ]) or '?' } ;"
401+ f"资产: { ', ' .join (assets [:5 ]) or '?' } ;"
402+ f"阶段: { ', ' .join (phases [:6 ]) or '未分类' } 。"
403+ )
264404 return (
265405 f"{ subject } 在 { len (source_types ) or 1 } 类日志源中关联到 "
266406 f"{ len (alerts )} 个告警、{ len (events )} 条关键事件;"
@@ -289,15 +429,31 @@ def _timeline(events: Sequence[LogEvent]) -> List[TimelineEntry]:
289429def _evidence (
290430 events : Sequence [LogEvent ],
291431 alerts : Sequence [DetectionAlert ],
432+ source_ips : Sequence [str ],
433+ accounts : Sequence [str ],
434+ assets : Sequence [str ],
435+ operators : Sequence [str ],
436+ workstations : Sequence [str ],
292437 source_types : Sequence [str ],
293438 phases : Sequence [str ],
294439) -> List [str ]:
295- evidence = [
440+ evidence = []
441+ if accounts :
442+ evidence .append (f"核心账号: { ', ' .join (_human_accounts (accounts )[:5 ])} " )
443+ if operators :
444+ evidence .append (f"操作者: { ', ' .join (operators [:5 ])} " )
445+ if source_ips :
446+ evidence .append (f"来源IP: { ', ' .join (source_ips [:5 ])} " )
447+ if workstations :
448+ evidence .append (f"来源工作站: { ', ' .join (workstations [:5 ])} " )
449+ if assets :
450+ evidence .append (f"资产: { ', ' .join (assets [:5 ])} " )
451+ evidence .extend ([
296452 f"日志源: { ', ' .join (source_types ) or '?' } " ,
297453 f"攻击阶段: { ', ' .join (phases ) or '?' } " ,
298454 f"关联告警: { len (alerts )} 个" ,
299455 f"关键事件: { len (events )} 条" ,
300- ]
456+ ])
301457 for alert in alerts [:3 ]:
302458 evidence .append (f"{ alert .rule_id } : { alert .rule_name } ({ alert .level .label } )" )
303459 for event in sorted (events , key = lambda item : item .timestamp or "" )[:3 ]:
@@ -314,6 +470,8 @@ def _recommended_actions(source_types: Sequence[str], families: Sequence[str], l
314470 actions .append ("核查入口 URL、漏洞命中规则和应用同时间窗口异常,确认是否利用成功。" )
315471 if "identity" in families or "vpn" in source_types :
316472 actions .append ("核查账号登录源、MFA 状态和登录后操作,必要时冻结账号并重置凭据。" )
473+ if "persistence" in families or "privilege-escalation" in families or "remote-access" in families :
474+ actions .append ("禁用可疑新建账号,移出特权组,核查 RDP/NTLM 来源工作站和同时间窗口管理员操作。" )
317475 if "command-control" in families or "proxy" in source_types or "dns" in source_types :
318476 actions .append ("封禁恶意域名/IP,回溯 DNS、代理和防火墙外联链路。" )
319477 if "exfiltration" in families :
@@ -329,6 +487,8 @@ def _next_logs(source_types: Sequence[str], families: Sequence[str]) -> List[str
329487 wanted .extend (["Web access/error 日志" , "业务应用日志" , "WAF 原始命中详情" ])
330488 if "identity" in families :
331489 wanted .extend (["VPN/SSO/MFA 审计" , "AD/域控 Security 日志" , "堡垒机会话审计" ])
490+ if "persistence" in families or "privilege-escalation" in families or "remote-access" in families :
491+ wanted .extend (["Windows Security 4720/4722/4724/4732/4624/4776" , "TerminalServices RDP 会话日志" , "EDR/XDR 进程树" , "防火墙/NAT 会话日志" ])
332492 if "compromise" in families or "execution" in families :
333493 wanted .extend (["EDR/XDR 进程树" , "Windows Sysmon" , "Linux auditd/auth.log" ])
334494 if "command-control" in families or "network" in families :
0 commit comments