Skip to content

Commit 6ba6e0b

Browse files
committed
feat(opensearch-web): replace top nav with collapsible sidebar, extend to all 25 modules
Replace the flat horizontal nav bar with a collapsible left sidebar grouped by protocol category (Core, Files & Certs, Authentication, Infrastructure, OT/SCADA, Diagnostic). Group expand/collapse state is persisted in localStorage; the sidebar itself can be collapsed to icon- only mode with tooltip fallback. Overview matrix gains collapsible column groups per category (non-core groups start collapsed). Modules without a src_ip field (pe, capture_loss) are excluded from the matrix and from IP pivot queries — run_cross_protocol_query and the ip_pivot route now check SUPPORTS_IP_FILTER before including a module. Template rendering for log_view, log_rows, and ip_pivot is now driven by WEB_COLUMNS instead of per-protocol if/elif chains, making new modules automatically appear without template edits. record_detail conditionally hides the FP filter button when SUPPORTS_FP=False. MODULE_PARAM_KEYS is extended for all 15 new modules. proto_icons and category_icons are lifted out of base.html into an inject_nav_data context processor in app.py so they are available to all templates. - Fix WEB_CATEGORY on FTP module: "core" → "infrastructure"
1 parent b2f212b commit 6ba6e0b

40 files changed

Lines changed: 5075 additions & 331 deletions

apps/opensearch_web/app.py

Lines changed: 64 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,12 @@
1515
make_mantis_blueprint,
1616
)
1717
from apps.shared.jinja_globals import register_shared_helpers
18-
from src.querier.zeek_modules import MODULES
18+
from src.querier.zeek_modules import (
19+
CATEGORY_LABELS,
20+
CATEGORY_ORDER,
21+
MODULES,
22+
MODULES_BY_CATEGORY,
23+
)
1924
from src.querier.zeek_modules.base import TIME_RANGES
2025
from src.utils.format import fmt_dur
2126

@@ -39,27 +44,75 @@ def create_app() -> Flask:
3944
app.register_blueprint(make_mantis_blueprint(_resolve_city))
4045
app.register_blueprint(make_cache_blueprint(wcache))
4146

42-
# Make TIME_RANGES and MODULES available to all templates
47+
# Make TIME_RANGES, MODULES, and nav data available to all templates
4348
@app.context_processor
44-
def inject_globals():
49+
def inject_globals() -> dict:
4550
return {
4651
"TIME_RANGES": TIME_RANGES,
47-
"MODULES": list(MODULES.keys()),
52+
"MODULES": MODULES,
4853
"script_name": request.environ.get("SCRIPT_NAME", ""),
4954
}
5055

56+
@app.context_processor
57+
def inject_nav_data() -> dict:
58+
return {
59+
"proto_icons": {
60+
"conn": "fa-network-wired",
61+
"dns": "fa-server",
62+
"http": "fa-globe",
63+
"ssl": "fa-lock",
64+
"smtp": "fa-envelope",
65+
"rdp": "fa-desktop",
66+
"smb": "fa-folder-open",
67+
"ssh": "fa-terminal",
68+
"notice": "fa-bell",
69+
"weird": "fa-triangle-exclamation",
70+
"files": "fa-file",
71+
"x509": "fa-certificate",
72+
"pe": "fa-file-code",
73+
"kerberos": "fa-key",
74+
"ntlm": "fa-user-lock",
75+
"dhcp": "fa-address-card",
76+
"ftp": "fa-file-arrow-up",
77+
"radius": "fa-wifi",
78+
"sip": "fa-phone",
79+
"tunnel": "fa-circle-nodes",
80+
"ntp": "fa-clock",
81+
"modbus": "fa-microchip",
82+
"dnp3": "fa-bolt",
83+
"capture_loss": "fa-gauge",
84+
"dpd": "fa-circle-question",
85+
},
86+
"category_icons": {
87+
"core": "fa-layer-group",
88+
"files": "fa-folder",
89+
"auth": "fa-shield-halved",
90+
"infrastructure": "fa-sitemap",
91+
"ot": "fa-industry",
92+
"diagnostic": "fa-stethoscope",
93+
},
94+
"category_order": CATEGORY_ORDER,
95+
"category_labels": CATEGORY_LABELS,
96+
"modules_by_category": MODULES_BY_CATEGORY,
97+
}
98+
5199
# ------------------------------------------------------------------
52100
# GET / — cross-protocol IP matrix
53101
# ------------------------------------------------------------------
54102
@app.route("/")
55103
def overview():
56104
search_params = build_search_params_from_request(request)
57105
rows = run_cross_protocol_query(search_params)
106+
# Build per-category module lists, excluding non-IP modules (pe, capture_loss)
107+
ip_modules_by_category = {
108+
cat: [lt for lt in lts if MODULES[lt].SUPPORTS_IP_FILTER]
109+
for cat, lts in MODULES_BY_CATEGORY.items()
110+
}
58111
return render_template(
59112
"overview.html",
60113
rows=rows,
61114
search_params=search_params,
62-
log_types=list(MODULES.keys()),
115+
ip_modules_by_category=ip_modules_by_category,
63116
)
64117

