Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
8 changes: 8 additions & 0 deletions changelog.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,11 @@
### [08.06.2026]
* Threat Discovery & Hunting Workstation Dashboard:
* Integrated centralized dynamic multi-faceted database clustering across 12 categories (Domains, IPs, Mutexes, Dropped Files, Commands, Registry Keys, Hashes, ImpHashes, and Signatures).
* Created dynamic, cascading, auto-reloaded JSON configuration cacher (`conf/hunt.json`) with hierarchical lookup order (`custom/conf` ➔ `conf` ➔ `conf/default`).
* Built high-performance, memory-speed caching system utilizing OS modification-time (`mtime`) checks for instant reloading without disk parsing or server restarts.
* Integrated inline threat intelligence OSINT pivoting links (VirusTotal, Shodan, Censys, MalwareBazaar, and AlienVault OTX) and transaction-safe, sanitized AJAX-based task tagging group actions.
* Added comprehensive unit testing covering all view states, error handling, and security measures.

### [05.06.2026]
* Monitor updates:
* NtCreateUserProcess hook: Dynamically patch ping commandline to thwart ping delays (e.g. Formbook/Xloader)
Expand Down
143 changes: 143 additions & 0 deletions conf/default/hunt.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
{
"domains": {
"title": "Top Shared Domains",
"icon": "fas fa-globe text-info",
"badge_color": "info",
"form_key": "cat_domains",
"db_unwind": "$network.domains",
"db_group": "$network.domains.domain",
"validator": "is_valid_domain",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/domain/"},
{"name": "AlienVault OTX", "icon": "fas fa-shield-alt", "url": "https://otx.alienvault.com/indicator/domain/"}
]
},
"ips": {
"title": "Top Shared IPs",
"icon": "fas fa-network-wired text-info",
"badge_color": "info",
"form_key": "cat_ips",
"db_unwind": "$network.hosts",
"db_group": "$network.hosts.ip",
"validator": "is_valid_ip",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/ip-address/"},
{"name": "Shodan", "icon": "fas fa-search", "url": "https://www.shodan.io/host/"}
]
},
"mutexes": {
"title": "Top Shared Mutexes",
"icon": "fas fa-lock text-warning",
"badge_color": "warning",
"form_key": "cat_mutexes",
"db_unwind": "$behavior.summary.mutexes",
"db_group": "$behavior.summary.mutexes",
"validator": "is_valid_mutex",
"pivots": []
},
"dropped_files": {
"title": "Shared Dropped Files",
"icon": "fas fa-file-alt text-success",
"badge_color": "success",
"form_key": "cat_files",
"db_unwind": "$behavior.summary.files",
"db_group": "$behavior.summary.files",
"validator": "is_valid_file",
"pivots": []
},
"executed_commands": {
"title": "Shared Executed Commands",
"icon": "fas fa-terminal text-success",
"badge_color": "success",
"form_key": "cat_commands",
"db_unwind": "$behavior.summary.executed_commands",
"db_group": "$behavior.summary.executed_commands",
"validator": "is_valid_command",
"pivots": []
},
"registry_keys": {
"title": "Shared Registry Keys",
"icon": "fas fa-key text-danger",
"badge_color": "danger",
"form_key": "cat_registry",
"db_unwind": "$behavior.summary.keys",
"db_group": "$behavior.summary.keys",
"validator": "is_valid_registry",
"pivots": []
},
"dropped_hashes": {
"title": "Top Dropped File Hashes",
"icon": "fas fa-hashtag text-white",
"badge_color": "light",
"form_key": "cat_dropped_hashes",
"db_unwind": "$dropped",
"db_group": "$dropped.sha256",
"validator": "is_valid_hash",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/file/"},
{"name": "MalwareBazaar", "icon": "fas fa-database", "url": "https://bazaar.abuse.ch/sample/"}
]
},
"procdump_hashes": {
"title": "Unpacked Memory Hashes",
"icon": "fas fa-memory text-white",
"badge_color": "light",
"form_key": "cat_procdump_hashes",
"db_unwind": "$procdump",
"db_group": "$procdump.sha256",
"validator": "is_valid_hash",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/file/"},
{"name": "MalwareBazaar", "icon": "fas fa-database", "url": "https://bazaar.abuse.ch/sample/"}
]
},
"extracted_hashes": {
"title": "Extracted Payload Hashes",
"icon": "fas fa-file-medical text-white",
"badge_color": "light",
"form_key": "cat_extracted_hashes",
"db_unwind": "$extracted",
"db_group": "$extracted.sha256",
"validator": "is_valid_hash",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/file/"},
{"name": "MalwareBazaar", "icon": "fas fa-database", "url": "https://bazaar.abuse.ch/sample/"}
]
},
"imphashes": {
"title": "PE Import Hashes (ImpHashes)",
"icon": "fas fa-file-invoice text-white",
"badge_color": "light",
"form_key": "cat_imphashes",
"db_unwind": null,
"db_group": "$static.pe.imphash",
"validator": "is_valid_md5",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/search/"},
{"name": "MalwareBazaar", "icon": "fas fa-database", "url": "https://bazaar.abuse.ch/browse.php?search=imphash%3A"}
],
"db_match": {"_id": {"$ne": null}}
},
"http_uris": {
"title": "Shared HTTP Request URIs",
"icon": "fas fa-link text-info",
"badge_color": "info",
"form_key": "cat_http_uris",
"db_unwind": "$network.http",
"db_group": "$network.http.uri",
"validator": "is_valid_string",
"pivots": [
{"name": "VirusTotal", "icon": "fas fa-external-link-alt", "url": "https://www.virustotal.com/gui/search/"}
]
},
"signatures": {
"title": "Shared Signatures",
"icon": "fas fa-signature text-warning",
"badge_color": "warning",
"form_key": "cat_signatures",
"db_unwind": "$signatures",
"db_group": "$signatures.name",
"validator": "is_valid_string",
"pivots": []
}
}
4 changes: 4 additions & 0 deletions conf/default/web.conf.default
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,7 @@ enabled = no
[display_authenticode]
# Show Authenticode certificate chain card on the analysis overview tab
enabled = no

