- Adds 23-04-SUMMARY.md documenting Task 1 (page) and Task 2 (index tile)
- Records that Task 1's human-verify checkpoint was approved via a
temporary production container rebuild rather than local npm run dev
- Company table with search + type filter (cloned from client-scope pattern)
- Three independent Switch toggles per row: auto-parse, auto-classify, auto-report
- toggle() PATCHes /api/admin/phishing-automation/{companyId} with all three current flags
- Helper caption clarifies stage dependency (informational, not enforced)
- triggerPhishingDetection now captures groupReportIntoCampaign's result and,
when a campaignId exists, calls new runGatedPhishingStages
- runGatedPhishingStages reads the per-company automation gate and
conditionally runs parseAndStoreMessage, classifyCampaign, and (only for
USER_AWARENESS verdicts) generateAndPostAcknowledgment
- each stage isolated in its own try/catch (T-23-09); detection + grouping
remain unconditional (D-07); auto_report never posts any other action
(D-04, T-23-08)
- remediateApprovedActions now captures the transaction's RemediateResult,
then post-commit checks whether an approved acknowledge_user row was
transitioned this pass (alreadyCompleted === false) and, if so, calls
generateAndPostAcknowledgment(campaignId) exactly once
- Call happens outside the DB transaction (network I/O hazard) and is
wrapped in its own try/catch that logs and swallows failures -- the DB
transition has already committed
- Every other action type (block_sender, purge_message, warn_user,
reset_password, isolate_endpoint, disable_forwarding_rule, quarantine)
remains a simulated status-only transition, unchanged
- Updated top-of-file D-01 doc comment to record the narrow D-04 carve-out
- Tests: acknowledge_user IS posted once when remediated, NOT called for
block_sender/warn_user-only remediation, NOT called on idempotent re-run
of an already-completed acknowledge_user row, and a post rejection does
not propagate out of remediateApprovedActions
- New generateAndPostAcknowledgment(campaignId) posts a short, appreciative
thank-you note to every ticket linked to a campaign, using noteType 18
(Client Portal Note, verified live against tenant's TicketNotes field
metadata) so the note is customer-visible; publish stays 1 unchanged
- Body is a fixed template with zero evidence/URL/classification
interpolation (T-23-01) -- not the evidence-dump formatTriageNote() template
- Mirrors generateAndPostTriageNote's per-ticket try/catch-in-loop error
isolation and { noteText, tickets } return shape
- Tests: noteType 18 + publish 1 payload assertion, per-ticket failure
isolation, and zero-linked-reports case
- GET /api/admin/phishing-automation: admin-gated list with COALESCE(...,false) gate defaults
- PATCH /api/admin/phishing-automation/[companyId]: upserts all three flags, actor+timestamp stamped
- DELETE /api/admin/phishing-automation/[companyId]: reverts company to all-OFF default
- Mirrors app/api/admin/company-scope/* route pattern
- Add USER_AWARENESS to the Verdict union in campaign-classifier.ts
- mapVerdictToActions('USER_AWARENESS') returns ['acknowledge_user']; not added to DESTRUCTIVE_ACTIONS so requires_approval computes false
- classifyCampaign's simulation branch now assigns verdict = 'USER_AWARENESS' directly instead of falling through to evaluateSpamVsUnwanted
- deriveDefaultParams('acknowledge_user') returns {} (no operator-editable params)
- Widen TriageNoteEvidence.verdict to admit 'USER_AWARENESS' (pure type widen, no formatting change)
- Tests: classifier simulation fixtures now assert USER_AWARENESS/acknowledge_user/requiresApproval=false; new mapVerdictToActions/computeRequiresApproval/deriveDefaultParams cases
- Add USER_AWARENESS to classification-entry verdict union and VERDICT_TINT (emerald, matching ClassificationCard)
- Widen campaign_classified audit-case cast to include USER_AWARENESS
- Add defensive fallback on both .split(' ') tint sites so any unrecognized runtime verdict string can never crash the render (T-23-12) — phishing-timeline.ts emits verdict as an unvalidated plain string
- Add acknowledge_user: 'Acknowledge user' to ACTION_LABEL
- Add case 'acknowledge_user' to ActionParamsForm mirroring no_action (no-params form)
- Renders as a normal checkbox + Approve action for every company; no automation-gate logic added (D-04) — the auto-approval carve-out lives only in the webhook path (Plan 05)
- Add USER_AWARENESS to ClassificationCardData verdict union
- Add emerald VERDICT_VARIANT_CLASS entry for USER_AWARENESS (distinct from UNWANTED amber and THREAT destructive)
- Add acknowledge_user: 'Acknowledge user' to ACTION_LABEL
Checker warning: creating a new '## v3 Requirements' top-level section would
misleadingly read as deferred/future work (that's what '## v2 Requirements'
means in this file). Phase 23 is active v3.0 roadmap work -- instruct adding
a subsection under the existing '## v1 Requirements' heading instead,
mirroring Phase 22's existing subsection.
Verified against TicketNotes/entityInformation/fields on the real tenant:
publish has no client-facing value (1=All Autotask Users, 2=Internal Project
Team, 4=Internal & Co-Managed -- all internal-staff tiers). Client Portal
visibility is controlled by noteType=18 ("Client Portal Note"), not publish.
Corrects D-03's original "flip the publish flag" framing before planning.
Scoping a dedicated "User Awareness" verdict for confirmed phishing-simulation
reports (KnowBe4/Breach Secure Now) plus a per-company automation gate for the
parse/classify/report-to-ticket pipeline stages. Surfaced live during Phase 22
review of a real Breach Secure Now report (ticket 699415).
classifications.confidence is 0.0-1.0 (1 = max confidence). ClassificationCard
appended "%" directly to the raw value, so confidence=1 rendered as "1%
confidence" -- read as near-zero, the opposite of its true meaning. Found live
while reviewing a Breach Secure Now simulated-phishing classification.
Mimecast blast-radius fixes: future end-date no longer silently swallowed
into a false-clean result, and campaigns from companies with their own
registered mimecast_tenants row (e.g. Seubert & Associates) now resolve
against that tenant instead of always querying the global Wulf tenant.
Documents the getBlastRadius per-tenant injection/swallowed-error fixes,
the campaign detail route's date-window clamp + tenant resolution, the
in-scope test-hygiene deviation, and the pre-existing out-of-scope
itglue-search.test.ts failure.
Bug 1: clamp dateWindow.end to Math.min(createdAt + 24h, Date.now()) so a
freshly-detected campaign (<24h old primary report) never sends Mimecast a
future end-date -- previously rejected as err_track_and_trace_invalid_end_date
and swallowed internally as a false-clean zero-count result.
Bug 2 (D-05): add company_id to the reports SELECT and, when the reporting
company has its own enabled mimecast_tenants row, resolve a tenant-scoped
client via getMimecastClientForTenant() and thread it into getBlastRadius as
{ client, cacheScope: companyId }. Falls back to the global env-configured
client when no company-specific tenant is registered.
Covers the already-implemented per-tenant factory: returns a MimecastClient
instance, builds a new independent instance per call (never the cached
global), doesn't affect getMimecastClient()'s singleton, and defaults
base_url when omitted. Uses fake credentials only.
- getBlastRadius(input, options?) accepts an optional injected MimecastClient
and cacheScope; an injected client bypasses the global isMimecastConfigured()
gate since it carries its own credentials
- cache key namespaced by cacheScope to prevent cross-tenant collisions
- deliveredResult.error (previously swallowed) now rethrown so the outer
catch converts it to status: unavailable / reason: lookup_failed --
defense-in-depth against Bug 1 (future end-date rejected by Mimecast)
- test mock hygiene: getMimecastClientMock now cleared in beforeEach
- Fake tenant client via options.client bypasses getMimecastClient
- Injected tenant client runs fan-out even when global env unconfigured
- searchDeliveredMessages error field now expected to degrade to unavailable/lookup_failed
All 6 plans merged: pure-logic extraction (resolver/default-params/timeline),
API read surface, evidence/classification/timeline cards, action-area card,
and the ticket-scoped review page + campaigns list + nav entry. Blocking
LiveLink checkpoint verified against a real Autotask ticket (numeric ticket
ID confirmed, no ticket_number fallback needed).
Task 3 (blocking human-verify) resolved as "verified": a real production
Autotask LiveLink click against ticket 699340 confirmed the numeric-
ticket-id resolver assumption, no ticket_number fallback needed. Marks
REVIEW-01, REVIEW-05, REVIEW-06 complete in REQUIREMENTS.md.
- app/phishing/page.tsx: minimal DataTable-backed campaigns list, row click
navigates to /phishing/tickets/{firstReportTicketId}, EmptyState when no
campaigns exist yet
- components/navigation/app-navigation.tsx: add flat "Phishing" nav item
(ShieldAlert icon) immediately after PAX8, visible to all roles (every
role has phishing:read)
- app/phishing/tickets/[ticketId]/page.tsx: resolves ticket->campaign via
the plan-02 resolver route, drives a loading/not-triaged/ungrouped/ready/
error state machine, branches ready into grouped-but-unclassified
(Classify CTA, no ClassificationCard/ActionAreaCard) vs. classified (all
four cards with explicit props), refetches after every action (D-04),
session-only auth (no token/query-param scheme)
- app/api/phishing/reports/[report_id]/route.ts (new, additive): thin
report-scoped evidence + fresh blast-radius lookup for the D-08
ungrouped-report state, which has no campaignId to key the existing
campaign-detail route on — added as a Rule 2 dependency since the plan's
own D-08 truth ("standalone-report notice + evidence") has no other data
source