65118
# ------------------------------------------------------------------
@@ -71,7 +124,9 @@ def ip_pivot(ip: str):
71124
search_params["src_ip"] = ip
72125

73126
results: dict = {}
74-
for lt in MODULES:
127+
for lt, mod in MODULES.items():
128+
if not mod.SUPPORTS_IP_FILTER:
129+
continue # pe, capture_loss have no src_ip to pivot on
75130
sp = dict(search_params)
76131
results[lt] = cached_run_query(lt, sp)
77132

@@ -80,7 +135,6 @@ def ip_pivot(ip: str):
80135
ip=ip,
81136
results=results,
82137
search_params=search_params,
83-
log_types=list(MODULES.keys()),
84138
MODULE_PARAM_KEYS=MODULE_PARAM_KEYS,
85139
)
86140

@@ -102,6 +156,7 @@ def log_view(log_type: str):
102156
search_params=search_params,
103157
extra_keys=extra_keys,
104158
detail_fields=mod.DETAIL_FIELDS,
159+
web_columns=mod.WEB_COLUMNS,
105160
)
106161

107162
# ------------------------------------------------------------------
@@ -120,6 +175,7 @@ def api_search(log_type: str):
120175
log_type=log_type,
121176
records=records,
122177
detail_fields=mod.DETAIL_FIELDS,
178+
web_columns=mod.WEB_COLUMNS,
123179
)
124180

125181
# ------------------------------------------------------------------
@@ -142,6 +198,7 @@ def api_detail(log_type: str, i: int):
142198
detail_fields=mod.DETAIL_FIELDS,
143199
idx=i,
144200
log_type=log_type,
201+
supports_fp=mod.SUPPORTS_FP,
145202
)
146203

147204
# ------------------------------------------------------------------

apps/opensearch_web/queries.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99

1010
# Protocol-specific search_params keys forwarded from HTTP request
1111
MODULE_PARAM_KEYS: dict = {
12+
# Core
1213
"conn": [],
1314
"dns": ["dns_query", "rcode", "qtype"],
1415
"http": ["http_method", "http_host", "http_uri", "status_code"],
@@ -19,6 +20,26 @@
1920
"ssh": ["ssh_failed_only", "ssh_auth_result"],
2021
"notice": ["notice_note"],
2122
"weird": ["weird_name"],
23+
# Files & Certs
24+
"files": ["mime", "hash", "source_proto", "extracted_only", "dest_ip"],
25+
"x509": ["subject", "issuer", "san", "self_signed", "expired"],
26+
"pe": ["no_aslr", "no_dep", "only_32bit"],
27+
# Authentication
28+
"kerberos": ["client", "service", "request_type", "cipher", "failed_only"],
29+
"ntlm": ["username", "domain", "failed_only"],
30+
"radius": ["username", "mac", "failed_only"],
31+
# Infrastructure
32+
"dhcp": ["hostname", "mac", "assigned_ip"],
33+
"ftp": ["user", "command", "reply_code", "anon_only"],
34+
"sip": ["method", "status_code", "user_agent"],
35+
"ntp": ["mode", "version"],
36+
"tunnel": ["tunnel_type"],
37+
# OT / SCADA
38+
"modbus": ["function", "exceptions_only"],
39+
"dnp3": ["function"],
40+
# Diagnostic
41+
"capture_loss": [],
42+
"dpd": ["analyzer"],
2243
}
2344

2445

@@ -51,18 +72,23 @@ def cached_run_query(log_type: str, search_params: dict) -> list:
5172

5273

5374
def run_cross_protocol_query(search_params: dict) -> list:
54-
"""Query all log types in parallel, aggregate by src_ip, sort by total freq."""
75+
"""Query all IP-capable log types in parallel, aggregate by src_ip, sort by total freq.
76+
77+
Modules with SUPPORTS_IP_FILTER=False (pe, capture_loss) are excluded — they have no
78+
src_ip to aggregate on.
79+
"""
80+
ip_modules = {lt: mod for lt, mod in MODULES.items() if mod.SUPPORTS_IP_FILTER}
5581
results_by_type: dict = {}
56-
with ThreadPoolExecutor(max_workers=len(MODULES)) as ex:
57-
futures = {ex.submit(cached_run_query, lt, search_params): lt for lt in MODULES}
82+
with ThreadPoolExecutor(max_workers=len(ip_modules)) as ex:
83+
futures = {ex.submit(cached_run_query, lt, search_params): lt for lt in ip_modules}
5884
for f in as_completed(futures):
5985
lt = futures[f]
6086
try:
6187
results_by_type[lt] = f.result()
6288
except Exception:
6389
results_by_type[lt] = []
6490

65-
ip_data: dict = defaultdict(lambda: {"per_protocol": {lt: 0 for lt in MODULES}, "total": 0})
91+
ip_data: dict = defaultdict(lambda: {"per_protocol": {lt: 0 for lt in ip_modules}, "total": 0})
6692
for lt, records in results_by_type.items():
6793
for rec in records:
6894
ip = rec.get("src_ip", "")

0 commit comments

Comments
 (0)