feat(exporters): allowlist token-usage schema + assert metric export (CWE-212)
This commit is contained in:
parent
708ba04571
commit
a9c442c201
3 changed files with 81 additions and 0 deletions
|
|
@ -76,6 +76,16 @@ const TREKCONTINUE_ALLOWED = Object.freeze(new Set([
|
||||||
'ts', 'next_session_label', 'status', 'profile', 'profile_source',
|
'ts', 'next_session_label', 'status', 'profile', 'profile_source',
|
||||||
]));
|
]));
|
||||||
|
|
||||||
|
// Source: tests/fixtures/jsonl-schemas.md row 9 (token-usage — SKAL-2)
|
||||||
|
// CWE-212: numeric + low-cardinality-label fields ONLY. DENY BY OMISSION:
|
||||||
|
// session_id (UUID), transcript_path (filesystem path), cwd (filesystem path)
|
||||||
|
// are written into the jsonl for upsert keying but MUST NOT reach the exporter.
|
||||||
|
const TOKEN_USAGE_ALLOWED = Object.freeze(new Set([
|
||||||
|
'ts', 'scope', 'model',
|
||||||
|
'tokens_input', 'tokens_output', 'tokens_cache_creation', 'tokens_cache_read',
|
||||||
|
'cost_usd', 'is_estimate', 'price_table_version',
|
||||||
|
]));
|
||||||
|
|
||||||
// Schema-id → allowlist set
|
// Schema-id → allowlist set
|
||||||
const SCHEMA_ALLOWLISTS = Object.freeze({
|
const SCHEMA_ALLOWLISTS = Object.freeze({
|
||||||
'trekbrief': TREKBRIEF_ALLOWED,
|
'trekbrief': TREKBRIEF_ALLOWED,
|
||||||
|
|
@ -87,6 +97,7 @@ const SCHEMA_ALLOWLISTS = Object.freeze({
|
||||||
'post_bash_stats': POST_BASH_STATS_ALLOWED, // common alt-spelling
|
'post_bash_stats': POST_BASH_STATS_ALLOWED, // common alt-spelling
|
||||||
'trekreview': TREKREVIEW_ALLOWED,
|
'trekreview': TREKREVIEW_ALLOWED,
|
||||||
'trekcontinue': TREKCONTINUE_ALLOWED,
|
'trekcontinue': TREKCONTINUE_ALLOWED,
|
||||||
|
'token-usage': TOKEN_USAGE_ALLOWED,
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -135,4 +146,5 @@ export {
|
||||||
TREKEXECUTE_ALLOWED,
|
TREKEXECUTE_ALLOWED,
|
||||||
TREKREVIEW_ALLOWED,
|
TREKREVIEW_ALLOWED,
|
||||||
TREKCONTINUE_ALLOWED,
|
TREKCONTINUE_ALLOWED,
|
||||||
|
TOKEN_USAGE_ALLOWED,
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -11,6 +11,8 @@ import { readFileSync } from 'node:fs';
|
||||||
import { join, dirname } from 'node:path';
|
import { join, dirname } from 'node:path';
|
||||||
import { fileURLToPath } from 'node:url';
|
import { fileURLToPath } from 'node:url';
|
||||||
import { transformToPrometheus, normalizeMetricName } from '../../lib/exporters/textfile-format.mjs';
|
import { transformToPrometheus, normalizeMetricName } from '../../lib/exporters/textfile-format.mjs';
|
||||||
|
import { transformToOtlpJson } from '../../lib/exporters/otlp-format.mjs';
|
||||||
|
import { applyFieldAllowlist } from '../../lib/exporters/field-allowlist.mjs';
|
||||||
|
|
||||||
const __dirname = dirname(fileURLToPath(import.meta.url));
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
||||||
const FIXTURES = join(__dirname, '..', 'fixtures');
|
const FIXTURES = join(__dirname, '..', 'fixtures');
|
||||||
|
|
@ -74,6 +76,38 @@ test('normalizeMetricName: dots/dashes/spaces → underscore, lowercase, voyage_
|
||||||
assert.equal(normalizeMetricName('METRIC NAME'), 'voyage_metric_name');
|
assert.equal(normalizeMetricName('METRIC NAME'), 'voyage_metric_name');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('SC#6: allowlisted token-usage record → Prometheus + OTLP metrics; PII absent (end-to-end)', () => {
|
||||||
|
// Raw record as written to token-usage-stats.jsonl (carries PII for upsert keying).
|
||||||
|
const raw = {
|
||||||
|
session_id: 'x',
|
||||||
|
transcript_path: '/p',
|
||||||
|
cwd: '/c',
|
||||||
|
ts: '2026-06-26T12:00:00.000Z',
|
||||||
|
scope: 'main-context',
|
||||||
|
model: 'claude-opus-4-8',
|
||||||
|
tokens_input: 12345,
|
||||||
|
tokens_output: 6789,
|
||||||
|
cost_usd: 0.42,
|
||||||
|
is_estimate: false,
|
||||||
|
price_table_version: '2026-06-26',
|
||||||
|
};
|
||||||
|
const rec = applyFieldAllowlist(raw, 'token-usage');
|
||||||
|
|
||||||
|
// Prometheus: numeric fields auto-promote to voyage_token_usage_* metrics.
|
||||||
|
const prom = transformToPrometheus([rec]);
|
||||||
|
assert.match(prom, /voyage_token_usage_tokens_input\b/);
|
||||||
|
assert.match(prom, /voyage_token_usage_cost_usd\b/);
|
||||||
|
// CWE-212: PII must never appear in exporter output.
|
||||||
|
assert.ok(!prom.includes('session_id'), 'session_id leaked into Prometheus output');
|
||||||
|
assert.ok(!prom.includes('transcript_path'), 'transcript_path leaked into Prometheus output');
|
||||||
|
assert.ok(!prom.includes('/p'), 'transcript_path value leaked into Prometheus output');
|
||||||
|
|
||||||
|
// OTLP parity: dot-separated, dash-preserved naming (NOT the Prometheus underscore form).
|
||||||
|
const otlp = JSON.stringify(transformToOtlpJson([rec]));
|
||||||
|
assert.ok(otlp.includes('voyage.token-usage.tokens_input'), 'OTLP token metric missing');
|
||||||
|
assert.ok(!otlp.includes('transcript_path'), 'transcript_path leaked into OTLP output');
|
||||||
|
});
|
||||||
|
|
||||||
test('determinism: identical input produces identical output (sorted keys)', () => {
|
test('determinism: identical input produces identical output (sorted keys)', () => {
|
||||||
const records = loadJsonl('stats-sample.jsonl');
|
const records = loadJsonl('stats-sample.jsonl');
|
||||||
const out1 = transformToPrometheus(records);
|
const out1 = transformToPrometheus(records);
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,7 @@ import {
|
||||||
applyFieldAllowlist,
|
applyFieldAllowlist,
|
||||||
POST_BASH_STATS_ALLOWED,
|
POST_BASH_STATS_ALLOWED,
|
||||||
EVENT_EMIT_PAYLOAD_ALLOWED,
|
EVENT_EMIT_PAYLOAD_ALLOWED,
|
||||||
|
TOKEN_USAGE_ALLOWED,
|
||||||
} from '../../lib/exporters/field-allowlist.mjs';
|
} from '../../lib/exporters/field-allowlist.mjs';
|
||||||
|
|
||||||
// ---- path-validator: CWE-22 mitigation -------------------------------------
|
// ---- path-validator: CWE-22 mitigation -------------------------------------
|
||||||
|
|
@ -241,6 +242,40 @@ test('field-allowlist: Object.freeze on allowlists (drift-pin)', () => {
|
||||||
assert.equal(Object.isFrozen(POST_BASH_STATS_ALLOWED), true,
|
assert.equal(Object.isFrozen(POST_BASH_STATS_ALLOWED), true,
|
||||||
'POST_BASH_STATS_ALLOWED must be frozen — runtime mutation prevention');
|
'POST_BASH_STATS_ALLOWED must be frozen — runtime mutation prevention');
|
||||||
assert.equal(Object.isFrozen(EVENT_EMIT_PAYLOAD_ALLOWED), true);
|
assert.equal(Object.isFrozen(EVENT_EMIT_PAYLOAD_ALLOWED), true);
|
||||||
|
assert.equal(Object.isFrozen(TOKEN_USAGE_ALLOWED), true,
|
||||||
|
'TOKEN_USAGE_ALLOWED must be frozen — runtime mutation prevention');
|
||||||
|
});
|
||||||
|
|
||||||
|
// ---- token-usage allowlist (SKAL-2, CWE-212) -------------------------------
|
||||||
|
|
||||||
|
test('field-allowlist: token-usage INCLUDES numeric/label fields, EXCLUDES session_id/transcript_path/cwd (two-sided)', () => {
|
||||||
|
const record = {
|
||||||
|
ts: '2026-06-26T12:00:00.000Z',
|
||||||
|
session_id: 'uuid-secret',
|
||||||
|
transcript_path: '/Users/ktg/.claude/projects/x/sesn.jsonl',
|
||||||
|
cwd: '/Users/ktg/secret/project',
|
||||||
|
scope: 'main-context',
|
||||||
|
model: 'claude-opus-4-8',
|
||||||
|
tokens_input: 12345,
|
||||||
|
tokens_output: 6789,
|
||||||
|
tokens_cache_creation: 400,
|
||||||
|
tokens_cache_read: 5000,
|
||||||
|
cost_usd: 0.42,
|
||||||
|
is_estimate: false,
|
||||||
|
price_table_version: '2026-06-26',
|
||||||
|
};
|
||||||
|
const out = applyFieldAllowlist(record, 'token-usage');
|
||||||
|
// INCLUDED (numeric + low-cardinality labels)
|
||||||
|
for (const k of ['tokens_input', 'tokens_output', 'tokens_cache_creation',
|
||||||
|
'tokens_cache_read', 'cost_usd', 'is_estimate', 'price_table_version', 'scope', 'model']) {
|
||||||
|
assert.equal(k in out, true, `${k} MUST be allowlisted`);
|
||||||
|
}
|
||||||
|
assert.equal(out.tokens_input, 12345);
|
||||||
|
assert.equal(out._schema_id, 'token-usage');
|
||||||
|
// EXCLUDED (CWE-212 boundary)
|
||||||
|
assert.equal('session_id' in out, false, 'session_id MUST be stripped (CWE-212)');
|
||||||
|
assert.equal('transcript_path' in out, false, 'transcript_path MUST be stripped (CWE-212)');
|
||||||
|
assert.equal('cwd' in out, false, 'cwd MUST be stripped (CWE-212)');
|
||||||
});
|
});
|
||||||
|
|
||||||
test('field-allowlist: null/undefined record handled safely', () => {
|
test('field-allowlist: null/undefined record handled safely', () => {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue