code-warden 3.1.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.
- package/CONFIGURE.md +39 -0
- package/DECISIONS.md +107 -0
- package/README.md +137 -0
- package/SKILL.md +169 -0
- package/codewarden.json +14 -0
- package/examples/governed-session.md +132 -0
- package/install.js +399 -0
- package/install.ps1 +32 -0
- package/install.sh +33 -0
- package/package.json +19 -0
- package/references/anti-drift.md +55 -0
- package/references/architecture.md +26 -0
- package/references/cleanup.md +30 -0
- package/references/cognition.md +36 -0
- package/references/operations.md +45 -0
- package/references/planning-gates.md +83 -0
- package/references/research-and-fit.md +51 -0
- package/references/safety.md +31 -0
- package/templates/ci/github-actions.yml +66 -0
- package/tools/auto-detect.js +91 -0
- package/tools/auto-targets.js +104 -0
- package/tools/auto-windsurf-adapter.js +75 -0
- package/tools/get-context.js +50 -0
- package/tools/hooks/claude/install-hooks.js +112 -0
- package/tools/hooks/claude/uninstall-hooks.js +75 -0
- package/tools/hooks/claude/warden-lint-hook.js +106 -0
- package/tools/hooks/claude/warden-secrets-hook.js +73 -0
- package/tools/hooks/codex/install-hooks.js +100 -0
- package/tools/hooks/codex/uninstall-hooks.js +53 -0
- package/tools/hooks/codex/warden-apply-patch-hook.js +113 -0
- package/tools/hooks/codex/warden-bash-hook.js +51 -0
- package/tools/lib/config.js +49 -0
- package/tools/lib/file-collection.js +72 -0
- package/tools/lib/line-count.js +28 -0
- package/tools/lib/secret-patterns.js +57 -0
- package/tools/tests/fixtures/clean.js +9 -0
- package/tools/tests/run-tests.js +210 -0
- package/tools/verify-secrets.js +26 -0
- package/tools/warden-lint.js +27 -0
|
@@ -0,0 +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 };
|
|
@@ -0,0 +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 };
|
|
@@ -0,0 +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
|
+
});
|
|
@@ -0,0 +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
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
const fs = require('fs');
|
|
5
|
+
const { countLines } = require('./lib/line-count');
|
|
6
|
+
const { expandPaths } = require('./lib/file-collection');
|
|
7
|
+
const { loadConfig } = require('./lib/config');
|
|
8
|
+
|
|
9
|
+
const { maxFileLength } = loadConfig();
|
|
10
|
+
const filePaths = expandPaths(process.argv.slice(2), 'warden-lint.js');
|
|
11
|
+
let hasErrors = false;
|
|
12
|
+
|
|
13
|
+
for (const filePath of filePaths) {
|
|
14
|
+
const content = fs.readFileSync(filePath, 'utf8');
|
|
15
|
+
const lines = countLines(content);
|
|
16
|
+
|
|
17
|
+
if (lines > maxFileLength) {
|
|
18
|
+
console.error(`[FAIL] [CodeWarden] File ${filePath} exceeds the maximum length of ${maxFileLength} lines (${lines} lines). Please refactor into smaller modules.`);
|
|
19
|
+
hasErrors = true;
|
|
20
|
+
} else {
|
|
21
|
+
console.log(`[PASS] [CodeWarden] ${filePath} (${lines} lines) complies with the length restriction.`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (hasErrors) {
|
|
26
|
+
process.exit(1);
|
|
27
|
+
}
|