fullcourtdefense-cli 1.14.15 → 1.14.17

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.
@@ -314,7 +314,7 @@ function pushDiscoveredServer(out, name, cfgRaw, source, configPath) {
314
314
  function parseConfigFile(filePath, source) {
315
315
  let raw;
316
316
  try {
317
- raw = fs.readFileSync(filePath, 'utf-8');
317
+ raw = fs.readFileSync(filePath, 'utf-8').replace(/^\uFEFF/, '');
318
318
  }
319
319
  catch {
320
320
  return [];
@@ -806,12 +806,16 @@ async function discoverCommand(args, config) {
806
806
  const scanRoots = (0, discoverPaths_1.resolveScanRoots)(args.scanRoot);
807
807
  const sweep = scanRoots.length > 0 ? (0, discoverPaths_1.scanRootsForProjectConfigs)(scanRoots) : { candidates: [], scannedDirs: 0 };
808
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));
809
813
  if (scanRoots.length > 0 && !silent) {
810
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}`);
811
815
  }
812
816
  const scanRootConfigPaths = new Set();
813
817
  if (runMcp) {
814
- const candidates = candidateConfigPaths(cwd, args.extraPath);
818
+ const candidates = [...knownConfigCandidates];
815
819
  const known = new Set(candidates.map(c => path.resolve(c.path).toLowerCase()));
816
820
  for (const candidate of sweep.candidates) {
817
821
  const key = path.resolve(candidate.path).toLowerCase();
@@ -849,7 +853,7 @@ async function discoverCommand(args, config) {
849
853
  }
850
854
  }
851
855
  const secrets = surfaces.has('secrets') || surfaces.has('posture')
852
- ? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots })
856
+ ? (0, discoverSecrets_1.scanSecrets)({ cwd, extraRoots: sweepProjectRoots, configFiles: secretConfigFiles })
853
857
  : undefined;
854
858
  const agentFiles = surfaces.has('agent-files') || surfaces.has('posture')
855
859
  ? (0, discoverAgentFiles_1.scanAgentFiles)({ cwd, extraRoots: sweepProjectRoots })
@@ -863,19 +867,6 @@ async function discoverCommand(args, config) {
863
867
  })
864
868
  : undefined;
865
869
  const host = buildHostMetadata(args.userEmail, deep && found.some(s => s.probeMode === 'deep') ? 'deep' : 'config');
866
- if (args.json === 'true') {
867
- console.log(JSON.stringify({
868
- host,
869
- surfaces: [...surfaces],
870
- scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
871
- clientCoverage,
872
- servers: found,
873
- secrets,
874
- agentFiles,
875
- posture,
876
- }, null, 2));
877
- return;
878
- }
879
870
  const home = os.homedir();
880
871
  const uploadExtras = {
881
872
  secrets,
@@ -915,6 +906,20 @@ async function discoverCommand(args, config) {
915
906
  }
916
907
  await upload(found, host, clientCoverage, creds.apiUrl, { apiKey: creds.apiKey, shieldId: creds.shieldId, shieldKey: creds.shieldKey, preferApiKey: !!args.apiKey }, args.connectorName, uploadExtras);
917
908
  }
909
+ if (args.json === 'true') {
910
+ await maybeUpload();
911
+ console.log(JSON.stringify({
912
+ host,
913
+ surfaces: [...surfaces],
914
+ scannedFiles: scanned.map(s => `${s.source} → ${s.path}`),
915
+ clientCoverage,
916
+ servers: found,
917
+ secrets,
918
+ agentFiles,
919
+ posture,
920
+ }, null, 2));
921
+ return;
922
+ }
918
923
  if (silent) {
919
924
  if (uploadRequested) {
920
925
  const creds = (0, config_1.resolveCliCredentials)(config, { apiKey: args.apiKey, apiUrl: args.apiUrl });
@@ -22,4 +22,5 @@ export declare function scanSecrets(options?: {
22
22
  cwd?: string;
23
23
  maxEnvDepth?: number;
24
24
  extraRoots?: string[];
25
+ configFiles?: string[];
25
26
  }): SecretsScanResult;
@@ -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;
@@ -398,6 +402,48 @@ function collectEnvFiles(roots, maxDepth) {
398
402
  }
399
403
  return found;
400
404
  }
405
+ function collectProjectCredentialFiles(roots, maxDepth) {
406
+ const found = [];
407
+ const queue = roots.map(dir => ({ dir, depth: 0 }));
408
+ const isCredentialFile = (filePath) => {
409
+ const normalized = filePath.replace(/\\/g, '/').toLowerCase();
410
+ const base = path.basename(normalized);
411
+ return base === '.npmrc'
412
+ || base === '.pypirc'
413
+ || base === '.netrc'
414
+ || normalized.endsWith('/.docker/config.json')
415
+ || normalized.endsWith('/.kube/config')
416
+ || normalized.endsWith('/gcloud/application_default_credentials.json');
417
+ };
418
+ while (queue.length > 0 && found.length < MAX_ENV_FILES) {
419
+ const { dir, depth } = queue.shift();
420
+ let entries;
421
+ try {
422
+ entries = fs.readdirSync(dir, { withFileTypes: true });
423
+ }
424
+ catch {
425
+ continue;
426
+ }
427
+ for (const entry of entries) {
428
+ if (found.length >= MAX_ENV_FILES)
429
+ break;
430
+ const full = path.join(dir, entry.name);
431
+ if (entry.isDirectory()) {
432
+ if (depth >= maxDepth)
433
+ continue;
434
+ if (SKIP_DIR_NAMES.has(entry.name))
435
+ continue;
436
+ if (entry.name.startsWith('.') && !['.cursor', '.claude', '.codex', '.vscode', '.docker', '.kube', '.config'].includes(entry.name))
437
+ continue;
438
+ queue.push({ dir: full, depth: depth + 1 });
439
+ continue;
440
+ }
441
+ if (entry.isFile() && isCredentialFile(full))
442
+ found.push(full);
443
+ }
444
+ }
445
+ return found;
446
+ }
401
447
  function scanSecrets(options = {}) {
402
448
  const home = os.homedir();
403
449
  const cwd = options.cwd || process.cwd();
@@ -447,6 +493,29 @@ function scanSecrets(options = {}) {
447
493
  scannedFiles += 1;
448
494
  scanTextLines(envFile, content, 'env_file', findings, seen);
449
495
  }
496
+ for (const credentialFile of collectProjectCredentialFiles([cwd, ...extraRoots], maxDepth)) {
497
+ if (findings.length >= MAX_FINDINGS)
498
+ break;
499
+ const content = readTextFile(credentialFile);
500
+ if (!content)
501
+ continue;
502
+ scannedFiles += 1;
503
+ scanTextLines(credentialFile, content, 'credential_store', findings, seen);
504
+ scanCredentialStorePosture(credentialFile, content, 'credential_store', findings, seen);
505
+ }
506
+ // MCP/client config files are already discovered by the MCP inventory pass.
507
+ // Scan only those known files for embedded env/header/arg secrets; this catches
508
+ // real-world mistakes like GitHub/Slack tokens in MCP `env`, DB URLs in `args`,
509
+ // and Bearer tokens in remote MCP headers without expanding the filesystem scan.
510
+ for (const configFile of Array.from(new Set(options.configFiles ?? []))) {
511
+ if (findings.length >= MAX_FINDINGS)
512
+ break;
513
+ const content = readTextFile(configFile);
514
+ if (!content)
515
+ continue;
516
+ scannedFiles += 1;
517
+ scanTextLines(configFile, content, 'config', findings, seen);
518
+ }
450
519
  findings.sort((a, b) => {
451
520
  const order = { critical: 0, high: 1, medium: 2, low: 3, info: 4 };
452
521
  return order[a.severity] - order[b.severity] || a.filePath.localeCompare(b.filePath);
@@ -759,6 +759,7 @@ class McpGatewayServer {
759
759
  shieldKey: this.gatewayConfig.shieldKey,
760
760
  developerName: this.gatewayConfig.developerName,
761
761
  machineName: os.hostname(),
762
+ force: true,
762
763
  });
763
764
  if (bundle.source !== 'default')
764
765
  expectedPolicyHash = bundle.policyHash || bundle.version;
@@ -1159,7 +1160,7 @@ async function installMcpGatewayCommand(args, config) {
1159
1160
  shieldId: args.shieldId,
1160
1161
  shieldKey: args.shieldKey,
1161
1162
  apiUrl: args.apiUrl,
1162
- }, { requireOrganizationId: false });
1163
+ }, { requireApiKey: false, requireOrganizationId: false });
1163
1164
  const cursorProject = args.cursorProject === 'true';
1164
1165
  const selection = parseClientList(args.clients);
1165
1166
  const clients = selection === 'auto'
@@ -1748,7 +1749,7 @@ function protectAllTargetFiles(extra, allowedClientKeys) {
1748
1749
  return files;
1749
1750
  }
1750
1751
  async function protectAllCommand(args, config) {
1751
- (0, config_1.requireCliSetup)(config, { shieldId: args.shieldId, shieldKey: args.shieldKey, apiUrl: args.apiUrl }, { requireOrganizationId: false });
1752
+ (0, config_1.requireCliSetup)(config, { shieldId: args.shieldId, shieldKey: args.shieldKey, apiUrl: args.apiUrl }, { requireApiKey: false, requireOrganizationId: false });
1752
1753
  const gatewayConfig = resolveGatewayConfig(args, config);
1753
1754
  const dryRun = args.dryRun === 'true';
1754
1755
  const allowedClientKeys = resolveProtectClientKeys(args.clients);
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.15"
2
+ "version": "1.14.17"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.15",
3
+ "version": "1.14.17",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -26,6 +26,7 @@
26
26
  "test:discover-stdio-mcp": "npm run build && node scripts/test-discover-stdio-mcp-tools.js",
27
27
  "test:per-server-agent-name": "npm run build && node scripts/test-per-server-agent-name.js",
28
28
  "test:remote-mcp-gateway": "npm run build && node scripts/test-remote-mcp-gateway.js",
29
+ "test:e2e-onboarding-personas": "npm run build && node scripts/test-e2e-onboarding-personas.js",
29
30
  "test:posture-blast": "npm run build && node scripts/test-posture-blast-radius.js",
30
31
  "test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
31
32
  "prepublishOnly": "npm run build"