[hunt]
# Proactively discover new emerging campaigns by grouping undetected analyses by shared indicators
enabled = no
2 changes: 1 addition & 1 deletion data/yara/CAPE/Vidar.yar
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ rule Vidar
cape_type = "Vidar Payload"
packed = "96ab9c389a6a53c54a3ea05d139aaf2d775e8db06f37d027f696828dcc55e2bb"
strings:
$code = {4D 85 C0 75 0? [0-16] (41|45) 88 ?? [0-20] (41|45) 38 (08|10) 74 ?? [0-16] (48|49|4C|4D) (63|2B) [0-16] 4? 3B ?? 73}
$code = {4D 85 C0 75 0? [0-16] (41|45) 88 ?? [0-20] (41|45) 38 ?? 74 ?? [0-16] (48|49|4C|4D) (63|2B) [0-16] 4? 3B ?? 73}
condition:
uint16(0) == 0x5A4D and all of them
}
Expand Down
221 changes: 221 additions & 0 deletions lib/cuckoo/common/hunting.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import ipaddress
import logging
import os
import re
import json

from data.safelist.domains import domain_passlist, domain_passlist_re
from data.safelist.replacepatterns import FILES_DENYLIST, FILES_ENDING_DENYLIST, MUTEX_DENYLIST

log = logging.getLogger(__name__)

# Resolve CUCKOO_ROOT
_current_dir = os.path.abspath(os.path.dirname(__file__))
CUCKOO_ROOT = os.path.normpath(os.path.join(_current_dir, "..", "..", ".."))

# Precompile regex list once at the module level for maximum performance
compiled_passlist_re = []
for safe_re in domain_passlist_re:
try:
if isinstance(safe_re, str):
compiled_passlist_re.append(re.compile(safe_re, re.IGNORECASE))
elif hasattr(safe_re, "match"):
compiled_passlist_re.append(safe_re)
except Exception:
pass


# Define module-level validation filters
def is_valid_domain(domain):
if not domain or not isinstance(domain, str):
return False
domain_lower = domain.lower()
for safe in domain_passlist:
if domain_lower == safe or domain_lower.endswith("." + safe):
return False
for regex in compiled_passlist_re:
try:
if regex.match(domain_lower):
return False
except Exception:
pass
return True


def is_valid_ip(ip):
if not ip or not isinstance(ip, str):
return False
try:
ip_obj = ipaddress.ip_address(ip)
if ip_obj.is_private or ip_obj.is_loopback or ip_obj.is_multicast or ip_obj.is_link_local:
return False
if ip in ("8.8.8.8", "8.8.4.4", "1.1.1.1", "9.9.9.9", "208.67.222.222", "208.67.220.220"):
return False
except ValueError:
return False
return True


def is_valid_file(file_path):
if not file_path or not isinstance(file_path, str):
return False
file_path_lower = file_path.lower()
for item in FILES_DENYLIST:
if item.lower() in file_path_lower:
return False
for item in FILES_ENDING_DENYLIST:
if file_path_lower.endswith(item.lower()):
return False
return True


def is_valid_hash(h):
if not h or not isinstance(h, str):
return False
if h in ("d41d8cd98f00b204e9800998ecf8427e", "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"):
return False
if len(h) != 64:
return False
return True


def is_valid_md5(h):
if not h or not isinstance(h, str):
return False
if h == "d41d8cd98f00b204e9800998ecf8427e":
return False
if len(h) != 32:
return False
return True


