Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
112 commits
Select commit Hold shift + click to select a range
4a9b862
feat(opensearch-web): add destination IP filter to query pipeline and…
liamadale Apr 25, 2026
c05bdf6
fix(opensearch-web): skip em-dash placeholder IPs in overview table
liamadale Apr 25, 2026
af6c7ff
feat(opensearch-web): redesign sidebar nav, persist filters per-tab, …
liamadale Apr 25, 2026
67754f1
ci(djlint): add HTML linting to pre-commit and CI pipeline
liamadale Apr 24, 2026
92bfd5b
style(templates): fix all djlint lint errors across 13 HTML files
liamadale Apr 24, 2026
0fca72d
feat(mantis): rewrite escalation detection and surface is_escalated o…
liamadale Apr 29, 2026
8b44a2d
feat(mantis-explorer): add new Flask web app for student activity exp…
liamadale Apr 29, 2026
5b90f0f
feat(hub): register mantis-explorer in run_all.py and hub landing page
liamadale Apr 29, 2026
33603bc
fix(mantis-explorer): add missing TICKETS_BY_ID re-export to data.py
liamadale Apr 29, 2026
0aee678
Merge branch 'main' into dev
liamadale Apr 29, 2026
0c0889e
perf(base): switch bool query to filter context and add 30s timeout
liamadale Apr 30, 2026
e8e7171
test(base): update assertions from bool.must to bool.filter
liamadale Apr 30, 2026
d7a4407
perf(filter-loader): add mtime-based cache to avoid re-parsing YAML o…
liamadale Apr 30, 2026
69d034d
fix(querier): unpack tuple from build_extra_must before passing to bu…
liamadale Apr 30, 2026
e68eb6a
fix(web): log protocol name and error in cross-protocol query excepti…
liamadale Apr 30, 2026
b59ce05
perf(mantis-web): replace linear scan and per-request sorts in data.py
liamadale Apr 30, 2026
80c9ab2
perf(mantis): parallelize index pagination and reuse HTTP sessions
liamadale Apr 30, 2026
1133f9a
test(mantis-web): add TestGetTicketsForIp covering dict-lookup path
liamadale Apr 30, 2026
ab98ecb
feat(dashboard): add Tickets tab and sensor filter modal
liamadale Apr 30, 2026
2ab3d2a
feat(dashboard): wire sensor filter and date controls into toolbar
liamadale Apr 30, 2026
1affe8f
feat(dashboard/opensearch): replace protocol bar with time-series are…
liamadale Apr 30, 2026
55c9c29
refactor(dashboard/opensearch): prune low-signal malcolm panels and u…
liamadale Apr 30, 2026
c35d84f
feat(dashboard/overview): redesign with alert trend chart and triage …
liamadale Apr 30, 2026
a94ddf7
feat(dashboard/mantis): switch to date range filter and drop blocklis…
liamadale Apr 30, 2026
afc7c47
docs(readme): update screenshots and app descriptions for current UI
liamadale Apr 30, 2026
f1902c1
refactor(threat-model): rename mantis_web app to threat_model
liamadale Apr 30, 2026
08808fe
docs(readme): add column headers to documentation table rows
liamadale Apr 30, 2026
b1c6ab3
feat(hub): redesign landing page with list layout and live data fresh…
liamadale Apr 30, 2026
3ab3b8b
chore: remove root-level standalone app launcher shims
liamadale Apr 30, 2026
258e6b9
fix(threat-model): simplify CLI command shown in help popover
liamadale Apr 30, 2026
08fbf98
feat(hub): show version and git update status in footer
liamadale Apr 30, 2026
06708e1
chore: bump version to 1.0.0
liamadale Apr 30, 2026
a2f3cea
feat(mantis-explorer): add warning modal about escalated count accuracy
liamadale Apr 30, 2026
21eef06
feat(shared): add shared static blueprint serving tokens, base CSS, t…
liamadale Apr 30, 2026
326b094
refactor(web): migrate all apps to shared static assets, remove dupli…
liamadale Apr 30, 2026
575a18b
refactor: remove theme toggle buttons from all web app navbars
liamadale Apr 30, 2026
a5676a8
feat: add Hub settings page with theme dropdown
liamadale Apr 30, 2026
afac740
fix: use CSS variables for ECharts colors so charts work on light theme
liamadale Apr 30, 2026
8c4cf0a
feat: add 8 community themes (Gruvbox, Tokyo Night, Catppuccin)
liamadale Apr 30, 2026
34dfbdb
fix: correct surface hierarchy for Catppuccin and Gruvbox themes
liamadale Apr 30, 2026
65f3b4e
fix(mantis-explorer): show notice modal only once per session
liamadale Apr 30, 2026
fa1dfa9
fix(threat-model): show notice modal only once per session
liamadale Apr 30, 2026
56e3467
feat: add notice button to nav bar in mantis explorer and threat model
liamadale Apr 30, 2026
06f4da4
feat(opensearch_web): redesign search bar layout with two-row pill-ba…
liamadale Apr 30, 2026
d328bca
feat(opensearch_web): redesign sensor selector as single clickable bu…
liamadale Apr 30, 2026
9c63aad
refactor: make brand link navigate to hub and remove separate home bu…
liamadale Apr 30, 2026
a4768e5
refactor: rename hub heading to PISCES Toolkit with toolbox icon
liamadale Apr 30, 2026
f8e489e
feat: add author attribution to hub footer
liamadale Apr 30, 2026
40363aa
Merge branch 'main' into dev
liamadale Apr 30, 2026
cb2c356
fix(dashboard): sanitise date query params to prevent reflected XSS
liamadale Apr 30, 2026
8cd887a
fix(dashboard): break CodeQL taint chain by returning parsed date iso…
liamadale Apr 30, 2026
30d9789
feat(correlator): add Phase 1 incident-context orchestrator
liamadale Apr 30, 2026
a7db77b
feat(mcp/opensearch): add investigate tool wrapping Phase 1 orchestrator
liamadale Apr 30, 2026
7cc8439
refactor(correlator): promote auth history and attack chain queries t…
liamadale Apr 30, 2026
d584dba
feat(opensearch_web): add investigate page for IP pair incident context
liamadale Apr 30, 2026
fae7dab
fix(opensearch_web): remove doubled script_name prefix from investiga…
liamadale Apr 30, 2026
69b27fe
feat(opensearch_web): add Investigate entry-points to IP pivot and no…
liamadale Apr 30, 2026
28b6931
test(correlator): expand test suite to 29 tests and add fixtures
liamadale Apr 30, 2026
b5978d0
fix(correlator): parallel profiles, timeline key override, ticket dedup
liamadale Apr 30, 2026
2cad4da
feat(profiler): public IP profiling with sensor presence and reverse DNS
liamadale Apr 30, 2026
674b307
feat(opensearch): public device card, profile buttons, and investigat…
liamadale Apr 30, 2026
32bcde6
feat(opensearch): Suricata alerts and Zeek notices investigate partials
liamadale Apr 30, 2026
88f5205
feat(mcp): profile_device supports public IPs
liamadale Apr 30, 2026
6ea5d1a
test(profiler): public IP profiler unit and integration tests
liamadale Apr 30, 2026
51c66d5
feat(opensearch): escalation indicator on investigate ticket cards
liamadale Apr 30, 2026
693b2c1
feat(opensearch): move Investigate button to global search bar
liamadale Apr 30, 2026
f7946df
style(opensearch): tinted pill styling for src/dst headers in detail …
liamadale Apr 30, 2026
535aaaf
feat(dashboard): add per-sensor log count time series chart
liamadale May 5, 2026
7bf64b9
feat(opensearch): replace auth history with search all logs section
liamadale May 5, 2026
29c17ce
feat(threat-model): add batch private IP profiling pipeline
liamadale May 6, 2026
6948b10
feat(threat-model): display device profiles for internal IPs in web UI
liamadale May 6, 2026
82e4125
refactor(querier): replace silent None returns with typed exceptions
liamadale May 11, 2026
cfc499b
feat(web): surface OpenSearch and Mantis errors in web routes
liamadale May 11, 2026
d80cd57
feat(ui): add error banners for OpenSearch, Mantis, and missing threa…
liamadale May 11, 2026
984a84e
feat(profiler): add conn_state-derived traffic summary to device cards
liamadale May 11, 2026
8acc061
perf(enricher): add persistent HTTP sessions to all enricher clients
liamadale May 12, 2026
50d69a8
feat(enricher): add parallel execution and result caching to web enri…
liamadale May 12, 2026
efe11d5
perf(querier): cache OpenSearch session and optimize query construction
liamadale May 12, 2026
686b013
fix(querier): correct FilesModule IP filter flag and SuricataAlert su…
liamadale May 12, 2026
bcd3860
perf(web): add single-flight dedup, shared thread pool, and ETag support
liamadale May 12, 2026
9431667
feat(web): add src/dest/both IP role toggle to ip_pivot view
liamadale May 12, 2026
8685957
chore: move pytest out of main dependencies into dev dependencies
liamadale May 12, 2026
8826161
fix(web): log exceptions in bare except handlers that were silently s…
liamadale May 12, 2026
4183221
refactor(web): hoist lazy imports, enable static asset caching, remov…
liamadale May 12, 2026
288f7e3
perf(querier): reduce redundant work in filter loading, remapping, an…
liamadale May 12, 2026
a7c457f
perf(enricher): add retry adapter, shared console, and atexit cleanup…
liamadale May 12, 2026
4f43e46
refactor(querier): split monolithic base.py into focused modules
liamadale May 12, 2026
ecadce4
feat(filter-loader): validate category/subcategory against categories…
liamadale May 12, 2026
8ffecb1
chore: add console entry points and httpx/flask[async] dependencies
liamadale May 12, 2026
3e4b8d6
feat(enricher): add prewarm_enrichment_cache for background cache war…
liamadale May 12, 2026
9234e51
perf(web): replace thread-pool fan-out with asyncio.gather on overvie…
liamadale May 12, 2026
729e20b
refactor(mantis): consolidate urllib3 warning suppression into __init__
liamadale May 12, 2026
3af92e8
chore(deps): drop cryptography, move geoip2 to offline-enrichment extra
liamadale May 12, 2026
6adfb2a
fix(mcp/opensearch): push dest_ip filter into ES query instead of pos…
liamadale May 12, 2026
9b5fdf8
feat(mcp/querier): add port/proto filters, multi-value IP/sensor, and…
liamadale May 12, 2026
7dc3a54
feat(histogram): add date-histogram aggregation for event-volume anal…
liamadale May 12, 2026
612d208
refactor(mcp/opensearch): consolidate pivot/profile/investigate and a…
liamadale May 12, 2026
d97ec5e
fix(mcp): rename mcp/ to mcp_servers/ to resolve package namespace co…
liamadale May 12, 2026
91f4e4b
feat(fp-manager): extract delete_ip_from_filter into fp_manager module
liamadale May 12, 2026
b3c21e7
feat(mcp/opensearch): add list_filter_categories, list_fp_filters, de…
liamadale May 12, 2026
f2eff4e
feat(zeek/notice,weird): support ES wildcard queries for notice_note …
liamadale May 12, 2026
8c8ac04
test(zeek/notice,weird): add 9 tests for wildcard vs exact-match quer…
liamadale May 12, 2026
8c45357
feat(mcp/opensearch): add bulk_enrich_ips and count tools; surface tr…
liamadale May 12, 2026
fc103b6
fix(zeek/notice,weird): target .keyword subfield for exact/wildcard m…
liamadale Jun 4, 2026
4ff630b
fix(opensearch): use _source script agg to survive mapping drift
liamadale Jun 14, 2026
d630ade
perf(client): raise HTTP timeout from 30s to 60s
liamadale Jun 14, 2026
6a47411
chore(release): v1.1.0
liamadale Jun 14, 2026
4e95794
Merge branch 'main' into dev for v1.1.0 release
liamadale Jun 14, 2026
9c69e73
fix(release): resolve CI blockers for v1.1.0 PR
liamadale Jun 14, 2026
0ffe591
style(opensearch_web): fix djlint H025/T028 in ip_pivot.html
liamadale Jun 14, 2026
6cce351
fix(fp-manager): inline path-traversal guard so CodeQL sees the barrier
liamadale Jun 14, 2026
7afeb6d
fix(fp-manager): use pathlib.is_relative_to() as path-traversal guard
liamadale Jun 14, 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
6 changes: 3 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@ jobs:
continue-on-error: ${{ env.ADVISORY == 'true' }}

