Commit graph

9 commits

Author SHA1 Message Date
1a126078d7 feat(20-02): add mark-false-positive route and classify audit event
- POST /mark-false-positive: phishing:approve gated (D-04 elevated tier),
  optional reason body, delegates to markCampaignFalsePositive, maps
  RemediationConflictError->409 (already remediated) and
  RemediationValidationError->400
- classify route now writes a 'campaign_classified' audit event after a
  successful classification, completing REMED-06's four-action audit
  coverage (classify/approve/remediate/mark-false-positive)
2026-07-16 10:44:30 -04:00
65c4253f98 feat(20-02): add approve and remediate routes for phishing campaigns
- POST /approve: phishing:approve gated, validates actions array (D-03),
  delegates to approveRemediationActions with actor from session
- POST /remediate: phishing:remediate gated, delegates to
  remediateApprovedActions (idempotent completion, REMED-03/04)
- Both UUID-guard the campaign id and map RemediationValidationError->400,
  RemediationConflictError->409
2026-07-16 10:43:53 -04:00
3e8d5b83c9 feat(19-02): add POST /api/phishing/campaigns/[id]/classify route
- requirePermission('phishing','analyze') early-return (same action as /analyze, Phase 18 D-06)
- UUID_RE guard on campaign id before any DB query (T-19-05)
- 404 when campaign id is well-formed but not found
- delegates to classifyCampaign(id) from lib/services/campaign-classifier.ts (Plan 01), returns flat ClassifyResult payload
2026-07-16 08:26:20 -04:00
650f9b8100 fix(18): decouple campaigns count-query params from list-query placeholder numbering
The count query reused statusFilter (built with $3 against the list query's
3-element params array) but only passed a 1-element params array, causing a
Postgres bind-parameter mismatch (500) on any `?status=` filtered request.
Pre-existing since 18-03; surfaced by the 18-04 gap-closure code re-review.
Gives the count query its own independent param array/placeholder numbering.
2026-07-15 22:35:47 -04:00
abe3d4b900 fix(18-04): clamp campaigns list limit param to [0, 200] (WR-02)
- Parse limit once with Number.isFinite instead of `|| 50`, so an explicit
  limit=0 is honored instead of silently replaced by the default
- Math.max/min clamps to [0, 200], preventing a negative limit from reaching
  the SQL LIMIT clause and raising an unhandled 500
2026-07-15 22:25:29 -04:00
701fea04cc chore: merge executor worktree (worktree-agent-a985b30606e37e1d0) 2026-07-15 19:33:32 -04:00
959907d63b feat(18-03): add GET /api/phishing/campaigns/[id] nested detail
- requirePermission('phishing','read') gate (ACCESS-01)
- UUID-validated id (400 on malformed), 404 when campaign absent
- bulk-fetch reports/messages/indicators via ANY($1::uuid[]) keyed by parent id array (device-link-conflicts pattern)
- requesterEmail derived via reports.requester_contact_id -> contacts join (campaigns has no recipients column)
- messages.subject pulled from headers->>'subject' JSONB (no subject column)
- classifications included in shape (Phase 19 stub, expected empty)
2026-07-15 19:31:35 -04:00
c852cfee13 feat(18-03): add GET /api/phishing/campaigns paginated list
- requirePermission('phishing','read') gate (ACCESS-01)
- limit/offset clamped, optional status filter via parameterized $n placeholder (never string-interpolated)
- camelCase response { items, total, limit, offset }
2026-07-15 19:31:19 -04:00
de013e6ec1 feat(18-02): add POST /api/phishing/tickets/[ticket_id]/analyze route
- Orchestrates detectPhishingTicket -> parseAndStoreMessage -> groupReportIntoCampaign
- requirePermission('phishing','analyze') gate first-line (D-06, 401/403)
- Validates ticket_id numeric (400), missing ticket (404), non-phishing ticket (400)
- No skipIfAlreadyGrouped (D-08) — always re-runs grouping on demand
2026-07-15 19:30:46 -04:00