Step 11 av v4.1-execute (Wave 2, Session 3).
3 sikkerhets-validatorer for OTel-eksporten:
path-validator.mjs (CWE-22 Path Traversal):
- Reject `..` segmenter, `~`-shorthand
- realpathSync symlink-resolution (med macOS quirk: /etc, /var, /tmp er
symlinks til /private/etc, /private/var, /private/tmp — begge former
i FORBIDDEN_PREFIXES)
- Allowlist-først evaluering: hvis allowedRoots gitt, det er primary defense
(caller's threat model). Forbidden-prefix-denylist er FALLBACK når
allowedRoots ikke spesifisert.
endpoint-validator.mjs (CWE-918 SSRF):
- Reject loopback (127.0.0.1, ::1, localhost, 0.0.0.0) UNLESS VOYAGE_OTEL_ALLOW_PRIVATE=1
- Reject RFC-1918 (10/8, 172.16/12, 192.168/16) UNLESS opt-in
- Reject link-local (169.254.x.x cloud metadata, fe80:* IPv6) UNLESS opt-in
- Krev https:// for non-private endpoints
- node:url-parsing, ingen runtime DNS-resolusjon (defense-in-depth)
field-allowlist.mjs (CWE-212 Improper Cross-boundary Removal of Sensitive Data):
- INLINE static const Object.freeze på modul-scope (IKKE runtime read fra fixtures)
- Per-schema allowlist for alle 8 schema-id (trekbrief, trekresearch, trekplan,
trekexecute, event-emit, post-bash-stats, trekreview, trekcontinue)
- Source-comment per allowlist refererer tests/fixtures/jsonl-schemas.md
- post-bash-stats DROPPER eksplisitt command_excerpt + session_id (CWE-212)
- event-emit applies sub-allowlist på payload-objekt (recursive)
- Unknown schema-type returnerer conservative {_schema_id, ts}
Tester (19 nye, baseline 413 → 432):
- path-validator x6 (CWE-22 traversal, forbidden-system, ~, allowedRoots accept/reject, drift-pin)
- endpoint-validator x7 (CWE-918 link-local, RFC-1918, loopback, https-required, opt-in, public-accept, empty-input)
- field-allowlist x6 (CWE-212 post-bash-stats, trekplan-PII, event-emit-payload, unknown-schema, Object.freeze, null-safe)
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
138 lines
5.3 KiB
JavaScript
138 lines
5.3 KiB
JavaScript
// 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,
|
|
};
|