xploitscan-shared-rules 1.2.1 → 1.2.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.
- package/LICENSE +1 -1
- package/README.md +58 -0
- package/dist/index.cjs +1077 -134
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +115 -1
- package/dist/index.d.ts +115 -1
- package/dist/index.js +1069 -134
- package/dist/index.js.map +1 -1
- package/package.json +9 -3
package/dist/index.d.cts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ParseResult } from '@babel/parser';
|
|
2
|
+
import { File, Node, CallExpression, ObjectExpression, BinaryExpression } from '@babel/types';
|
|
3
|
+
|
|
1
4
|
type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
2
5
|
type Confidence = "high" | "medium" | "low";
|
|
3
6
|
interface Finding {
|
|
@@ -289,4 +292,115 @@ declare function scanEntropy(files: {
|
|
|
289
292
|
content: string;
|
|
290
293
|
}[]): Finding[];
|
|
291
294
|
|
|
292
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Centralized Babel parser with per-file caching.
|
|
297
|
+
*
|
|
298
|
+
* Rules that opt in to AST analysis call parseFile(content, filename) to get
|
|
299
|
+
* a Program node back. Parsing is expensive (~5-50ms per KB of source); the
|
|
300
|
+
* LRU cache ensures a single scan pass touches each file at most once even
|
|
301
|
+
* when multiple rules walk the same AST.
|
|
302
|
+
*
|
|
303
|
+
* Failure mode: returns null on parse errors. Callers fall back to regex-only
|
|
304
|
+
* behavior so a syntax-error file in the user's repo can never break a scan.
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
interface ParsedFile {
|
|
308
|
+
ast: ParseResult<File>;
|
|
309
|
+
language: "js" | "ts" | "jsx" | "tsx";
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Parse a JS/TS file. Returns null for unsupported extensions, parse errors,
|
|
313
|
+
* or empty content. Callers MUST handle null and fall back to regex-only.
|
|
314
|
+
*/
|
|
315
|
+
declare function parseFile(content: string, filename: string): ParsedFile | null;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Lightweight taint tracking for AST-based rules.
|
|
319
|
+
*
|
|
320
|
+
* "Tainted" means "derived from untrusted input" — an Express/Next request
|
|
321
|
+
* body, query string, params, or headers; process.argv; environment
|
|
322
|
+
* variables known to be user-controlled; or file contents read at runtime.
|
|
323
|
+
*
|
|
324
|
+
* This is deliberately a local-only analysis: we walk the AST of one file,
|
|
325
|
+
* track which identifiers bind to a tainted source, and propagate through
|
|
326
|
+
* direct assignments and destructuring. Cross-function and cross-file flow
|
|
327
|
+
* is out of scope for Phase A — those require a module graph we don't build
|
|
328
|
+
* yet. A conservative local analysis is still enough to flip the benchmark
|
|
329
|
+
* recall on the data-flow rules.
|
|
330
|
+
*
|
|
331
|
+
* The analysis returns a TaintMap the rules can query:
|
|
332
|
+
* isTainted(node) → "is this AST expression tainted?"
|
|
333
|
+
* isTaintedIdent(name) → "is this identifier currently bound to tainted
|
|
334
|
+
* data in the file-level scope?"
|
|
335
|
+
*
|
|
336
|
+
* Shape of recognized sources:
|
|
337
|
+
* req.body, req.query, req.params, req.headers, req.cookies
|
|
338
|
+
* request.body, request.query, ... (Fastify)
|
|
339
|
+
* ctx.request.body, ctx.query (Koa)
|
|
340
|
+
* process.argv, process.env[Keys known to be user-controllable]
|
|
341
|
+
* event.queryStringParameters, event.body (AWS Lambda / API Gateway)
|
|
342
|
+
* searchParams.get(...), url.searchParams (Next.js / Web fetch API)
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
interface TaintMap {
|
|
346
|
+
/** true iff the given expression node evaluates to tainted data. */
|
|
347
|
+
isTainted(node: Node | null | undefined): boolean;
|
|
348
|
+
/** true iff the named identifier is bound to tainted data somewhere visible. */
|
|
349
|
+
isTaintedIdent(name: string): boolean;
|
|
350
|
+
/** Debug: all identifiers the analysis marked tainted, in discovery order. */
|
|
351
|
+
taintedNames(): string[];
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Build a TaintMap for one parsed file. Single traversal, propagates taint
|
|
355
|
+
* through:
|
|
356
|
+
* - const/let bindings: const x = req.body.foo → x tainted
|
|
357
|
+
* - destructuring: const { foo } = req.body → foo tainted
|
|
358
|
+
* - assignment expressions: y = x; x is tainted → y tainted
|
|
359
|
+
* - function params named req/request/ctx (inside handlers, `req.body` is
|
|
360
|
+
* tainted even if we didn't see the outer binding)
|
|
361
|
+
*/
|
|
362
|
+
declare function buildTaintMap(parsed: ParsedFile): TaintMap;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Visitor helpers for rule authors. Each helper takes a parsed file and a
|
|
366
|
+
* callback, and walks the AST in the most common pattern (find call
|
|
367
|
+
* expressions to a particular function, find member expressions of a
|
|
368
|
+
* particular shape, etc.).
|
|
369
|
+
*
|
|
370
|
+
* The rule code stays short and declarative; this file carries the
|
|
371
|
+
* @babel/traverse boilerplate and default-export interop.
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Visit every BinaryExpression in the parsed file. Rule authors use this to
|
|
376
|
+
* catch `a === b` / `a !== b` comparisons where one side is a secret identifier.
|
|
377
|
+
*/
|
|
378
|
+
declare function visitBinary(parsed: ParsedFile, visit: (node: BinaryExpression, line: number) => void): void;
|
|
379
|
+
/**
|
|
380
|
+
* Visit every CallExpression. `matchCallee` decides whether to surface the
|
|
381
|
+
* call; if it returns true, `visit` is invoked with the node and its
|
|
382
|
+
* 1-indexed source line.
|
|
383
|
+
*/
|
|
384
|
+
declare function visitCalls(parsed: ParsedFile, matchCallee: (callee: Node) => boolean, visit: (call: CallExpression, line: number) => void): void;
|
|
385
|
+
/**
|
|
386
|
+
* "Does this callee resolve to a member/global named <name>?" — handles
|
|
387
|
+
* foo(x)
|
|
388
|
+
* obj.foo(x)
|
|
389
|
+
* a.b.foo(x)
|
|
390
|
+
* foo?.(x)
|
|
391
|
+
* Matches the terminal identifier only; doesn't bind to a specific receiver.
|
|
392
|
+
*/
|
|
393
|
+
declare function isCalleeNamed(callee: Node, name: string): boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Matches a call to `objName.methodName(...)` — useful for things like
|
|
396
|
+
* `_.merge`, `Object.assign`, `Handlebars.compile`, `libxml.parseXml`.
|
|
397
|
+
*/
|
|
398
|
+
declare function isMethodCall(callee: Node, objName: string, methodName: string): boolean;
|
|
399
|
+
/** Looks up an ObjectExpression property by key name. */
|
|
400
|
+
declare function getObjectProperty(node: ObjectExpression, key: string): {
|
|
401
|
+
value: Node;
|
|
402
|
+
} | null;
|
|
403
|
+
/** Does this CallExpression spread an expression `matcher` returns true for? */
|
|
404
|
+
declare function callSpreads(call: CallExpression, matcher: (node: Node) => boolean): boolean;
|
|
405
|
+
|
|
406
|
+
export { type AIFilterResult, type Confidence, type CustomRule, type DetectedFramework, type FilteredFinding, type Finding, type GradeResult, type ParsedFile, type RuleMatch, type SecurityGrade, type Severity, type TaintMap, allCustomRules, allRules, androidDebuggable, blockingMainThread, buildTaintMap, calculateGrade, callSpreads, 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, getObjectProperty, 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, isCalleeNamed, isMethodCall, 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, parseFile, 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, visitBinary, visitCalls, vulnerableDependencies, weakHashing, weakPasswordRequirements, weakRSAKeySize, webhookSignatureVerification, xssVulnerability, xxeVulnerability };
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { ParseResult } from '@babel/parser';
|
|
2
|
+
import { File, Node, CallExpression, ObjectExpression, BinaryExpression } from '@babel/types';
|
|
3
|
+
|
|
1
4
|
type Severity = "critical" | "high" | "medium" | "low" | "info";
|
|
2
5
|
type Confidence = "high" | "medium" | "low";
|
|
3
6
|
interface Finding {
|
|
@@ -289,4 +292,115 @@ declare function scanEntropy(files: {
|
|
|
289
292
|
content: string;
|
|
290
293
|
}[]): Finding[];
|
|
291
294
|
|
|
292
|
-
|
|
295
|
+
/**
|
|
296
|
+
* Centralized Babel parser with per-file caching.
|
|
297
|
+
*
|
|
298
|
+
* Rules that opt in to AST analysis call parseFile(content, filename) to get
|
|
299
|
+
* a Program node back. Parsing is expensive (~5-50ms per KB of source); the
|
|
300
|
+
* LRU cache ensures a single scan pass touches each file at most once even
|
|
301
|
+
* when multiple rules walk the same AST.
|
|
302
|
+
*
|
|
303
|
+
* Failure mode: returns null on parse errors. Callers fall back to regex-only
|
|
304
|
+
* behavior so a syntax-error file in the user's repo can never break a scan.
|
|
305
|
+
*/
|
|
306
|
+
|
|
307
|
+
interface ParsedFile {
|
|
308
|
+
ast: ParseResult<File>;
|
|
309
|
+
language: "js" | "ts" | "jsx" | "tsx";
|
|
310
|
+
}
|
|
311
|
+
/**
|
|
312
|
+
* Parse a JS/TS file. Returns null for unsupported extensions, parse errors,
|
|
313
|
+
* or empty content. Callers MUST handle null and fall back to regex-only.
|
|
314
|
+
*/
|
|
315
|
+
declare function parseFile(content: string, filename: string): ParsedFile | null;
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Lightweight taint tracking for AST-based rules.
|
|
319
|
+
*
|
|
320
|
+
* "Tainted" means "derived from untrusted input" — an Express/Next request
|
|
321
|
+
* body, query string, params, or headers; process.argv; environment
|
|
322
|
+
* variables known to be user-controlled; or file contents read at runtime.
|
|
323
|
+
*
|
|
324
|
+
* This is deliberately a local-only analysis: we walk the AST of one file,
|
|
325
|
+
* track which identifiers bind to a tainted source, and propagate through
|
|
326
|
+
* direct assignments and destructuring. Cross-function and cross-file flow
|
|
327
|
+
* is out of scope for Phase A — those require a module graph we don't build
|
|
328
|
+
* yet. A conservative local analysis is still enough to flip the benchmark
|
|
329
|
+
* recall on the data-flow rules.
|
|
330
|
+
*
|
|
331
|
+
* The analysis returns a TaintMap the rules can query:
|
|
332
|
+
* isTainted(node) → "is this AST expression tainted?"
|
|
333
|
+
* isTaintedIdent(name) → "is this identifier currently bound to tainted
|
|
334
|
+
* data in the file-level scope?"
|
|
335
|
+
*
|
|
336
|
+
* Shape of recognized sources:
|
|
337
|
+
* req.body, req.query, req.params, req.headers, req.cookies
|
|
338
|
+
* request.body, request.query, ... (Fastify)
|
|
339
|
+
* ctx.request.body, ctx.query (Koa)
|
|
340
|
+
* process.argv, process.env[Keys known to be user-controllable]
|
|
341
|
+
* event.queryStringParameters, event.body (AWS Lambda / API Gateway)
|
|
342
|
+
* searchParams.get(...), url.searchParams (Next.js / Web fetch API)
|
|
343
|
+
*/
|
|
344
|
+
|
|
345
|
+
interface TaintMap {
|
|
346
|
+
/** true iff the given expression node evaluates to tainted data. */
|
|
347
|
+
isTainted(node: Node | null | undefined): boolean;
|
|
348
|
+
/** true iff the named identifier is bound to tainted data somewhere visible. */
|
|
349
|
+
isTaintedIdent(name: string): boolean;
|
|
350
|
+
/** Debug: all identifiers the analysis marked tainted, in discovery order. */
|
|
351
|
+
taintedNames(): string[];
|
|
352
|
+
}
|
|
353
|
+
/**
|
|
354
|
+
* Build a TaintMap for one parsed file. Single traversal, propagates taint
|
|
355
|
+
* through:
|
|
356
|
+
* - const/let bindings: const x = req.body.foo → x tainted
|
|
357
|
+
* - destructuring: const { foo } = req.body → foo tainted
|
|
358
|
+
* - assignment expressions: y = x; x is tainted → y tainted
|
|
359
|
+
* - function params named req/request/ctx (inside handlers, `req.body` is
|
|
360
|
+
* tainted even if we didn't see the outer binding)
|
|
361
|
+
*/
|
|
362
|
+
declare function buildTaintMap(parsed: ParsedFile): TaintMap;
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Visitor helpers for rule authors. Each helper takes a parsed file and a
|
|
366
|
+
* callback, and walks the AST in the most common pattern (find call
|
|
367
|
+
* expressions to a particular function, find member expressions of a
|
|
368
|
+
* particular shape, etc.).
|
|
369
|
+
*
|
|
370
|
+
* The rule code stays short and declarative; this file carries the
|
|
371
|
+
* @babel/traverse boilerplate and default-export interop.
|
|
372
|
+
*/
|
|
373
|
+
|
|
374
|
+
/**
|
|
375
|
+
* Visit every BinaryExpression in the parsed file. Rule authors use this to
|
|
376
|
+
* catch `a === b` / `a !== b` comparisons where one side is a secret identifier.
|
|
377
|
+
*/
|
|
378
|
+
declare function visitBinary(parsed: ParsedFile, visit: (node: BinaryExpression, line: number) => void): void;
|
|
379
|
+
/**
|
|
380
|
+
* Visit every CallExpression. `matchCallee` decides whether to surface the
|
|
381
|
+
* call; if it returns true, `visit` is invoked with the node and its
|
|
382
|
+
* 1-indexed source line.
|
|
383
|
+
*/
|
|
384
|
+
declare function visitCalls(parsed: ParsedFile, matchCallee: (callee: Node) => boolean, visit: (call: CallExpression, line: number) => void): void;
|
|
385
|
+
/**
|
|
386
|
+
* "Does this callee resolve to a member/global named <name>?" — handles
|
|
387
|
+
* foo(x)
|
|
388
|
+
* obj.foo(x)
|
|
389
|
+
* a.b.foo(x)
|
|
390
|
+
* foo?.(x)
|
|
391
|
+
* Matches the terminal identifier only; doesn't bind to a specific receiver.
|
|
392
|
+
*/
|
|
393
|
+
declare function isCalleeNamed(callee: Node, name: string): boolean;
|
|
394
|
+
/**
|
|
395
|
+
* Matches a call to `objName.methodName(...)` — useful for things like
|
|
396
|
+
* `_.merge`, `Object.assign`, `Handlebars.compile`, `libxml.parseXml`.
|
|
397
|
+
*/
|
|
398
|
+
declare function isMethodCall(callee: Node, objName: string, methodName: string): boolean;
|
|
399
|
+
/** Looks up an ObjectExpression property by key name. */
|
|
400
|
+
declare function getObjectProperty(node: ObjectExpression, key: string): {
|
|
401
|
+
value: Node;
|
|
402
|
+
} | null;
|
|
403
|
+
/** Does this CallExpression spread an expression `matcher` returns true for? */
|
|
404
|
+
declare function callSpreads(call: CallExpression, matcher: (node: Node) => boolean): boolean;
|
|
405
|
+
|
|
406
|
+
export { type AIFilterResult, type Confidence, type CustomRule, type DetectedFramework, type FilteredFinding, type Finding, type GradeResult, type ParsedFile, type RuleMatch, type SecurityGrade, type Severity, type TaintMap, allCustomRules, allRules, androidDebuggable, blockingMainThread, buildTaintMap, calculateGrade, callSpreads, 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, getObjectProperty, 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, isCalleeNamed, isMethodCall, 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, parseFile, 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, visitBinary, visitCalls, vulnerableDependencies, weakHashing, weakPasswordRequirements, weakRSAKeySize, webhookSignatureVerification, xssVulnerability, xxeVulnerability };
|