Skip to content

Commit e9a9817

Browse files
node9ainawi-25
andauthored
feat: implement log redaction and semantic parser enhancements (#1)
* feat: implement log redaction and semantic parser enhancements - Add redactSecrets to mask credentials in audit logs - Improve analyzeShellCommand to identify path-based commands (/usr/bin/rm) - Finalize v0.2.0 architectural leap with full validation * style: fix formatting in redactor tests --------- Co-authored-by: nadav <isr.nadav@gmail.com>
1 parent 6bda2c0 commit e9a9817

File tree

5 files changed

+154
-113
lines changed

5 files changed

+154
-113
lines changed

examples/demo.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,9 @@ async function main() {
1313

1414
try {
1515
await secureDelete('production-db-v1');
16-
} catch (err: any) {
17-
console.log(chalk.yellow(`\n🛡️ Node9 caught it: ${err.message}`));
16+
} catch (err: unknown) {
17+
const msg = err instanceof Error ? err.message : String(err);
18+
console.log(chalk.yellow(`\n🛡️ Node9 caught it: ${msg}`));
1819
}
1920
}
2021

package.json

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,14 @@
5454
"test": "vitest run",
5555
"test:watch": "vitest",
5656
"typecheck": "tsc --noEmit",
57-
"lint": "eslint src/",
58-
"lint:fix": "eslint src/ --fix",
59-
"format": "prettier --write src/",
60-
"format:check": "prettier --check src/",
57+
"lint": "eslint .",
58+
"lint:fix": "eslint . --fix",
59+
"format": "prettier --write .",
60+
"format:check": "prettier --check .",
61+
"fix": "npm run format && npm run lint:fix",
62+
"validate": "npm run format && npm run lint && npm run typecheck && npm run test && npm run test:e2e && npm run build",
6163
"test:e2e": "bash scripts/e2e.sh",
62-
"prepublishOnly": "npm run build"
64+
"prepublishOnly": "npm run validate"
6365
},
6466
"dependencies": {
6567
"@inquirer/prompts": "^8.3.0",

src/__tests__/redactor.test.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { describe, it, expect } from 'vitest';
2+
import { redactSecrets } from '../core';
3+
4+
describe('redactSecrets', () => {
5+
it('masks authorization bearer headers but keeps prefix', () => {
6+
const input = 'curl -H "Authorization: Bearer sk-1234567890abcdef"';
7+
const output = redactSecrets(input);
8+
expect(output).toContain('Authorization: Bearer ********');
9+
expect(output).not.toContain('sk-1234567890abcdef');
10+
});
11+
12+
it('masks api keys but keeps labels', () => {
13+
expect(redactSecrets('api_key="ABCDEFGHIJ1234567890"')).toContain('api_key="********');
14+
expect(redactSecrets('apikey: KEY_VALUE_9876543210')).toContain('apikey: ********');
15+
expect(redactSecrets('API-KEY=SOME_SECRET_VALUE_HERE')).toContain('API-KEY=********');
16+
});
17+
18+
it('masks tokens and passwords', () => {
19+
expect(redactSecrets('GITHUB_TOKEN=token_1234567890abcdefghijk')).toContain(
20+
'GITHUB_TOKEN=********'
21+
);
22+
expect(redactSecrets('password: "password_example_123"')).toContain('password: "********');
23+
});
24+
25+
it('masks generic long entropy strings', () => {
26+
const input = 'The hash is a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2';
27+
const output = redactSecrets(input);
28+
expect(output).toContain('********');
29+
});
30+
31+
it('does not mask short, safe words', () => {
32+
const input = 'npm install express';
33+
const output = redactSecrets(input);
34+
expect(output).toBe(input);
35+
});
36+
37+
it('handles JSON strings correctly', () => {
38+
const obj = { command: 'curl -H "Authorization: Bearer 12345678901234567890"' };
39+
const input = JSON.stringify(obj);
40+
const output = redactSecrets(input);
41+
expect(output).toContain('Bearer ********');
42+
});
43+
});

src/cli.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
#!/usr/bin/env node
22
import { Command } from 'commander';
3-
import { authorizeAction, authorizeHeadless } from './core';
3+
import { authorizeAction, authorizeHeadless, redactSecrets } from './core';
44
import { setupClaude, setupGemini, setupCursor } from './setup';
55
import { spawn } from 'child_process';
66
import { parseCommandString } from 'execa';
@@ -94,6 +94,9 @@ program
9494
if (target === 'claude') return await setupClaude();
9595
if (target === 'cursor') return await setupCursor();
9696
});
97+
98+
import { DANGEROUS_WORDS } from './core';
99+
97100
// 3. INIT
98101
program
99102
.command('init')
@@ -182,7 +185,7 @@ program
182185
decision: 'block',
183186
reason: msg,
184187
hookSpecificOutput: {
185-
hookEventName: 'PreToolUse',
188+
hookEvent_name: 'PreToolUse',
186189
permissionDecision: 'deny',
187190
permissionDecisionReason: msg,
188191
},
@@ -219,11 +222,14 @@ program
219222
try {
220223
if (!raw || raw.trim() === '') process.exit(0);
221224
const payload = JSON.parse(raw) as { tool_name?: string; tool_input?: unknown };
225+
226+
// Redact secrets from the input before stringifying for the log
222227
const entry = {
223228
ts: new Date().toISOString(),
224229
tool: sanitize(payload.tool_name ?? 'unknown'),
225-
input: payload.tool_input,
230+
input: JSON.parse(redactSecrets(JSON.stringify(payload.tool_input || {}))),
226231
};
232+
227233
const logPath = path.join(os.homedir(), '.node9', 'audit.log');
228234
if (!fs.existsSync(path.dirname(logPath)))
229235
fs.mkdirSync(path.dirname(logPath), { recursive: true });
@@ -268,5 +274,4 @@ program
268274
}
269275
});
270276

271-
import { DANGEROUS_WORDS } from './core';
272277
program.parse();

0 commit comments

Comments
 (0)