Skip to content

Commit 1cbb16f

Browse files
committed
chore(release): v1.0.7 - Security Hardening & Intelligence Accuracy
1 parent 8d5dd85 commit 1cbb16f

13 files changed

Lines changed: 461 additions & 43 deletions

CHANGELOG.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,31 @@
22

33
## [Unreleased]
44

5+
## [1.0.7] - 2026-04-18
6+
7+
### Security
8+
9+
- **SSRF Fix in News Article Extraction** — Hardened the `/api/news/article` reader endpoint against Server-Side Request Forgery; private/loopback addresses and non-HTTP schemes are now blocked before fetch.
10+
11+
### Fixed
12+
13+
- **AI Model Overload Advisory** — Domain agents (air, sea, orbital) now surface an amber `Model Overloaded` notice in the Analyst Panel when the LLM provider returns `503 / high demand` instead of silently showing heuristic fallback text.
14+
- **SatNOGS Timeout Resilience** — Added one retry for transient SatNOGS station fetch timeouts; timeout failures are now distinguished from broader network errors in the degraded-response metadata.
15+
- **News Feed Noise** — Removed DefenseNews from the default RSS feed set (intermittent `42` non-standard HTTP status); it remains configurable via env.
16+
- **Semantic Cache Warning Clarity** — Corrected the `redisvl` runtime-missing warning to avoid incorrectly instructing operators to edit `pyproject.toml`.
17+
- **HTTPS Reverse-Proxy WebSocket Fix** — Added a shared frontend URL resolver that promotes `ws://`/`http://` build-time endpoints to same-origin wss/https paths when served behind TLS, eliminating mixed-content WebSocket failures.
18+
- **WebSDR Mixed-Content Iframe** — WebSDR receiver iframes are now HTTPS-upgraded when discovered via an `http://` directory entry; the original URL is preserved as an external fallback link.
19+
- **FIRMS Poller Alias & Cooldown Loop** — Normalized the legacy `VIIRS_SNPP_NR` source alias to prevent repeated `400` errors; fixed the empty-cache cooldown-bypass guard to prevent tight re-poll loops when upstream returns no hotspots.
20+
- **FIRMS Test Stale Date** — Replaced the hardcoded `acq_date` fixture in the FIRMS router test with a dynamically computed value so the test never expires.
21+
- **Linkage Audit Form Accessibility** — Added explicit `htmlFor`/`id` attribute pairs to all `LinkageAuditView` form inputs.
22+
23+
### Changed
24+
25+
- **Active Conflict Zone Recalibration** — Recalibrated threat-score thresholds to match real-world GDELT distributions (CRITICAL ≤ −4.5, ELEVATED ≤ −2.0). Added a material-conflict volume shortcut: actors with >150 kinetic events promoted to CRITICAL; >50 promotes MONITORING to ELEVATED. The Intel Globe conflict zone panel now includes MONITORING-level actors with a WATCH badge.
26+
- **GDELT Conflict Filter & ReliefWeb Pulse Config**`gdelt_pulse` now exposes configurable conflict-filter keywords and `ReliefWeb` appname via environment variables; GDELT event logging enhanced with structured metadata.
27+
- **Frontend Static Nginx Cache**`frontend/nginx-spa.conf` no longer caches `index.html`, reducing stale-bundle issues behind CDNs and reverse proxies after deploys.
28+
- **Deck.gl Text Character Sets** — Explicit character sets added for label layers that render the middle-dot separator, eliminating missing-glyph console warnings.
29+
530
## [1.0.6] - 2026-04-15
631

732
### Added

RELEASE_NOTES.md

Lines changed: 73 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,47 +1,91 @@
1-
# Release - v1.0.6 - Thermal Watch
1+
# Release — v1.0.7 — Security Hardening & Intelligence Accuracy
2+
3+
**Released:** 2026-04-18
4+
**Type:** Patch
5+
**Previous:** v1.0.6
6+
7+
---
28

39
## Summary
410

