- 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
- 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)
- 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
ticket_notes and time_entries both carry an is_deleted soft-delete flag
(per CLAUDE.md audit-column convention); gatherTicketEvidence was reading
both without filtering it, so retracted notes and reversed time entries
showed up as evidence for every phishing report. Found during code-review
re-verification of the Phase 15 CR-01/WR-01/WR-02 fixes.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012wWroM6FXkQJiH3JgYcony
- triggerPhishingDetection() mirrors triggerWorkflowEngine's payload.entity-first shape
- reads createdByContactID (Autotask field) into created_by_contact_id, per entity-mapper.ts:211
- called alongside the existing workflow-engine trigger, not awaited in the request path
- gatherTicketEvidence: company_name, ticket_notes, time_entries (all
parameterized $1 queries), and Autotask attachment metadata only
(fullPath/title/contentType, never base64 data); Autotask call wrapped in
try/catch so a failure degrades to an empty attachments array
- detectPhishingTicket: matches, hashes, checks D-04 idempotency guard
(skips re-gathering/writing when content_hash is unchanged), then upserts
one reports row via ON CONFLICT (ticket_id) DO UPDATE ... RETURNING id
- requester_contact_id binds from ticket.contact_id, created_by_contact_id
from ticket.created_by_contact_id per interfaces contract
- KNOWN_PHISHING_PATTERNS: the 8 locked DETECT-01 strings
- matchesPhishingPatterns: case-insensitive substring match (toLowerCase +
includes only, no RegExp/eval), mirrors robotic-classifier.evaluateContains
- computePhishingContentHash: sha256 over title+description only (D-04),
excludes bump-prone fields like status/last_activity_date
- Migration 097 applied and verified in dev DB (7 tables, idempotent re-run confirmed)
- SUMMARY.md documents schema design decisions and worktree-path caveat with scripts/apply-migrations.sh
- 7-table schema: campaigns, reports, messages, indicators, classifications, remediation_actions, audit_events
- reports table fully designed with ticket_id FK, content_hash (D-04 idempotency), matched_patterns, evidence (EVID-01) columns
- All tables/indexes use IF NOT EXISTS for idempotent re-application
- Remaining 6 tables are stubs for Phases 16-21