xploitscan 0.5.0 → 0.7.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
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
- __require,
4
3
  checkUsage,
5
4
  clearToken,
6
5
  getStoredToken,
@@ -9,7 +8,7 @@ import {
9
8
  storeToken,
10
9
  syncUser,
11
10
  uploadScanResults
12
- } from "./chunk-47X3TUVR.js";
11
+ } from "./chunk-CBDFSACC.js";
13
12
 
14
13
  // src/index.ts
15
14
  import { Command } from "commander";
@@ -3166,7 +3165,42 @@ var complianceMap = {
3166
3165
  VC093: { owasp: "A01:2021", cwe: "CWE-22" },
3167
3166
  VC094: { owasp: "A03:2021", cwe: "CWE-78" },
3168
3167
  VC095: { owasp: "A05:2021", cwe: "CWE-942" },
3169
- VC096: { owasp: "A02:2021", cwe: "CWE-319" }
3168
+ VC096: { owasp: "A02:2021", cwe: "CWE-319" },
3169
+ VC097: { owasp: "A09:2021", cwe: "CWE-532" },
3170
+ VC098: { owasp: "A04:2021", cwe: "CWE-400" },
3171
+ VC099: { owasp: "A04:2021", cwe: "CWE-401" },
3172
+ VC100: { owasp: "A04:2021", cwe: "CWE-400" },
3173
+ VC101: { owasp: "A04:2021", cwe: "CWE-400" },
3174
+ VC102: { owasp: "A04:2021", cwe: "CWE-400" },
3175
+ VC103: { owasp: "A04:2021", cwe: "CWE-710" },
3176
+ VC104: { owasp: "A04:2021", cwe: "CWE-390" },
3177
+ VC105: { owasp: "A04:2021", cwe: "CWE-710" },
3178
+ VC106: { owasp: "A04:2021", cwe: "CWE-710" },
3179
+ VC107: { owasp: "A02:2021", cwe: "CWE-311" },
3180
+ VC108: { owasp: "A01:2021", cwe: "CWE-284" },
3181
+ VC109: { owasp: "A01:2021", cwe: "CWE-284" },
3182
+ VC110: { owasp: "A09:2021", cwe: "CWE-778" },
3183
+ VC111: { owasp: "A05:2021", cwe: "CWE-16" },
3184
+ VC112: { owasp: "A06:2021", cwe: "CWE-1104" },
3185
+ VC113: { owasp: "A05:2021", cwe: "CWE-200" },
3186
+ VC114: { owasp: "A05:2021", cwe: "CWE-16" },
3187
+ VC115: { owasp: "A02:2021", cwe: "CWE-311" },
3188
+ VC116: { owasp: "A05:2021", cwe: "CWE-770" },
3189
+ VC117: { owasp: "A03:2021", cwe: "CWE-22" },
3190
+ VC118: { owasp: "A09:2021", cwe: "CWE-532" },
3191
+ VC119: { owasp: "A07:2021", cwe: "CWE-798" },
3192
+ VC120: { owasp: "A07:2021", cwe: "CWE-352" },
3193
+ VC121: { owasp: "A06:2021", cwe: "CWE-1104" },
3194
+ VC122: { owasp: "A02:2021", cwe: "CWE-326" },
3195
+ VC123: { owasp: "A02:2021", cwe: "CWE-326" },
3196
+ VC124: { owasp: "A02:2021", cwe: "CWE-327" },
3197
+ VC125: { owasp: "A07:2021", cwe: "CWE-640" },
3198
+ VC126: { owasp: "A05:2021", cwe: "CWE-200" },
3199
+ VC127: { owasp: "A01:2021", cwe: "CWE-862" },
3200
+ VC128: { owasp: "A05:2021", cwe: "CWE-444" },
3201
+ VC129: { owasp: "A02:2021", cwe: "CWE-311" },
3202
+ VC130: { owasp: "A07:2021", cwe: "CWE-307" },
3203
+ VC131: { owasp: "A06:2021", cwe: "CWE-1104" }
3170
3204
  };
3171
3205
  var consoleLogProduction = {
3172
3206
  id: "VC097",
@@ -3365,7 +3399,870 @@ var magicNumbers = {
3365
3399
  return matches.slice(0, 3);
3366
3400
  }
3367
3401
  };