5-
v1.0.6 delivers the FIRMS thermal intelligence layer and dark-vessel anomaly engine to operators, resolves the two hold items that blocked the v1.0.6 candidate, and closes out a sweep of backend ingestion reliability fixes. The ISS ground-track now updates live in the animation loop and renders cleanly after container restarts. Fire hotspot data is now sourced globally by design, while the thermal layer starts off by default to reduce initial map noise.
11+
v1.0.7 ships a high-priority security patch alongside a set of runtime stability fixes and a significant accuracy improvement to the Active Conflict Zone intelligence engine. Operators on HTTPS deployments will also see mixed-content WebSocket and WebSDR iframe errors resolved.
12+
13+
---
14+
15+
## Key Changes
16+
17+
### 🛡️ Security
18+
19+
- **SSRF Fix — News Article Reader** *(HIGH)*
20+
The `/api/news/article` reader endpoint now blocks requests to private/loopback addresses (`127.0.0.1`, `192.168.x.x`, `169.254.x.x`, etc.) and non-HTTP schemes before making any outbound connection. This closes a Server-Side Request Forgery vector that could have allowed authenticated operators to probe internal services.
21+
22+
---
23+
24+
### 🎯 Active Conflict Zone Accuracy
25+
26+
The ACTIVE CONFLICT ZONES panel previously showed `[0]` almost always, even with active warzones (Ukraine, Israel, Sudan) appearing in the actor feed. Root cause: the old Goldstein thresholds were calibrated against an idealised model rather than real GDELT data distributions.
627

7-
## Key Features
28+
**What changed:**
29+
- CRITICAL threshold: `≤ −6.0`**`≤ −4.5`** (Ukraine/Gaza cluster at −3 to −5)
30+
- ELEVATED threshold: `≤ −3.0`**`≤ −2.0`**
31+
- New **material-conflict volume shortcut**: actors with >150 kinetic events → CRITICAL; >50 → at least ELEVATED (prevents dilution by high diplomatic-media volume)
32+
- **MONITORING-level actors** now appear in the conflict zone panel with a yellow `WATCH` badge, so operators see the full threat spectrum instead of just the top tier
833

9-
- **NRT NASA FIRMS Thermal Layer** — VIIRS/MODIS thermal infrared hotspots on the tactical map with FRP-scaled radii and confidence-coded colors.
10-
- **Dark Vessel Anomaly Detection** — Backend engine cross-references FIRMS heat signatures against AIS vessel positions. Vessel-scale hotspots with no AIS broadcast within 5nm/2h are surfaced as anomaly candidates.
11-
- **FIRMS Source Health Visibility** — Operations surfaces now show compact per-source FIRMS ingest summaries so NOAA-20, NOAA-21, and SNPP drift can be diagnosed without container log inspection.
12-
- **Live ISS Ground Track** — WebSocket-delivered positions now normalise Unix integer timestamps to ISO-8601 on arrival, keeping the orbital trail current in the animation loop.
13-
- **ISS Track Gap Detection** — Consecutive positions separated by >10 minutes start a new path segment, eliminating the distorted line artifact after container restarts.
34+
---
1435

15-
## Bug Fixes
36+
### 🔧 Runtime Stability
1637

17-
- FIRMS thermal visibility now defaults to `false`, while dark-vessel anomalies remain enabled and FIRMS coverage stays global.
18-
- FIRMS poller health now distinguishes healthy, empty, and failed upstream source responses instead of silently reporting partial upstream degradation as healthy.
19-
- ISS live-track timestamps normalised at the hook boundary (`useISSTracker`) so all sources (WebSocket, REST, DB) produce trail points that pass the `buildISSLayer` filter.
20-
- ISS track gap rendering fixed in `splitTrackAtAntimeridian`.
21-
- SatNOGS pagination loop indentation corrected; PeeringDB IXP coordinates recovered from facility centroids; IODA Nominatim geocoder restored.
38+
- **AI Model Overload Advisory** — Analyst Panel now shows an amber "Model Overloaded" notice instead of silently presenting heuristic fallback text when the LLM provider returns `503 / high demand`.
39+
- **SatNOGS Timeout Resilience** — One retry added for transient station fetch timeouts; timeout vs. network failures now distinguished in response metadata.
40+
- **HTTPS WebSocket Fix** — Shared URL resolver promotes `ws://`/`http://` build-time endpoints to same-origin secure paths when the app is served behind TLS. Eliminates mixed-content failures on HTTPS deployments.
41+
- **WebSDR HTTPS Iframe** — WebSDR receiver iframes are now HTTPS-upgraded on secure pages; original HTTP URL preserved as an external link fallback.
42+
- **FIRMS Poller** — Normalized legacy `VIIRS_SNPP_NR` alias; fixed empty-cache cooldown guard that caused tight re-poll loops.
43+
- **News Feed** — DefenseNews removed from the default feed set (intermittent non-standard HTTP status).
2244

23-
## Technical Details
45+
---
2446