- name: Lint (ruff check)
run: uv run ruff check src/ apps/ mcp/ tests/ *.py
run: uv run ruff check src/ apps/ mcp_servers/ tests/ *.py
continue-on-error: ${{ env.ADVISORY == 'true' }}

- name: Format check (ruff format) — advisory
run: uv run ruff format --check src/ apps/ mcp/ tests/ *.py
run: uv run ruff format --check src/ apps/ mcp_servers/ tests/ *.py
continue-on-error: true

- name: Dependency audit (pip-audit) — advisory
run: uv run pip-audit
continue-on-error: true

- name: SAST (bandit) — advisory
run: uv run bandit -r src/ apps/ mcp/ -c pyproject.toml
run: uv run bandit -r src/ apps/ mcp_servers/ -c pyproject.toml
continue-on-error: true

- name: Lint HTML (djlint check)
Expand Down
128 changes: 128 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Changelog

All notable changes to this project are documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [1.1.0] — 2026-06-14

### Added

- **Incident correlator** — new Phase 1 orchestrator (`src/correlator/`) that assembles
incident context from OpenSearch, Mantis tickets, and enrichment in one call.
Exposed via the MCP `investigate` tool and `/investigate` web pages in
opensearch_web.
- **Public IP profiler** — `profile_device` now supports public IPs, with sensor
presence and reverse DNS surfaced in web device cards.
- **Mantis Explorer** — new Flask app for browsing student activity, registered in
`run_all.py` and the Hub landing page.
- **Hub redesign** — list layout, live data-freshness indicators, settings page with
theme dropdown, version + git update status in the footer.
- **Theme system** — 8 community themes (Gruvbox, Tokyo Night, Catppuccin variants),
CSS-variable-driven ECharts colors so charts render correctly on every theme.
- **Shared static blueprint** — `apps/shared/` serves tokens, base CSS, and logos
across all four web apps; per-app duplicate assets removed.
- **Dashboard** — alert trend chart, triage workqueues, per-sensor log time-series,
Tickets tab, sensor filter modal, date-range controls wired into the toolbar.
- **MCP OpenSearch tools** — `investigate`, `histogram`, `aggregate`,
`bulk_enrich_ips`, `count`, `list_filter_categories`, `list_fp_filters`,
`delete_fp_filter`, `get_notice_summary`, `build_share_urls`,
`compare_to_baseline`, `enrich_top_talkers`.
- **MCP querier** — port/proto filters, multi-value IP/sensor parameters, absolute
timestamps, `truncated` flag surfaced on all search results.
- **Wildcard filters** — `notice_note` and `weird_name` accept ES wildcard syntax,
dispatched to `wildcard` queries with exact-match fallback.
- **Filter loader** — validates category/subcategory pairs against
`filters/categories.yaml`.
- **FP manager** — `delete_ip_from_filter` extracted into reusable module.
- **Investigate UX** — escalation indicators on ticket cards, Investigate entry-points
on IP pivot and notice/Suricata records, public device cards, profile buttons.
- **Web UX** — sidebar nav with per-tab persisted filters, sticky-column rendering,
src/dest/both IP role toggle on ip_pivot, error banners for OpenSearch/Mantis,
destination IP filter in search bar.
- **Enricher** — `prewarm_enrichment_cache` background warmer, parallel execution
and result caching on the web enrich path.

