Skip to content

fix(extension): alert on real org detections instead of missing usage fields - #248

Merged
ralyodio merged 1 commit into
masterfrom
fix/extension-threat-badge
Sep 26, 2026
Merged

ralyodio merged 1 commit into
masterfrom
fix/extension-threat-badge

Conversation

@ralyodio

Copy link
Copy Markdown
Contributor

What

  1. Alerts use real detections. The background worker read usage.threats / usage.warnings from /api/usage, which returns neither, so the count was always 0 and no notification ever fired. It now polls GET /api/orgs/:id/detections?status=new&limit=100 for the signed-in user's current org (current_org_id from /api/auth/me when the user has several orgs, else the only / most recently joined org from /api/orgs), on the existing threatcrush-event-check alarm and whenever the popup's Account tab opens. New listOrganizations() / listDetections() in src/lib/api.js (separate hunk; the top-up call site is untouched). Logic lives in the new src/background/detections.js.
  2. Notifications for high/critical detections that were not in the previous poll, respecting the existing Options → Account alerts toggle (notificationsEnabled). One notification per org (id threatcrush-detections:<slug>), replaced by the next one; clicking opens <app>/org/<slug>/detections. The first poll after sign-in or an org switch only records a baseline, so installing the extension doesn't replay old alerts. Alarm and popup polls are single-flighted so one detection can't notify twice.
  3. Popup Account tab shows the org's new-detection count, how many are high/critical, and the five newest (severity, title, age) with View all. The Alerts quick action now opens the org's detections page (falls back to /usage with no org). Signed in with no org shows "No organization". An API failure keeps the last known numbers and shows "Could not reach ThreatCrush."
  4. Badge presentation. The toolbar badge stays with the page checks from feat(extension): local security checks for the active page #240 (set per tab). The detection count goes to the popup, notifications, and the toolbar tooltip (ThreatCrush: N new detections in <org>). Why: feat(extension): local security checks for the active page #240 sets per-tab badges and falls back to the global badge while a page loads; putting the account count in the global badge would make the same slot mean "page problems" on one tab and "server detections" on another. The old global updateBadge() and the unused CHECK_NOW message are removed.
  5. Icons. apps/extension/public/icons/*.png were 1×1 PNGs with a bad IDAT CRC. Chrome rejects a basic notification whose icon can't be decoded (Unable to download all specified images.), so notifications could not have been shown even with a non-zero count, and the toolbar icon was blank. Regenerated at 16/32/48/128 from apps/web/public/icons/icon-512x512.png. The notification iconUrl now uses chrome.runtime.getURL (a relative path resolved against background/ in the worker).
  6. onInstalled re-creates the alarm with the interval saved in Options instead of resetting it to 5 minutes on every update.
  7. README: new "Detection alerts" section; Options label now reads "Browser notifications for new high/critical detections".

How verified

  • NODE_ENV=test pnpm --filter @profullstack/threatcrush-extension test: 5 files, 63 tests pass, including the new __tests__/detection-alerts.test.js (baseline without notifying, id diffing across polls, severity filter, notifications toggle, org switch re-baseline, signed-out no-op with no console output, no-org no-op, API error keeps state and doesn't notify, concurrent polls notify once, notification click URL). (My shell exports NODE_ENV=production, under which the pre-existing options-app.test.jsx fails with React.act is not a function; unrelated to this change.)
  • pnpm --filter @profullstack/threatcrush-extension build: Chrome, Firefox and Safari builds complete.
  • Real browser: built the Chrome extension against local Supabase and next dev -p 3447 (NEXT_PUBLIC_APP_URL=http://localhost:3447; for this run only I added http://localhost:3447/* and http://127.0.0.1:54321/* to host_permissions in the dist copy, since the committed manifest only lists localhost:3000). Loaded it in headless Chromium 1243 with --load-extension, throwaway user + org + server, detections inserted via SQL. Observed:
    • Signed out, alarm fired: tooltip ThreatCrush, badge empty, no notification, no console errors/warnings.
    • Signed in via the popup with 2 existing new detections (1 critical, 1 low): popup showed "High/critical detections, 2 new detections in EA Ext Org", tiles 2 / 1, both detections listed; no notification (baseline).
    • Inserted a critical detection with detected_at 3 h in the past plus a medium one, fired the alarm: exactly one chrome.notifications.create("threatcrush-detections:ea-ext-org", {title: "Critical detection in EA Ext Org", message: "New critical: SSH brute force from 203.0.113.9"}) (wrapped in the service worker to record calls); chrome.notifications.getAll() listed it as shown; tooltip ThreatCrush: 4 new detections in EA Ext Org; global badge still empty. A second alarm with unchanged data notified nothing. Reopened popup: 4 / 2, four detections listed; View all opened http://localhost:3447/org/ea-ext-org/detections.
    • Logout then alarm: tooltip back to ThreatCrush, stored state cleared.
    • Second user with no org: popup "No organization", tooltip ThreatCrush, badge empty, no console errors/warnings.
    • Page checks still own the badge: PAGE_CHECKS on a localhost:3447 tab set that tab's badge to 1 (one warning) while the global badge stayed empty.
    • Before the icon fix, the same run logged Notification failed: Unable to download all specified images. and getAll() was empty.
  • Throwaway users/org and the dev server were removed afterwards.

Blocked on the owner

  • Nothing required. Store builds still need the store accounts listed in the README.

Decisions

  • Id diff instead of since=<last check>. since filters on detected_at, which the daemon sets and which can predate the upload (spooled events are replayed later, per the cloud contract). A time window would drop those; the smoke run above shows a 3-hour-old detected_at still notifying. The worker keeps the ids from the last page (≤100) in chrome.storage.local.
  • Only high and critical notify; low/medium/info only count in the popup.
  • "High / critical" tile counts within the newest 100 new detections; it shows N+ when there are more new detections than that.
  • No notification for detections that already existed at sign-in or after switching orgs (they show in the popup count).
  • Removed the "Events Today" and "Modules" tiles from the Account tab. "Modules" was hard-coded to 0; "Events Today" was /api/usage today_requests (billed API requests), not security events. Easy to restore from git if wanted.
  • Regenerated icons from the web app's icon; swap in dedicated artwork if you have one.

… fields

The worker read usage.threats/usage.warnings from /api/usage, which returns
neither, so the count was always 0 and no notification ever fired.

It now polls GET /api/orgs/:id/detections?status=new for the user's current
org, shows the new-detection count and the newest detections in the popup and
toolbar tooltip, and notifies for high/critical detections not seen in the
previous poll (diffed by id, since detected_at can predate the upload).
Clicking the notification opens /org/<slug>/detections. The toolbar badge
stays with the per-tab page checks.

The bundled icons were corrupt 1x1 PNGs; Chrome refuses a basic notification
whose icon can't be decoded, so they are regenerated from the web app's
512px icon.
@github-actions

Copy link
Copy Markdown

ThreatCrush Security Scan

13 finding(s)

HIGH/CRITICAL: 1 | MEDIUM: 6 | LOW: 6

Severity Rule Location
HIGH secret-aws-access-key prd/0003-detect-hardcoded-secrets-before-they-are-committed-or-served.md:126
MEDIUM js-open-redirect apps/web/src/app/auth/login/page.tsx:50
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:104
MEDIUM js-unescaped-html-sink apps/web/src/app/hire/page.tsx:108
MEDIUM js-open-redirect apps/web/src/components/funding/FundingClient.tsx:97
MEDIUM js-unescaped-html-sink apps/web/src/components/GuideReader.tsx:265
MEDIUM js-uninitialized-buffer packages/scan/src/node-rules.ts:456
LOW secret-generic-credential apps/web/src/app/api/auth/refresh/route.ts:17
LOW secret-generic-credential PRD.md:269
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:121
LOW tls-verification-disabled prd/0004-find-dangerous-code-patterns-without-pretending-to-be-a-compiler.md:122
LOW sh-remote-script-execution scripts/smoke-test.sh:47
LOW secret-aws-access-key scripts/smoke-test.sh:112

Snippets are redacted; ThreatCrush never prints matched credential material.

@ralyodio
ralyodio merged commit b5d8971 into master Sep 26, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant