fullcourtdefense-cli 1.14.14 → 1.14.16
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/dist/commands/discover.js +50 -26
- package/dist/commands/discoverAgentFiles.d.ts +1 -0
- package/dist/commands/discoverAgentFiles.js +70 -24
- package/dist/commands/discoverSchedule.d.ts +2 -0
- package/dist/commands/discoverSchedule.js +18 -14
- package/dist/commands/discoverSecrets.d.ts +2 -0
- package/dist/commands/discoverSecrets.js +21 -2
- package/dist/version.json +1 -1
- package/package.json +1 -1
|
@@ -122,6 +122,20 @@ function buildHostMetadata(userEmail, probeMode = 'config') {
|
|
|
122
122
|
function candidateConfigPaths(cwd, extra) {
|
|
123
123
|
return (0, discoverPaths_1.discoverScanTargets)(cwd, extra);
|
|
124
124
|
}
|
|
125
|
+
/** Dot-dirs that hold a client's project config; the real project root is their parent. */
|
|
126
|
+
const CONFIG_DOT_DIRS = new Set(['.cursor', '.claude', '.codex', '.vscode', '.gemini', '.kiro']);
|
|
127
|
+
/** Map discovered project config file paths back to their owning project folders. */
|
|
128
|
+
function deriveProjectRoots(candidates) {
|
|
129
|
+
const roots = new Set();
|
|
130
|
+
for (const candidate of candidates) {
|
|
131
|
+
let dir = path.dirname(candidate.path);
|
|
132
|
+
if (CONFIG_DOT_DIRS.has(path.basename(dir).toLowerCase())) {
|
|
133
|
+
dir = path.dirname(dir);
|
|
134
|
+
}
|
|
135
|
+
roots.add(path.resolve(dir));
|
|
136
|
+
}
|
|
137
|
+
return [...roots];
|
|
138
|
+
}
|
|
125
139
|
/** MCP servers can be declared under several keys across clients. */
|
|
126
140
|
function extractServerMap(parsed) {
|
|
127
141
|
if (!parsed || typeof parsed !== 'object')
|
|
@@ -300,7 +314,7 @@ function pushDiscoveredServer(out, name, cfgRaw, source, configPath) {
|
|
|
300
314
|
function parseConfigFile(filePath, source) {
|
|
301
315
|
let raw;
|
|
302
316
|
try {
|
|
303
|
-
raw = fs.readFileSync(filePath, 'utf-8');
|
|
317
|
+
raw = fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '');
|
|
304
318
|
}
|
|
305
319
|
catch {
|
|
306
320
|
return [];
|
|
@@ -758,16 +772,23 @@ async function discoverCommand(args, config) {
|
|
|
758
772
|
}
|
|
759
773
|
if (args.schedule === 'daily' || args.schedule === 'logon') {
|
|
760
774
|
const hour = args.scheduleHour ? Number.parseInt(args.scheduleHour, 10) : 9;
|
|
761
|
-
|
|
775
|
+
// Scheduled runs default to the FULL posture (mcp + secrets + agent-files +
|
|
776
|
+
// blast radius) unless the user explicitly narrowed the surface — a daily
|
|
777
|
+
// job should cover everything, not just MCP configs.
|
|
778
|
+
const explicitSurface = Boolean(args.surface || args.type);
|
|
779
|
+
const surface = !explicitSurface ? 'all' : (surfaces.size === 4 ? 'all' : Array.from(surfaces).join(','));
|
|
762
780
|
(0, discoverSchedule_1.installDailyDiscoverSchedule)({
|
|
763
781
|
userEmail: args.userEmail,
|
|
764
782
|
hour: Number.isFinite(hour) ? hour : 9,
|
|
765
783
|
surface,
|
|
784
|
+
scanRoot: args.scanRoot,
|
|
785
|
+
deepRoots: args.deepRoots === 'true',
|
|
766
786
|
trigger: args.schedule,
|
|
767
787
|
});
|
|
768
788
|
if (!silent) {
|
|
769
789
|
const when = args.schedule === 'logon' ? 'at logon' : `daily at ${String(Number.isFinite(hour) ? hour : 9).padStart(2, '0')}:00`;
|
|
770
|
-
|
|
790
|
+
const rootNote = args.scanRoot ? ` --scan-root ${args.scanRoot}` : '';
|
|
791
|
+
console.log(`Desktop discovery scheduled ${when} (runs discover --surface ${surface} --upload --deep --silent${rootNote}${args.userEmail ? ` --user-email ${args.userEmail}` : ''}).`);
|
|
771
792
|
console.log('Uses login Shield credentials or an explicit org API key.');
|
|
772
793
|
}
|
|
773
794
|
return;
|
|
@@ -777,29 +798,32 @@ async function discoverCommand(args, config) {
|
|
|
777
798
|
const scanned = [];
|
|
778
799
|
let found = [];
|
|
779
800
|
let clientCoverage = [];
|
|
801
|
+
// Optional --scan-root sweep: find project-scoped MCP configs in OTHER repos
|
|
802
|
+
// under the given dev roots (bounded walk — see scanRootsForProjectConfigs).
|
|
803
|
+
// Computed once and shared across MCP, secrets, and agent-file scans so the
|
|
804
|
+
// whole posture references every mapped folder. Without the flag it is empty
|
|
805
|
+
// and behavior is exactly the known-locations scan.
|
|
806
|
+
const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
|
|
807
|
+
const sweep = scanRoots.length > 0 ? (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots) : { candidates: [], scannedDirs: 0 };
|
|
808
|
+
const sweepProjectRoots = deriveProjectRoots(sweep.candidates);
|
|
809
|
+
const knownConfigCandidates = candidateConfigPaths(cwd, args.extraPath);
|
|
810
|
+
const secretConfigFiles = [...knownConfigCandidates, ...sweep.candidates]
|
|
811
|
+
.map(candidate => path.resolve(candidate.path))
|
|
812
|
+
.filter(file => fs.existsSync(file));
|
|
813
|
+
if (scanRoots.length > 0 && !silent) {
|
|
814
|
+
console.log(`${COLOR.gray}Scan-root sweep: checked ${sweep.scannedDirs} folder(s) under ${scanRoots.length} root(s) — found ${sweep.candidates.length} project config(s) across ${sweepProjectRoots.length} project folder(s).${COLOR.reset}`);
|
|
815
|
+
}
|
|
780
816
|
const scanRootConfigPaths = new Set();
|
|
781
817
|
if (runMcp) {
|
|
782
|
-
const candidates =
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
for (const candidate of sweep.candidates) {
|
|
792
|
-
const key = path.resolve(candidate.path).toLowerCase();
|
|
793
|
-
if (known.has(key))
|
|
794
|
-
continue;
|
|
795
|
-
known.add(key);
|
|
796
|
-
candidates.push(candidate);
|
|
797
|
-
scanRootConfigPaths.add(key);
|
|
798
|
-
added += 1;
|
|
799
|
-
}
|
|
800
|
-
if (!silent) {
|
|
801
|
-
console.log(`${COLOR.gray}Scan-root sweep: checked ${sweep.scannedDirs} folder(s) under ${scanRoots.length} root(s) — found ${added} additional project config(s).${COLOR.reset}`);
|
|
802
|
-
}
|
|
818
|
+
const candidates = [...knownConfigCandidates];
|
|
819
|
+
const known = new Set(candidates.map(c => path.resolve(c.path).toLowerCase()));
|
|
820
|
+
for (const candidate of sweep.candidates) {
|
|
821
|
+
const key = path.resolve(candidate.path).toLowerCase();
|
|
822
|
+
if (known.has(key))
|
|
823
|
+
continue;
|
|
824
|
+
known.add(key);
|
|
825
|
+
candidates.push(candidate);
|
|
826
|
+
scanRootConfigPaths.add(key);
|
|
803
827
|
}
|
|
804
828
|
// Self-heal BEFORE parsing: configs written by old CLI versions can carry a stale
|
|
805
829
|
// generic `--agent-name` that breaks dashboard policy matching. Fix them in place so
|
|
@@ -829,10 +853,10 @@ async function discoverCommand(args, config) {
|
|
|
829
853
|
}
|
|
830
854
|
}
|
|
831
855
|
const secrets = surfaces.has('secrets') || surfaces.has('posture')
|
|
832
|
-
? (0, discoverSecrets_1.scanSecrets)({ cwd })
|
|
856
|
+
? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, configFiles: secretConfigFiles })
|
|
833
857
|
: undefined;
|
|
834
858
|
const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
|
|
835
|
-
? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd })
|
|
859
|
+
? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
|
|
836
860
|
: undefined;
|
|
837
861
|
const posture = surfaces.has('posture')
|
|
838
862
|
? (0, discoverBlastRadius_1.computeBlastRadius)({
|
|
@@ -182,27 +182,67 @@ function scanNamedFiles(roots, names, kind, titleFor, out, seen) {
|
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
}
|
|
185
|
+
/** Scan a single project root for agent instruction/rule/skill/hook files. */
|
|
186
|
+
function scanProjectRootAgentFiles(root, clientLabel, out, seen) {
|
|
187
|
+
scanNamedFiles([{ root, client: clientLabel }], [
|
|
188
|
+
'AGENTS.md',
|
|
189
|
+
'CLAUDE.md',
|
|
190
|
+
'GEMINI.md',
|
|
191
|
+
'.cursorrules',
|
|
192
|
+
'.windsurfrules',
|
|
193
|
+
], 'instructions', name => `Agent instructions (${name})`, out, seen);
|
|
194
|
+
scanNamedFiles([{ root: path.join(root, '.github'), client: 'github' }], [
|
|
195
|
+
'copilot-instructions.md',
|
|
196
|
+
], 'instructions', () => 'GitHub Copilot instructions', out, seen);
|
|
197
|
+
for (const base of [
|
|
198
|
+
{ dir: path.join(root, '.cursor', 'rules'), client: `Cursor (${clientLabel})`, kind: 'rules' },
|
|
199
|
+
{ dir: path.join(root, '.agents', 'skills'), client: `Agents (${clientLabel})`, kind: 'skills' },
|
|
200
|
+
]) {
|
|
201
|
+
if (!fs.existsSync(base.dir))
|
|
202
|
+
continue;
|
|
203
|
+
walkDir(base.dir, out, seen, (filePath, name) => name.endsWith('.md') || name.endsWith('.mdc') || name === 'SKILL.md', filePath => ({
|
|
204
|
+
kind: base.kind,
|
|
205
|
+
client: base.client,
|
|
206
|
+
filePath,
|
|
207
|
+
title: path.basename(filePath),
|
|
208
|
+
sizeBytes: 0,
|
|
209
|
+
lineCount: 0,
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
const hooksPath = path.join(root, '.cursor', 'hooks.json');
|
|
213
|
+
if (fs.existsSync(hooksPath)) {
|
|
214
|
+
const parsed = readAgentFile(hooksPath);
|
|
215
|
+
if (parsed) {
|
|
216
|
+
pushFinding(out, seen, {
|
|
217
|
+
kind: 'hooks',
|
|
218
|
+
client: 'Cursor',
|
|
219
|
+
filePath: hooksPath,
|
|
220
|
+
title: 'Cursor hooks config',
|
|
221
|
+
sizeBytes: parsed.sizeBytes,
|
|
222
|
+
lineCount: parsed.lineCount,
|
|
223
|
+
content: parsed.content,
|
|
224
|
+
riskTags: parsed.content.includes('fullcourtdefense') ? [] : ['no_runtime_hook'],
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
185
229
|
function scanAgentFiles(options = {}) {
|
|
186
230
|
const home = os.homedir();
|
|
187
231
|
const cwd = options.cwd || process.cwd();
|
|
188
232
|
const out = [];
|
|
189
233
|
const seen = new Set();
|
|
190
|
-
|
|
191
|
-
|
|
234
|
+
// Current project + user-global locations.
|
|
235
|
+
scanProjectRootAgentFiles(cwd, 'project', out, seen);
|
|
236
|
+
scanNamedFiles([{ root: home, client: 'user' }], [
|
|
192
237
|
'AGENTS.md',
|
|
193
238
|
'CLAUDE.md',
|
|
194
239
|
'GEMINI.md',
|
|
195
240
|
'.cursorrules',
|
|
196
241
|
'.windsurfrules',
|
|
197
242
|
], 'instructions', name => `Agent instructions (${name})`, out, seen);
|
|
198
|
-
scanNamedFiles([{ root: path.join(cwd, '.github'), client: 'github' }], [
|
|
199
|
-
'copilot-instructions.md',
|
|
200
|
-
], 'instructions', () => 'GitHub Copilot instructions', out, seen);
|
|
201
243
|
for (const base of [
|
|
202
244
|
{ dir: path.join(home, '.cursor', 'rules'), client: 'Cursor', kind: 'rules' },
|
|
203
|
-
{ dir: path.join(cwd, '.cursor', 'rules'), client: 'Cursor (project)', kind: 'rules' },
|
|
204
245
|
{ dir: path.join(home, '.agents', 'skills'), client: 'Agents', kind: 'skills' },
|
|
205
|
-
{ dir: path.join(cwd, '.agents', 'skills'), client: 'Agents (project)', kind: 'skills' },
|
|
206
246
|
]) {
|
|
207
247
|
if (!fs.existsSync(base.dir))
|
|
208
248
|
continue;
|
|
@@ -215,25 +255,31 @@ function scanAgentFiles(options = {}) {
|
|
|
215
255
|
lineCount: 0,
|
|
216
256
|
}));
|
|
217
257
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
258
|
+
const homeHooks = path.join(home, '.cursor', 'hooks.json');
|
|
259
|
+
if (fs.existsSync(homeHooks)) {
|
|
260
|
+
const parsed = readAgentFile(homeHooks);
|
|
261
|
+
if (parsed) {
|
|
262
|
+
pushFinding(out, seen, {
|
|
263
|
+
kind: 'hooks',
|
|
264
|
+
client: 'Cursor',
|
|
265
|
+
filePath: homeHooks,
|
|
266
|
+
title: 'Cursor hooks config',
|
|
267
|
+
sizeBytes: parsed.sizeBytes,
|
|
268
|
+
lineCount: parsed.lineCount,
|
|
269
|
+
content: parsed.content,
|
|
270
|
+
riskTags: parsed.content.includes('fullcourtdefense') ? [] : ['no_runtime_hook'],
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
// Additional project roots (e.g. from `discover --scan-root`) — reference agent
|
|
275
|
+
// files in every mapped project, not just the current folder.
|
|
276
|
+
const cwdKey = path.resolve(cwd).toLowerCase();
|
|
277
|
+
for (const extra of options.extraRoots ?? []) {
|
|
278
|
+
if (path.resolve(extra).toLowerCase() === cwdKey)
|
|
223
279
|
continue;
|
|
224
|
-
|
|
225
|
-
if (!parsed)
|
|
280
|
+
if (!fs.existsSync(extra))
|
|
226
281
|
continue;
|
|
227
|
-
|
|
228
|
-
kind: 'hooks',
|
|
229
|
-
client: 'Cursor',
|
|
230
|
-
filePath: hooksPath,
|
|
231
|
-
title: 'Cursor hooks config',
|
|
232
|
-
sizeBytes: parsed.sizeBytes,
|
|
233
|
-
lineCount: parsed.lineCount,
|
|
234
|
-
content: parsed.content,
|
|
235
|
-
riskTags: parsed.content.includes('fullcourtdefense') ? [] : ['no_runtime_hook'],
|
|
236
|
-
});
|
|
282
|
+
scanProjectRootAgentFiles(extra, 'scanned project', out, seen);
|
|
237
283
|
}
|
|
238
284
|
out.sort((a, b) => b.riskTags.length - a.riskTags.length || b.sizeBytes - a.sizeBytes);
|
|
239
285
|
return out;
|
|
@@ -45,19 +45,23 @@ function windowsStartupLauncherPath() {
|
|
|
45
45
|
const appData = process.env.APPDATA || path.join(os.homedir(), 'AppData', 'Roaming');
|
|
46
46
|
return path.join(appData, 'Microsoft', 'Windows', 'Start Menu', 'Programs', 'Startup', STARTUP_LAUNCHER);
|
|
47
47
|
}
|
|
48
|
-
function discoverCommandLine(
|
|
48
|
+
function discoverCommandLine(opts = {}) {
|
|
49
49
|
const node = process.execPath;
|
|
50
50
|
const script = path.resolve(process.argv[1] || path.join(__dirname, '..', 'index.js'));
|
|
51
51
|
const q = (value) => (/\s/.test(value) ? `"${value}"` : value);
|
|
52
52
|
const parts = [q(node), q(script), 'discover', '--upload', '--deep', '--silent'];
|
|
53
|
-
if (surface)
|
|
54
|
-
parts.push('--surface', q(surface));
|
|
55
|
-
if (
|
|
56
|
-
parts.push('--
|
|
53
|
+
if (opts.surface)
|
|
54
|
+
parts.push('--surface', q(opts.surface));
|
|
55
|
+
if (opts.scanRoot)
|
|
56
|
+
parts.push('--scan-root', q(opts.scanRoot));
|
|
57
|
+
if (opts.deepRoots)
|
|
58
|
+
parts.push('--deep-roots', 'true');
|
|
59
|
+
if (opts.userEmail)
|
|
60
|
+
parts.push('--user-email', q(opts.userEmail));
|
|
57
61
|
return parts.join(' ');
|
|
58
62
|
}
|
|
59
|
-
function installWindowsSchedule(hour,
|
|
60
|
-
const tr = discoverCommandLine(
|
|
63
|
+
function installWindowsSchedule(hour, opts, trigger = 'daily') {
|
|
64
|
+
const tr = discoverCommandLine(opts);
|
|
61
65
|
try {
|
|
62
66
|
(0, child_process_1.execSync)(`schtasks /Delete /TN "${TASK_NAME}" /F`, { stdio: 'ignore' });
|
|
63
67
|
}
|
|
@@ -79,12 +83,12 @@ function installWindowsSchedule(hour, userEmail, surface, trigger = 'daily') {
|
|
|
79
83
|
console.log(`Task Scheduler denied access; installed user logon launcher instead: ${launcher}`);
|
|
80
84
|
}
|
|
81
85
|
}
|
|
82
|
-
function installMacSchedule(hour,
|
|
86
|
+
function installMacSchedule(hour, opts, trigger = 'daily') {
|
|
83
87
|
const label = 'ai.fullcourtdefense.discover';
|
|
84
88
|
const plistDir = path.join(os.homedir(), 'Library', 'LaunchAgents');
|
|
85
89
|
const plistPath = path.join(plistDir, `${label}.plist`);
|
|
86
90
|
fs.mkdirSync(plistDir, { recursive: true });
|
|
87
|
-
const cmd = discoverCommandLine(
|
|
91
|
+
const cmd = discoverCommandLine(opts).split(/\s+/);
|
|
88
92
|
const program = cmd.shift() || process.execPath;
|
|
89
93
|
const triggerXml = trigger === 'logon'
|
|
90
94
|
? '<key>RunAtLoad</key><true/>'
|
|
@@ -106,9 +110,9 @@ function installMacSchedule(hour, userEmail, surface, trigger = 'daily') {
|
|
|
106
110
|
(0, child_process_1.spawnSync)('launchctl', ['unload', plistPath], { stdio: 'ignore' });
|
|
107
111
|
(0, child_process_1.spawnSync)('launchctl', ['load', plistPath], { stdio: 'inherit' });
|
|
108
112
|
}
|
|
109
|
-
function installLinuxCron(hour,
|
|
113
|
+
function installLinuxCron(hour, opts, trigger = 'daily') {
|
|
110
114
|
const schedule = trigger === 'logon' ? '@reboot' : `0 ${hour} * * *`;
|
|
111
|
-
const line = `${schedule} ${discoverCommandLine(
|
|
115
|
+
const line = `${schedule} ${discoverCommandLine(opts)} >> ${path.join(os.homedir(), '.fullcourtdefense-discover.log')} 2>&1`;
|
|
112
116
|
const marker = '# fullcourtdefense-discover';
|
|
113
117
|
let crontab = '';
|
|
114
118
|
try {
|
|
@@ -126,11 +130,11 @@ function installDailyDiscoverSchedule(args = {}) {
|
|
|
126
130
|
const hour = Number.isFinite(args.hour) ? Math.min(23, Math.max(0, args.hour)) : 9;
|
|
127
131
|
const trigger = args.trigger || 'daily';
|
|
128
132
|
if (process.platform === 'win32')
|
|
129
|
-
installWindowsSchedule(hour, args
|
|
133
|
+
installWindowsSchedule(hour, args, trigger);
|
|
130
134
|
else if (process.platform === 'darwin')
|
|
131
|
-
installMacSchedule(hour, args
|
|
135
|
+
installMacSchedule(hour, args, trigger);
|
|
132
136
|
else
|
|
133
|
-
installLinuxCron(hour, args
|
|
137
|
+
installLinuxCron(hour, args, trigger);
|
|
134
138
|
}
|
|
135
139
|
function uninstallDailyDiscoverSchedule() {
|
|
136
140
|
if (process.platform === 'win32') {
|
|
@@ -57,13 +57,15 @@ const SECRET_PATTERNS = [
|
|
|
57
57
|
{ re: /\bgithub_pat_[a-zA-Z0-9_]{20,}\b/, provider: 'github', severity: 'critical', title: 'GitHub fine-grained PAT', impact: 'scoped access to your repositories' },
|
|
58
58
|
{ re: /\bglpat-[a-zA-Z0-9_-]{20,}\b/, provider: 'gitlab', severity: 'critical', title: 'GitLab personal access token', impact: 'read/write access to your GitLab projects' },
|
|
59
59
|
{ re: /\bAKIA[0-9A-Z]{16}\b/, provider: 'aws', severity: 'critical', title: 'AWS access key ID', impact: 'AWS API access (paired with its secret key)' },
|
|
60
|
-
{ re: /\baws_secret_access_key\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}\b/i, provider: 'aws', severity: 'critical', title: 'AWS secret access key', impact: 'full AWS API access for the paired key ID' },
|
|
60
|
+
{ re: /\baws_secret_access_key\b['"]?\s*[=:]\s*['"]?[A-Za-z0-9/+=]{40}\b/i, provider: 'aws', severity: 'critical', title: 'AWS secret access key', impact: 'full AWS API access for the paired key ID' },
|
|
61
61
|
{ re: /\bAIza[0-9A-Za-z_-]{35}\b/, provider: 'google', severity: 'critical', title: 'Google API key', impact: 'Google Cloud API usage billed to your project' },
|
|
62
62
|
{ re: /\bxox[baprs]-[a-zA-Z0-9-]{10,}\b/, provider: 'slack', severity: 'high', title: 'Slack token', impact: 'read/post messages as your Slack app or user' },
|
|
63
63
|
{ re: /\bhooks\.slack\.com\/services\/T[a-zA-Z0-9]+\/B[a-zA-Z0-9]+\/[a-zA-Z0-9]+\b/, provider: 'slack', severity: 'high', title: 'Slack incoming webhook URL', impact: 'post arbitrary messages into your Slack channel' },
|
|
64
64
|
{ re: /\bsk_live_[0-9a-zA-Z]{24,}\b/, provider: 'stripe', severity: 'critical', title: 'Stripe live secret key', impact: 'charges, refunds, and customer data in live mode' },
|
|
65
65
|
{ re: /\brk_live_[0-9a-zA-Z]{24,}\b/, provider: 'stripe', severity: 'critical', title: 'Stripe restricted key', impact: 'scoped live-mode payment operations' },
|
|
66
66
|
{ re: /\bSG\.[a-zA-Z0-9_-]{20,}\.[a-zA-Z0-9_-]{20,}\b/, provider: 'sendgrid', severity: 'high', title: 'SendGrid API key', impact: 'send email from your verified domains (phishing risk)' },
|
|
67
|
+
{ re: /\bsbp_[a-zA-Z0-9_-]{20,}\b/, provider: 'supabase', severity: 'high', title: 'Supabase access token', impact: 'manage Supabase projects and data allowed by this token' },
|
|
68
|
+
{ re: /\bfc-[a-fA-F0-9]{24,}\b/, provider: 'firecrawl', severity: 'medium', title: 'Firecrawl API key', impact: 'use web crawling/extraction quota billed to your account' },
|
|
67
69
|
{ re: /\bSK[0-9a-fA-F]{32}\b/, provider: 'twilio', severity: 'high', title: 'Twilio API key SID', impact: 'send SMS/calls billed to your account' },
|
|
68
70
|
{ re: /\bhf_[a-zA-Z0-9]{30,}\b/, provider: 'huggingface', severity: 'high', title: 'HuggingFace token', impact: 'access to your private models and datasets' },
|
|
69
71
|
{ re: /\bnpm_[a-zA-Z0-9]{36,}\b/, provider: 'npm', severity: 'high', title: 'npm access token', impact: 'publish packages as you (supply-chain risk)' },
|
|
@@ -98,6 +100,8 @@ const PROVIDER_REMEDIATION = {
|
|
|
98
100
|
gcp_adc: 'Revoke the local ADC refresh token (`gcloud auth application-default revoke`) if unneeded, then re-auth with least-privilege accounts.',
|
|
99
101
|
netrc: 'Remove plaintext passwords from .netrc or replace them with token helpers / OS keychain storage.',
|
|
100
102
|
bearer: 'Rotate the token and avoid pasting bearer tokens into shell history or config files.',
|
|
103
|
+
supabase: 'Revoke this token in the Supabase dashboard and replace it with a scoped secret stored outside MCP config files.',
|
|
104
|
+
firecrawl: 'Rotate this API key in Firecrawl and store it in a secret manager or environment injected by the runtime.',
|
|
101
105
|
url_basic_auth: 'Rotate the password/token embedded in the URL and replace it with environment or secret-manager lookup.',
|
|
102
106
|
database: 'Change the database password and move the connection string to a secret manager; restrict the DB user\u2019s privileges.',
|
|
103
107
|
};
|
|
@@ -301,7 +305,7 @@ function readTextFile(filePath) {
|
|
|
301
305
|
const stat = fs.statSync(filePath);
|
|
302
306
|
if (!stat.isFile() || stat.size > MAX_FILE_BYTES)
|
|
303
307
|
return null;
|
|
304
|
-
return fs.readFileSync(filePath, 'utf-8');
|
|
308
|
+
return fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '');
|
|
305
309
|
}
|
|
306
310
|
catch {
|
|
307
311
|
return null;
|
|
@@ -402,6 +406,7 @@ function scanSecrets(options = {}) {
|
|
|
402
406
|
const home = os.homedir();
|
|
403
407
|
const cwd = options.cwd || process.cwd();
|
|
404
408
|
const maxDepth = options.maxEnvDepth ?? 4;
|
|
409
|
+
const extraRoots = options.extraRoots ?? [];
|
|
405
410
|
const findings = [];
|
|
406
411
|
const seen = new Set();
|
|
407
412
|
let scannedFiles = 0;
|
|
@@ -430,6 +435,7 @@ function scanSecrets(options = {}) {
|
|
|
430
435
|
const envRoots = Array.from(new Set([
|
|
431
436
|
home,
|
|
432
437
|
cwd,
|
|
438
|
+
...extraRoots,
|
|
433
439
|
path.join(home, 'dev'),
|
|
434
440
|
path.join(home, 'repos'),
|
|
435
441
|
path.join(home, 'projects'),
|
|
@@ -445,6 +451,19 @@ function scanSecrets(options = {}) {
|
|
|
445
451
|
scannedFiles += 1;
|
|
446
452
|
scanTextLines(envFile, content, 'env_file', findings, seen);
|
|
447
453
|
}
|
|
454
|
+
// MCP/client config files are already discovered by the MCP inventory pass.
|
|
455
|
+
// Scan only those known files for embedded env/header/arg secrets; this catches
|
|
456
|
+
// real-world mistakes like GitHub/Slack tokens in MCP `env`, DB URLs in `args`,
|
|
457
|
+
// and Bearer tokens in remote MCP headers without expanding the filesystem scan.
|
|
458
|
+
for (const configFile of Array.from(new Set(options.configFiles ?? []))) {
|
|
459
|
+
if (findings.length >= MAX_FINDINGS)
|
|
460
|
+
break;
|
|
461
|
+
const content = readTextFile(configFile);
|
|
462
|
+
if (!content)
|
|
463
|
+
continue;
|
|
464
|
+
scannedFiles += 1;
|
|
465
|
+
scanTextLines(configFile, content, 'config', findings, seen);
|
|
466
|
+
}
|
|
448
467
|
findings.sort((a, b) => {
|
|
449
468
|
const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
|
|
450
469
|
return order[a.severity] - order[b.severity] || a.filePath.localeCompare(b.filePath);
|
package/dist/version.json
CHANGED