xploitscan-shared-rules 1.0.0 → 1.2.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/dist/index.d.cts CHANGED
@@ -246,4 +246,47 @@ interface AIFilterResult {
246
246
  */
247
247
  declare function filterFalsePositives(findings: Finding[], fileContents: Map<string, string>): Promise<AIFilterResult>;
248
248
 
249
- export { type AIFilterResult, type Confidence, type CustomRule, type DetectedFramework, type FilteredFinding, type Finding, type GradeResult, type RuleMatch, type SecurityGrade, type Severity, allCustomRules, allRules, androidDebuggable, blockingMainThread, calculateGrade, callbackHell, clickjacking, clientComponentSecret, clientSideAuth, commandInjection, complianceMap, consoleLogProduction, corsLocalhost, corsServerless, corsWildcard, dangerousInnerHTML, deprecatedTLS, detectFramework, disabledTLSVerification, djangoDebug, dockerCopySensitive, dockerLatestTag, dockerRunAsRoot, dockerTooManyPorts, ecbModeEncryption, electronNavigationUnrestricted, emptyCatchBlock, envNotGitignored, evalUsage, eventListenerLeak, exposedAdminRoutes, exposedAuthSecret, exposedDBCredentials, exposedDatabaseStudio, exposedDebugMode, exposedDockerPorts, exposedEnvFile, exposedGitDir, exposedServerActions, exposedSourceMaps, exposedStackTraces, filterFalsePositives, firebaseClientConfig, flaskSecretKey, freeRules, getSnippet, githubActionsInjection, graphqlIntrospection, hardcodedAnthropicKey, hardcodedDatadogKey, hardcodedEncryptionKey, hardcodedGCPServiceAccount, hardcodedGitHubPAT, hardcodedGitLabToken, hardcodedIPAllowlist, hardcodedJWTSecret, hardcodedMailgunKey, hardcodedOAuthSecret, hardcodedPineconeKey, hardcodedSecrets, hardcodedSendGridKey, hardcodedShopifyToken, hardcodedSlackToken, hardcodedSupabaseServiceRole, hardcodedTwilioKey, hardcodedVaultToken, hardcodedVercelToken, hostHeaderRedirect, httpRequestSmuggling, insecureCookies, insecureDeepLink, insecureDeserialization, insecureDirectObjectReference, insecureElectronWindow, insecureFileUpload, insecureGRPC, insecureHTTPMethods, insecurePasswordReset, insecureRandomness, insecureWebSocket, ipcPathTraversal, javaDeserialization, jwtAlgConfusion, k8sNoResourceLimits, k8sPrivileged, k8sSecretNotEncrypted, lambdaWithoutVPC, largeBundleImport, logInjection, magicNumbers, massAssignment, missingAIRateLimit, missingAuthMiddleware, missingAuthRateLimit, missingBruteForce, missingCSP, missingCSRF, missingCertPinning, missingCloudTrail, missingContentDisposition, missingDBEncryption, missingErrorBoundary, missingFileSizeLimits, missingHSTS, missingHTTPS, missingLockFile, missingOAuthState, missingPagination, missingRequestSizeLimit, missingRequestValidation, missingSRI, missingSecurityMeta, nPlusOneQuery, nextPublicSecret, noRateLimiting, nosqlInjection, openRedirectParams, overlyPermissiveIAM, pathTraversal, pickleDeserialization, piiInLogs, prototypePollution, raceCondition, rdsPubliclyAccessible, reflectedCORSOrigin, regexDos, runCustomRules, s3BucketNoEncryption, secretInBundleConfig, secretInCLIArgument, secretInErrorResponse, secretInHTMLAttribute, secretInURLParam, secretLoggedToConsole, secretsInCI, securityGroupAllInbound, sensitiveAsyncStorage, sensitiveLocalStorage, sensitiveURLParams, sessionFixation, sqlInjection, ssrfVulnerability, ssti, stripeWebhookUnprotected, supabaseAnonAdmin, supabaseNoRLS, syncFileOps, terraformStateExposed, timingAttack, todoLeftInCode, unencryptedPII, unpinnedGitHubAction, unprotectedAPIRoutes, unprotectedDownload, unsafeObjectAssign, unsanitizedFilenames, unsanitizedHTMLExport, unvalidatedAPIParams, unvalidatedEventData, unvalidatedRedirect, vulnerableDependencies, weakHashing, weakPasswordRequirements, weakRSAKeySize, webhookSignatureVerification, xssVulnerability, xxeVulnerability };
249
+ /**
250
+ * Shannon-entropy based secret detection.
251
+ *
252
+ * Catches high-entropy string literals that look like API keys / tokens /
253
+ * credentials even when they don't match any of the service-specific
254
+ * hardcoded-secret rules (VC001, VC132, etc). Fires a single `ENTROPY`
255
+ * finding per suspicious literal.
256
+ *
257
+ * The trick to keeping this useful is keeping false positives down. Real
258
+ * codebases are full of high-entropy strings that aren't secrets — SHA-256
259
+ * hashes, UUIDs, base64 integrity hashes, Tailwind-generated class names,
260
+ * SVG path data, Next.js content-addressed filenames, publishable
261
+ * (intentionally-public) keys from Clerk/Stripe/etc. This scanner has three
262
+ * layers of suppression before emitting a finding:
263
+ *
264
+ * 1. File-level: skip lockfiles, CSS/SVG/image/map files, node_modules,
265
+ * minified/bundled output.
266
+ * 2. Shape-level: if the string matches a known-safe shape (UUID, Git SHA,
267
+ * integrity hash, publishable key prefix, Tailwind fingerprint, SVG
268
+ * path data, Next.js content hash), skip.
269
+ * 3. Context-level: inspect the assignment context — if the variable name
270
+ * implies "this holds a hash/digest/fingerprint" (not a secret), skip.
271
+ * If the variable name implies a secret (key/token/etc), lower the
272
+ * entropy bar. Otherwise require high entropy AND no safe signals.
273
+ *
274
+ * When in doubt we bias toward suppressing — one FP every 100 scans is much
275
+ * more costly to the product than one missed secret, because the expanded
276
+ * VC132-VC151 service-specific rules already cover the most common key
277
+ * formats.
278
+ */
279
+
280
+ /**
281
+ * Scan a batch of files for high-entropy string literals.
282
+ *
283
+ * Each element in `files` must be `{ path, content }`. Returns a list of
284
+ * `Finding` objects for suspicious string literals. See the file header for
285
+ * the suppression layers.
286
+ */
287
+ declare function scanEntropy(files: {
288
+ path: string;
289
+ content: string;
290
+ }[]): Finding[];
291
+
292
+ export { type AIFilterResult, type Confidence, type CustomRule, type DetectedFramework, type FilteredFinding, type Finding, type GradeResult, type RuleMatch, type SecurityGrade, type Severity, allCustomRules, allRules, androidDebuggable, blockingMainThread, calculateGrade, callbackHell, clickjacking, clientComponentSecret, clientSideAuth, commandInjection, complianceMap, consoleLogProduction, corsLocalhost, corsServerless, corsWildcard, dangerousInnerHTML, deprecatedTLS, detectFramework, disabledTLSVerification, djangoDebug, dockerCopySensitive, dockerLatestTag, dockerRunAsRoot, dockerTooManyPorts, ecbModeEncryption, electronNavigationUnrestricted, emptyCatchBlock, envNotGitignored, evalUsage, eventListenerLeak, exposedAdminRoutes, exposedAuthSecret, exposedDBCredentials, exposedDatabaseStudio, exposedDebugMode, exposedDockerPorts, exposedEnvFile, exposedGitDir, exposedServerActions, exposedSourceMaps, exposedStackTraces, filterFalsePositives, firebaseClientConfig, flaskSecretKey, freeRules, getSnippet, githubActionsInjection, graphqlIntrospection, hardcodedAnthropicKey, hardcodedDatadogKey, hardcodedEncryptionKey, hardcodedGCPServiceAccount, hardcodedGitHubPAT, hardcodedGitLabToken, hardcodedIPAllowlist, hardcodedJWTSecret, hardcodedMailgunKey, hardcodedOAuthSecret, hardcodedPineconeKey, hardcodedSecrets, hardcodedSendGridKey, hardcodedShopifyToken, hardcodedSlackToken, hardcodedSupabaseServiceRole, hardcodedTwilioKey, hardcodedVaultToken, hardcodedVercelToken, hostHeaderRedirect, httpRequestSmuggling, insecureCookies, insecureDeepLink, insecureDeserialization, insecureDirectObjectReference, insecureElectronWindow, insecureFileUpload, insecureGRPC, insecureHTTPMethods, insecurePasswordReset, insecureRandomness, insecureWebSocket, ipcPathTraversal, javaDeserialization, jwtAlgConfusion, k8sNoResourceLimits, k8sPrivileged, k8sSecretNotEncrypted, lambdaWithoutVPC, largeBundleImport, logInjection, magicNumbers, massAssignment, missingAIRateLimit, missingAuthMiddleware, missingAuthRateLimit, missingBruteForce, missingCSP, missingCSRF, missingCertPinning, missingCloudTrail, missingContentDisposition, missingDBEncryption, missingErrorBoundary, missingFileSizeLimits, missingHSTS, missingHTTPS, missingLockFile, missingOAuthState, missingPagination, missingRequestSizeLimit, missingRequestValidation, missingSRI, missingSecurityMeta, nPlusOneQuery, nextPublicSecret, noRateLimiting, nosqlInjection, openRedirectParams, overlyPermissiveIAM, pathTraversal, pickleDeserialization, piiInLogs, prototypePollution, raceCondition, rdsPubliclyAccessible, reflectedCORSOrigin, regexDos, runCustomRules, s3BucketNoEncryption, scanEntropy, secretInBundleConfig, secretInCLIArgument, secretInErrorResponse, secretInHTMLAttribute, secretInURLParam, secretLoggedToConsole, secretsInCI, securityGroupAllInbound, sensitiveAsyncStorage, sensitiveLocalStorage, sensitiveURLParams, sessionFixation, sqlInjection, ssrfVulnerability, ssti, stripeWebhookUnprotected, supabaseAnonAdmin, supabaseNoRLS, syncFileOps, terraformStateExposed, timingAttack, todoLeftInCode, unencryptedPII, unpinnedGitHubAction, unprotectedAPIRoutes, unprotectedDownload, unsafeObjectAssign, unsanitizedFilenames, unsanitizedHTMLExport, unvalidatedAPIParams, unvalidatedEventData, unvalidatedRedirect, vulnerableDependencies, weakHashing, weakPasswordRequirements, weakRSAKeySize, webhookSignatureVerification, xssVulnerability, xxeVulnerability };
package/dist/index.d.ts CHANGED
@@ -246,4 +246,47 @@ interface AIFilterResult {
246
246
  */
247
247
  declare function filterFalsePositives(findings: Finding[], fileContents: Map<string, string>): Promise<AIFilterResult>;
248
248
 
249
- export { type AIFilterResult, type Confidence, type CustomRule, type DetectedFramework, type FilteredFinding, type Finding, type GradeResult, type RuleMatch, type SecurityGrade, type Severity, allCustomRules, allRules, androidDebuggable, blockingMainThread, calculateGrade, callbackHell, clickjacking, clientComponentSecret, clientSideAuth, commandInjection, complianceMap, consoleLogProduction, corsLocalhost, corsServerless, corsWildcard, dangerousInnerHTML, deprecatedTLS, detectFramework, disabledTLSVerification, djangoDebug, dockerCopySensitive, dockerLatestTag, dockerRunAsRoot, dockerTooManyPorts, ecbModeEncryption, electronNavigationUnrestricted, emptyCatchBlock, envNotGitignored, evalUsage, eventListenerLeak, exposedAdminRoutes, exposedAuthSecret, exposedDBCredentials, exposedDatabaseStudio, exposedDebugMode, exposedDockerPorts, exposedEnvFile, exposedGitDir, exposedServerActions, exposedSourceMaps, exposedStackTraces, filterFalsePositives, firebaseClientConfig, flaskSecretKey, freeRules, getSnippet, githubActionsInjection, graphqlIntrospection, hardcodedAnthropicKey, hardcodedDatadogKey, hardcodedEncryptionKey, hardcodedGCPServiceAccount, hardcodedGitHubPAT, hardcodedGitLabToken, hardcodedIPAllowlist, hardcodedJWTSecret, hardcodedMailgunKey, hardcodedOAuthSecret, hardcodedPineconeKey, hardcodedSecrets, hardcodedSendGridKey, hardcodedShopifyToken, hardcodedSlackToken, hardcodedSupabaseServiceRole, hardcodedTwilioKey, hardcodedVaultToken, hardcodedVercelToken, hostHeaderRedirect, httpRequestSmuggling, insecureCookies, insecureDeepLink, insecureDeserialization, insecureDirectObjectReference, insecureElectronWindow, insecureFileUpload, insecureGRPC, insecureHTTPMethods, insecurePasswordReset, insecureRandomness, insecureWebSocket, ipcPathTraversal, javaDeserialization, jwtAlgConfusion, k8sNoResourceLimits, k8sPrivileged, k8sSecretNotEncrypted, lambdaWithoutVPC, largeBundleImport, logInjection, magicNumbers, massAssignment, missingAIRateLimit, missingAuthMiddleware, missingAuthRateLimit, missingBruteForce, missingCSP, missingCSRF, missingCertPinning, missingCloudTrail, missingContentDisposition, missingDBEncryption, missingErrorBoundary, missingFileSizeLimits, missingHSTS, missingHTTPS, missingLockFile, missingOAuthState, missingPagination, missingRequestSizeLimit, missingRequestValidation, missingSRI, missingSecurityMeta, nPlusOneQuery, nextPublicSecret, noRateLimiting, nosqlInjection, openRedirectParams, overlyPermissiveIAM, pathTraversal, pickleDeserialization, piiInLogs, prototypePollution, raceCondition, rdsPubliclyAccessible, reflectedCORSOrigin, regexDos, runCustomRules, s3BucketNoEncryption, secretInBundleConfig, secretInCLIArgument, secretInErrorResponse, secretInHTMLAttribute, secretInURLParam, secretLoggedToConsole, secretsInCI, securityGroupAllInbound, sensitiveAsyncStorage, sensitiveLocalStorage, sensitiveURLParams, sessionFixation, sqlInjection, ssrfVulnerability, ssti, stripeWebhookUnprotected, supabaseAnonAdmin, supabaseNoRLS, syncFileOps, terraformStateExposed, timingAttack, todoLeftInCode, unencryptedPII, unpinnedGitHubAction, unprotectedAPIRoutes, unprotectedDownload, unsafeObjectAssign, unsanitizedFilenames, unsanitizedHTMLExport, unvalidatedAPIParams, unvalidatedEventData, unvalidatedRedirect, vulnerableDependencies, weakHashing, weakPasswordRequirements, weakRSAKeySize, webhookSignatureVerification, xssVulnerability, xxeVulnerability };
249
+ /**
250
+ * Shannon-entropy based secret detection.
251
+ *
252
+ * Catches high-entropy string literals that look like API keys / tokens /
253
+ * credentials even when they don't match any of the service-specific
254
+ * hardcoded-secret rules (VC001, VC132, etc). Fires a single `ENTROPY`
255
+ * finding per suspicious literal.
256
+ *
257
+ * The trick to keeping this useful is keeping false positives down. Real
258
+ * codebases are full of high-entropy strings that aren't secrets — SHA-256
259
+ * hashes, UUIDs, base64 integrity hashes, Tailwind-generated class names,
260
+ * SVG path data, Next.js content-addressed filenames, publishable
261
+ * (intentionally-public) keys from Clerk/Stripe/etc. This scanner has three
262
+ * layers of suppression before emitting a finding:
263
+ *
264
+ * 1. File-level: skip lockfiles, CSS/SVG/image/map files, node_modules,
265
+ * minified/bundled output.
266
+ * 2. Shape-level: if the string matches a known-safe shape (UUID, Git SHA,
267
+ * integrity hash, publishable key prefix, Tailwind fingerprint, SVG
268
+ * path data, Next.js content hash), skip.
269
+ * 3. Context-level: inspect the assignment context — if the variable name
270
+ * implies "this holds a hash/digest/fingerprint" (not a secret), skip.
271
+ * If the variable name implies a secret (key/token/etc), lower the
272
+ * entropy bar. Otherwise require high entropy AND no safe signals.
273
+ *
274
+ * When in doubt we bias toward suppressing — one FP every 100 scans is much
275
+ * more costly to the product than one missed secret, because the expanded
276
+ * VC132-VC151 service-specific rules already cover the most common key
277
+ * formats.
278
+ */
279
+
280
+ /**
281
+ * Scan a batch of files for high-entropy string literals.
282
+ *
283
+ * Each element in `files` must be `{ path, content }`. Returns a list of
284
+ * `Finding` objects for suspicious string literals. See the file header for
285
+ * the suppression layers.
286
+ */
287
+ declare function scanEntropy(files: {
288
+ path: string;
289
+ content: string;
290
+ }[]): Finding[];
291
+
292
+ export { type AIFilterResult, type Confidence, type CustomRule, type DetectedFramework, type FilteredFinding, type Finding, type GradeResult, type RuleMatch, type SecurityGrade, type Severity, allCustomRules, allRules, androidDebuggable, blockingMainThread, calculateGrade, callbackHell, clickjacking, clientComponentSecret, clientSideAuth, commandInjection, complianceMap, consoleLogProduction, corsLocalhost, corsServerless, corsWildcard, dangerousInnerHTML, deprecatedTLS, detectFramework, disabledTLSVerification, djangoDebug, dockerCopySensitive, dockerLatestTag, dockerRunAsRoot, dockerTooManyPorts, ecbModeEncryption, electronNavigationUnrestricted, emptyCatchBlock, envNotGitignored, evalUsage, eventListenerLeak, exposedAdminRoutes, exposedAuthSecret, exposedDBCredentials, exposedDatabaseStudio, exposedDebugMode, exposedDockerPorts, exposedEnvFile, exposedGitDir, exposedServerActions, exposedSourceMaps, exposedStackTraces, filterFalsePositives, firebaseClientConfig, flaskSecretKey, freeRules, getSnippet, githubActionsInjection, graphqlIntrospection, hardcodedAnthropicKey, hardcodedDatadogKey, hardcodedEncryptionKey, hardcodedGCPServiceAccount, hardcodedGitHubPAT, hardcodedGitLabToken, hardcodedIPAllowlist, hardcodedJWTSecret, hardcodedMailgunKey, hardcodedOAuthSecret, hardcodedPineconeKey, hardcodedSecrets, hardcodedSendGridKey, hardcodedShopifyToken, hardcodedSlackToken, hardcodedSupabaseServiceRole, hardcodedTwilioKey, hardcodedVaultToken, hardcodedVercelToken, hostHeaderRedirect, httpRequestSmuggling, insecureCookies, insecureDeepLink, insecureDeserialization, insecureDirectObjectReference, insecureElectronWindow, insecureFileUpload, insecureGRPC, insecureHTTPMethods, insecurePasswordReset, insecureRandomness, insecureWebSocket, ipcPathTraversal, javaDeserialization, jwtAlgConfusion, k8sNoResourceLimits, k8sPrivileged, k8sSecretNotEncrypted, lambdaWithoutVPC, largeBundleImport, logInjection, magicNumbers, massAssignment, missingAIRateLimit, missingAuthMiddleware, missingAuthRateLimit, missingBruteForce, missingCSP, missingCSRF, missingCertPinning, missingCloudTrail, missingContentDisposition, missingDBEncryption, missingErrorBoundary, missingFileSizeLimits, missingHSTS, missingHTTPS, missingLockFile, missingOAuthState, missingPagination, missingRequestSizeLimit, missingRequestValidation, missingSRI, missingSecurityMeta, nPlusOneQuery, nextPublicSecret, noRateLimiting, nosqlInjection, openRedirectParams, overlyPermissiveIAM, pathTraversal, pickleDeserialization, piiInLogs, prototypePollution, raceCondition, rdsPubliclyAccessible, reflectedCORSOrigin, regexDos, runCustomRules, s3BucketNoEncryption, scanEntropy, secretInBundleConfig, secretInCLIArgument, secretInErrorResponse, secretInHTMLAttribute, secretInURLParam, secretLoggedToConsole, secretsInCI, securityGroupAllInbound, sensitiveAsyncStorage, sensitiveLocalStorage, sensitiveURLParams, sessionFixation, sqlInjection, ssrfVulnerability, ssti, stripeWebhookUnprotected, supabaseAnonAdmin, supabaseNoRLS, syncFileOps, terraformStateExposed, timingAttack, todoLeftInCode, unencryptedPII, unpinnedGitHubAction, unprotectedAPIRoutes, unprotectedDownload, unsafeObjectAssign, unsanitizedFilenames, unsanitizedHTMLExport, unvalidatedAPIParams, unvalidatedEventData, unvalidatedRedirect, vulnerableDependencies, weakHashing, weakPasswordRequirements, weakRSAKeySize, webhookSignatureVerification, xssVulnerability, xxeVulnerability };
package/dist/index.js CHANGED
@@ -269,26 +269,42 @@ var sqlInjection = {
269
269
  description: "String concatenation or template literals in SQL queries allow attackers to execute arbitrary database commands.",
270
270
  check(content, filePath) {
271
271
  const patterns = [
272
- // Template literals in SQL
273
- /(?:query|execute|raw|sql)\s*\(\s*`[^`]*\$\{/gi,
274
- // String concatenation in SQL
275
- /(?:query|execute)\s*\(\s*["'][^"']*["']\s*\+/gi,
276
- // Direct variable interpolation
272
+ // Template literal passed as first arg to query/execute/raw/sql/
273
+ // queryRaw/queryRawUnsafe/execute. Examples that SHOULD fire:
274
+ // db.query(`SELECT ... ${x}`)
275
+ // prisma.$queryRawUnsafe(`... ${x}`)
276
+ // knex.raw(`... ${x}`)
277
+ /(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*`[^`]*\$\{/gi,
278
+ // String concatenation in SQL — now includes raw() and sql() and
279
+ // Drizzle's sql.raw() / Prisma's $queryRawUnsafe() variants. Previous
280
+ // version only covered query()/execute() and missed knex.raw("..." + x).
281
+ //
282
+ // The string literal part uses a proper "quoted string" regex that
283
+ // allows the opposite quote type inside (common in SQL — "WHERE x = 'a'").
284
+ // The older `[^"']*` class bailed out at the first inner quote and
285
+ // missed every realistic SQL-containing concat.
286
+ /(?:query|execute|raw|sql|queryRaw|queryRawUnsafe|executeRaw|executeRawUnsafe)\s*\(\s*(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')\s*\+/gi,
287
+ // Direct variable interpolation in a SQL verb context
277
288
  /(?:SELECT|INSERT|UPDATE|DELETE|WHERE)\s+.*\$\{(?!.*parameterized)/gi
278
289
  ];
279
290
  const matches = [];
280
291
  const usesParams = /\?\s*,|\$\d+|:[\w]+|\bprepare\b|\bplaceholder\b/i.test(content);
281
292
  if (usesParams) return [];
282
293
  for (const pattern of patterns) {
283
- matches.push(
284
- ...findMatches(
285
- content,
286
- pattern,
287
- sqlInjection,
288
- filePath,
289
- () => "Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
290
- )
294
+ const raw = findMatches(
295
+ content,
296
+ pattern,
297
+ sqlInjection,
298
+ filePath,
299
+ () => "Use parameterized queries or prepared statements instead of string interpolation. Example: db.query('SELECT * FROM users WHERE id = ?', [userId])"
291
300
  );
301
+ for (const m of raw) {
302
+ const lineText = content.split("\n")[m.line - 1] || "";
303
+ if (/\bPrisma\.sql\s*`/.test(lineText)) continue;
304
+ if (/\bsql\s*`/.test(lineText) && !/\bsql\.raw\s*\(/.test(lineText)) continue;
305
+ if (/\$queryRaw\s*\(\s*Prisma\.sql|\$executeRaw\s*\(\s*Prisma\.sql/.test(lineText)) continue;
306
+ matches.push(m);
307
+ }
292
308
  }
293
309
  return matches;
294
310
  }
@@ -2829,13 +2845,22 @@ var commandInjection = {
2829
2845
  // Node.js — require standalone exec/execSync, not db.exec() or conn.exec()
2830
2846
  /(?<![.\w])(?:exec|execSync)\s*\(\s*(?:`[^`]*\$\{|["'][^"']*\+\s*(?:req\.|body\.|input|params|args|user))/gi,
2831
2847
  /child_process.*exec\s*\(\s*(?!["'`])/g,
2848
+ // spawn / execFile / exec with shell: true AND a template literal
2849
+ // first arg. `shell: true` converts the first argument into a string
2850
+ // passed to `/bin/sh -c`, so any ${} interpolation is a shell injection
2851
+ // opportunity. Without shell: true, spawn/execFile are safe because
2852
+ // the command and args are kept separate. Previously this class of
2853
+ // bug was missed — the "hasSafe" skip below assumed any spawn in the
2854
+ // file was fine, which is the opposite of true with shell: true.
2855
+ /(?<![.\w])(?:spawn|spawnSync|execFile|execFileSync|exec|execSync)\s*\(\s*`[^`]*\$\{[\s\S]*?shell\s*:\s*(?:true|["'][^"']+["'])/gi,
2832
2856
  // Python
2833
2857
  /os\.system\s*\(\s*(?!["'`].*["'`]\s*\))/g,
2834
2858
  /subprocess\.(?:call|run|Popen)\s*\([^)]*shell\s*=\s*True/gi,
2835
2859
  // Ruby
2836
2860
  /system\s*\(\s*["'].*#\{/g
2837
2861
  ];
2838
- const hasSafe = /execFile|spawn|escapeshellarg|shlex\.quote|shellEscape/i.test(content);
2862
+ const hasShellTrue = /shell\s*:\s*(?:true|["'][^"']+["'])/i.test(content);
2863
+ const hasSafe = !hasShellTrue && /execFile|spawn|escapeshellarg|shlex\.quote|shellEscape/i.test(content);
2839
2864
  if (hasSafe) return [];
2840
2865
  for (const p of patterns) {
2841
2866
  const raw = findMatches(
@@ -4336,7 +4361,7 @@ var hardcodedSupabaseServiceRole = {
4336
4361
  if (isTestFile(filePath)) return [];
4337
4362
  if (!SECRET_FILE_EXT.test(filePath)) return [];
4338
4363
  if (LOCK_FILE_RE.test(filePath)) return [];
4339
- const pattern = /(?:service[_-]?role[_-]?key|SUPABASE_SERVICE_ROLE_KEY|supabaseServiceRole)\s*[:=]\s*["'`](eyJ[A-Za-z0-9_\-]{50,})["'`]/gi;
4364
+ const pattern = /(?:service[_-]?role[_-]?key|SUPABASE_SERVICE_ROLE_KEY|supabaseServiceRole)\s*[:=]\s*["'`](eyJ[A-Za-z0-9_\-.]{50,})["'`]/gi;
4340
4365
  const findings = [];
4341
4366
  let m;
4342
4367
  while ((m = pattern.exec(content)) !== null) {
@@ -5222,6 +5247,163 @@ async function filterFalsePositives(findings, fileContents) {
5222
5247
  totalBefore
5223
5248
  };
5224
5249
  }
5250
+
5251
+ // src/entropy-scanner.ts
5252
+ function shannonEntropy(str) {
5253
+ const freq = {};
5254
+ for (const ch of str) {
5255
+ freq[ch] = (freq[ch] || 0) + 1;
5256
+ }
5257
+ const len = str.length;
5258
+ let entropy = 0;
5259
+ for (const count of Object.values(freq)) {
5260
+ const p = count / len;
5261
+ entropy -= p * Math.log2(p);
5262
+ }
5263
+ return entropy;
5264
+ }
5265
+ var SAFE_PATTERNS = [
5266
+ // UUIDs (v1-v5)
5267
+ /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
5268
+ // Git commit hashes (long and short)
5269
+ /^[0-9a-f]{40}$/i,
5270
+ /^[0-9a-f]{7,8}$/i,
5271
+ // Hex colors
5272
+ /^#?[0-9a-fA-F]{3,8}$/,
5273
+ // Small base64 (< 20 chars can't be a real key anyway)
5274
+ /^[A-Za-z0-9+/]{1,19}={0,2}$/,
5275
+ // Package versions
5276
+ /^\d+\.\d+\.\d+/,
5277
+ // Integrity-hash prefix (sha256-, sha512-, ...)
5278
+ /^sha\d+-/i,
5279
+ // URLs without credentials in them
5280
+ /^https?:\/\/[^:@]*$/,
5281
+ /^https?:\/\/registry\.npmjs\.org\//,
5282
+ /^https?:\/\/registry\.yarnpkg\.com\//,
5283
+ // Full package integrity hash (sha512-[base64]=)
5284
+ /^sha\d+-[A-Za-z0-9+/=]+$/,
5285
+ // Package tarball URLs
5286
+ /\.tgz$/,
5287
+ /registry.*\/-\/.*\.tgz$/,
5288
+ // ISO dates / times
5289
+ /^\d{4}-\d{2}-\d{2}/,
5290
+ // Locale tags
5291
+ /^[a-z]{2}-[A-Z]{2}$/,
5292
+ // Text encodings
5293
+ /^utf-?8|ascii|latin|iso-8859/i,
5294
+ // MIME types
5295
+ /^(?:application|text|image|audio|video)\//,
5296
+ // CSS keyword values
5297
+ /^(?:inherit|none|auto|block|flex|grid|absolute|relative|fixed|px|em|rem|%)/,
5298
+ // Developer-placeholder tokens
5299
+ /^(?:test|example|sample|demo|placeholder|temp|tmp|foo|bar|baz|lorem|ipsum)/i,
5300
+ // XML / DTD markers
5301
+ /DTD|DOCTYPE|w3\.org|apple\.com\/DTDs/i,
5302
+ /xmlns|schema|xsd|xsi/i,
5303
+ // NEW — added in Wave 3.2 for context-aware FP reduction
5304
+ // ──────────────────────────────────────────────────────
5305
+ // Tailwind JIT / CSS-in-JS class-name fingerprints, e.g. "css-2kx3yr8",
5306
+ // "tw-abc12def", "jss-1a2b3c4d". Typically prefix + 6-12 hex-ish chars.
5307
+ /^(?:css|tw|jss|emotion|styled|mui|chakra)-[a-z0-9]{4,14}$/i,
5308
+ // SVG path data — starts with a path command letter followed by coords.
5309
+ // These can get very long and high-entropy but are never secrets.
5310
+ /^[MmLlHhVvCcSsQqTtAaZz][\d.,\s\-MmLlHhVvCcSsQqTtAaZz]{10,}$/,
5311
+ // Next.js / Vite / webpack content-addressed asset filenames, e.g.
5312
+ // "main.4e5f6a78.js", "chunk-2a3b.js", "_next/static/chunks/pages-xyz".
5313
+ /\.[0-9a-f]{6,16}\.(?:js|css|mjs|woff2?|ttf|png|jpg|svg)(?:\?.*)?$/i,
5314
+ /^_next\//,
5315
+ // Publishable / client-side keys from common vendors. Flagged by their
5316
+ // own service-specific rules if they look wrong, but entropy should NOT
5317
+ // double-flag these. They are designed to ship to the browser.
5318
+ /^pk_(?:live|test|[a-z0-9]+)_/,
5319
+ // Stripe / Clerk publishable
5320
+ /^NEXT_PUBLIC_|^VITE_|^REACT_APP_/,
5321
+ // Build-time public env vars
5322
+ /^pub_/,
5323
+ // Segment etc.
5324
+ /^ey[A-Za-z0-9_-]+\.ey[A-Za-z0-9_-]+\./,
5325
+ // JWT header.payload prefix — don't flag solely on entropy
5326
+ // Content-Security-Policy hashes / nonces in HTML/JSON
5327
+ /^'sha\d+-/,
5328
+ /^nonce-/i,
5329
+ // Embedded data URIs
5330
+ /^data:[a-z]+\//i
5331
+ ];
5332
+ var SKIP_FILES = /\.(css|scss|less|svg|md|txt|html?|xml|yml|yaml|toml|lock|map|woff2?|ttf|eot|ico|png|jpg|gif|webp)$/i;
5333
+ var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
5334
+ var SAFE_VAR_NAMES = /(?:description|message|text|label|title|content|template|html|svg|css|style|class(?:Name)?|query|mutation|schema|regex|pattern|format|placeholder|comment|url|path|route|endpoint|href|src|alt|name|type|version|encoding|charset|locale|translation|copy|prose|markdown|slug|handle)/i;
5335
+ var HASH_LIKE_VAR_NAMES = /(?:^|[^a-z])(?:hash|digest|checksum|fingerprint|etag|crc|md5|sha1|sha256|sha512|contenthash|buildid|revision|commit|sri|integrity|cacheKey|fileHash|assetId|versionId)(?:$|[^a-z])/i;
5336
+ var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
5337
+ var SECRET_VAR_NAMES = /(?:^|[^a-z])(?:key|secret|token|password|passwd|pwd|api[_-]?key|auth|credential|private|signing|bearer|access[_-]?token|refresh[_-]?token|session[_-]?id)(?:$|[^a-z])/i;
5338
+ function getSnippet2(content, line) {
5339
+ const lines = content.split("\n");
5340
+ const start = Math.max(0, line - 2);
5341
+ const end = Math.min(lines.length, line + 2);
5342
+ return lines.slice(start, end).map((l, i) => {
5343
+ const lineNum = start + i + 1;
5344
+ const prefix = lineNum === line ? ">" : " ";
5345
+ return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;
5346
+ }).join("\n");
5347
+ }
5348
+ function scanEntropy(files) {
5349
+ const findings = [];
5350
+ for (const { path: filePath, content } of files) {
5351
+ if (SKIP_FILES.test(filePath)) continue;
5352
+ if (SKIP_FILENAMES.test(filePath)) continue;
5353
+ const basename = filePath.split("/").pop() || "";
5354
+ if (SKIP_FILENAMES.test(basename)) continue;
5355
+ if (filePath.includes("node_modules")) continue;
5356
+ if (filePath.includes(".min.")) continue;
5357
+ if (/pro-rules-bundle|\.bundle\.|\.chunk\./i.test(filePath)) continue;
5358
+ if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
5359
+ const lines = content.split("\n");
5360
+ for (let i = 0; i < lines.length; i++) {
5361
+ const line = lines[i];
5362
+ const trimmed = line.trimStart();
5363
+ if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
5364
+ const stringPattern = /(?:[:=]\s*)(["'`])([^"'`\n]{20,120})\1/g;
5365
+ let match;
5366
+ while ((match = stringPattern.exec(line)) !== null) {
5367
+ const value = match[2];
5368
+ if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
5369
+ if (HASH_PREFIX_RE.test(value)) continue;
5370
+ const beforeAssign = line.substring(0, match.index);
5371
+ if (SAFE_VAR_NAMES.test(beforeAssign)) continue;
5372
+ if (/^https?:\/\/[^:@]*$/.test(value)) continue;
5373
+ if ((value.match(/\s/g) || []).length > 2) continue;
5374
+ const isHex = /^[0-9a-fA-F]+$/.test(value);
5375
+ const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);
5376
+ let threshold = 4;
5377
+ if (isHex) threshold = 3;
5378
+ else if (isBase64) threshold = 4.5;
5379
+ if (value.length < 20) continue;
5380
+ const entropy = shannonEntropy(value);
5381
+ if (entropy < threshold) continue;
5382
+ const varName = beforeAssign.match(/(\w+)\s*[:=]\s*$/)?.[1] || "";
5383
+ if (HASH_LIKE_VAR_NAMES.test(varName) && (isHex || isBase64)) continue;
5384
+ const isLikelySecret = SECRET_VAR_NAMES.test(varName);
5385
+ if (entropy < 4.5 && !isLikelySecret) continue;
5386
+ const masked = value.substring(0, 6) + "..." + value.substring(value.length - 4);
5387
+ findings.push({
5388
+ id: `ENTROPY-${filePath}:${i + 1}`,
5389
+ rule: "ENTROPY",
5390
+ severity: isLikelySecret ? "critical" : "high",
5391
+ title: "High-Entropy String Detected (Possible Secret)",
5392
+ description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: "${masked}"`,
5393
+ file: filePath,
5394
+ line: i + 1,
5395
+ snippet: getSnippet2(content, i + 1),
5396
+ fix: "If this is a secret, move it to an environment variable. If it's not a secret (e.g., hash, encoded data), add it to .xploitscanignore.",
5397
+ category: "Secrets",
5398
+ source: "custom",
5399
+ owasp: "A02:2021",
5400
+ cwe: "CWE-798"
5401
+ });
5402
+ }
5403
+ }
5404
+ }
5405
+ return findings;
5406
+ }
5225
5407
  export {
5226
5408
  allCustomRules,
5227
5409
  allRules,
@@ -5351,6 +5533,7 @@ export {
5351
5533
  regexDos,
5352
5534
  runCustomRules,
5353
5535
  s3BucketNoEncryption,
5536
+ scanEntropy,
5354
5537
  secretInBundleConfig,
5355
5538
  secretInCLIArgument,
5356
5539
  secretInErrorResponse,