@haposoft/cafekit 0.15.3 → 0.16.0-rc.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/README.md +4 -2
- package/bin/__tests__/codex-hooks.test.js +140 -0
- package/bin/__tests__/codex-native.test.js +2 -2
- package/bin/__tests__/develop-contract.test.js +1120 -0
- package/bin/__tests__/installer-safety.test.js +510 -2
- package/bin/__tests__/package-inventory.test.js +258 -0
- package/bin/__tests__/secret-scanner.test.js +157 -0
- package/bin/__tests__/validator-grounding.test.js +713 -0
- package/bin/install.js +5 -1
- package/bin/lib/codex-install.js +21 -6
- package/bin/lib/context.js +17 -6
- package/bin/lib/copy-utils.js +31 -0
- package/bin/lib/instruction-blocks.js +85 -0
- package/bin/lib/managed-writer.js +4 -1
- package/bin/lib/opencode-install.js +75 -30
- package/bin/phases/claude-runtime.js +103 -4
- package/bin/phases/codex-runtime.js +9 -5
- package/bin/phases/post-install.js +29 -22
- package/bin/phases/root-config.js +89 -10
- package/package.json +14 -4
- package/src/claude/CLAUDE.md +6 -122
- package/src/claude/agents/code-auditor.md +21 -22
- package/src/claude/agents/debugger.md +3 -3
- package/src/claude/agents/deployer.md +1 -1
- package/src/claude/agents/{god-developer.md → implementer.md} +2 -2
- package/src/claude/agents/project-manager.md +1 -1
- package/src/claude/agents/spec-maker.md +2 -2
- package/src/claude/hooks/__tests__/spec-gate.test.js +356 -5
- package/src/claude/hooks/agent.cjs +7 -14
- package/src/claude/hooks/rules.cjs +33 -53
- package/src/claude/hooks/spec-gate.cjs +66 -21
- package/src/claude/hooks/spec-state.cjs +5 -0
- package/src/claude/migration-manifest.json +9 -2
- package/src/claude/runtime.json +0 -3
- package/src/claude/scripts/generate-skill-catalog.cjs +1 -1
- package/src/claude/scripts/scan-staged-secrets.cjs +170 -0
- package/src/claude/scripts/spec-ground.cjs +189 -83
- package/src/claude/scripts/validate-spec-output.cjs +294 -30
- package/src/claude/scripts/workflow-policy.cjs +609 -0
- package/src/claude/skills/ai-multimodal/SKILL.md +2 -2
- package/src/claude/skills/ai-multimodal/references/audio-processing.md +6 -6
- package/src/claude/skills/ai-multimodal/references/video-analysis.md +1 -1
- package/src/claude/skills/backend-development/SKILL.md +4 -4
- package/src/claude/skills/backend-development/references/backend-architecture.md +1 -1
- package/src/claude/skills/backend-development/references/backend-devops.md +2 -2
- package/src/claude/skills/backend-development/references/backend-mindset.md +3 -3
- package/src/claude/skills/backend-development/references/backend-performance.md +5 -5
- package/src/claude/skills/backend-development/references/backend-security.md +3 -3
- package/src/claude/skills/backend-development/references/backend-technologies.md +5 -5
- package/src/claude/skills/backend-development/references/backend-testing.md +2 -2
- package/src/claude/skills/brainstorm/SKILL.md +2 -3
- package/src/claude/skills/code-review/SKILL.md +12 -8
- package/src/claude/skills/code-review/references/verification-gate.md +4 -4
- package/src/claude/skills/debug/SKILL.md +2 -0
- package/src/claude/skills/develop/SKILL.md +100 -52
- package/src/claude/skills/develop/references/parallel-waves.md +105 -26
- package/src/claude/skills/develop/references/quality-gate.md +58 -38
- package/src/claude/skills/develop/references/subagent-patterns.md +2 -2
- package/src/claude/skills/git/SKILL.md +44 -50
- package/src/claude/skills/hotfix/SKILL.md +10 -8
- package/src/claude/skills/hotfix/references/parallel-patterns.md +2 -2
- package/src/claude/skills/hotfix/references/review-cycle.md +26 -21
- package/src/claude/skills/inspect/SKILL.md +12 -30
- package/src/claude/skills/mobile-development/SKILL.md +12 -12
- package/src/claude/skills/mobile-development/references/mobile-android.md +7 -7
- package/src/claude/skills/mobile-development/references/mobile-best-practices.md +22 -23
- package/src/claude/skills/mobile-development/references/mobile-frameworks.md +13 -13
- package/src/claude/skills/mobile-development/references/mobile-ios.md +1 -1
- package/src/claude/skills/mobile-development/references/mobile-mindset.md +17 -19
- package/src/claude/skills/question/SKILL.md +13 -13
- package/src/claude/skills/research/SKILL.md +2 -2
- package/src/claude/skills/specs/SKILL.md +32 -15
- package/src/claude/skills/specs/references/review.md +3 -3
- package/src/claude/skills/specs/rules/tasks-generation.md +1 -1
- package/src/claude/skills/specs/templates/spec-state.json +7 -3
- package/src/claude/skills/sync/SKILL.md +26 -1
- package/src/claude/skills/sync/references/sync-protocols.md +3 -3
- package/src/claude/skills/test/SKILL.md +12 -4
- package/src/claude/skills/test/references/failure-triage.md +17 -17
- package/src/claude/skills/test/references/test-memory.md +2 -2
- package/src/codex/AGENTS.md +16 -24
- package/src/codex/hooks/lib/spec-receipt.cjs +57 -4
- package/src/codex/hooks/rules.cjs +27 -26
- package/src/codex/hooks/spec-gate.cjs +19 -8
- package/src/codex/hooks/spec-state.cjs +10 -1
- package/src/common/AGENTS.md +43 -0
- package/src/opencode/AGENTS.md +21 -121
- package/src/opencode/plugins/rules.ts +26 -34
- package/src/opencode/plugins/spec-gate.ts +283 -0
- package/src/claude/skills/inspect/references/external-gemini-inspection.md +0 -169
|
@@ -9,13 +9,15 @@
|
|
|
9
9
|
* Block contract (exit 0 + stdout): {"decision":"block","reason":"..."}.
|
|
10
10
|
*
|
|
11
11
|
* Safety: stop_hook_active loop guard; runtime.spec.completion_gate=false
|
|
12
|
-
* escape hatch (missing key → ON);
|
|
13
|
-
* crash → fail-open + hooks/.logs/hook-log.jsonl. Exit: 0 always.
|
|
12
|
+
* escape hatch (missing key → ON); cache is optimization only - every
|
|
13
|
+
* done transition is validated even on first run / empty cache; crash → fail-open + hooks/.logs/hook-log.jsonl. Exit: 0 always.
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
16
|
try {
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
|
+
const policyPath = path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs');
|
|
20
|
+
const POLICY = require(policyPath);
|
|
19
21
|
|
|
20
22
|
const stdin = fs.readFileSync(0, 'utf8').trim();
|
|
21
23
|
if (!stdin) process.exit(0);
|
|
@@ -69,20 +71,24 @@ try {
|
|
|
69
71
|
currentStatuses[tp] = task?.status || 'pending';
|
|
70
72
|
}
|
|
71
73
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
74
|
+
const staleFlashTasks = Object.entries(taskRegistry)
|
|
75
|
+
.filter(([, task]) => POLICY.isStaleFlashDone(task))
|
|
76
|
+
.map(([taskPath]) => taskPath);
|
|
77
|
+
if (staleFlashTasks.length > 0) {
|
|
78
|
+
process.stdout.write(JSON.stringify({
|
|
79
|
+
decision: 'block',
|
|
80
|
+
reason: `Completion gate: ${staleFlashTasks.length} task(s) marked done with FLASH_UNVERIFIED (${staleFlashTasks.join(', ')}). Run /hapo:test for exact proof, then use explicit sync-finalize.`
|
|
81
|
+
}) + '\n');
|
|
79
82
|
process.exit(0);
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
|
|
83
|
-
//
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
// Cache hardening: every Stop revalidates the canonical receipt for
|
|
86
|
+
// every task currently marked done, so a cached PASS cannot hide later
|
|
87
|
+
// mutations (deletion, placeholder, changed Verification/Command/Exit/
|
|
88
|
+
// Base/Head, stale provenance). Cache is an optimization, not truth.
|
|
89
|
+
const featureCache = cacheExists ? (cache[featureName] || {}) : {};
|
|
90
|
+
const allDoneTasks = Object.keys(taskRegistry).filter((tp) =>
|
|
91
|
+
(taskRegistry[tp]?.status || 'pending') === 'done'
|
|
86
92
|
);
|
|
87
93
|
// /m so ^ matches line starts (Evidence is never at byte 0 of the file).
|
|
88
94
|
// legacy heading aliases: read-compat only, no longer advertised
|
|
@@ -108,12 +114,35 @@ try {
|
|
|
108
114
|
return lines.slice(start, end).join('\n');
|
|
109
115
|
}
|
|
110
116
|
|
|
111
|
-
|
|
112
|
-
|
|
117
|
+
function validateCanonicalReceipt(body) {
|
|
118
|
+
const fails = [];
|
|
119
|
+
if (!/^\s*Verification:\s*PASS\s*$/m.test(body)) fails.push('verification_state');
|
|
120
|
+
if (!/^\s*Command(?:\(s\))?\s*:/m.test(body)) fails.push('command');
|
|
121
|
+
if (!/^\s*Exit\s*:|exit\s+code\s*[:=]|\bResult\s*:\s*PASS\b/im.test(body)) fails.push('exit_result');
|
|
122
|
+
const hasBase = /^\s*Base[ \t]*:[ \t]*\S/im.test(body);
|
|
123
|
+
const hasHead = /^\s*Head[ \t]*:[ \t]*\S/im.test(body);
|
|
124
|
+
const hasBaseSha = /\bbase_sha[ \t]*:[ \t]*\S/im.test(body);
|
|
125
|
+
const hasHeadSha = /\bhead_sha[ \t]*:[ \t]*\S/im.test(body);
|
|
126
|
+
if (!((hasBase && hasHead) || (hasBaseSha && hasHeadSha))) fails.push('provenance');
|
|
127
|
+
if (/\bartifact\b/i.test(body) && !/sha256:/i.test(body)) fails.push('artifact_hash');
|
|
128
|
+
return fails;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Receipt checks a–h → failed letter list.
|
|
132
|
+
// a: file + Status header has "done"; b: Evidence heading; c: no {{...}} + unambiguous PASS; d: completed_at; e: command; f: exit/result; g: provenance; h: artifact hash
|
|
133
|
+
function safeTaskFile(featureDir, taskPath) {
|
|
134
|
+
const target = path.resolve(featureDir, taskPath);
|
|
135
|
+
const relative = path.relative(featureDir, target);
|
|
136
|
+
// sibling-prefix safe: must not be .., ../..., or absolute (cross-platform)
|
|
137
|
+
if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) return null;
|
|
138
|
+
return target;
|
|
139
|
+
}
|
|
140
|
+
|
|
113
141
|
function checkReceipt(taskPath) {
|
|
114
142
|
const fails = [];
|
|
115
|
-
const
|
|
116
|
-
|
|
143
|
+
const featureDir = path.join(specsPath, featureName);
|
|
144
|
+
const abs = safeTaskFile(featureDir, taskPath);
|
|
145
|
+
if (!abs || !fs.existsSync(abs)) return ['a'];
|
|
117
146
|
let text = '';
|
|
118
147
|
try { text = fs.readFileSync(abs, 'utf8'); } catch { return ['a']; }
|
|
119
148
|
|
|
@@ -129,6 +158,20 @@ try {
|
|
|
129
158
|
!(PASS_MARKER_RE.test(body) || LEGACY_SUCCESS_RE.test(body))
|
|
130
159
|
) {
|
|
131
160
|
fails.push('c');
|
|
161
|
+
} else {
|
|
162
|
+
// Canonical receipt checks for done tasks
|
|
163
|
+
const canonicalFails = validateCanonicalReceipt(body);
|
|
164
|
+
const map = { verification_state: 'c', command: 'e', exit_result: 'f', provenance: 'g', artifact_hash: 'h' };
|
|
165
|
+
for (const cf of canonicalFails) {
|
|
166
|
+
if (cf === 'verification_state') {
|
|
167
|
+
if (!PASS_MARKER_RE.test(body) && !fails.includes('c')) fails.push('c');
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const letter = map[cf];
|
|
171
|
+
if (letter && !fails.includes(letter)) fails.push(letter);
|
|
172
|
+
}
|
|
173
|
+
// Enforce strict Verification: PASS, not just legacy success
|
|
174
|
+
if (!PASS_MARKER_RE.test(body) && !fails.includes('c')) fails.push('c');
|
|
132
175
|
}
|
|
133
176
|
|
|
134
177
|
const at = taskRegistry[taskPath]?.completed_at;
|
|
@@ -140,12 +183,14 @@ try {
|
|
|
140
183
|
return fails;
|
|
141
184
|
}
|
|
142
185
|
|
|
143
|
-
const failures =
|
|
186
|
+
const failures = allDoneTasks
|
|
144
187
|
.map((tp) => ({ taskPath: tp, fails: checkReceipt(tp) }))
|
|
145
188
|
.filter((f) => f.fails.length > 0);
|
|
146
189
|
|
|
147
|
-
// Always persist status transitions, including done → pending.
|
|
148
|
-
//
|
|
190
|
+
// Always persist status transitions, including done → pending. Keep
|
|
191
|
+
// failing done tasks at their previous cached status so a stale valid
|
|
192
|
+
// entry cannot mask a mutated receipt on the next Stop; with
|
|
193
|
+
// revalidation of every done task the gate still re-fires regardless.
|
|
149
194
|
try {
|
|
150
195
|
const nextFeature = { ...featureCache, ...currentStatuses };
|
|
151
196
|
for (const { taskPath } of failures) {
|
|
@@ -157,7 +202,7 @@ try {
|
|
|
157
202
|
fs.writeFileSync(cacheFile, JSON.stringify(cache));
|
|
158
203
|
} catch { /* fail-open */ }
|
|
159
204
|
|
|
160
|
-
if (
|
|
205
|
+
if (allDoneTasks.length === 0) process.exit(0);
|
|
161
206
|
if (failures.length === 0) process.exit(0);
|
|
162
207
|
|
|
163
208
|
const lines = [
|
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
try {
|
|
15
15
|
const fs = require('fs');
|
|
16
16
|
const path = require('path');
|
|
17
|
+
const POLICY = require(path.join(__dirname, '..', 'scripts', 'workflow-policy.cjs'));
|
|
17
18
|
|
|
18
19
|
// ── Main ──────────────────────────────────────────────────────────────────
|
|
19
20
|
|
|
@@ -68,6 +69,7 @@ try {
|
|
|
68
69
|
|
|
69
70
|
const phase = activeSpec.current_phase || activeSpec.phase || 'unknown';
|
|
70
71
|
const taskRegistry = activeSpec.task_registry || {};
|
|
72
|
+
const flashTasks = POLICY.flashState(taskRegistry);
|
|
71
73
|
const taskEntries = Object.entries(taskRegistry);
|
|
72
74
|
const taskCounts = taskEntries.reduce((acc, [, task]) => {
|
|
73
75
|
const status = task?.status || 'pending';
|
|
@@ -113,6 +115,9 @@ try {
|
|
|
113
115
|
if (nextUnblocked) {
|
|
114
116
|
lines.push(`- Next unblocked: \`${nextUnblocked[0]}\``);
|
|
115
117
|
}
|
|
118
|
+
if (flashTasks.length > 0) {
|
|
119
|
+
lines.push(`- Flash verification pending: ${flashTasks.map((taskPath) => `\`${taskPath}\``).join(', ')}. PASS promotion keeps task in_progress until explicit sync-finalize.`);
|
|
120
|
+
}
|
|
116
121
|
lines.push(`- Sync \`spec.json\` + task file after verified work; run \`node .claude/scripts/validate-spec-output.cjs specs/${featureName}\` before \`ready_for_implementation=true\`.`);
|
|
117
122
|
lines.push(`- A completion gate verifies receipts when you end a turn with newly-done tasks.`);
|
|
118
123
|
lines.push('');
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
"required": [
|
|
42
42
|
"test-runner.md",
|
|
43
43
|
"code-auditor.md",
|
|
44
|
-
"
|
|
44
|
+
"implementer.md",
|
|
45
45
|
"debugger.md",
|
|
46
46
|
"docs-keeper.md",
|
|
47
47
|
"git-ops.md",
|
|
@@ -59,7 +59,9 @@
|
|
|
59
59
|
"validate-docs.cjs",
|
|
60
60
|
"validate-docs-reconstruct.cjs",
|
|
61
61
|
"generate-skill-catalog.cjs",
|
|
62
|
-
"validate-spec-output.cjs"
|
|
62
|
+
"validate-spec-output.cjs",
|
|
63
|
+
"scan-staged-secrets.cjs",
|
|
64
|
+
"workflow-policy.cjs"
|
|
63
65
|
]
|
|
64
66
|
},
|
|
65
67
|
"agentReferences": {
|
|
@@ -69,6 +71,11 @@
|
|
|
69
71
|
"assumed": []
|
|
70
72
|
},
|
|
71
73
|
"obsolete": {
|
|
74
|
+
"agents": {
|
|
75
|
+
"claude": ["agents/god-developer.md"],
|
|
76
|
+
"codex": ["agents/god_developer.toml"],
|
|
77
|
+
"opencode": ["agents/god-developer.md"]
|
|
78
|
+
},
|
|
72
79
|
"runtimeFiles": [
|
|
73
80
|
"hooks/skill-router.cjs",
|
|
74
81
|
"hooks/lib/skill-router-routes.cjs",
|
package/src/claude/runtime.json
CHANGED
|
@@ -38,10 +38,10 @@ function parseArgs(argv) {
|
|
|
38
38
|
|
|
39
39
|
function defaultSkillsRoot() {
|
|
40
40
|
const candidates = [
|
|
41
|
+
path.resolve(__dirname, '..', 'skills'),
|
|
41
42
|
path.resolve(process.cwd(), '.claude', 'skills'),
|
|
42
43
|
path.resolve(process.cwd(), '.agents', 'skills'),
|
|
43
44
|
path.resolve(process.cwd(), '.opencode', 'skills'),
|
|
44
|
-
path.resolve(__dirname, '..', 'skills'),
|
|
45
45
|
path.resolve(__dirname, '..', '..', '.agents', 'skills')
|
|
46
46
|
];
|
|
47
47
|
return candidates.find((candidate) => fs.existsSync(candidate)) || candidates[0];
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { spawnSync } = require('node:child_process');
|
|
4
|
+
|
|
5
|
+
const STRONG_SENSITIVE_NAMES = new Set([
|
|
6
|
+
'api_key', 'apikey', 'api-key', 'password', 'passwd', 'access_token',
|
|
7
|
+
'auth_token', 'bearer_token', 'client_secret', 'private_key',
|
|
8
|
+
'aws_secret_access_key', 'aws_access_key_id', 'github_token', 'jwt_secret',
|
|
9
|
+
'openai_api_key', 'anthropic_api_key', 'credential_url', 'credentials_url',
|
|
10
|
+
'database_url', 'connection_string', 'connection_url', 'dsn', 'basic_auth',
|
|
11
|
+
'basic-auth', 'auth_header', 'authorization'
|
|
12
|
+
]);
|
|
13
|
+
const GENERIC_SENSITIVE_NAMES = new Set(['secret', 'token', 'key', 'auth']);
|
|
14
|
+
const SAFE_SUFFIXES = /(?:_label|_hint|_path|_file)$/i;
|
|
15
|
+
const PLACEHOLDER = /^(?:$|(?:your|my|replace|change|set)[-_ ]?(?:value|secret|key|token|password)?$|(?:changeme|change-me|example|sample|dummy|placeholder|test|testing|fake|xxx+|<[^>]+>|\.{3,}|\*{3,}))/i;
|
|
16
|
+
const ENV_REFERENCE = /^(?:\$\{?[A-Z0-9_]+\}?|process\.env(?:\.|\[)|import\.meta\.env(?:\.|\[)|(?:env|os\.environ)\s*\()/i;
|
|
17
|
+
const FUNCTION_REFERENCE = /^[A-Za-z_$][\w$]*(?:\.[A-Za-z_$][\w$]*)*\s*\(/;
|
|
18
|
+
const VALUE_SIGNAL = /(?:^|[\s_-])(sk-[a-z0-9_-]{8,}|gh[pousr]_[a-z0-9_]{8,}|ey[a-z0-9_-]{20,}|[a-f0-9]{24,}|[a-z0-9+/]{20,}={0,2})(?:$|[^a-z0-9_])/i;
|
|
19
|
+
const PEM_SIGNAL = /-----BEGIN [A-Z0-9 ]*(?:PRIVATE KEY|CERTIFICATE)[A-Z0-9 ]*-----/i;
|
|
20
|
+
const CREDENTIAL_URL_SIGNAL = /\b(?:https?|postgres(?:ql)?|mysql|mongodb(?:\+srv)?):\/\/[^\s/@:]+(?::[^\s/@]*)?@[^\s/]+/i;
|
|
21
|
+
const PUBLIC_URL_SIGNAL = /^(?:https?|postgres(?:ql)?|mysql|mongodb(?:\+srv)?:)\/\//i;
|
|
22
|
+
const BASIC_AUTH_SIGNAL = /\bBasic\s+[A-Za-z0-9+/]{4,}={0,2}\b/i;
|
|
23
|
+
const SAFE_LITERAL = /^(?:true|false|null|undefined|enabled|disabled|none|safe)$/i;
|
|
24
|
+
const ASSIGNMENT = /(?:^|[\s,{\-])(["']?[A-Za-z_$][A-Za-z0-9_$-]*["']?)\s*(?::|=)\s*(?:"([^"]*)"|'([^']*)'|`([^`]*)`|([^\s#},]*))/g;
|
|
25
|
+
|
|
26
|
+
function normalizeIdentifier(name) {
|
|
27
|
+
return name.replace(/([a-z0-9])([A-Z])/g, '$1_$2').replace(/-/g, '_').toLowerCase();
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function sensitivityLevel(name) {
|
|
31
|
+
const normalized = normalizeIdentifier(name);
|
|
32
|
+
if (SAFE_SUFFIXES.test(normalized) || normalized === 'tokenizer') return null;
|
|
33
|
+
if (STRONG_SENSITIVE_NAMES.has(normalized)
|
|
34
|
+
|| /(?:^|_)(?:api_key|password|passwd|private_key|client_secret|access_token|auth_token|credential|credentials)(?:_|$)/i.test(normalized)) {
|
|
35
|
+
return 'strong';
|
|
36
|
+
}
|
|
37
|
+
if (GENERIC_SENSITIVE_NAMES.has(normalized)
|
|
38
|
+
|| /(?:^|_)(?:secret|token|key|auth)(?:_|$)/i.test(normalized)) {
|
|
39
|
+
return 'generic';
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function identifierIsSensitive(name) {
|
|
45
|
+
return sensitivityLevel(name) !== null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function stripValue(value) {
|
|
49
|
+
return String(value || '').trim().replace(/^(['"`])([\s\S]*)\1$/, '$2').replace(/[;,]$/, '');
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function valueLooksSecret(value, level = 'generic') {
|
|
53
|
+
const clean = stripValue(value);
|
|
54
|
+
if (!clean || PLACEHOLDER.test(clean) || ENV_REFERENCE.test(clean) || FUNCTION_REFERENCE.test(clean)) return false;
|
|
55
|
+
if (PUBLIC_URL_SIGNAL.test(clean) && !CREDENTIAL_URL_SIGNAL.test(clean)) return false;
|
|
56
|
+
if (PEM_SIGNAL.test(clean) || CREDENTIAL_URL_SIGNAL.test(clean) || BASIC_AUTH_SIGNAL.test(clean)) return true;
|
|
57
|
+
if (level === 'strong') return clean.length >= 4 && !SAFE_LITERAL.test(clean);
|
|
58
|
+
return VALUE_SIGNAL.test(clean) || (clean.length >= 20 && /[A-Za-z]/.test(clean) && /\d/.test(clean));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function parseAssignments(text) {
|
|
62
|
+
const assignments = [];
|
|
63
|
+
let match;
|
|
64
|
+
while ((match = ASSIGNMENT.exec(text)) !== null) {
|
|
65
|
+
const name = match[1].replace(/^["']|["']$/g, '');
|
|
66
|
+
const value = match[2] ?? match[3] ?? match[4] ?? match[5] ?? '';
|
|
67
|
+
assignments.push({ name, value, level: sensitivityLevel(name) });
|
|
68
|
+
}
|
|
69
|
+
return assignments;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function inspectAssignment(assignment, file, sourceLine, findings) {
|
|
73
|
+
const specializedValue = PEM_SIGNAL.test(assignment.value)
|
|
74
|
+
|| CREDENTIAL_URL_SIGNAL.test(assignment.value)
|
|
75
|
+
|| BASIC_AUTH_SIGNAL.test(assignment.value);
|
|
76
|
+
if ((assignment.level && valueLooksSecret(assignment.value, assignment.level)) || (!assignment.level && specializedValue)) {
|
|
77
|
+
findings.push({ file, sourceLine, name: assignment.name });
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function parseDiff(diff) {
|
|
82
|
+
const findings = [];
|
|
83
|
+
let file = null;
|
|
84
|
+
let sourceLine = 0;
|
|
85
|
+
let pending = null;
|
|
86
|
+
|
|
87
|
+
const flushPending = () => {
|
|
88
|
+
if (!pending) return;
|
|
89
|
+
if (valueLooksSecret(pending.value, pending.level)) {
|
|
90
|
+
findings.push({ file: pending.file, sourceLine: pending.sourceLine, name: pending.name });
|
|
91
|
+
}
|
|
92
|
+
pending = null;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
for (const line of diff.split(/\r?\n/)) {
|
|
96
|
+
if (line.startsWith('+++ b/')) {
|
|
97
|
+
flushPending();
|
|
98
|
+
file = line.slice(6);
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
const hunk = line.match(/^@@ -\d+(?:,\d+)? \+(\d+)(?:,(\d+))? @@/);
|
|
102
|
+
if (hunk) {
|
|
103
|
+
flushPending();
|
|
104
|
+
sourceLine = Number(hunk[1]);
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (!file || sourceLine < 1) continue;
|
|
108
|
+
|
|
109
|
+
if (line.startsWith('+') && !line.startsWith('+++')) {
|
|
110
|
+
const text = line.slice(1);
|
|
111
|
+
const assignments = parseAssignments(text);
|
|
112
|
+
const first = assignments[0];
|
|
113
|
+
const indented = /^\s+/.test(text);
|
|
114
|
+
if (pending && (!indented || first)) flushPending();
|
|
115
|
+
|
|
116
|
+
if (first) {
|
|
117
|
+
for (const assignment of assignments) inspectAssignment(assignment, file, sourceLine, findings);
|
|
118
|
+
if (first.level && (!first.value || /^(?:\||>|\{|\[)$/.test(first.value))) {
|
|
119
|
+
pending = {
|
|
120
|
+
file,
|
|
121
|
+
sourceLine,
|
|
122
|
+
name: first.name,
|
|
123
|
+
level: first.level,
|
|
124
|
+
value: ''
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
} else if (pending && indented) {
|
|
128
|
+
pending.value += `\n${text.trim()}`;
|
|
129
|
+
} else if (pending && (PEM_SIGNAL.test(text) || BASIC_AUTH_SIGNAL.test(text))) {
|
|
130
|
+
pending.value += `\n${text.trim()}`;
|
|
131
|
+
}
|
|
132
|
+
sourceLine++;
|
|
133
|
+
} else if (!line.startsWith('-') && !line.startsWith('\\')) {
|
|
134
|
+
flushPending();
|
|
135
|
+
sourceLine++;
|
|
136
|
+
} else if (line.startsWith('-')) {
|
|
137
|
+
flushPending();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
flushPending();
|
|
141
|
+
return findings;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function main() {
|
|
145
|
+
const result = spawnSync(
|
|
146
|
+
'git',
|
|
147
|
+
['diff', '--cached', '--unified=0', '--no-color', '--diff-filter=ACMR', '--'],
|
|
148
|
+
{ encoding: 'utf8' }
|
|
149
|
+
);
|
|
150
|
+
if (result.error || result.status !== 0) {
|
|
151
|
+
process.stderr.write(`Unable to read staged diff: ${result.error?.message || result.stderr || 'git diff failed'}\n`);
|
|
152
|
+
return 2;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const findings = parseDiff(result.stdout);
|
|
156
|
+
const unique = new Map(findings.map((finding) => [
|
|
157
|
+
`${finding.file}:${finding.sourceLine}:${finding.name}`,
|
|
158
|
+
finding
|
|
159
|
+
]));
|
|
160
|
+
for (const finding of unique.values()) {
|
|
161
|
+
process.stdout.write(
|
|
162
|
+
`Blocked: possible secret \`${finding.name}\` at ${finding.file}:${finding.sourceLine}. Value not shown.\n`
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
if (unique.size === 0) process.stdout.write('No staged secrets found.\n');
|
|
166
|
+
return unique.size > 0 ? 1 : 0;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (require.main === module) process.exit(main());
|
|
170
|
+
module.exports = { parseDiff, identifierIsSensitive, valueLooksSecret };
|