9.2 KiB
9.2 KiB
| phase | plan | type | wave | depends_on | files_modified | autonomous | requirements | must_haves | |||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 19-classification-engine | 02 | execute | 2 |
|
|
true |
|
|
Purpose: An operator can (re-)classify a campaign through a properly access-controlled endpoint.
Output: app/api/phishing/campaigns/[id]/classify/route.ts.
<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_context>
@.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/19-classification-engine/19-CONTEXT.md @.planning/phases/19-classification-engine/19-PATTERNS.md @.planning/phases/19-classification-engine/19-VALIDATION.md From lib/services/campaign-classifier.ts: ```typescript export interface ClassifyResult { id: string; campaignId: string; verdict: 'SPAM' | 'UNWANTED' | 'THREAT'; confidence: number; summary: string; reasons: string[]; recommendedActions: string[]; requiresApproval: boolean; createdAt: string; } export function classifyCampaign(campaignId: string): Promise; ```Auth (lib/permissions.ts): phishing: ["read","analyze","approve","remediate"]; analyze
is granted to super-admin + admin only (a plain user role has read only → expect 403).
UUID guard (from app/api/phishing/campaigns/[id]/route.ts):
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
Task 1: POST /api/phishing/campaigns/[id]/classify route handler
app/api/phishing/campaigns/[id]/classify/route.ts
- app/api/phishing/tickets/[ticket_id]/analyze/route.ts (exact auth early-return: `const { error } = await requirePermission('phishing','analyze'); if (error) return error;`; try/catch → 500 with message; [PHISHING-ANALYZE] log-prefix convention)
- app/api/phishing/campaigns/[id]/route.ts (UUID_RE guard + 400; params is Promise<{ id: string }>; flat camelCase response shape; [PHISHING-CAMPAIGN-DETAIL] prefix; 404 when campaign not found)
- lib/permissions.ts (confirm phishing 'analyze' action already exists — do NOT add a new action)
- lib/services/campaign-classifier.ts (classifyCampaign signature + ClassifyResult from Plan 01)
- .planning/phases/19-classification-engine/19-PATTERNS.md (route/controller section — verbatim analog mapping)
Create `app/api/phishing/campaigns/[id]/classify/route.ts` exporting `async function POST(request: NextRequest, { params }: { params: Promise<{ id: string }> })`.
Body, in order:
1. `const { error } = await requirePermission('phishing', 'analyze'); if (error) return error;` — same `'analyze'` action as the existing `/analyze` route (Phase 18 D-06; T-19-04 access-control). Import `requirePermission` from `@/lib/auth-utils`.
2. `const { id } = await params;` then the `UUID_RE` guard copied verbatim from `campaigns/[id]/route.ts` — return `NextResponse.json({ error: 'Invalid campaign id' }, { status: 400 })` on failure, BEFORE any DB access (V5 / T-19-05 tampering guard). Use `campaigns/[id]/route.ts`'s UUID regex, NOT `analyze/route.ts`'s numeric `Number.isFinite` check (this id is a campaign UUID, not a ticket_id).
3. `try { ... } catch (err) { console.error('[PHISHING-CLASSIFY] Failed to classify campaign', id, err); return NextResponse.json({ error: 'Failed to classify campaign', message: err instanceof Error ? err.message : 'Unknown error' }, { status: 500 }); }`.
4. Inside try: verify the campaign exists first — `SELECT id FROM campaigns WHERE id = $1` (postgresClient default import from `@/lib/services/postgres-client`); if no row, return `NextResponse.json({ error: 'Campaign not found' }, { status: 404 })` (mirrors campaigns/[id]/route.ts 404). Then `const result = await classifyCampaign(id);` (import from `@/lib/services/campaign-classifier`) and `return NextResponse.json(result);` — `ClassifyResult` is already flat camelCase, no re-mapping needed.
Use path-alias `@/lib/...` imports (route-handler convention), NOT the sibling-filename imports used inside lib/services. Do not add the route to middleware public paths — `/api/phishing/*` is authenticated (this is intentional, T-19-04).
npx tsc --noEmit --pretty
test -f "app/api/phishing/campaigns/[id]/classify/route.ts" && grep -q "requirePermission('phishing', 'analyze')" "app/api/phishing/campaigns/[id]/classify/route.ts" && echo AUTH_OK
Against a dev server (npm run dev, port 3100) with a known campaign UUID:
1. `curl -X POST http://localhost:3100/api/phishing/campaigns/{uuid}/classify` with a valid admin/super-admin session cookie → 200 with verdict/confidence/summary/reasons/recommendedActions/requiresApproval
2. Same with NO session cookie → 401
3. Same with a plain `user`-role session cookie → 403 (analyze not granted to user)
4. `curl -X POST http://localhost:3100/api/phishing/campaigns/not-a-uuid/classify` (authed) → 400
5. Valid UUID that is not a real campaign → 404
- `npx tsc --noEmit --pretty` exits 0
- `grep -c "requirePermission('phishing', 'analyze')" "app/api/phishing/campaigns/[id]/classify/route.ts"` === 1 (T-19-04)
- `grep -q "UUID_RE" "app/api/phishing/campaigns/[id]/classify/route.ts"` — UUID guard present before DB access (T-19-05)
- `grep -q "classifyCampaign" "app/api/phishing/campaigns/[id]/classify/route.ts"` — delegates to the Plan 01 service
- Manual curl (human-check): 200 for authed admin, 401 no-session, 403 user-role, 400 malformed id, 404 unknown campaign
Route compiles, enforces analyze permission + UUID validation, delegates to classifyCampaign, and returns the verdict payload; manual auth curl matrix passes.
<threat_model>
Trust Boundaries
| Boundary | Description |
|---|---|
| client → API route | Unauthenticated/unauthorized HTTP request + attacker-controlled id path param crosses into the handler |
STRIDE Threat Register
| Threat ID | Category | Component | Disposition | Mitigation Plan |
|---|---|---|---|---|
| T-19-04 | Elevation of Privilege | POST /classify handler | mitigate | requirePermission('phishing','analyze') early-return before any work; analyze granted only to admin/super-admin (V4 access control, server-side not just middleware cookie check) |
| T-19-05 | Tampering | id path param |
mitigate | UUID_RE shape validation returns 400 before any query, preventing a malformed id from surfacing as an uncaught Postgres 500 (V5 input validation) |
| T-19-SC | Tampering | npm/pip/cargo installs | accept | No new packages installed this phase; Package Legitimacy Gate N/A per 19-RESEARCH.md |
| </threat_model> |
<success_criteria>
- POST /api/phishing/campaigns/{id}/classify enforces requirePermission('phishing','analyze'), validates UUID, delegates to classifyCampaign, returns the verdict payload; rejects unauthenticated/unauthorized/malformed requests (CLASSIFY-05) </success_criteria>