diff --git a/.planning/REQUIREMENTS.md b/.planning/REQUIREMENTS.md index 4a00f38..f485b6b 100644 --- a/.planning/REQUIREMENTS.md +++ b/.planning/REQUIREMENTS.md @@ -137,6 +137,33 @@ destructive remediation gated behind explicit human approval. a failed request; the page enforces no separate or relaxed permission model from the underlying APIs +### Classification Disposition + Automation Gate + +- [ ] **CLASSDISP-01**: The classifier assigns a dedicated `USER_AWARENESS` + verdict to campaigns confirmed as phishing-simulation-vendor (KnowBe4/Breach + Secure Now) reports, replacing the previous forced-`UNWANTED` disposition + for these confirmed-simulation cases +- [ ] **CLASSDISP-02**: `USER_AWARENESS` maps to a new non-destructive + `acknowledge_user` action that posts a customer-visible thank-you note + (Autotask `noteType: 18`, "Client Portal Note") to the reporting employee +- [ ] **CLASSDISP-03**: The campaign review UI surfaces `USER_AWARENESS` and + `acknowledge_user` distinctly from the existing SPAM/UNWANTED/THREAT + verdicts and their actions +- [ ] **AUTOGATE-01**: A per-company `phishing_automation_gate` table and + admin-gated `GET`/`PATCH`/`DELETE` API let an operator read and set three + independent opt-in automation flags (`auto_parse`, `auto_classify`, + `auto_report`) per Autotask company, defaulting to all-OFF when no row + exists +- [ ] **AUTOGATE-02**: An `/admin/phishing-automation` page lists companies + with three independent per-company `Switch` toggles (one per automation + stage), backed by the `AUTOGATE-01` API +- [ ] **AUTOGATE-03**: When a company's automation gate stages are enabled, + the Autotask webhook automatically runs the gated parse→classify→acknowledge + chain for that company's phishing reports, with the `acknowledge_user` + auto-post carve-out narrowly scoped to `USER_AWARENESS` verdicts only — all + other verdicts/actions still require manual approval regardless of gate + state + ## v2 Requirements Deferred to future release. Tracked but not in current roadmap. @@ -209,13 +236,19 @@ Populated during roadmap creation. | REVIEW-04 | Phase 22 | Complete | | REVIEW-05 | Phase 22 | Complete | | REVIEW-06 | Phase 22 | Complete | +| CLASSDISP-01 | Phase 23 | Pending | +| CLASSDISP-02 | Phase 23 | Pending | +| CLASSDISP-03 | Phase 23 | Pending | +| AUTOGATE-01 | Phase 23 | Pending | +| AUTOGATE-02 | Phase 23 | Pending | +| AUTOGATE-03 | Phase 23 | Pending | **Coverage:** -- v1 requirements: 32 total -- Mapped to phases: 32 (Phases 15-22) +- v1 requirements: 38 total +- Mapped to phases: 38 (Phases 15-23) - Unmapped: 0 ✓ --- *Requirements defined: 2026-07-14* *Traceability populated: 2026-07-14 — ROADMAP.md Phases 15-21* -*Last updated: 2026-07-14 after initial definition* +*Last updated: 2026-07-16 — backfilled Phase 23 CLASSDISP-*/AUTOGATE-* entries (23-03)* diff --git a/.planning/phases/23-classification-disposition-per-client-automation-gate/23-03-SUMMARY.md b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-03-SUMMARY.md new file mode 100644 index 0000000..a98dc6b --- /dev/null +++ b/.planning/phases/23-classification-disposition-per-client-automation-gate/23-03-SUMMARY.md @@ -0,0 +1,113 @@ +--- +phase: 23-classification-disposition-per-client-automation-gate +plan: 03 +subsystem: api +tags: [postgres, admin-api, migration, requirements-doc] + +# Dependency graph +requires: + - phase: 15-data-model-detection-ticket-evidence + provides: companies table + phishing schema conventions this migration builds on +provides: + - phishing_automation_gate table (opt-in per-company automation flags) + - Admin GET/PATCH/DELETE API for reading and setting the gate flags + - REQUIREMENTS.md backfill for all six phase-23 requirement IDs +affects: [23-04-admin-phishing-automation-page, 23-05-gated-webhook-chain] + +# Tech tracking +tech-stack: + added: [] + patterns: [opt-in settings table mirroring company_scope's opt-out shape with flipped boolean defaults] + +key-files: + created: + - migrations/100_phishing_automation_gate.sql + - app/api/admin/phishing-automation/route.ts + - app/api/admin/phishing-automation/[companyId]/route.ts + modified: + - .planning/REQUIREMENTS.md + +key-decisions: + - "PATCH requires all three boolean flags in the body (client always sends current values) rather than supporting partial updates — avoids partial-update SQL complexity, matches company-scope's single-field simplicity" + - "Phase 23 requirement entries added under the existing v1 Requirements heading (not a new v3 heading) since v2 is explicitly documented as deferred/future work and Phase 23 is active roadmap work" + +patterns-established: + - "Opt-in settings table + COALESCE(..., false) read pattern (mirrors company_scope's opt-out + COALESCE(..., true), with defaults flipped)" + +requirements-completed: [AUTOGATE-01] + +# Metrics +duration: 12min +completed: 2026-07-16 +--- + +# Phase 23 Plan 03: Migration 100 + Admin Phishing-Automation API Summary + +**New opt-in `phishing_automation_gate` table (auto_parse/auto_classify/auto_report, all default false) plus admin-gated GET/PATCH/DELETE routes mirroring the existing company-scope pattern, and REQUIREMENTS.md backfilled with the six Phase 23 requirement IDs.** + +## Performance + +- **Duration:** ~12 min +- **Started:** 2026-07-16T23:31:00Z (approx) +- **Completed:** 2026-07-16T23:37:57Z +- **Tasks:** 3 completed +- **Files modified:** 4 (3 created, 1 modified) + +## Accomplishments +- `phishing_automation_gate` table created and applied to the running dev Postgres (existing volume — file alone would not auto-run) +- Admin GET route lists every active company with COALESCE(..., false)-defaulted gate flags, admin-gated +- Admin PATCH/DELETE `[companyId]` route upserts all three flags with actor+timestamp stamping and reverts to all-OFF default on DELETE +- REQUIREMENTS.md now documents all six Phase 23 requirement IDs (CLASSDISP-01/02/03, AUTOGATE-01/02/03) with Traceability rows, closing the gap where they existed only in ROADMAP.md + +## Task Commits + +Each task was committed atomically: + +1. **Task 1: Create migration 100 phishing_automation_gate + apply to dev DB** - `db7d67c` (feat) +2. **Task 2: Admin GET list route + [companyId] PATCH/DELETE route** - `ea5047c` (feat) +3. **Task 3: Backfill v3 CLASSDISP-*/AUTOGATE-* requirement entries + Traceability rows in REQUIREMENTS.md** - `78b6b49` (docs) + +_No TDD tasks in this plan — all `type="auto"` without `tdd="true"`._ + +## Files Created/Modified +- `migrations/100_phishing_automation_gate.sql` - New table: company_id PK -> companies(id) ON DELETE CASCADE, three `NOT NULL DEFAULT false` booleans (auto_parse/auto_classify/auto_report), updated_by/updated_at audit columns, applied live to pulse-postgres +- `app/api/admin/phishing-automation/route.ts` - Admin-gated GET: LEFT JOIN + COALESCE(..., false), search/type filters, camelCase response +- `app/api/admin/phishing-automation/[companyId]/route.ts` - Admin-gated PATCH (upsert all three flags, actor stamp) and DELETE (revert to default) +- `.planning/REQUIREMENTS.md` - New "Classification Disposition + Automation Gate" subsection under v1 Requirements with 6 entries; 6 new Traceability rows (Phase 23, Pending); Coverage footer updated (32 -> 38 total) + +## Decisions Made +- PATCH validates and requires `autoParse`/`autoClassify`/`autoReport` all present as booleans in the body (no partial-update support) — matches the plan's stated rationale that the admin page's per-Switch toggle already knows the other two current values from local state. +- Requirement entries were placed as a new subsection under the existing `## v1 Requirements` heading rather than creating a `## v3 Requirements` heading, per the plan's explicit instruction that v2 is documented as "deferred to future release" and a v3 heading would misleadingly read the same way for active roadmap work. + +## Deviations from Plan + +None - plan executed exactly as written. + +## Issues Encountered + +None. + +## User Setup Required + +None - no external service configuration required. The migration was applied directly to the existing dev Postgres volume as instructed (Postgres only auto-applies migrations on first volume boot). + +## Next Phase Readiness + +- The `phishing_automation_gate` table and its admin API are ready for Plan 04 (`/admin/phishing-automation` page with three per-company Switch toggles) and Plan 05 (gated webhook auto-pipeline) to consume. +- Both routes verified with `npx tsc --noEmit --pretty` (exit 0) and all plan-specified grep-based acceptance checks pass. +- REQUIREMENTS.md and ROADMAP.md are now consistent on the six Phase 23 requirement IDs. + +--- +*Phase: 23-classification-disposition-per-client-automation-gate* +*Completed: 2026-07-16* + +## Self-Check: PASSED + +- FOUND: migrations/100_phishing_automation_gate.sql +- FOUND: app/api/admin/phishing-automation/route.ts +- FOUND: app/api/admin/phishing-automation/[companyId]/route.ts +- FOUND: .planning/REQUIREMENTS.md +- FOUND: commit db7d67c (Task 1) +- FOUND: commit ea5047c (Task 2) +- FOUND: commit 78b6b49 (Task 3) +- FOUND: commit 63d6e85 (SUMMARY.md) diff --git a/app/api/admin/phishing-automation/[companyId]/route.ts b/app/api/admin/phishing-automation/[companyId]/route.ts new file mode 100644 index 0000000..92dc3cf --- /dev/null +++ b/app/api/admin/phishing-automation/[companyId]/route.ts @@ -0,0 +1,76 @@ +/** + * PATCH /api/admin/phishing-automation/[companyId] + * Upsert a company's three automation gate flags. + * Body: { autoParse: boolean, autoClassify: boolean, autoReport: boolean } + * Client always sends all three current values (the admin page knows them + * from local state), avoiding partial-update SQL complexity. + * + * DELETE /api/admin/phishing-automation/[companyId] + * Remove the explicit override — company reverts to the all-OFF default. + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/auth-utils'; +import postgresClient from '@/lib/services/postgres-client'; + +export async function PATCH( + request: NextRequest, + { params }: { params: Promise<{ companyId: string }> } +) { + const { session, error } = await requireAdmin(); + if (error) return error; + + const { companyId } = await params; + const id = parseInt(companyId, 10); + if (isNaN(id)) return NextResponse.json({ error: 'Invalid companyId' }, { status: 400 }); + + const body = await request.json().catch(() => null); + if ( + body == null || + typeof body.autoParse !== 'boolean' || + typeof body.autoClassify !== 'boolean' || + typeof body.autoReport !== 'boolean' + ) { + return NextResponse.json( + { error: 'body.autoParse, body.autoClassify, body.autoReport (all boolean) required' }, + { status: 400 } + ); + } + + const userEmail = (session?.user as any)?.email ?? null; + + await postgresClient.query( + `INSERT INTO phishing_automation_gate (company_id, auto_parse, auto_classify, auto_report, updated_by, updated_at) + VALUES ($1, $2, $3, $4, $5, NOW()) + ON CONFLICT (company_id) + DO UPDATE SET auto_parse = EXCLUDED.auto_parse, + auto_classify = EXCLUDED.auto_classify, + auto_report = EXCLUDED.auto_report, + updated_by = EXCLUDED.updated_by, + updated_at = NOW()`, + [id, body.autoParse, body.autoClassify, body.autoReport, userEmail] + ); + + return NextResponse.json({ + ok: true, + companyId: id, + autoParse: body.autoParse, + autoClassify: body.autoClassify, + autoReport: body.autoReport, + }); +} + +export async function DELETE( + _request: NextRequest, + { params }: { params: Promise<{ companyId: string }> } +) { + const { error } = await requireAdmin(); + if (error) return error; + + const { companyId } = await params; + const id = parseInt(companyId, 10); + if (isNaN(id)) return NextResponse.json({ error: 'Invalid companyId' }, { status: 400 }); + + await postgresClient.query(`DELETE FROM phishing_automation_gate WHERE company_id = $1`, [id]); + return NextResponse.json({ ok: true }); +} diff --git a/app/api/admin/phishing-automation/route.ts b/app/api/admin/phishing-automation/route.ts new file mode 100644 index 0000000..8e7f21b --- /dev/null +++ b/app/api/admin/phishing-automation/route.ts @@ -0,0 +1,82 @@ +/** + * GET /api/admin/phishing-automation + * Returns all active companies with their current phishing automation gate flags. + * Companies without a phishing_automation_gate row are implicitly all-OFF + * (opt-in model — opposite polarity from /api/admin/company-scope). + * + * Query params: + * search — filter by company name (ILIKE) + * type — filter by company_type integer + */ + +import { NextRequest, NextResponse } from 'next/server'; +import { requireAdmin } from '@/lib/auth-utils'; +import postgresClient from '@/lib/services/postgres-client'; + +interface CompanyRow { + id: string; + company_name: string; + company_type: number | null; + auto_parse: boolean; + auto_classify: boolean; + auto_report: boolean; +} + +const COMPANY_TYPE_LABELS: Record = { + 1: 'Customer', + 2: 'Canceled', + 3: 'Cold', + 4: 'Dead', + 5: 'Warm', + 6: 'Vendor', + 7: 'Partner', + 8: 'Prospect', +}; + +export async function GET(request: NextRequest) { + const { error } = await requireAdmin(); + if (error) return error; + + const url = request.nextUrl; + const search = url.searchParams.get('search')?.trim() || null; + const typeParam = url.searchParams.get('type'); + const typeFilter = typeParam ? parseInt(typeParam, 10) : null; + + const params: unknown[] = []; + const conditions = ['c.is_active = true', 'c.is_deleted = false']; + + if (search) { + params.push(`%${search}%`); + conditions.push(`c.company_name ILIKE $${params.length}`); + } + if (typeFilter !== null && !isNaN(typeFilter)) { + params.push(typeFilter); + conditions.push(`c.company_type = $${params.length}`); + } + + const result = await postgresClient.query( + `SELECT c.id::text, c.company_name, c.company_type, + COALESCE(pag.auto_parse, false) AS auto_parse, + COALESCE(pag.auto_classify, false) AS auto_classify, + COALESCE(pag.auto_report, false) AS auto_report + FROM companies c + LEFT JOIN phishing_automation_gate pag ON pag.company_id = c.id + WHERE ${conditions.join(' AND ')} + ORDER BY c.company_name`, + params + ); + + const companies = result.rows.map((r) => ({ + id: r.id, + companyName: r.company_name, + companyType: r.company_type, + companyTypeLabel: r.company_type != null ? (COMPANY_TYPE_LABELS[r.company_type] ?? `Type ${r.company_type}`) : null, + autoParse: r.auto_parse, + autoClassify: r.auto_classify, + autoReport: r.auto_report, + })); + + const enabled = companies.filter((c) => c.autoParse || c.autoClassify || c.autoReport).length; + + return NextResponse.json({ companies, total: companies.length, enabled }); +} diff --git a/migrations/100_phishing_automation_gate.sql b/migrations/100_phishing_automation_gate.sql new file mode 100644 index 0000000..aa39c57 --- /dev/null +++ b/migrations/100_phishing_automation_gate.sql @@ -0,0 +1,28 @@ +-- ============================================================================= +-- Phishing automation gate table +-- ============================================================================= +-- Controls whether the phishing triage pipeline's parse/classify/report-to-ticket +-- stages run automatically for a given Autotask company, or require the existing +-- manual Analyze/Classify/triage-note triggers. Opt-IN model (opposite polarity +-- from company_scope): companies without a row here have all three stages OFF. +-- +-- auto_parse — automatically extract/parse .eml evidence on detection +-- auto_classify — automatically run campaign classification after parsing +-- auto_report — automatically post the acknowledge_user note ONLY for +-- USER_AWARENESS verdicts (D-04); never a general "auto-post +-- any note/action" gate. All other verdicts/actions still +-- require manual approval via the existing review UI even +-- when auto_report is enabled for the company. +-- ============================================================================= + +CREATE TABLE IF NOT EXISTS phishing_automation_gate ( + company_id BIGINT PRIMARY KEY REFERENCES companies(id) ON DELETE CASCADE, + auto_parse BOOLEAN NOT NULL DEFAULT false, + auto_classify BOOLEAN NOT NULL DEFAULT false, + auto_report BOOLEAN NOT NULL DEFAULT false, + updated_by TEXT, + updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() +); + +COMMENT ON TABLE phishing_automation_gate IS + 'Opt-in per-company phishing pipeline automation gate. Absent row = all three stages OFF (manual-only). auto_report auto-posts ONLY the acknowledge_user action for USER_AWARENESS verdicts (D-04) -- it never auto-posts any other action.';