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/{api-QTNXZY4O.js → api-Z7VNGPT2.js} +2 -2
- package/dist/{chunk-47X3TUVR.js → chunk-CBDFSACC.js} +1 -8
- package/dist/{chunk-47X3TUVR.js.map → chunk-CBDFSACC.js.map} +1 -1
- package/dist/index.js +948 -10
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
- /package/dist/{api-QTNXZY4O.js.map → api-Z7VNGPT2.js.map} +0 -0
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-
|
|
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
|
|
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
|
-
|
|
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.
|
|
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-
|
|
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 } =
|
|
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
|
});
|