feat(voyage): add lib/exporters/{path,endpoint,field-allowlist}-validators — CWE-22, CWE-918, CWE-212 mitigering
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>
This commit is contained in:
parent
9e7b96fb95
commit
8805df8b0d
4 changed files with 530 additions and 0 deletions
138
lib/exporters/field-allowlist.mjs
Normal file
138
lib/exporters/field-allowlist.mjs
Normal file
|
|
@ -0,0 +1,138 @@
|
|||
// 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,
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue