feat(ui): expose attachment_policy + folder_policies in SecuritySettingsPanel (#83) - #25
Closed
schmug wants to merge 41 commits into
Closed
feat(ui): expose attachment_policy + folder_policies in SecuritySettingsPanel (#83)#25schmug wants to merge 41 commits into
schmug wants to merge 41 commits into
Conversation
Introduces an opt-in, MISP-compatible threat-intel platform layered on top of agentic-inbox. Two pieces: an edge pipeline that extends the existing inbox Worker, and a new `hub/` subproject for community intel aggregation. Edge (agentic-inbox extensions): - Security pipeline (workers/security/): SPF/DKIM/DMARC parser, URL extraction + homograph/shortener heuristics, per-sender reputation, Workers-AI classifier (llama-3.1-8b-instruct-fast), and a deterministic scoring function that aggregates into allow / tag / quarantine / block - Layered triage tiers (workers/security/triage.ts) that short-circuit the LLM call: hard-block on confirmed intel hits or flagged senders, hard-allow on DMARC pass + allowlist or long trusted history. The hard-allow tier REQUIRES DMARC pass — allowlist alone is never enough. - Threat-intel feed consumer (workers/intel/): bloom-filter-backed membership checks in KV, hourly refresh cron, URLhaus + OpenPhish defaults, MISP-compatible client for posting to a hub - DMARC aggregate report ingestor (workers/dmarc/): detects RUA emails, gunzips XML via DecompressionStream, parses + stores per-source-IP stats, with a dashboard route and summary API - Case workflow (workers/routes/cases.ts + UI routes): TheHive-lite case management with a Report-as-Phish button; optional anonymized push to the community hub when configured - Quarantine folder + schema migration 10 adding verdict columns, intel_feed_state, dmarc_reports/records/sources, cases, case_emails, case_observables, urls, sender_reputation Hub (new `hub/` subproject): - MISP-compatible REST subset: POST/GET /events, /events/restSearch, /feeds/destroylist.txt, /sharing_groups, /orgs/invite + /orgs/accept - Invite-based trust circles (MISP "sharing groups") with API-key auth (keys stored as sha256 hashes so D1 leaks can't impersonate) - Trust-weighted aggregation: attributes promote to published feeds when score >= 2.0 and contributors >= 2 (tunable) - Queue-backed LLM triage agent that assigns MISP taxonomy tags only, never affects scoring — deliberate prompt-injection defense Opt-in throughout. Existing mailboxes are unchanged until `settings.security.enabled = true` is set in the mailbox R2 JSON. Deferred for follow-up PRs: - Attachment deep-scan (OCR, macro analysis) - URL fetch preview + RDAP domain-age - Attachment-type gates, per-folder bypass tier, time-of-day tier - Fixture-based pipeline tests Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
) Stands up a Vitest suite for the security-critical pure-function modules (auth parser, URL/homograph heuristics, verdict aggregation, triage, reputation, DMARC XML parser, bloom filter) — 64 tests. The suite caught two real bugs: - scoreReputation subtracted 5 when avg_score > 70, so a historically malicious sender looked LESS suspicious to the aggregator than an unknown sender. Flipped to +10 and attach a reason string so the signal surfaces in verdicts. - isHomographic compared the last two labels as the registrable domain, so `amazom.co.uk` was checked against `co.uk` instead of `amazon.co.uk` and slipped past detection. Added a small multi-label public-suffix set and a registrableDomain() helper. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Authentication-Results hardening (closes forgery vector) -------------------------------------------------------- An attacker-controlled upstream mail server can inject its own Authentication-Results header claiming pass results. Cloudflare Email Routing preserves the header, so the previous parser — which accepted any Authentication-Results header without validating authserv-id — could be tricked into reporting DMARC=pass for a forged message, potentially triggering hard-allow triage. Two defensive changes in workers/security/auth.ts: 1. Per-mailbox `trusted_authserv_ids` setting. When configured, only headers whose authserv-id matches (or is a dotted subdomain of) an entry on the list contribute to the verdict. Empty list preserves back-compat "trust any" behaviour so existing mailboxes aren't broken on upgrade; README guidance should recommend populating it. 2. Track per-method set-ness separately from the AuthResult value. Previously the string "none" was both a valid result and the "not yet set" sentinel, so a legitimate first header with `dkim=none` allowed any later (possibly attacker-controlled) header to overwrite it. Off-hours scoring (completes half-built feature) ----------------------------------------------- The `business_hours` setting already existed in MailboxSecuritySettings but nothing consumed it. Added workers/security/time-rules.ts with a small +10 off-hours boost, wired into the pipeline AFTER aggregation so it can't short-circuit triage. Targeted at BEC signal where wire fraud disproportionately lands outside working hours. Also refactored the duplicated post-aggregation boost logic in workers/security/index.ts into an applyBoost() helper so the intel and off-hours cases share a single code path. 14 new tests; 78/78 passing. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously the entire workers/security/settings.ts surface was only reachable by editing mailbox JSON in R2 — the UI had no toggle for enabling the pipeline at all, let alone for the authserv-id gating, allowlists, thresholds, or the off-hours boost. Adds SecuritySettingsPanel with grouped controls for: - Master enable + learning mode - Score thresholds (tag / quarantine / block) - Trusted authserv-id list (forgery defense — routes straight to the Authentication-Results gating added in the previous commit) - Sender / domain allowlists (DMARC-pass gated) - Intel auto-block + history-based auto-allow tuning - Business-hours off-hours boost with IANA timezone + hour window MailboxSettings type gained a typed `security` field so the UI, API, and server settings loader share the same shape. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Previously hub/ had zero tests despite hosting the trust-weighted corroboration logic that decides what shows up on the destroylist feed — a decision that directly shapes what threats downstream Agentic Inbox mailboxes act on. Stands up vitest + a minimal in-memory D1 adapter backed by better-sqlite3 (foreign_keys OFF to match Cloudflare D1 defaults; ?N placeholders translated to object-keyed binds). Tests exercise: - Sybil resistance: one org cannot single-handedly promote intel by resubmitting, even with a high trust multiplier (contributor_count guard is the invariant). - Trust accumulation across distinct orgs. - Non-promotable attribute types are ignored. - Sharing-group isolation between tenants. - Promotion thresholds scope to the requested sharing group. - sha256 produces known digests and is deterministic; generateSecret returns 43-char base64url (~256 bits). - requireOrg rejects missing/unknown/revoked keys; accepts both bare-key (MISP) and Bearer-prefixed auth; only stores hashes, never the raw key. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ics) (#9) * Add async deep-scan (URL redirect chain, RDAP age, attachment heuristics) Introduces workers/intel/deep-scan.ts — the previously-referenced but never-built async stage. It runs AFTER the sync pipeline's store-and- decide step, inside ctx.waitUntil, so it can take seconds without blocking email receipt. Three new signal sources, each isolated in its own module so the pure parts can be unit-tested without network or Workers runtime: - workers/intel/url-resolver.ts — manual-redirect fetch chain (max 5 hops, 8s per hop). Surfaces the final hostname, <title>, and a host_changed flag. Resolved host feeds the homograph and intel-feed checks, so a bit.ly-wrapped phish gets inspected at its real destination. - workers/intel/rdap.ts — registration-age via rdap.org. Fresh domains (<30d) are a strong phishing signal; <7d gets a +20, <30d gets +10. Never fails closed: a dead RDAP server drops the signal rather than holding mail. - workers/intel/attachment-checks.ts — pure filename/MIME heuristics: dangerous extensions (.exe, .scr, .vbs, …), macro-enabled Office, `invoice.pdf.exe` double-extensions, MIME/extension mismatches, archives whose filename advertises a payload. Orchestrator caps its total contribution at +40 and ONLY upgrades the verdict — never downgrades — so a sync "allow" can become a deep-scan "tag" or "quarantine" but not vice-versa. Quarantined deep-scan verdicts also move the email to the QUARANTINE folder. Wired into workers/index.ts via ctx.waitUntil after the sync pipeline. Gated on security.enabled so disabled mailboxes see no extra latency. Six new DO methods added for persistence: getUrlsForEmail, updateUrlScan, getAttachmentsForEmail, updateAttachmentScan, updateDeepScanStatus, getStoredVerdict. 35 new tests; 113/113 passing total. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * README: add Security section (#10) Explains the opt-in pipeline, the two high-leverage configuration knobs (trusted_authserv_ids — forgery defense; business_hours — BEC nudge), the full stage list with latency budgets, and the async deep-scan guarantees (only tightens, capped contribution, fails silently on flaky external services). Also cross-links to the hub subdirectory so downstream adopters know the feed loop exists. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Extends triage with a tier-0 folder-policy check evaluated before hard-block and hard-allow. Each mailbox can configure, per folder, whether to run the full pipeline, skip the LLM classifier, or skip the pipeline entirely with a synthetic allow verdict tagged `triage: "folder_bypass"`. The `skip_classifier` path keeps every other signal (auth, URLs, reputation, off-hours) so truly suspicious mail can still surface a non-zero score. Folder bypass is independent of the hard-allow DMARC invariant — it reflects an explicit owner decision rather than a trust claim about the sender. Inbound mail defaults to INBOX today; if a future filter-rule engine delivers into other folders it must pass the destination through so the tier can honour per-folder policy. Also wires a `treat_as_verified` hook into `/emails/:id/move`: when the user moves a message into a verified-flagged folder from a non-verified folder, we fire `upsertSenderReputation(sender, 0)` best-effort. Snapshotting the email's pre-move folder keeps the bump idempotent — moving out and back in does not double-count. Adds the new `folder_bypass` discriminant to the frontend SecurityVerdict type. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Injectable classifier seam (__setClassifier) lets tests drive runSecurityPipeline end-to-end against real .eml fixtures without Workers AI. Map-backed fakes stand in for MAILBOX/BUCKET; BLOOM_KV is intentionally absent so intel-feed lookups return null. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The `workers/security/attachments.ts` module and the `attachment_block` triage tier shipped in #4 alongside folder-bypass, but the pipeline plumbing was only half-finished: - `DEFAULT_SECURITY_SETTINGS.attachment_policy` was missing, so the policy was always undefined and the triage tier never fired on fresh mailboxes. - `getSecuritySettings` didn't merge partial user policies, so supplying just `custom_blocklist_extensions` would silently drop the default `executable_action: "block"` back to undefined. - `aggregateVerdict` never picked up the container / macro-office "score" contributions — those categories were scored in isolation only, never reflected in the final verdict. This change promotes `attachment_policy` to a required field with `DEFAULT_ATTACHMENT_POLICY`, adds field-by-field merge in `getSecuritySettings` (custom blocklist is additive, actions fall back to defaults), threads `attachments` + `attachmentPolicy` through `VerdictInputs`, and calls `scoreAttachments` inside `aggregateVerdict` so container (+25) and macro-office (+15) contributions land in the final score. Adds 9 new tests: 6 triage-level (executable blocks under default policy, double-extension trick, runs-before-hard-allow invariant, safe-attachment no-op, container/macro don't hard-block, custom blocklist extends the set) and 3 aggregate-level (iso +25, docm +15, pdf no-op — baselined on DMARC-fail so the [0,100] clamp doesn't mask deltas). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The DELETE /api/v1/mailboxes/:id handler previously removed only the
R2 settings JSON and left everything else behind: every email, every
attachment blob, every folder, sender-reputation row, DMARC report,
and the agent's chat history. Recreating the same mailbox inherited
all of it — silent data leak across "deletions", plus unbounded R2
storage growth.
The new flow:
1. Delete the settings JSON first. Mailbox becomes invisible to all
list/get endpoints immediately, and a retried DELETE 404s cleanly.
2. Fire reapMailbox(env, mailboxId) via executionCtx.waitUntil —
the endpoint returns 204 without waiting on cleanup.
3. reapMailbox lists every attachment's R2 key via a new DO method
listAllAttachmentKeys(), batch-deletes them (100/batch to stay
under R2 binding + subrequest caps), then calls a new reset()
on both MailboxDO and EmailAgent that does ctx.storage.deleteAll().
Every step is isolated in its own try/catch so one transient failure
(e.g. the DO unreachable for a beat) doesn't abandon the remaining
work. Errors are logged but never bubble — the settings JSON is
already gone, so the mailbox is effectively deleted from the user's
point of view. Orphaned rows/blobs are a cleanup cost, never a
correctness bug.
Also consolidated the R2 attachment-key format into a single helper
`attachmentObjectKey(emailId, attachmentId, filename)` shared by
storeAttachments, the receive path, the email-delete handler, the
attachment download endpoint, and the new DO method. Previously the
literal `attachments/${emailId}/${attId}/${filename}` template was
duplicated in five places; a format drift between write and
read/delete sites would silently orphan blobs.
Confirmed safe: EmailAgent DOs are per-mailbox via
idFromName(mailboxId), so reset() only wipes this mailbox's state.
Cloudflare Access JWT middleware (workers/app.ts) covers the
endpoint — this is not exposing destructive action to unauthed
callers.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes the TODO in workers/security/attachments.ts that flagged
password-protected archives as a classic malware-smuggling pattern
we weren't catching. Emotet, Qakbot and similar campaigns attach an
encrypted ZIP and put the password in the email body — the encryption
defeats AV scanning at the gateway while the user still opens it.
Implemented in the async deep-scan path rather than the sync pipeline
so the hot receive path stays filename+MIME only (no R2 reads). The
new `detectEncryptedArchive(bytes, ext)` parses the first 32KB of the
attachment:
- ZIP: scans for the local-file-header signature (0x04034b50) —
not assuming offset 0 so self-extracting archives with an EXE
stub are handled — then checks general-purpose flag bit 0.
Central Directory Encryption is a documented false-negative
(CD lives at EOF, outside our window).
- RAR4: walks block headers, returns true on MHD_PASSWORD
(0x0080 on MAIN_HEAD) or LHD_PASSWORD (0x0004 on file blocks).
- RAR5 / 7z: deferred. Conservative false rather than risk false
positives on legitimate archives.
`scoreAttachment` now accepts an optional `{encryptedArchive}`
signal; when true it adds 25 (between macro_enabled_office=20 and
dangerous_extension=40) and suppresses the weaker
`suspicious_archive_name` nudge so reasons don't double-up in the UI.
Existing +30 attachment-aggregation cap and +40 deep-scan cap
preserved — no single flaky signal can push a verdict on its own.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Wire two-domain config (cortech.online + dmarc.mx) and document setup DOMAINS now lists both domains for E2E testing of inbound/outbound mail. Replaces KV namespace placeholders with real IDs and documents the namespace + R2 bucket prerequisites in the README, including a two-domain end-to-end test checklist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Expose attachments on the MCP send_email tool The attachment-type gate in the security pipeline could only be exercised end-to-end via inbound mail; the MCP send_email tool dropped the existing sendEmail attachments capability. Surface it through the zod schema and forward base64 payloads to env.EMAIL.send, persisting metadata on the Sent record so the UI matches what was wired up. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * Pin RDAP fresh-domain path with deep-scan integration tests Live testing couldn't isolate the RDAP-only signal — every fresh domain candidate also had a threat-intel hit, so the score boost and reason came from the intel feed match rather than RDAP. Cover the four meaningful RDAP states (≤7 days, 7–29 days, action-upgrade across threshold, established) by stubbing a fetch that returns canned RDAP bodies. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the 30-second inbox poll with a server-pushed event channel so new mail surfaces immediately and can fire a desktop notification when the tab is hidden. - MailboxDO now accepts WebSocket upgrades via the hibernation API and exposes notifyNewEmail(emailId, folder) for fanout. - New /api/v1/mailboxes/:mailboxId/events route gated by the existing CF Access middleware + requireMailbox; auth piggybacks on the cf-access-jwt-assertion header the edge injects on origin upgrades. - receiveEmail fanout fires after the sync security verdict and passes the final folder so quarantined mail never raises a desktop toast. - Client useMailboxEvents hook reconnects with jittered backoff; mailbox.tsx subscribes for the session, invalidates emails+folders on each event, and only fires Notification (tagged by emailId to dedupe across tabs) when the tab is hidden and the folder is INBOX. - Header gains a notification permission bell with granted/denied/ default states. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* feat(hub): add inbound peer + source_peer_uuid schema * feat(hub): add admin-key middleware for operator routes * feat(hub): admin CRUD for inbound peer configs Adds POST/GET/DELETE /admin/peers routes behind HUB_ADMIN_KEY auth. POST atomically inserts a peer, synthetic org, inbound_peer, and sharing_group_orgs row. Also adds batch() to the d1 test shim — required by admin routes which use multi-statement atomic writes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(hub): inbound MISP sync — pullFromPeer with watermark + UPSERT Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * feat(hub): mount admin routes and wire inbound sync to cron * docs(hub): restore endpoint reference comments in index.ts * docs(hub): inbound sync configuration and promotion semantics * fix(hub): force ASC order on inbound sync + clarify corroboration scope Final-review fixes: 1. fetchPage now sends order: 'Event.timestamp ASC' to upstream MISP. Without it the upstream's order is undefined; if it returns DESC, a deep first backfill (>MAX_PAGES * PAGE_SIZE events) advances the watermark past older unprocessed events with no way to recover them. 2. README clarifies that local-org corroboration of pulled intel only counts when the local report lands in the same sharing group as the peer's default_sharing_group_uuid (corroboration rows key on sharing_group_uuid). --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
* PhishPilot UI pivot — POC shell, tokens, cases redesign Foundation for the SOC-console rebrand from design_handoff_phishpilot. Scope is intentionally limited to evaluating the visual direction on a preview branch: token system + theme/hue store, new app shell, and the Cases queue + detail rebuilt against existing case data. Dashboard, threat-intel hub, and mail review are nav stubs; existing email list and compose flows remain reachable but un-restyled. - Tokens: oklch palette + dark/light + brand-hue var ported into app/index.css via Tailwind v4 @theme + plain :root / [data-theme] blocks. Custom variant wires Kumo's dark: utilities to data-theme. - Theme/hue: zustand store gains theme/hue/accent fields with manual localStorage persistence; /theme-boot.js applies persisted prefs to <html> before hydration so first paint has no FOUC. - Primitives: VerdictPill, ScoreRing, Sparkline, Logo, status/verdict tone helpers under app/components/phishpilot. - Shell: replaces Sidebar+Header with PhishPilot's 232px sidebar (org switcher, primary nav with active accent stripe, pipeline status pill, user row + theme toggle) and the 52px topbar (search + Ask co-pilot). - Cases queue: serif H1, segmented tab strip with counts, hairline table, status pill per row, mono case IDs and ages. - Case detail: score ring, status pill, observable IOC rows, resolution controls, co-pilot/pipeline-trace placeholders. Score is a deliberate placeholder until the security pipeline writes per-case scoring. - Routes: /dashboard and /hub stubs added; mailbox index now lands on Dashboard instead of inbox. - Dev: vite.config.ts now allows fs serving from the parent checkout so worktree dev sessions can resolve symlinked node_modules. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * fix: keep mailbox index landing on inbox until dashboard is real Dashboard route is still a placeholder showing "—" for every KPI; redirecting the bare /mailbox/:id URL there would regress the UX for anyone hitting the root. Restore the original emails/inbox landing. The /dashboard and /hub routes remain reachable via direct nav. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* Support full Access cert URLs for TEAM_DOMAIN * Clarify Access troubleshooting in README * Link Access reset docs in troubleshooting * Revise Cloudflare deployment setup instructions Updated setup instructions for deploying to Cloudflare, including important notes and reordering of steps. --------- Co-authored-by: Thomas Gauvin <tgauvin@cloudflare.com> Co-authored-by: Thomas Gauvin <35609369+thomasgauvin@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* chore: rename project to PhishSOC across code and docs Closes #17 modulo the deferred Cloudflare resource renames. Replaces the Agentic-Inbox-era names from main and the PhishPilot branding shipped in #21 with PhishSOC. PhishSOC speaks the buyer's vocabulary directly (SOC == Security Operations Center) where Pilot was generic AI-assistant framing in a crowded namespace. User-visible name strings: - README.md heading + intro + screenshot alt text - hub/README.md project description - package.json name + cloudflare label - package-lock.json name (top-level + root package) - app/root.tsx <title> - app/routes/home.tsx meta title - workers/mcp/index.ts MCP server name Brand identity from #21: - app/components/phishpilot/ -> app/components/phishsoc/ (5 imports updated) - Logo wordmark "Phish/Pilot" -> "Phish/SOC" + comment - index.css comments - useUIStore STORAGE_KEY "phishpilot-ui" -> "phishsoc-ui" + comment - public/theme-boot.js localStorage key (must match useUIStore) Localstorage migration: changing the storage key resets persisted theme/hue prefs for any preview users. SSR defaults (dark / Rust) take over so first paint is unchanged; only manual customizations are lost. Acceptable at POC stage. Intentionally untouched (per #17 deferral): - wrangler.jsonc worker name + R2 bucket_name / preview_bucket_name (worker rename = new deploy; R2 not in-place renameable) - README.md R2 bucket setup instructions (match deferred bucket name) - README.md Deploy-to-Cloudflare button URL (still upstream cloudflare/agentic-inbox; moves with the GitHub repo rename) - pp- CSS class prefix (internal-only styling; rename is pure churn) Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: point Deploy to Cloudflare button at schmug/PhishSOC Repo was renamed from schmug/agentic-inbox to schmug/PhishSOC. Deploy button now provisions the rebranded fork instead of upstream Cloudflare so the README is internally consistent (clicking "Deploy to Cloudflare" under a PhishSOC heading shouldn't deploy a different project). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: add GitHub Actions, CodeQL, and Dependabot
Adds three baseline checks under .github/ chosen for low cost on PRs:
- ci.yml: typecheck + test on PRs to main and pushes to main. Single
Ubuntu/Node 20 runner with npm cache, 15-min timeout, paths-ignore
for docs-only changes, and concurrency that cancels superseded PR
runs (but not main).
- codeql.yml: javascript-typescript scan with security-and-quality
query suite. Runs on JS/TS PR diffs, pushes to main, and a weekly
Monday cron. 20-min timeout.
- dependabot.yml: weekly grouped npm + actions updates. Tiptap (~25
packages), react/react-router, and Cloudflare/wrangler are grouped
to collapse what would otherwise be 30+ weekly PRs into ~5; capped
at 5 npm + 3 actions PRs open.
No matrix and no separate build job — typecheck covers compile-time
correctness and deploy is run locally, so we don't pay for it twice.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(ui): annotate toggleTheme local so theme keeps Theme type
The conditional `get().theme === "dark" ? "light" : "dark"` widened to
`string` when spread into the prefs object, so `savePrefs(next)` was
called with `{ theme: string, ... }` against `PersistedPrefs` (TS2345).
Annotating the local as `Theme` preserves the literal narrowing through
the spread.
Surfaced by the new CI typecheck gate added in the previous commit.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#35 added paths-ignore to CI and a paths allowlist to CodeQL to skip docs-only changes. With branch protection now requiring both checks (plus enforce_admins), a PR that only touches the ignored paths never triggers either workflow — the required checks stay pending forever and the merge is blocked even for admins. Drop both filters. Trade-off: - Cost: ~37s of Typecheck & Test + ~1m18s of CodeQL on every PR, including docs-only ones. Negligible on a public repo with free Actions minutes. - Benefit: required checks always fire, branch protection works honestly, no chance of a workflow-skip × required-check deadlock, and no ghost-check companion workflow to keep in sync as the CI matrix grows. This is what kubernetes, rust-lang, react, and most large OSS projects do for the same reason. The ghost-check pattern is the right answer when CI is genuinely expensive (long matrix builds); for a 37-second job it's over-engineering with a real maintenance footgun. Unblocks #40 and any future docs-only PR. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds the GitHub-side hygiene that complements the CI/CodeQL/Dependabot baseline in #35: - SECURITY.md routes vuln reports through GitHub Security Advisories - CODEOWNERS auto-requests review on every change - PR template locks in the Summary / Why / Test plan structure - bug_report and feature_request issue forms with required fields, config.yml disables blank issues and points security reports to the private advisory flow Docs/config only — no source paths touched. CI on this PR still runs (these files are not in the workflow paths-ignore). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [postcss](https://github.com/postcss/postcss) from 8.5.8 to 8.5.12. - [Release notes](https://github.com/postcss/postcss/releases) - [Changelog](https://github.com/postcss/postcss/blob/main/CHANGELOG.md) - [Commits](postcss/postcss@8.5.8...8.5.12) --- updated-dependencies: - dependency-name: postcss dependency-version: 8.5.12 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
Bumps [hono](https://github.com/honojs/hono) from 4.12.8 to 4.12.15. - [Release notes](https://github.com/honojs/hono/releases) - [Commits](honojs/hono@v4.12.8...v4.12.15) --- updated-dependencies: - dependency-name: hono dependency-version: 4.12.15 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
Bumps [dompurify](https://github.com/cure53/DOMPurify) from 3.3.3 to 3.4.0. - [Release notes](https://github.com/cure53/DOMPurify/releases) - [Commits](cure53/DOMPurify@3.3.3...3.4.0) --- updated-dependencies: - dependency-name: dompurify dependency-version: 3.4.0 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
The top-of-file tagline and first two paragraphs still inherited the
upstream cloudflare/agentic-inbox framing ("a self-hosted email client
with an AI agent"), which underplays what PhishSOC actually does. The
rest of the README — Features, Stack, Security, Architecture — already
covers the security pipeline; only the lede was stale.
- Tagline now leads with "phishing-aware email SOC" and names the
scoring pipeline (SPF/DKIM/DMARC + URL/RDAP).
- First paragraph rewrites the email-client framing as a phishing-
detection layer wrapped around the Cloudflare-native mail stack, and
inlines the pipeline stages so the value prop is visible above the
fold.
- Agent paragraph reflects what the rest of the doc already says: 9
tools, MCP-exposed, send-confirmation always required.
No claims added that aren't already documented elsewhere in the README.
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [drizzle-orm](https://github.com/drizzle-team/drizzle-orm) from 0.45.1 to 0.45.2. - [Release notes](https://github.com/drizzle-team/drizzle-orm/releases) - [Commits](drizzle-team/drizzle-orm@0.45.1...0.45.2) --- updated-dependencies: - dependency-name: drizzle-orm dependency-version: 0.45.2 dependency-type: direct:production ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.23 to 4.18.1. - [Release notes](https://github.com/lodash/lodash/releases) - [Commits](lodash/lodash@4.17.23...4.18.1) --- updated-dependencies: - dependency-name: lodash dependency-version: 4.18.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
Bumps [@hono/node-server](https://github.com/honojs/node-server) from 1.19.11 to 1.19.14. - [Release notes](https://github.com/honojs/node-server/releases) - [Commits](honojs/node-server@v1.19.11...v1.19.14) --- updated-dependencies: - dependency-name: "@hono/node-server" dependency-version: 1.19.14 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 6.4.1 to 6.4.2. - [Release notes](https://github.com/vitejs/vite/releases) - [Changelog](https://github.com/vitejs/vite/blob/v6.4.2/packages/vite/CHANGELOG.md) - [Commits](https://github.com/vitejs/vite/commits/v6.4.2/packages/vite) --- updated-dependencies: - dependency-name: vite dependency-version: 6.4.2 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
Bumps [picomatch](https://github.com/micromatch/picomatch) from 4.0.3 to 4.0.4. - [Release notes](https://github.com/micromatch/picomatch/releases) - [Changelog](https://github.com/micromatch/picomatch/blob/master/CHANGELOG.md) - [Commits](micromatch/picomatch@4.0.3...4.0.4) --- updated-dependencies: - dependency-name: picomatch dependency-version: 4.0.4 dependency-type: indirect ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
* Bump undici, @cloudflare/vite-plugin and wrangler Bumps [undici](https://github.com/nodejs/undici) to 7.24.8 and updates ancestor dependencies [undici](https://github.com/nodejs/undici), [@cloudflare/vite-plugin](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/vite-plugin-cloudflare) and [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together. Updates `undici` from 7.18.2 to 7.24.8 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](nodejs/undici@v7.18.2...v7.24.8) Updates `@cloudflare/vite-plugin` from 1.29.0 to 1.34.0 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Changelog](https://github.com/cloudflare/workers-sdk/blob/main/packages/vite-plugin-cloudflare/CHANGELOG.md) - [Commits](https://github.com/cloudflare/workers-sdk/commits/@cloudflare/vite-plugin@1.34.0/packages/vite-plugin-cloudflare) Updates `wrangler` from 4.74.0 to 4.86.0 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.86.0/packages/wrangler) --- updated-dependencies: - dependency-name: "@cloudflare/vite-plugin" dependency-version: 1.34.0 dependency-type: direct:development - dependency-name: undici dependency-version: 7.24.8 dependency-type: indirect - dependency-name: wrangler dependency-version: 4.86.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> * fix(types): drop now-redundant @ts-expect-error on llama model string wrangler 4.86 / @cloudflare/workers-types now includes "@cf/meta/llama-3.1-8b-instruct-fast" in the generated Ai.run() model union, so the two @ts-expect-error suppressions become "unused" directives (TS2578) and fail typecheck. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* ci: extend Typecheck & Test to cover the hub/ subpackage Two pieces: 1. Adds a `Typecheck & Test (hub)` job to .github/workflows/ci.yml that runs `npm ci`, `npm run typecheck`, and `npm test` inside `hub/`. Uses setup-node's `cache-dependency-path` so each subpackage gets its own npm cache key. Runs in parallel with the existing root verify job. 2. Refreshes hub/package-lock.json. Discovered while writing the new job: `npm ci` on hub failed with "Missing: @emnapi/core@1.9.2 from lock file" — the lockfile had drifted from package.json on main. This is why hub tests have never been in CI: nobody could run them reproducibly. The refresh adds one missing transitive (@emnapi/core under @rolldown/binding-wasm32-wasi) and clears one stale `peer` flag. After this lands, every PR touching the hub/ Worker (including any future Dependabot bumps) will be exercised against hub's own Vitest suite (currently 42 tests across 6 files) and tsc, instead of getting a false-green from the root pipeline. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * ci: use npm install for hub-verify (lenient on platform drift) The first hub-verify run failed on Linux because hub's lockfile carries platform-specific optional deps that resolve differently on macOS vs Linux runners. Switch to `npm install` so CI is resilient to this drift. Goal of hub-verify is "hub's code typechecks and tests pass" — root CI's `npm ci` is what gates byte-exact reproducibility for the deployable artifact. The hub Worker is deployed via wrangler from a fresh install anyway. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bumps [undici](https://github.com/nodejs/undici) to 7.24.8 and updates ancestor dependency [wrangler](https://github.com/cloudflare/workers-sdk/tree/HEAD/packages/wrangler). These dependencies need to be updated together. Updates `undici` from 5.29.0 to 7.24.8 - [Release notes](https://github.com/nodejs/undici/releases) - [Commits](nodejs/undici@v5.29.0...v7.24.8) Updates `wrangler` from 3.114.17 to 4.86.0 - [Release notes](https://github.com/cloudflare/workers-sdk/releases) - [Commits](https://github.com/cloudflare/workers-sdk/commits/wrangler@4.86.0/packages/wrangler) --- updated-dependencies: - dependency-name: undici dependency-version: 7.24.8 dependency-type: indirect - dependency-name: wrangler dependency-version: 4.86.0 dependency-type: direct:development ... Signed-off-by: dependabot[bot] <support@github.com> Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: schmug <38227427+schmug@users.noreply.github.com>
…imit (#54) * feat(hub): rate-limit POST /orgs/accept against token brute-force Adds a Workers Rate Limiting binding (10 req/min/IP) on the public invite-redemption endpoint. Without it, an attacker who learns a hub URL can brute-force invite tokens (43-char base64url space, but no rate cap is no rate cap). Authenticated routes are unchanged — they already require a hashed API key. Also wires the D1 database_id so this branch deploys cleanly without needing a manual edit per the README bootstrap steps. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(hub): log upstream status + body on inbound sync fetch failure Previously, sync.ts only recorded "upstream returned non-OK on page N" when a peer's restSearch failed. That message is the same for HTTP 401 (bad API key), 522 (CF couldn't connect), 526 (TLS chain incomplete), and a network exception — distinguishing them required reading code or attaching a tail. Operators were burning hours diagnosing peer onboarding. Now logs include: - the request URL - HTTP status + statusText - first 400 bytes of the response body - the underlying exception message when fetch() itself throws This is `console.warn`, so it's tail-visible without changing log levels in prod. The DB-recorded `last_error` shape is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(hub): preflight probe on POST /admin/peers with TLS hint catalog Without a probe, the only way to learn that a peer is misconfigured was to wait up to 5 min for the cron to fire and then check D1 for last_error. The error itself was opaque (see prior commit). Operators hit this hard during real onboarding — a 30-second TLS misconfig burned half an hour of triage time. The probe is a single restSearch (limit: 1) issued from the Worker during peer creation. It runs after the sharing-group check, before any DB inserts, with a 10s timeout. On failure, the admin gets HTTP 400 with a structured response: { "error": "preflight_failed", "probe": { "stage": "secret" | "fetch" | "status", "status": <number?>, "body_snippet": <string?>, "error": <string?>, "hint": <string?> } } The hint catalog handles the cases I've seen in practice or expect: HTTP 526 (incomplete cert chain — most common nginx misconfig), 525 (handshake), 522 (CF can't reach origin), 401/403 (bad API key), 404 (wrong base_url or non-MISP), and bot-challenge bodies. Each hint is one sentence, copy-pastable, with the verification command where applicable. Escape hatch: `skip_probe: true` in the body. Lets operators register a peer that's currently down (e.g. waiting for the upstream to fix their chain) and gives tests a way to bypass the outbound fetch. Tests cover all three failure stages plus the skip path. Nothing is written to D1 on probe failure — the existing `peers` / `inbound_peers` / `orgs` rows stay clean. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-draft & model picker (#59) * docs: add PhishSOC UI continuation design spec Plans the three-PR sequence for finishing the PhishSOC visual-language migration on remaining surfaces and shipping the two UI-touching open issues: #19 (loading/error states) and #11 (auto-draft toggle + agent model picker). Defers infra rename (worker, R2 bucket), README link cleanup, and onboarding rebuild to follow-up work. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * docs: add PhishSOC UI continuation implementation plan Three-PR plan with bite-sized tasks: PR A token sweep + dead-code, PR B loading/error states (#19), PR C settings auto-draft + model (#11). Discovered no frontend test runner exists, so PR A/B validate via typecheck + manual smoke and PR C ships workers tests for the new schema. Toast helper uses kumo's actual variant: "error" API. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * refactor: delete unused Header and Sidebar components Both replaced by app/components/phishsoc/Shell. No remaining importers. * fix(types): restore @ts-expect-error on llama model string The @cf/meta/llama-3.1-8b-instruct-fast model is not present in the AiModels union generated by the current wrangler version, so the ai.run(...) call sites need the suppression. The directives were removed in ab66b01 when the model was briefly in the union; PR #47's dependency bump (undici, vite-plugin, wrangler) regenerated worker-configuration.d.ts without the model and re-introduced the typecheck failure on main. Restoring the directives unblocks CI for this branch and any other branches based on current main. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * style(home): swap kumo classes to PhishSOC tokens, add Logo header * style(settings): swap kumo classes to PhishSOC tokens * style(email-list): swap kumo classes to PhishSOC tokens * style(search-results): swap kumo classes to PhishSOC tokens * style(routes): finish kumo→token sweep across remaining routes * style(verdict-badge): swap kumo classes to PhishSOC tokens * style(mailbox-split-view): swap kumo classes to PhishSOC tokens * style(email-attachment-list): swap kumo classes to PhishSOC tokens * style(rich-text-editor): swap kumo classes to PhishSOC tokens * style(mcp-panel): swap kumo classes to PhishSOC tokens * style(security-settings-panel): swap kumo classes to PhishSOC tokens * style(email-panel-header): swap kumo classes to PhishSOC tokens * style(email-panel-toolbar): swap kumo classes to PhishSOC tokens * style(email-panel-dialogs): swap kumo classes to PhishSOC tokens * style(email-panel-security-verdict): swap kumo classes to PhishSOC tokens * style(email-panel-single-message): swap kumo classes to PhishSOC tokens * style(email-panel-thread-message): swap kumo classes to PhishSOC tokens * style(email-panel): swap kumo classes to PhishSOC tokens * style(compose-panel): swap kumo classes to PhishSOC tokens * style(compose-email): swap kumo classes to PhishSOC tokens * style(agent-panel): swap kumo classes to PhishSOC tokens * style(agent-sidebar): swap kumo classes to PhishSOC tokens * style(root): swap kumo classes to PhishSOC tokens * Revert "fix(types): restore @ts-expect-error on llama model string" This reverts commit 21694ad. * feat(lib): add useFeedback toast helper Wraps kumo's useKumoToastManager so call sites stop repeating the {variant: 'error'} shape and so the toast surface is centralized for future style/layout changes. * feat(compose): route compose toasts through useFeedback Replaces direct useKumoToastManager calls in useComposeForm with the new feedback helper so the toast surface is centralized. ComposeEmail's send and save-draft buttons were already disabled while isSending is true, so no wiring change was needed there. No behavior change beyond the toast plumbing. * feat(agent-panel): toast on agent chat errors Wires onError on useAgentChat to surface failures via the feedback helper. Without this, a failed request silently leaves the chat in a non-streaming state with no user feedback. * feat(email-list): toast on mutation errors Wires onError on fire-and-forget mutations (move/delete/mark-read/star) in EmailPanel and email-list so failures surface as toasts instead of silent no-ops. Migrates existing toastManager.add sites to the shared useFeedback helper. * feat(search-results): row-shaped skeleton + error toast Replaces the single Loader with a 5-row pulsing skeleton that matches the search-result row layout, and surfaces query failures via the feedback helper. * refactor(toasts): route remaining sites through useFeedback Migrates ReportPhishButton, settings, home, and case-detail off direct useKumoToastManager calls so the toast surface is fully centralized. * feat(search-results): toast on row-click mark-read failure Fire-and-forget mark-as-read mutation in search-results lacked onError, unlike its email-list counterpart. Closes the gap so silent failures don't slip through. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> * feat(shared): add MailboxSettings zod schema with autoDraft + agentModel * test(settings): cover MailboxSettings defaults and passthrough * feat(workers): gate auto-draft dispatch on settings.autoDraft.enabled Reads the per-mailbox MailboxSettings blob from R2 and skips the agent onNewEmail dispatch when autoDraft.enabled is false. Defaults to true when settings are absent or malformed (preserves existing behavior). Adds workers/lib/mailbox-settings.ts as a shared helper used by both workers/index.ts (auto-draft gate) and workers/agent/index.ts (model + prompt selection — see follow-up commit). * feat(agent): read agentModel from per-mailbox settings Both streamText (chat) and generateText (auto-draft) now use settings.agentModel. Defaults to the existing kimi-k2.5 model when the field is absent — no migration required for existing mailboxes. * feat(settings): add Behavior card with auto-draft toggle and model picker Closes upstream issue #11. Hand-curated model list with custom free-text fallback; validates @cf/ prefix client-side. Defaults preserve existing behavior for already-configured mailboxes (autoDraft enabled, current default model). * feat(settings): explicit error for empty custom model Reviewer flagged that picking "Custom…" and leaving the input blank silently saved as the default Kimi model. Add a guard so the user gets an explicit toast and the save bails. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
#70) Closes #65. The existing vitest config only ran Node-only suites under `tests/` and `test/`, so frontend behavior (mutation pending states, toast assertions, form interactions) could not be covered in tests. PRs #60 and #61 had to validate via `npm run typecheck` plus manual smoke. Adds a second vitest project with `environment: "jsdom"`, RTL, and jest-dom matchers. Two starter test files exercise the harness: - `feedback.test.tsx` — smallest possible jsdom + RTL check; mocks kumo's toast manager and asserts `useFeedback` forwards the right shape. - `settings-behavior.test.tsx` — renders `SettingsRoute` via a `MemoryRouter`, mocks `useMailbox` / `useUpdateMailbox`, exercises the auto-draft toggle + agent-model picker (including the two validation error paths) end-to-end against the real `<Toasty>`. Implementation notes: - Uses Vitest 4's `projects` config (the path forward; the issue body suggested `environmentMatchGlobs` but that was deprecated in v3). - `~/*` and `shared/*` are wired via explicit `resolve.alias` rather than `vite-tsconfig-paths`. The plugin works in `vite.config.ts` but does not propagate into nested project configs in Vitest 4 — explicit aliases are the documented escape hatch. - `tests/frontend/**` added to `tsconfig.cloudflare.json` includes so `tsc -b` actually picks it up. Verified by introducing a deliberate type error and confirming the probe failed before adding the include and passed after. - `esbuild.jsx: "automatic"` set explicitly so `.tsx` test files don't need a `React` import (the project tsconfig declares this but vite doesn't follow project references). The kumo `Input must have an accessible name` warnings in test stderr are pre-existing in `app/routes/settings.tsx` and not introduced here. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…nt (#73) Closes #56. Replaces the POC stub at `app/routes/dashboard.tsx` (hardcoded sparkline, `—` placeholders) with a real Operations dashboard backed by a single aggregation endpoint. Flips `mailbox-index.tsx` back to land on the dashboard — the inbox stays one click away via the Shell's "Mail review" nav item. Backend (`workers/`): - New DO method `MailboxDO.getDashboardSummary()` runs five indexed queries against the existing schema and returns a single payload. - New route `GET /api/v1/mailboxes/:mailboxId/dashboard` composes the DO result with two pure helpers (`bucketThreatPressure`, `pipelineSuccessRate`) in `workers/lib/dashboard-aggregation.ts`. - Migration `11_dashboard_indexes` adds `idx_cases_updated_at` to support the `updated_at >= now-24h` filters. Cards: - Threats blocked · 24h — count of cases with `status = 'closed-tp'` closed in the last 24h. - Open cases — count of `cases.status = 'open'`. - Pipeline success · 24h — completed / (completed + failed) over `emails.deep_scan_status` for the last 24h, or `—` when no runs. - Hub contributions · 24h — local proxy on `cases.shared_to_hub`. - Threat pressure sparkline — 12 × 2-hour buckets of the count of emails with verdict in {tag, quarantine, block} over the last 24h. - Recent cases — top 5 by `updated_at desc`, each linking to `/mailbox/:id/cases/:caseId`. Frontend (`app/`): - `useDashboardSummary` hook + `api.getDashboardSummary`. - New `DashboardSummary` and `DashboardCase` types. - Loading state (kumo Loader), error state with retry that calls React Query's refetch, and empty-state per-card copy. Spec deltas (issue acceptance vs. ship): - "p95 latency" card scope-cut to "% success" — pipeline doesn't yet log per-run timing. Follow-up: #71. - "Hub corroboration" card scope-cut to "Hub contributions" (local signal). Follow-up: #72. - Issue says "loader endpoint(s) under `app/routes/api.*`"; this stack has its API in the worker, so the endpoint lives in `workers/index.ts`. Functionally equivalent. - Auth scoping: `requireMailbox` is path-only today (pre-existing per #27); the dashboard endpoint inherits that scope. Tests: - 10 unit tests for `bucketThreatPressure` + `pipelineSuccessRate` pure helpers. - 5 frontend tests on `DashboardRoute` (loading / error+retry / KPIs / empty-card copy / case link). - 180 tests pass overall (was 165). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Closes #57. Replaces the "Hub coming soon" placeholder at `app/routes/hub.tsx` with three real panels backed by worker proxy endpoints. The browser can't talk to the MISP hub directly because the hub API key is a worker secret, so each panel call goes through `GET /api/v1/mailboxes/:id/hub/*` which resolves the per-mailbox `intel.hub` config and forwards. Backend (`workers/`): - New helper `workers/lib/hub-config.ts` with `loadHubConfig` + `loadHubCredentials`. Replaces the inline `loadHubSettings` from `routes/cases.ts` (now deleted) — single source of truth. - `MispClient` extended with `searchEvents`, `listSharingGroups`, and a scoped variant of `fetchDestroyList({ sharingGroup })`. - New `workers/routes/hub-ui.ts` exposes `/contributions`, `/destroylist`, `/sharing-groups` under `/api/v1/mailboxes/:mailboxId/hub`. Each returns `{ configured: true, data }` or `{ configured: false }` so the UI renders one empty state for the not-configured case. Frontend (`app/`): - New types `HubContribution`, `HubSharingGroup`, `HubEnvelope<T>`. - New `useHubContributions`, `useHubDestroylist`, `useHubSharingGroups` hooks (60s staleTime — hub data isn't volatile). - `app/routes/hub.tsx` rebuilt with three panels (My contributions, Destroylist preview, Sharing groups), per-panel loading / empty / error states, and a global "Hub not configured" hint that flips to if any panel reports `configured: false`. The hint links to the mailbox settings. Sharing-group visibility is enforced server-side by the hub (`hub/src/routes/feeds.ts:31-36` returns 403 to non-members, `events.ts:168-172` filters restSearch by `sharing_group_orgs` membership). A new MispClient test asserts the client doesn't silently widen a 403'd scoped destroylist request to the unscoped endpoint. Spec deltas (issue acceptance vs. ship): - Invite flow (button → modal → POST /orgs/invite → one-time token UX) cut to its own PR. Filed as #74. The acceptance bullet "Invite flow" is the only one not addressed in this PR. - Auth scoping for the proxy endpoints inherits the same path-only `requireMailbox` check the rest of the worker uses (#27 unchanged). Tests: - 7 unit tests for `MispClient` (`searchEvents`, `listSharingGroups`, `fetchDestroyList` happy path + scoped + 403-no-widen). - 4 frontend tests for `HubRoute` (not-configured hint, populated panels, per-panel error+retry, partial loading). - 191 tests pass overall (was 180). Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…it (#76) Closes #58 (mostly — see "Out of scope" below). Issue #58 had two reach goals once the bigger token migration shipped in #21/#59: replace the legacy `VerdictBadge` with the new `phishsoc/VerdictPill`, and confirm no hard-coded hex/rgb colors leaked into the SOC chrome. Both done here. VerdictBadge → VerdictPill: - New `verdictActionToPill(action)` helper in `app/components/phishsoc/verdict.ts` maps the security pipeline's `verdict.action` onto a `{tone, label}` pair. Returns null for `allow` so clean rows stay un-pilled, matching previous behavior. - `VerdictPill` gained an optional `title` prop so the email-list call site can carry the verdict's `explanation` as a hover tooltip — no loss of information vs. the old badge. - `app/routes/email-list.tsx` now renders an inline `EmailVerdictPill` component that composes `parseVerdict` + `verdictActionToPill` + `VerdictPill` with the same shield iconography (block/quarantine → ShieldWarning, tag → Shield). - `app/components/VerdictBadge.tsx` deleted. Hex/rgb audit: - `grep -nE '#[0-9a-fA-F]{3,8}\b|rgba?\(|hsla?\(' app/components app/routes` is now clean except for `app/components/EmailIframe.tsx`, which styles the email body inside a fully-sandboxed iframe (CSP locks down external resources, no same-origin → the SOC tokens aren't reachable). Added a comment in EmailIframe explaining why those hex values are intentionally distinct from the app's design system. Tests: 5 unit tests for `verdictActionToPill`. 196 pass overall (was 191). Out of scope for this PR (still open from #58): - AgentSidebar / AgentPanel co-pilot affordance restyle to match the topbar entry point — that's UX work, not a token sweep, and benefits from a separate dedicated review. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The global search input in `app/components/phishsoc/Shell.tsx` was wired to nothing — pressing Enter, typing, or ⌘K all did nothing. The placeholder also promised "indicators, cases" but the backend at /api/v1/mailboxes/:mailboxId/search only searches emails. This change: - Wraps the input in a `<form role="search">` with an onSubmit handler that navigates to `/mailbox/:id/search?q=<encoded>` when the trimmed query is non-empty and a mailboxId is present. - Adds a global Cmd+K / Ctrl+K listener that focuses the input from any screen, matching the placeholder's promise. - Replaces the placeholder copy with "Search emails… ⌘K" to match what the backend can actually search. Tests: new `tests/frontend/shell-search.test.tsx` covers Enter-to-navigate, URL encoding, empty-input no-op, both Cmd+K and Ctrl+K focus, and placeholder honesty. Full suite: 202 passing. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The pill in `app/components/phishsoc/Shell.tsx` was a static-green "Pipeline online" indicator that ignored real pipeline state — a misinformation bug worse than no indicator. The "p50 —" placeholder beside it advertised latency the chrome had no data to back. Wired the pill to the existing `useDashboardSummary` hook, mapping `pipelineSuccess` (0..1, or null) to four states: - >=0.95 → "Pipeline online" (safe, pulse) - >=0.5 → "Degraded" (suspect, no pulse) - < 0.5 → "Pipeline failing" (danger, no pulse) - null → "No data" (muted, no pulse) Loading and error states fall through to "No data" — never green — so an outage during page load can't masquerade as healthy. Pill is now a button that navigates to the mailbox dashboard, with `role=status` + `aria-live=polite` + `aria-label` for screen readers. Dropped the fake "p50 —" text; real latency is tracked separately (#71) and will land with the pipeline-runs aggregation. Tests: new `tests/frontend/shell-pipeline-pill.test.tsx` covers the four state transitions, navigation on click, no-fake-green during loading, and absence of fabricated latency. Full suite: 209 passing. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ngsPanel (#83) Two security settings existed in the worker but had no UI: attachment_policy (executable/container/macro actions + custom blocklist) and folder_policies (per-folder skip_classifier / skip_all / treat_as_verified). Operators had no way to change them without editing R2 directly. This PR adds two collapsible sections to the existing SecuritySettingsPanel, extends the zod schema in shared/mailbox-settings.ts to validate the new shapes (passthrough at every level so unrelated security fields like allowlist_senders round-trip untouched), and wires safeParse + 400 into the PUT /api/v1/mailboxes/:id route. The runtime consumer in workers/security/settings.ts (getSecuritySettings) already merges defaults field-by-field; no consumer wiring was needed. Schema tests cover defaults preservation, valid policies, invalid enums (400 path), forward-compatible folder names, and the passthrough regression guard. UI tests assert save round-trip for executable_action, custom blocklist, skip_classifier mode, and treat_as_verified. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Author
|
Wrong remote — re-creating on schmug/PhishSOC |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Two security settings existed in the worker but had no UI — operators couldn't change
attachment_policy(executable / container / macro-Office filetype actions + a custom blocklist) orfolder_policies(per-folderskip_classifier/skip_all/treat_as_verifiedbypasses) without editing R2 directly. This PR adds two collapsible sections to the existingSecuritySettingsPanel. Closes #83.What changed
shared/mailbox-settings.ts): typedsecuritysub-shape addsattachment_policy(passthrough) andfolder_policies(z.recordof passthrough). Every level uses.passthrough()so unrelated security fields (allowlist_senders, thresholds, business_hours, …) round-trip untouched. No defaults set in the schema — the runtime consumer inworkers/security/settings.tsis the single source of defaults; duplicating them here would invite drift.workers/index.ts):MailboxSettings.safeParse→400 { error, issues }on bad input. Schema-level test (rejects an invalid attachment action enum) covers the same validation path the route calls.app/types/index.ts): addedAttachmentAction,AttachmentPolicySettings,FolderPolicySettings; extendedSecuritySettingsto include the two new fields.app/components/SecuritySettingsPanel.tsx): two new<details>sections — radio groups for executable/container/macro actions plus a custom-blocklist comma input; per-folder rows (driven bySYSTEM_FOLDER_IDS) with checkboxes forskip_classifier,treat_as_verified, and a red-styledskip_allwarning.modeis mutex on the server side, soskip_allandskip_classifierinterlock client-side: turning onskip_alldisables theskip_classifiercheckbox.workers/security/triage.ts:97and:137already readsettings.folder_policiesandsettings.attachment_policy;workers/security/settings.ts:145-152already merges attachment_policy defaults field-by-field.Note: PUT now normalizes settings on disk
The PUT route used to write
body.settingsverbatim; it now writesMailboxSettings.parse(...)output. So a partial PUT like{settings: {agentSystemPrompt: "x"}}now stores{agentSystemPrompt: "x", autoDraft: {enabled: true}, agentModel: "@cf/moonshotai/kimi-k2.5"}. The frontend already merges these on the client; this just normalizes the at-rest shape.Test plan
npm test— 220 passing, 0 failing (11 new: 7 schema + 4 UI; baseline was 209)npm run typecheck— cleanexecutable_action: "lol"produces a zod issue with path.executable_action(the route's only added behavior is the 400 wrapper)allowlist_sendersandattachment_policyround-trips both fields (passthrough)npm run devrequires Cloudflare Access service-token credentials per #89; component-level UI tests assert the save round-trip for executable_action, custom blocklist,skip_classifiermode, andtreat_as_verifiedDefaults preserve current behavior
Existing mailboxes with no
attachment_policy/folder_policiesblob continue to work —getSecuritySettingsalready mergesDEFAULT_ATTACHMENT_POLICYfield-by-field andfolder_policies?.[folderId]returnsundefinedfor unconfigured folders. No migration.Out of scope (separate follow-ups)
parseList(typed comma-lists collapse partial entries) — pre-existing, surfaced while writing tests; flagged as a separate task🤖 Generated with Claude Code