3368
- var allRules = [
3402
+ var s3BucketNoEncryption = {
3403
+ id: "VC107",
3404
+ title: "S3 Bucket Without Encryption",
3405
+ severity: "high",
3406
+ category: "Infrastructure",
3407
+ description: "AWS S3 buckets without server-side encryption leave data at rest unprotected. Enable encryption to protect sensitive data.",
3408
+ check(content, filePath) {
3409
+ if (!filePath.match(/\.tf$/)) return [];
3410
+ if (!/resource\s+"aws_s3_bucket"/.test(content)) return [];
3411
+ const matches = [];
3412
+ const bucketPattern = /resource\s+"aws_s3_bucket"\s+"(\w+)"/g;
3413
+ let m;
3414
+ while ((m = bucketPattern.exec(content)) !== null) {
3415
+ if (isCommentLine(content, m.index)) continue;
3416
+ const blockEnd = Math.min(m.index + 2e3, content.length);
3417
+ const blockContent = content.substring(m.index, blockEnd);
3418
+ if (!/server_side_encryption/.test(blockContent)) {
3419
+ const lineNum = content.substring(0, m.index).split("\n").length;
3420
+ matches.push({
3421
+ rule: "VC107",
3422
+ title: s3BucketNoEncryption.title,
3423
+ severity: "high",
3424
+ category: "Infrastructure",
3425
+ file: filePath,
3426
+ line: lineNum,
3427
+ snippet: getSnippet(content, lineNum),
3428
+ fix: "Enable S3 bucket encryption: server_side_encryption_configuration { rule { apply_server_side_encryption_by_default { sse_algorithm = 'aws:kms' } } }"
3429
+ });
3430
+ }
3431
+ }
3432
+ return matches;
3433
+ }
3434
+ };
3435
+ var securityGroupAllInbound = {
3436
+ id: "VC108",
3437
+ title: "Security Group Allows All Inbound",
3438
+ severity: "critical",
3439
+ category: "Infrastructure",
3440
+ description: "Security groups allowing all inbound traffic (0.0.0.0/0 on all ports) expose resources to the entire internet.",
3441
+ check(content, filePath) {
3442
+ if (!filePath.match(/\.(tf|json|yaml|yml)$/)) return [];
3443
+ const matches = [];
3444
+ if (filePath.match(/\.tf$/)) {
3445
+ const ingressPattern = /ingress\s*\{[^}]*cidr_blocks\s*=\s*\[\s*"0\.0\.0\.0\/0"\s*\][^}]*\}/gs;
3446
+ let m;
3447
+ while ((m = ingressPattern.exec(content)) !== null) {
3448
+ if (isCommentLine(content, m.index)) continue;
3449
+ if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
3450
+ const lineNum = content.substring(0, m.index).split("\n").length;
3451
+ matches.push({
3452
+ rule: "VC108",
3453
+ title: securityGroupAllInbound.title,
3454
+ severity: "critical",
3455
+ category: "Infrastructure",
3456
+ file: filePath,
3457
+ line: lineNum,
3458
+ snippet: getSnippet(content, lineNum),
3459
+ fix: "Restrict security group ingress to specific IP ranges and ports."
3460
+ });
3461
+ }
3462
+ }
3463
+ }
3464
+ if (filePath.match(/\.(json|yaml|yml)$/)) {
3465
+ const cfnPattern = /AWS::EC2::SecurityGroup/g;
3466
+ if (cfnPattern.test(content) && /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/.test(content)) {
3467
+ matches.push(...findMatches(
3468
+ content,
3469
+ /CidrIp\s*:\s*["']?0\.0\.0\.0\/0/g,
3470
+ securityGroupAllInbound,
3471
+ filePath,
3472
+ () => "Restrict security group ingress to specific IP ranges and ports."
3473
+ ));
3474
+ }
3475
+ }
3476
+ return matches;
3477
+ }
3478
+ };
3479
+ var rdsPubliclyAccessible = {
3480
+ id: "VC109",
3481
+ title: "RDS Instance Publicly Accessible",
3482
+ severity: "critical",
3483
+ category: "Infrastructure",
3484
+ description: "RDS instances with publicly_accessible = true are exposed to the internet, risking unauthorized database access.",
3485
+ check(content, filePath) {
3486
+ if (!filePath.match(/\.tf$/)) return [];
3487
+ if (!/resource\s+"aws_db_instance"/.test(content)) return [];
3488
+ return findMatches(
3489
+ content,
3490
+ /publicly_accessible\s*=\s*true/g,
3491
+ rdsPubliclyAccessible,
3492
+ filePath,
3493
+ () => "Set publicly_accessible = false. Access RDS through VPC private subnets."
3494
+ );
3495
+ }
3496
+ };
3497
+ var missingCloudTrail = {
3498
+ id: "VC110",
3499
+ title: "Missing CloudTrail Logging",
3500
+ severity: "high",
3501
+ category: "Infrastructure",
3502
+ description: "AWS environments without CloudTrail lack audit logging of API calls, making it difficult to detect unauthorized activity.",
3503
+ check(content, filePath) {
3504
+ if (!filePath.match(/\.tf$/)) return [];
3505
+ if (!/provider\s+"aws"/.test(content)) return [];
3506
+ if (/aws_cloudtrail/.test(content)) return [];
3507
+ const lineNum = content.substring(0, content.search(/provider\s+"aws"/)).split("\n").length;
3508
+ return [{
3509
+ rule: "VC110",
3510
+ title: missingCloudTrail.title,
3511
+ severity: "high",
3512
+ category: "Infrastructure",
3513
+ file: filePath,
3514
+ line: lineNum,
3515
+ snippet: getSnippet(content, lineNum),
3516
+ fix: "Enable CloudTrail for audit logging of all AWS API calls."
3517
+ }];
3518
+ }
3519
+ };
3520
+ var lambdaWithoutVPC = {
3521
+ id: "VC111",
3522
+ title: "Lambda Without VPC",
3523
+ severity: "medium",
3524
+ category: "Infrastructure",
3525
+ description: "Lambda functions not placed in a VPC lack network isolation and cannot access VPC-only resources securely.",
3526
+ check(content, filePath) {
3527
+ if (!filePath.match(/\.tf$/)) return [];
3528
+ if (!/resource\s+"aws_lambda_function"/.test(content)) return [];
3529
+ const matches = [];
3530
+ const lambdaPattern = /resource\s+"aws_lambda_function"\s+"(\w+)"/g;
3531
+ let m;
3532
+ while ((m = lambdaPattern.exec(content)) !== null) {
3533
+ if (isCommentLine(content, m.index)) continue;
3534
+ const blockEnd = Math.min(m.index + 2e3, content.length);
3535
+ const blockContent = content.substring(m.index, blockEnd);
3536
+ if (!/vpc_config\s*\{/.test(blockContent)) {
3537
+ const lineNum = content.substring(0, m.index).split("\n").length;
3538
+ matches.push({
3539
+ rule: "VC111",
3540
+ title: lambdaWithoutVPC.title,
3541
+ severity: "medium",
3542
+ category: "Infrastructure",
3543
+ file: filePath,
3544
+ line: lineNum,
3545
+ snippet: getSnippet(content, lineNum),
3546
+ fix: "Place Lambda functions in a VPC for network isolation."
3547
+ });
3548
+ }
3549
+ }
3550
+ return matches;
3551
+ }
3552
+ };
3553
+ var dockerLatestTag = {
3554
+ id: "VC112",
3555
+ title: "Docker Image Using Latest Tag",
3556
+ severity: "high",
3557
+ category: "Configuration",
3558
+ description: "Using :latest or no tag in Docker FROM directives leads to non-reproducible builds and potential security regressions.",
3559
+ check(content, filePath) {
3560
+ if (!filePath.match(/Dockerfile$/i)) return [];
3561
+ const matches = [];
3562
+ const fromPattern = /^FROM\s+(?!scratch)(\S+?)(?:\s+AS\s+\S+)?\s*$/gm;
3563
+ let m;
3564
+ while ((m = fromPattern.exec(content)) !== null) {
3565
+ const image = m[1];
3566
+ if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
3567
+ const lineNum = content.substring(0, m.index).split("\n").length;
3568
+ matches.push({
3569
+ rule: "VC112",
3570
+ title: dockerLatestTag.title,
3571
+ severity: "high",
3572
+ category: "Configuration",
3573
+ file: filePath,
3574
+ line: lineNum,
3575
+ snippet: getSnippet(content, lineNum),
3576
+ fix: "Pin Docker image versions: FROM node:20-alpine instead of FROM node:latest"
3577
+ });
3578
+ }
3579
+ }
3580
+ return matches;
3581
+ }
3582
+ };
3583
+ var dockerCopySensitive = {
3584
+ id: "VC113",
3585
+ title: "Docker COPY With Sensitive Files",
3586
+ severity: "high",
3587
+ category: "Configuration",
3588
+ description: "Using COPY . . or ADD . . without a .dockerignore can leak .env files, .git history, and other sensitive data into the Docker image.",
3589
+ check(content, filePath) {
3590
+ if (!filePath.match(/Dockerfile$/i)) return [];
3591
+ if (!/(?:COPY|ADD)\s+\.\s+\./.test(content)) return [];
3592
+ return findMatches(
3593
+ content,
3594
+ /(?:COPY|ADD)\s+\.\s+\./g,
3595
+ dockerCopySensitive,
3596
+ filePath,
3597
+ () => "Use .dockerignore to exclude .env, .git, node_modules, and sensitive files from the Docker build context."
3598
+ );
3599
+ }
3600
+ };
3601
+ var dockerTooManyPorts = {
3602
+ id: "VC114",
3603
+ title: "Docker Exposing Too Many Ports",
3604
+ severity: "medium",
3605
+ category: "Configuration",
3606
+ description: "Exposing many ports or wide port ranges increases the attack surface of a container.",
3607
+ check(content, filePath) {
3608
+ if (!filePath.match(/Dockerfile$/i)) return [];
3609
+ const matches = [];
3610
+ const rangePattern = /^EXPOSE\s+\d+-\d+/gm;
3611
+ matches.push(...findMatches(
3612
+ content,
3613
+ rangePattern,
3614
+ dockerTooManyPorts,
3615
+ filePath,
3616
+ () => "Only expose necessary ports. Each EXPOSE should have a clear purpose."
3617
+ ));
3618
+ const exposeLines = content.split("\n").filter((l) => /^\s*EXPOSE\s+/.test(l));
3619
+ if (exposeLines.length > 3) {
3620
+ const firstExpose = content.search(/^\s*EXPOSE\s+/m);
3621
+ if (firstExpose >= 0) {
3622
+ const lineNum = content.substring(0, firstExpose).split("\n").length;
3623
+ matches.push({
3624
+ rule: "VC114",
3625
+ title: dockerTooManyPorts.title,
3626
+ severity: "medium",
3627
+ category: "Configuration",
3628
+ file: filePath,
3629
+ line: lineNum,
3630
+ snippet: getSnippet(content, lineNum),
3631
+ fix: "Only expose necessary ports. Each EXPOSE should have a clear purpose."
3632
+ });
3633
+ }
3634
+ }
3635
+ return matches;
3636
+ }
3637
+ };
3638
+ var k8sSecretNotEncrypted = {
3639
+ id: "VC115",
3640
+ title: "Kubernetes Secret Not Encrypted",
3641
+ severity: "high",
3642
+ category: "Infrastructure",
3643
+ description: "Kubernetes Secrets stored as plain base64 in manifests are not encrypted and can be trivially decoded. Use sealed-secrets or external-secrets.",
3644
+ check(content, filePath) {
3645
+ if (!filePath.match(/\.(yaml|yml)$/)) return [];
3646
+ if (!/kind\s*:\s*Secret/i.test(content)) return [];
3647
+ if (/sealedsecrets\.bitnami\.com|external-secrets\.io/i.test(content)) return [];
3648
+ return findMatches(
3649
+ content,
3650
+ /kind\s*:\s*Secret/g,
3651
+ k8sSecretNotEncrypted,
3652
+ filePath,
3653
+ () => "Use sealed-secrets or external-secrets-operator. Never store plain secrets in manifests."
3654
+ );
3655
+ }
3656
+ };
3657
+ var k8sNoResourceLimits = {
3658
+ id: "VC116",
3659
+ title: "Kubernetes Pod Without Resource Limits",
3660
+ severity: "medium",
3661
+ category: "Infrastructure",
3662
+ description: "Pods or deployments without resource limits can consume excessive CPU/memory, causing cluster instability.",
3663
+ check(content, filePath) {
3664
+ if (!filePath.match(/\.(yaml|yml)$/)) return [];
3665
+ if (!/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i.test(content)) return [];
3666
+ if (/resources\s*:\s*\n\s+limits\s*:/m.test(content)) return [];
3667
+ if (/resources\s*:.*limits/i.test(content)) return [];
3668
+ const kindMatch = content.match(/kind\s*:\s*(?:Pod|Deployment|StatefulSet|DaemonSet|Job|CronJob)/i);
3669
+ if (!kindMatch) return [];
3670
+ const lineNum = content.substring(0, kindMatch.index).split("\n").length;
3671
+ return [{
3672
+ rule: "VC116",
3673
+ title: k8sNoResourceLimits.title,
3674
+ severity: "medium",
3675
+ category: "Infrastructure",
3676
+ file: filePath,
3677
+ line: lineNum,
3678
+ snippet: getSnippet(content, lineNum),
3679
+ fix: "Set resource limits to prevent pods from consuming excessive CPU/memory."
3680
+ }];
3681
+ }
3682
+ };
3683
+ var pathTraversal = {
3684
+ id: "VC117",
3685
+ title: "Path Traversal Vulnerability",
3686
+ severity: "critical",
3687
+ category: "Injection",
3688
+ description: "User input is used to construct file paths without sanitization, allowing attackers to read/write arbitrary files (e.g., ../../etc/passwd).",
3689
+ check(content, filePath) {
3690
+ if (isTestFile(filePath)) return [];
3691
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|php|java)$/)) return [];
3692
+ const findings = [];
3693
+ const patterns = [
3694
+ /(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
3695
+ /(?:path\.join|path\.resolve)\s*\([^)]*(?:req\.|request\.|ctx\.|params\.|query\.)[^)]*\)/gi,
3696
+ /(?:res\.sendFile|res\.download)\s*\([^)]*(?:req\.|request\.)/gi,
3697
+ /open\s*\(\s*(?:request\.|params\[|args\.)/gi
3698
+ ];
3699
+ for (const pat of patterns) {
3700
+ let m;
3701
+ while ((m = pat.exec(content)) !== null) {
3702
+ if (isCommentLine(content, m.index)) continue;
3703
+ const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
3704
+ if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\./i.test(surrounding)) continue;
3705
+ const lineNum = content.substring(0, m.index).split("\n").length;
3706
+ findings.push({
3707
+ rule: "VC117",
3708
+ title: pathTraversal.title,
3709
+ severity: "critical",
3710
+ category: "Injection",
3711
+ file: filePath,
3712
+ line: lineNum,
3713
+ snippet: getSnippet(content, lineNum),
3714
+ fix: "Sanitize file paths: use path.normalize(), reject paths containing '..', and validate against an allowed base directory."
3715
+ });
3716
+ }
3717
+ }
3718
+ return findings;
3719
+ }
3720
+ };
3721
+ var piiInLogs = {
3722
+ id: "VC118",
3723
+ title: "Personally Identifiable Information in Logs",
3724
+ severity: "high",
3725
+ category: "Information Leakage",
3726
+ description: "Logging statements that include email addresses, passwords, SSNs, credit card numbers, or other PII can leak sensitive data to log aggregation systems.",
3727
+ check(content, filePath) {
3728
+ if (isTestFile(filePath)) return [];
3729
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
3730
+ const findings = [];
3731
+ const logPattern = /(?:console\.(?:log|info|warn|error|debug)|logger\.(?:info|warn|error|debug|log)|log\.(?:info|warn|error|debug)|logging\.(?:info|warn|error|debug))\s*\([^)]*(?:password|passwd|secret|ssn|social.?security|credit.?card|cardNumber|cvv|token|bearer|authorization)\b/gi;
3732
+ let m;
3733
+ while ((m = logPattern.exec(content)) !== null) {
3734
+ if (isCommentLine(content, m.index)) continue;
3735
+ if (isInsideFixMessage(content, m.index)) continue;
3736
+ const lineNum = content.substring(0, m.index).split("\n").length;
3737
+ findings.push({
3738
+ rule: "VC118",
3739
+ title: piiInLogs.title,
3740
+ severity: "high",
3741
+ category: "Information Leakage",
3742
+ file: filePath,
3743
+ line: lineNum,
3744
+ snippet: getSnippet(content, lineNum),
3745
+ fix: "Never log sensitive data. Redact or mask PII before logging: log('user login', { email: maskEmail(email) })."
3746
+ });
3747
+ }
3748
+ return findings;
3749
+ }
3750
+ };
3751
+ var hardcodedOAuthSecret = {
3752
+ id: "VC119",
3753
+ title: "Hardcoded OAuth Client Secret",
3754
+ severity: "critical",
3755
+ category: "Secrets",
3756
+ description: "OAuth client secrets hardcoded in source code can be extracted and used to impersonate your application.",
3757
+ check(content, filePath) {
3758
+ if (isTestFile(filePath)) return [];
3759
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php|env|json|yaml|yml)$/)) return [];
3760
+ if (/package-lock|yarn\.lock|pnpm-lock|composer\.lock/i.test(filePath)) return [];
3761
+ const findings = [];
3762
+ const patterns = [
3763
+ /client[_-]?secret\s*[:=]\s*["'][a-zA-Z0-9_\-]{20,}["']/gi,
3764
+ /GOOGLE_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
3765
+ /GITHUB_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
3766
+ /FACEBOOK_APP_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi,
3767
+ /AUTH0_CLIENT_SECRET\s*[:=]\s*["'][^"']{10,}["']/gi
3768
+ ];
3769
+ for (const pat of patterns) {
3770
+ let m;
3771
+ while ((m = pat.exec(content)) !== null) {
3772
+ if (isCommentLine(content, m.index)) continue;
3773
+ if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
3774
+ const lineNum = content.substring(0, m.index).split("\n").length;
3775
+ findings.push({
3776
+ rule: "VC119",
3777
+ title: hardcodedOAuthSecret.title,
3778
+ severity: "critical",
3779
+ category: "Secrets",
3780
+ file: filePath,
3781
+ line: lineNum,
3782
+ snippet: getSnippet(content, lineNum),
3783
+ fix: "Move OAuth client secrets to environment variables. Never commit secrets to source control."
3784
+ });
3785
+ }
3786
+ }
3787
+ return findings;
3788
+ }
3789
+ };
3790
+ var missingOAuthState = {
3791
+ id: "VC120",
3792
+ title: "OAuth Flow Missing State Parameter",
3793
+ severity: "high",
3794
+ category: "Authentication",
3795
+ description: "OAuth authorization requests without a state parameter are vulnerable to CSRF attacks, allowing attackers to link their account to a victim's session.",
3796
+ check(content, filePath) {
3797
+ if (isTestFile(filePath)) return [];
3798
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
3799
+ const findings = [];
3800
+ const oauthUrlPattern = /(?:authorize\?|\/oauth\/authorize|\/auth\?|authorization_endpoint)[^}]*(?:client_id|response_type)/gi;
3801
+ let m;
3802
+ while ((m = oauthUrlPattern.exec(content)) !== null) {
3803
+ if (isCommentLine(content, m.index)) continue;
3804
+ const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
3805
+ if (/state\s*[=:]/i.test(surrounding)) continue;
3806
+ const lineNum = content.substring(0, m.index).split("\n").length;
3807
+ findings.push({
3808
+ rule: "VC120",
3809
+ title: missingOAuthState.title,
3810
+ severity: "high",
3811
+ category: "Authentication",
3812
+ file: filePath,
3813
+ line: lineNum,
3814
+ snippet: getSnippet(content, lineNum),
3815
+ fix: "Always include a cryptographically random 'state' parameter in OAuth authorization requests and validate it on callback."
3816
+ });
3817
+ }
3818
+ return findings;
3819
+ }
3820
+ };
3821
+ var unpinnedGitHubAction = {
3822
+ id: "VC121",
3823
+ title: "Unpinned GitHub Actions Version",
3824
+ severity: "high",
3825
+ category: "Supply Chain",
3826
+ description: "GitHub Actions using branch references (@main, @master) instead of commit SHAs can be compromised via supply-chain attacks.",
3827
+ check(content, filePath) {
3828
+ if (!filePath.match(/\.github\/workflows\/.*\.(yml|yaml)$/)) return [];
3829
+ const findings = [];
3830
+ const usesPattern = /uses\s*:\s*[\w\-]+\/[\w\-]+@(main|master|dev|develop|latest)\b/gi;
3831
+ let m;
3832
+ while ((m = usesPattern.exec(content)) !== null) {
3833
+ if (isCommentLine(content, m.index)) continue;
3834
+ const lineNum = content.substring(0, m.index).split("\n").length;
3835
+ findings.push({
3836
+ rule: "VC121",
3837
+ title: unpinnedGitHubAction.title,
3838
+ severity: "high",
3839
+ category: "Supply Chain",
3840
+ file: filePath,
3841
+ line: lineNum,
3842
+ snippet: getSnippet(content, lineNum),
3843
+ fix: "Pin GitHub Actions to a specific commit SHA instead of a branch name: uses: owner/action@<full-sha>"
3844
+ });
3845
+ }
3846
+ return findings;
3847
+ }
3848
+ };
3849
+ var deprecatedTLS = {
3850
+ id: "VC122",
3851
+ title: "Deprecated TLS Version Configured",
3852
+ severity: "high",
3853
+ category: "Cryptography",
3854
+ description: "TLS 1.0 and 1.1 are deprecated and have known vulnerabilities. Only TLS 1.2+ should be used.",
3855
+ check(content, filePath) {
3856
+ if (isTestFile(filePath)) return [];
3857
+ if (!filePath.match(/\.(js|ts|py|rb|go|java|yaml|yml|json|conf|cfg|xml)$/)) return [];
3858
+ const findings = [];
3859
+ const patterns = [
3860
+ /(?:TLSv1_METHOD|TLSv1\.0|TLSv1\.1|ssl\.PROTOCOL_TLSv1|SSLv3|SSLv2|TLS_1_0|TLS_1_1|minVersion\s*[:=]\s*["']TLSv1(?:\.1)?["'])/gi,
3861
+ /(?:tls\.DEFAULT_MIN_VERSION|secureProtocol)\s*[:=]\s*["'](?:TLSv1_method|TLSv1_1_method|SSLv3_method)["']/gi,
3862
+ /ssl_protocols\s+.*(?:TLSv1(?:\.1)?(?:\s|;))/gi
3863
+ ];
3864
+ for (const pat of patterns) {
3865
+ let m;
3866
+ while ((m = pat.exec(content)) !== null) {
3867
+ if (isCommentLine(content, m.index)) continue;
3868
+ const lineNum = content.substring(0, m.index).split("\n").length;
3869
+ findings.push({
3870
+ rule: "VC122",
3871
+ title: deprecatedTLS.title,
3872
+ severity: "high",
3873
+ category: "Cryptography",
3874
+ file: filePath,
3875
+ line: lineNum,
3876
+ snippet: getSnippet(content, lineNum),
3877
+ fix: "Use TLS 1.2 or higher. Set minVersion to 'TLSv1.2' and remove support for TLS 1.0/1.1."
3878
+ });
3879
+ }
3880
+ }
3881
+ return findings;
3882
+ }
3883
+ };
3884
+ var weakRSAKeySize = {
3885
+ id: "VC123",
3886
+ title: "Weak RSA Key Size",
3887
+ severity: "high",
3888
+ category: "Cryptography",
3889
+ description: "RSA keys smaller than 2048 bits are considered insecure and can be factored with modern hardware.",
3890
+ check(content, filePath) {
3891
+ if (isTestFile(filePath)) return [];
3892
+ if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
3893
+ const findings = [];
3894
+ const patterns = [
3895
+ /generateKeyPair\s*\(\s*["']rsa["']\s*,\s*\{[^}]*modulusLength\s*:\s*(512|768|1024)\b/gi,
3896
+ /RSA\.generate\s*\(\s*(512|768|1024)\b/gi,
3897
+ /rsa\.GenerateKey\s*\([^,]*,\s*(512|768|1024)\b/gi,
3898
+ /KeyPairGenerator.*initialize\s*\(\s*(512|768|1024)\b/gi
3899
+ ];
3900
+ for (const pat of patterns) {
3901
+ let m;
3902
+ while ((m = pat.exec(content)) !== null) {
3903
+ if (isCommentLine(content, m.index)) continue;
3904
+ const lineNum = content.substring(0, m.index).split("\n").length;
3905
+ findings.push({
3906
+ rule: "VC123",
3907
+ title: weakRSAKeySize.title,
3908
+ severity: "high",
3909
+ category: "Cryptography",
3910
+ file: filePath,
3911
+ line: lineNum,
3912
+ snippet: getSnippet(content, lineNum),
3913
+ fix: "Use RSA key sizes of at least 2048 bits. 4096 bits is recommended for long-term security."
3914
+ });
3915
+ }
3916
+ }
3917
+ return findings;
3918
+ }
3919
+ };
3920
+ var ecbModeEncryption = {
3921
+ id: "VC124",
3922
+ title: "Insecure ECB Mode Encryption",
3923
+ severity: "high",
3924
+ category: "Cryptography",
3925
+ description: "ECB (Electronic Codebook) mode encrypts identical plaintext blocks to identical ciphertext, leaking patterns in the data.",
3926
+ check(content, filePath) {
3927
+ if (isTestFile(filePath)) return [];
3928
+ if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
3929
+ const findings = [];
3930
+ const patterns = [
3931
+ /createCipher(?:iv)?\s*\(\s*["'](?:aes-(?:128|192|256)-ecb|des-ecb)["']/gi,
3932
+ /AES\.(?:new|MODE_ECB)|MODE_ECB/gi,
3933
+ /Cipher\.getInstance\s*\(\s*["']AES\/ECB/gi,
3934
+ /aes\.NewCipher|cipher\.NewECBEncrypter/gi
3935
+ ];
3936
+ for (const pat of patterns) {
3937
+ let m;
3938
+ while ((m = pat.exec(content)) !== null) {
3939
+ if (isCommentLine(content, m.index)) continue;
3940
+ const lineNum = content.substring(0, m.index).split("\n").length;
3941
+ findings.push({
3942
+ rule: "VC124",
3943
+ title: ecbModeEncryption.title,
3944
+ severity: "high",
3945
+ category: "Cryptography",
3946
+ file: filePath,
3947
+ line: lineNum,
3948
+ snippet: getSnippet(content, lineNum),
3949
+ fix: "Use AES-GCM or AES-CBC with HMAC instead of ECB mode. GCM provides both encryption and authentication."
3950
+ });
3951
+ }
3952
+ }
3953
+ return findings;
3954
+ }
3955
+ };
3956
+ var insecurePasswordReset = {
3957
+ id: "VC125",
3958
+ title: "Insecure Password Reset Implementation",
3959
+ severity: "high",
3960
+ category: "Authentication",
3961
+ description: "Password reset using predictable tokens, no expiration, or user-enumeration leaks can be exploited to take over accounts.",
3962
+ check(content, filePath) {
3963
+ if (isTestFile(filePath)) return [];
3964
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
3965
+ if (!/(?:reset|forgot).*(?:password|passwd)/i.test(content)) return [];
3966
+ const findings = [];
3967
+ const weakTokens = /(?:reset|forgot).*(?:token|code)\s*[:=].*(?:Date\.now|Math\.random|uuid\(\)|nanoid\(\))/gi;
3968
+ let m;
3969
+ while ((m = weakTokens.exec(content)) !== null) {
3970
+ if (isCommentLine(content, m.index)) continue;
3971
+ const lineNum = content.substring(0, m.index).split("\n").length;
3972
+ findings.push({
3973
+ rule: "VC125",
3974
+ title: insecurePasswordReset.title,
3975
+ severity: "high",
3976
+ category: "Authentication",
3977
+ file: filePath,
3978
+ line: lineNum,
3979
+ snippet: getSnippet(content, lineNum),
3980
+ fix: "Use crypto.randomBytes(32).toString('hex') for reset tokens. Set expiration (15-60 minutes) and single-use enforcement."
3981
+ });
3982
+ }
3983
+ const enumeration = /(?:user|email|account)\s*(?:not\s*found|does\s*not\s*exist|invalid)/gi;
3984
+ while ((m = enumeration.exec(content)) !== null) {
3985
+ if (isCommentLine(content, m.index)) continue;
3986
+ const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
3987
+ if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
3988
+ const lineNum = content.substring(0, m.index).split("\n").length;
3989
+ findings.push({
3990
+ rule: "VC125",
3991
+ title: insecurePasswordReset.title,
3992
+ severity: "high",
3993
+ category: "Authentication",
3994
+ file: filePath,
3995
+ line: lineNum,
3996
+ snippet: getSnippet(content, lineNum),
3997
+ fix: "Always return the same response regardless of whether the email exists. Say 'If an account exists, a reset link was sent.'"
3998
+ });
3999
+ }
4000
+ return findings;
4001
+ }
4002
+ };
4003
+ var terraformStateExposed = {
4004
+ id: "VC126",
4005
+ title: "Terraform State File Committed",
4006
+ severity: "critical",
4007
+ category: "Secrets",
4008
+ description: "Terraform state files contain sensitive infrastructure details, secrets, and access credentials in plaintext. They must never be committed to version control.",
4009
+ check(content, filePath) {
4010
+ const findings = [];
4011
+ if (/terraform\.tfstate(\.backup)?$/.test(filePath)) {
4012
+ findings.push({
4013
+ rule: "VC126",
4014
+ title: terraformStateExposed.title,
4015
+ severity: "critical",
4016
+ category: "Secrets",
4017
+ file: filePath,
4018
+ line: 1,
4019
+ snippet: getSnippet(content, 1),
4020
+ fix: "Add '*.tfstate' and '*.tfstate.backup' to .gitignore. Use remote state backends (S3, GCS, Terraform Cloud) instead."
4021
+ });
4022
+ }
4023
+ if (filePath.endsWith(".gitignore") && !content.includes("tfstate")) {
4024
+ if (/\.tf$/.test(filePath) || content.includes(".tf")) {
4025
+ }
4026
+ }
4027
+ return findings;
4028
+ }
4029
+ };
4030
+ var insecureHTTPMethods = {
4031
+ id: "VC127",
4032
+ title: "Dangerous HTTP Methods Without Auth",
4033
+ severity: "high",
4034
+ category: "Authorization",
4035
+ description: "DELETE, PUT, and PATCH endpoints without authentication checks allow unauthorized data modification or deletion.",
4036
+ check(content, filePath) {
4037
+ if (isTestFile(filePath)) return [];
4038
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
4039
+ const findings = [];
4040
+ const routePatterns = [
4041
+ /(?:app|router|server)\.(delete|put|patch)\s*\(\s*["']/gi
4042
+ ];
4043
+ for (const pat of routePatterns) {
4044
+ let m;
4045
+ while ((m = pat.exec(content)) !== null) {
4046
+ if (isCommentLine(content, m.index)) continue;
4047
+ const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
4048
+ if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
4049
+ const lineNum = content.substring(0, m.index).split("\n").length;
4050
+ findings.push({
4051
+ rule: "VC127",
4052
+ title: insecureHTTPMethods.title,
4053
+ severity: "high",
4054
+ category: "Authorization",
4055
+ file: filePath,
4056
+ line: lineNum,
4057
+ snippet: getSnippet(content, lineNum),
4058
+ fix: "Add authentication middleware to DELETE, PUT, and PATCH routes. Verify the user has permission to modify the resource."
4059
+ });
4060
+ }
4061
+ }
4062
+ return findings;
4063
+ }
4064
+ };
4065
+ var httpRequestSmuggling = {
4066
+ id: "VC128",
4067
+ title: "HTTP Request Smuggling Risk",
4068
+ severity: "high",
4069
+ category: "Configuration",
4070
+ description: "Manually parsing Content-Length or Transfer-Encoding headers can lead to request smuggling when behind a reverse proxy.",
4071
+ check(content, filePath) {
4072
+ if (isTestFile(filePath)) return [];
4073
+ if (!filePath.match(/\.(js|ts|py|rb|go|java|php)$/)) return [];
4074
+ const findings = [];
4075
+ const patterns = [
4076
+ /(?:headers?\[?|getHeader\s*\(\s*)["'](?:content-length|transfer-encoding)["']\s*\]?\s*\)/gi,
4077
+ /parseInt\s*\(\s*(?:req|request)\.headers?\[?\s*["']content-length["']/gi
4078
+ ];
4079
+ for (const pat of patterns) {
4080
+ let m;
4081
+ while ((m = pat.exec(content)) !== null) {
4082
+ if (isCommentLine(content, m.index)) continue;
4083
+ const lineNum = content.substring(0, m.index).split("\n").length;
4084
+ findings.push({
4085
+ rule: "VC128",
4086
+ title: httpRequestSmuggling.title,
4087
+ severity: "high",
4088
+ category: "Configuration",
4089
+ file: filePath,
4090
+ line: lineNum,
4091
+ snippet: getSnippet(content, lineNum),
4092
+ fix: "Let your framework handle Content-Length and Transfer-Encoding headers. Do not manually parse or trust these values."
4093
+ });
4094
+ }
4095
+ }
4096
+ return findings;
4097
+ }
4098
+ };
4099
+ var unencryptedPII = {
4100
+ id: "VC129",
4101
+ title: "Sensitive Data Stored Without Encryption",
4102
+ severity: "high",
4103
+ category: "Information Leakage",
4104
+ description: "Database schemas storing PII (SSN, credit cards, health records) in plaintext violate compliance requirements and expose data in breaches.",
4105
+ check(content, filePath) {
4106
+ if (isTestFile(filePath)) return [];
4107
+ if (!filePath.match(/\.(sql|prisma|py|ts|js|rb)$/)) return [];
4108
+ const findings = [];
4109
+ const schemaPatterns = [
4110
+ /(?:column|field|Column|Field)\s*\(\s*["'](?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport|driver_license|bank_account|routing_number)["']\s*,\s*(?:String|TEXT|VARCHAR|Text|text)/gi,
4111
+ /(?:ssn|social_security|tax_id|credit_card|card_number|cvv|passport_number|driver_license|bank_account|routing_number)\s+(?:TEXT|VARCHAR|String|text|character varying)/gi
4112
+ ];
4113
+ for (const pat of schemaPatterns) {
4114
+ let m;
4115
+ while ((m = pat.exec(content)) !== null) {
4116
+ if (isCommentLine(content, m.index)) continue;
4117
+ const lineNum = content.substring(0, m.index).split("\n").length;
4118
+ findings.push({
4119
+ rule: "VC129",
4120
+ title: unencryptedPII.title,
4121
+ severity: "high",
4122
+ category: "Information Leakage",
4123
+ file: filePath,
4124
+ line: lineNum,
4125
+ snippet: getSnippet(content, lineNum),
4126
+ fix: "Encrypt sensitive fields at the application level before storing. Use field-level encryption with a KMS for SSN, credit cards, and health data."
4127
+ });
4128
+ }
4129
+ }
4130
+ return findings;
4131
+ }
4132
+ };
4133
+ var missingAuthRateLimit = {
4134
+ id: "VC130",
4135
+ title: "Authentication Endpoint Without Rate Limiting",
4136
+ severity: "high",
4137
+ category: "Authentication",
4138
+ description: "Login, registration, and password reset endpoints without rate limiting are vulnerable to credential stuffing and brute-force attacks.",
4139
+ check(content, filePath) {
4140
+ if (isTestFile(filePath)) return [];
4141
+ if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb|go|java|php)$/)) return [];
4142
+ const findings = [];
4143
+ const authRoutePattern = /(?:app|router|server)\.(?:post|put)\s*\(\s*["'](?:\/(?:api\/)?(?:auth\/)?(?:login|signin|sign-in|register|signup|sign-up|forgot-password|reset-password))["']/gi;
4144
+ let m;
4145
+ while ((m = authRoutePattern.exec(content)) !== null) {
4146
+ if (isCommentLine(content, m.index)) continue;
4147
+ const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
4148
+ if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
4149
+ const lineNum = content.substring(0, m.index).split("\n").length;
4150
+ findings.push({
4151
+ rule: "VC130",
4152
+ title: missingAuthRateLimit.title,
4153
+ severity: "high",
4154
+ category: "Authentication",
4155
+ file: filePath,
4156
+ line: lineNum,
4157
+ snippet: getSnippet(content, lineNum),
4158
+ fix: "Add rate limiting to authentication endpoints. Limit to 5-10 attempts per minute per IP. Use express-rate-limit or similar."
4159
+ });
4160
+ }
4161
+ return findings;
4162
+ }
4163
+ };
4164
+ var vulnerableDependencies = {
4165
+ id: "VC131",
4166
+ title: "Potentially Vulnerable Dependency",
4167
+ severity: "high",
4168
+ category: "Supply Chain",
4169
+ description: "Dependencies with known security issues that are commonly found in AI-generated codebases.",
4170
+ check(content, filePath) {
4171
+ if (!filePath.endsWith("package.json")) return [];
4172
+ if (/node_modules|\.next|dist|build/.test(filePath)) return [];
4173
+ const findings = [];
4174
+ const vulnerablePackages = [
4175
+ [/"jsonwebtoken"\s*:\s*"[\^~]?[0-7]\./g, "jsonwebtoken < 8.x has signature bypass vulnerabilities"],
4176
+ [/"lodash"\s*:\s*"[\^~]?[0-3]\./g, "lodash < 4.x has prototype pollution vulnerabilities"],
4177
+ [/"minimist"\s*:\s*"[\^~]?[01]\.[01]\./g, "minimist < 1.2.6 has prototype pollution"],
4178
+ [/"node-fetch"\s*:\s*"[\^~]?[12]\./g, "node-fetch < 3.x has data exposure issues"],
4179
+ [/"express"\s*:\s*"[\^~]?[0-3]\./g, "express < 4.x has multiple known vulnerabilities"],
4180
+ [/"axios"\s*:\s*"[\^~]?0\.[0-9]\./g, "axios < 0.21 has SSRF vulnerabilities"],
4181
+ [/"tar"\s*:\s*"[\^~]?[0-5]\./g, "tar < 6.x has path traversal vulnerabilities"],
4182
+ [/"glob-parent"\s*:\s*"[\^~]?[0-4]\./g, "glob-parent < 5.1.2 has ReDoS vulnerability"]
4183
+ ];
4184
+ for (const [pat, message] of vulnerablePackages) {
4185
+ let m;
4186
+ while ((m = pat.exec(content)) !== null) {
4187
+ const lineNum = content.substring(0, m.index).split("\n").length;
4188
+ findings.push({
4189
+ rule: "VC131",
4190
+ title: vulnerableDependencies.title,
4191
+ severity: "high",
4192
+ category: "Supply Chain",
4193
+ file: filePath,
4194
+ line: lineNum,
4195
+ snippet: getSnippet(content, lineNum),
4196
+ fix: `${message}. Update to the latest version and run 'npm audit' regularly.`
4197
+ });
4198
+ }
4199
+ }
4200
+ return findings;
4201
+ }
4202
+ };
4203
+ var freeRules = [
4204
+ hardcodedSecrets,
4205
+ // VC001
4206
+ exposedEnvFile,
4207
+ // VC002
4208
+ missingAuthMiddleware,
4209
+ // VC003
4210
+ supabaseNoRLS,
4211
+ // VC004
4212
+ stripeWebhookUnprotected,
4213
+ // VC005
4214
+ sqlInjection,
4215
+ // VC006
4216
+ xssVulnerability,
4217
+ // VC007
4218
+ noRateLimiting,
4219
+ // VC008
4220
+ corsWildcard,
4221
+ // VC009
4222
+ clientSideAuth,
4223
+ // VC010
4224
+ nextPublicSecret,
4225
+ // VC011
4226
+ envNotGitignored,
4227
+ // VC014
4228
+ evalUsage,
4229
+ // VC015
4230
+ unvalidatedRedirect,
4231
+ // VC016
4232
+ insecureCookies,
4233
+ // VC017
4234
+ exposedAuthSecret,
4235
+ // VC018
4236
+ missingCSP,
4237
+ // VC020
4238
+ hardcodedJWTSecret,
4239
+ // VC031
4240
+ missingHTTPS,
4241
+ // VC032
4242
+ exposedDebugMode,
4243
+ // VC033
4244
+ insecureRandomness,
4245
+ // VC034
4246
+ missingErrorBoundary,
4247
+ // VC036
4248
+ exposedStackTraces,
4249
+ // VC037
4250
+ missingLockFile,
4251
+ // VC039
4252
+ dangerousInnerHTML,
4253
+ // VC063
4254
+ consoleLogProduction,
4255
+ // VC097
4256
+ emptyCatchBlock,
4257
+ // VC104
4258
+ todoLeftInCode,
4259
+ // VC103
4260
+ weakHashing,
4261
+ // VC060
4262
+ disabledTLSVerification
4263
+ // VC061
4264
+ ];
4265
+ var proRules = [
3369
4266
  hardcodedSecrets,
3370
4267
  exposedEnvFile,
3371
4268
  missingAuthMiddleware,
@@ -3471,14 +4368,55 @@ var allRules = [
3471
4368
  todoLeftInCode,
3472
4369
  emptyCatchBlock,
3473
4370
  callbackHell,
3474
- magicNumbers
4371
+ magicNumbers,
4372
+ s3BucketNoEncryption,
4373
+ securityGroupAllInbound,
4374
+ rdsPubliclyAccessible,
4375
+ missingCloudTrail,
4376
+ lambdaWithoutVPC,
4377
+ dockerLatestTag,
4378
+ dockerCopySensitive,
4379
+ dockerTooManyPorts,
4380
+ k8sSecretNotEncrypted,
4381
+ k8sNoResourceLimits,
4382
+ pathTraversal,
4383
+ // VC117
4384
+ piiInLogs,
4385
+ // VC118
4386
+ hardcodedOAuthSecret,
4387
+ // VC119
4388
+ missingOAuthState,
4389
+ // VC120
4390
+ unpinnedGitHubAction,
4391
+ // VC121
4392
+ deprecatedTLS,
4393
+ // VC122
4394
+ weakRSAKeySize,
4395
+ // VC123
4396
+ ecbModeEncryption,
4397
+ // VC124
4398
+ insecurePasswordReset,
4399
+ // VC125
4400
+ terraformStateExposed,
4401
+ // VC126
4402
+ insecureHTTPMethods,
4403
+ // VC127
4404
+ httpRequestSmuggling,
4405
+ // VC128
4406
+ unencryptedPII,
4407
+ // VC129
4408
+ missingAuthRateLimit,
4409
+ // VC130
4410
+ vulnerableDependencies
4411
+ // VC131
3475
4412
  ];
3476
- function runCustomRules(content, filePath, disabledRules = []) {
4413
+ function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
3477
4414
  const findings = [];
3478
4415
  if (/function runScan\(files\)|export function runCustomRules/.test(content) && /const (?:rules|allRules)\s*[:=]/.test(content) && /findMatches/.test(content)) {
3479
4416
  return findings;
3480
4417
  }
3481
- for (const rule of allRules) {
4418
+ const ruleset = tier === "pro" ? proRules : freeRules;
4419
+ for (const rule of ruleset) {
3482
4420
  if (disabledRules.includes(rule.id)) continue;
3483
4421
  const matches = rule.check(content, filePath);
3484
4422
  const compliance = complianceMap[rule.id];
@@ -5479,7 +6417,7 @@ Open this URL in your browser to log in:`));
5479
6417
  var program = new Command();
5480
6418
  program.name("xploitscan").description(
5481
6419
  "AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
5482
- ).version("0.3.0");
6420
+ ).version("0.7.0");
5483
6421
  program.command("scan").description("Scan a directory for security vulnerabilities").argument("[directory]", "Directory to scan", ".").option("--no-ai", "Skip AI-powered analysis").option("-f, --format <format>", "Output format: terminal, json, sarif", "terminal").option("-v, --verbose", "Show detailed output", false).option("--diff [base]", "Scan only files changed vs base branch (default: main)").option("-w, --watch", "Watch for file changes and re-scan automatically", false).action(async (directory, opts) => {
5484
6422
  await scanCommand(directory, {
5485
6423
  directory,
@@ -5495,7 +6433,7 @@ auth.command("login").description("Log in to your XploitScan account").action(lo
5495
6433
  auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
5496
6434
  auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
5497
6435
  program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
5498
- const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-QTNXZY4O.js");
6436
+ const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-Z7VNGPT2.js");
5499
6437
  const chalk4 = (await import("chalk")).default;
5500
6438
  const token = getStoredToken2();
5501
6439
  if (!token) {
@@ -5508,7 +6446,7 @@ program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited
5508
6446
  console.log(chalk4.green(`
5509
6447
  Open this URL to upgrade:`));
5510
6448
  console.log(chalk4.bold.underline(url));
5511
- const { execFile: execFile4 } = __require("child_process");
6449
+ const { execFile: execFile4 } = await import("child_process");
5512
6450
  const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
5513
6451
  execFile4(openCmd, [url], () => {
5514
6452
  });