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 <noreply@anthropic.com>
This commit is contained in:
lorentz 2026-07-15 14:31:10 -04:00
parent efbc437e2e
commit 2bb1817e8d
2 changed files with 106 additions and 4 deletions

View file

@ -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 |

View file

@ -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:
- "is<Name>Configured() 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 <service>:<resource>:<discriminators> (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 `is<Name>Configured()` 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 `<files>` 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' `<action>` and `<behavior>` 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*