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/LICENSE +21 -0
- package/dist/index.cjs +199 -15
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +44 -1
- package/dist/index.d.ts +44 -1
- package/dist/index.js +198 -15
- package/dist/index.js.map +1 -1
- package/package.json +9 -9
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
|
-
|
|
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
|
-
|
|
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
|
|
273
|
-
/
|
|
274
|
-
//
|
|
275
|
-
|
|
276
|
-
//
|
|
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
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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
|
|
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_
|
|
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,
|