Commit graph

964 commits

Author SHA1 Message Date
e76ab1bf43 docs(18-01): complete campaign grouping core + phishing permission resource plan
Summary for Phase 18 Plan 01 — groupReportIntoCampaign tiered matching,
self-exclusion safeguard, and the phishing permission resource. Requirements
CAMP-01, CAMP-02, ACCESS-01 complete.
2026-07-15 19:25:18 -04:00
04ec51f370 feat(18-01): add phishing permission resource + role grants
- statement gets the full D-05 vocabulary now: read/analyze/approve/remediate
- superAdminRole and adminRole grant read+analyze
- userRole grants read only (cannot trigger /analyze)
- approve/remediate declared but ungranted to any role until Phase 20
2026-07-15 19:23:09 -04:00
da926bbe97 feat(18-01): implement groupReportIntoCampaign tiered matching
- Tiered find-or-create inside postgresClient.transaction (Pitfall 2 —
  campaigns.campaign_key has no UNIQUE constraint): Tier 1 Message-ID,
  Tier 2 attachment-hash/URL-domain + subject + sender + 24h, Tier 3
  sender + normalized subject + client + 24h (CAMP-01)
- Match path bumps report_count/last_seen_at and links reports.campaign_id
  without creating a second campaign; no-match path inserts a new
  campaigns row keyed by the strongest available tier signal (CAMP-02)
- skipIfAlreadyGrouped short-circuits before the transaction (D-08); the
  /analyze route path always re-runs full tiered matching
- Every tier query excludes the report's own id (r.id != $n) so a
  self-match against a report's own messages/indicators can never
  double-increment its already-linked campaign on re-run
- D-07 doc comment states the Tier-3-only automatic-path limitation:
  parseAndStoreMessage is not wired into the webhook/cron path this phase
2026-07-15 19:22:31 -04:00
ea677b7aba test(18-01): add campaign-grouping tier-key helpers + test scaffold
- normalizeSubject strips repeated Re:/Fwd:/Fw: prefixes case-insensitively, lowercases, trims (D-03)
- extractUrlDomain returns hostname or null (never throws) for malformed URLs (Pitfall 4)
- test file mocks ./postgres-client before import, mirroring phishing-eml-service.test.ts's discipline
2026-07-15 19:19:44 -04:00
78c984dceb docs(state): record phase 18 planning session 2026-07-15 16:51:02 -04:00
a494cd0c0b docs(18): fix plan-checker warnings (tier self-exclusion + resolved open questions) 2026-07-15 16:50:46 -04:00
808604e861 docs(18): create phase plan (3 plans, 2 waves) 2026-07-15 16:45:29 -04:00
ffdd86a0ed docs(18): add pattern map 2026-07-15 16:37:37 -04:00
1c6d2fdde5 docs(phase-18): add validation strategy 2026-07-15 16:34:20 -04:00
e05d34640c docs(18): add research findings + EML-parsing scope decision 2026-07-15 16:33:49 -04:00
4ccd379dc3 docs(18): research phase domain 2026-07-15 16:22:25 -04:00
0ec690b261 docs(state): record phase 18 context session 2026-07-15 15:39:03 -04:00
a37dcebd2d docs(18): capture phase context 2026-07-15 15:39:03 -04:00
4decefb940 docs(requirements): mark EVID-02/03/04 complete (phase 16 gap) 2026-07-15 15:19:59 -04:00
3fe1c02570 docs(phase-17): mark phase complete 2026-07-15 14:34:03 -04:00
fc7e47532b docs(phase-17): update tracking after wave 1 2026-07-15 14:31:59 -04:00
30bfa127bb chore: merge executor worktree (worktree-agent-a5e56aaa233a4df21) 2026-07-15 14:31:40 -04:00
eaee6292dc docs(17-01): record self-check result in SUMMARY.md
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 14:31:22 -04:00
2bb1817e8d 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>
2026-07-15 14:31:10 -04:00
efbc437e2e feat(17-01): build mimecast-blast-radius.ts fan-out orchestration
- Add getBlastRadius(): never-throwing orchestration that fans out to
  searchDeliveredMessages + getHeldMessages + getThreatEvents (D-01,
  unconditional fan-out) and merges into normalized matched/delivered/
  held/rejected/clicked counts + perRecipient status array
- Config gate (BLAST-02): returns status:'unavailable' reason:'not_configured'
  synchronously when Mimecast is unconfigured, never constructs the client
- Redis-backed 5-min cache (D-04) via redis-client.ts, short-circuits before
  any MimecastClient call on hit
- clicked derived best-effort from getThreatEvents() analysis[] (D-02);
  documents the /api/ttp/url/get-logs limitation in code
- Documents D-05 known limitation: single global getMimecastClient() only,
  not per-company mimecast_tenants
- Unrecognized delivered-message status strings treated conservatively as
  non-rejected (A3 unconfirmed enum), raw values logged at debug level
