feat(21-01): implement triage-note sanitizer
- sanitizeUrl strips query+fragment, keeps scheme+host+path, never throws - sanitizeNoteText redacts Bearer/Authorization tokens and credential query-param values while preserving sender emails and attachment hashes - All 8 sanitizer tests pass
This commit is contained in:
parent
cc93707e41
commit
226f300513
1 changed files with 81 additions and 0 deletions
81
lib/services/triage-note-sanitize.ts
Normal file
81
lib/services/triage-note-sanitize.ts
Normal file
|
|
@ -0,0 +1,81 @@
|
|||
/**
|
||||
* Triage-note sanitization (Phase 21, NOTE-01).
|
||||
*
|
||||
* SECURITY-CRITICAL. This module exists so that no raw secret, token, or full
|
||||
* malicious URL query string ever reaches an Autotask note posted by Pulse.
|
||||
* `formatTriageNote()` (triage-note-format.ts) routes every URL and its final
|
||||
* assembled output through these two pure functions before returning text
|
||||
* that gets written to `TicketNotes`.
|
||||
*
|
||||
* Two things this module deliberately does NOT redact, per
|
||||
* `.planning/phases/21-autotask-triage-note/21-CONTEXT.md` (Claude's
|
||||
* Discretion): bare sender email addresses and attachment hashes. Those are
|
||||
* evidence about the phishing campaign itself, not credentials belonging to
|
||||
* Pulse or its operators — an analyst needs to see them to triage.
|
||||
*/
|
||||
|
||||
export const REDACTED_MARKER = '[REDACTED]';
|
||||
|
||||
/** Keys that indicate a credential-bearing query param. Broad on purpose —
|
||||
* false positives (redacting a benign param) are acceptable; leaking a real
|
||||
* token is not. */
|
||||
const CREDENTIAL_QUERY_KEY_PATTERN = /token|secret|password|api[_-]?key|key|credential/i;
|
||||
|
||||
/** Matches a `key=value` pair inside a query string / free text where `key`
|
||||
* looks like a credential. Value is greedy up to the next `&`, whitespace, or
|
||||
* end of string. */
|
||||
const CREDENTIAL_PARAM_REGEX = /\b([A-Za-z_][A-Za-z0-9_-]*)=([^&\s]+)/g;
|
||||
|
||||
/** Matches `Bearer <token>` or `Authorization: <value>` sequences. */
|
||||
const BEARER_AUTH_REGEX = /\b(Bearer\s+|Authorization:\s*)([^\s,;]+)/gi;
|
||||
|
||||
/** Matches a full http(s) URL substring embedded in free text. */
|
||||
const URL_IN_TEXT_REGEX = /\bhttps?:\/\/[^\s)]+/gi;
|
||||
|
||||
/**
|
||||
* Strip a URL down to scheme+host+path — query string and fragment are ALWAYS
|
||||
* removed, never selectively kept, even for seemingly-benign params. This
|
||||
* satisfies NOTE-01's "no full malicious URL query strings" requirement
|
||||
* without needing to distinguish safe from unsafe params.
|
||||
*
|
||||
* Never throws: a malformed/non-URL string falls back to truncating at the
|
||||
* first `?` or `#`, returning the head unchanged if neither is present.
|
||||
*/
|
||||
export function sanitizeUrl(value: string): string {
|
||||
if (!value) return value;
|
||||
|
||||
try {
|
||||
const url = new URL(value);
|
||||
return url.origin + url.pathname;
|
||||
} catch {
|
||||
const cutIndex = value.search(/[?#]/);
|
||||
return cutIndex === -1 ? value : value.slice(0, cutIndex);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Redact secrets/tokens from free text destined for an Autotask note, and
|
||||
* strip query strings from any embedded URLs. Order matters: Bearer/
|
||||
* Authorization sequences first (they don't look like `key=value` pairs so
|
||||
* they wouldn't otherwise be caught), then credential query-param
|
||||
* assignments, then any remaining full URL substrings via `sanitizeUrl`.
|
||||
*
|
||||
* Bare email addresses and hex attachment hashes are intentionally left
|
||||
* untouched — see module doc-comment.
|
||||
*/
|
||||
export function sanitizeNoteText(text: string): string {
|
||||
if (!text) return text;
|
||||
|
||||
let out = text.replace(BEARER_AUTH_REGEX, REDACTED_MARKER);
|
||||
|
||||
out = out.replace(CREDENTIAL_PARAM_REGEX, (match, key: string, val: string) => {
|
||||
if (CREDENTIAL_QUERY_KEY_PATTERN.test(key)) {
|
||||
return `${key}=${REDACTED_MARKER}`;
|
||||
}
|
||||
return match;
|
||||
});
|
||||
|
||||
out = out.replace(URL_IN_TEXT_REGEX, (match) => sanitizeUrl(match));
|
||||
|
||||
return out;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue