code-warden 3.1.1 → 3.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. package/CONFIGURE.md +39 -39
  2. package/DECISIONS.md +107 -107
  3. package/README.md +199 -137
  4. package/SKILL.md +169 -169
  5. package/bin/code-warden.js +82 -0
  6. package/codewarden.json +14 -14
  7. package/examples/governed-session.md +132 -132
  8. package/install.js +399 -399
  9. package/install.ps1 +32 -32
  10. package/install.sh +33 -33
  11. package/package.json +45 -2
  12. package/references/anti-drift.md +55 -55
  13. package/references/architecture.md +26 -26
  14. package/references/cleanup.md +30 -30
  15. package/references/cognition.md +36 -36
  16. package/references/operations.md +45 -45
  17. package/references/planning-gates.md +83 -83
  18. package/references/research-and-fit.md +51 -51
  19. package/references/safety.md +31 -31
  20. package/templates/ci/github-actions.yml +83 -66
  21. package/tools/auto-detect.js +91 -91
  22. package/tools/auto-targets.js +104 -104
  23. package/tools/auto-windsurf-adapter.js +75 -75
  24. package/tools/get-context.js +50 -50
  25. package/tools/governance-report.js +302 -0
  26. package/tools/hooks/claude/install-hooks.js +112 -112
  27. package/tools/hooks/claude/uninstall-hooks.js +75 -75
  28. package/tools/hooks/claude/warden-lint-hook.js +106 -106
  29. package/tools/hooks/claude/warden-secrets-hook.js +73 -73
  30. package/tools/hooks/codex/install-hooks.js +100 -100
  31. package/tools/hooks/codex/uninstall-hooks.js +53 -53
  32. package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
  33. package/tools/hooks/codex/warden-bash-hook.js +51 -51
  34. package/tools/lib/config.js +49 -49
  35. package/tools/lib/file-collection.js +72 -72
  36. package/tools/lib/line-count.js +28 -28
  37. package/tools/lib/secret-patterns.js +57 -57
  38. package/tools/tests/fixtures/clean.js +9 -9
  39. package/tools/tests/run-tests.js +210 -210
  40. package/tools/verify-secrets.js +26 -26
  41. package/tools/warden-lint.js +27 -27
