code-warden 3.3.0 → 3.3.1

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 (40) hide show
  1. package/CONFIGURE.md +39 -39
  2. package/DECISIONS.md +107 -107
  3. package/README.md +6 -0
  4. package/SKILL.md +169 -169
  5. package/bin/code-warden.js +82 -82
  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 +62 -62
  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/tools/auto-detect.js +91 -91
  21. package/tools/auto-targets.js +104 -104
  22. package/tools/auto-windsurf-adapter.js +75 -75
  23. package/tools/get-context.js +50 -50
  24. package/tools/governance-report.js +302 -302
  25. package/tools/hooks/claude/install-hooks.js +112 -112
  26. package/tools/hooks/claude/uninstall-hooks.js +75 -75
  27. package/tools/hooks/claude/warden-lint-hook.js +106 -106
  28. package/tools/hooks/claude/warden-secrets-hook.js +73 -73
  29. package/tools/hooks/codex/install-hooks.js +100 -100
  30. package/tools/hooks/codex/uninstall-hooks.js +53 -53
  31. package/tools/hooks/codex/warden-apply-patch-hook.js +113 -113
  32. package/tools/hooks/codex/warden-bash-hook.js +51 -51
  33. package/tools/lib/config.js +49 -49
  34. package/tools/lib/file-collection.js +5 -2
  35. package/tools/lib/line-count.js +28 -28
  36. package/tools/lib/secret-patterns.js +57 -57
  37. package/tools/tests/fixtures/clean.js +9 -9
  38. package/tools/tests/run-tests.js +210 -210
  39. package/tools/verify-secrets.js +26 -26
  40. package/tools/warden-lint.js +27 -27
