Skip to content

Commit 002186c

Browse files
authored
Network process map (kevoreilly#2899)
1 parent 7163caf commit 002186c

File tree

17 files changed

+788
-598
lines changed

17 files changed

+788
-598
lines changed

changelog.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
### [04.02.2026]
2+
* Network Analysis:
3+
* Integrated process mapping directly into `network` processing module.
4+
* Added ability to show network details (DNS, HTTP, TCP/UDP) captured from behavioral analysis in the network results.
5+
* This allows recovery of network activity that might be missing from PCAP (e.g., due to capture evasion or failed interception).
6+
* Centralized network utility functions into `lib/cuckoo/common/network_utils.py` for better maintainability and performance.
7+
* New configuration option `process_map` under `[network]` section in `processing.conf`.
8+
* Web UI:
9+
* Added Process Name and PID columns across all network analysis views (TCP, UDP, ICMP, DNS, HTTP, IRC, SMTP).
10+
111
### [28.01.2026]
212
* CAPE Agent:
313
* Ported to Golang for improved stealth, performance, and zero-dependency deployment.

conf/default/processing.conf.default

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,10 @@ enabled = no
9999
[network]
100100
enabled = yes
101101
sort_pcap = no
102+
# Enable mapping of network events to specific processes using behavioral analysis data
103+
process_map = no
104+
# Adds network connections seen in behavior but not in PCAP. Requires process_map = yes
105+
merge_behavior_map = no
102106
# DNS whitelisting to ignore domains/IPs configured in network.py
103107
dnswhitelist = yes
104108
# additional entries
@@ -324,5 +328,3 @@ enabled = no
324328
# plain-text TLS streams into the task PCAP.
325329
enabled = no
326330

327-
[network_process_map]
328-
enabled = no

lib/cuckoo/common/network_utils.py

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
# Copyright (C) 2010-2015 Cuckoo Foundation.
2+
# This file is part of Cuckoo Sandbox - http://www.cuckoosandbox.org
3+
# See the file 'docs/LICENSE' for copying permission.
4+
5+
import datetime
6+
from contextlib import suppress
7+
from urllib.parse import urlparse
8+
9+
DNS_APIS = {
10+
"getaddrinfo",
11+
"getaddrinfow",
12+
"getaddrinfoex",
13+
"getaddrinfoexw",
14+
"gethostbyname",
15+
"gethostbynamew",
16+
"dnsquery_a",
17+
"dnsquery_w",
18+
"dnsqueryex",
19+
"dnsquery",
20+
}
21+
22+
23+
HTTP_HINT_APIS = {
24+
"internetcrackurla",
25+
"internetcrackurlw",
26+
"httpsendrequesta",
27+
"httpsendrequestw",
28+
"internetsendrequesta",
29+
"internetsendrequestw",
30+
"internetconnecta",
31+
"internetconnectw",
32+
"winhttpopenrequest",
33+
"winhttpsendrequest",
34+
"winhttpconnect",
35+
"winhttpopen",
36+
"internetopenurla",
37+
"internetopenurlw",
38+
"httpopenrequesta",
39+
"httpopenrequestw",
40+
}
41+
42+
43+
TLS_HINT_APIS = {
44+
"sslencryptpacket",
45+
"ssldecryptpacket",
46+
"initializesecuritycontexta",
47+
"initializesecuritycontextw",
48+
"initializesecuritycontextexa",
49+
"initializesecuritycontextexw",
50+
"acceptsecuritycontext",
51+
}
52+
53+
54+
def _norm_domain(d):
55+
if not d or not isinstance(d, str):
56+
return None
57+
d = d.strip().strip(".").lower()
58+
return d or None
59+
60+
61+
def _parse_behavior_ts(ts_str):
62+
"""
63+
Parse behavior timestamp like: '2026-01-22 23:46:58,199' -> epoch float
64+
Returns None if parsing fails.
65+
"""
66+
if not ts_str or not isinstance(ts_str, str):
67+
return None
68+
try:
69+
return datetime.datetime.strptime(ts_str, "%Y-%m-%d %H:%M:%S,%f").timestamp()
70+
except ValueError:
71+
return None
72+
73+
74+
def _get_call_args_dict(call):
75+
"""Convert arguments list to a dictionary for O(1) access."""
76+
return {a["name"]: a["value"] for a in call.get("arguments", []) if "name" in a}
77+
78+
79+
def _extract_domain_from_call(call, args_map):
80+
# Check named arguments first
81+
for name in (
82+
"hostname",
83+
"host",
84+
"node",
85+
"nodename",
86+
"name",
87+
"domain",
88+
"szName",
89+
"pszName",
90+
"lpName",
91+
"query",
92+
"queryname",
93+
"dns_name",
94+
"QueryName",
95+
"lpstrName",
96+
"pName",
97+
):
98+
v = args_map.get(name)
99+
if isinstance(v, str) and v.strip():
100+
return v
101+
102+
# Heuristic scan of all string arguments
103+
for v in args_map.values():
104+
if isinstance(v, str):
105+
s = v.strip()
106+
if "." in s and " " not in s and s.count(".") <= 10:
107+
return s
108+
109+
return None
110+
111+
112+
def _get_arg_any(args_map, *names):
113+
"""Return the first matching argument value for any of the provided names."""
114+
for n in names:
115+
if n in args_map:
116+
return args_map[n]
117+
return None
118+
119+
120+
def _norm_ip(ip):
121+
if ip is None:
122+
return None
123+
if not isinstance(ip, str):
124+
ip = str(ip)
125+
ip = ip.strip()
126+
return ip or None
127+
128+
129+
def _looks_like_http(buf):
130+
if not buf or not isinstance(buf, str):
131+
return False
132+
133+
first = buf.splitlines()[0].strip() if buf else ""
134+
if not first:
135+
return False
136+
137+
u = first.upper()
138+
if u.startswith("HTTP/1.") or u.startswith("HTTP/2"):
139+
return True
140+
141+
methods = ("GET ", "POST ", "HEAD ", "PUT ", "DELETE ", "OPTIONS ", "PATCH ", "TRACE ")
142+
if any(u.startswith(m) for m in methods) and " HTTP/1." in u:
143+
return True
144+
145+
if u.startswith("CONNECT ") and " HTTP/1." in u:
146+
return True
147+
148+
return False
149+
150+
151+
def _http_host_from_buf(buf):
152+
if not buf or not isinstance(buf, str):
153+
return None
154+
155+
lines = buf.splitlines()
156+
if not lines:
157+
return None
158+
159+
for line in lines[1:50]:
160+
if line.lower().startswith("host:"):
161+
try:
162+
return line.split(":", 1)[1].strip()
163+
except IndexError:
164+
continue
165+
166+
with suppress(Exception):
167+
first = lines[0].strip()
168+
parts = first.split()
169+
if len(parts) >= 2:
170+
target = parts[1].strip()
171+
url = _extract_first_url(target)
172+
if url:
173+
host = _host_from_url(url)
174+
if host:
175+
return host
176+
177+
with suppress(Exception):
178+
first = lines[0].strip()
179+
parts = first.split()
180+
if len(parts) >= 2 and parts[0].upper() == "CONNECT":
181+
return parts[1].strip()
182+
183+
return None
184+
185+
186+
def _safe_int(x):
187+
with suppress(Exception):
188+
return int(x)
189+
return None
190+
191+
192+
def _host_from_url(url):
193+
if not url or not isinstance(url, str):
194+
return None
195+
196+
with suppress(Exception):
197+
u = urlparse(url)
198+
return u.hostname
199+
200+
return None
201+
202+
203+
def _extract_first_url(text):
204+
if not text or not isinstance(text, str):
205+
return None
206+
s = text.strip()
207+
for scheme in ("http://", "https://"):
208+
idx = s.lower().find(scheme)
209+
if idx != -1:
210+
return s[idx:].split()[0].strip('"\',')
211+
return None
212+
213+
214+
def _add_http_host(http_host_map, host, pinfo, sock=None):
215+
"""
216+
Store host keys in a stable way.
217+
Adds:
218+
- normalized host
219+
- if host is host:port and port parses, also normalized host-only
220+
"""
221+
hk = _norm_domain(host)
222+
if not hk:
223+
return
224+
225+
entry = dict(pinfo)
226+
if sock is not None:
227+
entry["socket"] = sock
228+
229+
http_host_map[hk].append(entry)
230+
231+
if ":" in hk:
232+
h_only, p = hk.rsplit(":", 1)
233+
if _safe_int(p) is not None and h_only:
234+
http_host_map[h_only].append(entry)
235+
236+
237+
def _extract_tls_server_name(call, args_map):
238+
"""
239+
Best-effort server name extraction for TLS/SChannel/SSPI.
240+
"""
241+
for name in (
242+
"sni",
243+
"SNI",
244+
"ServerName",
245+
"servername",
246+
"server_name",
247+
"TargetName",
248+
"targetname",
249+
"Host",
250+
"host",
251+
"hostname",
252+
"Url",
253+
"URL",
254+
"url",
255+
):
256+
v = args_map.get(name)
257+
if isinstance(v, str) and v.strip():
258+
s = v.strip()
259+
u = _extract_first_url(s)
260+
if u:
261+
return _host_from_url(u) or s
262+
if "." in s and " " not in s and len(s) < 260:
263+
return s
264+
265+
for v in args_map.values():
266+
if isinstance(v, str):
267+
s = v.strip()
268+
if "." in s and " " not in s and len(s) < 260:
269+
u = _extract_first_url(s)
270+
if u:
271+
return _host_from_url(u) or s
272+
return s
273+
274+
return None

0 commit comments

Comments
 (0)