- Add lib/services/mimecast-blast-radius.test.ts covering config gate,
  cache-hit short-circuit, fan-out merge, never-throw-on-error, and
  unknown-recipient classification
- Log pre-existing unrelated itglue-search.test.ts failures to
  deferred-items.md (out of scope for this plan)
2026-07-15 14:30:00 -04:00
8b032c3890 feat(17-01): add isMimecastConfigured() config gate + test seam
- Add isMimecastConfigured() to lib/services/mimecast-client.ts mirroring
  the pax8-factory.ts is<Name>Configured() convention
- Add _resetMimecastClient() test seam so tests can isolate env-var state
- Add lib/services/mimecast-client.test.ts covering config gate + throw/cache
  behavior of getMimecastClient()
2026-07-15 14:27:47 -04:00
683d90edd0 docs(state): record phase 17 planning session 2026-07-15 13:48:04 -04:00
3424812d60 docs(17): fix plan-checker warnings (merge formula clarity + resolved open questions) 2026-07-15 13:47:48 -04:00
7303b16cbb docs(17): create phase plan (1 plan, 1 wave) 2026-07-15 13:44:14 -04:00
78ff39fa48 docs(17): add pattern map 2026-07-15 13:39:21 -04:00
c702cb9b03 docs(phase-17): add validation strategy 2026-07-15 13:35:57 -04:00
9dbc1e7510 docs(17): add research findings + tenant-scope decision 2026-07-15 13:35:21 -04:00
cacf41a493 docs(17): research phase domain 2026-07-15 13:28:58 -04:00
e30d9af46d docs(state): record phase 17 context session 2026-07-15 13:13:37 -04:00
09ff5b9bb8 docs(17): capture phase context 2026-07-15 13:13:31 -04:00
50bb8e2ac9 docs(phase-16): mark phase complete 2026-07-15 10:45:47 -04:00
40268e709f docs(phase-16): update tracking after wave 2 2026-07-15 10:42:00 -04:00
cad1a63e3d chore: merge executor worktree (worktree-agent-adbfd43ed3e884d1d) 2026-07-15 10:41:39 -04:00
135d448127 docs(16-03): complete EML/MIME evidence orchestration service plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 10:41:15 -04:00
ce16e6747c test(16-03): phishing-eml-service.test.ts orchestration coverage (mocked I/O)
- mocks postgresClient, getAutotaskClient, and B2 client (isB2Configured/presignUpload); no real Postgres/Autotask/B2 credentials or network
- happy path: one messages insert with D-06 auth verdicts in headers, indicators inserted
- no-eml no-op: NO_EML_ATTACHMENTS -> stored:false, no messages insert
- B2 unconfigured: raw_ref null, no presignUpload/PUT
- B2 configured: raw_ref = phishing/{reportId}/{attachmentId}.eml, exactly one PUT
- no-network invariant: fetch never called with a message-body URL from the fixture
- attachment_hash indicator metadata carries filename/contentType/size/related
- all fixtures reused from eml-parser.fixtures.ts (synthetic only)
2026-07-15 10:39:59 -04:00
5088d8d511 feat(16-03): phishing-eml-service.ts orchestration (list->select->fetch->B2->parse->persist)
- parseAndStoreMessage lists ticket attachments, selects the original message via selectOriginalMessage
- fetches full content via getAttachmentContent, size-guards the decoded buffer against MAX_EML_BYTES
- uploads raw bytes to B2 under phishing/{reportId}/{attachmentId}.eml when isB2Configured(), gracefully degrading rawRef=null on failure/absence
- parses via parseEml and persists one messages row (headers incl. D-06 auth verdicts, urls, attachments, body_preview, raw_ref) plus indicators rows (attachment_hash/url/sender) carrying D-07 metadata JSONB
- never fetches any URL extracted from the message; the only outbound calls are the Autotask attachment GET and the B2 presigned PUT
2026-07-15 10:37:35 -04:00
36ef9e7ce3 docs(phase-16): update tracking after wave 1 2026-07-15 10:33:49 -04:00
f3ace33f86 chore: merge executor worktree (worktree-agent-a2b7d8cafac0f4270) 2026-07-15 10:32:42 -04:00
49dd22bbd6 chore: merge executor worktree (worktree-agent-a692342f48398a2c2) 2026-07-15 10:32:38 -04:00
d7c868a2d7 docs(16-01): complete EML/MIME evidence parser core plan
Summary of Plan 16-01: mailparser + linkify-it dependency install,
selectOriginalMessage (EVID-02 three-tier selection), and parseEml +
parseAuthResults + extractUrls + buildBodyPreview (EVID-03/EVID-04),
with a DoS size guard ahead of simpleParser. 26/26 tests pass, tsc
clean. Logs 2 pre-existing unrelated itglue-search.test.ts failures to
deferred-items.md (out of scope, not fixed).
2026-07-15 10:31:45 -04:00
654e624505 feat(16-01): implement parseEml, parseAuthResults, extractUrls, buildBodyPreview
GREEN: parseEml normalizes headers (From/Reply-To/Return-Path/To/Cc/
Subject/Date/Message-ID), builds the ordered Received chain from
mail.headerLines, and maps mailparser attachments to AttachmentMeta
(name/content-type/size/sha256 checksum, related flag preserved for
inline/CID parts per Pitfall 5).

