chore: merge executor worktree (worktree-agent-a1093d44311d63302)
This commit is contained in:
commit
d9d8962ca7
5 changed files with 335 additions and 3 deletions
|
|
@ -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)*
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
76
app/api/admin/phishing-automation/[companyId]/route.ts
Normal file
76
app/api/admin/phishing-automation/[companyId]/route.ts
Normal file
|
|
@ -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 });
|
||||
}
|
||||
82
app/api/admin/phishing-automation/route.ts
Normal file
82
app/api/admin/phishing-automation/route.ts
Normal file
|
|
@ -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<number, string> = {
|
||||
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<CompanyRow>(
|
||||
`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 });
|
||||
}
|
||||
28
migrations/100_phishing_automation_gate.sql
Normal file
28
migrations/100_phishing_automation_gate.sql
Normal file
|
|
@ -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.';
|
||||
Loading…
Add table
Add a link
Reference in a new issue