### Changed

- **Querier refactor** — `src/querier/zeek_modules/base.py` split into focused
modules; silent `None` returns replaced with typed exceptions.
- **Web concurrency** — overview route switched from `ThreadPoolExecutor` fan-out
to `asyncio.gather`; single-flight dedup, shared thread pool, and ETag support
added; `bool.must` switched to `bool.filter` context for cacheability.
- **MCP package rename** — `mcp/` → `mcp_servers/` to resolve a namespace collision
with the upstream `mcp` package. **Breaking** for anyone importing the old path.
- **App rename** — `mantis_web` → `threat_model`. **Breaking** for any external
bookmarks or imports referencing the old name.
- **Pivot/profile/investigate** and `aggregate` MCP tools consolidated.
- **Mantis Explorer** — escalation detection rewritten; `is_escalated` surfaced on
tickets; warning modal added about escalated-count accuracy.
- **Enricher clients** — persistent HTTP sessions, retry adapter, shared console,
`atexit` cleanup across all enricher modules.
- **Dashboard / OpenSearch panels** — low-signal Malcolm panels pruned; protocol
bar replaced with time-series area charts; unified to horizontal bars.
- **Hub branding** — heading renamed to "PISCES Toolkit" with toolbox icon; brand
link navigates to hub; redundant home button removed.
- **OpenSearch web** — search bar redesigned as two-row pill layout; sensor
selector reworked as single clickable button; Investigate button moved to the
global search bar; auth history section replaced with search-all-logs.