@@ -1,113 +1,113 @@
1
- #!/usr/bin/env node
2
- /**
3
- * warden-apply-patch-hook.js — Codex PreToolUse hook
4
- *
5
- * Fires on apply_patch tool calls. Scans added lines for hardcoded credentials
6
- * and estimates resulting file size where the target path can be extracted.
7
- *
8
- * Codex hook payload (stdin, JSON):
9
- * { tool: "apply_patch", toolInput: { patch: "<patch text>" } }
10
- *
11
- * Exit codes:
12
- * 0 — proceed
13
- * 2 — block (writes JSON deny to stdout)
14
- */
15
-
16
- 'use strict';
17
-
18
- const fs = require('fs');
19
- const path = require('path');
20
- const { scanForSecrets } = require('../../lib/secret-patterns');
21
- const { countLines } = require('../../lib/line-count');
22
- const { loadConfig } = require('../../lib/config');
23
-
24
- const { maxFileLength: maxLines } = loadConfig();
25
-
26
-
27
- function deny(reason) {
28
- process.stdout.write(JSON.stringify({ deny: true, message: `[CodeWarden] ${reason}` }) + '\n');
29
- process.exit(2);
30
- }
31
-
32
- // ---------------------------------------------------------------------------
33
- // Patch parsing helpers
34
- // ---------------------------------------------------------------------------
35
-
36
- /**
37
- * Extract lines added by the patch (lines starting with '+', not '+++').
38
- */
39
- function extractAddedLines(patch) {
40
- return patch.split('\n')
41
- .filter(l => l.startsWith('+') && !l.startsWith('+++'))
42
- .map(l => l.slice(1));
43
- }
44
-
45
- /**
46
- * Extract target file path from patch header (*** path or +++ path or --- path).
47
- * Returns null if not detectable.
48
- */
49
- function extractTargetPath(patch) {
50
- for (const line of patch.split('\n')) {
51
- const m = line.match(/^\+{3}\s+(.+?)(\s+\d{4}-\d{2}-\d{2}.*)?$/) ||
52
- line.match(/^\*{3}\s+(.+?)(\s+\d{4}-\d{2}-\d{2}.*)?$/);
53
- if (m) {
54
- const p = m[1].trim();
55
- if (p !== '/dev/null') return p;
56
- }
57
- }
58
- return null;
59
- }
60
-
61
- /**
62
- * Estimate resulting line count: base file lines + added lines - removed lines.
63
- * Returns null if file not readable.
64
- */
65
- function estimateResultLines(patch, targetPath) {
66
- let baseLines = 0;
67
- if (targetPath && fs.existsSync(targetPath)) {
68
- try {
69
- baseLines = countLines(fs.readFileSync(targetPath, 'utf8'));
70
- } catch { return null; }
71
- } else if (!targetPath) {
72
- return null;
73
- }
74
- // Count added and removed lines
75
- let added = 0;
76
- let removed = 0;
77
- for (const line of patch.split('\n')) {
78
- if (line.startsWith('+') && !line.startsWith('+++')) added++;
79
- else if (line.startsWith('-') && !line.startsWith('---')) removed++;
80
- }
81
- return baseLines + added - removed;
82
- }
83
-
84
- // ---------------------------------------------------------------------------
85
- // Main
86
- // ---------------------------------------------------------------------------
87
-
88
- let raw = '';
89
- process.stdin.setEncoding('utf8');
90
- process.stdin.on('data', chunk => { raw += chunk; });
91
- process.stdin.on('end', () => {
92
- let payload;
93
- try { payload = JSON.parse(raw); } catch { process.exit(0); }
94
-
95
- const input = payload.toolInput || payload.tool_input || {};
96
- const patch = String(input.patch || '');
97
- if (!patch) process.exit(0);
98
-
99
- // --- Secrets check on added lines ---
100
- const added = extractAddedLines(patch);
101
- const addedText = added.join('\n');
102
- const hit = scanForSecrets(addedText);
103
- if (hit) deny(`Blocked apply_patch — hardcoded credential detected (${hit.label}). Remove before patching.`);
104
-
105
- // --- File length check ---
106
- const targetPath = extractTargetPath(patch);
107
- const estimated = estimateResultLines(patch, targetPath);
108
- if (estimated !== null && estimated > maxLines) {
109
- deny(`Blocked apply_patch — resulting file would be ~${estimated} lines (limit ${maxLines}). Break it up first.`);
110
- }
111
-
112
- process.exit(0);
113
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * warden-apply-patch-hook.js — Codex PreToolUse hook
4
+ *
5
+ * Fires on apply_patch tool calls. Scans added lines for hardcoded credentials
6
+ * and estimates resulting file size where the target path can be extracted.
7
+ *
8
+ * Codex hook payload (stdin, JSON):
9
+ * { tool: "apply_patch", toolInput: { patch: "<patch text>" } }
10
+ *
11
+ * Exit codes:
12
+ * 0 — proceed
13
+ * 2 — block (writes JSON deny to stdout)
14
+ */
15
+
16
+ 'use strict';
17
+
18
+ const fs = require('fs');
19
+ const path = require('path');
20
+ const { scanForSecrets } = require('../../lib/secret-patterns');
21
+ const { countLines } = require('../../lib/line-count');
22
+ const { loadConfig } = require('../../lib/config');
23
+
24
+ const { maxFileLength: maxLines } = loadConfig();
25
+
26
+
27
+ function deny(reason) {
28
+ process.stdout.write(JSON.stringify({ deny: true, message: `[CodeWarden] ${reason}` }) + '\n');
29
+ process.exit(2);
30
+ }
31
+
32
+ // ---------------------------------------------------------------------------
33
+ // Patch parsing helpers
34
+ // ---------------------------------------------------------------------------
35
+
36
+ /**
37
+ * Extract lines added by the patch (lines starting with '+', not '+++').
38
+ */
39
+ function extractAddedLines(patch) {
40
+ return patch.split('\n')
41
+ .filter(l => l.startsWith('+') && !l.startsWith('+++'))
42
+ .map(l => l.slice(1));
43
+ }
44
+
45
+ /**
46
+ * Extract target file path from patch header (*** path or +++ path or --- path).
47
+ * Returns null if not detectable.
48
+ */
49
+ function extractTargetPath(patch) {
50
+ for (const line of patch.split('\n')) {
51
+ const m = line.match(/^\+{3}\s+(.+?)(\s+\d{4}-\d{2}-\d{2}.*)?$/) ||
52
+ line.match(/^\*{3}\s+(.+?)(\s+\d{4}-\d{2}-\d{2}.*)?$/);
53
+ if (m) {
54
+ const p = m[1].trim();
55
+ if (p !== '/dev/null') return p;
56
+ }
57
+ }
58
+ return null;
59
+ }
60
+
61
+ /**
62
+ * Estimate resulting line count: base file lines + added lines - removed lines.
63
+ * Returns null if file not readable.
64
+ */
65
+ function estimateResultLines(patch, targetPath) {
66
+ let baseLines = 0;
67
+ if (targetPath && fs.existsSync(targetPath)) {
68
+ try {
69
+ baseLines = countLines(fs.readFileSync(targetPath, 'utf8'));
70
+ } catch { return null; }
71
+ } else if (!targetPath) {
72
+ return null;
73
+ }
74
+ // Count added and removed lines
75
+ let added = 0;
76
+ let removed = 0;
77
+ for (const line of patch.split('\n')) {
78
+ if (line.startsWith('+') && !line.startsWith('+++')) added++;
79
+ else if (line.startsWith('-') && !line.startsWith('---')) removed++;
80
+ }
81
+ return baseLines + added - removed;
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Main
86
+ // ---------------------------------------------------------------------------
87
+
88
+ let raw = '';
89
+ process.stdin.setEncoding('utf8');
90
+ process.stdin.on('data', chunk => { raw += chunk; });
91
+ process.stdin.on('end', () => {
92
+ let payload;
93
+ try { payload = JSON.parse(raw); } catch { process.exit(0); }
94
+
95
+ const input = payload.toolInput || payload.tool_input || {};
96
+ const patch = String(input.patch || '');
97
+ if (!patch) process.exit(0);
98
+
99
+ // --- Secrets check on added lines ---
100
+ const added = extractAddedLines(patch);
101
+ const addedText = added.join('\n');
102
+ const hit = scanForSecrets(addedText);
103
+ if (hit) deny(`Blocked apply_patch — hardcoded credential detected (${hit.label}). Remove before patching.`);
104
+
105
+ // --- File length check ---
106
+ const targetPath = extractTargetPath(patch);
107
+ const estimated = estimateResultLines(patch, targetPath);
108
+ if (estimated !== null && estimated > maxLines) {
109
+ deny(`Blocked apply_patch — resulting file would be ~${estimated} lines (limit ${maxLines}). Break it up first.`);
110
+ }
111
+
112
+ process.exit(0);
113
+ });
@@ -1,51 +1,51 @@
1
- #!/usr/bin/env node
2
- /**
3
- * warden-bash-hook.js — Codex PreToolUse hook
4
- *
5
- * Fires on Bash tool calls. Scans the command string for patterns that
6
- * would embed hardcoded credentials into files (e.g. echo/printf/cat with
7
- * secret values, curl with Authorization headers, env assignments, etc.).
8
- *
9
- * This is a best-effort surface: Bash is intentionally wide. The hook catches
10
- * the most common accidental secret exposure patterns; it does not attempt to
11
- * sandbox arbitrary shell execution.
12
- *
13
- * Codex hook payload (stdin, JSON):
14
- * { tool: "Bash", toolInput: { command: "<shell command>" } }
15
- *
16
- * Exit codes:
17
- * 0 — proceed
18
- * 2 — block (writes JSON deny to stdout)
19
- */
20
-
21
- 'use strict';
22
-
23
- const { scanForSecrets } = require('../../lib/secret-patterns');
24
-
25
- function deny(reason) {
26
- process.stdout.write(JSON.stringify({ deny: true, message: `[CodeWarden] ${reason}` }) + '\n');
27
- process.exit(2);
28
- }
29
-
30
- // ---------------------------------------------------------------------------
31
- // Main
32
- // ---------------------------------------------------------------------------
33
-
34
- let raw = '';
35
- process.stdin.setEncoding('utf8');
36
- process.stdin.on('data', chunk => { raw += chunk; });
37
- process.stdin.on('end', () => {
38
- let payload;
39
- try { payload = JSON.parse(raw); } catch { process.exit(0); }
40
-
41
- const input = payload.toolInput || payload.tool_input || {};
42
- const command = String(input.command || '');
43
- if (!command) process.exit(0);
44
-
45
- const hit = scanForSecrets(command);
46
- if (hit) {
47
- deny(`Blocked Bash command — hardcoded credential detected (${hit.label}). Use environment variables or a secrets manager instead.`);
48
- }
49
-
50
- process.exit(0);
51
- });
1
+ #!/usr/bin/env node
2
+ /**
3
+ * warden-bash-hook.js — Codex PreToolUse hook
4
+ *
5
+ * Fires on Bash tool calls. Scans the command string for patterns that
6
+ * would embed hardcoded credentials into files (e.g. echo/printf/cat with
7
+ * secret values, curl with Authorization headers, env assignments, etc.).
8
+ *
9
+ * This is a best-effort surface: Bash is intentionally wide. The hook catches
10
+ * the most common accidental secret exposure patterns; it does not attempt to
11
+ * sandbox arbitrary shell execution.
12
+ *
13
+ * Codex hook payload (stdin, JSON):
14
+ * { tool: "Bash", toolInput: { command: "<shell command>" } }
15
+ *
16
+ * Exit codes:
17
+ * 0 — proceed
18
+ * 2 — block (writes JSON deny to stdout)
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ const { scanForSecrets } = require('../../lib/secret-patterns');
24
+
25
+ function deny(reason) {
26
+ process.stdout.write(JSON.stringify({ deny: true, message: `[CodeWarden] ${reason}` }) + '\n');
27
+ process.exit(2);
28
+ }
29
+
30
+ // ---------------------------------------------------------------------------
31
+ // Main
32
+ // ---------------------------------------------------------------------------
33
+
34
+ let raw = '';
35
+ process.stdin.setEncoding('utf8');
36
+ process.stdin.on('data', chunk => { raw += chunk; });
37
+ process.stdin.on('end', () => {
38
+ let payload;
39
+ try { payload = JSON.parse(raw); } catch { process.exit(0); }
40
+
41
+ const input = payload.toolInput || payload.tool_input || {};
42
+ const command = String(input.command || '');
43
+ if (!command) process.exit(0);
44
+
45
+ const hit = scanForSecrets(command);
46
+ if (hit) {
47
+ deny(`Blocked Bash command — hardcoded credential detected (${hit.label}). Use environment variables or a secrets manager instead.`);
48
+ }
49
+
50
+ process.exit(0);
51
+ });
@@ -1,49 +1,49 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * config.js
6
- * Shared codewarden.json loader.
7
- *
8
- * Previously warden-lint.js, warden-lint-hook.js, and warden-apply-patch-hook.js
9
- * each parsed codewarden.json independently with slightly different fallback paths.
10
- * This module centralises config loading with a single documented precedence.
11
- *
12
- * Resolution order for config file:
13
- * 1. Explicit path passed to loadConfig(configPath)
14
- * 2. <__dirname>/../../codewarden.json (relative to tools/lib/ → skill root)
15
- */
16
-
17
- const fs = require('fs');
18
- const path = require('path');
19
-
20
- const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'codewarden.json');
21
-
22
- /**
23
- * Load and parse codewarden.json, returning a merged config object.
24
- * Falls back to defaults silently if the file is missing or unparseable.
25
- *
26
- * @param {string} [configPath] - Override the config file location
27
- * @returns {{ maxFileLength: number }}
28
- */
29
- function loadConfig(configPath) {
30
- const target = configPath || DEFAULT_CONFIG_PATH;
31
- let maxFileLength = 400;
32
-
33
- try {
34
- const raw = fs.readFileSync(target, 'utf8');
35
- const cfg = JSON.parse(raw);
36
- const configured =
37
- cfg?.thresholds?.max_file_length ??
38
- cfg?.max_file_length;
39
- if (typeof configured === 'number' && configured > 0) {
40
- maxFileLength = configured;
41
- }
42
- } catch {
43
- // Missing or invalid config — use defaults
44
- }
45
-
46
- return { maxFileLength };
47
- }
48
-
49
- module.exports = { loadConfig, DEFAULT_CONFIG_PATH };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * config.js
6
+ * Shared codewarden.json loader.
7
+ *
8
+ * Previously warden-lint.js, warden-lint-hook.js, and warden-apply-patch-hook.js
9
+ * each parsed codewarden.json independently with slightly different fallback paths.
10
+ * This module centralises config loading with a single documented precedence.
11
+ *
12
+ * Resolution order for config file:
13
+ * 1. Explicit path passed to loadConfig(configPath)
14
+ * 2. <__dirname>/../../codewarden.json (relative to tools/lib/ → skill root)
15
+ */
16
+
17
+ const fs = require('fs');
18
+ const path = require('path');
19
+
20
+ const DEFAULT_CONFIG_PATH = path.join(__dirname, '..', '..', 'codewarden.json');
21
+
22
+ /**
23
+ * Load and parse codewarden.json, returning a merged config object.
24
+ * Falls back to defaults silently if the file is missing or unparseable.
25
+ *
26
+ * @param {string} [configPath] - Override the config file location
27
+ * @returns {{ maxFileLength: number }}
28
+ */
29
+ function loadConfig(configPath) {
30
+ const target = configPath || DEFAULT_CONFIG_PATH;
31
+ let maxFileLength = 400;
32
+
33
+ try {
34
+ const raw = fs.readFileSync(target, 'utf8');
35
+ const cfg = JSON.parse(raw);
36
+ const configured =
37
+ cfg?.thresholds?.max_file_length ??
38
+ cfg?.max_file_length;
39
+ if (typeof configured === 'number' && configured > 0) {
40
+ maxFileLength = configured;
41
+ }
42
+ } catch {
43
+ // Missing or invalid config — use defaults
44
+ }
45
+
46
+ return { maxFileLength };
47
+ }
48
+
49
+ module.exports = { loadConfig, DEFAULT_CONFIG_PATH };
@@ -30,10 +30,13 @@ const SKIP_EXTS = new Set([
30
30
  * @param {string[]} results - accumulator (mutated)
31
31
  */
32
32
  function collectFiles(dir, results) {
33
- for (const entry of fs.readdirSync(dir)) {
33
+ let entries;
34
+ try { entries = fs.readdirSync(dir); } catch { return; }
35
+ for (const entry of entries) {
34
36
  if (SKIP_DIRS.has(entry)) continue;
35
37
  const full = path.join(dir, entry);
36
- const stat = fs.statSync(full);
38
+ let stat;
39
+ try { stat = fs.statSync(full); } catch { continue; }
37
40
  if (stat.isDirectory()) {
38
41
  collectFiles(full, results);
39
42
  } else if (!SKIP_EXTS.has(path.extname(entry).toLowerCase())) {
@@ -1,28 +1,28 @@
1
- #!/usr/bin/env node
2
- 'use strict';
3
-
4
- /**
5
- * line-count.js
6
- * Shared line-counting helper for warden-lint and hook consumers.
7
- *
8
- * Problem with the naive split('\n').length:
9
- * "a\nb\n".split('\n') === ['a', 'b', ''] → length 3, not 2
10
- * A trailing newline (standard in well-formed files) would push a file
11
- * that is exactly at the limit over it, producing false positives.
12
- *
13
- * This helper normalises CRLF, strips the trailing newline before
14
- * splitting, and treats an empty string as 0 lines.
15
- */
16
-
17
- /**
18
- * Count logical lines in a file's content string.
19
- *
20
- * @param {string} content - Raw file content (may include CRLF or trailing newline)
21
- * @returns {number} Line count (0 for empty content)
22
- */
23
- function countLines(content) {
24
- if (typeof content !== 'string' || content.length === 0) return 0;
25
- return content.replace(/\r\n/g, '\n').trimEnd().split('\n').length;
26
- }
27
-
28
- module.exports = { countLines };
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ /**
5
+ * line-count.js
6
+ * Shared line-counting helper for warden-lint and hook consumers.
7
+ *
8
+ * Problem with the naive split('\n').length:
9
+ * "a\nb\n".split('\n') === ['a', 'b', ''] → length 3, not 2
10
+ * A trailing newline (standard in well-formed files) would push a file
11
+ * that is exactly at the limit over it, producing false positives.
12
+ *
13
+ * This helper normalises CRLF, strips the trailing newline before
14
+ * splitting, and treats an empty string as 0 lines.
15
+ */
16
+
17
+ /**
18
+ * Count logical lines in a file's content string.
19
+ *
20
+ * @param {string} content - Raw file content (may include CRLF or trailing newline)
21
+ * @returns {number} Line count (0 for empty content)
22
+ */
23
+ function countLines(content) {
24
+ if (typeof content !== 'string' || content.length === 0) return 0;
25
+ return content.replace(/\r\n/g, '\n').trimEnd().split('\n').length;
26
+ }
27
+
28
+ module.exports = { countLines };
@@ -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 };