xploitscan-shared-rules 1.0.0 → 1.1.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 +159 -0
- 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 +158 -0
- 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
|
@@ -5222,6 +5222,163 @@ async function filterFalsePositives(findings, fileContents) {
|
|
|
5222
5222
|
totalBefore
|
|
5223
5223
|
};
|
|
5224
5224
|
}
|
|
5225
|
+
|
|
5226
|
+
// src/entropy-scanner.ts
|
|
5227
|
+
function shannonEntropy(str) {
|
|
5228
|
+
const freq = {};
|
|
5229
|
+
for (const ch of str) {
|
|
5230
|
+
freq[ch] = (freq[ch] || 0) + 1;
|
|
5231
|
+
}
|
|
5232
|
+
const len = str.length;
|
|
5233
|
+
let entropy = 0;
|
|
5234
|
+
for (const count of Object.values(freq)) {
|
|
5235
|
+
const p = count / len;
|
|
5236
|
+
entropy -= p * Math.log2(p);
|
|
5237
|
+
}
|
|
5238
|
+
return entropy;
|
|
5239
|
+
}
|
|
5240
|
+
var SAFE_PATTERNS = [
|
|
5241
|
+
// UUIDs (v1-v5)
|
|
5242
|
+
/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i,
|
|
5243
|
+
// Git commit hashes (long and short)
|
|
5244
|
+
/^[0-9a-f]{40}$/i,
|
|
5245
|
+
/^[0-9a-f]{7,8}$/i,
|
|
5246
|
+
// Hex colors
|
|
5247
|
+
/^#?[0-9a-fA-F]{3,8}$/,
|
|
5248
|
+
// Small base64 (< 20 chars can't be a real key anyway)
|
|
5249
|
+
/^[A-Za-z0-9+/]{1,19}={0,2}$/,
|
|
5250
|
+
// Package versions
|
|
5251
|
+
/^\d+\.\d+\.\d+/,
|
|
5252
|
+
// Integrity-hash prefix (sha256-, sha512-, ...)
|
|
5253
|
+
/^sha\d+-/i,
|
|
5254
|
+
// URLs without credentials in them
|
|
5255
|
+
/^https?:\/\/[^:@]*$/,
|
|
5256
|
+
/^https?:\/\/registry\.npmjs\.org\//,
|
|
5257
|
+
/^https?:\/\/registry\.yarnpkg\.com\//,
|
|
5258
|
+
// Full package integrity hash (sha512-[base64]=)
|
|
5259
|
+
/^sha\d+-[A-Za-z0-9+/=]+$/,
|
|
5260
|
+
// Package tarball URLs
|
|
5261
|
+
/\.tgz$/,
|
|
5262
|
+
/registry.*\/-\/.*\.tgz$/,
|
|
5263
|
+
// ISO dates / times
|
|
5264
|
+
/^\d{4}-\d{2}-\d{2}/,
|
|
5265
|
+
// Locale tags
|
|
5266
|
+
/^[a-z]{2}-[A-Z]{2}$/,
|
|
5267
|
+
// Text encodings
|
|
5268
|
+
/^utf-?8|ascii|latin|iso-8859/i,
|
|
5269
|
+
// MIME types
|
|
5270
|
+
/^(?:application|text|image|audio|video)\//,
|
|
5271
|
+
// CSS keyword values
|
|
5272
|
+
/^(?:inherit|none|auto|block|flex|grid|absolute|relative|fixed|px|em|rem|%)/,
|
|
5273
|
+
// Developer-placeholder tokens
|
|
5274
|
+
/^(?:test|example|sample|demo|placeholder|temp|tmp|foo|bar|baz|lorem|ipsum)/i,
|
|
5275
|
+
// XML / DTD markers
|
|
5276
|
+
/DTD|DOCTYPE|w3\.org|apple\.com\/DTDs/i,
|
|
5277
|
+
/xmlns|schema|xsd|xsi/i,
|
|
5278
|
+
// NEW — added in Wave 3.2 for context-aware FP reduction
|
|
5279
|
+
// ──────────────────────────────────────────────────────
|
|
5280
|
+
// Tailwind JIT / CSS-in-JS class-name fingerprints, e.g. "css-2kx3yr8",
|
|
5281
|
+
// "tw-abc12def", "jss-1a2b3c4d". Typically prefix + 6-12 hex-ish chars.
|
|
5282
|
+
/^(?:css|tw|jss|emotion|styled|mui|chakra)-[a-z0-9]{4,14}$/i,
|
|
5283
|
+
// SVG path data — starts with a path command letter followed by coords.
|
|
5284
|
+
// These can get very long and high-entropy but are never secrets.
|
|
5285
|
+
/^[MmLlHhVvCcSsQqTtAaZz][\d.,\s\-MmLlHhVvCcSsQqTtAaZz]{10,}$/,
|
|
5286
|
+
// Next.js / Vite / webpack content-addressed asset filenames, e.g.
|
|
5287
|
+
// "main.4e5f6a78.js", "chunk-2a3b.js", "_next/static/chunks/pages-xyz".
|
|
5288
|
+
/\.[0-9a-f]{6,16}\.(?:js|css|mjs|woff2?|ttf|png|jpg|svg)(?:\?.*)?$/i,
|
|
5289
|
+
/^_next\//,
|
|
5290
|
+
// Publishable / client-side keys from common vendors. Flagged by their
|
|
5291
|
+
// own service-specific rules if they look wrong, but entropy should NOT
|
|
5292
|
+
// double-flag these. They are designed to ship to the browser.
|
|
5293
|
+
/^pk_(?:live|test|[a-z0-9]+)_/,
|
|
5294
|
+
// Stripe / Clerk publishable
|
|
5295
|
+
/^NEXT_PUBLIC_|^VITE_|^REACT_APP_/,
|
|
5296
|
+
// Build-time public env vars
|
|
5297
|
+
/^pub_/,
|
|
5298
|
+
// Segment etc.
|
|
5299
|
+
/^ey[A-Za-z0-9_-]+\.ey[A-Za-z0-9_-]+\./,
|
|
5300
|
+
// JWT header.payload prefix — don't flag solely on entropy
|
|
5301
|
+
// Content-Security-Policy hashes / nonces in HTML/JSON
|
|
5302
|
+
/^'sha\d+-/,
|
|
5303
|
+
/^nonce-/i,
|
|
5304
|
+
// Embedded data URIs
|
|
5305
|
+
/^data:[a-z]+\//i
|
|
5306
|
+
];
|
|
5307
|
+
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;
|
|
5308
|
+
var SKIP_FILENAMES = /(?:package-lock\.json|pnpm-lock\.yaml|yarn\.lock|composer\.lock|Gemfile\.lock|Cargo\.lock|poetry\.lock|Pipfile\.lock|shrinkwrap\.json)$/i;
|
|
5309
|
+
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;
|
|
5310
|
+
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;
|
|
5311
|
+
var HASH_PREFIX_RE = /^(?:sha\d+|md5|crc32|base64|bcrypt|argon2|pbkdf2|blake2b?)[:_]/i;
|
|
5312
|
+
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;
|
|
5313
|
+
function getSnippet2(content, line) {
|
|
5314
|
+
const lines = content.split("\n");
|
|
5315
|
+
const start = Math.max(0, line - 2);
|
|
5316
|
+
const end = Math.min(lines.length, line + 2);
|
|
5317
|
+
return lines.slice(start, end).map((l, i) => {
|
|
5318
|
+
const lineNum = start + i + 1;
|
|
5319
|
+
const prefix = lineNum === line ? ">" : " ";
|
|
5320
|
+
return `${prefix} ${String(lineNum).padStart(5)} | ${l}`;
|
|
5321
|
+
}).join("\n");
|
|
5322
|
+
}
|
|
5323
|
+
function scanEntropy(files) {
|
|
5324
|
+
const findings = [];
|
|
5325
|
+
for (const { path: filePath, content } of files) {
|
|
5326
|
+
if (SKIP_FILES.test(filePath)) continue;
|
|
5327
|
+
if (SKIP_FILENAMES.test(filePath)) continue;
|
|
5328
|
+
const basename = filePath.split("/").pop() || "";
|
|
5329
|
+
if (SKIP_FILENAMES.test(basename)) continue;
|
|
5330
|
+
if (filePath.includes("node_modules")) continue;
|
|
5331
|
+
if (filePath.includes(".min.")) continue;
|
|
5332
|
+
if (/pro-rules-bundle|\.bundle\.|\.chunk\./i.test(filePath)) continue;
|
|
5333
|
+
if (/(?:\.test\.|\.spec\.|__tests__|__mocks__|fixtures?\/)/i.test(filePath)) continue;
|
|
5334
|
+
const lines = content.split("\n");
|
|
5335
|
+
for (let i = 0; i < lines.length; i++) {
|
|
5336
|
+
const line = lines[i];
|
|
5337
|
+
const trimmed = line.trimStart();
|
|
5338
|
+
if (trimmed.startsWith("//") || trimmed.startsWith("#") || trimmed.startsWith("*") || trimmed.startsWith("/*")) continue;
|
|
5339
|
+
const stringPattern = /(?:[:=]\s*)(["'`])([^"'`\n]{20,120})\1/g;
|
|
5340
|
+
let match;
|
|
5341
|
+
while ((match = stringPattern.exec(line)) !== null) {
|
|
5342
|
+
const value = match[2];
|
|
5343
|
+
if (SAFE_PATTERNS.some((p) => p.test(value))) continue;
|
|
5344
|
+
if (HASH_PREFIX_RE.test(value)) continue;
|
|
5345
|
+
const beforeAssign = line.substring(0, match.index);
|
|
5346
|
+
if (SAFE_VAR_NAMES.test(beforeAssign)) continue;
|
|
5347
|
+
if (/^https?:\/\/[^:@]*$/.test(value)) continue;
|
|
5348
|
+
if ((value.match(/\s/g) || []).length > 2) continue;
|
|
5349
|
+
const isHex = /^[0-9a-fA-F]+$/.test(value);
|
|
5350
|
+
const isBase64 = /^[A-Za-z0-9+/]+=*$/.test(value);
|
|
5351
|
+
let threshold = 4;
|
|
5352
|
+
if (isHex) threshold = 3;
|
|
5353
|
+
else if (isBase64) threshold = 4.5;
|
|
5354
|
+
if (value.length < 20) continue;
|
|
5355
|
+
const entropy = shannonEntropy(value);
|
|
5356
|
+
if (entropy < threshold) continue;
|
|
5357
|
+
const varName = beforeAssign.match(/(\w+)\s*[:=]\s*$/)?.[1] || "";
|
|
5358
|
+
if (HASH_LIKE_VAR_NAMES.test(varName) && (isHex || isBase64)) continue;
|
|
5359
|
+
const isLikelySecret = SECRET_VAR_NAMES.test(varName);
|
|
5360
|
+
if (entropy < 4.5 && !isLikelySecret) continue;
|
|
5361
|
+
const masked = value.substring(0, 6) + "..." + value.substring(value.length - 4);
|
|
5362
|
+
findings.push({
|
|
5363
|
+
id: `ENTROPY-${filePath}:${i + 1}`,
|
|
5364
|
+
rule: "ENTROPY",
|
|
5365
|
+
severity: isLikelySecret ? "critical" : "high",
|
|
5366
|
+
title: "High-Entropy String Detected (Possible Secret)",
|
|
5367
|
+
description: `Found a high-entropy string (${entropy.toFixed(1)} bits) that may be a hardcoded secret or API key: "${masked}"`,
|
|
5368
|
+
file: filePath,
|
|
5369
|
+
line: i + 1,
|
|
5370
|
+
snippet: getSnippet2(content, i + 1),
|
|
5371
|
+
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.",
|
|
5372
|
+
category: "Secrets",
|
|
5373
|
+
source: "custom",
|
|
5374
|
+
owasp: "A02:2021",
|
|
5375
|
+
cwe: "CWE-798"
|
|
5376
|
+
});
|
|
5377
|
+
}
|
|
5378
|
+
}
|
|
5379
|
+
}
|
|
5380
|
+
return findings;
|
|
5381
|
+
}
|
|
5225
5382
|
export {
|
|
5226
5383
|
allCustomRules,
|
|
5227
5384
|
allRules,
|
|
@@ -5351,6 +5508,7 @@ export {
|
|
|
5351
5508
|
regexDos,
|
|
5352
5509
|
runCustomRules,
|
|
5353
5510
|
s3BucketNoEncryption,
|
|
5511
|
+
scanEntropy,
|
|
5354
5512
|
secretInBundleConfig,
|
|
5355
5513
|
secretInCLIArgument,
|
|
5356
5514
|
secretInErrorResponse,
|