fullcourtdefense-cli 1.14.0 → 1.14.2
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.
|
@@ -38,29 +38,60 @@ const crypto = __importStar(require("crypto"));
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const os = __importStar(require("os"));
|
|
40
40
|
const path = __importStar(require("path"));
|
|
41
|
-
/**
|
|
41
|
+
/**
|
|
42
|
+
* Patterns aligned with Gitleaks / TruffleHog / GitHub push-protection defaults
|
|
43
|
+
* (local-only, no network). ORDER MATTERS: specific prefixes (sk-proj-, sk-ant-,
|
|
44
|
+
* sk-or-) must come before the generic OpenAI sk- pattern — matches are deduped
|
|
45
|
+
* by the matched string, first pattern wins.
|
|
46
|
+
*/
|
|
42
47
|
const SECRET_PATTERNS = [
|
|
43
|
-
{ re: /\bsk-[a-zA-Z0-9_-]{20,}\b/, provider: 'openai', severity: 'critical', title: 'OpenAI
|
|
44
|
-
{ re: /\bsk-
|
|
45
|
-
{ re: /\bsk-
|
|
46
|
-
{ re: /\
|
|
47
|
-
{ re: /\
|
|
48
|
-
{ re: /\
|
|
49
|
-
{ re: /\
|
|
50
|
-
{ re: /\
|
|
51
|
-
{ re: /\
|
|
52
|
-
{ re: /\
|
|
53
|
-
{ re: /\
|
|
54
|
-
{ re: /\
|
|
55
|
-
{ re: /\
|
|
56
|
-
{ re: /\
|
|
57
|
-
{ re: /\
|
|
58
|
-
{ re: /\
|
|
59
|
-
{ re: /\
|
|
60
|
-
{ re:
|
|
61
|
-
{ re:
|
|
62
|
-
{ re:
|
|
48
|
+
{ re: /\bsk-proj-[a-zA-Z0-9_-]{20,}\b/, provider: 'openai', severity: 'critical', title: 'OpenAI project key', impact: 'full OpenAI API usage billed to your account' },
|
|
49
|
+
{ re: /\bsk-ant-[a-zA-Z0-9_-]{20,}\b/, provider: 'anthropic', severity: 'critical', title: 'Anthropic API key', impact: 'full Anthropic API usage billed to your account' },
|
|
50
|
+
{ re: /\bsk-or-[a-zA-Z0-9_-]{20,}\b/, provider: 'openrouter', severity: 'high', title: 'OpenRouter API key', impact: 'LLM usage across providers billed to your account' },
|
|
51
|
+
{ re: /\bsk-[a-zA-Z0-9_-]{20,}\b/, provider: 'openai', severity: 'critical', title: 'OpenAI API key', impact: 'full OpenAI API usage billed to your account' },
|
|
52
|
+
{ re: /\bgsk_[a-zA-Z0-9]{20,}\b/, provider: 'groq', severity: 'high', title: 'Groq API key', impact: 'LLM inference billed to your account' },
|
|
53
|
+
{ re: /\bxai-[a-zA-Z0-9]{20,}\b/, provider: 'xai', severity: 'high', title: 'xAI API key', impact: 'LLM inference billed to your account' },
|
|
54
|
+
{ re: /\bghp_[a-zA-Z0-9]{36,}\b/, provider: 'github', severity: 'critical', title: 'GitHub personal access token', impact: 'read/write access to your repositories' },
|
|
55
|
+
{ re: /\bgho_[a-zA-Z0-9]{36,}\b/, provider: 'github', severity: 'critical', title: 'GitHub OAuth token', impact: 'read/write access to your repositories' },
|
|
56
|
+
{ re: /\bghs_[a-zA-Z0-9]{36,}\b/, provider: 'github', severity: 'critical', title: 'GitHub app server token', impact: 'app-scoped repository access' },
|
|
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
|
+
{ 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
|
+
{ 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' },
|
|
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
|
+
{ 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
|
+
{ 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
|
+
{ 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
|
+
{ re: /\brk_live_[0-9a-zA-Z]{24,}\b/, provider: 'stripe', severity: 'critical', title: 'Stripe restricted key', impact: 'scoped live-mode payment operations' },
|
|
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: /\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
|
+
{ re: /\bhf_[a-zA-Z0-9]{30,}\b/, provider: 'huggingface', severity: 'high', title: 'HuggingFace token', impact: 'access to your private models and datasets' },
|
|
69
|
+
{ re: /\bnpm_[a-zA-Z0-9]{36,}\b/, provider: 'npm', severity: 'high', title: 'npm access token', impact: 'publish packages as you (supply-chain risk)' },
|
|
70
|
+
{ re: /\bhvs\.[a-zA-Z0-9_-]{20,}\b/, provider: 'vault', severity: 'critical', title: 'HashiCorp Vault token', impact: 'access to every secret the token policy allows' },
|
|
71
|
+
{ re: /\bag_org_[a-zA-Z0-9_-]{20,}\b/, provider: 'fullcourtdefense', severity: 'high', title: 'FullCourtDefense org API key', impact: 'org-scoped scan and shield API access' },
|
|
72
|
+
{ re: /\bshsk_[a-zA-Z0-9]{20,}\b/, provider: 'fullcourtdefense', severity: 'high', title: 'Shield API key', impact: 'submit scans as this shield' },
|
|
73
|
+
{ re: /\beyJ[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\.[a-zA-Z0-9_-]+\b/, provider: 'jwt', severity: 'medium', title: 'JWT token', impact: 'impersonate the session until the token expires' },
|
|
74
|
+
{ re: /-----BEGIN (?:RSA |EC |OPENSSH |PGP )?PRIVATE KEY(?: BLOCK)?-----/, provider: 'private_key', severity: 'critical', title: 'Private key material', impact: 'authenticate as the key owner' },
|
|
75
|
+
{ re: /postgres(?:ql)?:\/\/[^\s'"]+:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'Database connection string', impact: 'direct database read/write access' },
|
|
76
|
+
{ re: /mysql:\/\/[^\s'"]+:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'MySQL connection string', impact: 'direct database read/write access' },
|
|
77
|
+
{ re: /rediss?:\/\/[^\s'"]*:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'Redis connection string', impact: 'direct cache/queue access' },
|
|
78
|
+
{ re: /mongodb(?:\+srv)?:\/\/[^\s'"]+:[^\s'"]+@/i, provider: 'database', severity: 'critical', title: 'MongoDB connection string', impact: 'direct database read/write access' },
|
|
63
79
|
];
|
|
80
|
+
/** Provider-specific rotation guidance (falls back to the generic advice). */
|
|
81
|
+
const PROVIDER_REMEDIATION = {
|
|
82
|
+
openai: 'Revoke it at platform.openai.com → API keys, create a new one, and store it in a secret manager or OS keychain — not in plain files.',
|
|
83
|
+
anthropic: 'Revoke it in the Anthropic console → API keys, create a new one, and store it in a secret manager — not in plain files.',
|
|
84
|
+
github: 'Revoke it at github.com → Settings → Developer settings → Tokens, then create a minimally-scoped replacement.',
|
|
85
|
+
gitlab: 'Revoke it in GitLab → Preferences → Access tokens, then create a minimally-scoped replacement.',
|
|
86
|
+
aws: 'Deactivate the key in IAM → Security credentials, rotate, and prefer short-lived credentials (SSO/assume-role) over static keys.',
|
|
87
|
+
google: 'Regenerate the key in Google Cloud console → APIs & Services → Credentials, and add API/IP restrictions to the new key.',
|
|
88
|
+
stripe: 'Roll the key in the Stripe dashboard → Developers → API keys. Live keys should only exist in your deployment platform\u2019s secret store.',
|
|
89
|
+
sendgrid: 'Delete this key in SendGrid → Settings → API Keys and create a scoped replacement. SendGrid keys are shown once — the new one goes straight into a secret manager.',
|
|
90
|
+
slack: 'Regenerate the token/webhook in your Slack app settings (api.slack.com/apps) and update the consumer.',
|
|
91
|
+
npm: 'Revoke it with `npm token revoke` and create a granular automation token with the minimum package scope.',
|
|
92
|
+
vault: 'Revoke the token (`vault token revoke`) and issue short-TTL tokens instead of long-lived ones.',
|
|
93
|
+
database: 'Change the database password and move the connection string to a secret manager; restrict the DB user\u2019s privileges.',
|
|
94
|
+
};
|
|
64
95
|
const SKIP_DIR_NAMES = new Set([
|
|
65
96
|
'node_modules', '.git', 'vendor', 'dist', 'build', '__pycache__', '.venv', 'venv',
|
|
66
97
|
'Library', 'Caches', 'Cache', 'Temp', 'tmp', 'Windows', 'Program Files', 'Program Files (x86)',
|
|
@@ -98,11 +129,17 @@ function scanTextLines(filePath, content, category, out, seen) {
|
|
|
98
129
|
const line = lines[i];
|
|
99
130
|
if (!line.trim() || line.trim().startsWith('#'))
|
|
100
131
|
continue;
|
|
132
|
+
// First (most specific) pattern wins per matched string — prevents e.g. an
|
|
133
|
+
// sk-ant- Anthropic key from also being reported as a generic OpenAI sk- key.
|
|
134
|
+
const matchedRaws = [];
|
|
101
135
|
for (const pattern of SECRET_PATTERNS) {
|
|
102
136
|
const match = line.match(pattern.re);
|
|
103
137
|
if (!match)
|
|
104
138
|
continue;
|
|
105
139
|
const raw = match[0];
|
|
140
|
+
if (matchedRaws.some(prev => prev.includes(raw) || raw.includes(prev)))
|
|
141
|
+
continue;
|
|
142
|
+
matchedRaws.push(raw);
|
|
106
143
|
const id = findingId([filePath, String(i + 1), pattern.provider, raw.slice(0, 12)]);
|
|
107
144
|
if (seen.has(id))
|
|
108
145
|
continue;
|
|
@@ -115,11 +152,14 @@ function scanTextLines(filePath, content, category, out, seen) {
|
|
|
115
152
|
filePath,
|
|
116
153
|
lineNumber: i + 1,
|
|
117
154
|
title: pattern.title,
|
|
118
|
-
detail:
|
|
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)}`,
|
|
119
158
|
redactedPreview: redact(raw),
|
|
120
159
|
remediation: pattern.provider === 'private_key'
|
|
121
160
|
? 'Move keys to a secure store; never commit private keys. Use ssh-agent or encrypted keys.'
|
|
122
|
-
:
|
|
161
|
+
: PROVIDER_REMEDIATION[pattern.provider]
|
|
162
|
+
|| 'Rotate this credential immediately and store it in a secret manager or OS keychain — not in plain files.',
|
|
123
163
|
});
|
|
124
164
|
if (out.length >= MAX_FINDINGS)
|
|
125
165
|
return;
|
package/dist/commands/hook.js
CHANGED
|
@@ -456,7 +456,7 @@ async function hookCommand(args, config) {
|
|
|
456
456
|
const failClosed = args.failClosed === 'true';
|
|
457
457
|
const timeoutMs = Number(args.timeout) > 0 ? Number(args.timeout) : 8000;
|
|
458
458
|
const approvalMode = args.approvalMode === 'block' ? 'block' : 'wait';
|
|
459
|
-
const approvalTimeoutMs = Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) :
|
|
459
|
+
const approvalTimeoutMs = Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 900000;
|
|
460
460
|
const approvalPollMs = Math.max(1000, Number(args.approvalPollMs) > 0 ? Number(args.approvalPollMs) : 3000);
|
|
461
461
|
const creds = (0, config_1.resolveCliCredentials)(config, {
|
|
462
462
|
shieldId: args.shieldId,
|
|
@@ -191,7 +191,7 @@ function resolveGatewayConfig(args, config, defaults = {}) {
|
|
|
191
191
|
userObjective: args.userObjective || process.env.FCD_USER_OBJECTIVE || defaults.userObjective || 'Developer requested this action from Cursor.',
|
|
192
192
|
authority: args.authority || process.env.FCD_AUTHORITY || 'developer',
|
|
193
193
|
approvalMode: args.approvalMode === 'block' ? 'block' : 'wait',
|
|
194
|
-
approvalTimeoutMs: Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) :
|
|
194
|
+
approvalTimeoutMs: Number(args.approvalTimeoutMs) > 0 ? Number(args.approvalTimeoutMs) : 900000,
|
|
195
195
|
approvalPollMs: Math.max(1000, Number(args.approvalPollMs) > 0 ? Number(args.approvalPollMs) : 3000),
|
|
196
196
|
scanResponse: args.scanResponse === 'true',
|
|
197
197
|
failClosed: args.failClosed !== 'false',
|
package/dist/telemetry.js
CHANGED
|
@@ -155,8 +155,12 @@ async function flushSpool(input) {
|
|
|
155
155
|
const body = await resp.json().catch(() => ({}));
|
|
156
156
|
if (!body.success)
|
|
157
157
|
return null;
|
|
158
|
-
// Successfully delivered → drop the
|
|
159
|
-
|
|
158
|
+
// Successfully delivered → drop ONLY the events we actually sent. The spool
|
|
159
|
+
// may have gained new events while the POST was in flight (e.g. a terminal
|
|
160
|
+
// guard block landing mid-flush) — rewriting with the pre-flush snapshot
|
|
161
|
+
// (`overflow`) would silently discard them.
|
|
162
|
+
const flushedIds = new Set(batch.map(e => e.eventId));
|
|
163
|
+
rewriteSpool(readSpool().filter(e => !flushedIds.has(e.eventId)));
|
|
160
164
|
return body.data || { accepted: batch.length, deduped: 0 };
|
|
161
165
|
}
|
|
162
166
|
catch {
|
package/dist/version.json
CHANGED