mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 12:15:29 +02:00
fix: redact escaped quoted credentials in observation records
Carry forward the bounded serialized-quote defect identified by PR 2786. Metadata-only observation consent remains separate; exclude the broader provider lifecycle head. Co-authored-by: 646826 <646826@gmail.com>
This commit is contained in:
@@ -273,7 +273,7 @@ import json, sys, os, re
|
||||
# prevent the catastrophic backtracking that pegged python at 100% CPU (#2278).
|
||||
_SECRET_RE = re.compile(
|
||||
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
|
||||
r"""(["'"'"'\s:=]{1,8})"""
|
||||
r"""(["'"'"'\s:=\\]{1,8})"""
|
||||
r"((?:bearer|basic|token|bot)\s+)?"
|
||||
r"([A-Za-z0-9_\-/.+=]{8,256})"
|
||||
)
|
||||
@@ -336,14 +336,15 @@ observation = {
|
||||
"project_name": os.environ.get("PROJECT_NAME_ENV", "global")
|
||||
}
|
||||
|
||||
# Scrub secrets: match common key=value, key: value, and key"value patterns
|
||||
# Scrub secrets, including JSON-escaped quotes/backslashes in serialized tool I/O.
|
||||
# This is defense in depth; arbitrary content still requires a metadata-only capture boundary.
|
||||
# Includes optional auth scheme (e.g., "Bearer", "Basic") before token
|
||||
# Linear-time secret matcher. Bounded quantifiers and a fixed set of auth
|
||||
# schemes (instead of a generic [A-Za-z]+\s+ that overlapped the value class)
|
||||
# prevent the catastrophic backtracking that pegged python at 100% CPU (#2278).
|
||||
_SECRET_RE = re.compile(
|
||||
r"(?i)(api[_-]?key|token|secret|password|authorization|credentials?|auth)"
|
||||
r"""(["'"'"'\s:=]{1,8})"""
|
||||
r"""(["'"'"'\s:=\\]{1,8})"""
|
||||
r"((?:bearer|basic|token|bot)\s+)?"
|
||||
r"([A-Za-z0-9_\-/.+=]{8,256})"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
'use strict';
|
||||
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const os = require('node:os');
|
||||
const path = require('node:path');
|
||||
const { spawnSync } = require('node:child_process');
|
||||
|
||||
const repoRoot = path.resolve(__dirname, '../..');
|
||||
const runner = path.join(repoRoot, 'scripts/hooks/run-with-flags.js');
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name, fn) {
|
||||
try { fn(); console.log(` PASS ${name}`); passed += 1; }
|
||||
catch (error) { console.log(` FAIL ${name}: ${error.message}`); failed += 1; }
|
||||
}
|
||||
|
||||
function capture(phase, input) {
|
||||
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'ecc-observe-redaction-'));
|
||||
try {
|
||||
const data = path.join(root, 'data');
|
||||
const result = spawnSync(process.execPath, [runner, `${phase}:observe`,
|
||||
'scripts/hooks/observe-runner.js', 'standard,strict'], {
|
||||
input: JSON.stringify({ cwd: root, session_id: 'redaction-fixture', ...input }),
|
||||
cwd: root, encoding: 'utf8', timeout: 12000,
|
||||
env: { ...process.env, HOME: root, USERPROFILE: root,
|
||||
CLV2_HOMUNCULUS_DIR: data, CLAUDE_CODE_ENTRYPOINT: 'cli',
|
||||
ECC_HOOK_PROFILE: 'standard', ECC_SKIP_OBSERVE: '0',
|
||||
ECC_DISABLED_HOOKS: '', ECC_OBSERVER_ENABLED: 'false' }
|
||||
});
|
||||
assert.equal(result.status, 0, result.stderr);
|
||||
assert.doesNotMatch(result.stderr, /runner failed|timed out/i);
|
||||
const records = fs.readFileSync(path.join(data, 'observations.jsonl'), 'utf8');
|
||||
assert.equal(records.trim().split('\n').length, 1);
|
||||
assert.equal(JSON.parse(records).event, phase === 'pre' ? 'tool_start' : 'tool_complete');
|
||||
return records;
|
||||
} finally {
|
||||
fs.rmSync(root, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('=== Actual registered observation secret redaction ===');
|
||||
if (process.platform !== 'win32' && spawnSync('python3', ['--version']).status === 0) {
|
||||
for (const [label, content] of [
|
||||
['double-quoted source', 'const apiKey = "CANARY_TOKEN_abcdefghijk";'],
|
||||
['single-quoted source', "const password = 'CANARY_TOKEN_abcdefghijk';"],
|
||||
['JSON credential', '{"token":"CANARY_TOKEN_abcdefghijk"}'],
|
||||
['nested escaped source', 'const auth = \\"CANARY_TOKEN_abcdefghijk\\";']
|
||||
]) {
|
||||
test(`redacts ${label} before persisting tool input`, () => {
|
||||
const records = capture('pre', { tool_name: 'Write',
|
||||
tool_input: { file_path: '/tmp/example.js', content } });
|
||||
assert.ok(!records.includes('CANARY_TOKEN_abcdefghijk'), 'seeded credential persisted');
|
||||
assert.ok(records.includes('[REDACTED]'));
|
||||
});
|
||||
test(`redacts ${label} before persisting structured tool output`, () => {
|
||||
const records = capture('post', { tool_name: 'Read',
|
||||
tool_input: { file_path: '/tmp/example.js' }, tool_response: { content } });
|
||||
assert.ok(!records.includes('CANARY_TOKEN_abcdefghijk'), 'seeded credential persisted');
|
||||
assert.ok(records.includes('[REDACTED]'));
|
||||
});
|
||||
}
|
||||
} else {
|
||||
console.log('Skipping shell fixtures: native Windows runner requires platform CI.');
|
||||
}
|
||||
console.log(`Passed: ${passed}`);
|
||||
console.log(`Failed: ${failed}`);
|
||||
process.exitCode = failed ? 1 : 0;
|
||||
Reference in New Issue
Block a user