From c3dd95f6a94902b8ca7cac449c2423ccb92c7301 Mon Sep 17 00:00:00 2001 From: BigBodyCobain <43977454+BigBodyCobain@users.noreply.github.com> Date: Tue, 2 Jun 2026 13:34:11 -0600 Subject: [PATCH] Address remaining safe security hardening --- .env.example | 6 +++++ README.md | 20 +++++++++++++--- backend/config/news_feeds.json | 4 ++-- backend/scripts/release_helper.py | 5 ++++ .../infonet/privacy/function_keys/__init__.py | 23 ++++++++++++------- backend/services/news_feed_config.py | 6 +++-- backend/tests/test_news_keywords.py | 13 ++++++++++- .../__tests__/csp/cspNoncePlumbing.test.ts | 16 ++++++------- .../csp/cspProductionHardening.test.ts | 9 ++++---- frontend/src/app/globals.css | 6 ++--- frontend/src/app/layout.tsx | 15 +++++++----- .../InfonetTerminal/FunctionKeyView.tsx | 12 ++++------ frontend/src/components/map/MapMarkers.tsx | 2 +- frontend/src/middleware.ts | 4 ++-- 14 files changed, 94 insertions(+), 47 deletions(-) diff --git a/.env.example b/.env.example index 4f43126..f5c3b8e 100644 --- a/.env.example +++ b/.env.example @@ -128,8 +128,14 @@ ADMIN_KEY= # MESH_DM_ROOT_TRANSPARENCY_LEDGER_READBACK_URI=backend/../ops/root_transparency_ledger.json # ── Self Update ──────────────────────────────────────────────── +# Optional ZIP updater digest pin. The updater checks this first, then +# backend/data/release_digests.json, then the release SHA256SUMS.txt asset. # MESH_UPDATE_SHA256= +# Optional strict nonce-only frontend CSP. Leave unset unless the exact build +# has been verified to hydrate cleanly in your deployment. +# SHADOWBROKER_STRICT_CSP=1 + # ── Wormhole (Local Agent) ───────────────────────────────────── # WORMHOLE_URL=http://127.0.0.1:8787 # WORMHOLE_TRANSPORT=direct diff --git a/README.md b/README.md index 9fd0365..4c4441d 100644 --- a/README.md +++ b/README.md @@ -19,7 +19,7 @@ **ShadowBroker** is a decentralized intelligence platform that aggregates real-time, multi-domain OSINT telemetry from 60+ live intelligence feeds into a single dark-ops map interface. Aircraft, ships, satellites, conflict zones, CCTV networks, GPS jamming, internet-connected devices, police scanners, mesh radio nodes, and breaking geopolitical events — all updating in real time on one screen as well as an obfuscated communications protocol and information exchange infrastructure. -Built with **Next.js**, **MapLibre GL**, **FastAPI**, and **Python**. 35+ toggleable data layers, including SAR ground-change detection. Multiple visual modes (DEFAULT / SATELLITE / FLIR / NVG / CRT). Right-click any point on Earth for a country dossier, head-of-state lookup, and the latest Sentinel-2 satellite photo. No user data is collected or transmitted — the dashboard runs entirely in your browser against a self-hosted backend. +Built with **Next.js**, **MapLibre GL**, **FastAPI**, and **Python**. 35+ toggleable data layers, including SAR ground-change detection. Multiple visual modes (DEFAULT / SATELLITE / FLIR / NVG / CRT). Right-click any point on Earth for a country dossier, head-of-state lookup, and the latest Sentinel-2 satellite photo. ShadowBroker has no accounts, product telemetry, or analytics; the dashboard talks to your self-hosted backend, while optional live OSINT panels may contact their configured public data providers when you use them. Designed for analysts, researchers, radio operators, and anyone who wants to see what the world looks like when every public signal is on the same map. @@ -28,7 +28,7 @@ Designed for analysts, researchers, radio operators, and anyone who wants to see A surprising amount of global telemetry is already public — aircraft ADS-B broadcasts, maritime AIS signals, satellite orbital data, earthquake sensors, mesh radio networks, police scanner feeds, environmental monitoring stations, internet infrastructure telemetry, and more. This data is scattered across dozens of tools and APIs. ShadowBroker combines all of it into a single interface. -The project does not introduce new surveillance capabilities — it aggregates and visualizes existing public datasets. It is fully open-source so anyone can audit exactly what data is accessed and how. No user data is collected or transmitted — everything runs locally against a self-hosted backend. No telemetry, no analytics, no accounts. +The project does not introduce new surveillance capabilities — it aggregates and visualizes existing public datasets. It is fully open-source so anyone can audit exactly what data is accessed and how. ShadowBroker does not include product telemetry, analytics, or accounts. Operator-supplied keys stay in your local deployment, but live OSINT features necessarily make outbound requests to the public data providers you enable or query. ### Shodan Connector @@ -113,6 +113,20 @@ That's it. `pull` grabs the latest images, `up -d` restarts the containers. > > Podman users should run the equivalent provider command, for example `podman-compose pull` and `podman-compose up -d`, or use `./compose.sh --engine podman pull` and `./compose.sh --engine podman up -d` from a bash-compatible shell. +### Update Integrity + +Docker updates are delivered through signed container registries. The legacy ZIP self-updater verifies release archives through this chain, in order: + +* `MESH_UPDATE_SHA256` when an operator pins a digest explicitly. +* `backend/data/release_digests.json` for bundled release pins. +* The release `SHA256SUMS.txt` asset on GitHub when a bundled pin is not present. + +Release maintainers should run `python backend/scripts/release_helper.py hash ` before publishing, then publish `SHA256SUMS.txt` and update `backend/data/release_digests.json` when shipping a ZIP updater target. The updater keeps the operator override path intact instead of failing closed on missing bundled digests, so existing installs do not get stranded by a release-process mistake. + +### CSP Hardening + +The production frontend ships with a hydration-compatible CSP and a strict nonce-only CSP in `Content-Security-Policy-Report-Only`. Set `SHADOWBROKER_STRICT_CSP=1` only after verifying the exact build hydrates correctly in your deployment. Runtime Google Fonts are not required; the bundled Next font pipeline serves the dashboard font from the app build. + ### ⚠️ **Stuck on the old version?** **If `git pull` fails or `docker compose up` keeps building from source instead of pulling images**, your clone predates a March 2026 repository migration that rewrote commit history. A normal `git pull` cannot fix this. Run: @@ -219,7 +233,7 @@ The first decentralized intelligence communication and governance layer built di **Privacy primitive runway (NEW in v0.9.7):** -* **Function Keys — Anonymous Citizenship Proof** — A citizen proves "I am an Infonet citizen" without revealing their Infonet identity. 5 of 6 pieces shipped: nullifiers, challenge-response, two-phase commit receipts, enumerated denial codes, batched settlement. Issuance via blind signatures waits on a primitive decision (RSA blind sigs vs BBS+ vs U-Prove vs Idemix). +* **Function Keys — Anonymous Credential Scaffolding** — The plumbing is in place for nullifiers, challenge-response, two-phase commit receipts, enumerated denial codes, and batched settlement. Today's challenge-response is an HMAC-based placeholder for integration testing, not a production anonymous or zero-knowledge citizenship proof. True unlinkable issuance still waits on a primitive decision (RSA blind sigs vs BBS+ vs U-Prove vs Idemix). * **Locked Protocol Contracts** — Stable interfaces in `services/infonet/privacy/contracts.py` for ring signatures, stealth addresses, Pedersen commitments, range proofs, and DEX matching. The `privacy-core` Rust crate is the integration target — no caller of the privacy module needs to know which scheme is active. * **Sprint 11+ Path** — When the cryptographic scheme is chosen, primitives wire into the locked Protocols without API churn. diff --git a/backend/config/news_feeds.json b/backend/config/news_feeds.json index ebc06fc..f7cd88e 100644 --- a/backend/config/news_feeds.json +++ b/backend/config/news_feeds.json @@ -7,7 +7,7 @@ }, { "name": "BBC", - "url": "http://feeds.bbci.co.uk/news/world/rss.xml", + "url": "https://feeds.bbci.co.uk/news/world/rss.xml", "weight": 3 }, { @@ -47,7 +47,7 @@ }, { "name": "Xinhua", - "url": "http://www.news.cn/english/rss/worldrss.xml", + "url": "https://www.news.cn/english/rss/worldrss.xml", "weight": 2 }, { diff --git a/backend/scripts/release_helper.py b/backend/scripts/release_helper.py index 0936648..52c8bbb 100644 --- a/backend/scripts/release_helper.py +++ b/backend/scripts/release_helper.py @@ -167,6 +167,11 @@ def cmd_hash(args: argparse.Namespace) -> int: print("") print("Updater pin:") print(f"MESH_UPDATE_SHA256={digest}") + print("") + print("Release checklist:") + print(" - add this digest to SHA256SUMS.txt for the GitHub release") + print(" - add/update backend/data/release_digests.json for bundled updater verification") + print(" - keep MESH_UPDATE_SHA256 available as the operator override path") return 0 if asset_matches else 2 diff --git a/backend/services/infonet/privacy/function_keys/__init__.py b/backend/services/infonet/privacy/function_keys/__init__.py index c7fd6f0..b19a880 100644 --- a/backend/services/infonet/privacy/function_keys/__init__.py +++ b/backend/services/infonet/privacy/function_keys/__init__.py @@ -1,14 +1,20 @@ -"""Function Keys — anonymous citizenship proof. +"""Function Keys — anonymous credential scaffolding. Source of truth: ``infonet-economy/IMPLEMENTATION_PLAN.md`` §4.4, ``infonet-economy/BRAINDUMP.md`` §11 item 9. -A citizen should be able to prove "I am a UBI-eligible Infonet -citizen" to a real-world operator (food bank, community service) -**without revealing their Infonet identity**. The naive approach -(scramble a public key, record each redemption on chain) leaks -identity through metadata correlation (time, location, operator, -frequency). +A citizen should eventually be able to prove "I am a UBI-eligible +Infonet citizen" to a real-world operator (food bank, community +service) **without revealing their Infonet identity**. The current +Python implementation wires the accounting, nullifier, receipt, and +operator flows, but its HMAC challenge-response is a placeholder for +integration tests. It is not a production anonymous or zero-knowledge +citizenship proof until blind signatures or anonymous credentials are +selected and wired. + +The naive approach (scramble a public key, record each redemption on +chain) leaks identity through metadata correlation (time, location, +operator, frequency). The full design has six pieces; five are implemented in pure Python here. The remaining piece — issuance via blind signatures or @@ -27,7 +33,8 @@ Pieces: operator: tracked via ``NullifierTracker``. 3. **Challenge-response** (`challenge_response.py`) — operator issues a fresh nonce, key-holder signs with the Function Key's - secret. Prevents screenshot attacks, key sharing, replay. + secret. This is HMAC placeholder plumbing for screenshot/replay + resistance, not the final anonymous credential proof. 4. **Two-phase commit receipts** (`receipt.py`) — Phase 1 verification receipt (operator-signed, day-level date NOT timestamp, no node_id). Phase 2 fulfillment receipt (citizen diff --git a/backend/services/news_feed_config.py b/backend/services/news_feed_config.py index 7f80eb6..2e8bcd0 100644 --- a/backend/services/news_feed_config.py +++ b/backend/services/news_feed_config.py @@ -12,6 +12,8 @@ logger = logging.getLogger(__name__) CONFIG_PATH = Path(__file__).parent.parent / "config" / "news_feeds.json" MAX_FEEDS = 50 _FEED_URL_REPLACEMENTS = { + "http://feeds.bbci.co.uk/news/world/rss.xml": "https://feeds.bbci.co.uk/news/world/rss.xml", + "http://www.news.cn/english/rss/worldrss.xml": "https://www.news.cn/english/rss/worldrss.xml", "https://www.channelnewsasia.com/rssfeed/8395986": "https://www.channelnewsasia.com/api/v1/rss-outbound-feed?_format=xml", } _DEAD_FEED_URLS = { @@ -27,7 +29,7 @@ _DEAD_FEED_URLS = { DEFAULT_FEEDS = [ {"name": "NPR", "url": "https://feeds.npr.org/1004/rss.xml", "weight": 4}, - {"name": "BBC", "url": "http://feeds.bbci.co.uk/news/world/rss.xml", "weight": 3}, + {"name": "BBC", "url": "https://feeds.bbci.co.uk/news/world/rss.xml", "weight": 3}, {"name": "AlJazeera", "url": "https://www.aljazeera.com/xml/rss/all.xml", "weight": 2}, {"name": "NYT", "url": "https://rss.nytimes.com/services/xml/rss/nyt/World.xml", "weight": 1}, {"name": "GDACS", "url": "https://www.gdacs.org/xml/rss.xml", "weight": 5}, @@ -35,7 +37,7 @@ DEFAULT_FEEDS = [ {"name": "Bellingcat", "url": "https://www.bellingcat.com/feed/", "weight": 4}, {"name": "Guardian", "url": "https://www.theguardian.com/world/rss", "weight": 3}, {"name": "TASS", "url": "https://tass.com/rss/v2.xml", "weight": 2}, - {"name": "Xinhua", "url": "http://www.news.cn/english/rss/worldrss.xml", "weight": 2}, + {"name": "Xinhua", "url": "https://www.news.cn/english/rss/worldrss.xml", "weight": 2}, {"name": "CNA", "url": "https://www.channelnewsasia.com/api/v1/rss-outbound-feed?_format=xml", "weight": 3}, {"name": "Mercopress", "url": "https://en.mercopress.com/rss/", "weight": 3}, {"name": "SCMP", "url": "https://www.scmp.com/rss/91/feed", "weight": 4}, diff --git a/backend/tests/test_news_keywords.py b/backend/tests/test_news_keywords.py index 393d48b..7766c46 100644 --- a/backend/tests/test_news_keywords.py +++ b/backend/tests/test_news_keywords.py @@ -5,7 +5,7 @@ from pathlib import Path import pytest from services.fetchers.news import _resolve_coords -from services.news_feed_config import DEFAULT_FEEDS +from services.news_feed_config import DEFAULT_FEEDS, _normalise_feeds CONFIG_PATH = Path(__file__).parent.parent / "config" / "news_feeds.json" @@ -152,3 +152,14 @@ class TestFeedConfig: urls = {f["url"] for f in DEFAULT_FEEDS} assert "https://www.reutersagency.com/feed/?best-topics=world" not in urls assert "https://rsshub.app/apnews/topics/world-news" not in urls + + def test_legacy_http_feeds_are_migrated_to_https(self): + feeds = _normalise_feeds( + [ + {"name": "BBC", "url": "http://feeds.bbci.co.uk/news/world/rss.xml", "weight": 3}, + {"name": "Xinhua", "url": "http://www.news.cn/english/rss/worldrss.xml", "weight": 2}, + ] + ) + urls = {f["url"] for f in feeds} + assert "https://feeds.bbci.co.uk/news/world/rss.xml" in urls + assert "https://www.news.cn/english/rss/worldrss.xml" in urls diff --git a/frontend/src/__tests__/csp/cspNoncePlumbing.test.ts b/frontend/src/__tests__/csp/cspNoncePlumbing.test.ts index 942cbf2..b17d7b0 100644 --- a/frontend/src/__tests__/csp/cspNoncePlumbing.test.ts +++ b/frontend/src/__tests__/csp/cspNoncePlumbing.test.ts @@ -147,18 +147,18 @@ describe('middleware matcher exclusions', () => { }); // --------------------------------------------------------------------------- -// 5. Google Fonts domains are preserved in CSP +// 5. Runtime Google Fonts domains are not required in CSP // --------------------------------------------------------------------------- -describe('Google Fonts domains in CSP', () => { - it('style-src includes https://fonts.googleapis.com', () => { +describe('local font CSP', () => { + it('style-src does not allow https://fonts.googleapis.com', () => { const csp = getCsp(); - expect(csp).toContain('https://fonts.googleapis.com'); + expect(csp).not.toContain('https://fonts.googleapis.com'); }); - it('font-src includes https://fonts.gstatic.com', () => { + it('font-src does not allow https://fonts.gstatic.com', () => { const csp = getCsp(); - expect(csp).toContain('https://fonts.gstatic.com'); + expect(csp).not.toContain('https://fonts.gstatic.com'); }); }); @@ -178,9 +178,9 @@ describe('production CSP directive completeness', () => { expect(csp).not.toMatch(/script-src [^;]*'nonce-/); }); - it('has style-src with unsafe-inline and fonts.googleapis.com', () => { + it('has style-src with hydration-compatible inline styles only', () => { expect(csp).toMatch(/style-src [^;]*'unsafe-inline'/); - expect(csp).toMatch(/style-src [^;]*https:\/\/fonts\.googleapis\.com/); + expect(csp).not.toMatch(/style-src [^;]*https:\/\/fonts\.googleapis\.com/); }); it('has worker-src self blob:', () => { diff --git a/frontend/src/__tests__/csp/cspProductionHardening.test.ts b/frontend/src/__tests__/csp/cspProductionHardening.test.ts index 0fa2d9b..fe5d577 100644 --- a/frontend/src/__tests__/csp/cspProductionHardening.test.ts +++ b/frontend/src/__tests__/csp/cspProductionHardening.test.ts @@ -130,16 +130,17 @@ describe('unchanged directives in production', () => { vi.unstubAllEnvs(); }); - it('style-src preserves unsafe-inline and Google Fonts', () => { + it('style-src preserves unsafe-inline without runtime Google Fonts', () => { const styleSrc = getDirective('style-src'); expect(styleSrc).toContain("'unsafe-inline'"); - expect(styleSrc).toContain('https://fonts.googleapis.com'); + expect(styleSrc).not.toContain('https://fonts.googleapis.com'); }); - it('font-src preserves data: and fonts.gstatic.com', () => { + it('font-src preserves self and data without runtime Google Fonts', () => { const fontSrc = getDirective('font-src'); + expect(fontSrc).toContain("'self'"); expect(fontSrc).toContain('data:'); - expect(fontSrc).toContain('https://fonts.gstatic.com'); + expect(fontSrc).not.toContain('https://fonts.gstatic.com'); }); it('worker-src self blob:', () => { diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 918a756..772ff3b 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -55,7 +55,7 @@ body { background: var(--background); color: var(--foreground); - font-family: 'JetBrains Mono', var(--font-roboto-mono), 'Roboto Mono', monospace; + font-family: var(--font-jetbrains-mono), var(--font-roboto-mono), 'Roboto Mono', monospace; } /* Global interactive cursor hints */ @@ -139,7 +139,7 @@ textarea:disabled { padding: 12px 16px; color: #d1d5db; font-family: - 'JetBrains Mono', var(--font-roboto-mono), 'Roboto Mono', monospace, 'Microsoft YaHei', 'PingFang SC', + var(--font-jetbrains-mono), var(--font-roboto-mono), 'Roboto Mono', monospace, 'Microsoft YaHei', 'PingFang SC', 'Noto Sans SC', 'Noto Sans JP', 'Noto Sans KR', sans-serif; font-size: 13px; min-width: 240px; @@ -377,7 +377,7 @@ textarea:disabled { /* ── INFONET CRT TERMINAL EFFECTS ── */ .infonet-font { - font-family: 'JetBrains Mono', ui-monospace, SFMono-Regular, monospace; + font-family: var(--font-jetbrains-mono), ui-monospace, SFMono-Regular, monospace; } /* CRT scanline overlay — scoped to .crt containers only */ diff --git a/frontend/src/app/layout.tsx b/frontend/src/app/layout.tsx index 351642d..44f6819 100644 --- a/frontend/src/app/layout.tsx +++ b/frontend/src/app/layout.tsx @@ -1,9 +1,17 @@ import type { Metadata } from 'next'; +import { JetBrains_Mono } from 'next/font/google'; import DesktopBridgeBootstrap from '@/components/DesktopBridgeBootstrap'; import { ThemeProvider } from '@/lib/ThemeContext'; import { I18nProvider } from '@/i18n'; import './globals.css'; +const jetBrainsMono = JetBrains_Mono({ + subsets: ['latin'], + weight: ['400', '700'], + display: 'swap', + variable: '--font-jetbrains-mono', +}); + export const metadata: Metadata = { title: 'WORLDVIEW // ORBITAL TRACKING', description: 'Advanced Geopolitical Risk Dashboard', @@ -22,12 +30,7 @@ export default function RootLayout({ }>) { return ( - - - - - - + diff --git a/frontend/src/components/InfonetTerminal/FunctionKeyView.tsx b/frontend/src/components/InfonetTerminal/FunctionKeyView.tsx index 8000e04..259f012 100644 --- a/frontend/src/components/InfonetTerminal/FunctionKeyView.tsx +++ b/frontend/src/components/InfonetTerminal/FunctionKeyView.tsx @@ -38,19 +38,17 @@ export default function FunctionKeyView({ onBack }: FunctionKeyViewProps) { BACK
- FUNCTION KEYS — Anonymous Citizenship Proof + FUNCTION KEYS — Credential Scaffolding
- A citizen proves "I am an Infonet citizen" to a real-world - operator without revealing their Infonet identity. - The naive approach (scramble a public key, record each redemption on chain) leaks - identity through metadata correlation. The Function Keys design is six pieces; - five are implemented; one (issuance via blind signatures / anonymous credentials) - waits on a cryptographic primitive decision. + Function Keys wire the nullifier, receipt, and settlement plumbing for future + anonymous credential proofs. The current challenge-response is an HMAC placeholder, + not production zero-knowledge citizenship. True unlinkable issuance still waits on + blind signatures or anonymous credentials.
{status && ( diff --git a/frontend/src/components/map/MapMarkers.tsx b/frontend/src/components/map/MapMarkers.tsx index d5a8a89..3f3e0c6 100644 --- a/frontend/src/components/map/MapMarkers.tsx +++ b/frontend/src/components/map/MapMarkers.tsx @@ -387,7 +387,7 @@ export function ThreatMarkers({ borderRadius: '4px', padding: '8px 20px 8px 12px', color: riskColor, - fontFamily: "'JetBrains Mono', monospace", + fontFamily: 'var(--font-jetbrains-mono), monospace', fontSize: '12px', fontWeight: 'bold', textAlign: 'center', diff --git a/frontend/src/middleware.ts b/frontend/src/middleware.ts index 4735604..156436e 100644 --- a/frontend/src/middleware.ts +++ b/frontend/src/middleware.ts @@ -18,12 +18,12 @@ function buildCsp(nonce: string, strictScripts = false): string { const directives = [ "default-src 'self'", scriptSrc, - "style-src 'self' 'unsafe-inline' https://fonts.googleapis.com", + "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https:", isDev ? "connect-src 'self' ws: wss: http://127.0.0.1:8000 http://127.0.0.1:8787 https:" : "connect-src 'self' ws: wss: https:", - "font-src 'self' data: https://fonts.gstatic.com", + "font-src 'self' data:", "object-src 'none'", "worker-src 'self' blob:", "child-src 'self' blob:",