### Fixed

- **OpenSearch mapping drift** — `terms` aggregations now use a `_source` Painless
script via `source_terms_script()`, surviving indices whose mapping for the
same field disagrees (keyword vs text + `.keyword` subfield on rolled-over
write index).
- **Zeek notice/weird** — exact and wildcard filters now target the `.keyword`
subfield.
- **Dashboard XSS** — date query parameters sanitised; CodeQL taint chain broken
by returning parsed ISO-format date.
- **MCP dest_ip** — pushed into the ES query instead of being post-filtered.
- **Web exceptions** — bare `except` handlers that silently swallowed tracebacks
now log the exception.
- **Cross-protocol query handler** — logs protocol name and error on failure.
- **Querier** — `FilesModule` IP filter flag corrected; `SuricataAlert` summary
type fix; `build_extra_must` tuple correctly unpacked before
`build_base_query`.
- **Correlator** — parallel profile fetches, timeline key override, ticket
deduplication.
- **OpenSearch web** — doubled `script_name` prefix removed from investigate
HTMX paths; em-dash placeholder IPs skipped in overview table.
- **Threat model / Mantis Explorer** — one-time-per-session notice modal.
- **Surface hierarchy** — Catppuccin and Gruvbox themes corrected.

### Performance

- **HTTP timeout** — sync and async OpenSearch clients bumped from 30s → 60s to
accommodate slower script aggregations.
- **OpenSearch client cache** — session reused across queries; query construction
optimised.
- **Filter loader** — mtime-based cache avoids re-parsing YAML on every query.
- **Mantis** — index pagination parallelised; HTTP sessions reused; linear scan
and per-request sorts replaced with dict lookups in `data.py`.
- **Filter loading / remapping / post-filtering** — redundant work removed.