25-
- TimescaleDB migration **V004** adds `firms_hotspots` hypertable and `dark_vessel_candidates` table.
26-
- `space_pulse` cadence policy: daily TLE refresh gated on UTC hour, FIRMS/space-weather cadence persisted in Redis across restarts.
27-
- FIRMS no longer supports a mission-vs-global ingest mode; the poller always uses the NASA world endpoint and `FIRMS_BBOX_MODE` is no longer part of the runtime configuration.
28-
- FIRMS source-cycle snapshots are now published to Redis under `firms:source_status` and surfaced through `/api/config/poller-health`.
29-
- No breaking API or schema changes, no new required environment variables.
47+
### ⚙️ Configuration
48+
49+
- **GDELT Conflict Keywords** — Now configurable via `GDELT_CONFLICT_KEYWORDS` env var without a code change.
50+
- **ReliefWeb App Name** — Now configurable via `RELIEFWEB_APPNAME` env var.
51+
- **nginx-spa.conf**`index.html` is no longer cached, reducing stale-bundle issues after deploys behind CDNs.
52+
53+
---
54+
55+
## Technical Notes
56+
57+
- No database migrations — no schema changes in this release.
58+
- No new environment variables are mandatory; all new vars have sensible defaults matching previous behaviour.
59+
- The `sovereign-backend` container should be rebuilt to pick up AI overload handling, SatNOGS, and news changes.
60+
- The `sovereign-gdelt-pulse` container should be rebuilt to pick up the conflict-filter and ReliefWeb config changes.
61+
- The `sovereign-space-pulse` container should be rebuilt to pick up the FIRMS poller alias fix.
62+
- Frontend: no rebuild required for already-deployed prod builds if served via nginx (nginx-spa.conf change only affects cache headers, not functionality).
63+
64+
---
3065

3166
## Verification
3267

33-
- Frontend: `pnpm run lint` ✅ · `pnpm run typecheck` ✅ · `pnpm run test` ✅ (18/18 files, 268/268 tests)
34-
- Backend API: `uv tool run ruff check . && uv run python -m pytest tests/test_firms_router.py tests/test_system_router.py` ✅ (7 targeted tests)
35-
- Targeted poller verification: `space_pulse` lint ✅; host `space_pulse` pytest remains blocked by the known Windows/Python 3.14 `asyncpg==0.30.0` build issue.
68+
| Suite | Result |
69+
|-------|--------|
70+
| Frontend lint | ✅ Clean |
71+
| Frontend typecheck | ✅ Clean |
72+
| Frontend tests | ✅ 272/272 |
73+
| Backend API lint | ✅ Clean |
74+
| Backend API tests | ✅ 152/152 |
75+
| GDELT threshold tests | ✅ 11/11 (6 new) |
76+
77+
---
3678

3779
## Upgrade Instructions
3880

3981
```bash
40-
git pull origin main
41-
docker compose pull
42-
docker compose up -d --build
43-
```
82+
git pull origin dev
4483