@@ -1,57 +1,57 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * secret-patterns.js
6
- * Canonical hardcoded-credential patterns shared across all Code-Warden
7
- * scanners (CLI and hooks).
8
- *
9
- * Previously each consumer defined its own copy, causing drift:
10
- * - verify-secrets.js: gh[housr]_ (separate entries per prefix)
11
- * - warden-secrets-hook.js: gh[pousr]_
12
- * - warden-apply-patch-hook.js: gh[posx]_ ← missed 'u' (refresh tokens)
13
- * - warden-bash-hook.js: gh[posx]_ ← missed 'u' (refresh tokens)
14
- *
15
- * This module is the single source of truth. All consumers require() it.
16
- *
17
- * Terminology alignment (per v3.1.1 docs):
18
- * - This module implements the "hardcoded credential scanner" logic.
19
- * - The governance rule that mandates its use is the "zero-trust secrets policy".
20
- */
21
-
22
- /**
23
- * @typedef {{ label: string, re: RegExp }} SecretPattern
24
- */
25
-
26
- /** @type {SecretPattern[]} */
27
- const SECRET_PATTERNS = [
28
- { label: 'AWS access key', re: /\bAKIA[0-9A-Z]{16}\b/ },
29
- { label: 'OpenAI key', re: /\bsk-[A-Za-z0-9]{32,}\b/ },
30
- { label: 'GitHub token', re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/i },
31
- { label: 'Stripe secret key', re: /\bsk_(live|test)_[A-Za-z0-9]{24,}\b/ },
32
- { label: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9\-]{10,}\b/ },
33
- { label: 'SendGrid key', re: /\bSG\.[A-Za-z0-9_\-.]{20,}\b/ },
34
- { label: 'Twilio SID', re: /\bAC[a-f0-9]{32}\b/ },
35
- { label: 'generic API key', re: /api[_-]?key\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
36
- { label: 'generic secret key', re: /secret[_-]?key\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
37
- { label: 'password assignment', re: /password\s*[:=]\s*['"][^'"]{8,}['"]/i },
38
- { label: 'bearer token', re: /bearer\s+[A-Za-z0-9\-._~+\/]{20,}/i },
39
- { label: 'private key header', re: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
40
- { label: 'database URL with creds', re: /[a-z][a-z0-9+\-.]*:\/\/[^:@\s]+:[^@\s]{4,}@[^/\s]+\// },
41
- ];
42
-
43
- /**
44
- * Scan a content string against all secret patterns.
45
- *
46
- * @param {string} content
47
- * @returns {{ label: string } | null} First matching pattern label, or null if clean.
48
- */
49
- function scanForSecrets(content) {
50
- if (typeof content !== 'string' || content.length === 0) return null;
51
- for (const { label, re } of SECRET_PATTERNS) {
52
- if (re.test(content)) return { label };
53
- }
54
- return null;
55
- }
56
-
57
- module.exports = { SECRET_PATTERNS, scanForSecrets };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * secret-patterns.js
6
+ * Canonical hardcoded-credential patterns shared across all Code-Warden
7
+ * scanners (CLI and hooks).
8
+ *
9
+ * Previously each consumer defined its own copy, causing drift:
10
+ * - verify-secrets.js: gh[housr]_ (separate entries per prefix)
11
+ * - warden-secrets-hook.js: gh[pousr]_
12
+ * - warden-apply-patch-hook.js: gh[posx]_ ← missed 'u' (refresh tokens)
13
+ * - warden-bash-hook.js: gh[posx]_ ← missed 'u' (refresh tokens)
14
+ *
15
+ * This module is the single source of truth. All consumers require() it.
16
+ *
17
+ * Terminology alignment (per v3.1.1 docs):
18
+ * - This module implements the "hardcoded credential scanner" logic.
19
+ * - The governance rule that mandates its use is the "zero-trust secrets policy".
20
+ */
21
+
22
+ /**
23
+ * @typedef {{ label: string, re: RegExp }} SecretPattern
24
+ */
25
+
26
+ /** @type {SecretPattern[]} */
27
+ const SECRET_PATTERNS = [
28
+ { label: 'AWS access key', re: /\bAKIA[0-9A-Z]{16}\b/ },
29
+ { label: 'OpenAI key', re: /\bsk-[A-Za-z0-9]{32,}\b/ },
30
+ { label: 'GitHub token', re: /\bgh[pousr]_[A-Za-z0-9]{36,}\b/i },
31
+ { label: 'Stripe secret key', re: /\bsk_(live|test)_[A-Za-z0-9]{24,}\b/ },
32
+ { label: 'Slack token', re: /\bxox[baprs]-[A-Za-z0-9\-]{10,}\b/ },
33
+ { label: 'SendGrid key', re: /\bSG\.[A-Za-z0-9_\-.]{20,}\b/ },
34
+ { label: 'Twilio SID', re: /\bAC[a-f0-9]{32}\b/ },
35
+ { label: 'generic API key', re: /api[_-]?key\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
36
+ { label: 'generic secret key', re: /secret[_-]?key\s*[:=]\s*['"]?[A-Za-z0-9_\-]{20,}/i },
37
+ { label: 'password assignment', re: /password\s*[:=]\s*['"][^'"]{8,}['"]/i },
38
+ { label: 'bearer token', re: /bearer\s+[A-Za-z0-9\-._~+\/]{20,}/i },
39
+ { label: 'private key header', re: /-----BEGIN (RSA |EC |OPENSSH )?PRIVATE KEY-----/ },
40
+ { label: 'database URL with creds', re: /[a-z][a-z0-9+\-.]*:\/\/[^:@\s]+:[^@\s]{4,}@[^/\s]+\// },
41
+ ];
42
+
43
+ /**
44
+ * Scan a content string against all secret patterns.
45
+ *
46
+ * @param {string} content
47
+ * @returns {{ label: string } | null} First matching pattern label, or null if clean.
48
+ */
49
+ function scanForSecrets(content) {
50
+ if (typeof content !== 'string' || content.length === 0) return null;
51
+ for (const { label, re } of SECRET_PATTERNS) {
52
+ if (re.test(content)) return { label };
53
+ }
54
+ return null;
55
+ }
56
+
57
+ module.exports = { SECRET_PATTERNS, scanForSecrets };
@@ -1,9 +1,9 @@
1
- // Fixture: clean file — no secrets, well within line limit.
2
- // Used by: warden-lint (expect PASS) and verify-secrets (expect PASS).
3
- 'use strict';
4
-
5
- function greet(name) {
6
- return `Hello, ${name}`;
7
- }
8
-
9
- module.exports = { greet };
1
+ // Fixture: clean file — no secrets, well within line limit.
2
+ // Used by: warden-lint (expect PASS) and verify-secrets (expect PASS).
3
+ 'use strict';
4
+
5
+ function greet(name) {
6
+ return `Hello, ${name}`;
7
+ }
8
+
9
+ module.exports = { greet };
@@ -1,210 +1,210 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * run-tests.js
6
- * Behavioral test suite for Code-Warden scanners and hooks.
7
- *
8
- * Verifies that enforcement tools actually enforce — not just that they exist.
9
- * Uses Node's built-in node:test (no external test framework required).
10
- *
11
- * Tests:
12
- * CLI tools
13
- * 1. warden-lint clean file → exit 0
14
- * 2. warden-lint oversized → exit 1
15
- * 3. verify-secrets clean file → exit 0
16
- * 4. verify-secrets secret file → exit 1
17
- *
18
- * Claude hooks (PreToolUse JSON payloads via stdin)
19
- * 5. warden-lint-hook Write oversized → exit 2
20
- * 6. warden-secrets-hook Write secret → exit 2
21
- *
22
- * Codex hooks
23
- * 7. warden-apply-patch-hook patch with secret → exit 2
24
- * 8. warden-bash-hook command with secret → exit 2
25
- *
26
- * Fixture strategy:
27
- * - clean.js is committed (no secrets, short).
28
- * - Oversized content is generated in memory / tmpdir (committed file would
29
- * trip warden-lint itself).
30
- * - Secret content is generated via makeFakeSecret() which builds the
31
- * pattern from parts so the scanner does not flag this source file.
32
- */
33
-
34
- const { test } = require('node:test');
35
- const assert = require('node:assert/strict');
36
- const { spawnSync } = require('node:child_process');
37
- const path = require('node:path');
38
- const fs = require('node:fs');
39
- const os = require('node:os');
40
-
41
- // ---------------------------------------------------------------------------
42
- // Paths
43
- // ---------------------------------------------------------------------------
44
-
45
- const ROOT = path.join(__dirname, '..', '..');
46
- const TOOLS = path.join(ROOT, 'tools');
47
- const FIXTURES = path.join(__dirname, 'fixtures');
48
- const CLEAN = path.join(FIXTURES, 'clean.js');
49
-
50
- if (!fs.existsSync(CLEAN)) throw new Error(`Missing fixture: clean at ${CLEAN}`);
51
-
52
- // ---------------------------------------------------------------------------
53
- // Fixture generators — never committed, avoids false-positive self-scan
54
- // ---------------------------------------------------------------------------
55
-
56
- /**
57
- * Build a fake OpenAI-style key from parts so this source file does not
58
- * contain a string that matches the secret scanner's pattern literally.
59
- */
60
- function makeFakeSecret() {
61
- return ['sk', '-', 'a'.repeat(48)].join('');
62
- }
63
-
64
- /** Generate content containing a hardcoded credential. */
65
- function makeSecretContent() {
66
- return [
67
- '// Generated secret fixture — not committed to repo.',
68
- "'use strict';",
69
- `const KEY = '${makeFakeSecret()}';`,
70
- 'module.exports = { KEY };',
71
- ].join('\n') + '\n';
72
- }
73
-
74
- /** Generate content that exceeds the 400-line limit. */
75
- function makeOversizedContent(limit = 400) {
76
- const lines = [
77
- '// Generated oversized fixture — not committed to repo.',
78
- "'use strict';",
79
- '',
80
- ];
81
- for (let i = 1; i <= limit + 15; i++) {
82
- lines.push(`const line${i} = ${i}; // padding`);
83
- }
84
- return lines.join('\n') + '\n';
85
- }
86
-
87
- /** Write content to a temp file; caller is responsible for cleanup. */
88
- function writeTmp(name, content) {
89
- const p = path.join(os.tmpdir(), `cw-fixture-${process.pid}-${name}`);
90
- fs.writeFileSync(p, content);
91
- return p;
92
- }
93
-
94
- // ---------------------------------------------------------------------------
95
- // Helpers
96
- // ---------------------------------------------------------------------------
97
-
98
- function runCLI(scriptPath, args = []) {
99
- const result = spawnSync(process.execPath, [scriptPath, ...args], { encoding: 'utf8' });
100
- return { code: result.status ?? result.signal, stdout: result.stdout || '', stderr: result.stderr || '' };
101
- }
102
-
103
- function runHook(scriptPath, payload) {
104
- const result = spawnSync(process.execPath, [scriptPath], { input: JSON.stringify(payload), encoding: 'utf8' });
105
- return { code: result.status ?? result.signal, stdout: result.stdout || '', stderr: result.stderr || '' };
106
- }
107
-
108
- // ---------------------------------------------------------------------------
109
- // CLI: warden-lint
110
- // ---------------------------------------------------------------------------
111
-
112
- test('warden-lint: clean file exits 0', () => {
113
- const { code } = runCLI(path.join(TOOLS, 'warden-lint.js'), [CLEAN]);
114
- assert.equal(code, 0, 'expected exit 0 for a clean, short file');
115
- });
116
-
117
- test('warden-lint: oversized file exits 1', () => {
118
- const tmp = writeTmp('oversized.js', makeOversizedContent());
119
- try {
120
- const { code, stderr } = runCLI(path.join(TOOLS, 'warden-lint.js'), [tmp]);
121
- assert.equal(code, 1, 'expected exit 1 for an oversized file');
122
- assert.ok(stderr.includes('[FAIL]'), 'expected [FAIL] in stderr');
123
- } finally {
124
- fs.rmSync(tmp, { force: true });
125
- }
126
- });
127
-
128
- // ---------------------------------------------------------------------------
129
- // CLI: verify-secrets
130
- // ---------------------------------------------------------------------------
131
-
132
- test('verify-secrets: clean file exits 0', () => {
133
- const { code } = runCLI(path.join(TOOLS, 'verify-secrets.js'), [CLEAN]);
134
- assert.equal(code, 0, 'expected exit 0 for a file with no secrets');
135
- });
136
-
137
- test('verify-secrets: secret file exits 1', () => {
138
- const tmp = writeTmp('secret.js', makeSecretContent());
139
- try {
140
- const { code, stderr } = runCLI(path.join(TOOLS, 'verify-secrets.js'), [tmp]);
141
- assert.equal(code, 1, 'expected exit 1 for a file containing a hardcoded secret');
142
- assert.ok(stderr.includes('[FAIL]'), 'expected [FAIL] in stderr');
143
- } finally {
144
- fs.rmSync(tmp, { force: true });
145
- }
146
- });
147
-
148
- // ---------------------------------------------------------------------------
149
- // Claude hook: warden-lint-hook (Write with oversized content)
150
- // ---------------------------------------------------------------------------
151
-
152
- test('Claude lint hook: Write oversized content exits 2', () => {
153
- const payload = {
154
- tool_name: 'Write',
155
- tool_input: { file_path: '/tmp/test-oversized.js', content: makeOversizedContent() },
156
- };
157
- const { code, stdout } = runHook(path.join(TOOLS, 'hooks', 'claude', 'warden-lint-hook.js'), payload);
158
- assert.equal(code, 2, 'expected exit 2 (deny) for oversized Write');
159
- const response = JSON.parse(stdout);
160
- assert.equal(response.hookSpecificOutput.permissionDecision, 'deny', 'expected deny decision');
161
- });
162
-
163
- // ---------------------------------------------------------------------------
164
- // Claude hook: warden-secrets-hook (Write with secret content)
165
- // ---------------------------------------------------------------------------
166
-
167
- test('Claude secrets hook: Write secret content exits 2', () => {
168
- const payload = {
169
- tool_name: 'Write',
170
- tool_input: { file_path: '/tmp/test-secret.js', content: makeSecretContent() },
171
- };
172
- const { code, stdout } = runHook(path.join(TOOLS, 'hooks', 'claude', 'warden-secrets-hook.js'), payload);
173
- assert.equal(code, 2, 'expected exit 2 (deny) for Write containing secret');
174
- const response = JSON.parse(stdout);
175
- assert.equal(response.hookSpecificOutput.permissionDecision, 'deny', 'expected deny decision');
176
- });
177
-
178
- // ---------------------------------------------------------------------------
179
- // Codex hook: warden-apply-patch-hook (patch adding a secret)
180
- // ---------------------------------------------------------------------------
181
-
182
- test('Codex apply_patch hook: patch with secret exits 2', () => {
183
- const patch = [
184
- '*** /dev/null',
185
- '+++ tmp/secret-patch.js',
186
- '@@ -0,0 +1,2 @@',
187
- `+const KEY = '${makeFakeSecret()}';`,
188
- '+module.exports = { KEY };',
189
- ].join('\n');
190
-
191
- const { code, stdout } = runHook(
192
- path.join(TOOLS, 'hooks', 'codex', 'warden-apply-patch-hook.js'),
193
- { tool: 'apply_patch', toolInput: { patch } }
194
- );
195
- assert.equal(code, 2, 'expected exit 2 (deny) for apply_patch with hardcoded secret');
196
- assert.ok(JSON.parse(stdout).deny === true, 'expected deny:true');
197
- });
198
-
199
- // ---------------------------------------------------------------------------
200
- // Codex hook: warden-bash-hook (command containing a secret)
201
- // ---------------------------------------------------------------------------
202
-
203
- test('Codex Bash hook: command with secret exits 2', () => {
204
- const { code, stdout } = runHook(
205
- path.join(TOOLS, 'hooks', 'codex', 'warden-bash-hook.js'),
206
- { tool: 'Bash', toolInput: { command: `echo ${makeFakeSecret()}` } }
207
- );
208
- assert.equal(code, 2, 'expected exit 2 (deny) for Bash command with hardcoded secret');
209
- assert.ok(JSON.parse(stdout).deny === true, 'expected deny:true');
210
- });
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * run-tests.js
6
+ * Behavioral test suite for Code-Warden scanners and hooks.
7
+ *
8
+ * Verifies that enforcement tools actually enforce — not just that they exist.
9
+ * Uses Node's built-in node:test (no external test framework required).
10
+ *
11
+ * Tests:
12
+ * CLI tools
13
+ * 1. warden-lint clean file → exit 0
14
+ * 2. warden-lint oversized → exit 1
15
+ * 3. verify-secrets clean file → exit 0
16
+ * 4. verify-secrets secret file → exit 1
17
+ *
18
+ * Claude hooks (PreToolUse JSON payloads via stdin)
19
+ * 5. warden-lint-hook Write oversized → exit 2
20
+ * 6. warden-secrets-hook Write secret → exit 2
21
+ *
22
+ * Codex hooks
23
+ * 7. warden-apply-patch-hook patch with secret → exit 2
24
+ * 8. warden-bash-hook command with secret → exit 2
25
+ *
26
+ * Fixture strategy:
27
+ * - clean.js is committed (no secrets, short).
28
+ * - Oversized content is generated in memory / tmpdir (committed file would
29
+ * trip warden-lint itself).
30
+ * - Secret content is generated via makeFakeSecret() which builds the
31
+ * pattern from parts so the scanner does not flag this source file.
32
+ */
33
+
34
+ const { test } = require('node:test');
35
+ const assert = require('node:assert/strict');
36
+ const { spawnSync } = require('node:child_process');
37
+ const path = require('node:path');
38
+ const fs = require('node:fs');
39
+ const os = require('node:os');
40
+
41
+ // ---------------------------------------------------------------------------
42
+ // Paths
43
+ // ---------------------------------------------------------------------------
44
+
45
+ const ROOT = path.join(__dirname, '..', '..');
46
+ const TOOLS = path.join(ROOT, 'tools');
47
+ const FIXTURES = path.join(__dirname, 'fixtures');
48
+ const CLEAN = path.join(FIXTURES, 'clean.js');
49
+
50
+ if (!fs.existsSync(CLEAN)) throw new Error(`Missing fixture: clean at ${CLEAN}`);
51
+
52
+ // ---------------------------------------------------------------------------
53
+ // Fixture generators — never committed, avoids false-positive self-scan
54
+ // ---------------------------------------------------------------------------
55
+
56
+ /**
57
+ * Build a fake OpenAI-style key from parts so this source file does not
58
+ * contain a string that matches the secret scanner's pattern literally.
59
+ */
60
+ function makeFakeSecret() {
61
+ return ['sk', '-', 'a'.repeat(48)].join('');
62
+ }
63
+
64
+ /** Generate content containing a hardcoded credential. */
65
+ function makeSecretContent() {
66
+ return [
67
+ '// Generated secret fixture — not committed to repo.',
68
+ "'use strict';",
69
+ `const KEY = '${makeFakeSecret()}';`,
70
+ 'module.exports = { KEY };',
71
+ ].join('\n') + '\n';
72
+ }
73
+
74
+ /** Generate content that exceeds the 400-line limit. */
75
+ function makeOversizedContent(limit = 400) {
76
+ const lines = [
77
+ '// Generated oversized fixture — not committed to repo.',
78
+ "'use strict';",
79
+ '',
80
+ ];
81
+ for (let i = 1; i <= limit + 15; i++) {
82
+ lines.push(`const line${i} = ${i}; // padding`);
83
+ }
84
+ return lines.join('\n') + '\n';
85
+ }
86
+
87
+ /** Write content to a temp file; caller is responsible for cleanup. */
88
+ function writeTmp(name, content) {
89
+ const p = path.join(os.tmpdir(), `cw-fixture-${process.pid}-${name}`);
90
+ fs.writeFileSync(p, content);
91
+ return p;
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Helpers
96
+ // ---------------------------------------------------------------------------
97
+
98
+ function runCLI(scriptPath, args = []) {
99
+ const result = spawnSync(process.execPath, [scriptPath, ...args], { encoding: 'utf8' });
100
+ return { code: result.status ?? result.signal, stdout: result.stdout || '', stderr: result.stderr || '' };
101
+ }
102
+
103
+ function runHook(scriptPath, payload) {
104
+ const result = spawnSync(process.execPath, [scriptPath], { input: JSON.stringify(payload), encoding: 'utf8' });
105
+ return { code: result.status ?? result.signal, stdout: result.stdout || '', stderr: result.stderr || '' };
106
+ }
107
+
108
+ // ---------------------------------------------------------------------------
109
+ // CLI: warden-lint
110
+ // ---------------------------------------------------------------------------
111
+
112
+ test('warden-lint: clean file exits 0', () => {
113
+ const { code } = runCLI(path.join(TOOLS, 'warden-lint.js'), [CLEAN]);
114
+ assert.equal(code, 0, 'expected exit 0 for a clean, short file');
115
+ });
116
+
117
+ test('warden-lint: oversized file exits 1', () => {
118
+ const tmp = writeTmp('oversized.js', makeOversizedContent());
119
+ try {
120
+ const { code, stderr } = runCLI(path.join(TOOLS, 'warden-lint.js'), [tmp]);
121
+ assert.equal(code, 1, 'expected exit 1 for an oversized file');
122
+ assert.ok(stderr.includes('[FAIL]'), 'expected [FAIL] in stderr');
123
+ } finally {
124
+ fs.rmSync(tmp, { force: true });
125
+ }
126
+ });
127
+
128
+ // ---------------------------------------------------------------------------
129
+ // CLI: verify-secrets
130
+ // ---------------------------------------------------------------------------
131
+
132
+ test('verify-secrets: clean file exits 0', () => {
133
+ const { code } = runCLI(path.join(TOOLS, 'verify-secrets.js'), [CLEAN]);
134
+ assert.equal(code, 0, 'expected exit 0 for a file with no secrets');
135
+ });
136
+
137
+ test('verify-secrets: secret file exits 1', () => {
138
+ const tmp = writeTmp('secret.js', makeSecretContent());
139
+ try {
140
+ const { code, stderr } = runCLI(path.join(TOOLS, 'verify-secrets.js'), [tmp]);
141
+ assert.equal(code, 1, 'expected exit 1 for a file containing a hardcoded secret');
142
+ assert.ok(stderr.includes('[FAIL]'), 'expected [FAIL] in stderr');
143
+ } finally {
144
+ fs.rmSync(tmp, { force: true });
145
+ }
146
+ });
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Claude hook: warden-lint-hook (Write with oversized content)
150
+ // ---------------------------------------------------------------------------
151
+
152
+ test('Claude lint hook: Write oversized content exits 2', () => {
153
+ const payload = {
154
+ tool_name: 'Write',
155
+ tool_input: { file_path: '/tmp/test-oversized.js', content: makeOversizedContent() },
156
+ };
157
+ const { code, stdout } = runHook(path.join(TOOLS, 'hooks', 'claude', 'warden-lint-hook.js'), payload);
158
+ assert.equal(code, 2, 'expected exit 2 (deny) for oversized Write');
159
+ const response = JSON.parse(stdout);
160
+ assert.equal(response.hookSpecificOutput.permissionDecision, 'deny', 'expected deny decision');
161
+ });
162
+
163
+ // ---------------------------------------------------------------------------
164
+ // Claude hook: warden-secrets-hook (Write with secret content)
165
+ // ---------------------------------------------------------------------------
166
+
167
+ test('Claude secrets hook: Write secret content exits 2', () => {
168
+ const payload = {
169
+ tool_name: 'Write',
170
+ tool_input: { file_path: '/tmp/test-secret.js', content: makeSecretContent() },
171
+ };
172
+ const { code, stdout } = runHook(path.join(TOOLS, 'hooks', 'claude', 'warden-secrets-hook.js'), payload);
173
+ assert.equal(code, 2, 'expected exit 2 (deny) for Write containing secret');
174
+ const response = JSON.parse(stdout);
175
+ assert.equal(response.hookSpecificOutput.permissionDecision, 'deny', 'expected deny decision');
176
+ });
177
+
178
+ // ---------------------------------------------------------------------------
179
+ // Codex hook: warden-apply-patch-hook (patch adding a secret)
180
+ // ---------------------------------------------------------------------------
181
+
182
+ test('Codex apply_patch hook: patch with secret exits 2', () => {
183
+ const patch = [
184
+ '*** /dev/null',
185
+ '+++ tmp/secret-patch.js',
186
+ '@@ -0,0 +1,2 @@',
187
+ `+const KEY = '${makeFakeSecret()}';`,
188
+ '+module.exports = { KEY };',
189
+ ].join('\n');
190
+
191
+ const { code, stdout } = runHook(
192
+ path.join(TOOLS, 'hooks', 'codex', 'warden-apply-patch-hook.js'),
193
+ { tool: 'apply_patch', toolInput: { patch } }
194
+ );
195
+ assert.equal(code, 2, 'expected exit 2 (deny) for apply_patch with hardcoded secret');
196
+ assert.ok(JSON.parse(stdout).deny === true, 'expected deny:true');
197
+ });
198
+
199
+ // ---------------------------------------------------------------------------
200
+ // Codex hook: warden-bash-hook (command containing a secret)
201
+ // ---------------------------------------------------------------------------
202
+
203
+ test('Codex Bash hook: command with secret exits 2', () => {
204
+ const { code, stdout } = runHook(
205
+ path.join(TOOLS, 'hooks', 'codex', 'warden-bash-hook.js'),
206
+ { tool: 'Bash', toolInput: { command: `echo ${makeFakeSecret()}` } }
207
+ );
208
+ assert.equal(code, 2, 'expected exit 2 (deny) for Bash command with hardcoded secret');
209
+ assert.ok(JSON.parse(stdout).deny === true, 'expected deny:true');
210
+ });
@@ -1,26 +1,26 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- const fs = require('fs');
5
- const { scanForSecrets } = require('./lib/secret-patterns');
6
- const { expandPaths } = require('./lib/file-collection');
7
-
8
- const filePaths = expandPaths(process.argv.slice(2), 'verify-secrets.js');
9
- let hasErrors = false;
10
-
11
- for (const filePath of filePaths) {
12
- const content = fs.readFileSync(filePath, 'utf8');
13
- const hit = scanForSecrets(content);
14
-
15
- if (hit) {
16
- console.error(`[FAIL] [CodeWarden] Hardcoded credential detected in ${filePath} - pattern: ${hit.label}`);
17
- console.error(' Rule: All secrets must be sourced from an environment variable (e.g., process.env)');
18
- hasErrors = true;
19
- } else {
20
- console.log(`[PASS] [CodeWarden] ${filePath} passed hardcoded credential scan.`);
21
- }
22
- }
23
-
24
- if (hasErrors) {
25
- process.exit(1);
26
- }
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('fs');
5
+ const { scanForSecrets } = require('./lib/secret-patterns');
6
+ const { expandPaths } = require('./lib/file-collection');
7
+
8
+ const filePaths = expandPaths(process.argv.slice(2), 'verify-secrets.js');
9
+ let hasErrors = false;
10
+
11
+ for (const filePath of filePaths) {
12
+ const content = fs.readFileSync(filePath, 'utf8');
13
+ const hit = scanForSecrets(content);
14
+
15
+ if (hit) {
16
+ console.error(`[FAIL] [CodeWarden] Hardcoded credential detected in ${filePath} - pattern: ${hit.label}`);
17
+ console.error(' Rule: All secrets must be sourced from an environment variable (e.g., process.env)');
18
+ hasErrors = true;
19
+ } else {
20
+ console.log(`[PASS] [CodeWarden] ${filePath} passed hardcoded credential scan.`);
21
+ }
22
+ }
23
+
24
+ if (hasErrors) {
25
+ process.exit(1);
26
+ }