### Removed

- Root-level standalone app launcher shims.
- `cryptography` dependency dropped; `geoip2` moved to the `offline-enrichment`
extra.
- `pytest` moved out of main dependencies into dev dependencies.
- Theme toggle buttons removed from per-app navbars (now centralised in Hub
settings).

### CI

- `djlint` HTML linting added to pre-commit and the CI pipeline.

## [1.0.0] — 2026-XX-XX

Initial tagged release. Dashboard redesign, theming, threat model rename, and
Mantis Explorer (PR #40).

[Unreleased]: https://github.com/liamadale/pisces-scripts/compare/v1.1.0...HEAD
[1.1.0]: https://github.com/liamadale/pisces-scripts/compare/v1.0.0...v1.1.0
[1.0.0]: https://github.com/liamadale/pisces-scripts/releases/tag/v1.0.0
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,8 @@ See [CONTRIBUTING.md](CONTRIBUTING.md) for development guidelines and how to ope

To report a vulnerability, follow the process in [SECURITY.md](SECURITY.md).

Release notes for each version are recorded in [CHANGELOG.md](CHANGELOG.md).

---

## Development Transparency — Use of AI Tooling
Expand Down
2 changes: 2 additions & 0 deletions apps/dashboard_web/opensearch/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
from apps.dashboard_web import cache as dcache
from apps.dashboard_web.opensearch.aggregations import (
agg_conn_volume_over_time,
agg_logs_by_sensor_over_time,
agg_notice_over_time,
agg_opensearch_sensors,
agg_opensearch_top_ips,
Expand Down Expand Up @@ -30,6 +31,7 @@ def section():
"conn_over_time": agg_conn_volume_over_time(time_range, sensors),
"sensors": agg_opensearch_sensors(time_range),
"top_ips": agg_opensearch_top_ips(time_range, sensors),
"sensor_trend": agg_logs_by_sensor_over_time(time_range, sensors),
}
except Exception as exc:
data = {"error": str(exc)}
Expand Down
86 changes: 79 additions & 7 deletions apps/dashboard_web/opensearch/aggregations.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,22 @@

from src.querier.zeek_modules.base import (
FILTERS_DIR,
OpenSearchAuthError,
OpenSearchConnectionError,
build_base_query,
load_with_remap,
query_opensearch,
)


def _safe_query(body: dict, params: dict) -> dict | None:
"""query_opensearch wrapper that returns None on connectivity/auth errors."""
try:
return query_opensearch(body, params)
except (OpenSearchConnectionError, OpenSearchAuthError):
return None


def parse_sensors(raw: str) -> list | None:
"""Parse a comma-separated sensor string into a list, or None for 'all'."""
if not raw or raw.strip().lower() == "all":
Expand Down Expand Up @@ -53,7 +63,7 @@ def agg_opensearch_sensors(time_range: str) -> dict:
}
}
}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
buckets = raw.get("aggregations", {}).get("sensors", {}).get("buckets", []) if raw else []
return {
"labels": [b["key"] for b in buckets],
Expand Down Expand Up @@ -81,7 +91,7 @@ def agg_opensearch_notice_count(time_range: str, sensors: list | None = None) ->
body["size"] = 0
body.pop("sort", None)
body.pop("_source", None)
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
if not raw:
return 0
return raw.get("hits", {}).get("total", {}).get("value", 0)
Expand Down Expand Up @@ -127,7 +137,7 @@ def agg_suricata_alert_count(time_range: str, sensors: list | None = None) -> in
body["size"] = 0
body.pop("sort", None)
body.pop("_source", None)
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
if not raw:
return 0
return raw.get("hits", {}).get("total", {}).get("value", 0)
Expand Down Expand Up @@ -160,7 +170,7 @@ def agg_suricata_over_time(time_range: str, sensors: list | None = None) -> dict
}
}
}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
buckets = raw.get("aggregations", {}).get("over_time", {}).get("buckets", []) if raw else []
return {
"timestamps": [b["key_as_string"] for b in buckets],
Expand Down Expand Up @@ -199,7 +209,7 @@ def agg_notice_over_time(time_range: str, sensors: list | None = None) -> dict:
}
}
}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
buckets = raw.get("aggregations", {}).get("over_time", {}).get("buckets", []) if raw else []
return {
"timestamps": [b["key_as_string"] for b in buckets],
Expand Down Expand Up @@ -235,7 +245,7 @@ def agg_conn_volume_over_time(time_range: str, sensors: list | None = None) -> d
}
}
}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
buckets = raw.get("aggregations", {}).get("over_time", {}).get("buckets", []) if raw else []
return {
"timestamps": [b["key_as_string"] for b in buckets],
Expand All @@ -244,6 +254,68 @@ def agg_conn_volume_over_time(time_range: str, sensors: list | None = None) -> d
}


