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 CHANGED
@@ -3365,7 +3365,350 @@ var magicNumbers = {
3365
3365
  return matches.slice(0, 3);
3366
3366
  }
3367
3367
  };
3368
- var allRules = [
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
- for (const rule of allRules) {
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];