45-
TimescaleDB migration V004 runs automatically on backend startup.
84+
# Rebuild affected services
85+
docker compose up -d --build sovereign-backend sovereign-gdelt-pulse sovereign-space-pulse
86+
87+
# Frontend (if running prod static build)
88+
docker compose up -d --build sovereign-frontend
89+
```
4690

47-
**SITREP Status: [GO]**
91+
No `docker compose down` required. No migrations to run.
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Fix: SSRF Vulnerability in News Article Extraction
2+
3+
## Issue
4+
5+
The `/api/news/article` reader endpoint made outbound HTTP requests to arbitrary URLs supplied by the frontend. An authenticated operator could craft a request URL pointing at internal services (e.g., `http://localhost/`, `http://169.254.169.254/` metadata endpoints, `http://redis:6379/`) and receive their content, constituting a Server-Side Request Forgery (SSRF) vulnerability rated HIGH.
6+
7+
## Solution
8+
9+
Added a blocklist validator that rejects requests before any socket connection is opened. The validator refuses:
10+
- Non-HTTP/HTTPS schemes (`file://`, `ftp://`, `gopher://`, etc.)
11+
- Loopback addresses (`127.0.0.0/8`, `::1`)
12+
- Link-local / metadata ranges (`169.254.0.0/16`, `fe80::/10`)
13+
- RFC-1918 private ranges (`10.0.0.0/8`, `172.16.0.0/12`, `192.168.0.0/16`)
14+
- Unspecified (`0.0.0.0`, `::`)
15+
16+
## Changes
17+
18+
| File | Change |
19+
|------|--------|
20+
| `backend/api/routers/news.py` | Added `_validate_url()` guard called before `httpx` fetch in the article reader route; returns `400 Bad Request` on blocked targets. |
21+
22+
## Verification
23+
24+
- `cd backend/api && uv tool run ruff check .` — passed (clean).
25+
- `cd backend/api && uv run python -m pytest tests/test_news_router.py` — passed.
26+
- Manual review: private IP and `file://` URLs return `400`; public URLs proceed normally.
27+
28+
## Benefits
29+
30+
- Eliminates SSRF attack surface in the news article extraction path.
31+
- Fails closed (400) rather than proxying the disallowed request.
32+
- Validator is a pure-Python stdlib function — no new dependencies.
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
# Linkage Audit View Form Accessibility
2+
3+
## Issue
4+
5+
The `LinkageAuditView` form inputs lacked explicit `htmlFor` / `id` attribute pairs, failing accessibility linting rules (`jsx-a11y/label-has-associated-control`) and preventing screen readers from associating labels with their controls.
6+
7+
## Solution
8+
9+
Added matching `id` attributes to every controlled input and `htmlFor` attributes to their corresponding labels.
10+
11+
## Changes
12+
13+
| File | Change |
14+
|------|--------|
15+
| `frontend/src/components/views/LinkageAuditView.tsx` | Added `id` and `htmlFor` pairs to all form label/input elements. |
16+
17+
## Verification
18+
19+
- `cd frontend && pnpm run lint` — exit 0 (previously flagged `jsx-a11y` warnings).
20+
- `cd frontend && pnpm run typecheck` — passed.
21+
- `cd frontend && pnpm run test` — all tests passed.
22+
23+
## Benefits
24+
25+
- Screen readers can now correctly associate labels with form controls in the diagnostic view.
26+
- Satisfies `jsx-a11y/label-has-associated-control` for CI lint gate.
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
# GDELT Conflict Filter & ReliefWeb Pulse Configuration
2+
3+
## Issue
4+
5+
The `gdelt_pulse` service had hardcoded conflict-filter keywords and a hardcoded ReliefWeb `appname` query parameter, making it impossible to tune the filter without a code change and container rebuild. Additionally, GDELT event ingestion lacked structured metadata in its log output, making it harder to diagnose filter behavior in production.
6+
7+
## Solution
8+
9+
Exposed conflict-filter keyword list and ReliefWeb `appname` as environment variables (`GDELT_CONFLICT_KEYWORDS`, `RELIEFWEB_APPNAME`), defaulting to the previous hardcoded values so existing deployments are unaffected. Enhanced the event-logging path with structured per-event metadata for better observability.
10+
11+
## Changes
12+
13+
| File | Change |
14+
|------|--------|
15+
| `backend/ingestion/gdelt_pulse/service.py` | Read `GDELT_CONFLICT_KEYWORDS` and `RELIEFWEB_APPNAME` from env; pass to filter/fetch helpers; add structured logging fields per ingested event. |
16+
| `backend/api/routers/gdelt.py` | Minor logging enhancement to surface filter metadata in API layer logs. |
17+
| `.env.example` | Added `GDELT_CONFLICT_KEYWORDS` and `RELIEFWEB_APPNAME` documentation entries. |
18+
19+
## Verification
20+
21+
- Docker: `docker compose up -d --build sovereign-gdelt-pulse` — container starts, poll cycle logs structured events.
22+
- Ruff: `uv tool run ruff check backend/ingestion/gdelt_pulse/` — clean.
23+
24+
## Benefits
25+
26+
- Operators can tune GDELT conflict keyword granularity without a code change.
27+
- ReliefWeb `appname` is now a documented, overridable config value.
28+
- Structured logging enables per-event tracing in production log aggregation tools.
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# Fix: FIRMS Test Stale Hardcoded Date
2+
3+
## Issue
4+
5+
The CI PR run was failing with:
6+
7+
```
8+
FAILED tests/test_firms_router.py::test_parse_firms_csv_to_rows_filters_and_normalizes_live_world_feed
9+
assert 0 == 1
10+
```
11+
12+
`_parse_firms_csv_to_rows` filters rows by comparing `acq_dt` against
13+
`datetime.now(UTC) - timedelta(hours=hours_back)`. The test fixture used a
14+
hardcoded `acq_date` of `2026-04-14`, which was written within the 72-hour
15+
window at the time of authoring but naturally expired as calendar time advanced
16+
past `2026-04-17` (72 h later). Every row was silently dropped by the cutoff
17+
filter, returning an empty list and causing `assert len(rows) == 1` to fail.
18+
19+
## Solution
20+
21+
Replace the hardcoded `acq_date`/`acq_time` values in the CSV fixture with
22+
values derived from `datetime.now(UTC) - timedelta(hours=1)`. This keeps the
23+
fixture data perpetually one hour old — always well inside any `hours_back`
24+
window the test might use — without requiring any changes to the production
25+
router code.
26+
27+
## Changes
28+
29+
| File | Change |
30+
|------|--------|
31+
| `backend/api/tests/test_firms_router.py` | Added `from datetime import UTC, datetime, timedelta` import; replaced hardcoded `2026-04-14` date/time strings with dynamically computed values 1 hour before test execution. |
32+
33+
## Verification
34+
35+
```
36+
cd backend/api && uv run python -m pytest tests/test_firms_router.py -v
37+
```
38+
39+
All 5 tests in the file pass. No router production code was changed.
40+
41+
## Benefits
42+
43+
- **Stability**: The test is now date-agnostic and will never fail due to the
44+
fixture going stale.
45+
- **Correctness**: The fixture accurately models "recent" FIRMS data, which is
46+
the scenario the test was always intended to cover.
Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
# Recalibrate Active Conflict Zone Detection
2+
3+
## Issue
4+
5+
The ACTIVE CONFLICT ZONES panel consistently showed `[0]` even with ISR, RUS, IND, and UKR all active and carrying amber threat indicators. Three root causes:
6+
7+
1. **CRITICAL threshold too conservative** — the old gate (`threat_score ≤ -6.0`) required both a severely negative Goldstein average *and* a high material-conflict ratio. Real-world active warzones (Ukraine, Gaza) typically cluster in the −3 to −5 Goldstein band under GDELT and would never trigger it.
8+
2. **No kinetic-volume bypass** — a country with 200 material conflict events over a quiet news cycle could still be classified as MONITORING if its average Goldstein was diluted by non-conflict articles.
9+
3. **Conflict zone panel excluded MONITORING actors** — the frontend filter only showed CRITICAL and ELEVATED actors, meaning even correctly-classified MONITORING conflict zones were invisible in that panel.
10+
11+
## Solution
12+
13+
Three coordinated changes:
14+
15+
1. **Recalibrated Goldstein thresholds** to match observed GDELT distributions:
16+
- CRITICAL: `threat_score ≤ -4.5` (was `-6.0`)
17+
- ELEVATED: `threat_score ≤ -2.0` (was `-3.0`)
18+
19+
2. **Added material-conflict volume shortcut** — bypasses the Goldstein average when a country shows high absolute kinetic event counts:
20+
- `material_conflict > 150` → CRITICAL (regardless of Goldstein)
21+
- `material_conflict > 50` and was MONITORING → promote to ELEVATED
22+
23+
3. **Conflict zone panel now includes MONITORING** — shows all three negative threat levels (CRITICAL →red, ELEVATED →amber, MONITORING →yellow/WATCH badge). The zone counter now breaks out counts by level (`[2]+3+7`) instead of a single total.
24+
25+
## Changes
26+
27+
| File | Change |
28+
|------|--------|
29+
| `backend/api/routers/gdelt.py` | Lowered CRITICAL/ELEVATED thresholds; added material-conflict shortcut |
30+
| `frontend/src/components/layouts/IntelSidebar.tsx` | `conflictZones` filter includes MONITORING; header count is split by level; zone badge uses `conflictZoneBadgeLabel()` returning CRITICAL/ELEVATED/WATCH |
31+
| `frontend/src/components/widgets/ActiveConflictWidget.tsx` | Same filter expansion and WATCH badge; count display shows active/watch split |
32+
| `backend/api/tests/test_gdelt_router.py` | 6 new threshold/shortcut regression tests |
33+
34+
## Verification
35+
36+
- `cd backend/api && uv tool run ruff check .` — clean.
37+
- `cd backend/api && uv run python -m pytest tests/test_gdelt_router.py -v`**11/11 passed**.
38+
- `cd frontend && pnpm run lint` — exit 0.
39+
- `cd frontend && pnpm run typecheck` — exit 0.
40+
41+
## Benefits
42+
43+
- ISR, UKR, RUS, and other active conflict zones will now correctly surface in the ACTIVE CONFLICT ZONES panel.
44+
- High-kinetic actors (Gaza, Sudan) are promoted even during subdued media cycles.
45+
- MONITORING actors are visible without polluting the CRITICAL/ELEVATED severity hierarchy — operators can see the full threat picture in one panel.

0 commit comments

Comments
 (0)