def agg_logs_by_sensor_over_time(time_range: str, sensors: list | None = None) -> dict:
"""Total log count per sensor as aligned time series (terms → date_histogram)."""
interval = _interval_for_range(time_range)
body, params = build_base_query(
must_not=[],
extra_must=[],
source_fields=[],
limit=0,
time_range=time_range,
sensors=sensors,
datasets=["all"],
public_only=False,
src_ip_filter=None,
direction=None,
)
body["size"] = 0
body.pop("sort", None)
body.pop("_source", None)
body["aggs"] = {
"by_sensor": {
"terms": {"field": "host.name", "size": 50, "order": {"_count": "desc"}},
"aggs": {
"over_time": {
"date_histogram": {
"field": "@timestamp",
"fixed_interval": interval,
"min_doc_count": 0,
}
}
},
}
}
raw = _safe_query(body, params)
sensor_buckets = (
raw.get("aggregations", {}).get("by_sensor", {}).get("buckets", []) if raw else []
)

# Collect all unique timestamps in order across all sensors
all_ts: dict[str, None] = {}
for sb in sensor_buckets:
for tb in sb.get("over_time", {}).get("buckets", []):
all_ts[tb["key_as_string"]] = None
timestamps = list(all_ts.keys())

# Build per-sensor series aligned to the shared timestamp list
series = []
for sb in sensor_buckets:
ts_map = {
tb["key_as_string"]: tb["doc_count"]
for tb in sb.get("over_time", {}).get("buckets", [])
}
series.append(
{
"sensor": sb["key"],
"counts": [ts_map.get(t, 0) for t in timestamps],
"total": sb["doc_count"],
}
)

