xploitscan 0.5.0 → 0.6.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.js +358 -4
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -3365,7 +3365,350 @@ var magicNumbers = {
|
|
|
3365
3365
|
return matches.slice(0, 3);
|
|
3366
3366
|
}
|
|
3367
3367
|
};
|
|
3368
|
-
var
|
|
3368
|
+
var s3BucketNoEncryption = {
|
|
3369
|
+
id: "VC107",
|
|
3370
|
+
title: "S3 Bucket Without Encryption",
|
|
3371
|
+
severity: "high",
|
|
3372
|
+
category: "Infrastructure",
|
|
3373
|
+
description: "AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.",
|
|
3374
|
+
check(content, filePath) {
|
|
3375
|
+
if (!filePath.match(/\.tf$/)) return [];
|
|
3376
|
+
if (!/resource\s+"aws_s3_bucket"/.test(content)) return [];
|
|
3377
|
+
const matches = [];
|
|
3378
|
+
const bucketPattern = /resource\s+"aws_s3_bucket"\s+"(\w+)"/g;
|
|
3379
|
+
let m;
|
|
3380
|
+
while ((m = bucketPattern.exec(content)) !== null) {
|
|
3381
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3382
|
+
const blockEnd = Math.min(m.index + 2e3, content.length);
|
|
3383
|
+
const blockContent = content.substring(m.index, blockEnd);
|
|
3384
|
+
if (!/server_side_encryption/.test(blockContent)) {
|
|
3385
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3386
|
+
matches.push({
|
|
3387
|
+
rule: "VC107",
|
|
3388
|
+
title: s3BucketNoEncryption.title,
|
|
3389
|
+
severity: "high",
|
|
3390
|
+
category: "Infrastructure",
|
|
3391
|
+
file: filePath,
|
|
3392
|
+
line: lineNum,
|
|
3393
|
+
snippet: getSnippet(content, lineNum),
|
|
3394
|
+
fix: "Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }"
|
|
3395
|
+
});
|
|
3396
|
+
}
|
|
3397
|
+
}
|
|
3398
|
+
return matches;
|
|
3399
|
+
}
|
|
3400
|
+
};
|
|
3401
|
+
var securityGroupAllInbound = {
|
|
3402
|
+
id: "VC108",
|
|
3403
|
+
title: "Security Group Allows All Inbound",
|
|
3404
|
+
severity: "critical",
|
|
3405
|
+
category: "Infrastructure",
|
|
3406
|
+
description: "Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.",
|
|
3407
|
+
check(content, filePath) {
|
|
3408
|
+
if (!filePath.match(/\.(tf|json|yaml|yml)$/)) return [];
|
|
3409
|
+
const matches = [];
|
|
3410
|
+
if (filePath.match(/\.tf$/)) {
|
|
3411
|
+
const ingressPattern = /ingress\s*\{[^}]*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\][^}]*\}/gs;
|
|
3412
|
+
let m;
|
|
3413
|
+
while ((m = ingressPattern.exec(content)) !== null) {
|
|
3414
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3415
|
+
if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
|
|
3416
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3417
|
+
matches.push({
|
|
3418
|
+
rule: "VC108",
|
|
3419
|
+
title: securityGroupAllInbound.title,
|
|
3420
|
+
severity: "critical",
|
|
3421
|
+
category: "Infrastructure",
|
|
3422
|
+
file: filePath,
|
|
3423
|
+
line: lineNum,
|
|
3424
|
+
snippet: getSnippet(content, lineNum),
|
|
3425
|
+
fix: "Restrict security group ingress to specific IP ranges and ports."
|
|
3426
|
+
});
|
|
3427
|
+
}
|
|
3428
|
+
}
|
|
3429
|
+
}
|
|
3430
|
+
if (filePath.match(/\.(json|yaml|yml)$/)) {
|
|
3431
|
+
const cfnPattern = /AWS::EC2::SecurityGroup/g;
|
|
3432
|
+
if (cfnPattern.test(content) && /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/.test(content)) {
|
|
3433
|
+
matches.push(...findMatches(
|
|
3434
|
+
content,
|
|
3435
|
+
/CidrIp\s*:\s*["']?0\.0\.0\.0\/0/g,
|
|
3436
|
+
securityGroupAllInbound,
|
|
3437
|
+
filePath,
|
|
3438
|
+
() => "Restrict security group ingress to specific IP ranges and ports."
|
|
3439
|
+
));
|
|
3440
|
+
}
|
|
3441
|
+
}
|
|
3442
|
+
return matches;
|
|
3443
|
+
}
|
|
3444
|
+
};
|
|
3445
|
+
var rdsPubliclyAccessible = {
|
|
3446
|
+
id: "VC109",
|
|
3447
|
+
title: "RDS Instance Publicly Accessible",
|
|
3448
|
+
severity: "critical",
|
|
3449
|
+
category: "Infrastructure",
|
|
3450
|
+
description: "RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.",
|
|
3451
|
+
check(content, filePath) {
|
|
3452
|
+
if (!filePath.match(/\.tf$/)) return [];
|
|
3453
|
+
if (!/resource\s+"aws_db_instance"/.test(content)) return [];
|
|
3454
|
+
return findMatches(
|
|
3455
|
+
content,
|
|
3456
|
+
/publicly_accessible\s*=\s*true/g,
|
|
3457
|
+
rdsPubliclyAccessible,
|
|
3458
|
+
filePath,
|
|
3459
|
+
() => "Set publicly_accessible = false. Access RDS through VPC private subnets."
|
|
3460
|
+
);
|
|
3461
|
+
}
|
|
3462
|
+
};
|
|
3463
|
+
var missingCloudTrail = {
|
|
3464
|
+
id: "VC110",
|
|
3465
|
+
title: "Missing CloudTrail Logging",
|
|
3466
|
+
severity: "high",
|
|
3467
|
+
category: "Infrastructure",
|
|
3468
|
+
description: "AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.",
|
|
3469
|
+
check(content, filePath) {
|
|
3470
|
+
if (!filePath.match(/\.tf$/)) return [];
|
|
3471
|
+
if (!/provider\s+"aws"/.test(content)) return [];
|
|
3472
|
+
if (/aws_cloudtrail/.test(content)) return [];
|
|
3473
|
+
const lineNum = content.substring(0, content.search(/provider\s+"aws"/)).split("\n").length;
|
|
3474
|
+
return [{
|
|
3475
|
+
rule: "VC110",
|
|
3476
|
+
title: missingCloudTrail.title,
|
|
3477
|
+
severity: "high",
|
|
3478
|
+
category: "Infrastructure",
|
|
3479
|
+
file: filePath,
|
|
3480
|
+
line: lineNum,
|
|
3481
|
+
snippet: getSnippet(content, lineNum),
|
|
3482
|
+
fix: "Enable CloudTrail for audit logging of all AWS API calls."
|
|
3483
|
+
}];
|
|
3484
|
+
}
|
|
3485
|
+
};
|
|
3486
|
+
var lambdaWithoutVPC = {
|
|
3487
|
+
id: "VC111",
|
|
3488
|
+
title: "Lambda Without VPC",
|
|
3489
|
+
severity: "medium",
|
|
3490
|
+
category: "Infrastructure",
|
|
3491
|
+
description: "Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.",
|
|
3492
|
+
check(content, filePath) {
|
|
3493
|
+
if (!filePath.match(/\.tf$/)) return [];
|
|
3494
|
+
if (!/resource\s+"aws_lambda_function"/.test(content)) return [];
|
|
3495
|
+
const matches = [];
|
|
3496
|
+
const lambdaPattern = /resource\s+"aws_lambda_function"\s+"(\w+)"/g;
|
|
3497
|
+
let m;
|
|
3498
|
+
while ((m = lambdaPattern.exec(content)) !== null) {
|
|
3499
|
+
if (isCommentLine(content, m.index)) continue;
|
|
3500
|
+
const blockEnd = Math.min(m.index + 2e3, content.length);
|
|
3501
|
+
const blockContent = content.substring(m.index, blockEnd);
|
|
3502
|
+
if (!/vpc_config\s*\{/.test(blockContent)) {
|
|
3503
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3504
|
+
matches.push({
|
|
3505
|
+
rule: "VC111",
|
|
3506
|
+
title: lambdaWithoutVPC.title,
|
|
3507
|
+
severity: "medium",
|
|
3508
|
+
category: "Infrastructure",
|
|
3509
|
+
file: filePath,
|
|
3510
|
+
line: lineNum,
|
|
3511
|
+
snippet: getSnippet(content, lineNum),
|
|
3512
|
+
fix: "Place Lambda functions in a VPC for network isolation."
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
}
|
|
3516
|
+
return matches;
|
|
3517
|
+
}
|
|
3518
|
+
};
|
|
3519
|
+
var dockerLatestTag = {
|
|
3520
|
+
id: "VC112",
|
|
3521
|
+
title: "Docker Image Using Latest Tag",
|
|
3522
|
+
severity: "high",
|
|
3523
|
+
category: "Configuration",
|
|
3524
|
+
description: "Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.",
|
|
3525
|
+
check(content, filePath) {
|
|
3526
|
+
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
3527
|
+
const matches = [];
|
|
3528
|
+
const fromPattern = /^FROM\s+(?!scratch)(\S+?)(?:\s+AS\s+\S+)?\s*$/gm;
|
|
3529
|
+
let m;
|
|
3530
|
+
while ((m = fromPattern.exec(content)) !== null) {
|
|
3531
|
+
const image = m[1];
|
|
3532
|
+
if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
|
|
3533
|
+
const lineNum = content.substring(0, m.index).split("\n").length;
|
|
3534
|
+
matches.push({
|
|
3535
|
+
rule: "VC112",
|
|
3536
|
+
title: dockerLatestTag.title,
|
|
3537
|
+
severity: "high",
|
|
3538
|
+
category: "Configuration",
|
|
3539
|
+
file: filePath,
|
|
3540
|
+
line: lineNum,
|
|
3541
|
+
snippet: getSnippet(content, lineNum),
|
|
3542
|
+
fix: "Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest"
|
|
3543
|
+
});
|
|
3544
|
+
}
|
|
3545
|
+
}
|
|
3546
|
+
return matches;
|
|
3547
|
+
}
|
|
3548
|
+
};
|
|
3549
|
+
var dockerCopySensitive = {
|
|
3550
|
+
id: "VC113",
|
|
3551
|
+
title: "Docker COPY With Sensitive Files",
|
|
3552
|
+
severity: "high",
|
|
3553
|
+
category: "Configuration",
|
|
3554
|
+
description: "Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.",
|
|
3555
|
+
check(content, filePath) {
|
|
3556
|
+
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
3557
|
+
if (!/(?:COPY|ADD)\s+\.\s+\./.test(content)) return [];
|
|
3558
|
+
return findMatches(
|
|
3559
|
+
content,
|
|
3560
|
+
/(?:COPY|ADD)\s+\.\s+\./g,
|
|
3561
|
+
dockerCopySensitive,
|
|
3562
|
+
filePath,
|
|
3563
|
+
() => "Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context."
|
|
3564
|
+
);
|
|
3565
|
+
}
|
|
3566
|
+
};
|
|
3567
|
+
var dockerTooManyPorts = {
|
|
3568
|
+
id: "VC114",
|
|
3569
|
+
title: "Docker Exposing Too Many Ports",
|
|
3570
|
+
severity: "medium",
|
|
3571
|
+
category: "Configuration",
|
|
3572
|
+
description: "Exposing many ports or wide port ranges increases the attack surface of a container.",
|
|
3573
|
+
check(content, filePath) {
|
|
3574
|
+
if (!filePath.match(/Dockerfile$/i)) return [];
|
|
3575
|
+
const matches = [];
|
|
3576
|
+
const rangePattern = /^EXPOSE\s+\d+-\d+/gm;
|
|
3577
|
+
matches.push(...findMatches(
|
|
3578
|
+
content,
|
|
3579
|
+
rangePattern,
|
|
3580
|
+
dockerTooManyPorts,
|
|
3581
|
+
filePath,
|
|
3582
|
+
() => "Only expose necessary ports. Each EXPOSE should have a clear purpose."
|
|
3583
|
+
));
|
|
3584
|
+
const exposeLines = content.split("\n").filter((l) => /^\s*EXPOSE\s+/.test(l));
|
|
3585
|
+
if (exposeLines.length > 3) {
|
|
3586
|
+
const firstExpose = content.search(/^\s*EXPOSE\s+/m);
|
|
3587
|
+
if (firstExpose >= 0) {
|
|
3588
|
+
const lineNum = content.substring(0, firstExpose).split("\n").length;
|
|
3589
|
+
matches.push({
|
|
3590
|
+
rule: "VC114",
|
|
3591
|
+
title: dockerTooManyPorts.title,
|
|
3592
|
+
severity: "medium",
|
|
3593
|
+
category: "Configuration",
|
|
3594
|
+
file: filePath,
|
|
3595
|
+
line: lineNum,
|
|
3596
|
+
snippet: getSnippet(content, lineNum),
|
|
3597
|
+
fix: "Only expose necessary ports. Each EXPOSE should have a clear purpose."
|
|
3598
|
+
});
|
|
3599
|
+
}
|
|
3600
|
+
}
|
|
3601
|
+
return matches;
|
|
3602
|
+
}
|
|
3603
|
+
};
|
|
3604
|
+
var k8sSecretNotEncrypted = {
|
|
3605
|
+
id: "VC115",
|
|
3606
|
+
title: "Kubernetes Secret Not Encrypted",
|
|
3607
|
+
severity: "high",
|
|
3608
|
+
category: "Infrastructure",
|
|
3609
|
+
description: "Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.",
|
|
3610
|
+
check(content, filePath) {
|
|
3611
|
+
if (!filePath.match(/\.(yaml|yml)$/)) return [];
|
|
3612
|
+
if (!/kind\s*:\s*Secret/i.test(content)) return [];
|
|
3613
|
+
if (/sealedsecrets\.bitnami\.com|external-secrets\.io/i.test(content)) return [];
|
|
3614
|
+
return findMatches(
|
|
3615
|
+
content,
|
|
3616
|
+
/kind\s*:\s*Secret/g,
|
|
3617
|
+
k8sSecretNotEncrypted,
|
|
3618
|
+
filePath,
|
|
3619
|
+
() => "Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests."
|
|
3620
|
+
);
|
|
3621
|
+
}
|
|
3622
|
+
};
|
|
3623
|
+
var k8sNoResourceLimits = {
|
|
3624
|
+
id: "VC116",
|
|
3625
|
+
title: "Kubernetes Pod Without Resource Limits",
|
|
3626
|
+
severity: "medium",
|
|
3627
|
+
category: "Infrastructure",
|
|
3628
|
+
description: "Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.",
|
|
3629
|
+
check(content, filePath) {
|
|
3630
|
+
if (!filePath.match(/\.(yaml|yml)$/)) return [];
|
|
3631
|
+
if (!/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];
|
|
3632
|
+
if (/resources\s*:\s*\n\s+limits\s*:/m.test(content)) return [];
|
|
3633
|
+
if (/resources\s*:.*limits/i.test(content)) return [];
|
|
3634
|
+
const kindMatch = content.match(/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);
|
|
3635
|
+
if (!kindMatch) return [];
|
|
3636
|
+
const lineNum = content.substring(0, kindMatch.index).split("\n").length;
|
|
3637
|
+
return [{
|
|
3638
|
+
rule: "VC116",
|
|
3639
|
+
title: k8sNoResourceLimits.title,
|
|
3640
|
+
severity: "medium",
|
|
3641
|
+
category: "Infrastructure",
|
|
3642
|
+
file: filePath,
|
|
3643
|
+
line: lineNum,
|
|
3644
|
+
snippet: getSnippet(content, lineNum),
|
|
3645
|
+
fix: "Set resource limits to prevent pods from consuming excessive CPU/memory."
|
|
3646
|
+
}];
|
|
3647
|
+
}
|
|
3648
|
+
};
|
|
3649
|
+
var freeRules = [
|
|
3650
|
+
hardcodedSecrets,
|
|
3651
|
+
// VC001
|
|
3652
|
+
exposedEnvFile,
|
|
3653
|
+
// VC002
|
|
3654
|
+
missingAuthMiddleware,
|
|
3655
|
+
// VC003
|
|
3656
|
+
supabaseNoRLS,
|
|
3657
|
+
// VC004
|
|
3658
|
+
stripeWebhookUnprotected,
|
|
3659
|
+
// VC005
|
|
3660
|
+
sqlInjection,
|
|
3661
|
+
// VC006
|
|
3662
|
+
xssVulnerability,
|
|
3663
|
+
// VC007
|
|
3664
|
+
noRateLimiting,
|
|
3665
|
+
// VC008
|
|
3666
|
+
corsWildcard,
|
|
3667
|
+
// VC009
|
|
3668
|
+
clientSideAuth,
|
|
3669
|
+
// VC010
|
|
3670
|
+
nextPublicSecret,
|
|
3671
|
+
// VC011
|
|
3672
|
+
envNotGitignored,
|
|
3673
|
+
// VC014
|
|
3674
|
+
evalUsage,
|
|
3675
|
+
// VC015
|
|
3676
|
+
unvalidatedRedirect,
|
|
3677
|
+
// VC016
|
|
3678
|
+
insecureCookies,
|
|
3679
|
+
// VC017
|
|
3680
|
+
exposedAuthSecret,
|
|
3681
|
+
// VC018
|
|
3682
|
+
missingCSP,
|
|
3683
|
+
// VC020
|
|
3684
|
+
hardcodedJWTSecret,
|
|
3685
|
+
// VC031
|
|
3686
|
+
missingHTTPS,
|
|
3687
|
+
// VC032
|
|
3688
|
+
exposedDebugMode,
|
|
3689
|
+
// VC033
|
|
3690
|
+
insecureRandomness,
|
|
3691
|
+
// VC034
|
|
3692
|
+
missingErrorBoundary,
|
|
3693
|
+
// VC036
|
|
3694
|
+
exposedStackTraces,
|
|
3695
|
+
// VC037
|
|
3696
|
+
missingLockFile,
|
|
3697
|
+
// VC039
|
|
3698
|
+
dangerousInnerHTML,
|
|
3699
|
+
// VC063
|
|
3700
|
+
consoleLogProduction,
|
|
3701
|
+
// VC097
|
|
3702
|
+
emptyCatchBlock,
|
|
3703
|
+
// VC104
|
|
3704
|
+
todoLeftInCode,
|
|
3705
|
+
// VC103
|
|
3706
|
+
weakHashing,
|
|
3707
|
+
// VC060
|
|
3708
|
+
disabledTLSVerification
|
|
3709
|
+
// VC061
|
|
3710
|
+
];
|
|
3711
|
+
var proRules = [
|
|
3369
3712
|
hardcodedSecrets,
|
|
3370
3713
|
exposedEnvFile,
|
|
3371
3714
|
missingAuthMiddleware,
|
|
@@ -3471,14 +3814,25 @@ var allRules = [
|
|
|
3471
3814
|
todoLeftInCode,
|
|
3472
3815
|
emptyCatchBlock,
|
|
3473
3816
|
callbackHell,
|
|
3474
|
-
magicNumbers
|
|
3817
|
+
magicNumbers,
|
|
3818
|
+
s3BucketNoEncryption,
|
|
3819
|
+
securityGroupAllInbound,
|
|
3820
|
+
rdsPubliclyAccessible,
|
|
3821
|
+
missingCloudTrail,
|
|
3822
|
+
lambdaWithoutVPC,
|
|
3823
|
+
dockerLatestTag,
|
|
3824
|
+
dockerCopySensitive,
|
|
3825
|
+
dockerTooManyPorts,
|
|
3826
|
+
k8sSecretNotEncrypted,
|
|
3827
|
+
k8sNoResourceLimits
|
|
3475
3828
|
];
|
|
3476
|
-
function runCustomRules(content, filePath, disabledRules = []) {
|
|
3829
|
+
function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
|
|
3477
3830
|
const findings = [];
|
|
3478
3831
|
if (/function runScan\(files\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\s*[:=]/.test(content) && /findMatches/.test(content)) {
|
|
3479
3832
|
return findings;
|
|
3480
3833
|
}
|
|
3481
|
-
|
|
3834
|
+
const ruleset = tier === "pro" ? proRules : freeRules;
|
|
3835
|
+
for (const rule of ruleset) {
|
|
3482
3836
|
if (disabledRules.includes(rule.id)) continue;
|
|
3483
3837
|
const matches = rule.check(content, filePath);
|
|
3484
3838
|
const compliance = complianceMap[rule.id];
|