xploitscan 0.6.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 +591 -7
- 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",
|
|
@@ -3646,6 +3680,526 @@ var k8sNoResourceLimits = {
|
|
|
3646
3680
|
}];
|
|
3647
3681
|
}
|
|
3648
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
|
+
};
|
|
3649
4203
|
var freeRules = [
|
|
3650
4204
|
hardcodedSecrets,
|
|
3651
4205
|
// VC001
|
|
@@ -3824,7 +4378,37 @@ var proRules = [
|
|
|
3824
4378
|
dockerCopySensitive,
|
|
3825
4379
|
dockerTooManyPorts,
|
|
3826
4380
|
k8sSecretNotEncrypted,
|
|
3827
|
-
k8sNoResourceLimits
|
|
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
|
|
3828
4412
|
];
|
|
3829
4413
|
function runCustomRules(content, filePath, disabledRules = [], tier = "free") {
|
|
3830
4414
|
const findings = [];
|
|
@@ -5833,7 +6417,7 @@ Open this URL in your browser to log in:`));
|
|
|
5833
6417
|
var program = new Command();
|
|
5834
6418
|
program.name("xploitscan").description(
|
|
5835
6419
|
"AI security scanner for vibe-coded apps. Find vulnerabilities before attackers do."
|
|
5836
|
-
).version("0.
|
|
6420
|
+
).version("0.7.0");
|
|
5837
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) => {
|
|
5838
6422
|
await scanCommand(directory, {
|
|
5839
6423
|
directory,
|
|
@@ -5849,7 +6433,7 @@ auth.command("login").description("Log in to your XploitScan account").action(lo
|
|
|
5849
6433
|
auth.command("logout").description("Log out of your XploitScan account").action(logoutCommand);
|
|
5850
6434
|
auth.command("whoami").description("Show current logged-in user").action(whoamiCommand);
|
|
5851
6435
|
program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited scans").action(async () => {
|
|
5852
|
-
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-
|
|
6436
|
+
const { getStoredToken: getStoredToken2, getCheckoutUrl } = await import("./api-Z7VNGPT2.js");
|
|
5853
6437
|
const chalk4 = (await import("chalk")).default;
|
|
5854
6438
|
const token = getStoredToken2();
|
|
5855
6439
|
if (!token) {
|
|
@@ -5862,7 +6446,7 @@ program.command("upgrade").description("Upgrade to XploitScan Pro for unlimited
|
|
|
5862
6446
|
console.log(chalk4.green(`
|
|
5863
6447
|
Open this URL to upgrade:`));
|
|
5864
6448
|
console.log(chalk4.bold.underline(url));
|
|
5865
|
-
const { execFile: execFile4 } =
|
|
6449
|
+
const { execFile: execFile4 } = await import("child_process");
|
|
5866
6450
|
const openCmd = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
5867
6451
|
execFile4(openCmd, [url], () => {
|
|
5868
6452
|
});
|