return {"timestamps": timestamps, "series": series, "interval": interval}


def agg_new_ips_delta(time_range: str, sensors: list | None = None) -> dict:
"""Compare unique source IPs in the current window vs the previous window.

Expand All @@ -270,7 +342,7 @@ def _unique_count(tr: str) -> int:
body["aggs"] = {
"uniq": {"cardinality": {"field": "source.ip", "precision_threshold": 3000}}
}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
if not raw:
return 0
return raw.get("aggregations", {}).get("uniq", {}).get("value", 0)
Expand Down
19 changes: 16 additions & 3 deletions apps/dashboard_web/opensearch/malcolm.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,20 @@

import concurrent.futures

from src.querier.zeek_modules.base import build_base_query, query_opensearch
from src.querier.zeek_modules.base import (
OpenSearchAuthError,
OpenSearchConnectionError,
build_base_query,
query_opensearch,
)


def _safe_query(body: dict, params: dict) -> dict | None:
"""query_opensearch wrapper that returns None on connectivity/auth errors."""
try:
return query_opensearch(body, params)
except (OpenSearchConnectionError, OpenSearchAuthError):
return None


def _terms(field: str, time_range: str, datasets: list, size: int = 20) -> dict:
Expand All @@ -27,7 +40,7 @@ def _terms(field: str, time_range: str, datasets: list, size: int = 20) -> dict:
body.pop("sort", None)
body.pop("_source", None)
body["aggs"] = {"r": {"terms": {"field": field, "size": size, "order": {"_count": "desc"}}}}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
buckets = raw.get("aggregations", {}).get("r", {}).get("buckets", []) if raw else []
return {
"labels": [b["key"] for b in buckets],
Expand Down Expand Up @@ -60,7 +73,7 @@ def _sum_terms(
"aggs": {"total": {"sum": {"field": sum_field}}},
}
}
raw = query_opensearch(body, params)
raw = _safe_query(body, params)
buckets = raw.get("aggregations", {}).get("r", {}).get("buckets", []) if raw else []
buckets = sorted(buckets, key=lambda b: -b.get("total", {}).get("value", 0))
return {
Expand Down
Loading
Loading