parseAuthResults hand-rolls RFC 8601 Authentication-Results parsing
(spf/dkim/dmarc verdicts) rather than using mailauth, which performs
live DNS/HTTP verification (SC#3 violation). Both Authentication-Results
and Authentication-Results-Original are read via mail.headerLines
(Pitfall 4 — headers Map only exposes one occurrence of a repeated
header) and parsed into distinct authResults/authResultsOriginal fields.

extractUrls uses linkify-it with fuzzyLink enabled (scheme-less www.
URLs) scanning both text and html parts, deduped, never dereferenced.

buildBodyPreview prefers mail.text, falling back to a small hand-rolled
HTML-to-text stripper (not the undeclared transitive html-to-text
dependency — see SUMMARY deviations) when only HTML exists; truncated
to 500 chars.

MAX_EML_BYTES (10 MB, below B2's 25 MB cap) is enforced before
simpleParser is ever called (T-16-01 DoS guard).

26/26 tests pass; tsc clean for eml-parser files; full npm test run
confirms 2 pre-existing itglue-search.test.ts failures are unrelated
(logged to deferred-items.md).
2026-07-15 10:30:18 -04:00
4df4816b21 test(16-01): add failing tests for parseEml/parseAuthResults/extractUrls/buildBodyPreview
RED: covers EVID-03 (normalized headers, structured auth verdicts,
Received chain, URLs, attachment metadata incl. related flag),
EVID-04 (no-network spy, truncated body preview), and the DoS size
guard (oversized buffer rejected before simpleParser). New synthetic
fixtures: rich multipart, auth-results-original, inline/CID attachment,
long-body, fuzzy-URL, and an oversized-buffer generator. None of these
exports exist on eml-parser.ts yet.
2026-07-15 10:28:22 -04:00
094d34c2c8 docs(16-02): append self-check result to summary 2026-07-15 10:25:00 -04:00
3f55e145e3 docs(16-02): complete Autotask attachment content / B2 EML regex / indicators metadata plan
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-15 10:24:47 -04:00
0bf37cef0c feat(16-02): add migration 099 — indicators.metadata JSONB (D-07)
- Nullable JSONB column, no backfill needed (no rows exist yet)
- Applied to the running dev DB (docker exec against pulse-postgres);
  Postgres init only runs migrations on first volume boot
2026-07-15 10:23:55 -04:00
8630fd5151 feat(16-02): add EML_OBJECT_KEY_REGEX + parameterize B2 key validation (D-05)
- New EML_OBJECT_KEY_REGEX enforces phishing/<id>/<id>.eml, rejects traversal
- presignDownload/presignUpload/downloadToBuffer take optional keyRegex,
  defaulting to OBJECT_KEY_REGEX so existing LogLift call sites are unchanged
- OBJECT_KEY_REGEX itself left untouched (skill-doc rule)
2026-07-15 10:23:19 -04:00
e4718ae71a feat(16-01): implement selectOriginalMessage three-tier attachment selection
GREEN: rfc.eml exact match -> single non-OriginatingEmail message/rfc822
candidate (covers KnowBe4 versioned filenames) -> OriginatingEmail.eml
fallback -> null. Case-insensitive on both content-type and filename
(checked via title/fullPath basename). All 8 selectOriginalMessage tests
pass; tsc clean for eml-parser files.
2026-07-15 10:23:00 -04:00
6de92a507b test(16-02): add failing tests for EML_OBJECT_KEY_REGEX + parameterized B2 key validation
- EML_OBJECT_KEY_REGEX must match phishing/<id>/<id>.eml and reject traversal/wrong-ext/LogLift shapes
- presignUpload must accept an optional keyRegex arg, defaulting to OBJECT_KEY_REGEX
2026-07-15 10:22:46 -04:00
9b65de72dc feat(16-02): add AutotaskClient.getAttachmentContent()
- Fetches Tickets/{id}/Attachments/{attachmentId}, reads response.items?.[0]
- Confirmed live: per-attachment-ID GET is list-shaped, not {item:...}
2026-07-15 10:22:03 -04:00
8be10db6e0 test(16-02): add failing test for AutotaskClient.getAttachmentContent
- Asserts items[0] convention for per-attachment-ID GET
- Asserts {item:...}-shaped response yields null (guards against regression)
2026-07-15 10:22:01 -04:00