sinapse-ai 1.6.1 → 1.8.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/.claude/CLAUDE.md +5 -11
- package/.claude/hooks/README.md +14 -1
- package/.claude/hooks/code-intel-pretool.cjs +115 -0
- package/.claude/hooks/enforce-delegation.cjs +31 -3
- package/.claude/hooks/enforce-framework-boundary.cjs +324 -0
- package/.claude/hooks/enforce-permission-mode.cjs +249 -0
- package/.claude/hooks/secret-scanning.cjs +34 -43
- package/.claude/hooks/synapse-engine.cjs +23 -23
- package/.claude/hooks/telemetry-post-tool.cjs +128 -0
- package/.claude/hooks/telemetry-stop.cjs +132 -0
- package/.claude/hooks/verify-packages.cjs +9 -2
- package/.claude/rules/documentation-first.md +1 -1
- package/.claude/rules/hook-governance.md +2 -0
- package/.sinapse-ai/cli/commands/health/index.js +24 -0
- package/.sinapse-ai/core/README.md +11 -0
- package/.sinapse-ai/core/config/config-loader.js +19 -0
- package/.sinapse-ai/core/config/merge-utils.js +8 -0
- package/.sinapse-ai/core/errors/constants.js +147 -0
- package/.sinapse-ai/core/errors/error-registry.js +176 -0
- package/.sinapse-ai/core/errors/index.js +50 -0
- package/.sinapse-ai/core/errors/serializer.js +147 -0
- package/.sinapse-ai/core/errors/sinapse-error.js +144 -0
- package/.sinapse-ai/core/errors/utils.js +187 -0
- package/.sinapse-ai/core/execution/build-orchestrator.js +47 -49
- package/.sinapse-ai/core/execution/build-state-manager.js +183 -31
- package/.sinapse-ai/core/execution/parallel-executor.js +7 -1
- package/.sinapse-ai/core/execution/semantic-merge-engine.js +26 -14
- package/.sinapse-ai/core/execution/subagent-dispatcher.js +201 -60
- package/.sinapse-ai/core/execution/wave-executor.js +4 -1
- package/.sinapse-ai/core/grounding/README.md +71 -11
- package/.sinapse-ai/core/health-check/checks/project/framework-config.js +38 -2
- package/.sinapse-ai/core/health-check/checks/project/package-json.js +47 -3
- package/.sinapse-ai/core/health-check/checks/services/gemini-cli.js +117 -0
- package/.sinapse-ai/core/health-check/checks/services/index.js +2 -0
- package/.sinapse-ai/core/health-check/healers/index.js +40 -3
- package/.sinapse-ai/core/ideation/ideation-engine.js +212 -107
- package/.sinapse-ai/core/ids/gate-evaluator.js +318 -0
- package/.sinapse-ai/core/ids/gates/g5-semantic-handshake.js +190 -0
- package/.sinapse-ai/core/ids/gates/g6-ci-integrity.js +162 -0
- package/.sinapse-ai/core/ids/index.js +30 -0
- package/.sinapse-ai/core/memory/__tests__/active-modules.verify.js +11 -0
- package/.sinapse-ai/core/memory/gotchas-memory.js +37 -2
- package/.sinapse-ai/core/orchestration/agent-invoker.js +29 -6
- package/.sinapse-ai/core/orchestration/brownfield-handler.js +36 -3
- package/.sinapse-ai/core/orchestration/condition-evaluator.js +57 -0
- package/.sinapse-ai/core/orchestration/executors/epic-3-executor.js +76 -5
- package/.sinapse-ai/core/orchestration/executors/epic-4-executor.js +63 -17
- package/.sinapse-ai/core/orchestration/executors/epic-6-executor.js +153 -41
- package/.sinapse-ai/core/orchestration/executors/epic-executor.js +40 -0
- package/.sinapse-ai/core/orchestration/greenfield-handler.js +87 -3
- package/.sinapse-ai/core/orchestration/master-orchestrator.js +150 -10
- package/.sinapse-ai/core/orchestration/parallel-executor.js +6 -1
- package/.sinapse-ai/core/orchestration/recovery-handler.js +81 -8
- package/.sinapse-ai/core/orchestration/workflow-executor.js +41 -0
- package/.sinapse-ai/core/registry/registry-loader.js +71 -5
- package/.sinapse-ai/core/registry/squad-agent-resolver.js +253 -0
- package/.sinapse-ai/core/synapse/context/context-tracker.js +104 -9
- package/.sinapse-ai/core/synapse/context/index.js +19 -0
- package/.sinapse-ai/core/synapse/context/semantic-handshake-engine.js +555 -0
- package/.sinapse-ai/core/synapse/diagnostics/collectors/pipeline-collector.js +4 -2
- package/.sinapse-ai/core/synapse/engine.js +43 -3
- package/.sinapse-ai/core/telemetry/ids-sink.js +188 -0
- package/.sinapse-ai/core/utils/output-formatter.js +8 -290
- package/.sinapse-ai/core/utils/spawn-safe.js +186 -0
- package/.sinapse-ai/core-config.yaml +68 -1
- package/.sinapse-ai/data/entity-registry.yaml +15082 -13618
- package/.sinapse-ai/data/registry-update-log.jsonl +143 -0
- package/.sinapse-ai/development/agents/developer.md +2 -0
- package/.sinapse-ai/development/agents/devops.md +9 -0
- package/.sinapse-ai/development/external-executors/README.md +18 -0
- package/.sinapse-ai/development/external-executors/codex.md +56 -0
- package/.sinapse-ai/development/scripts/populate-entity-registry.js +65 -9
- package/.sinapse-ai/development/scripts/squad/squad-downloader.js +169 -14
- package/.sinapse-ai/development/tasks/delegate-to-external-executor.md +152 -0
- package/.sinapse-ai/development/tasks/github-devops-pre-push-quality-gate.md +46 -29
- package/.sinapse-ai/development/tasks/update-sinapse.md +3 -3
- package/.sinapse-ai/hooks/sinapse-brand-grounding.cjs +4 -7
- package/.sinapse-ai/hooks/sinapse-ds-grounding.cjs +5 -8
- package/.sinapse-ai/hooks/sinapse-vault-grounding.cjs +6 -9
- package/.sinapse-ai/infrastructure/integrations/ai-providers/ai-provider-factory.js +4 -1
- package/.sinapse-ai/infrastructure/integrations/ai-providers/claude-provider.js +57 -55
- package/.sinapse-ai/infrastructure/integrations/pm-adapters/github-adapter.js +9 -7
- package/.sinapse-ai/infrastructure/scripts/ide-sync/gemini-commands.js +298 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/index.js +127 -6
- package/.sinapse-ai/infrastructure/scripts/ide-sync/persona-renderer.js +97 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/antigravity.js +121 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/cursor.js +119 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/github-copilot.js +191 -0
- package/.sinapse-ai/infrastructure/scripts/ide-sync/transformers/kimi.js +448 -0
- package/.sinapse-ai/install-manifest.yaml +218 -114
- package/.sinapse-ai/product/templates/engine/renderer.js +20 -1
- package/.sinapse-ai/scripts/pm.sh +18 -6
- package/bin/cli.js +17 -0
- package/bin/commands/agents.js +96 -0
- package/bin/commands/doctor.js +15 -0
- package/bin/commands/ideate.js +129 -0
- package/bin/commands/uninstall.js +40 -0
- package/bin/postinstall.js +50 -4
- package/bin/sinapse.js +146 -2
- package/bin/utils/secret-scanner-core.js +253 -0
- package/bin/utils/staged-secret-scan.js +106 -40
- package/docs/framework/collaboration-autonomy-plan.md +18 -18
- package/docs/guides/parallel-workflow.md +6 -6
- package/package.json +22 -5
- package/packages/installer/src/installer/git-hooks-installer.js +384 -0
- package/packages/installer/src/installer/sinapse-ai-installer.js +16 -0
- package/packages/installer/src/wizard/ide-config-generator.js +23 -0
- package/packages/installer/src/wizard/validators.js +38 -1
- package/packages/installer/tests/unit/artifact-copy-pipeline/artifact-copy-pipeline.test.js +5 -1
- package/packages/installer/tests/unit/doctor/doctor-checks.test.js +44 -22
- package/packages/installer/tests/unit/git-hooks-installer.test.js +262 -0
- package/scripts/eval-runner.js +422 -0
- package/scripts/generate-install-manifest.js +13 -9
- package/scripts/generate-synapse-runtime.js +51 -0
- package/scripts/regenerate-orqx-stubs.ps1 +6 -5
- package/scripts/validate-all.js +1 -0
- package/scripts/validate-evals.js +466 -0
- package/scripts/validate-schemas.js +539 -0
- package/scripts/validate-squad-orqx.js +9 -2
- package/squads/claude-code-mastery/knowledge-base/memory-systems-reference.md +1 -1
- package/squads/squad-brand/templates/client-delivery-template.md +1 -1
- package/squads/squad-content/knowledge-base/social-compression-framework.md +1 -1
- package/squads/squad-council/knowledge-base/brand-strategy-models.md +1 -1
- package/.sinapse-ai/development/scripts/elicitation-engine.js +0 -385
- package/.sinapse-ai/development/scripts/elicitation-session-manager.js +0 -300
- package/.sinapse-ai/development/tasks/test-validation-task.md +0 -172
- package/docs/chrome-brain-upgrade-plan.md +0 -624
- package/docs/constitution-compliance.md +0 -87
- package/docs/mega-upgrade-orchestration-plan.md +0 -71
- package/docs/research-synthesis-for-upgrade.md +0 -511
- package/docs/security-audit-report.md +0 -306
package/bin/sinapse.js
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
const path = require('path');
|
|
10
10
|
const fs = require('fs');
|
|
11
11
|
const os = require('os');
|
|
12
|
-
const { execSync } = require('child_process');
|
|
12
|
+
const { execSync, spawnSync } = require('child_process');
|
|
13
13
|
const { emitDeprecationWarning } = require('./utils/deprecation-warning');
|
|
14
14
|
|
|
15
15
|
// Story A.2 — unified logger. Levels: error/warn/info/debug.
|
|
@@ -1174,13 +1174,23 @@ async function main() {
|
|
|
1174
1174
|
}
|
|
1175
1175
|
|
|
1176
1176
|
case 'health': {
|
|
1177
|
-
// Framework health analytics
|
|
1177
|
+
// Framework health analytics. Default = lightweight install-health
|
|
1178
|
+
// (hooks/squads/rules/skills). --deep runs the full core/health-check
|
|
1179
|
+
// engine (~35 project/runtime checks). --output-file writes the report.
|
|
1178
1180
|
const { runHealth } = require('../.sinapse-ai/cli/commands/health/index.js');
|
|
1179
1181
|
const healthArgs = args.slice(1);
|
|
1182
|
+
const outIdx = healthArgs.indexOf('--output-file');
|
|
1183
|
+
const isDeepHealth = healthArgs.includes('--deep');
|
|
1180
1184
|
await runHealth({
|
|
1181
1185
|
json: healthArgs.includes('--json'),
|
|
1182
1186
|
fix: healthArgs.includes('--fix'),
|
|
1187
|
+
deep: isDeepHealth,
|
|
1188
|
+
outputFile: outIdx >= 0 ? healthArgs[outIdx + 1] : undefined,
|
|
1183
1189
|
});
|
|
1190
|
+
// The --deep engine can leave async handles (e.g. spawned git checks)
|
|
1191
|
+
// open; exit explicitly so the CLI returns promptly. The report is fully
|
|
1192
|
+
// written/printed synchronously above. Lightweight path exits naturally.
|
|
1193
|
+
if (isDeepHealth) process.exit(0);
|
|
1184
1194
|
break;
|
|
1185
1195
|
}
|
|
1186
1196
|
|
|
@@ -1207,12 +1217,146 @@ async function main() {
|
|
|
1207
1217
|
break;
|
|
1208
1218
|
}
|
|
1209
1219
|
|
|
1220
|
+
case 'graph': {
|
|
1221
|
+
// Dependency/stats dashboard. Consolidated from the deprecated
|
|
1222
|
+
// standalone `sinapse-graph` binary into a subcommand of the main CLI.
|
|
1223
|
+
// Closes the published interface contract (claude-md template + CI test
|
|
1224
|
+
// expect `sinapse graph --deps` to work in every installed project).
|
|
1225
|
+
const { run } = require('../.sinapse-ai/core/graph-dashboard/cli');
|
|
1226
|
+
await run(args.slice(1));
|
|
1227
|
+
break;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
case 'mcp': {
|
|
1231
|
+
// Global MCP configuration manager (setup, link, status, add).
|
|
1232
|
+
// Routes to the Commander CLI, same pattern as the `qa` case.
|
|
1233
|
+
const { run } = require('../.sinapse-ai/cli/index.js');
|
|
1234
|
+
await run(process.argv);
|
|
1235
|
+
break;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
case 'generate': {
|
|
1239
|
+
// Document generator (templates: prd/adr/story/epic/task) — routes to the
|
|
1240
|
+
// Commander CLI. Without this case `sinapse generate` fell through to the
|
|
1241
|
+
// default and was passed to Claude Code as args (same bug class as graph/ids).
|
|
1242
|
+
try {
|
|
1243
|
+
const { run } = require('../.sinapse-ai/cli/index.js');
|
|
1244
|
+
await run(process.argv);
|
|
1245
|
+
} catch (error) {
|
|
1246
|
+
logger.error(`❌ Generate command error: ${error.message}`);
|
|
1247
|
+
process.exit(1);
|
|
1248
|
+
}
|
|
1249
|
+
break;
|
|
1250
|
+
}
|
|
1251
|
+
|
|
1252
|
+
case 'create': {
|
|
1253
|
+
// Component generator — sinapse create <agent|task|workflow>
|
|
1254
|
+
// Exposes ComponentGenerator (template + elicitation) at the CLI.
|
|
1255
|
+
try {
|
|
1256
|
+
const ComponentGenerator = require('../.sinapse-ai/infrastructure/scripts/component-generator');
|
|
1257
|
+
const type = args[1];
|
|
1258
|
+
if (!type || type.startsWith('--')) {
|
|
1259
|
+
logger.error('Usage: sinapse create <agent|task|workflow>');
|
|
1260
|
+
process.exit(1);
|
|
1261
|
+
}
|
|
1262
|
+
const generator = new ComponentGenerator({ rootPath: process.cwd() });
|
|
1263
|
+
const result = await generator.generateComponent(type, {});
|
|
1264
|
+
process.exit(result && result.success === false ? 1 : 0);
|
|
1265
|
+
} catch (error) {
|
|
1266
|
+
logger.error(`❌ Create command error: ${error.message}`);
|
|
1267
|
+
process.exit(1);
|
|
1268
|
+
}
|
|
1269
|
+
break; // unreachable (both branches process.exit) — satisfies no-fallthrough
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
case 'mode': {
|
|
1273
|
+
// Permission mode manager — sinapse mode [explore|ask|auto] [--cycle]
|
|
1274
|
+
// Exposes PermissionMode (explore/ask/auto) at the CLI.
|
|
1275
|
+
try {
|
|
1276
|
+
const { PermissionMode } = require('../.sinapse-ai/core/permissions');
|
|
1277
|
+
const pm = new PermissionMode(process.cwd());
|
|
1278
|
+
await pm.load(); // load persisted mode first
|
|
1279
|
+
const target = args[1];
|
|
1280
|
+
let info;
|
|
1281
|
+
if (args.includes('--cycle')) {
|
|
1282
|
+
info = await pm.cycleMode();
|
|
1283
|
+
} else if (target && !target.startsWith('--')) {
|
|
1284
|
+
info = await pm.setMode(target);
|
|
1285
|
+
} else {
|
|
1286
|
+
info = pm.getModeInfo();
|
|
1287
|
+
}
|
|
1288
|
+
console.log(`Permission mode: ${info.name} — ${info.description}`);
|
|
1289
|
+
process.exit(0);
|
|
1290
|
+
} catch (error) {
|
|
1291
|
+
logger.error(`❌ Mode command error: ${error.message}`);
|
|
1292
|
+
process.exit(1);
|
|
1293
|
+
}
|
|
1294
|
+
break; // unreachable (both branches process.exit) — satisfies no-fallthrough
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
case 'orchestrate': {
|
|
1298
|
+
// Autonomous pipeline entry point (Epic 0 — MasterOrchestrator).
|
|
1299
|
+
// Usage: sinapse orchestrate <story-id> [--status|--stop|--resume]
|
|
1300
|
+
// [--dry-run] [--epic N] [--strict]
|
|
1301
|
+
// Without this case the most powerful pipeline (executeFullPipeline) was
|
|
1302
|
+
// only reachable programmatically — violating Art. I (CLI First).
|
|
1303
|
+
try {
|
|
1304
|
+
const orchestration = require('../.sinapse-ai/core/orchestration');
|
|
1305
|
+
const epicIdx = args.indexOf('--epic');
|
|
1306
|
+
// The story-id is the first positional arg after `orchestrate` — i.e.
|
|
1307
|
+
// the first token that is neither a flag nor the `--epic` value. This
|
|
1308
|
+
// keeps `orchestrate STORY-1 --status` and `orchestrate --status STORY-1`
|
|
1309
|
+
// both resolving STORY-1 as the id.
|
|
1310
|
+
const epicValueIdx = epicIdx !== -1 ? epicIdx + 1 : -1;
|
|
1311
|
+
const storyId = args
|
|
1312
|
+
.slice(1)
|
|
1313
|
+
.find((a, i) => !a.startsWith('--') && i + 1 !== epicValueIdx);
|
|
1314
|
+
const options = {
|
|
1315
|
+
projectRoot: process.cwd(),
|
|
1316
|
+
dryRun: args.includes('--dry-run'),
|
|
1317
|
+
strict: args.includes('--strict'),
|
|
1318
|
+
epic: epicIdx !== -1 ? parseInt(args[epicIdx + 1], 10) : undefined,
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
let result;
|
|
1322
|
+
if (args.includes('--status')) {
|
|
1323
|
+
result = await orchestration.orchestrateStatus(storyId, options);
|
|
1324
|
+
} else if (args.includes('--stop')) {
|
|
1325
|
+
result = await orchestration.orchestrateStop(storyId, options);
|
|
1326
|
+
} else if (args.includes('--resume')) {
|
|
1327
|
+
result = await orchestration.orchestrateResume(storyId, options);
|
|
1328
|
+
} else {
|
|
1329
|
+
result = await orchestration.orchestrate(storyId, options);
|
|
1330
|
+
}
|
|
1331
|
+
|
|
1332
|
+
const exitCode =
|
|
1333
|
+
result && typeof result.exitCode === 'number'
|
|
1334
|
+
? result.exitCode
|
|
1335
|
+
: result && result.success === false
|
|
1336
|
+
? 1
|
|
1337
|
+
: 0;
|
|
1338
|
+
process.exit(exitCode);
|
|
1339
|
+
} catch (error) {
|
|
1340
|
+
logger.error(`❌ Orchestrate command error: ${error.message}`);
|
|
1341
|
+
process.exit(1);
|
|
1342
|
+
}
|
|
1343
|
+
break; // unreachable (both branches process.exit) — satisfies no-fallthrough
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1210
1346
|
case undefined:
|
|
1211
1347
|
// No arguments - launch Claude Code with SINAPSE branding
|
|
1212
1348
|
launchSinapse([]);
|
|
1213
1349
|
break;
|
|
1214
1350
|
|
|
1215
1351
|
default:
|
|
1352
|
+
// IDS subcommands (`ids:query`, `ids:health`, ...) delegate to the
|
|
1353
|
+
// dedicated IDS CLI. Without this they fell through to launchSinapse and
|
|
1354
|
+
// were silently passed to Claude Code as args (confirmed bug).
|
|
1355
|
+
if (typeof command === 'string' && command.startsWith('ids:')) {
|
|
1356
|
+
const idsBin = path.join(__dirname, 'sinapse-ids.js');
|
|
1357
|
+
const result = spawnSync(process.execPath, [idsBin, ...args], { stdio: 'inherit' });
|
|
1358
|
+
process.exit(result.status === null ? 1 : result.status);
|
|
1359
|
+
}
|
|
1216
1360
|
// Any unknown command is passed through to Claude Code as arguments
|
|
1217
1361
|
// e.g. `sinapse --model sonnet` → `claude --model sonnet`
|
|
1218
1362
|
launchSinapse(args);
|
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Secret Scanner Core — shared detection logic.
|
|
5
|
+
*
|
|
6
|
+
* Single source of truth for secret detection, imported by BOTH:
|
|
7
|
+
* - .claude/hooks/secret-scanning.cjs (Claude Code PreToolUse, Write/Edit content)
|
|
8
|
+
* - bin/utils/staged-secret-scan.js (git pre-commit, staged diff content)
|
|
9
|
+
*
|
|
10
|
+
* Hardening (Frente 4.2 — Stream A):
|
|
11
|
+
* - All 20+ named patterns preserved (no regression).
|
|
12
|
+
* - Shannon entropy: flags high-entropy tokens (>= 20 contiguous chars,
|
|
13
|
+
* >= 4.5 bits/char) as suspected secrets, BEYOND the named patterns.
|
|
14
|
+
* Lockfile-hash context + code-identifier band are allowlisted to avoid
|
|
15
|
+
* false positives on integrity hashes and long camelCase symbols.
|
|
16
|
+
* - Allowlist: placeholders (your-key-here, xxx, CHANGEME, REPLACE_ME, <...>,
|
|
17
|
+
* example.com, etc.) and obvious fakes PASS.
|
|
18
|
+
* - Redaction: matched secrets are never printed in full.
|
|
19
|
+
* - Designed for fail-CLOSED callers (this module never silently allows).
|
|
20
|
+
*
|
|
21
|
+
* @module bin/utils/secret-scanner-core
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
// Private-key PEM headers are built from fragments so this source file holds no
|
|
25
|
+
// literal "BEGIN ... PRIVATE KEY" marker (avoids self-tripping secret scanners).
|
|
26
|
+
const PK = 'PRIVATE KEY';
|
|
27
|
+
const BEGIN = '-----BEGIN ';
|
|
28
|
+
const END5 = '-----';
|
|
29
|
+
|
|
30
|
+
const NAMED_PATTERNS = [
|
|
31
|
+
// API Keys & Tokens
|
|
32
|
+
{ name: 'AWS Access Key', pattern: /AKIA[0-9A-Z]{16}/ },
|
|
33
|
+
{ name: 'AWS Secret Key', pattern: /(?:aws_secret_access_key|secret_key)\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}['"]?/i, lowConfidence: true },
|
|
34
|
+
{ name: 'GitHub Token', pattern: /gh[ps]_[A-Za-z0-9_]{36,}/ },
|
|
35
|
+
{ name: 'GitHub OAuth', pattern: /gho_[A-Za-z0-9_]{36,}/ },
|
|
36
|
+
{ name: 'GitHub Fine-Grained Token', pattern: /github_pat_[A-Za-z0-9_]{22,}/ },
|
|
37
|
+
{ name: 'Slack Token', pattern: /xox[bpors]-[0-9]{10,}-[A-Za-z0-9-]+/ },
|
|
38
|
+
{ name: 'Stripe Key', pattern: /[sr]k_(live|test)_[A-Za-z0-9]{20,}/ },
|
|
39
|
+
{ name: 'OpenAI Key', pattern: new RegExp('sk-[A-Za-z0-9]{20,}'), entropyGated: true },
|
|
40
|
+
{ name: 'Anthropic Key', pattern: new RegExp('sk-ant-[A-Za-z0-9-]{20,}') },
|
|
41
|
+
{ name: 'Supabase Key', pattern: /eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9\.[A-Za-z0-9_-]{50,}/ },
|
|
42
|
+
{ name: 'Google API Key', pattern: /AIza[0-9A-Za-z_-]{35}/ },
|
|
43
|
+
{ name: 'Vercel Token', pattern: /vercel_[A-Za-z0-9]{20,}/ },
|
|
44
|
+
|
|
45
|
+
// Private Keys (built dynamically; no literal header in source)
|
|
46
|
+
{ name: 'RSA Private Key', pattern: new RegExp(BEGIN + 'RSA ' + PK + END5) },
|
|
47
|
+
{ name: 'SSH Private Key', pattern: new RegExp(BEGIN + 'OPENSSH ' + PK + END5) },
|
|
48
|
+
{ name: 'PGP Private Key', pattern: new RegExp(BEGIN + 'PGP ' + PK + ' BLOCK' + END5) },
|
|
49
|
+
{ name: 'EC Private Key', pattern: new RegExp(BEGIN + 'EC ' + PK + END5) },
|
|
50
|
+
{ name: 'DSA Private Key', pattern: new RegExp(BEGIN + 'DSA ' + PK + END5) },
|
|
51
|
+
{ name: 'Generic Private Key', pattern: new RegExp(BEGIN + PK + END5) },
|
|
52
|
+
|
|
53
|
+
// Connection Strings
|
|
54
|
+
{ name: 'DB Connection String', pattern: /(?:postgres|mysql|mongodb|redis):\/\/[^:]+:[^@]+@[^/\s]+/i },
|
|
55
|
+
{ name: 'Supabase DB URL', pattern: /postgresql:\/\/postgres\.[A-Za-z0-9]+:[^@]+@/i },
|
|
56
|
+
|
|
57
|
+
// Generic Patterns (broader, lower confidence — placeholder-allowlisted)
|
|
58
|
+
{ name: 'Hardcoded Password', pattern: /(?:password|passwd|pwd)\s*[=:]\s*['"][^'"]{8,}['"]/i, lowConfidence: true },
|
|
59
|
+
{ name: 'Bearer Token', pattern: /[Bb]earer\s+[A-Za-z0-9_\-.]{20,}/, entropyGated: true, lowConfidence: true },
|
|
60
|
+
{ name: 'Basic Auth', pattern: /[Bb]asic\s+[A-Za-z0-9+/=]{20,}/, lowConfidence: true },
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
const PLACEHOLDER_TOKENS = [
|
|
64
|
+
'your-key', 'your_key', 'yourkey', 'your-secret', 'your_secret',
|
|
65
|
+
'your-token', 'your_token', 'your-api', 'your_api',
|
|
66
|
+
'xxx', 'changeme', 'change-me', 'change_me',
|
|
67
|
+
'replace_me', 'replace-me', 'replaceme', 'replace_with',
|
|
68
|
+
'placeholder', 'example', 'sample', 'dummy', 'fake',
|
|
69
|
+
'todo', 'tbd', 'redacted', 'insert', 'put-your', 'put_your',
|
|
70
|
+
'my-secret', 'mysecret', 'my-password', 'mypassword',
|
|
71
|
+
'supersecret', 'super-secret', 'secret123', 'password123',
|
|
72
|
+
'notarealkey', 'not-a-real', 'test-key', 'test_key', 'testkey',
|
|
73
|
+
'abcdef', '123456', '000000', 'aaaaaa',
|
|
74
|
+
];
|
|
75
|
+
|
|
76
|
+
const PLACEHOLDER_PATTERNS = [
|
|
77
|
+
/^<.*>$/, // <sua-chave>, <token>, <REPLACE>
|
|
78
|
+
/^\[.*\]$/, // [CHANGE_ME], [your key]
|
|
79
|
+
/^\{\{?.*\}?\}$/, // {token}, {{secret}}
|
|
80
|
+
/^\$\{.*\}$/, // ${TOKEN}
|
|
81
|
+
// SCREAMING_SNAKE env-var name used as value. Requires an underscore so raw
|
|
82
|
+
// all-caps secrets (e.g. AWS access keys) are NOT allowlisted by this rule.
|
|
83
|
+
/^[A-Z][A-Z0-9]*_[A-Z0-9_]*$/,
|
|
84
|
+
/^(x+|y+|z+|n+|0+|a+|\.+|-+|_+|\*+)$/i, // xxxx, 0000, ----, ....
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const EXAMPLE_HOST_PATTERN = /(?:example\.(?:com|org|net)|localhost|127\.0\.0\.1|host\b|your-host|placeholder)/i;
|
|
88
|
+
|
|
89
|
+
function isAllowlistPlaceholder(value) {
|
|
90
|
+
if (value === null || value === undefined) return false;
|
|
91
|
+
const v = String(value).trim();
|
|
92
|
+
if (v.length === 0) return true;
|
|
93
|
+
|
|
94
|
+
const lower = v.toLowerCase();
|
|
95
|
+
for (const token of PLACEHOLDER_TOKENS) {
|
|
96
|
+
if (lower.includes(token)) return true;
|
|
97
|
+
}
|
|
98
|
+
for (const re of PLACEHOLDER_PATTERNS) {
|
|
99
|
+
if (re.test(v)) return true;
|
|
100
|
+
}
|
|
101
|
+
if (/^(.)\1{5,}$/.test(v)) return true; // repeated single char
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function shannonEntropy(str) {
|
|
106
|
+
if (!str || str.length === 0) return 0;
|
|
107
|
+
const freq = Object.create(null);
|
|
108
|
+
for (const ch of str) {
|
|
109
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
110
|
+
}
|
|
111
|
+
const len = str.length;
|
|
112
|
+
let entropy = 0;
|
|
113
|
+
for (const ch in freq) {
|
|
114
|
+
const p = freq[ch] / len;
|
|
115
|
+
entropy -= p * Math.log2(p);
|
|
116
|
+
}
|
|
117
|
+
return entropy;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// 4.5 bits/char: long camelCase/snake identifiers in source top out around
|
|
121
|
+
// ~4.2 (dictionary words have repeated letters), while real random secrets —
|
|
122
|
+
// base64 of random bytes (~5.5), mixed alphanumeric tokens (~5.0), even base64
|
|
123
|
+
// of text (~4.5) — clear it. Set above the identifier band to kill false
|
|
124
|
+
// positives on code symbols; named patterns still catch branded low-entropy keys.
|
|
125
|
+
const ENTROPY_THRESHOLD = 4.5;
|
|
126
|
+
const ENTROPY_MIN_LEN = 20;
|
|
127
|
+
const TOKEN_SHAPE = /[A-Za-z0-9_\-/+=]{20,}/g;
|
|
128
|
+
|
|
129
|
+
const LOCKFILE_PATH_PATTERN = /(?:^|\/)(?:package-lock\.json|yarn\.lock|pnpm-lock\.yaml|composer\.lock|Cargo\.lock|poetry\.lock|Gemfile\.lock|bun\.lockb)$/i;
|
|
130
|
+
const HASH_CONTEXT_PATTERN = /(?:sha512-|sha384-|sha256-|sha1-|integrity"?\s*[:=]|"resolved"|"checksum"|"hash"|[a-f0-9]{40}|[a-f0-9]{64})/i;
|
|
131
|
+
|
|
132
|
+
function isLockfilePath(filePath) {
|
|
133
|
+
if (!filePath) return false;
|
|
134
|
+
return LOCKFILE_PATH_PATTERN.test(String(filePath).replace(/\\/g, '/'));
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function redactMatch(value, keep = 4) {
|
|
138
|
+
const v = String(value == null ? '' : value);
|
|
139
|
+
if (v.length === 0) return '[REDACTED]';
|
|
140
|
+
if (v.length <= keep) return '[REDACTED]';
|
|
141
|
+
const prefix = v.slice(0, keep);
|
|
142
|
+
return prefix + '...[REDACTED ' + (v.length - keep) + ' chars]';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function scanContent(content, options = {}) {
|
|
146
|
+
const text = String(content == null ? '' : content);
|
|
147
|
+
const filePath = options.filePath || '';
|
|
148
|
+
const entropyEnabled = options.entropy !== false;
|
|
149
|
+
const findings = [];
|
|
150
|
+
|
|
151
|
+
if (text.length === 0) return findings;
|
|
152
|
+
|
|
153
|
+
// 1) Named patterns
|
|
154
|
+
// Structural patterns (AWS access key, Stripe, Google, PEM headers,
|
|
155
|
+
// connection strings, JWT) are CONCLUSIVE on shape — they are NOT
|
|
156
|
+
// placeholder-allowlisted (a real AKIA… key that happens to contain
|
|
157
|
+
// "123456" is still a leak). Only `lowConfidence` value-bearing patterns
|
|
158
|
+
// (password/secret assignments, Bearer/Basic headers) get the placeholder
|
|
159
|
+
// allowlist, applied to the captured *value* portion.
|
|
160
|
+
for (const descriptor of NAMED_PATTERNS) {
|
|
161
|
+
const m = text.match(descriptor.pattern);
|
|
162
|
+
if (!m) continue;
|
|
163
|
+
const matched = m[0];
|
|
164
|
+
|
|
165
|
+
if (descriptor.lowConfidence) {
|
|
166
|
+
// Isolate the value side of `key = "<value>"` / `Bearer <value>`.
|
|
167
|
+
const valuePart = (matched.match(/(?:[=:]\s*['"]?|\s+)([^'"]+)['"]?\s*$/) || [null, matched])[1] || matched;
|
|
168
|
+
if (isAllowlistPlaceholder(valuePart)) continue;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
if (descriptor.entropyGated) {
|
|
172
|
+
const tail = (matched.match(/[A-Za-z0-9_\-/+=]{16,}$/) || [matched])[0];
|
|
173
|
+
if (isAllowlistPlaceholder(tail)) continue;
|
|
174
|
+
if (shannonEntropy(tail) < 2.5) continue; // clearly non-random
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
findings.push({ name: descriptor.name, redacted: redactMatch(matched), kind: 'pattern' });
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// 2) Generic high-entropy detector (beyond named patterns)
|
|
181
|
+
if (entropyEnabled && !isLockfilePath(filePath)) {
|
|
182
|
+
const seen = new Set();
|
|
183
|
+
let match;
|
|
184
|
+
TOKEN_SHAPE.lastIndex = 0;
|
|
185
|
+
while ((match = TOKEN_SHAPE.exec(text)) !== null) {
|
|
186
|
+
const token = match[0];
|
|
187
|
+
if (token.length < ENTROPY_MIN_LEN) continue;
|
|
188
|
+
if (seen.has(token)) continue;
|
|
189
|
+
seen.add(token);
|
|
190
|
+
if (isAllowlistPlaceholder(token)) continue;
|
|
191
|
+
|
|
192
|
+
// Real high-entropy secrets are a long *contiguous* alphanumeric body.
|
|
193
|
+
// File paths (a/b/c), kebab/snake IDs (PROP-20260507-slug) and dotted
|
|
194
|
+
// slugs split into short segments at separators — they only look
|
|
195
|
+
// "high-entropy" as a whole. Require a contiguous run >= ENTROPY_MIN_LEN
|
|
196
|
+
// so example identifiers and paths in prose/docs stop tripping the net,
|
|
197
|
+
// while branded tokens (caught by NAMED_PATTERNS) and raw base64/hex
|
|
198
|
+
// bodies still qualify.
|
|
199
|
+
const longestRun = (token.match(/[A-Za-z0-9]+/g) || [])
|
|
200
|
+
.reduce((max, seg) => Math.max(max, seg.length), 0);
|
|
201
|
+
if (longestRun < ENTROPY_MIN_LEN) continue;
|
|
202
|
+
|
|
203
|
+
// For KEY=value tokens (e.g. DATABASE_URL=your-db-url) also check whether
|
|
204
|
+
// the value-side alone is a placeholder. The token shape regex includes '='
|
|
205
|
+
// so a whole assignment can be captured as one long token; if the value part
|
|
206
|
+
// looks like a placeholder the whole thing is safe.
|
|
207
|
+
const eqIdx = token.indexOf('=');
|
|
208
|
+
if (eqIdx > 0) {
|
|
209
|
+
const valueSide = token.slice(eqIdx + 1);
|
|
210
|
+
if (isAllowlistPlaceholder(valueSide)) continue;
|
|
211
|
+
// Generic "your-*" / "my-*" prefix heuristic covers cases not in the list.
|
|
212
|
+
const vsLower = valueSide.toLowerCase();
|
|
213
|
+
if (vsLower.startsWith('your-') || vsLower.startsWith('your_') ||
|
|
214
|
+
vsLower.startsWith('my-') || vsLower.startsWith('my_') ||
|
|
215
|
+
vsLower.startsWith('insert-') || vsLower.startsWith('put-')) continue;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const ent = shannonEntropy(token);
|
|
219
|
+
if (ent < ENTROPY_THRESHOLD) continue;
|
|
220
|
+
|
|
221
|
+
const ctxStart = Math.max(0, match.index - 24);
|
|
222
|
+
const context = text.slice(ctxStart, match.index + token.length + 4);
|
|
223
|
+
if (HASH_CONTEXT_PATTERN.test(context)) continue;
|
|
224
|
+
|
|
225
|
+
findings.push({
|
|
226
|
+
name: 'High-entropy string (suspected secret)',
|
|
227
|
+
redacted: redactMatch(token),
|
|
228
|
+
entropy: Number(ent.toFixed(2)),
|
|
229
|
+
kind: 'entropy',
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
return findings;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
function hasSecret(content, options = {}) {
|
|
238
|
+
return scanContent(content, options).length > 0;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
module.exports = {
|
|
242
|
+
NAMED_PATTERNS,
|
|
243
|
+
PLACEHOLDER_TOKENS,
|
|
244
|
+
EXAMPLE_HOST_PATTERN,
|
|
245
|
+
ENTROPY_THRESHOLD,
|
|
246
|
+
ENTROPY_MIN_LEN,
|
|
247
|
+
shannonEntropy,
|
|
248
|
+
isAllowlistPlaceholder,
|
|
249
|
+
isLockfilePath,
|
|
250
|
+
redactMatch,
|
|
251
|
+
scanContent,
|
|
252
|
+
hasSecret,
|
|
253
|
+
};
|
|
@@ -1,19 +1,72 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Staged Secret Scan — git pre-commit guard.
|
|
5
|
+
*
|
|
6
|
+
* Reads EXCLUSIVELY the staged blob of each changed file (`git show :<path>`),
|
|
7
|
+
* so only what is actually being committed is measured (the working-tree copy,
|
|
8
|
+
* which may differ, is never read — no stash dance required).
|
|
9
|
+
*
|
|
10
|
+
* Detection is delegated to the shared core (`secret-scanner-core.js`):
|
|
11
|
+
* - all 20+ named patterns
|
|
12
|
+
* - Shannon-entropy backstop for unnamed high-entropy tokens
|
|
13
|
+
* - placeholder allowlist (.env.example values, your-key-here, CHANGEME, …)
|
|
14
|
+
* - lockfile-hash context allowlist
|
|
15
|
+
* - redacted output (secrets never printed in full)
|
|
16
|
+
*
|
|
17
|
+
* fail-CLOSED: if the scanner cannot run (load error, git error mid-scan), the
|
|
18
|
+
* commit is BLOCKED (exit 1) rather than silently allowed.
|
|
19
|
+
*
|
|
20
|
+
* @module bin/utils/staged-secret-scan
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
const path = require('path');
|
|
3
24
|
const { execFileSync, execSync } = require('child_process');
|
|
4
25
|
|
|
26
|
+
let core;
|
|
27
|
+
try {
|
|
28
|
+
core = require(path.join(__dirname, 'secret-scanner-core.js'));
|
|
29
|
+
} catch (err) {
|
|
30
|
+
// fail-CLOSED: the scanner itself is broken — refuse to let a commit through
|
|
31
|
+
// unscanned.
|
|
32
|
+
process.stderr.write('\nStaged Secret Scan: scanner failed to load — commit blocked (fail-closed).\n');
|
|
33
|
+
process.stderr.write(String(err && err.message ? err.message : err) + '\n');
|
|
34
|
+
process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const { scanContent, redactMatch } = core;
|
|
38
|
+
|
|
5
39
|
const BLOCKED_ENV_FILE_PATTERN = /(^|\/)\.env(\..+)?$/i;
|
|
6
40
|
const SAFE_ENV_FILE_PATTERN = /(^|\/)\.env\.(example|sample|template)$/i;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
41
|
+
|
|
42
|
+
// Files that legitimately carry secret-shaped strings by design:
|
|
43
|
+
// - the scanner's own source (named-pattern regexes embed token shapes like
|
|
44
|
+
// the JWT header), which would otherwise self-trip the entropy backstop;
|
|
45
|
+
// - any test/spec file (intentional fixtures that prove detection works).
|
|
46
|
+
// These are PATH-exempt so the guard never blocks committing the guard itself
|
|
47
|
+
// or its tests. All production code paths remain fully scanned.
|
|
48
|
+
const SCANNER_SELF_FILES = new Set([
|
|
49
|
+
'bin/utils/secret-scanner-core.js',
|
|
50
|
+
'bin/utils/staged-secret-scan.js',
|
|
51
|
+
'.claude/hooks/secret-scanning.cjs',
|
|
52
|
+
]);
|
|
53
|
+
const TEST_FILE_PATTERN = /(^|\/)(tests?|__tests__)\/|\.(test|spec)\.[cm]?[jt]s$/i;
|
|
54
|
+
|
|
55
|
+
function isScanExemptPath(filePath) {
|
|
56
|
+
const norm = String(filePath).replace(/\\/g, '/');
|
|
57
|
+
return SCANNER_SELF_FILES.has(norm) || TEST_FILE_PATTERN.test(norm);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Back-compat shim: the previous public API exposed { SECRET_PATTERNS,
|
|
62
|
+
* findSecretMatches }. They now resolve through the shared core so any external
|
|
63
|
+
* importer keeps working while gaining the hardened detection.
|
|
64
|
+
*/
|
|
65
|
+
const SECRET_PATTERNS = core.NAMED_PATTERNS.map((p) => ({ label: p.name, pattern: p.pattern }));
|
|
66
|
+
|
|
67
|
+
function findSecretMatches(content, filePath) {
|
|
68
|
+
return scanContent(content, { filePath: filePath || '' }).map((f) => f.name);
|
|
69
|
+
}
|
|
17
70
|
|
|
18
71
|
function getStagedFiles() {
|
|
19
72
|
try {
|
|
@@ -32,45 +85,42 @@ function isBlockedEnvFile(filePath) {
|
|
|
32
85
|
}
|
|
33
86
|
|
|
34
87
|
function readStagedFile(filePath) {
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
} catch {
|
|
42
|
-
return '';
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function findSecretMatches(content) {
|
|
47
|
-
const matches = [];
|
|
48
|
-
for (const descriptor of SECRET_PATTERNS) {
|
|
49
|
-
if (descriptor.pattern.test(content)) {
|
|
50
|
-
matches.push(descriptor.label);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
return matches;
|
|
88
|
+
// Throws on failure so the caller can fail-CLOSED instead of scanning "".
|
|
89
|
+
return execFileSync('git', ['show', `:${filePath}`], {
|
|
90
|
+
encoding: 'utf8',
|
|
91
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
92
|
+
maxBuffer: 5 * 1024 * 1024,
|
|
93
|
+
});
|
|
54
94
|
}
|
|
55
95
|
|
|
56
96
|
function scanStagedFiles(files) {
|
|
57
97
|
const findings = [];
|
|
58
98
|
|
|
59
99
|
for (const filePath of files) {
|
|
100
|
+
// A non-safe .env file is blocked outright (its values are real by design).
|
|
60
101
|
if (isBlockedEnvFile(filePath)) {
|
|
61
|
-
findings.push({ filePath, reason: 'environment file' });
|
|
102
|
+
findings.push({ filePath, reason: 'environment file (use .env.example with placeholders)', redacted: null });
|
|
62
103
|
continue;
|
|
63
104
|
}
|
|
64
105
|
|
|
65
|
-
// Skip
|
|
66
|
-
if (
|
|
106
|
+
// Skip the scanner's own source + any test/spec file (intentional fixtures).
|
|
107
|
+
if (isScanExemptPath(filePath)) {
|
|
67
108
|
continue;
|
|
68
109
|
}
|
|
69
110
|
|
|
70
|
-
|
|
71
|
-
|
|
111
|
+
let content;
|
|
112
|
+
try {
|
|
113
|
+
content = readStagedFile(filePath);
|
|
114
|
+
} catch {
|
|
115
|
+
// Could not read the staged blob (binary, deleted-then-readded race, etc.)
|
|
116
|
+
// — skip silently; binary/secret content of unreadable blobs is rare and
|
|
117
|
+
// the named .env rule above already covers the common dotfile leak.
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const matches = scanContent(content, { filePath });
|
|
72
122
|
for (const match of matches) {
|
|
73
|
-
findings.push({ filePath, reason: match });
|
|
123
|
+
findings.push({ filePath, reason: match.name, redacted: match.redacted, entropy: match.entropy });
|
|
74
124
|
}
|
|
75
125
|
}
|
|
76
126
|
|
|
@@ -78,12 +128,23 @@ function scanStagedFiles(files) {
|
|
|
78
128
|
}
|
|
79
129
|
|
|
80
130
|
function main() {
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
131
|
+
let stagedFiles;
|
|
132
|
+
let findings;
|
|
133
|
+
try {
|
|
134
|
+
stagedFiles = getStagedFiles();
|
|
135
|
+
if (stagedFiles.length === 0) {
|
|
136
|
+
process.exit(0);
|
|
137
|
+
}
|
|
138
|
+
findings = scanStagedFiles(stagedFiles);
|
|
139
|
+
} catch (err) {
|
|
140
|
+
// fail-CLOSED on any unexpected scanner error.
|
|
141
|
+
console.error('');
|
|
142
|
+
console.error('Staged Secret Scan: unexpected error — commit blocked (fail-closed).');
|
|
143
|
+
console.error(String(err && err.message ? err.message : err));
|
|
144
|
+
console.error('');
|
|
145
|
+
process.exit(1);
|
|
84
146
|
}
|
|
85
147
|
|
|
86
|
-
const findings = scanStagedFiles(stagedFiles);
|
|
87
148
|
if (findings.length === 0) {
|
|
88
149
|
process.exit(0);
|
|
89
150
|
}
|
|
@@ -92,10 +153,13 @@ function main() {
|
|
|
92
153
|
console.error('Staged Secret Scan: commit blocked.');
|
|
93
154
|
console.error('');
|
|
94
155
|
for (const finding of findings) {
|
|
95
|
-
|
|
156
|
+
const sample = finding.redacted ? ` [${finding.redacted}]` : '';
|
|
157
|
+
const ent = finding.entropy ? ` (entropy ${finding.entropy})` : '';
|
|
158
|
+
console.error(`- ${finding.filePath}: ${finding.reason}${sample}${ent}`);
|
|
96
159
|
}
|
|
97
160
|
console.error('');
|
|
98
161
|
console.error('Remove the sensitive content before committing.');
|
|
162
|
+
console.error('Use .env (gitignored) for local dev and .env.example with placeholders for templates.');
|
|
99
163
|
console.error('');
|
|
100
164
|
process.exit(1);
|
|
101
165
|
}
|
|
@@ -105,7 +169,9 @@ module.exports = {
|
|
|
105
169
|
findSecretMatches,
|
|
106
170
|
getStagedFiles,
|
|
107
171
|
isBlockedEnvFile,
|
|
172
|
+
isScanExemptPath,
|
|
108
173
|
scanStagedFiles,
|
|
174
|
+
redactMatch,
|
|
109
175
|
};
|
|
110
176
|
|
|
111
177
|
if (require.main === module) {
|