From 2bb1817e8d6b3edfc1e9e3522ab223fcdcb52a89 Mon Sep 17 00:00:00 2001 From: lorentz Date: Wed, 15 Jul 2026 14:31:10 -0400 Subject: [PATCH] docs(17-01): complete Mimecast blast-radius lookup plan Mark BLAST-01/BLAST-02 complete in REQUIREMENTS.md and add the plan's SUMMARY.md documenting getBlastRadius() delivery. Co-Authored-By: Claude Sonnet 5 --- .planning/REQUIREMENTS.md | 8 +- .../17-01-SUMMARY.md | 102 ++++++++++++++++++ 2 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 .planning/phases/17-mimecast-blast-radius-lookup/17-01-SUMMARY.md diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 7388e59..fd527f3 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -50,11 +50,11 @@ destructive remediation gated behind explicit human approval. ### Blast Radius (Mimecast) -- [ ] **BLAST-01**: The system can query a Mimecast blast-radius abstraction for +- [x] **BLAST-01**: The system can query a Mimecast blast-radius abstraction for message delivery data (matched/delivered/held/rejected/clicked counts, per-recipient status) when Mimecast is configured, keyed on message ID, sender, recipient/reporter, subject, and date window -- [ ] **BLAST-02**: When Mimecast is not configured, the system records +- [x] **BLAST-02**: When Mimecast is not configured, the system records `status: unavailable` for that lookup and classification proceeds using ticket/email evidence alone — it never blocks on missing Mimecast config @@ -159,8 +159,8 @@ Populated during roadmap creation. | CAMP-01 | Phase 18 | Pending | | CAMP-02 | Phase 18 | Pending | | CAMP-03 | Phase 18 | Pending | -| BLAST-01 | Phase 17 | Pending | -| BLAST-02 | Phase 17 | Pending | +| BLAST-01 | Phase 17 | Complete | +| BLAST-02 | Phase 17 | Complete | | CLASSIFY-01 | Phase 19 | Pending | | CLASSIFY-02 | Phase 19 | Pending | | CLASSIFY-03 | Phase 19 | Pending | diff --git a/.planning/phases/17-mimecast-blast-radius-lookup/17-01-SUMMARY.md b/.planning/phases/17-mimecast-blast-radius-lookup/17-01-SUMMARY.md new file mode 100644 index 0000000..765189e --- /dev/null +++ b/.planning/phases/17-mimecast-blast-radius-lookup/17-01-SUMMARY.md @@ -0,0 +1,102 @@ +--- +phase: 17-mimecast-blast-radius-lookup +plan: 01 +subsystem: api +tags: [mimecast, redis-cache, blast-radius, phishing-triage, vitest] + +# Dependency graph +requires: + - phase: 15-data-model-detection-ticket-evidence + provides: migrations/097_phishing_triage_schema.sql (classifications.reasons JSONB — the eventual persistence point for a blast-radius result, not touched by this phase) +provides: + - "isMimecastConfigured() config gate + _resetMimecastClient() test seam in lib/services/mimecast-client.ts" + - "getBlastRadius() orchestration in lib/services/mimecast-blast-radius.ts — normalized matched/delivered/held/rejected/clicked counts + perRecipient status, never-throwing, Redis-cached (5min TTL)" +affects: [19-classifier-service] + +# Tech tracking +tech-stack: + added: [] + patterns: + - "isConfigured() config-gate convention extended to mimecast-client.ts (matches pax8-factory.ts/veeam-factory.ts)" + - "Never-throw orchestration wrapper: config gate returns synchronously, unexpected errors degrade to a discriminated-union 'unavailable' result instead of propagating" + - "Redis cache-key format :: (mirrors app/api/addigy-devices/route.ts)" + +key-files: + created: + - lib/services/mimecast-blast-radius.ts + - lib/services/mimecast-blast-radius.test.ts + - lib/services/mimecast-client.test.ts + modified: + - lib/services/mimecast-client.ts + +key-decisions: + - "Fan-out (searchDeliveredMessages + getHeldMessages + getThreatEvents) runs unconditionally, not as a fallback after getMessageInfo — per corrected D-01, getMessageInfo() has no status/count data" + - "clicked is best-effort from getThreatEvents() analysis[] click-type match; documented in code as not a confirmed-zero signal (D-02)" + - "Delivered-message .status rejection classification treats unrecognized strings as non-rejected (conservative) since the real enum (17-RESEARCH A3) is unconfirmed; raw values logged at console.debug for first-real-tenant validation" + - "Single global getMimecastClient() only for v1 — per-company mimecast_tenants/getMimecastClientForTenant() explicitly out of scope, documented as D-05 known limitation in the module doc-comment" + +patterns-established: + - "Blast-radius abstraction is a pure function: config-gate → cache-check → fan-out → merge → cache-write → return; every branch returns a typed discriminated union, never throws" + +requirements-completed: [BLAST-01, BLAST-02] + +# Metrics +duration: ~20min +completed: 2026-07-15 +--- + +# Phase 17 Plan 01: Mimecast Blast Radius Lookup Summary + +**New `getBlastRadius()` orchestration composes Mimecast's searchDeliveredMessages/getHeldMessages/getThreatEvents into normalized matched/delivered/held/rejected/clicked counts + per-recipient status, gated by a new `isMimecastConfigured()` and Redis-cached for 5 minutes — never throws, degrades to `status: 'unavailable'` on missing config or unexpected error.** + +## Performance + +- **Duration:** ~20 min +- **Completed:** 2026-07-15T18:30:12Z +- **Tasks:** 2 completed +- **Files modified:** 4 (1 modified, 3 created) + +## Accomplishments +- `isMimecastConfigured()` + `_resetMimecastClient()` added to `mimecast-client.ts`, matching the project's `isConfigured()` factory convention (mirrors `pax8-factory.ts`) +- `lib/services/mimecast-blast-radius.ts`: pure, never-throwing `getBlastRadius()` that fans out to the three existing `MimecastClient` methods unconditionally (not gated on `getMessageInfo`), merges into a normalized shape, and short-circuits on a Redis cache hit (5-min TTL, matching D-04) +- Both new test files (13 tests total) pass; full `npm test` shows no new failures (2 pre-existing, unrelated `itglue-search.test.ts` failures logged to `deferred-items.md`, out of scope for this plan) + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Add isMimecastConfigured() config gate + _resetMimecastClient() test seam + tests** - `8b032c3` (feat) +2. **Task 2: Build mimecast-blast-radius.ts orchestration (fan-out merge, never-throw, Redis cache) + tests** - `efbc437` (feat) + +_Both tasks were tdd="true"; test files were authored alongside the implementation in the same commit per the plan's task grouping (test file + implementation in one commit, matching the plan's `` grouping rather than separate RED/GREEN commits)._ + +## Files Created/Modified +- `lib/services/mimecast-client.ts` - Added `isMimecastConfigured()` + `_resetMimecastClient()`; `getMimecastClient()` and `getMimecastClientForTenant()` untouched +- `lib/services/mimecast-client.test.ts` - New: covers `isMimecastConfigured()` env-var combinations + `getMimecastClient()` throw/cache/reset behavior +- `lib/services/mimecast-blast-radius.ts` - New: `BlastRadiusInput`/`BlastRadiusResult` types + `getBlastRadius()` orchestration +- `lib/services/mimecast-blast-radius.test.ts` - New: covers not-configured short-circuit, cache-hit short-circuit, fan-out merge (delivered/rejected/held/clicked/perRecipient), never-throw-on-error, and unknown-recipient classification + +## Decisions Made +- Fan-out is unconditional (corrected D-01) — `getMessageInfo()` is called only as a supplementary body/header fetch when `messageId` is present, and never gates whether the fan-out runs +- `clicked` best-effort derivation and the D-05 multi-tenant known-limitation are both documented directly in the module's doc-comment, per the plan's acceptance criteria (`grep -qi "D-05"` passes) +- Delivered-message rejection status classification is conservative (unrecognized → non-rejected) since the real Mimecast `.status` enum is unconfirmed (17-RESEARCH A3); raw values are logged via `console.debug` for future validation against a real tenant, not hardcoded as a guessed enum + +## Deviations from Plan + +None — plan executed exactly as written. Both tasks' `` and `` requirements were implemented as specified; all acceptance criteria (exports present, D-05 comment present, test/tsc/npm-test green) verified directly. + +## Issues Encountered + +Full `npm test` run surfaces 2 pre-existing failures in `lib/services/analyzer/itglue-search.test.ts` (`client.getFlexibleAssetsForOrganization is not a function`), unrelated to any file this plan touched. Verified via `git log` that the last commit affecting those files (`a0a6e7f`) predates this plan's work. Logged to `.planning/phases/17-mimecast-blast-radius-lookup/deferred-items.md` per the SCOPE BOUNDARY rule (only auto-fix issues directly caused by the current task's changes) — not fixed here. + +## User Setup Required + +None - no external service configuration required. This phase reuses the existing `MIMECAST_CLIENT_ID`/`MIMECAST_CLIENT_SECRET`/`REDIS_URL` env vars already documented in CLAUDE.md; no new env vars, migrations, or dependencies were introduced. + +## Next Phase Readiness + +`getBlastRadius({ messageId?, sender, recipient, subject, dateWindow })` is ready for Phase 19's classifier to call directly — it never throws, and its `BlastRadiusResult` discriminated union (`status: 'ok' | 'unavailable'`) is fully typed. Known, documented limitation: v1 uses only the single global env-var Mimecast client, not per-company `mimecast_tenants` — Phase 19 (or a later phase) should be aware that companies with their own registered Mimecast tenant will see `status: 'unavailable'` from this abstraction even when Mimecast is otherwise configured for them. + +--- +*Phase: 17-mimecast-blast-radius-lookup* +*Completed: 2026-07-15*