Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
659f9a8
Warn instead of fail when script storage fails during submission
josh-feather May 12, 2026
668276f
mongo indexes
doomedraven May 12, 2026
76ce548
Add guacamole idle timeout detection and upgrade to 1.6
josh-feather May 12, 2026
c7aa003
Replace f-string without placeholders with normal string
josh-feather May 12, 2026
dcd8cb1
feat: COM logical parent enrichment in process tree (LethalHTA / DCOM…
wmetcalf May 12, 2026
00daa76
fix: Guac UI dark theme, End Session button, and user_stop API
wmetcalf May 12, 2026
31f2f35
fix: DNS and Hosts network tables support multiple process attributions
wmetcalf May 12, 2026
271a64d
feat: Authenticode certificate card on analysis overview tab
wmetcalf May 12, 2026
d9542f3
feat: Analysis list view enhancements — YARA, task tags, submitter, s…
wmetcalf May 12, 2026
95c9b1f
feat: network_etw processing module + ETW Telemetry tab
wmetcalf May 12, 2026
6e41d51
fix: restore views.py to upstream, only add auth for tasks_status
wmetcalf May 12, 2026
50c6cc9
fix: restore calls.reset(), fix CLSID mapping, _safe_int, allow multi…
wmetcalf May 12, 2026
3204796
fix: authenticode template duplicate rows, timestamp chain scope, iss…
wmetcalf May 12, 2026
4e779c4
fix: split user_task_tags into list; add cape_yara to get_analysis_in…
wmetcalf May 12, 2026
d211b96
fix: utcfromtimestamp deprecated; N+1 submitter query via lru_cache h…
wmetcalf May 12, 2026
009d6be
fix: add display_authenticode section to web.conf.default
wmetcalf May 12, 2026
c34948d
fix: split_csv filter for tags_tasks; search.html browser martians; a…
wmetcalf May 12, 2026
424ef85
fix: remove unused socket/encode imports; os.scandir for ETW dir chec…
wmetcalf May 12, 2026
4fac9e0
fix: cape_yara projection needs file_ref for hook resolution; merge y…
wmetcalf May 12, 2026
da720cc
fix: Self-Signed badge only on depth-1 chains; root CAs are always se…
wmetcalf May 13, 2026
48e74e1
Removed unused import of 'os' in the _enrich_tree_com_parents function
kevoreilly May 15, 2026
0631249
Removed unused imports for SessionAuthentication and authentication_c…
kevoreilly May 15, 2026
71176a5
Refactor title attribute in badge span as suggested by Copilot
kevoreilly May 15, 2026
5df3027
Merge pull request #3022 from wmetcalf/feat/authenticode-cert-card
kevoreilly May 15, 2026
1145c74
Merge branch 'master' into feat/analysis-list-view-enhancements
kevoreilly May 15, 2026
78f85e4
Merge branch 'master' into feat/network-etw-telemetry-tab
kevoreilly May 15, 2026
e47c824
Merge pull request #3019 from wmetcalf/feature/com-process-tree-enric…
kevoreilly May 15, 2026
338f0d3
Merge pull request #3020 from wmetcalf/fix/guac-ui-theme-and-session
kevoreilly May 15, 2026
a3a0073
Merge pull request #3021 from wmetcalf/fix/network-dns-multi-process-…
kevoreilly May 15, 2026
f8e1cba
Merge pull request #3023 from wmetcalf/feat/analysis-list-view-enhanc…
kevoreilly May 15, 2026
d80ae99
Merge branch 'master' into feat/network-etw-telemetry-tab
kevoreilly May 15, 2026
c58ec08
Removed duplicate fields from projection in views.py
kevoreilly May 15, 2026
5cd7c75
Merge pull request #3024 from wmetcalf/feat/network-etw-telemetry-tab
kevoreilly May 15, 2026
40304c6
Merge pull request #3015 from josh-feather/warn-instead-of-fail-for-i…
kevoreilly May 15, 2026
52406cc
Merge pull request #3016 from kevoreilly/indexes
kevoreilly May 15, 2026
1230fa0
Merge branch 'master' into guac-a-mole-idle-timeout
josh-feather May 15, 2026
6767653
Merge pull request #3017 from josh-feather/guac-a-mole-idle-timeout
kevoreilly May 15, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion analyzer/windows/modules/auxiliary/network_etw.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ def __init__(self, options, config):
log.debug("Could not read analysis config for filters: %s", e)

filter_ports.add(8000)
filter_ports.add(53)
# filter_ports.add(53) # do NOT filter DNS — we need UDP/53 events with PIDs to attribute sample DNS queries that bypass dnsapi.dll (direct UDP DNS in malware)

log.info("NetworkETW filters: ips=%s ports=%s", filter_ips, filter_ports)

Expand Down
7 changes: 7 additions & 0 deletions conf/default/reporting.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,13 @@ db = cuckoo
# password =
# authsource = cuckoo

# Extended search indexes (may increase disk/RAM usage)
index_yara = no
index_clamav = no
index_hashes = no
index_detections = no
index_filenames = no

# Set this value if you are using mongodb with TLS enabled
# tlscafile =

Expand Down
22 changes: 22 additions & 0 deletions conf/default/web.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -212,6 +212,12 @@ ignore_rdp_cert = false
rdp_disable_wallpaper = yes
rdp_disable_theming = yes
rdp_enable_font_smoothing = no
# Idle timeout settings for interactive sessions
# idle_timeout_seconds: Maximum idle time before session is terminated
# Defaults to 0 (disabled) when omitted or set to an invalid value
idle_timeout_seconds = 0
# activity_check_interval: How often to check for timeout in seconds when enabled
activity_check_interval = 30
rdp_enable_full_window_drag = no
rdp_enable_desktop_composition = no
rdp_enable_menu_animations = no
Expand Down Expand Up @@ -252,3 +258,19 @@ enabled = no

[audit_framework]
enabled = no

[display_etw]
# Show ETW Telemetry tab on analysis report (requires network_etw processing module)
enabled = no

[display_cape_yara]
# Show CAPE YARA hit count column on analysis list and search results
enabled = no

[display_submitter]
# Show submitting user column on analysis list (requires WEB_AUTHENTICATION)
enabled = no

[display_authenticode]
# Show Authenticode certificate chain card on the analysis overview tab
enabled = no
15 changes: 15 additions & 0 deletions lib/cuckoo/common/guac_utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
"""Utilities for Guacamole protocol handling and activity detection."""

import re

# Matches the opcode of a Guacamole instruction at message start or after ';'.
# Guacamole wire format: <len>.<value>,<len>.<value>...;
# Example: 5.mouse,3.100,3.200,1.0;
_ACTIVITY_RE = re.compile(r"(?:^|;)\d+\.(key|mouse),")


def is_user_activity(message: str) -> bool:
"""Return ``True`` if *message* contains a mouse or keyboard instruction."""
if not message or not isinstance(message, str):
return False
return _ACTIVITY_RE.search(message) is not None
6 changes: 4 additions & 2 deletions lib/cuckoo/common/web_utils.py
Original file line number Diff line number Diff line change
Expand Up @@ -930,6 +930,7 @@ def download_file(**kwargs):
if not static and "dist_extract" in kwargs["options"]:
static = True

warnings = []
for machine in kwargs.get("task_machines", []):
if machine == "first":
machine = None
Expand Down Expand Up @@ -961,7 +962,7 @@ def download_file(**kwargs):
save_script_to_storage(task_ids_new, kwargs)
except Exception as e:
log.error("Error saving scripts to storage: %s", e)
return "error", {"error": "Error: Storing scripts to tempstorage"}
warnings.append({"script": f"{e}"})

if isinstance(kwargs.get("task_ids", False), list):
kwargs["task_ids"].extend(task_ids_new)
Expand All @@ -972,7 +973,8 @@ def download_file(**kwargs):
if not onesuccess:
return "error", {"error": f"Provided hash not found on {kwargs['service']}"}

return "ok", {"task_ids": kwargs["task_ids"], "errors": extra_details.get("errors", [])}
errors = extra_details.get("errors", []) + warnings
return "ok", {"task_ids": kwargs["task_ids"], "errors": errors}


def save_script_to_storage(task_ids: list, kwargs):
Expand Down
30 changes: 22 additions & 8 deletions lib/cuckoo/core/startup.py
Original file line number Diff line number Diff line change
Expand Up @@ -122,16 +122,25 @@ def check_webgui_mongo():
# with large amounts of data.
# Note: Silently ignores the creation if the index already exists.
mongo_create_index("analysis", "info.id", name="info.id_1")
# Some indexes that can be useful for some users
mongo_create_index("files", "md5", name="file_md5")
mongo_create_index("files", [("_task_ids", 1)])

# side indexes as ideas
"""
mongo_create_index("analysis", "detections", name="detections_1")
mongo_create_index("analysis", "target.file.name", name="name_1")
"""
if repconf.mongodb.get("index_yara", False):
mongo_create_index("files", "yara.name", name="yara_name")
mongo_create_index("files", "cape_yara.name", name="cape_yara_name")

if repconf.mongodb.get("index_clamav", False):
mongo_create_index("files", "clamav", name="clamav_index")

if repconf.mongodb.get("index_hashes", False):
mongo_create_index("files", "md5", name="file_md5")
mongo_create_index("files", "sha1", name="file_sha1")
mongo_create_index("files", "ssdeep", name="file_ssdeep")

if repconf.mongodb.get("index_detections", False):
mongo_create_index("analysis", "detections.family", name="detections_family")

if repconf.mongodb.get("index_filenames", False):
mongo_create_index("analysis", "target.file.name", name="name_1")
elif repconf.elasticsearchdb.enabled:
# ToDo add check
pass
Expand Down Expand Up @@ -204,7 +213,10 @@ def check_linux_dist():
with suppress(AttributeError):
platform_details = platform.dist()
if platform_details[0] != "Ubuntu" and platform_details[1] not in ubuntu_versions:
log.info("[!] You are using NOT supported Linux distribution by devs! Any issue report is invalid! We only support Ubuntu LTS %s", ubuntu_versions)
log.info(
"[!] You are using NOT supported Linux distribution by devs! Any issue report is invalid! We only support Ubuntu LTS %s",
ubuntu_versions,
)


def init_logging(level: int):
Expand Down Expand Up @@ -359,6 +371,8 @@ def check_snapshot_state():
machine_config = machinery_config.get(machine_name)
machine_name = machine_config.get("label")
domain = conn.lookupByName(machine_name)


# Check for valid architecture configuration.
arch = machine_config.get("arch")
if not arch:
Expand Down
82 changes: 74 additions & 8 deletions modules/processing/behavior.py
Original file line number Diff line number Diff line change
Expand Up @@ -1239,9 +1239,37 @@ def __init__(self):
self.http_requests = [] # url -> [pinfo]
self.dns_intents = defaultdict(list) # domain -> [intent]
self._winhttp_state = {"processes": {}}
self.com_activations = [] # out-of-process CoCreateInstance calls

# CLSIDs for known out-of-process COM servers
_OOP_CLSIDS = {
"3050f4d8-98b5-11cf-bb82-00aa00bdce0b": "mshta.exe",
"0002df01-0000-0000-c000-000000000046": "iexplore.exe",
"9ba05972-f6a8-11cf-a442-00a0c90a8f39": "explorer.exe",
"c08afd90-f2a1-11d1-8455-00a0c91f3880": "explorer.exe",
"25336920-03f9-11cf-8fd0-00aa00686f13": "mshta.exe", # HTMLDocument OOP → mshta
}

def event_apicall(self, call, process):
if call.get("category") != "network":
cat = call.get("category") or ""
if cat == "com":
api = (call.get("api") or "").lower()
if api == "cocreateinstance":
args_map = _get_call_args_dict(call)
clsid = (args_map.get("rclsid") or "").lower()
progid = (args_map.get("progid") or "").strip()
# Capture any out-of-process activation (CLSCTX includes LOCAL_SERVER=4)
ctx = _safe_int(args_map.get("clscontext", "0"))
if ctx & 0x4 or clsid in self._OOP_CLSIDS:
self.com_activations.append({
"clsid": clsid,
"progid": progid,
"activator_pid": process.get("process_id"),
"activator_name": process.get("process_name", ""),
"target_binary": self._OOP_CLSIDS.get(clsid, ""),
})
return
if cat != "network":
return

api = (call.get("api") or "").lower()
Expand Down Expand Up @@ -1350,17 +1378,17 @@ def run(self):
# BSON/JSON keys must be strings.
# Let's convert tuple keys to string representation "ip:port"

endpoint_map_list = [{"ip_port": f"{ip}:{port}", "pinfo": entries} for (ip, port), entries in self.endpoint_map.items()]

http_host_map_list = [{"host": k, "pinfo": v} for k, v in self.http_host_map.items()]
dns_intents_list = [{"domain": k, "intents": v} for k, v in self.dns_intents.items()]
endpoint_map_str = {}
for (ip, port), entries in self.endpoint_map.items():
endpoint_map_str[f"{ip}:{port}"] = entries

return {
"endpoint_map": endpoint_map_list,
"http_host_map": http_host_map_list,
"dns_intents": dns_intents_list,
"endpoint_map": endpoint_map_str,
"http_host_map": self.http_host_map,
"dns_intents": self.dns_intents,
"http_requests": self.http_requests,
"winhttp_sessions": winhttp_finalize_sessions(self._winhttp_state),
"com_activations": self.com_activations,
}


Expand Down Expand Up @@ -1436,6 +1464,39 @@ def run(self):
return self.bufs



def _enrich_tree_com_parents(tree_nodes, com_activations):
"""Walk the processtree and annotate nodes whose binary matches a COM activation record."""
# Build lookup: target_binary_lower -> list of activations
binary_map = {}
for act in com_activations:
binary = (act.get("target_binary") or "").lower()
if not binary:
# Fall back to ProgID heuristic
progid = (act.get("progid") or "").lower()
_progid_to_binary = {
"htafile": "mshta.exe",
"internetexplorer.application": "iexplore.exe",
"shell.application": "explorer.exe",
}
binary = _progid_to_binary.get(progid, "")
if binary:
binary_map.setdefault(binary, []).append(act)

def _walk(nodes):
for node in nodes:
path = node.get("module_path") or ""
name = path.replace("\\", "/").rsplit("/", 1)[-1].lower()
if name in binary_map:
act = binary_map[name][0]
node["com_logical_parent_pid"] = act["activator_pid"]
node["com_logical_parent_name"] = act["activator_name"]
node["com_progid"] = act.get("progid", "")
node["com_clsid"] = act.get("clsid", "")
_walk(node.get("children") or [])

_walk(tree_nodes)

class BehaviorAnalysis(Processing):
"""Behavior Analyzer."""

Expand Down Expand Up @@ -1478,6 +1539,11 @@ def run(self):
behavior[instance.key] = instance.run()
except Exception as e:
log.exception('Failed to run partial behavior class "%s" due to "%s"', instance.key, e)

# Enrich processtree nodes with COM logical parent relationships
com_acts = (behavior.get("network_map") or {}).get("com_activations") or []
if com_acts and behavior.get("processtree"):
_enrich_tree_com_parents(behavior["processtree"], com_acts)
else:
log.warning('Analysis results folder does not exist at path "%s"', self.logs_path)
# load behavior from json if exist or env CAPE_REPORT variable
Expand Down
Loading
Loading