// lib/exporters/field-allowlist.mjs // CWE-212 (Improper Cross-boundary Removal of Sensitive Data) mitigation: // inline static allowlist that strips PII / high-cardinality fields before // records reach the OTel exporter. // // The allowlist is INLINE STATIC at module-scope (Object.freeze). NEVER read // runtime from tests/fixtures/ — fixture file is AUTHORING reference only // (Source: tests/fixtures/jsonl-schemas.md per Step 1 audit). // // Per-schema allowlists drop: // - command_excerpt (post-bash-stats: arbitrary Bash slice, CWE-212) // - session_id (UUID, high-cardinality, log-only) // - task / question / project_dir / brief_path / plan / project (PII-ish prose, paths) // - payload (event-emit: open-ended object, allowlist payload-keys instead) // - phase_models (structured object, summarize via `profile` label instead) // - counts (review nested object, flatten to per-severity metrics elsewhere) // ---- Per-schema allowlists (Source: tests/fixtures/jsonl-schemas.md) ------- // Source: tests/fixtures/jsonl-schemas.md row 1 (trekbrief) const TREKBRIEF_ALLOWED = Object.freeze(new Set([ 'ts', 'slug', 'mode', 'interview_turns', 'review_iterations', 'brief_quality', 'research_topics', 'auto_research', 'auto_result', 'profile', 'profile_source', ])); // Source: tests/fixtures/jsonl-schemas.md row 2 (trekresearch) const TREKRESEARCH_ALLOWED = Object.freeze(new Set([ 'ts', 'slug', 'mode', 'scope', 'dimensions', 'agents_local', 'agents_external', 'gemini_used', 'confidence', 'contradictions', 'open_questions', 'profile', 'parallel_agents', 'external_research_enabled', 'profile_source', ])); // Source: tests/fixtures/jsonl-schemas.md row 3 (trekplan) const TREKPLAN_ALLOWED = Object.freeze(new Set([ 'ts', 'slug', 'mode', 'codebase_size', 'codebase_files', 'agents_deployed', 'deep_dives', 'research_briefs_used', 'research_scout_used', 'critic_verdict', 'guardian_verdict', 'outcome', 'profile', 'parallel_agents', 'profile_source', ])); // Source: tests/fixtures/jsonl-schemas.md row 4 (trekexecute Phase 9 record) const TREKEXECUTE_ALLOWED = Object.freeze(new Set([ 'ts', 'plan_type', 'mode', 'result', 'steps_total', 'steps_passed', 'steps_failed', 'steps_skipped', 'failed_at_step', 'profile', 'profile_source', ])); // Source: tests/fixtures/jsonl-schemas.md row 5 (trekexecute autonomy events) const EVENT_EMIT_ALLOWED = Object.freeze(new Set([ 'ts', 'event', 'known_event', ])); // Source: tests/fixtures/jsonl-schemas.md row 5 (event-emit payload sub-allowlist) const EVENT_EMIT_PAYLOAD_ALLOWED = Object.freeze(new Set([ 'profile', 'phase_models', 'parallel_agents', 'external_research_enabled', 'profile_source', 'brief_quality', 'plan_grade', ])); // Source: tests/fixtures/jsonl-schemas.md row 6 (post-bash-stats — PII-flag) // CWE-212: command_excerpt + session_id MUST be stripped. const POST_BASH_STATS_ALLOWED = Object.freeze(new Set([ 'ts', 'duration_ms', 'success', ])); // Source: tests/fixtures/jsonl-schemas.md row 7 (trekreview) const TREKREVIEW_ALLOWED = Object.freeze(new Set([ 'ts', 'slug', 'verdict', 'reviewed_files_count', 'mode', 'duration_ms', 'profile', 'profile_source', ])); // Source: tests/fixtures/jsonl-schemas.md row 8 (trekcontinue) const TREKCONTINUE_ALLOWED = Object.freeze(new Set([ 'ts', 'next_session_label', 'status', 'profile', 'profile_source', ])); // Schema-id → allowlist set const SCHEMA_ALLOWLISTS = Object.freeze({ 'trekbrief': TREKBRIEF_ALLOWED, 'trekresearch': TREKRESEARCH_ALLOWED, 'trekplan': TREKPLAN_ALLOWED, 'trekexecute': TREKEXECUTE_ALLOWED, 'event-emit': EVENT_EMIT_ALLOWED, 'post-bash-stats': POST_BASH_STATS_ALLOWED, 'post_bash_stats': POST_BASH_STATS_ALLOWED, // common alt-spelling 'trekreview': TREKREVIEW_ALLOWED, 'trekcontinue': TREKCONTINUE_ALLOWED, }); /** * Apply field-allowlist to a record. Drops any field not in the schema's * allowlist. Returns a new object (does not mutate input). * * Always preserves `_schema_id` (caller-supplied identifier used by exporters * to group records by source). * * @param {object} record * @param {string} schemaType Schema-id (e.g. 'trekplan', 'post-bash-stats') * @returns {object} Redacted record with only allowlisted fields. */ export function applyFieldAllowlist(record, schemaType) { if (!record || typeof record !== 'object') return {}; const allowed = SCHEMA_ALLOWLISTS[schemaType]; if (!allowed) { // Unknown schema-type: be conservative — keep ts only + _schema_id. return record.ts ? { _schema_id: schemaType, ts: record.ts } : { _schema_id: schemaType }; } const out = { _schema_id: schemaType }; for (const k of allowed) { if (k in record) out[k] = record[k]; } // Special: event-emit records have a `payload` sub-object that needs its // own allowlist applied recursively. if (schemaType === 'event-emit' && record.payload && typeof record.payload === 'object') { const subOut = {}; for (const k of EVENT_EMIT_PAYLOAD_ALLOWED) { if (k in record.payload) subOut[k] = record.payload[k]; } out.payload = subOut; } return out; } export { SCHEMA_ALLOWLISTS, EVENT_EMIT_PAYLOAD_ALLOWED, POST_BASH_STATS_ALLOWED, TREKBRIEF_ALLOWED, TREKRESEARCH_ALLOWED, TREKPLAN_ALLOWED, TREKEXECUTE_ALLOWED, TREKREVIEW_ALLOWED, TREKCONTINUE_ALLOWED, };