fix(23): resolve checker blockers (timeline crash, acknowledge_user manual post) + doc/type-safety warnings
This commit is contained in:
parent
683e647984
commit
b50c1f5e1a
3 changed files with 135 additions and 16 deletions
|
|
@ -11,6 +11,9 @@ files_modified:
|
|||
- lib/services/remediation-default-params.test.ts
|
||||
- lib/services/triage-note-service.ts
|
||||
- lib/services/triage-note-service.test.ts
|
||||
- lib/services/triage-note-format.ts
|
||||
- lib/services/remediation-service.ts
|
||||
- lib/services/remediation-service.test.ts
|
||||
autonomous: true
|
||||
requirements: [CLASSDISP-01, CLASSDISP-02]
|
||||
must_haves:
|
||||
|
|
@ -19,6 +22,7 @@ must_haves:
|
|||
- "The USER_AWARENESS verdict recommends exactly the acknowledge_user action"
|
||||
- "acknowledge_user is NOT treated as destructive (requires_approval stays false for it)"
|
||||
- "acknowledge_user posts a customer-visible Autotask ticket note (noteType 18) with an appreciative thank-you body"
|
||||
- "When an approved acknowledge_user action is remediated (the manual path), a real customer-visible note is posted via generateAndPostAcknowledgment; the 7 existing action types remain simulated status-only transitions"
|
||||
artifacts:
|
||||
- path: "lib/services/campaign-classifier.ts"
|
||||
provides: "USER_AWARENESS verdict in Verdict union + acknowledge_user action mapping + simulation-branch assignment"
|
||||
|
|
@ -29,6 +33,9 @@ must_haves:
|
|||
- path: "lib/services/triage-note-service.ts"
|
||||
provides: "generateAndPostAcknowledgment writer using noteType 18"
|
||||
contains: "noteType: 18"
|
||||
- path: "lib/services/remediation-service.ts"
|
||||
provides: "acknowledge_user completion triggers a real note post; other action types unchanged"
|
||||
contains: "generateAndPostAcknowledgment"
|
||||
key_links:
|
||||
- from: "classifyCampaign isSimulation branch"
|
||||
to: "USER_AWARENESS verdict"
|
||||
|
|
@ -38,13 +45,17 @@ must_haves:
|
|||
to: "['acknowledge_user']"
|
||||
via: "new switch arm"
|
||||
pattern: "acknowledge_user"
|
||||
- from: "remediateApprovedActions acknowledge_user completion"
|
||||
to: "generateAndPostAcknowledgment(campaignId)"
|
||||
via: "post-commit call scoped to acknowledge_user only"
|
||||
pattern: "generateAndPostAcknowledgment"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Add the dedicated "User Awareness" classification disposition and its `acknowledge_user` delivery action. Today the classifier already detects phishing-simulation-vendor senders (KnowBe4 / Breach Secure Now) and skips the THREAT tier, but the result falls into the generic `UNWANTED` bucket with a `warn_user` action. This plan introduces a distinct 4th verdict `USER_AWARENESS`, maps it to a new non-destructive `acknowledge_user` action, and adds a triage-note-service writer that posts a customer-visible thank-you note.
|
||||
Add the dedicated "User Awareness" classification disposition and its `acknowledge_user` delivery action. Today the classifier already detects phishing-simulation-vendor senders (KnowBe4 / Breach Secure Now) and skips the THREAT tier, but the result falls into the generic `UNWANTED` bucket with a `warn_user` action. This plan introduces a distinct 4th verdict `USER_AWARENESS`, maps it to a new non-destructive `acknowledge_user` action, adds a triage-note-service writer that posts a customer-visible thank-you note, and wires that writer into the manual approve->remediate path so the note actually posts (the D-04 real-effect carve-out, scoped to `acknowledge_user` only).
|
||||
|
||||
Purpose: Correctly distinguish "an employee did the right thing by reporting a training simulation" from "unwanted marketing spam", and reward it with a customer-facing acknowledgment instead of a warning.
|
||||
Output: Extended `Verdict` union + action mapping, `acknowledge_user` default-params case, and `generateAndPostAcknowledgment()`.
|
||||
Output: Extended `Verdict` union + action mapping, `acknowledge_user` default-params case, `generateAndPostAcknowledgment()`, and the manual-path wiring in remediation-service.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
|
@ -82,7 +93,11 @@ From lib/services/triage-note-service.ts — existing TicketNotes write (lines 1
|
|||
`client.createEntity('TicketNotes', { ticketID, title, description: noteText, noteType: 1, publish: 1 })`
|
||||
`generateAndPostTriageNote(campaignId: string): Promise<TriageNoteResult>` — per-ticket try/catch loop over reports, returns `{ noteText, tickets: TriageNotePostResult[] }`.
|
||||
|
||||
From lib/services/triage-note-format.ts — `TriageNoteEvidence.verdict` (line 25) is currently `'SPAM' | 'UNWANTED' | 'THREAT' | null`; its only caller (triage-note-service.ts) force-casts the DB value via `as TriageNoteEvidence['verdict']`. verdict is only interpolated into note text here (no Record/switch lookup).
|
||||
|
||||
From lib/services/remediation-default-params.ts — `deriveDefaultParams(actionType, evidence)` exhaustive switch with `no_action -> {}` precedent.
|
||||
|
||||
From lib/services/remediation-service.ts — `remediateApprovedActions(campaignId, actor)` (lines 173-207) transitions status='approved' rows to 'completed' inside a `postgresClient.transaction`, returning `RemediateResult { campaignId, actions: RemediateResultAction[] }` where each action carries `{ id, actionType, status:'completed', alreadyCompleted }`. Per the top-of-file D-01 doc comment (lines 1-19), this file makes NO real provider call for any of the 7 existing action types — the "effect" is the simulated approved->completed transition. This plan adds ONE narrowly-scoped real effect for the new `acknowledge_user` action only.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
|
|
@ -90,12 +105,13 @@ From lib/services/remediation-default-params.ts — `deriveDefaultParams(actionT
|
|||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 1: Add USER_AWARENESS verdict + acknowledge_user action mapping</name>
|
||||
<files>lib/services/campaign-classifier.ts, lib/services/campaign-classifier.test.ts, lib/services/remediation-default-params.ts, lib/services/remediation-default-params.test.ts</files>
|
||||
<files>lib/services/campaign-classifier.ts, lib/services/campaign-classifier.test.ts, lib/services/remediation-default-params.ts, lib/services/remediation-default-params.test.ts, lib/services/triage-note-format.ts</files>
|
||||
<read_first>
|
||||
- lib/services/campaign-classifier.ts (Verdict union line 150, DESTRUCTIVE_ACTIONS line 153, mapVerdictToActions lines 164-186, computeRequiresApproval line 189, classifyCampaign simulation branch lines 449-490)
|
||||
- lib/services/campaign-classifier.test.ts (existing test structure + how verdict/action assertions are written)
|
||||
- lib/services/remediation-default-params.ts (switch lines 23-42)
|
||||
- lib/services/remediation-default-params.test.ts (existing case assertions)
|
||||
- lib/services/triage-note-format.ts (TriageNoteEvidence.verdict union line 25 — the narrow type to widen)
|
||||
- .planning/phases/23-classification-disposition-per-client-automation-gate/23-PATTERNS.md (campaign-classifier + remediation-default-params sections, watch-out flag #4)
|
||||
</read_first>
|
||||
<behavior>
|
||||
|
|
@ -105,7 +121,7 @@ From lib/services/remediation-default-params.ts — `deriveDefaultParams(actionT
|
|||
- deriveDefaultParams('acknowledge_user', evidence) returns {}
|
||||
</behavior>
|
||||
<action>
|
||||
Add the literal `'USER_AWARENESS'` to the `Verdict` union (line 150) so it becomes `'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS'`. The verdict string is LOCKED to `USER_AWARENESS` (planner decision, per D-01 which delegated exact string to discretion) — Plans 02 and 05 reference this exact literal, do not rename it. In `mapVerdictToActions`, add a `case 'USER_AWARENESS': return ['acknowledge_user'];` arm. Do NOT add `acknowledge_user` to `DESTRUCTIVE_ACTIONS` (watch-out flag #4) — its auto-approval carve-out (D-04) is enforced in the webhook path, not via this invariant, so `requires_approval` must compute false for it. In `classifyCampaign`'s `if (isSimulation)` branch, replace `verdict = evaluateSpamVsUnwanted(evidence);` with `verdict = 'USER_AWARENESS';` (per D-01/D-02 this is the exact code path the classifier already isolates for the allowlist match — no new branching). Keep the existing `reasons.push('Sender domain matches a known phishing-simulation vendor allowlist ...')`. In `remediation-default-params.ts`, add `case 'acknowledge_user': return {};` alongside the `no_action` precedent. Write/extend tests: add classifier assertions that a simulation fixture yields verdict USER_AWARENESS and actions ['acknowledge_user'] with requires_approval false; add a remediation-default-params assertion for the acknowledge_user empty-params case. Follow RED (add failing tests first) -> GREEN.
|
||||
Add the literal `'USER_AWARENESS'` to the `Verdict` union (line 150) so it becomes `'SPAM' | 'UNWANTED' | 'THREAT' | 'USER_AWARENESS'`. The verdict string is LOCKED to `USER_AWARENESS` (planner decision, per D-01 which delegated exact string to discretion) — Plans 02 and 05 reference this exact literal, do not rename it. In `mapVerdictToActions`, add a `case 'USER_AWARENESS': return ['acknowledge_user'];` arm. Do NOT add `acknowledge_user` to `DESTRUCTIVE_ACTIONS` (watch-out flag #4) — its auto-approval carve-out (D-04) is enforced in the webhook path, not via this invariant, so `requires_approval` must compute false for it. In `classifyCampaign`'s `if (isSimulation)` branch, replace `verdict = evaluateSpamVsUnwanted(evidence);` with `verdict = 'USER_AWARENESS';` (per D-01/D-02 this is the exact code path the classifier already isolates for the allowlist match — no new branching). Keep the existing `reasons.push('Sender domain matches a known phishing-simulation vendor allowlist ...')`. In `remediation-default-params.ts`, add `case 'acknowledge_user': return {};` alongside the `no_action` precedent. ALSO widen `TriageNoteEvidence.verdict` in `lib/services/triage-note-format.ts` (currently `'SPAM' | 'UNWANTED' | 'THREAT' | null`) to include `'USER_AWARENESS'` (WARNING 2 — the caller force-casts the DB verdict via `as TriageNoteEvidence['verdict']`, so once USER_AWARENESS exists at runtime the type must admit it). This is a pure type widen — verdict is only interpolated into note text in that formatter, never used in a Record/switch lookup, so no formatting/logic change is needed. Write/extend tests: add classifier assertions that a simulation fixture yields verdict USER_AWARENESS and actions ['acknowledge_user'] with requires_approval false; add a remediation-default-params assertion for the acknowledge_user empty-params case. Follow RED (add failing tests first) -> GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/stacks/pulse && npx vitest run lib/services/campaign-classifier.test.ts lib/services/remediation-default-params.test.ts && npx tsc --noEmit --pretty</automated>
|
||||
|
|
@ -114,10 +130,11 @@ From lib/services/remediation-default-params.ts — `deriveDefaultParams(actionT
|
|||
- `grep -c "USER_AWARENESS" lib/services/campaign-classifier.ts` >= 2 (union + branch assignment + action case)
|
||||
- `grep -q "case 'acknowledge_user'" lib/services/remediation-default-params.ts` succeeds
|
||||
- `grep -v '^#' lib/services/campaign-classifier.ts | grep -c "acknowledge_user" ` >= 1 and acknowledge_user is absent from the DESTRUCTIVE_ACTIONS set block (lines 153-158)
|
||||
- `grep -q "USER_AWARENESS" lib/services/triage-note-format.ts` succeeds in the TriageNoteEvidence.verdict union
|
||||
- vitest run for both test files passes; a test asserts verdict === 'USER_AWARENESS' and recommendedActions === ['acknowledge_user'] and requiresApproval === false for a simulation fixture
|
||||
- `npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>USER_AWARENESS is a first-class verdict producing the non-destructive acknowledge_user action; simulation campaigns classify as USER_AWARENESS; type-check and tests pass.</done>
|
||||
<done>USER_AWARENESS is a first-class verdict producing the non-destructive acknowledge_user action; simulation campaigns classify as USER_AWARENESS; the triage-note evidence type admits the new verdict; type-check and tests pass.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
|
|
@ -146,6 +163,38 @@ From lib/services/remediation-default-params.ts — `deriveDefaultParams(actionT
|
|||
<done>generateAndPostAcknowledgment posts a customer-visible (noteType 18) appreciative note per linked ticket with the same error-isolation semantics as the existing triage-note writer; tests and type-check pass.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto" tdd="true">
|
||||
<name>Task 3: Wire the acknowledge_user manual-path real note post into remediateApprovedActions</name>
|
||||
<files>lib/services/remediation-service.ts, lib/services/remediation-service.test.ts</files>
|
||||
<read_first>
|
||||
- lib/services/remediation-service.ts (top-of-file D-01 doc comment lines 1-19; remediateApprovedActions lines 173-207 — the approved->completed transition loop + RemediateResult return shape)
|
||||
- lib/services/triage-note-service.ts (generateAndPostAcknowledgment from Task 2 — signature + per-ticket error isolation)
|
||||
- lib/services/remediation-service.test.ts (queryMock/transactionMock/writeAuditEventMock mock setup lines 1-86; makeClient + stage() helpers; existing remediate tests staging remediationRows via `SELECT id::text, action_type, status ... FOR UPDATE`)
|
||||
- .planning/phases/23-classification-disposition-per-client-automation-gate/23-CONTEXT.md (D-04 — the carve-out applies to acknowledge_user ONLY; every other action stays manual/simulated)
|
||||
</read_first>
|
||||
<behavior>
|
||||
- remediateApprovedActions on a campaign whose approved rows include an acknowledge_user action calls generateAndPostAcknowledgment(campaignId) exactly once, after the transition
|
||||
- remediateApprovedActions on a campaign whose approved rows are only existing action types (e.g. block_sender, warn_user) does NOT call generateAndPostAcknowledgment
|
||||
- an acknowledge_user row that is already 'completed' (idempotent re-run — no approved->completed transition this pass) does NOT re-post
|
||||
- a generateAndPostAcknowledgment rejection does not propagate out of remediateApprovedActions (the DB transition already committed) — it is caught and logged
|
||||
</behavior>
|
||||
<action>
|
||||
Special-case `acknowledge_user` in `remediateApprovedActions` — the narrow D-04 real-effect carve-out for the ONE new non-destructive action, leaving all 7 existing action types (quarantine, block_sender, purge_message, warn_user, reset_password, isolate_endpoint, disable_forwarding_rule) as simulated status-only transitions (do NOT add a provider call for any of them). Do NOT call the Autotask writer inside the `postgresClient.transaction` callback (it does network I/O — a held transaction and a post-then-rollback are both hazards). Instead, capture the transaction's `RemediateResult` in a const, and AFTER the transaction returns (post-commit), check `result.actions.some(a => a.actionType === 'acknowledge_user' && a.alreadyCompleted === false)` — this is true only when an approved acknowledge_user row was transitioned this pass, and false for the already-completed idempotent case. When true, `await generateAndPostAcknowledgment(campaignId)` inside its OWN `try/catch` that does `console.error('[REMEDIATE] acknowledge_user note post failed', campaignId, err)` and swallows the error (the remediation already committed; generateAndPostAcknowledgment already isolates per-ticket failures internally). Import `generateAndPostAcknowledgment` from `'./triage-note-service'`. Update the top-of-file D-01 doc comment to record the carve-out: as of Phase 23 the ONLY real provider call in this file is the post-commit `acknowledge_user` customer-note post (a non-destructive thank-you — D-04); the original 7 action types remain simulated status-only transitions. In remediation-service.test.ts add `vi.mock('./triage-note-service', () => ({ generateAndPostAcknowledgment: (...args) => generateAndPostAcknowledgmentMock(...args) }))` with a top-level `const generateAndPostAcknowledgmentMock = vi.fn()` (reset in beforeEach), then add tests asserting: it IS called once with the campaignId when an approved acknowledge_user row is remediated; it is NOT called for a block_sender/warn_user-only remediation; and it is NOT called when the acknowledge_user row staged is already 'completed'. Follow RED (failing tests first) -> GREEN.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/stacks/pulse && npx vitest run lib/services/remediation-service.test.ts && npx tsc --noEmit --pretty</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -q "generateAndPostAcknowledgment" lib/services/remediation-service.ts` and it is imported from './triage-note-service'
|
||||
- The post call is guarded by `actionType === 'acknowledge_user'` AND `alreadyCompleted === false` and runs only after the transaction commits (outside the transaction callback)
|
||||
- No provider/Autotask call is added for any other action type (the acknowledge post is the only external call introduced in this file)
|
||||
- Tests assert generateAndPostAcknowledgment IS called for a remediated acknowledge_user action and is NOT called for a block_sender/warn_user-only remediation, and NOT called for an already-completed acknowledge_user row
|
||||
- `npx vitest run lib/services/remediation-service.test.ts` passes (existing REMED tests still green)
|
||||
- `npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>Approving then remediating an acknowledge_user action posts a real customer-visible note (the manual path is now functional) while every other action type keeps its simulated status-only behavior; tests and type-check pass.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
|
@ -153,25 +202,26 @@ From lib/services/remediation-default-params.ts — `deriveDefaultParams(actionT
|
|||
|
||||
| Boundary | Description |
|
||||
|----------|-------------|
|
||||
| Pulse -> Autotask API | acknowledge_user note text crosses to a customer-visible surface (client portal) |
|
||||
| Pulse -> Autotask API | acknowledge_user note text crosses to a customer-visible surface (client portal), on both the manual remediate path and the automatic webhook path (Plan 05) |
|
||||
|
||||
## STRIDE Threat Register
|
||||
|
||||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-23-01 | Information Disclosure | generateAndPostAcknowledgment note body | mitigate | Note body is a fixed appreciative template with zero evidence/URL/secret interpolation — nothing from the parsed email or classification reasons is placed in a customer-visible note |
|
||||
| T-23-02 | Tampering | acknowledge_user approval invariant | mitigate | acknowledge_user is deliberately kept OUT of DESTRUCTIVE_ACTIONS and its auto-post carve-out is scoped to the webhook path only (Plan 05); no destructive action inherits the exemption |
|
||||
| T-23-02 | Tampering | acknowledge_user approval invariant | mitigate | acknowledge_user is deliberately kept OUT of DESTRUCTIVE_ACTIONS; the real-effect carve-out in remediateApprovedActions is guarded by `actionType === 'acknowledge_user'` so no destructive action inherits a real provider call; the auto-post-without-approval carve-out is separately scoped to the webhook path only (Plan 05) |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx vitest run lib/services/campaign-classifier.test.ts lib/services/remediation-default-params.test.ts lib/services/triage-note-service.test.ts` passes
|
||||
- `npx vitest run lib/services/campaign-classifier.test.ts lib/services/remediation-default-params.test.ts lib/services/triage-note-service.test.ts lib/services/remediation-service.test.ts` passes
|
||||
- `npx tsc --noEmit --pretty` passes
|
||||
- USER_AWARENESS present in Verdict union and simulation branch; acknowledge_user maps and is non-destructive; note writer uses noteType 18
|
||||
- USER_AWARENESS present in Verdict union and simulation branch; acknowledge_user maps and is non-destructive; note writer uses noteType 18; remediating acknowledge_user posts a real note while other action types stay simulated
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- A simulation-vendor campaign classifies as USER_AWARENESS with the single acknowledge_user action and requires_approval false
|
||||
- generateAndPostAcknowledgment posts a customer-visible thank-you note (noteType 18, publish 1) per linked ticket with per-ticket error isolation
|
||||
- The manual approve->remediate path posts the acknowledge_user note for real; the 7 existing action types remain simulated status-only transitions
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ depends_on: []
|
|||
files_modified:
|
||||
- components/phishing/classification-card.tsx
|
||||
- components/phishing/action-area-card.tsx
|
||||
- components/phishing/timeline-card.tsx
|
||||
autonomous: true
|
||||
requirements: [CLASSDISP-03]
|
||||
must_haves:
|
||||
|
|
@ -14,6 +15,7 @@ must_haves:
|
|||
- "A campaign classified USER_AWARENESS renders in ClassificationCard with a distinct badge color (not UNWANTED's amber)"
|
||||
- "The acknowledge_user action renders with a human-readable label ('Acknowledge user') wherever action labels appear"
|
||||
- "In the manual review UI, acknowledge_user appears as a normal checkbox + Approve action for every company, with no gate-check logic"
|
||||
- "A USER_AWARENESS classification entry or campaign_classified audit entry renders in TimelineCard with a distinct (emerald) tint and does NOT crash the review page"
|
||||
artifacts:
|
||||
- path: "components/phishing/classification-card.tsx"
|
||||
provides: "USER_AWARENESS badge variant + acknowledge_user label"
|
||||
|
|
@ -21,6 +23,9 @@ must_haves:
|
|||
- path: "components/phishing/action-area-card.tsx"
|
||||
provides: "acknowledge_user ActionParamsForm case + label"
|
||||
contains: "acknowledge_user"
|
||||
- path: "components/phishing/timeline-card.tsx"
|
||||
provides: "USER_AWARENESS VERDICT_TINT entry + defensive .split fallback"
|
||||
contains: "USER_AWARENESS"
|
||||
key_links:
|
||||
- from: "ClassificationCardData['verdict'] union"
|
||||
to: "VERDICT_VARIANT_CLASS record"
|
||||
|
|
@ -30,13 +35,17 @@ must_haves:
|
|||
to: "acknowledge_user case"
|
||||
via: "new switch arm mirroring no_action"
|
||||
pattern: "case 'acknowledge_user'"
|
||||
- from: "TimelineCard VERDICT_TINT lookup"
|
||||
to: "USER_AWARENESS tint entry"
|
||||
via: "4th record key + fallback on the .split sites"
|
||||
pattern: "USER_AWARENESS"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Surface the new `USER_AWARENESS` verdict and `acknowledge_user` action in the Phase 22 review UI. `ClassificationCard` gets a distinct badge color for the new verdict; `ActionAreaCard` renders `acknowledge_user` as a normal recommended-action checkbox (like every other action) so an operator can approve it manually on the review page for companies that do not have report-to-ticket automation enabled.
|
||||
Surface the new `USER_AWARENESS` verdict and `acknowledge_user` action in the Phase 22 review UI. `ClassificationCard` gets a distinct badge color for the new verdict; `ActionAreaCard` renders `acknowledge_user` as a normal recommended-action checkbox (like every other action) so an operator can approve it manually on the review page for companies that do not have report-to-ticket automation enabled; and `TimelineCard` gets a 4th verdict tint plus a defensive fallback so a USER_AWARENESS verdict no longer crashes the review page's timeline.
|
||||
|
||||
Purpose: Operators must be able to see the User Awareness disposition distinctly and manually approve the acknowledgment where automation is off.
|
||||
Output: Extended verdict/label maps in two Phase 22 components.
|
||||
Purpose: Operators must be able to see the User Awareness disposition distinctly and manually approve the acknowledgment where automation is off — and the review page must not crash for the exact verdict this phase creates.
|
||||
Output: Extended verdict/label maps + a defensive tint fallback across three Phase 22 components.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
|
@ -66,6 +75,13 @@ From components/phishing/action-area-card.tsx:
|
|||
- `ACTION_LABEL` record (lines 67-75)
|
||||
- `ActionParamsForm` switch (lines 111-246); `no_action` case (lines 126-132) is the template for a no-params action
|
||||
- Manual approval flow handleApprove -> POST /api/phishing/campaigns/[id]/approve (lines 375-399)
|
||||
|
||||
From components/phishing/timeline-card.tsx:
|
||||
- `TimelineEntry` classification-entry `verdict` union (line 10): `'SPAM' | 'UNWANTED' | 'THREAT'` — add USER_AWARENESS
|
||||
- `VERDICT_TINT` record (lines 17-21): SPAM `'bg-slate-500 text-slate-600'`, UNWANTED `'bg-amber-500 text-amber-600'`, THREAT `'bg-destructive text-destructive'` (dot class + text class, space-separated)
|
||||
- Classification branch (lines 52-53): `VERDICT_TINT[entry.verdict].split(' ')[0]` / `[1]` — crashes if lookup is undefined
|
||||
- `campaign_classified` audit case (lines 86-95): casts `payload.verdict as 'SPAM'|'UNWANTED'|'THREAT'|undefined` then `tint.split(' ')`
|
||||
- NOTE: the timeline SERVICE `lib/services/phishing-timeline.ts` emits `verdict` as plain runtime `string` (line 15, unvalidated) — so a `'USER_AWARENESS'` value reaches this component regardless of the component's own narrowed type; the fallback below is the runtime safety net.
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
|
|
@ -116,6 +132,29 @@ From components/phishing/action-area-card.tsx:
|
|||
<done>acknowledge_user renders as a normal manual checkbox+Approve action with a no-params form; no gate logic added to the manual UI; type-check passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Add USER_AWARENESS support to TimelineCard (prevents review-page crash)</name>
|
||||
<files>components/phishing/timeline-card.tsx</files>
|
||||
<read_first>
|
||||
- components/phishing/timeline-card.tsx (TimelineEntry classification verdict line 10, VERDICT_TINT record lines 17-21, classification .split sites lines 52-53, campaign_classified audit case lines 86-95)
|
||||
- lib/services/phishing-timeline.ts (TimelineEntry.verdict is plain `string` at runtime line 15 — unvalidated, so a USER_AWARENESS value flows straight into this component)
|
||||
- .planning/phases/23-classification-disposition-per-client-automation-gate/23-PATTERNS.md (classification-card.tsx section — mirror the emerald choice)
|
||||
</read_first>
|
||||
<action>
|
||||
TimelineCard independently declares `verdict: 'SPAM' | 'UNWANTED' | 'THREAT'` and `VERDICT_TINT: Record<'SPAM'|'UNWANTED'|'THREAT', string>`, then does `VERDICT_TINT[entry.verdict].split(' ')[0]`. Because the timeline service (`phishing-timeline.ts`) emits `verdict` as plain runtime `string`, a `'USER_AWARENESS'` value hits `VERDICT_TINT['USER_AWARENESS']` = undefined and `undefined.split(' ')` throws — crashing the review page for exactly this phase's verdict. Fix: (1) add `'USER_AWARENESS'` to the classification-entry `verdict` union (line 10). (2) Add a 4th `VERDICT_TINT` entry keyed `USER_AWARENESS` and widen the Record key type to include it — use emerald to match Task 1's ClassificationCard choice, in this file's dot+text format: `USER_AWARENESS: 'bg-emerald-500 text-emerald-600'`. (3) Widen the `campaign_classified` audit-case cast (line 87) to include `'USER_AWARENESS'`. (4) Defensively guard BOTH `.split(' ')` sites — the classification branch (lines 52-53) and the audit branch (the `tint` var, lines 88-93) — with a fallback so ANY unexpected runtime verdict string can never crash: e.g. `const tint = VERDICT_TINT[entry.verdict] ?? 'bg-muted-foreground text-muted-foreground';` then `.split(' ')`, and in the audit case `const tint = (verdict && VERDICT_TINT[verdict]) || 'bg-muted-foreground text-muted-foreground';`. Do not change layout or any other behavior.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/stacks/pulse && npx tsc --noEmit --pretty</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- `grep -q "USER_AWARENESS" components/phishing/timeline-card.tsx` succeeds in both the verdict union and VERDICT_TINT
|
||||
- `grep -c "?? 'bg-muted-foreground text-muted-foreground'\||| 'bg-muted-foreground text-muted-foreground'" components/phishing/timeline-card.tsx` >= 1 (defensive fallback covering the .split sites)
|
||||
- The USER_AWARENESS tint differs from UNWANTED (amber) and THREAT (destructive)
|
||||
- `npx tsc --noEmit` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>TimelineCard renders a USER_AWARENESS classification/audit entry with a distinct emerald tint and no longer crashes on an unrecognized verdict; type-check passes.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
|
@ -130,16 +169,18 @@ From components/phishing/action-area-card.tsx:
|
|||
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|
||||
|-----------|----------|-----------|-------------|-----------------|
|
||||
| T-23-03 | Elevation of Privilege | action-area-card acknowledge_user manual approve | accept | This plan adds no new permission path — acknowledge_user routes through the existing handleApprove -> POST approve flow whose server-side requirePermission gate (Phase 20) is unchanged; UI-only display change |
|
||||
| T-23-12 | Denial of Service | TimelineCard render on new verdict | mitigate | The 4th VERDICT_TINT entry plus a fallback on both `.split` sites means an unrecognized/new runtime verdict string can never throw and crash the review page |
|
||||
</threat_model>
|
||||
|
||||
<verification>
|
||||
- `npx tsc --noEmit --pretty` passes (exhaustive Record keys enforce completeness)
|
||||
- USER_AWARENESS badge distinct from UNWANTED; acknowledge_user labeled and manually approvable; no gate logic in action-area-card
|
||||
- USER_AWARENESS badge distinct from UNWANTED; acknowledge_user labeled and manually approvable; no gate logic in action-area-card; TimelineCard renders USER_AWARENESS without crashing
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- USER_AWARENESS renders with a distinct badge color in the review page
|
||||
- acknowledge_user shows as a normal manual checkbox+Approve action for every company with no gate-check logic in the component
|
||||
- The review-page timeline renders a USER_AWARENESS classification/audit entry without crashing
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ files_modified:
|
|||
- migrations/100_phishing_automation_gate.sql
|
||||
- app/api/admin/phishing-automation/route.ts
|
||||
- app/api/admin/phishing-automation/[companyId]/route.ts
|
||||
- .planning/REQUIREMENTS.md
|
||||
autonomous: true
|
||||
requirements: [AUTOGATE-01]
|
||||
must_haves:
|
||||
|
|
@ -16,6 +17,7 @@ must_haves:
|
|||
- "GET /api/admin/phishing-automation returns every company with its three gate flags, defaulting absent rows to all-false"
|
||||
- "PATCH /api/admin/phishing-automation/{companyId} upserts the three flags with actor+timestamp stamping, admin-gated"
|
||||
- "A company with no row reads as all three stages OFF (opt-in)"
|
||||
- "REQUIREMENTS.md documents all six phase-23 requirement IDs (CLASSDISP-01/02/03, AUTOGATE-01/02/03) with Traceability rows"
|
||||
artifacts:
|
||||
- path: "migrations/100_phishing_automation_gate.sql"
|
||||
provides: "opt-in per-company automation gate table"
|
||||
|
|
@ -26,6 +28,9 @@ must_haves:
|
|||
- path: "app/api/admin/phishing-automation/[companyId]/route.ts"
|
||||
provides: "admin-gated PATCH upsert + DELETE revert-to-default"
|
||||
exports: ["PATCH", "DELETE"]
|
||||
- path: ".planning/REQUIREMENTS.md"
|
||||
provides: "v3 CLASSDISP-*/AUTOGATE-* requirement entries + Traceability rows"
|
||||
contains: "AUTOGATE-01"
|
||||
key_links:
|
||||
- from: "GET route"
|
||||
to: "phishing_automation_gate"
|
||||
|
|
@ -38,10 +43,10 @@ must_haves:
|
|||
---
|
||||
|
||||
<objective>
|
||||
Create the per-company automation-gate persistence and admin API. A new `phishing_automation_gate` table (migration 100) stores three independent opt-in booleans per company (`auto_parse`, `auto_classify`, `auto_report`), defaulting to all-OFF. Two admin-gated routes mirror the existing `/api/admin/company-scope` pattern: a GET listing every company with its flags, and a `[companyId]` PATCH that upserts flags (plus a DELETE that reverts a company to the all-OFF default).
|
||||
Create the per-company automation-gate persistence and admin API. A new `phishing_automation_gate` table (migration 100) stores three independent opt-in booleans per company (`auto_parse`, `auto_classify`, `auto_report`), defaulting to all-OFF. Two admin-gated routes mirror the existing `/api/admin/company-scope` pattern: a GET listing every company with its flags, and a `[companyId]` PATCH that upserts flags (plus a DELETE that reverts a company to the all-OFF default). Also backfills the REQUIREMENTS.md entries for this phase's six requirement IDs (housekeeping — they currently exist only in ROADMAP.md).
|
||||
|
||||
Purpose: The data + API layer the admin UI (Plan 04) and the webhook auto-pipeline (Plan 05) both depend on.
|
||||
Output: migration 100 + two API routes.
|
||||
Purpose: The data + API layer the admin UI (Plan 04) and the webhook auto-pipeline (Plan 05) both depend on, plus requirement-doc consistency.
|
||||
Output: migration 100 + two API routes + REQUIREMENTS.md v3 entries.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
|
|
@ -124,6 +129,27 @@ DB conventions: snake_case columns, camelCase API responses, postgresClient.quer
|
|||
<done>GET returns all companies with COALESCE(false) gate defaults; PATCH upserts all three flags with actor stamping; DELETE reverts to default; both admin-gated; type-check passes.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 3: Backfill v3 CLASSDISP-*/AUTOGATE-* requirement entries + Traceability rows in REQUIREMENTS.md</name>
|
||||
<files>.planning/REQUIREMENTS.md</files>
|
||||
<read_first>
|
||||
- .planning/REQUIREMENTS.md (v2 Requirements section lines 140-162, Traceability table lines 174-216 — mirror its row format and the Coverage footer)
|
||||
- .planning/ROADMAP.md (Phase 23 block lines 529-541 — source the six requirement descriptions)
|
||||
</read_first>
|
||||
<action>
|
||||
REQUIREMENTS.md has no entries for the six phase-23 requirement IDs — they exist only in ROADMAP.md (WARNING 1). Add a `## v3 Requirements` section (if one does not already exist; if it does, append) with entries for all six, sourced from the ROADMAP Phase 23 block: CLASSDISP-01 (dedicated USER_AWARENESS verdict for confirmed phishing-simulation-vendor reports, replacing the forced-UNWANTED disposition), CLASSDISP-02 (USER_AWARENESS maps to the new non-destructive acknowledge_user action that posts a customer-visible noteType-18 thank-you note), CLASSDISP-03 (review UI surfaces USER_AWARENESS + acknowledge_user distinctly), AUTOGATE-01 (per-company phishing_automation_gate table + admin GET/PATCH/DELETE API, opt-in defaults), AUTOGATE-02 (/admin/phishing-automation page with three per-company Switch toggles), AUTOGATE-03 (webhook auto-runs the gated parse->classify->acknowledge chain with the D-04 acknowledge_user auto-post carve-out). Then add six rows to the Traceability table mapping each ID to Phase 23 / Pending, and update the Coverage footer counts to include the six new requirements. Keep the existing entry + table format and wording style verbatim; do NOT remove or alter any existing v1/v2 entry or row.
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /opt/stacks/pulse && grep -Eq "CLASSDISP-01|CLASSDISP-02|CLASSDISP-03" .planning/REQUIREMENTS.md && grep -Eq "AUTOGATE-01|AUTOGATE-02|AUTOGATE-03" .planning/REQUIREMENTS.md && [ "$(grep -c 'Phase 23' .planning/REQUIREMENTS.md)" -ge 6 ] && echo OK</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- All six IDs (CLASSDISP-01/02/03, AUTOGATE-01/02/03) appear both as requirement entries and as Traceability rows mapped to Phase 23
|
||||
- The Coverage footer reflects the added requirements
|
||||
- No existing v1/v2 entries or Traceability rows are removed or altered
|
||||
</acceptance_criteria>
|
||||
<done>REQUIREMENTS.md documents the six phase-23 requirement IDs with descriptions and Traceability rows mapped to Phase 23; ROADMAP and REQUIREMENTS are consistent.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<threat_model>
|
||||
|
|
@ -145,11 +171,13 @@ DB conventions: snake_case columns, camelCase API responses, postgresClient.quer
|
|||
<verification>
|
||||
- Migration file present + applied; `npx tsc --noEmit --pretty` passes
|
||||
- GET defaults absent rows to all-false; PATCH upserts with actor stamp; both admin-gated
|
||||
- REQUIREMENTS.md lists all six phase-23 IDs with Traceability rows
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- phishing_automation_gate table exists with opt-in (all-false) defaults
|
||||
- GET lists companies with three flags; PATCH upserts; DELETE reverts; all admin-gated
|
||||
- REQUIREMENTS.md and ROADMAP.md agree on the phase-23 requirement IDs
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue