agentic-sdd-framework 1.4.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.
- package/.agents/AGENTS.template.md +59 -0
- package/.agents/CONTEXT.template.md +41 -0
- package/.agents/ENTRYPOINT.template.md +31 -0
- package/.agents/skills/ast-navigator/SKILL.md +31 -0
- package/.agents/skills/ast-navigator/adapters/ast-grep.md +18 -0
- package/.agents/skills/ast-navigator/adapters/graphify.md +19 -0
- package/.agents/skills/ast-navigator/adapters/lsp.md +16 -0
- package/.agents/skills/ast-navigator/adapters/ripgrep.md +19 -0
- package/.agents/skills/auditor-executor-protocol/SKILL.md +410 -0
- package/.agents/skills/auditor-executor-protocol/references/autonomous-mode.md +144 -0
- package/.agents/skills/auditor-executor-protocol/references/failure-modes-and-example.md +103 -0
- package/.agents/skills/auditor-executor-protocol/references/handoffs.md +133 -0
- package/.agents/skills/auditor-executor-protocol/references/tasks-and-gates.md +81 -0
- package/.agents/skills/no-ai-slop/LICENSE +21 -0
- package/.agents/skills/no-ai-slop/SKILL.md +52 -0
- package/.agents/skills/strategic-cto/SKILL.md +54 -0
- package/CHANGELOG.md +117 -0
- package/LICENSE +21 -0
- package/README.md +244 -0
- package/docs/SPEC_TEMPLATE.md +78 -0
- package/docs/decisions/ADR_TEMPLATE.md +49 -0
- package/docs/guidelines/AST_NAVIGATION.md +51 -0
- package/docs/guides/AGENT_CREDENTIALS.md +75 -0
- package/docs/guides/GITHUB_CLI_SETUP.md +74 -0
- package/docs/incidents/0000-00-00-incident-template.md +35 -0
- package/docs/roadmap/templates/compliance-log.md +37 -0
- package/docs/roadmap/templates/execution-guide.md +75 -0
- package/docs/roadmap/templates/plan-of-record.md +49 -0
- package/package.json +49 -0
- package/scripts/check-copy-slop.js +120 -0
- package/scripts/check-file-size.js +66 -0
- package/scripts/check-spec.js +201 -0
- package/scripts/check-system-prerequisites.js +133 -0
- package/scripts/check-versions.js +50 -0
- package/scripts/dev/fuzz-spec-markup.js +123 -0
- package/scripts/dev/set-npm-publish-token.sh +40 -0
- package/scripts/dev/sync-vendored.js +94 -0
- package/scripts/install-git-hooks.js +103 -0
- package/scripts/lib/cli.js +60 -0
- package/scripts/lib/config.js +111 -0
- package/scripts/lib/git.js +211 -0
- package/scripts/lib/markdown.js +46 -0
- package/scripts/lib/provision.js +323 -0
- package/scripts/lib/runner.js +70 -0
- package/scripts/lib/sdd.config.schema.json +213 -0
- package/scripts/lib/slop-patterns.js +57 -0
- package/scripts/lib/spec-markup.js +346 -0
- package/scripts/lib/spec.js +226 -0
- package/scripts/lib/state.js +107 -0
- package/scripts/lib/vendor/README.md +11 -0
- package/scripts/lib/vendor/markdown-it.LICENSE +22 -0
- package/scripts/lib/vendor/markdown-it.min.js +3 -0
- package/scripts/quality-gate.js +151 -0
- package/scripts/sdd-init.js +245 -0
- package/scripts/sdd-verify.js +176 -0
- package/scripts/verify-no-secrets.js +216 -0
- package/sdd.config.json +33 -0
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/sdd-init.js
|
|
5
|
+
*
|
|
6
|
+
* Day-0 Bootstrapping Wizard for the Agentic SDD Framework.
|
|
7
|
+
* Pure Node.js standard library implementation (zero npm dependencies).
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* node scripts/sdd-init.js Guided mode (interactive)
|
|
11
|
+
* node scripts/sdd-init.js --express [flags] Non-interactive mode
|
|
12
|
+
* npx github:tBeltty/agentic-sdd-framework Install into the current directory
|
|
13
|
+
*
|
|
14
|
+
* Flags (`--flag=value` or `--flag value`; unknown flags are an error):
|
|
15
|
+
* --target=<dir> Project to provision (default: current directory)
|
|
16
|
+
* --name=<name> Project name (default: target directory name)
|
|
17
|
+
* --runtime=<runtime> e.g. node-24-lts, go-1.23, python-3.12
|
|
18
|
+
* --mode=lite|rigor Specification depth (default: lite)
|
|
19
|
+
* --ast=<adapter> ast-grep | graphify | ripgrep | lsp (default: ast-grep)
|
|
20
|
+
* --concurrency=, --hardware=, --workload= Discovery answers
|
|
21
|
+
* --i18n, --pwa Enable the matching capability flags
|
|
22
|
+
* --force Refresh copied skills and templates in install mode
|
|
23
|
+
* --help Print this help
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
const fs = require('fs');
|
|
27
|
+
const path = require('path');
|
|
28
|
+
const readline = require('readline');
|
|
29
|
+
const { execFileSync, spawnSync } = require('child_process');
|
|
30
|
+
const { FRAMEWORK_ROOT, provision } = require('./lib/provision');
|
|
31
|
+
const { loadConfig, getIn } = require('./lib/config');
|
|
32
|
+
|
|
33
|
+
const VALUE_FLAGS = ['target', 'name', 'runtime', 'mode', 'ast', 'concurrency', 'hardware', 'workload'];
|
|
34
|
+
const BOOLEAN_FLAGS = ['express', 'i18n', 'pwa', 'force', 'help'];
|
|
35
|
+
const USAGE = `Usage: sdd-init [--express] [--target=<dir>] [--name=<name>] [--runtime=<id>]
|
|
36
|
+
[--mode=lite|rigor] [--ast=ast-grep|graphify|ripgrep|lsp]
|
|
37
|
+
[--concurrency=<text>] [--hardware=<text>] [--workload=<text>]
|
|
38
|
+
[--i18n] [--pwa] [--force] [--help]`;
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const values = new Map();
|
|
42
|
+
const flags = new Set();
|
|
43
|
+
for (let i = 0; i < argv.length; i++) {
|
|
44
|
+
const arg = argv[i];
|
|
45
|
+
const match = arg.match(/^--([a-z0-9-]+)(?:=(.*))?$/);
|
|
46
|
+
if (!match) throw new Error(`Unexpected argument "${arg}".`);
|
|
47
|
+
const [, name, inline] = match;
|
|
48
|
+
if (VALUE_FLAGS.includes(name)) {
|
|
49
|
+
const value = inline !== undefined ? inline : argv[++i];
|
|
50
|
+
if (value === undefined || (inline === undefined && value.startsWith('--'))) {
|
|
51
|
+
throw new Error(`--${name} needs a value.`);
|
|
52
|
+
}
|
|
53
|
+
values.set(name, value);
|
|
54
|
+
} else if (BOOLEAN_FLAGS.includes(name) && inline === undefined) {
|
|
55
|
+
flags.add(name);
|
|
56
|
+
} else {
|
|
57
|
+
throw new Error(`Unknown flag "${arg}".`);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return { values, flags };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let parsed;
|
|
64
|
+
try {
|
|
65
|
+
parsed = parseArgs(process.argv.slice(2));
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`❌ ${error.message}\n${USAGE}`);
|
|
68
|
+
process.exit(2);
|
|
69
|
+
}
|
|
70
|
+
if (parsed.flags.has('help')) {
|
|
71
|
+
console.log(USAGE);
|
|
72
|
+
process.exit(0);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function getArgValue(name, defaultValue) {
|
|
76
|
+
return parsed.values.has(name) ? parsed.values.get(name) : defaultValue;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const target = path.resolve(getArgValue('target', process.cwd()));
|
|
80
|
+
const force = parsed.flags.has('force');
|
|
81
|
+
const wantsExpress = parsed.flags.has('express') || ['mode', 'ast', 'name', 'runtime'].some(f => parsed.values.has(f));
|
|
82
|
+
|
|
83
|
+
// A previous run's sdd.config.json seeds the wizard's defaults, so re-running sdd-init
|
|
84
|
+
// (e.g. to pick up a new template version) doesn't reset answers the project already made.
|
|
85
|
+
// The framework's own repository is exempt: its config describes the framework, not a template.
|
|
86
|
+
function loadExistingDefaults() {
|
|
87
|
+
let config;
|
|
88
|
+
try {
|
|
89
|
+
config = loadConfig(target);
|
|
90
|
+
} catch {
|
|
91
|
+
return {};
|
|
92
|
+
}
|
|
93
|
+
if (getIn(config, 'project.type', 'application') === 'framework') return {};
|
|
94
|
+
return {
|
|
95
|
+
projectName: getIn(config, 'project.name'),
|
|
96
|
+
runtime: getIn(config, 'project.runtime'),
|
|
97
|
+
specMode: getIn(config, 'specification.mode'),
|
|
98
|
+
astAdapter: getIn(config, 'capabilities.astNavigation.adapter'),
|
|
99
|
+
concurrency: getIn(config, 'discovery.concurrency'),
|
|
100
|
+
hardware: getIn(config, 'discovery.hardware'),
|
|
101
|
+
workload: getIn(config, 'discovery.workload'),
|
|
102
|
+
i18n: getIn(config, 'capabilities.i18n.enabled'),
|
|
103
|
+
pwa: getIn(config, 'capabilities.pwa.enabled')
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const EXISTING = loadExistingDefaults();
|
|
108
|
+
|
|
109
|
+
const DEFAULTS = {
|
|
110
|
+
runtime: EXISTING.runtime || 'node-24-lts',
|
|
111
|
+
concurrency: EXISTING.concurrency || '1-10 internal',
|
|
112
|
+
hardware: EXISTING.hardware || '$5 VPS',
|
|
113
|
+
workload: EXISTING.workload || 'I/O-heavy API'
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
function printHeader() {
|
|
117
|
+
console.log('\n===============================================================');
|
|
118
|
+
console.log(' 🧙 Agentic SDD Framework: Day-0 Wizard');
|
|
119
|
+
console.log(' Spec-Driven Development with Autonomous AI Agents');
|
|
120
|
+
console.log('===============================================================\n');
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
async function runGuidedMode() {
|
|
124
|
+
printHeader();
|
|
125
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
126
|
+
const ask = (query, defaultVal) => new Promise(resolve => {
|
|
127
|
+
rl.question(defaultVal ? `${query} [${defaultVal}]: ` : `${query}: `, answer => {
|
|
128
|
+
resolve(answer.trim() || defaultVal);
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
const askYesNo = async (query, defaultYes) =>
|
|
132
|
+
(await ask(`${query} (${defaultYes ? 'Y/n' : 'y/N'})`, defaultYes ? 'y' : 'n')).toLowerCase().startsWith('y');
|
|
133
|
+
|
|
134
|
+
const exists = fs.existsSync(target);
|
|
135
|
+
const installMode = !exists || fs.realpathSync(target) !== fs.realpathSync(FRAMEWORK_ROOT);
|
|
136
|
+
const question = exists ? `Install SDD governance into ${target}?` : `${target} does not exist. Create it and install SDD governance?`;
|
|
137
|
+
if (installMode && !(await askYesNo(question, true))) {
|
|
138
|
+
rl.close();
|
|
139
|
+
console.log('Aborted. Pass --target=<dir> to choose another project.');
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
fs.mkdirSync(target, { recursive: true });
|
|
143
|
+
|
|
144
|
+
console.log('--- Step 1: System Prerequisites Check ---');
|
|
145
|
+
try {
|
|
146
|
+
execFileSync(process.execPath, [path.join(__dirname, 'check-system-prerequisites.js')], { stdio: 'inherit', cwd: target });
|
|
147
|
+
} catch {
|
|
148
|
+
console.log('\n⚠️ Prerequisites warning detected. Continuing with configuration...\n');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
console.log('\n--- Step 2: Project Definition ---');
|
|
152
|
+
const projectName = await ask('Project Name', EXISTING.projectName || path.basename(target));
|
|
153
|
+
const runtime = await ask('Primary Runtime / Stack (e.g. node-24-lts, go-1.23, python-3.12)', DEFAULTS.runtime);
|
|
154
|
+
|
|
155
|
+
console.log('\n--- Step 3: 4-Pillar Discovery Interview (recorded in ADR-0001) ---');
|
|
156
|
+
const concurrency = await ask('1. Expected peak concurrency (e.g. 1-10 internal, 100-1,000, 50k+ public)', DEFAULTS.concurrency);
|
|
157
|
+
const hardware = await ask('2. Target deployment environment (e.g. local machine, $5 VPS, serverless, cloud)', DEFAULTS.hardware);
|
|
158
|
+
const workload = await ask('3. Primary compute workload (e.g. I/O-heavy API, static SPA, live transcoding)', DEFAULTS.workload);
|
|
159
|
+
const i18n = await askYesNo('4a. Multi-language support (i18n) required?', EXISTING.i18n === true);
|
|
160
|
+
const pwa = await askYesNo('4b. Offline / Progressive Web App support required?', EXISTING.pwa === true);
|
|
161
|
+
|
|
162
|
+
console.log('\n--- Step 4: Specification Depth ---');
|
|
163
|
+
console.log(' [1] 🟢 Lite Mode (Default): Single file docs/SPEC.md (Specify + Plan + Tasks + Gate).');
|
|
164
|
+
console.log(' Recommended for solo developers and rapid MVP building.');
|
|
165
|
+
console.log(' [2] 🔴 Rigor Mode: Plan of Record + Execution Guide + Compliance Log (+ Remediation annexes).');
|
|
166
|
+
console.log(' Recommended for multi-agent teams and mission-critical systems.');
|
|
167
|
+
const specMode = (await ask('Select Mode (1 or 2)', EXISTING.specMode === 'rigor' ? '2' : '1')) === '2' ? 'rigor' : 'lite';
|
|
168
|
+
|
|
169
|
+
console.log('\n--- Step 5: AST Navigation Adapter ---');
|
|
170
|
+
console.log(' [1] ast-grep (Fast native Tree-sitter structural search, zero Python)');
|
|
171
|
+
console.log(' [2] graphify (Relational knowledge graph, maximum token reduction)');
|
|
172
|
+
console.log(' [3] ripgrep (Universal fast regex baseline)');
|
|
173
|
+
console.log(' [4] lsp (Language Server Protocol / SCIP compiler type indexing)');
|
|
174
|
+
const astAdapters = { '1': 'ast-grep', '2': 'graphify', '3': 'ripgrep', '4': 'lsp' };
|
|
175
|
+
const astDefault = Object.keys(astAdapters).find(key => astAdapters[key] === EXISTING.astAdapter) || '1';
|
|
176
|
+
const astAdapter = astAdapters[await ask('Select AST Adapter (1-4)', astDefault)] || 'ast-grep';
|
|
177
|
+
|
|
178
|
+
rl.close();
|
|
179
|
+
bootstrap({ projectName, runtime, specMode, astAdapter, concurrency, hardware, workload, i18n, pwa });
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function runExpressMode() {
|
|
183
|
+
printHeader();
|
|
184
|
+
console.log(`⚡ Express Mode: provisioning ${target}\n`);
|
|
185
|
+
bootstrap({
|
|
186
|
+
projectName: getArgValue('name', EXISTING.projectName || path.basename(target)),
|
|
187
|
+
runtime: getArgValue('runtime', DEFAULTS.runtime),
|
|
188
|
+
specMode: getArgValue('mode', EXISTING.specMode || 'lite').toLowerCase(),
|
|
189
|
+
astAdapter: getArgValue('ast', EXISTING.astAdapter || 'ast-grep').toLowerCase(),
|
|
190
|
+
concurrency: getArgValue('concurrency', DEFAULTS.concurrency),
|
|
191
|
+
hardware: getArgValue('hardware', DEFAULTS.hardware),
|
|
192
|
+
workload: getArgValue('workload', DEFAULTS.workload),
|
|
193
|
+
i18n: parsed.flags.has('i18n') ? true : EXISTING.i18n === true,
|
|
194
|
+
pwa: parsed.flags.has('pwa') ? true : EXISTING.pwa === true
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function bootstrap(answers) {
|
|
199
|
+
console.log('\n===============================================================');
|
|
200
|
+
console.log(' 📦 Provisioning SDD Governance Structure...');
|
|
201
|
+
console.log('===============================================================\n');
|
|
202
|
+
|
|
203
|
+
let result;
|
|
204
|
+
try {
|
|
205
|
+
result = provision(answers, { target, force });
|
|
206
|
+
} catch (error) {
|
|
207
|
+
console.error(`\n❌ ${error.message}\n`);
|
|
208
|
+
process.exit(1);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
console.log('\n🎉 Bootstrapping complete!\n');
|
|
212
|
+
console.log('Next steps:');
|
|
213
|
+
if (result.skippedEntrypoints.length === 0) {
|
|
214
|
+
console.log(' 1. Commit the generated files. AGENTS.md, CLAUDE.md and .claude/skills/ load the');
|
|
215
|
+
console.log(' rules automatically in Claude Code, Codex, Cursor and other AGENTS.md-aware agents.');
|
|
216
|
+
} else {
|
|
217
|
+
console.log(` 1. Commit the generated files. ${result.skippedEntrypoints.join(' and ')} already existed and`);
|
|
218
|
+
console.log(' were left untouched, so agents will not load the rules until you add to them:');
|
|
219
|
+
console.log(' "Before any task, read .agents/AGENTS.md and .agents/CONTEXT.md."');
|
|
220
|
+
}
|
|
221
|
+
console.log(' 2. Record the incident or rationale behind each rule in .agents/AGENTS.md.');
|
|
222
|
+
const { specification = {} } = result.config;
|
|
223
|
+
if (answers.specMode === 'lite') {
|
|
224
|
+
console.log(` 3. Define your tasks in ${specification.specFile || 'docs/SPEC.md'} and implement with verifiable gates.`);
|
|
225
|
+
} else {
|
|
226
|
+
console.log(` 3. Write ${specification.roadmapDir || 'docs/roadmap'}/plan-of-record.md and coordinate with the Auditor-Executor protocol.`);
|
|
227
|
+
if (spawnSync('auditkit', ['--version']).error) {
|
|
228
|
+
console.log(' Rigor mode is checked by auditkit, which is not installed. Install it with:');
|
|
229
|
+
console.log(' pipx install git+https://github.com/tBeltty/auditor-executor-protocol');
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
console.log(` 4. Quality gate: ${result.gateCommand}${result.hookInstalled ? ' (runs on every git push)' : ''}\n`);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
if (wantsExpress) {
|
|
236
|
+
runExpressMode();
|
|
237
|
+
} else if (!process.stdin.isTTY) {
|
|
238
|
+
console.log('ℹ️ No interactive terminal detected; using Express Mode defaults.');
|
|
239
|
+
runExpressMode();
|
|
240
|
+
} else {
|
|
241
|
+
runGuidedMode().catch(error => {
|
|
242
|
+
console.error(`\n❌ ${error.message}\n`);
|
|
243
|
+
process.exit(1);
|
|
244
|
+
});
|
|
245
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/sdd-verify.js
|
|
5
|
+
*
|
|
6
|
+
* Runs verification commands for the Lite specification and records the results.
|
|
7
|
+
*
|
|
8
|
+
* node scripts/sdd-verify.js [--record]
|
|
9
|
+
* Runs the "Verification Gate" command. Passes when it exits 0, every non-empty
|
|
10
|
+
* expected line appears in its output (a /.../ line is a regular expression), and it
|
|
11
|
+
* did not modify tracked files. --record writes
|
|
12
|
+
* "Last Verified: <date> PASS|FAIL (commit <sha>, exit <code>, state <fingerprint>, check <hash>)"
|
|
13
|
+
* into the spec; the gate compares that fingerprint with the committed content.
|
|
14
|
+
*
|
|
15
|
+
* node scripts/sdd-verify.js --task <ID> -- <command ...>
|
|
16
|
+
* Runs <command>, writes its transcript as the task's Evidence with the exit code and
|
|
17
|
+
* a hash of the transcript, and checks the task's box.
|
|
18
|
+
*
|
|
19
|
+
* Commands come from the specification or the command line and run in a shell
|
|
20
|
+
* (specification.verifyShell, default /bin/sh or cmd.exe) with a time limit
|
|
21
|
+
* (specification.verifyTimeoutSeconds, default 900). The quality gate never runs them.
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
const fs = require('fs');
|
|
25
|
+
const path = require('path');
|
|
26
|
+
const { git, repoRoot, WORKTREE } = require('./lib/git');
|
|
27
|
+
const { loadConfig, getIn } = require('./lib/config');
|
|
28
|
+
const { parseSpec, formatLastVerified, withLastVerified, withTaskEvidence } = require('./lib/spec');
|
|
29
|
+
const { stateOf, currentCommit } = require('./lib/state');
|
|
30
|
+
const { DEFAULT_TIMEOUT_SECONDS, runCommand, joinCommand } = require('./lib/runner');
|
|
31
|
+
|
|
32
|
+
const MAX_EVIDENCE_LINES = 200;
|
|
33
|
+
const USAGE = 'Usage: sdd-verify [--record] | sdd-verify --task <ID> -- <command ...>';
|
|
34
|
+
|
|
35
|
+
function matchExpected(expected, output) {
|
|
36
|
+
const missing = [];
|
|
37
|
+
for (const raw of expected.split('\n')) {
|
|
38
|
+
const line = raw.trim();
|
|
39
|
+
if (!line) continue;
|
|
40
|
+
const regex = line.length > 2 && line.startsWith('/') && line.endsWith('/');
|
|
41
|
+
const found = regex ? new RegExp(line.slice(1, -1), 'm').test(output) : output.includes(line);
|
|
42
|
+
if (!found) missing.push(line);
|
|
43
|
+
}
|
|
44
|
+
return missing;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function loadLiteSpec(root) {
|
|
48
|
+
const config = loadConfig(root, WORKTREE);
|
|
49
|
+
if (getIn(config, 'specification.mode', 'lite') === 'rigor') {
|
|
50
|
+
throw new Error('sdd-verify covers Lite mode. In Rigor mode, gates are run per task (see the execution guide) and checked with auditkit lint.');
|
|
51
|
+
}
|
|
52
|
+
const specFile = getIn(config, 'specification.specFile', 'docs/SPEC.md');
|
|
53
|
+
const specPath = path.join(root, specFile);
|
|
54
|
+
if (!fs.existsSync(specPath)) throw new Error(`${specFile} not found.`);
|
|
55
|
+
const runOptions = {
|
|
56
|
+
cwd: root,
|
|
57
|
+
timeoutSeconds: getIn(config, 'specification.verifyTimeoutSeconds', DEFAULT_TIMEOUT_SECONDS),
|
|
58
|
+
shell: getIn(config, 'specification.verifyShell', undefined)
|
|
59
|
+
};
|
|
60
|
+
return { specFile, specPath, text: fs.readFileSync(specPath, 'utf8'), runOptions };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function today(date) {
|
|
64
|
+
return date || new Date().toISOString().slice(0, 10);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Untracked files take part in the verification but not in the recorded state fingerprint,
|
|
68
|
+
// so a recorded PASS could depend on files that are never committed.
|
|
69
|
+
function untrackedFiles(root) {
|
|
70
|
+
return git(['ls-files', '--others', '--exclude-standard', '-z'], root).split('\0').filter(Boolean);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function verify({ root = repoRoot(), record = false, log = console.log, date } = {}) {
|
|
74
|
+
const { specFile, specPath, text, runOptions } = loadLiteSpec(root);
|
|
75
|
+
const { gate, hiddenProblems } = parseSpec(text);
|
|
76
|
+
if (hiddenProblems.length > 0) throw new Error(`${specFile}: ${hiddenProblems.join(' ')}`);
|
|
77
|
+
if (!gate) throw new Error(`${specFile} has no "Verification Gate" section.`);
|
|
78
|
+
if (!gate.command) throw new Error(`${specFile}: the verification command is still a placeholder.`);
|
|
79
|
+
if (!gate.expected) throw new Error(`${specFile}: the expected output is still a placeholder.`);
|
|
80
|
+
const untracked = untrackedFiles(root);
|
|
81
|
+
if (record && untracked.length > 0) {
|
|
82
|
+
throw new Error(`Untracked files would take part in the verification but not in the recorded state: ${untracked.slice(0, 5).join(', ')}${untracked.length > 5 ? ', ...' : ''}. Commit, ignore, or remove them, then record again.`);
|
|
83
|
+
}
|
|
84
|
+
if (untracked.length > 0) log(`⚠️ ${untracked.length} untracked file(s) take part in this run; --record refuses to run until they are committed, ignored, or removed.`);
|
|
85
|
+
|
|
86
|
+
const before = stateOf(root, WORKTREE, specFile);
|
|
87
|
+
log(`$ ${gate.command}\n`);
|
|
88
|
+
const { exit, output } = await runCommand(gate.command, runOptions);
|
|
89
|
+
log(output.trimEnd());
|
|
90
|
+
const after = stateOf(root, WORKTREE, specFile);
|
|
91
|
+
|
|
92
|
+
const missing = matchExpected(gate.expected, output);
|
|
93
|
+
const modified = before !== after;
|
|
94
|
+
const pass = exit === '0' && missing.length === 0 && !modified;
|
|
95
|
+
|
|
96
|
+
log('');
|
|
97
|
+
if (exit !== '0') log(`❌ Command exited with ${exit}.`);
|
|
98
|
+
for (const line of missing) log(`❌ Expected output not found: ${line}`);
|
|
99
|
+
if (modified) log('❌ The command modified tracked files; verify a clean state, then run it again.');
|
|
100
|
+
if (pass) log('✅ Verification gate passed.');
|
|
101
|
+
|
|
102
|
+
if (record) {
|
|
103
|
+
const fields = { date: today(date), result: pass ? 'PASS' : 'FAIL', commit: currentCommit(root), exit, state: before };
|
|
104
|
+
const value = formatLastVerified(fields, gate.command, gate.expected);
|
|
105
|
+
fs.writeFileSync(specPath, withLastVerified(text, value));
|
|
106
|
+
log(`📝 Recorded in ${specFile}: Last Verified ${value}`);
|
|
107
|
+
}
|
|
108
|
+
return { pass, exitCode: exit, missing, modified };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
async function recordTask({ root = repoRoot(), taskId, command, log = console.log, date } = {}) {
|
|
112
|
+
const { specFile, specPath, text, runOptions } = loadLiteSpec(root);
|
|
113
|
+
const spec = parseSpec(text);
|
|
114
|
+
if (spec.hiddenProblems.length > 0) throw new Error(`${specFile}: ${spec.hiddenProblems.join(' ')}`);
|
|
115
|
+
if (!spec.tasks.some(t => t.id === taskId)) {
|
|
116
|
+
throw new Error(`Task ${taskId} not found in ${specFile}.`);
|
|
117
|
+
}
|
|
118
|
+
log(`$ ${command}\n`);
|
|
119
|
+
const { exit, output } = await runCommand(command, runOptions);
|
|
120
|
+
log(output.trimEnd());
|
|
121
|
+
|
|
122
|
+
let lines = output.replace(/\r\n?/g, '\n').replace(/\s+$/, '').split('\n');
|
|
123
|
+
if (lines.length > MAX_EVIDENCE_LINES) {
|
|
124
|
+
const omitted = lines.length - MAX_EVIDENCE_LINES;
|
|
125
|
+
lines = [...lines.slice(0, MAX_EVIDENCE_LINES / 2), `[... ${omitted} lines omitted ...]`, ...lines.slice(-MAX_EVIDENCE_LINES / 2)];
|
|
126
|
+
}
|
|
127
|
+
const transcript = [`$ ${command}`, ...lines].join('\n');
|
|
128
|
+
fs.writeFileSync(specPath, withTaskEvidence(text, taskId, { date: today(date), exit, transcript }));
|
|
129
|
+
log(`\n${exit === '0' ? '✅' : '❌'} ${taskId}: exit ${exit}, evidence recorded in ${specFile}.`);
|
|
130
|
+
return { exitCode: exit };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function parseArgs(argv) {
|
|
134
|
+
const separator = argv.indexOf('--');
|
|
135
|
+
const flags = separator === -1 ? argv : argv.slice(0, separator);
|
|
136
|
+
const command = separator === -1 ? [] : argv.slice(separator + 1);
|
|
137
|
+
const options = { record: false, taskId: null, command: null };
|
|
138
|
+
for (let i = 0; i < flags.length; i++) {
|
|
139
|
+
const flag = flags[i];
|
|
140
|
+
if (flag === '--record') options.record = true;
|
|
141
|
+
else if (flag === '--task') options.taskId = flags[++i];
|
|
142
|
+
else if (flag.startsWith('--task=')) options.taskId = flag.slice('--task='.length);
|
|
143
|
+
else if (flag === '--help' || flag === '-h') throw new Error(USAGE);
|
|
144
|
+
else throw new Error(`Unknown argument "${flag}". ${USAGE}`);
|
|
145
|
+
}
|
|
146
|
+
if (options.taskId !== null) {
|
|
147
|
+
if (!options.taskId) throw new Error(`--task needs a task ID. ${USAGE}`);
|
|
148
|
+
if (command.length === 0) throw new Error(`--task needs a command after "--". ${USAGE}`);
|
|
149
|
+
if (options.record) throw new Error(`--record and --task are separate modes. ${USAGE}`);
|
|
150
|
+
options.command = joinCommand(command);
|
|
151
|
+
} else if (command.length > 0) {
|
|
152
|
+
throw new Error(`A command after "--" is only used with --task. ${USAGE}`);
|
|
153
|
+
}
|
|
154
|
+
return options;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
async function main(argv) {
|
|
158
|
+
try {
|
|
159
|
+
const options = parseArgs(argv);
|
|
160
|
+
if (options.taskId) {
|
|
161
|
+
const { exitCode } = await recordTask({ taskId: options.taskId, command: options.command });
|
|
162
|
+
return exitCode === '0' ? 0 : 1;
|
|
163
|
+
}
|
|
164
|
+
const { pass } = await verify({ record: options.record });
|
|
165
|
+
return pass ? 0 : 1;
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`❌ ${error.message}`);
|
|
168
|
+
return 2;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (require.main === module) {
|
|
173
|
+
main(process.argv.slice(2)).then(code => process.exit(code));
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
module.exports = { matchExpected, parseArgs, verify, recordTask };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* scripts/verify-no-secrets.js
|
|
5
|
+
*
|
|
6
|
+
* Credential leak scanner. Reads tracked files from the working tree, the index
|
|
7
|
+
* (--staged) or a commit (--ref=<sha>) and reports:
|
|
8
|
+
* - provider keys and private key blocks (pattern match on content);
|
|
9
|
+
* - credentials in URLs and high-entropy values assigned to secret-named keys;
|
|
10
|
+
* - tracked files whose name marks them as secret (.env, id_rsa, *.key, *.p12, ...).
|
|
11
|
+
*
|
|
12
|
+
* A content match is ignored when the matched value is an obvious placeholder, or when
|
|
13
|
+
* the line carries the pragma "sdd-allow-secret"; suppressed lines are counted in the
|
|
14
|
+
* report. A sensitive file name is accepted only when listed in security.allowFiles.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const { listFiles, readFiles, isBinary, WORKTREE } = require('./lib/git');
|
|
19
|
+
const { loadConfig, getIn } = require('./lib/config');
|
|
20
|
+
const { runCheckCli } = require('./lib/cli');
|
|
21
|
+
|
|
22
|
+
// Ordered from most to least specific; every match of every pattern on a line is checked,
|
|
23
|
+
// so a placeholder earlier on the line cannot hide a real key later on it.
|
|
24
|
+
const SECRET_PATTERNS = [
|
|
25
|
+
{ name: 'Private Key Block', regex: /-----BEGIN (?:[A-Z0-9]+ )*PRIVATE KEY(?: BLOCK)?-----/g },
|
|
26
|
+
{ name: 'Anthropic API Key', regex: /\bsk-ant-[0-9A-Za-z_-]{20,}/g },
|
|
27
|
+
{ name: 'OpenAI API Key', regex: /\bsk-(?:proj|svcacct|admin)-[0-9A-Za-z_-]{20,}/g },
|
|
28
|
+
{ name: 'OpenAI Legacy API Key', regex: /\bsk-[0-9A-Za-z]{32,}\b/g },
|
|
29
|
+
{ name: 'Stripe Live Key', regex: /\b[rs]k_live_[0-9A-Za-z]{16,}/g },
|
|
30
|
+
{ name: 'GitHub Token', regex: /\bgh[pousr]_[0-9A-Za-z]{36}\b/g },
|
|
31
|
+
{ name: 'GitHub Fine-Grained Token', regex: /\bgithub_pat_[0-9A-Za-z_]{22,}/g },
|
|
32
|
+
{ name: 'Slack Token', regex: /\bxox[abposr]-[0-9A-Za-z-]{10,}/g },
|
|
33
|
+
// Requires both a digit and a letter so identifiers like re_compiled_pattern_list pass.
|
|
34
|
+
{ name: 'Resend API Key', regex: /\bre_(?=[A-Za-z_]*\d)(?=[0-9_]*[A-Za-z])[0-9A-Za-z_]{24,}/g },
|
|
35
|
+
{ name: 'AWS Access Key ID', regex: /\b(?:AKIA|ABIA|ACCA|ASIA)[0-9A-Z]{16}\b/g },
|
|
36
|
+
{ name: 'Google API Key', regex: /\bAIza[0-9A-Za-z_-]{35}/g },
|
|
37
|
+
{
|
|
38
|
+
name: 'Credentials in URL',
|
|
39
|
+
regex: /\b[a-z][a-z0-9+.-]*:\/\/[^\s:/@'"]+:([^\s@/'"]{6,})@/gi,
|
|
40
|
+
group: 1,
|
|
41
|
+
accept: value => !isReference(value) && !/^\*+$/.test(value)
|
|
42
|
+
}
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Keys whose value is a secret: ends in secret, token, password, pwd, credential(s), or an
|
|
46
|
+
// api, private, access, secret, signing, encryption or master key. `max_tokens`,
|
|
47
|
+
// `tokenizer` or `cache_key` do not match.
|
|
48
|
+
const SECRET_KEY_RE = /(?:^|[^A-Za-z0-9_])([A-Za-z0-9_.-]*?(?:secret|token|passw(?:or)?d|pwd|credentials?|(?:api|private|access|secret|signing|encryption|master)[_-]?key))["']?\s*(?::=|[:=])\s*(.*)$/i;
|
|
49
|
+
const PASSWORD_KEY_RE = /passw(?:or)?d|pwd|secret|credential/i;
|
|
50
|
+
// Files where an unquoted value is a literal (KEY=value), not a code expression: env and
|
|
51
|
+
// config files, package manager rc files, shell and batch scripts, and Dockerfiles. PowerShell
|
|
52
|
+
// is not included: its right-hand sides are expressions (Read-Host, Get-Credential, ...).
|
|
53
|
+
const CONFIG_FILE_RE = /(^|\/)(\.env[^/]*|\.npmrc|\.pypirc|\.yarnrc(\.yml)?|Dockerfile[^/]*|Containerfile)$|\.(env|ini|cfg|conf|properties|toml|ya?ml|json|sh|bash|zsh|bat|cmd|tfvars|dockerfile)$/i;
|
|
54
|
+
|
|
55
|
+
// Shannon entropy in bits per character.
|
|
56
|
+
function entropy(value) {
|
|
57
|
+
const counts = {};
|
|
58
|
+
for (const ch of value) counts[ch] = (counts[ch] || 0) + 1;
|
|
59
|
+
return Object.values(counts).reduce((sum, n) => {
|
|
60
|
+
const p = n / value.length;
|
|
61
|
+
return sum - p * Math.log2(p);
|
|
62
|
+
}, 0);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// $VAR, ${VAR}, %VAR%, {{var}}, <value>, process.env.X, os.environ[...]
|
|
66
|
+
function isReference(value) {
|
|
67
|
+
return /^\$|\$\{|^%\w+%$|\{\{|<[^>]*>|process\.env|os\.environ|getenv/.test(value);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Checked against the matched value only; a placeholder elsewhere on the line does not
|
|
71
|
+
// hide a real key.
|
|
72
|
+
const SAFE_PLACEHOLDERS = [
|
|
73
|
+
'test_secret', 'test-secret', 'your_api_key', 'your-api-key', 'dummy', 'placeholder',
|
|
74
|
+
'mock_key', 'example', 'xxxxxxxx', 'changeme', 'redacted', 'fake', 'sample'
|
|
75
|
+
];
|
|
76
|
+
const isPlaceholder = value => SAFE_PLACEHOLDERS.some(p => value.toLowerCase().includes(p))
|
|
77
|
+
|| /^(password|secret|token|none|null|undefined|true|false)$/i.test(value);
|
|
78
|
+
|
|
79
|
+
// The literal assigned to a secret-named key, if any. In code, only quoted values are
|
|
80
|
+
// literals (an unquoted value is an expression such as a call or a variable); in config
|
|
81
|
+
// files, an unquoted value is a literal too.
|
|
82
|
+
function secretAssignment(rawLine, { configFile }) {
|
|
83
|
+
// Dockerfile legacy form `ENV KEY value` is `ENV KEY=value`.
|
|
84
|
+
let line = configFile ? rawLine.replace(/^(\s*(?:ENV|ARG)\s+)([A-Za-z_][A-Za-z0-9_]*)\s+(?=\S)/i, '$1$2=') : rawLine;
|
|
85
|
+
// `set /p NAME=Prompt:` in a batch file reads input; the text is a prompt, not a value.
|
|
86
|
+
if (configFile && /^\s*set\s+\/p\b/i.test(line)) return null;
|
|
87
|
+
// `setx [/M] NAME value` (batch) is `NAME=value`.
|
|
88
|
+
if (configFile) line = line.replace(/^(\s*@?setx(?:\s+\/\w+)*\s+)"?([A-Za-z_][A-Za-z0-9_]*)"?\s+(?=\S)/i, '$1$2=');
|
|
89
|
+
const match = line.match(SECRET_KEY_RE);
|
|
90
|
+
if (!match) return null;
|
|
91
|
+
const [, key, rest] = match;
|
|
92
|
+
const quoted = rest.match(/^(["'`])((?:\\.|(?!\1).)*)\1/);
|
|
93
|
+
let value;
|
|
94
|
+
if (quoted) value = quoted[2];
|
|
95
|
+
// `#` starts a comment only after whitespace (dotenv, YAML), so it can be part of a value.
|
|
96
|
+
else if (configFile) value = (rest.match(/^\S+/) || [''])[0].replace(/[,;]$/, '');
|
|
97
|
+
else return null;
|
|
98
|
+
if (value.length < 8 || isPlaceholder(value) || isReference(value)) return null;
|
|
99
|
+
if (/\s/.test(value)) return null; // prose, not a credential
|
|
100
|
+
// Lockfile dependency specifiers (pnpm/yarn) put peer-dependency ranges in parens, e.g.
|
|
101
|
+
// "0.4.8(@solana/web3.js@1.95.3(...))"; no credential format uses parens.
|
|
102
|
+
if (/[()]/.test(value)) return null;
|
|
103
|
+
// Passwords can be low-entropy words; tokens and keys must look random.
|
|
104
|
+
if (!PASSWORD_KEY_RE.test(key) && (value.length < 16 || entropy(value) < 3.0)) return null;
|
|
105
|
+
return { key, value };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const ALLOW_PRAGMA = 'sdd-allow-secret';
|
|
109
|
+
|
|
110
|
+
// Skipped: this scanner at the paths the framework installs it (its patterns would match
|
|
111
|
+
// themselves) and dependency directories as whole path segments. Lockfiles are scanned:
|
|
112
|
+
// a private-registry URL in them can embed a token.
|
|
113
|
+
const SKIPPED_PATHS = ['scripts/verify-no-secrets.js', '.sdd/scripts/verify-no-secrets.js'];
|
|
114
|
+
const isSkipped = file => SKIPPED_PATHS.includes(file) || /(^|\/)node_modules\//.test(file);
|
|
115
|
+
|
|
116
|
+
const SENSITIVE_FILES = [
|
|
117
|
+
{
|
|
118
|
+
name: 'Environment file',
|
|
119
|
+
test: base => /^\.env(\..+)?$/.test(base) && !/^\.env\.(example|sample|template|dist|defaults)$/.test(base)
|
|
120
|
+
},
|
|
121
|
+
{ name: 'SSH private key', test: base => /^id_(rsa|dsa|ecdsa|ed25519)$/.test(base) },
|
|
122
|
+
{ name: 'Key or keystore file', test: base => /\.(key|p12|pfx|jks|keystore|ppk|kdbx)$/i.test(base) }
|
|
123
|
+
];
|
|
124
|
+
|
|
125
|
+
function maskMatch(str) {
|
|
126
|
+
if (str.length <= 8) return '****';
|
|
127
|
+
return str.slice(0, 4) + '...' + str.slice(-4);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function findInLine(line, options) {
|
|
131
|
+
for (const pattern of SECRET_PATTERNS) {
|
|
132
|
+
for (const match of line.matchAll(pattern.regex)) {
|
|
133
|
+
const value = pattern.group ? match[pattern.group] : match[0];
|
|
134
|
+
if (isPlaceholder(value)) continue;
|
|
135
|
+
if (pattern.accept && !pattern.accept(value)) continue;
|
|
136
|
+
return { patternName: pattern.name, value };
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const assignment = secretAssignment(line, options);
|
|
140
|
+
return assignment ? { patternName: 'Secret Assignment', value: assignment.value } : null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// `file` (a repository path) decides whether unquoted values count as literals.
|
|
144
|
+
function scanContent(content, { file = '' } = {}) {
|
|
145
|
+
const options = { configFile: CONFIG_FILE_RE.test(file) };
|
|
146
|
+
const violations = [];
|
|
147
|
+
let suppressed = 0;
|
|
148
|
+
content.split(/\r?\n/).forEach((line, index) => {
|
|
149
|
+
const hit = findInLine(line, options);
|
|
150
|
+
if (!hit) return;
|
|
151
|
+
if (line.includes(ALLOW_PRAGMA)) {
|
|
152
|
+
suppressed++;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
violations.push({ line: index + 1, patternName: hit.patternName, snippet: maskMatch(hit.value) });
|
|
156
|
+
});
|
|
157
|
+
return { violations, suppressed };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Text to scan from a file's bytes. UTF-16 files are decoded; other binary files are
|
|
161
|
+
// reduced to their printable runs (like `strings`), so a NUL byte cannot hide a secret.
|
|
162
|
+
function textOf(buffer) {
|
|
163
|
+
if (buffer[0] === 0xff && buffer[1] === 0xfe) return buffer.subarray(2).toString('utf16le');
|
|
164
|
+
if (buffer[0] === 0xfe && buffer[1] === 0xff) return Buffer.from(buffer.subarray(2)).swap16().toString('utf16le');
|
|
165
|
+
if (!isBinary(buffer)) return buffer.toString('utf8');
|
|
166
|
+
return (buffer.toString('latin1').match(/[\x20-\x7e]{8,}/g) || []).join('\n');
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function sensitiveFileName(file) {
|
|
170
|
+
const rule = SENSITIVE_FILES.find(r => r.test(path.posix.basename(file)));
|
|
171
|
+
return rule ? rule.name : null;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// `files` restricts the scan (the pre-push hook passes the files each new commit changed).
|
|
175
|
+
function run({ root, source = WORKTREE, files: only = null } = {}) {
|
|
176
|
+
const config = loadConfig(root, source);
|
|
177
|
+
const allowFiles = new Set(getIn(config, 'security.allowFiles', []));
|
|
178
|
+
const files = (only || listFiles(root, source)).filter(f =>
|
|
179
|
+
!isSkipped(f));
|
|
180
|
+
|
|
181
|
+
const violations = [];
|
|
182
|
+
let suppressed = 0;
|
|
183
|
+
for (const file of files) {
|
|
184
|
+
const fileRule = sensitiveFileName(file);
|
|
185
|
+
if (fileRule && !allowFiles.has(file)) {
|
|
186
|
+
violations.push({ file, line: 0, patternName: `Tracked ${fileRule}`, snippet: 'file should not be in git' });
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
for (const [file, buffer] of readFiles(root, files, source)) {
|
|
190
|
+
if (!buffer) continue;
|
|
191
|
+
const result = scanContent(textOf(buffer), { file });
|
|
192
|
+
suppressed += result.suppressed;
|
|
193
|
+
for (const v of result.violations) violations.push({ file, ...v });
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const suppressedNote = suppressed > 0 ? ` ${suppressed} line(s) suppressed with "${ALLOW_PRAGMA}".` : '';
|
|
197
|
+
if (violations.length === 0) {
|
|
198
|
+
return { ok: true, report: `✅ Scanned ${files.length} files. Zero unmasked secrets found.${suppressedNote}` };
|
|
199
|
+
}
|
|
200
|
+
const lines = ['❌ CRITICAL ERROR: Potential secrets detected in codebase!\n'];
|
|
201
|
+
for (const v of violations) {
|
|
202
|
+
lines.push(` - File: ${v.file}${v.line ? `:${v.line}` : ''}`);
|
|
203
|
+
lines.push(` Pattern: ${v.patternName}`);
|
|
204
|
+
lines.push(` Detected: ${v.snippet}\n`);
|
|
205
|
+
}
|
|
206
|
+
lines.push('Action Required: Move secrets to Tier 3 Vault (~/secrets/<app>/.vault) or environment variables, and remove secret files from git.');
|
|
207
|
+
lines.push(`False positive? Add the comment "${ALLOW_PRAGMA}" to that line, or list the file in security.allowFiles.`);
|
|
208
|
+
if (suppressedNote) lines.push(suppressedNote.trim());
|
|
209
|
+
return { ok: false, report: lines.join('\n') };
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if (require.main === module) {
|
|
213
|
+
runCheckCli('🔒 Agentic SDD Framework: Secret Leak Scanner', run);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { SECRET_PATTERNS, entropy, secretAssignment, scanContent, sensitiveFileName, textOf, run };
|
package/sdd.config.json
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": "1.4.0",
|
|
3
|
+
"project": {
|
|
4
|
+
"name": "agentic-sdd-framework",
|
|
5
|
+
"type": "framework",
|
|
6
|
+
"runtime": "node-24-lts"
|
|
7
|
+
},
|
|
8
|
+
"specification": {
|
|
9
|
+
"mode": "lite",
|
|
10
|
+
"allowedModes": [
|
|
11
|
+
"lite",
|
|
12
|
+
"rigor"
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"architecture": {
|
|
16
|
+
"style": "clean-architecture",
|
|
17
|
+
"maxLocPerFile": 400
|
|
18
|
+
},
|
|
19
|
+
"capabilities": {
|
|
20
|
+
"astNavigation": {
|
|
21
|
+
"adapter": "ast-grep"
|
|
22
|
+
},
|
|
23
|
+
"noAiSlop": {
|
|
24
|
+
"enabled": true
|
|
25
|
+
},
|
|
26
|
+
"i18n": {
|
|
27
|
+
"enabled": false
|
|
28
|
+
},
|
|
29
|
+
"pwa": {
|
|
30
|
+
"enabled": false
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|