fullcourtdefense-cli 1.14.2 → 1.14.4

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.
@@ -60,6 +60,8 @@ export interface DesktopDiscoveryHost {
60
60
  probeMode: 'config' | 'deep';
61
61
  /** Windows-only: PowerShell audit coverage (ScriptBlock Logging + Transcription). */
62
62
  shellAudit?: ShellAuditReport;
63
+ /** True when the scan ran on an ephemeral CI runner (GitHub Actions, GitLab, etc.) — not a user machine. */
64
+ isCi?: boolean;
63
65
  }
64
66
  export declare function discoverCommand(args: DiscoverArgs, config: BotGuardConfig): Promise<void>;
65
67
  /** Upload-only discover — used after install-mcp-gateway --upload. */
@@ -93,6 +93,16 @@ function machineFingerprint() {
93
93
  const seed = [os.hostname(), os.userInfo().username, os.platform(), os.arch()].join('|');
94
94
  return crypto.createHash('sha256').update(seed).digest('hex').slice(0, 16);
95
95
  }
96
+ /** Detect ephemeral CI runners so they are never listed as user machines in the fleet. */
97
+ function isCiEnvironment() {
98
+ return process.env.GITHUB_ACTIONS === 'true'
99
+ || process.env.GITLAB_CI === 'true'
100
+ || process.env.TF_BUILD === 'True' // Azure Pipelines
101
+ || !!process.env.JENKINS_URL
102
+ || !!process.env.BUILDKITE
103
+ || !!process.env.CIRCLECI
104
+ || process.env.CI === 'true';
105
+ }
96
106
  function buildHostMetadata(userEmail, probeMode = 'config') {
97
107
  const info = os.userInfo();
98
108
  return {
@@ -103,6 +113,7 @@ function buildHostMetadata(userEmail, probeMode = 'config') {
103
113
  scannedAt: new Date().toISOString(),
104
114
  probeMode,
105
115
  shellAudit: (0, windowsAudit_1.getShellAuditReport)(),
116
+ isCi: isCiEnvironment() || undefined,
106
117
  };
107
118
  }
108
119
  /** Where MCP client configs live — see discoverPaths.ts for the canonical list. */
@@ -100,6 +100,8 @@ const SKIP_DIR_NAMES = new Set([
100
100
  const MAX_FILE_BYTES = 256 * 1024;
101
101
  const MAX_ENV_FILES = 400;
102
102
  const MAX_FINDINGS = 200;
103
+ const INLINE_ALLOW_COMMENT = /\b(?:gitleaks:allow|trufflehog:ignore|pragma:\s*allowlist\s+secret|nosecret|secretlint-disable)\b/i;
104
+ const PLACEHOLDER_WORDS = /(?:example|sample|dummy|fake|placeholder|changeme|change_me|replace_me|your[_-]?key|your[_-]?token|test[_-]?only)/i;
103
105
  function redact(value) {
104
106
  const trimmed = value.trim();
105
107
  if (trimmed.length <= 8)
@@ -109,6 +111,21 @@ function redact(value) {
109
111
  function findingId(parts) {
110
112
  return crypto.createHash('sha256').update(parts.join('|')).digest('hex').slice(0, 16);
111
113
  }
114
+ function looksLikePlaceholder(raw, line) {
115
+ const joined = `${raw} ${line}`;
116
+ if (PLACEHOLDER_WORDS.test(joined))
117
+ return true;
118
+ if (/(?:sk-proj-|sk-|gsk_|xai-|hf_|npm_|ag_org_|shsk_)[x0a]{16,}/i.test(raw))
119
+ return true;
120
+ const compact = raw.replace(/[^A-Za-z0-9]/g, '');
121
+ if (!compact)
122
+ return false;
123
+ if (/^(?:x+|0+|1+|a+|z+)$/i.test(compact))
124
+ return true;
125
+ if (/^(?:1234567890|abcdef|abc123){2,}$/i.test(compact))
126
+ return true;
127
+ return false;
128
+ }
112
129
  function scoreFromFindings(findings) {
113
130
  let score = 100;
114
131
  for (const f of findings) {
@@ -124,11 +141,19 @@ function scoreFromFindings(findings) {
124
141
  return Math.max(0, Math.min(100, score));
125
142
  }
126
143
  function scanTextLines(filePath, content, category, out, seen) {
144
+ // Placeholder/template files (.env.example, *.sample, *.template) hold intentional
145
+ // dummy values — report as info so real findings aren't drowned out (Gitleaks
146
+ // ignores these entirely; we keep them visible but non-alarming).
147
+ const isExampleFile = /\.(example|sample|template|dist)(\.|$)/i.test(path.basename(filePath));
148
+ // Vars with client-bundle prefixes ship to the browser by design.
149
+ const clientExposedPrefix = /^\s*(?:VITE_|NEXT_PUBLIC_|REACT_APP_|EXPO_PUBLIC_|NUXT_PUBLIC_)/;
127
150
  const lines = content.split(/\r?\n/);
128
151
  for (let i = 0; i < lines.length; i++) {
129
152
  const line = lines[i];
130
153
  if (!line.trim() || line.trim().startsWith('#'))
131
154
  continue;
155
+ if (INLINE_ALLOW_COMMENT.test(line))
156
+ continue;
132
157
  // First (most specific) pattern wins per matched string — prevents e.g. an
133
158
  // sk-ant- Anthropic key from also being reported as a generic OpenAI sk- key.
134
159
  const matchedRaws = [];
@@ -144,22 +169,47 @@ function scanTextLines(filePath, content, category, out, seen) {
144
169
  if (seen.has(id))
145
170
  continue;
146
171
  seen.add(id);
172
+ // Firebase/Google web API keys behind a client-bundle prefix are public by
173
+ // design — the risk is a missing restriction, not the exposure itself.
174
+ const isClientExposedWebKey = pattern.provider === 'google' && clientExposedPrefix.test(line);
175
+ let severity = pattern.severity;
176
+ let title = pattern.title;
177
+ let detail = pattern.impact
178
+ ? `Matched ${pattern.provider} pattern in ${path.basename(filePath)}. If leaked: ${pattern.impact}.`
179
+ : `Matched ${pattern.provider} pattern in ${path.basename(filePath)}`;
180
+ let remediation = pattern.provider === 'private_key'
181
+ ? 'Move keys to a secure store; never commit private keys. Use ssh-agent or encrypted keys.'
182
+ : PROVIDER_REMEDIATION[pattern.provider]
183
+ || 'Rotate this credential immediately and store it in a secret manager or OS keychain — not in plain files.';
184
+ if (isExampleFile) {
185
+ severity = 'info';
186
+ title = `${pattern.title} (example/template file)`;
187
+ detail = `Matched ${pattern.provider} pattern in ${path.basename(filePath)} — looks like an intentional placeholder.`;
188
+ remediation = 'Confirm this is a dummy value. If a real credential was pasted into the example file, rotate it and replace with a placeholder.';
189
+ }
190
+ else if (looksLikePlaceholder(raw, line)) {
191
+ severity = 'info';
192
+ title = `${pattern.title} (placeholder-like value)`;
193
+ detail = `Matched ${pattern.provider} pattern in ${path.basename(filePath)}, but the value/line contains common placeholder markers.`;
194
+ remediation = 'Replace placeholders before production. If this is a real credential despite the placeholder wording, rotate it and move it to a secret manager.';
195
+ }
196
+ else if (isClientExposedWebKey) {
197
+ severity = 'medium';
198
+ title = 'Google/Firebase web API key (client-exposed)';
199
+ detail = `Matched google pattern in ${path.basename(filePath)} on a client-bundle variable — this key ships to the browser by design.`;
200
+ remediation = 'Not a secret in the classic sense, but add HTTP-referrer/API restrictions in Google Cloud console → Credentials so it only works from your domains.';
201
+ }
147
202
  out.push({
148
203
  id,
149
204
  category,
150
- severity: pattern.severity,
205
+ severity,
151
206
  provider: pattern.provider,
152
207
  filePath,
153
208
  lineNumber: i + 1,
154
- title: pattern.title,
155
- detail: pattern.impact
156
- ? `Matched ${pattern.provider} pattern in ${path.basename(filePath)}. If leaked: ${pattern.impact}.`
157
- : `Matched ${pattern.provider} pattern in ${path.basename(filePath)}`,
209
+ title,
210
+ detail,
158
211
  redactedPreview: redact(raw),
159
- remediation: pattern.provider === 'private_key'
160
- ? 'Move keys to a secure store; never commit private keys. Use ssh-agent or encrypted keys.'
161
- : PROVIDER_REMEDIATION[pattern.provider]
162
- || 'Rotate this credential immediately and store it in a secret manager or OS keychain — not in plain files.',
212
+ remediation,
163
213
  });
164
214
  if (out.length >= MAX_FINDINGS)
165
215
  return;
package/dist/version.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
- "version": "1.14.2"
2
+ "version": "1.14.4"
3
3
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fullcourtdefense-cli",
3
- "version": "1.14.2",
3
+ "version": "1.14.4",
4
4
  "description": "Full Court Defense CLI — security scanning for AI agents from your terminal",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "test:shell-guard": "npm run build && node scripts/test-shell-guard.js",
21
21
  "test:cmd-guard": "npm run build && node scripts/test-cmd-guard.js",
22
22
  "test:posix-guard": "npm run build && node scripts/test-posix-guard.js",
23
+ "test:secret-posture": "npm run build && node scripts/test-secret-posture-fixtures.js",
23
24
  "test:ping-e2e": "npm run build && node scripts/test-ping-e2e.js",
24
25
  "prepublishOnly": "npm run build"
25
26
  },