---
phase: 21-autotask-triage-note
plan: 01
subsystem: api
tags: [phishing-triage, sanitization, autotask, vitest, pure-functions]

# Dependency graph
requires:
  - phase: 17-mimecast-blast-radius-lookup
    provides: BlastRadiusResult discriminated union imported as a type
  - phase: 19-classification-engine
    provides: Verdict type and classification shape (verdict/confidence/summary/reasons/recommended_actions) this formatter summarizes
  - phase: 20-remediation-approval-audit-safety
    provides: remediation_actions status lifecycle (proposed/approved/completed) this formatter renders
provides:
  - "sanitizeUrl / sanitizeNoteText pure functions (lib/services/triage-note-sanitize.ts)"
  - "formatTriageNote(evidence) pure function + TriageNoteEvidence contract (lib/services/triage-note-format.ts)"
affects: [21-02-triage-note-service-and-endpoint]

# Tech tracking
tech-stack:
  added: []
  patterns:
    - "SECURITY-CRITICAL module-header convention mirrored from lib/services/analyzer/itglue-redact.ts (why-it-exists doc-comment + exported-pure-function-for-tests shape)"
    - "Formatter routes all free text and URLs through a shared sanitizer before returning, so no source field can bypass redaction"

key-files:
  created:
    - lib/services/triage-note-sanitize.ts
    - lib/services/triage-note-sanitize.test.ts
    - lib/services/triage-note-format.ts
    - lib/services/triage-note-format.test.ts
  modified: []

key-decisions:
  - "sanitizeUrl always strips query+fragment wholesale (never selectively keeps benign params) per NOTE-01's 'full malicious URL query strings' requirement"
  - "sanitizeNoteText intentionally does NOT redact bare emails or hex attachment hashes — those are evidence per 21-CONTEXT.md Claude's Discretion, not credentials"
  - "formatTriageNote passes its entire assembled string through sanitizeNoteText (not just discrete fields) so a secret embedded anywhere in free text is caught regardless of source field"

patterns-established:
  - "Pure-function-first design for security-critical text transforms: no I/O, fully unit-testable, consumed later by a thin service layer (Plan 02)"

requirements-completed: [NOTE-01]

# Metrics
duration: 2min
completed: 2026-07-16
---

# Phase 21 Plan 01: Triage Note Sanitizer + Formatter Summary

**Pure sanitizer (URL query/fragment stripping + Bearer/credential redaction) and pure formatter (`formatTriageNote`) turning classification + blast-radius + remediation-state evidence into human-readable, secret-free Autotask note text, both fully unit-tested with Vitest.**

## Performance

- **Duration:** ~2 min
- **Started:** 2026-07-16T12:04:58-04:00
- **Completed:** 2026-07-16T12:06:31-04:00
- **Tasks:** 2 completed
- **Files modified:** 4 (all newly created)

## Accomplishments
- `sanitizeUrl` strips every URL down to scheme+host+path (query + fragment always removed), never throws on malformed input, falls back to truncating at `?`/`#`
- `sanitizeNoteText` redacts Bearer/Authorization tokens and credential-style query-param values (`token`, `access_token`, `password`, `api_key`, etc.) while explicitly preserving bare sender emails and attachment hashes as evidence
- `formatTriageNote` renders a `TriageNoteEvidence` object into labeled prose sections (header, Classification, Reasons, Blast Radius — both `ok`/`unavailable` branches, Recommended Actions, Current Remediation State, Indicator URLs), routing every URL through `sanitizeUrl` and the whole assembled string through `sanitizeNoteText`
- `TriageNoteEvidence` interface exported for Plan 02 to build its evidence-gathering service against

## Task Commits

Each task followed RED → GREEN (TDD):

1. **Task 1: Sanitizer** - `cc93707` (test: RED, 8 failing tests) → `226f300` (feat: GREEN, all 8 pass)
2. **Task 2: Note formatter + TriageNoteEvidence contract** - `b4e05eb` (test: RED, 9 failing tests) → `17ba0e8` (feat: GREEN, all 9 pass)

_TDD gate compliance: both tasks have a `test(...)` commit followed by a `feat(...)` commit; no refactor step was needed._

## Files Created/Modified
- `lib/services/triage-note-sanitize.ts` - `sanitizeUrl` + `sanitizeNoteText` pure functions with `REDACTED_MARKER` constant
- `lib/services/triage-note-sanitize.test.ts` - 8 Vitest cases covering query/fragment stripping, malformed-input safety, token/credential redaction, email/hash preservation
- `lib/services/triage-note-format.ts` - `TriageNoteEvidence` interface + `formatTriageNote(evidence)` pure function
- `lib/services/triage-note-format.test.ts` - 9 Vitest cases covering verdict/confidence rendering, both blast-radius branches, empty/populated remediation state, URL sanitization, whole-output secret redaction, null-verdict handling

## Decisions Made
- Query strings are stripped unconditionally in `sanitizeUrl` — no allowlist of "safe" params, since selectively keeping some params risks missing a novel credential-param name (matches NOTE-01's wording exactly: "full malicious URL query strings").
- `sanitizeNoteText` explicitly does not touch email addresses or hex hashes — confirmed as intentional in `21-CONTEXT.md` Claude's Discretion block (these are attacker-identity evidence, not secrets belonging to Pulse/its operators).
- `formatTriageNote` sanitizes the entire assembled output string (not just individual fields) as a defense-in-depth measure — if a future evidence field carries an embedded secret, it's still caught at the final sanitize pass regardless of which section it landed in.

## Deviations from Plan

None - plan executed exactly as written. Both tasks' `<behavior>`, `<action>`, and `<acceptance_criteria>` blocks were implemented as specified; no architectural changes, no missing critical functionality found, no blocking issues encountered.

## Issues Encountered
None.

## User Setup Required
None - no external service configuration required. Both modules are pure functions with no DB/network/Autotask dependency.

## Next Phase Readiness
- `TriageNoteEvidence` interface and `formatTriageNote()` are ready for Plan 02 to import and build its evidence-gathering service + `POST /api/phishing/campaigns/{id}/triage-note` endpoint against.
- `sanitizeUrl`/`sanitizeNoteText` are exported and stable for direct reuse if Plan 02 needs to sanitize any additional free text outside the formatter's own URL list.
- No blockers.

---
*Phase: 21-autotask-triage-note*
*Completed: 2026-07-16*

## Self-Check: PASSED

All created files and task commit hashes verified present on disk / in git history.