# Common system mutexes that generate noise
noisy_mutexes = [
"Local\\ZoneBaseMutex", "CTF.Asm.Mutex", "Global\\Access_Registry_Mutex",
"Local\\__wf_mut__", "cuckoo_mutex", "Local\\_Global_", "Local\\MS-LanguageProfile"
]
def is_valid_mutex(mutex):
if not mutex or not isinstance(mutex, str):
return False
mutex_lower = mutex.lower()
for m in MUTEX_DENYLIST:
if m.lower() in mutex_lower:
return False
for m in noisy_mutexes:
if m.lower() in mutex_lower:
return False
return True


noisy_registry_substrings = [
"Controlset001\\Control\\Lsa",
"Cryptography\\Providers",
"System\\CurrentControlSet\\Control\\Nls",
"SOFTWARE\\Microsoft\\Windows NT\\CurrentVersion\\Font",
"SOFTWARE\\Microsoft\\CTF\\",
"SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\Explorer\\MountPoints2"
]
def is_valid_registry(key):
if not key or not isinstance(key, str):
return False
key_lower = key.lower()
for sub in noisy_registry_substrings:
if sub.lower() in key_lower:
return False
return True


noisy_command_substrings = [
"chcp", "reg query", "sc query", "net start", "tasklist"
]
def is_valid_command(cmd):
if not cmd or not isinstance(cmd, str):
return False
cmd_lower = cmd.lower()
for sub in noisy_command_substrings:
if sub.lower() in cmd_lower:
return False
return True


VALIDATORS = {
"is_valid_domain": is_valid_domain,
"is_valid_ip": is_valid_ip,
"is_valid_mutex": is_valid_mutex,
"is_valid_file": is_valid_file,
"is_valid_command": is_valid_command,
"is_valid_registry": is_valid_registry,
"is_valid_hash": is_valid_hash,
"is_valid_md5": is_valid_md5,
"is_valid_string": lambda x: isinstance(x, str) and bool(x),
}

# Module level caching for Hunt Configuration
_CACHED_HUNT_MAP = None
_CACHED_HUNT_MTIME = None
_CACHED_HUNT_PATH = None


def load_hunt_map(min_count: int = 3):
"""
Dynamically loads the hunting configuration from a hierarchical search of paths.
Lookup order (reverse mode, most specific to least specific):
1. custom/conf/hunt.json
2. conf/hunt.json
3. conf/default/hunt.json (fallback defaults)

Utilizes system mtime caching on the resolved path to achieve zero disk reads when unmodified.
Returns (HUNT_MAP, VALIDATORS) tuple, or (None, error_reason) on error.
"""
global _CACHED_HUNT_MAP, _CACHED_HUNT_MTIME, _CACHED_HUNT_PATH

lookup_paths = [
os.path.normpath(os.path.join(CUCKOO_ROOT, "custom", "conf", "hunt.json")),
os.path.normpath(os.path.join(CUCKOO_ROOT, "conf", "hunt.json")),
os.path.normpath(os.path.join(CUCKOO_ROOT, "conf", "default", "hunt.json"))
]

has_invalid_syntax = False

for path in lookup_paths:
if os.path.exists(path):
try:
current_mtime = os.path.getmtime(path)

# Cache Hit: If cached configuration matches this path and modification time, return instantly!
if _CACHED_HUNT_MAP is not None and _CACHED_HUNT_PATH == path and _CACHED_HUNT_MTIME == current_mtime:
return _CACHED_HUNT_MAP, VALIDATORS

# Cache Miss: Parse the JSON file
with open(path, "r") as f:
raw_map = json.load(f)
if raw_map and isinstance(raw_map, dict):
temp_map = {}
for cat_id, cat_config in raw_map.items():
val_func_name = cat_config.get("validator", "is_valid_string")
cat_config["validator"] = VALIDATORS.get(val_func_name, lambda x: isinstance(x, str) and bool(x))

# Dynamically replace min_count placeholders inside the custom db_match if present
if "db_match" in cat_config:
if "count" in cat_config["db_match"] and "$gte" in cat_config["db_match"]["count"]:
cat_config["db_match"]["count"]["$gte"] = min_count

temp_map[cat_id] = cat_config

# Save to cache
_CACHED_HUNT_MAP = temp_map
_CACHED_HUNT_MTIME = current_mtime
_CACHED_HUNT_PATH = path

return _CACHED_HUNT_MAP, VALIDATORS
except Exception as e:
# Log detailed traceback of corrupted file, but proceed to fallback paths
log.exception("Failed to load hunting configuration from %s: %s", path, e)
has_invalid_syntax = True

# If no configuration file could be loaded successfully
if has_invalid_syntax:
return None, "invalid"
else:
log.error("All hunting configuration lookup paths are missing: %s", lookup_paths)
return None, "missing"
Loading
Loading