xploitscan-shared-rules 1.13.0 → 1.14.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.cjs +287 -84
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +5 -4
- package/dist/index.d.ts +5 -4
- package/dist/index.js +285 -84
- package/dist/index.js.map +1 -1
- package/package.json +3 -3
package/dist/index.js
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
// src/snippet.ts
|
|
2
|
-
|
|
2
|
+
var cacheContent = null;
|
|
3
|
+
var cacheLines = [];
|
|
4
|
+
var cacheOffsets = [];
|
|
5
|
+
function lineIndex(content) {
|
|
6
|
+
if (content === cacheContent) return { lines: cacheLines, offsets: cacheOffsets };
|
|
3
7
|
const lines = content.split("\n");
|
|
8
|
+
const offsets = new Array(lines.length);
|
|
9
|
+
let off = 0;
|
|
10
|
+
for (let i = 0; i < lines.length; i++) {
|
|
11
|
+
offsets[i] = off;
|
|
12
|
+
off += lines[i].length + 1;
|
|
13
|
+
}
|
|
14
|
+
cacheContent = content;
|
|
15
|
+
cacheLines = lines;
|
|
16
|
+
cacheOffsets = offsets;
|
|
17
|
+
return { lines, offsets };
|
|
18
|
+
}
|
|
19
|
+
function lineNumberAt(content, index) {
|
|
20
|
+
const { offsets } = lineIndex(content);
|
|
21
|
+
let lo = 0;
|
|
22
|
+
let hi = offsets.length - 1;
|
|
23
|
+
let ans = 0;
|
|
24
|
+
while (lo <= hi) {
|
|
25
|
+
const mid = lo + hi >> 1;
|
|
26
|
+
if (offsets[mid] <= index) {
|
|
27
|
+
ans = mid;
|
|
28
|
+
lo = mid + 1;
|
|
29
|
+
} else {
|
|
30
|
+
hi = mid - 1;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return ans + 1;
|
|
34
|
+
}
|
|
35
|
+
function getSnippet(content, line, contextLines = 2) {
|
|
36
|
+
const { lines } = lineIndex(content);
|
|
4
37
|
const start = Math.max(0, line - 1 - contextLines);
|
|
5
38
|
const end = Math.min(lines.length, line + contextLines);
|
|
6
|
-
|
|
7
|
-
|
|
39
|
+
const out = [];
|
|
40
|
+
for (let i = start; i < end; i++) {
|
|
41
|
+
const lineNum = i + 1;
|
|
8
42
|
const marker = lineNum === line ? ">" : " ";
|
|
9
|
-
|
|
10
|
-
}
|
|
43
|
+
out.push(`${marker} ${lineNum.toString().padStart(4)} | ${lines[i]}`);
|
|
44
|
+
}
|
|
45
|
+
return out.join("\n");
|
|
11
46
|
}
|
|
12
47
|
|
|
13
48
|
// src/rule-impacts.ts
|
|
@@ -221,7 +256,9 @@ var RULE_IMPACTS = {
|
|
|
221
256
|
VC207: "Model output is attacker-influenceable via prompt injection. Feeding it into eval, new Function, a shell command, a raw SQL string, or a filesystem path turns a crafted or hallucinated response into remote code execution, command injection, SQL injection, or path traversal \u2014 your most dangerous sinks, driven by untrusted text.",
|
|
222
257
|
VC208: "Interpolating a secret into a prompt ships your API key, token, or password to a third-party model provider, where it persists in their request logs and training-eligible data. A credential that leaves your infrastructure in prompt text should be considered compromised and rotated.",
|
|
223
258
|
VC209: "Webhooks are delivered at-least-once. Without de-duplicating on the event id, a retried or replayed delivery re-runs the side effect \u2014 a customer is charged twice, a record is duplicated, or an entitlement is granted again. Stripe and Svix both retry on any non-2xx, so this fires in normal operation, not just under attack.",
|
|
224
|
-
VC210: "If your auth middleware skips /api, those routes run with no gate unless each one re-checks auth itself. It is the most common way a Next.js app ends up with publicly callable API routes that everyone assumed the middleware was protecting."
|
|
259
|
+
VC210: "If your auth middleware skips /api, those routes run with no gate unless each one re-checks auth itself. It is the most common way a Next.js app ends up with publicly callable API routes that everyone assumed the middleware was protecting.",
|
|
260
|
+
VC211: "A real credit card number in source is cardholder data sitting in git history, CI logs, and every backup \u2014 a direct PCI-DSS violation. Anyone with repo access can read it, and it cannot be un-leaked once committed; the card must be treated as compromised.",
|
|
261
|
+
VC212: "A hardcoded Social Security Number is regulated PII permanently embedded in your git history and backups. It exposes a real person to identity theft, and its presence in source can trigger breach-notification and privacy-law obligations the moment the repo is accessed."
|
|
225
262
|
};
|
|
226
263
|
|
|
227
264
|
// src/exposure.ts
|
|
@@ -815,7 +852,7 @@ var SERVER_SIDE_PATH_RE = new RegExp(
|
|
|
815
852
|
].join("|"),
|
|
816
853
|
"i"
|
|
817
854
|
);
|
|
818
|
-
var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b|(?:import|require)\b[^;\n]
|
|
855
|
+
var SERVER_SIDE_CONTENT_RE = /\b(?:req|request)\.(?:body|query|params|headers|cookies)\b|\b(?:app|router)\.(?:get|post|put|patch|delete|use|all)\s*\(|\bctx\.request\b|\bevent\.(?:body|queryStringParameters|pathParameters)\b|\(\s*req\s*,\s*res\b|(?:import|require)\b[^;\n]{0,200}['"]express['"]|\bNextFunction\b/;
|
|
819
856
|
function isServerSideFile(filePath, content) {
|
|
820
857
|
if (isTestFile(filePath)) return false;
|
|
821
858
|
if (SERVER_SIDE_PATH_RE.test(filePath)) return true;
|
|
@@ -862,13 +899,23 @@ function filterSilenced(matches, content, ruleId) {
|
|
|
862
899
|
}
|
|
863
900
|
function findMatches(content, pattern, rule, filePath, fixTemplate) {
|
|
864
901
|
const matches = [];
|
|
865
|
-
const lines = content.split("\n");
|
|
866
902
|
let m;
|
|
867
903
|
const re = new RegExp(pattern.source, pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`);
|
|
904
|
+
let scanned = 0;
|
|
905
|
+
let lineNum = 1;
|
|
906
|
+
let lastMatchIndex = -1;
|
|
907
|
+
const MAX_MATCHES = 500;
|
|
868
908
|
while ((m = re.exec(content)) !== null) {
|
|
909
|
+
if (m.index === lastMatchIndex && m[0].length === 0) {
|
|
910
|
+
re.lastIndex++;
|
|
911
|
+
continue;
|
|
912
|
+
}
|
|
913
|
+
lastMatchIndex = m.index;
|
|
869
914
|
if (isCommentLine(content, m.index)) continue;
|
|
870
915
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
871
|
-
|
|
916
|
+
for (; scanned < m.index; scanned++) {
|
|
917
|
+
if (content.charCodeAt(scanned) === 10) lineNum++;
|
|
918
|
+
}
|
|
872
919
|
matches.push({
|
|
873
920
|
rule: rule.id,
|
|
874
921
|
title: rule.title,
|
|
@@ -879,6 +926,7 @@ function findMatches(content, pattern, rule, filePath, fixTemplate) {
|
|
|
879
926
|
snippet: getSnippet(content, lineNum),
|
|
880
927
|
fix: fixTemplate?.(m)
|
|
881
928
|
});
|
|
929
|
+
if (matches.length >= MAX_MATCHES) break;
|
|
882
930
|
}
|
|
883
931
|
return matches;
|
|
884
932
|
}
|
|
@@ -894,10 +942,26 @@ function astMatch(content, filePath, line, rule, fix) {
|
|
|
894
942
|
fix
|
|
895
943
|
};
|
|
896
944
|
}
|
|
945
|
+
var parseCacheContent = null;
|
|
946
|
+
var parseCachePath = null;
|
|
947
|
+
var parseCacheResult = null;
|
|
948
|
+
var parseCacheValid = false;
|
|
897
949
|
function tryParse(content, filePath) {
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
950
|
+
if (parseCacheValid && content === parseCacheContent && filePath === parseCachePath) {
|
|
951
|
+
return parseCacheResult;
|
|
952
|
+
}
|
|
953
|
+
let result = null;
|
|
954
|
+
try {
|
|
955
|
+
const parsed = parseFile(content, filePath);
|
|
956
|
+
result = parsed ? { parsed, taint: buildTaintMap(parsed) } : null;
|
|
957
|
+
} catch {
|
|
958
|
+
result = null;
|
|
959
|
+
}
|
|
960
|
+
parseCacheResult = result;
|
|
961
|
+
parseCacheContent = content;
|
|
962
|
+
parseCachePath = filePath;
|
|
963
|
+
parseCacheValid = true;
|
|
964
|
+
return result;
|
|
901
965
|
}
|
|
902
966
|
var hardcodedSecrets = {
|
|
903
967
|
id: "VC001",
|
|
@@ -1265,7 +1329,7 @@ var xssVulnerability = {
|
|
|
1265
1329
|
const hasSanitizerBypass = /bypassSecurityTrust(?:Html|Script|Style|Url|ResourceUrl)\s*\(/.test(content);
|
|
1266
1330
|
if (/(?:sanitize|purify|escape|xss)/i.test(filePath)) return [];
|
|
1267
1331
|
if (!hasSanitizerBypass && /DOMPurify\.sanitize|sanitizeHtml|xss\(|escapeHtml/i.test(content)) return [];
|
|
1268
|
-
if (!hasSanitizerBypass && /(?:import|require)\s*\(
|
|
1332
|
+
if (!hasSanitizerBypass && /(?:import|require)\s*\(?[^\n]{0,200}(?:DOMPurify|dompurify|sanitize|sanitizer)/i.test(content)) return [];
|
|
1269
1333
|
const patterns = [
|
|
1270
1334
|
// React dangerouslySetInnerHTML
|
|
1271
1335
|
/dangerouslySetInnerHTML\s*=\s*\{\s*\{\s*__html\s*:/g,
|
|
@@ -1294,9 +1358,9 @@ var xssVulnerability = {
|
|
|
1294
1358
|
() => "Sanitize user input before rendering as HTML. Use a library like DOMPurify: DOMPurify.sanitize(userInput). If this site is intentional (JSON-LD structured data, a const boot script, a sandboxed preview), add an inline `// VC007-OK: <reason>` comment above the line to silence."
|
|
1295
1359
|
);
|
|
1296
1360
|
for (const m of raw) {
|
|
1297
|
-
const lineText = allLines[m.line - 1] || "";
|
|
1361
|
+
const lineText = (allLines[m.line - 1] || "").slice(0, 4e3);
|
|
1298
1362
|
if (/\.innerHTML\s*=\s*['"]/.test(lineText) && !/\$\{/.test(lineText)) continue;
|
|
1299
|
-
if (/\.innerHTML\s*=\s*['"][^'"]
|
|
1363
|
+
if (/\.innerHTML\s*=\s*['"][^'"]{0,2000}['"]\s*$/.test(lineText)) continue;
|
|
1300
1364
|
if (/dangerouslySetInnerHTML/.test(lineText)) {
|
|
1301
1365
|
const windowText = allLines.slice(m.line - 1, m.line + 2).join("\n");
|
|
1302
1366
|
if (/JSON\.stringify/i.test(windowText)) continue;
|
|
@@ -1547,7 +1611,7 @@ var evalUsage = {
|
|
|
1547
1611
|
/new\s+Function\s*\(\s*(?!["'`])/g
|
|
1548
1612
|
];
|
|
1549
1613
|
const hasEvalInString = /["'`]eval(?:-source-map|["'`])/i.test(content);
|
|
1550
|
-
if (hasEvalInString && !/\beval\s*\([^)]
|
|
1614
|
+
if (hasEvalInString && !/\beval\s*\([^)]{0,500}(?:req\.|body\.|input|params|user|data)/i.test(content)) return [];
|
|
1551
1615
|
const matches = [];
|
|
1552
1616
|
for (const p of patterns) {
|
|
1553
1617
|
const rawMatches = findMatches(
|
|
@@ -1588,7 +1652,7 @@ var unvalidatedRedirect = {
|
|
|
1588
1652
|
const isPureLiteral = /^["'`][^"'`]*["'`]$/.test(value);
|
|
1589
1653
|
if (!hasInterpolation && isPureLiteral) continue;
|
|
1590
1654
|
if (isInlineSilenced(content, m.index, "VC016")) continue;
|
|
1591
|
-
const line = content
|
|
1655
|
+
const line = lineNumberAt(content, m.index);
|
|
1592
1656
|
matches.push({
|
|
1593
1657
|
rule: "VC016",
|
|
1594
1658
|
title: unvalidatedRedirect.title,
|
|
@@ -1811,11 +1875,11 @@ var prototypePollution = {
|
|
|
1811
1875
|
];
|
|
1812
1876
|
const hasValidation = /schema|validate|sanitize|whitelist|allowedKeys|pick\(|Object\.freeze|zod|yup|joi|ajv/i.test(content);
|
|
1813
1877
|
if (!hasValidation) {
|
|
1814
|
-
const hasUnsafeMerge = /Object\.assign\s*\([^)]
|
|
1878
|
+
const hasUnsafeMerge = /Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse|\{[^\n]{0,300}\.\.\.(?:stored|saved|cached|parsed|data)/i.test(content);
|
|
1815
1879
|
if (hasUnsafeMerge) {
|
|
1816
1880
|
matches.push(...findMatches(
|
|
1817
1881
|
content,
|
|
1818
|
-
/Object\.assign\s*\([^)]
|
|
1882
|
+
/Object\.assign\s*\([^)]{0,500}JSON\.parse|\.\.\.JSON\.parse/g,
|
|
1819
1883
|
prototypePollution,
|
|
1820
1884
|
filePath,
|
|
1821
1885
|
() => "Validate parsed data against an expected schema before merging into objects. Use Object.freeze(), a validation library (Zod, Yup), or manually check for __proto__ and constructor keys."
|
|
@@ -3361,7 +3425,7 @@ var dangerousInnerHTML = {
|
|
|
3361
3425
|
if (/^[A-Z][A-Z0-9_]+$/.test(value)) continue;
|
|
3362
3426
|
if (/^["'`][^$]*["'`]$/.test(value)) continue;
|
|
3363
3427
|
if (/JSON\.stringify/i.test(value)) continue;
|
|
3364
|
-
const lineNum = content
|
|
3428
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
3365
3429
|
findings.push({
|
|
3366
3430
|
rule: "VC063",
|
|
3367
3431
|
title: dangerousInnerHTML.title,
|
|
@@ -4103,7 +4167,7 @@ var missingSRI = {
|
|
|
4103
4167
|
const re = new RegExp(scriptPattern.source, scriptPattern.flags);
|
|
4104
4168
|
while ((m = re.exec(content)) !== null) {
|
|
4105
4169
|
if (!m[0].includes("integrity")) {
|
|
4106
|
-
const lineNum = content
|
|
4170
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
4107
4171
|
matches.push({
|
|
4108
4172
|
rule: "VC084",
|
|
4109
4173
|
title: missingSRI.title,
|
|
@@ -4735,8 +4799,13 @@ var complianceMap = {
|
|
|
4735
4799
|
// VC209–VC210: advisory heuristics
|
|
4736
4800
|
VC209: { owasp: "A04:2021", cwe: "CWE-799" },
|
|
4737
4801
|
// webhook missing idempotency
|
|
4738
|
-
VC210: { owasp: "A01:2021", cwe: "CWE-862" }
|
|
4802
|
+
VC210: { owasp: "A01:2021", cwe: "CWE-862" },
|
|
4739
4803
|
// middleware matcher excludes /api
|
|
4804
|
+
// VC211–VC212: hardcoded sensitive personal data (PII) in source
|
|
4805
|
+
VC211: { owasp: "A02:2021", cwe: "CWE-540" },
|
|
4806
|
+
// hardcoded credit card number
|
|
4807
|
+
VC212: { owasp: "A02:2021", cwe: "CWE-540" }
|
|
4808
|
+
// hardcoded US SSN
|
|
4740
4809
|
};
|
|
4741
4810
|
var consoleLogProduction = {
|
|
4742
4811
|
id: "VC097",
|
|
@@ -4974,7 +5043,7 @@ var s3BucketNoEncryption = {
|
|
|
4974
5043
|
const blockEnd = Math.min(m.index + 2e3, content.length);
|
|
4975
5044
|
const blockContent = content.substring(m.index, blockEnd);
|
|
4976
5045
|
if (!/server_side_encryption/.test(blockContent)) {
|
|
4977
|
-
const lineNum = content
|
|
5046
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
4978
5047
|
matches.push({
|
|
4979
5048
|
rule: "VC107",
|
|
4980
5049
|
title: s3BucketNoEncryption.title,
|
|
@@ -5005,7 +5074,7 @@ var securityGroupAllInbound = {
|
|
|
5005
5074
|
while ((m = ingressPattern.exec(content)) !== null) {
|
|
5006
5075
|
if (isCommentLine(content, m.index)) continue;
|
|
5007
5076
|
if (/from_port\s*=\s*0/.test(m[0]) || !/from_port/.test(m[0])) {
|
|
5008
|
-
const lineNum = content
|
|
5077
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5009
5078
|
matches.push({
|
|
5010
5079
|
rule: "VC108",
|
|
5011
5080
|
title: securityGroupAllInbound.title,
|
|
@@ -5092,7 +5161,7 @@ var lambdaWithoutVPC = {
|
|
|
5092
5161
|
const blockEnd = Math.min(m.index + 2e3, content.length);
|
|
5093
5162
|
const blockContent = content.substring(m.index, blockEnd);
|
|
5094
5163
|
if (!/vpc_config\s*\{/.test(blockContent)) {
|
|
5095
|
-
const lineNum = content
|
|
5164
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5096
5165
|
matches.push({
|
|
5097
5166
|
rule: "VC111",
|
|
5098
5167
|
title: lambdaWithoutVPC.title,
|
|
@@ -5122,7 +5191,7 @@ var dockerLatestTag = {
|
|
|
5122
5191
|
while ((m = fromPattern.exec(content)) !== null) {
|
|
5123
5192
|
const image = m[1];
|
|
5124
5193
|
if (image.endsWith(":latest") || !image.includes(":") && !image.startsWith("$")) {
|
|
5125
|
-
const lineNum = content
|
|
5194
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5126
5195
|
matches.push({
|
|
5127
5196
|
rule: "VC112",
|
|
5128
5197
|
title: dockerLatestTag.title,
|
|
@@ -5250,17 +5319,26 @@ var pathTraversal = {
|
|
|
5250
5319
|
const findings = [];
|
|
5251
5320
|
const patterns = [
|
|
5252
5321
|
/(?:readFile|readFileSync|createReadStream|writeFile|writeFileSync|appendFile|unlink|unlinkSync|access|stat|statSync|existsSync)\s*\(\s*(?:req\.|request\.|ctx\.|params\.|query\.)/gi,
|
|
5253
|
-
/(?:path\.join|path\.resolve)\s*\([^)]
|
|
5254
|
-
/(?:res\.sendFile|res\.download)\s*\([^)]
|
|
5322
|
+
/(?:path\.join|path\.resolve)\s*\([^)]{0,500}(?:req\.|request\.|ctx\.|params\.|query\.)[^)]{0,500}\)/gi,
|
|
5323
|
+
/(?:res\.sendFile|res\.download)\s*\([^)]{0,500}(?:req\.|request\.)/gi,
|
|
5255
5324
|
/open\s*\(\s*(?:request\.|params\[|args\.)/gi
|
|
5256
5325
|
];
|
|
5326
|
+
const MAX_MATCHES = 500;
|
|
5257
5327
|
for (const pat of patterns) {
|
|
5328
|
+
if (findings.length >= MAX_MATCHES) break;
|
|
5258
5329
|
let m;
|
|
5330
|
+
let lastIdx = -1;
|
|
5259
5331
|
while ((m = pat.exec(content)) !== null) {
|
|
5332
|
+
if (findings.length >= MAX_MATCHES) break;
|
|
5333
|
+
if (m.index === lastIdx && m[0].length === 0) {
|
|
5334
|
+
pat.lastIndex++;
|
|
5335
|
+
continue;
|
|
5336
|
+
}
|
|
5337
|
+
lastIdx = m.index;
|
|
5260
5338
|
if (isCommentLine(content, m.index)) continue;
|
|
5261
5339
|
const surrounding = content.substring(Math.max(0, m.index - 200), m.index + 200);
|
|
5262
5340
|
if (/path\.normalize|sanitize|\.replace\(\s*['"]\.\.|\.startsWith\s*\(|realpath/i.test(surrounding)) continue;
|
|
5263
|
-
const lineNum = content
|
|
5341
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5264
5342
|
findings.push({
|
|
5265
5343
|
rule: "VC117",
|
|
5266
5344
|
title: pathTraversal.title,
|
|
@@ -5291,7 +5369,7 @@ var piiInLogs = {
|
|
|
5291
5369
|
while ((m = logPattern.exec(content)) !== null) {
|
|
5292
5370
|
if (isCommentLine(content, m.index)) continue;
|
|
5293
5371
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
5294
|
-
const lineNum = content
|
|
5372
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5295
5373
|
findings.push({
|
|
5296
5374
|
rule: "VC118",
|
|
5297
5375
|
title: piiInLogs.title,
|
|
@@ -5329,7 +5407,7 @@ var hardcodedOAuthSecret = {
|
|
|
5329
5407
|
while ((m = pat.exec(content)) !== null) {
|
|
5330
5408
|
if (isCommentLine(content, m.index)) continue;
|
|
5331
5409
|
if (/process\.env|os\.environ|ENV\[|getenv|\$\{|<your|CHANGE_ME|xxx|placeholder/i.test(m[0])) continue;
|
|
5332
|
-
const lineNum = content
|
|
5410
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5333
5411
|
findings.push({
|
|
5334
5412
|
rule: "VC119",
|
|
5335
5413
|
title: hardcodedOAuthSecret.title,
|
|
@@ -5361,7 +5439,7 @@ var missingOAuthState = {
|
|
|
5361
5439
|
if (isCommentLine(content, m.index)) continue;
|
|
5362
5440
|
const surrounding = content.substring(m.index, Math.min(content.length, m.index + 500));
|
|
5363
5441
|
if (/state\s*[=:]/i.test(surrounding)) continue;
|
|
5364
|
-
const lineNum = content
|
|
5442
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5365
5443
|
findings.push({
|
|
5366
5444
|
rule: "VC120",
|
|
5367
5445
|
title: missingOAuthState.title,
|
|
@@ -5389,7 +5467,7 @@ var unpinnedGitHubAction = {
|
|
|
5389
5467
|
let m;
|
|
5390
5468
|
while ((m = usesPattern.exec(content)) !== null) {
|
|
5391
5469
|
if (isCommentLine(content, m.index)) continue;
|
|
5392
|
-
const lineNum = content
|
|
5470
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5393
5471
|
findings.push({
|
|
5394
5472
|
rule: "VC121",
|
|
5395
5473
|
title: unpinnedGitHubAction.title,
|
|
@@ -5423,7 +5501,7 @@ var deprecatedTLS = {
|
|
|
5423
5501
|
let m;
|
|
5424
5502
|
while ((m = pat.exec(content)) !== null) {
|
|
5425
5503
|
if (isCommentLine(content, m.index)) continue;
|
|
5426
|
-
const lineNum = content
|
|
5504
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5427
5505
|
findings.push({
|
|
5428
5506
|
rule: "VC122",
|
|
5429
5507
|
title: deprecatedTLS.title,
|
|
@@ -5459,7 +5537,7 @@ var weakRSAKeySize = {
|
|
|
5459
5537
|
let m;
|
|
5460
5538
|
while ((m = pat.exec(content)) !== null) {
|
|
5461
5539
|
if (isCommentLine(content, m.index)) continue;
|
|
5462
|
-
const lineNum = content
|
|
5540
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5463
5541
|
findings.push({
|
|
5464
5542
|
rule: "VC123",
|
|
5465
5543
|
title: weakRSAKeySize.title,
|
|
@@ -5495,7 +5573,7 @@ var ecbModeEncryption = {
|
|
|
5495
5573
|
let m;
|
|
5496
5574
|
while ((m = pat.exec(content)) !== null) {
|
|
5497
5575
|
if (isCommentLine(content, m.index)) continue;
|
|
5498
|
-
const lineNum = content
|
|
5576
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5499
5577
|
findings.push({
|
|
5500
5578
|
rule: "VC124",
|
|
5501
5579
|
title: ecbModeEncryption.title,
|
|
@@ -5526,7 +5604,7 @@ var insecurePasswordReset = {
|
|
|
5526
5604
|
let m;
|
|
5527
5605
|
while ((m = weakTokens.exec(content)) !== null) {
|
|
5528
5606
|
if (isCommentLine(content, m.index)) continue;
|
|
5529
|
-
const lineNum = content
|
|
5607
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5530
5608
|
findings.push({
|
|
5531
5609
|
rule: "VC125",
|
|
5532
5610
|
title: insecurePasswordReset.title,
|
|
@@ -5543,7 +5621,7 @@ var insecurePasswordReset = {
|
|
|
5543
5621
|
if (isCommentLine(content, m.index)) continue;
|
|
5544
5622
|
const surrounding = content.substring(Math.max(0, m.index - 500), m.index);
|
|
5545
5623
|
if (!/(?:reset|forgot).*password/i.test(surrounding)) continue;
|
|
5546
|
-
const lineNum = content
|
|
5624
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5547
5625
|
findings.push({
|
|
5548
5626
|
rule: "VC125",
|
|
5549
5627
|
title: insecurePasswordReset.title,
|
|
@@ -5604,7 +5682,7 @@ var insecureHTTPMethods = {
|
|
|
5604
5682
|
if (isCommentLine(content, m.index)) continue;
|
|
5605
5683
|
const handlerBlock = content.substring(m.index, Math.min(content.length, m.index + 500));
|
|
5606
5684
|
if (/auth|authenticate|authorize|requireAuth|isAuthenticated|protect|guard|middleware|session|jwt|verify|clerk|getAuth/i.test(handlerBlock)) continue;
|
|
5607
|
-
const lineNum = content
|
|
5685
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5608
5686
|
findings.push({
|
|
5609
5687
|
rule: "VC127",
|
|
5610
5688
|
title: insecureHTTPMethods.title,
|
|
@@ -5638,7 +5716,7 @@ var httpRequestSmuggling = {
|
|
|
5638
5716
|
let m;
|
|
5639
5717
|
while ((m = pat.exec(content)) !== null) {
|
|
5640
5718
|
if (isCommentLine(content, m.index)) continue;
|
|
5641
|
-
const lineNum = content
|
|
5719
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5642
5720
|
findings.push({
|
|
5643
5721
|
rule: "VC128",
|
|
5644
5722
|
title: httpRequestSmuggling.title,
|
|
@@ -5672,7 +5750,7 @@ var unencryptedPII = {
|
|
|
5672
5750
|
let m;
|
|
5673
5751
|
while ((m = pat.exec(content)) !== null) {
|
|
5674
5752
|
if (isCommentLine(content, m.index)) continue;
|
|
5675
|
-
const lineNum = content
|
|
5753
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5676
5754
|
findings.push({
|
|
5677
5755
|
rule: "VC129",
|
|
5678
5756
|
title: unencryptedPII.title,
|
|
@@ -5704,7 +5782,7 @@ var missingAuthRateLimit = {
|
|
|
5704
5782
|
if (isCommentLine(content, m.index)) continue;
|
|
5705
5783
|
const surrounding = content.substring(Math.max(0, m.index - 300), Math.min(content.length, m.index + 300));
|
|
5706
5784
|
if (/rateLimit|rateLimiter|throttle|slowDown|express-rate-limit|rate_limit|RateLimiter|limiter/i.test(surrounding)) continue;
|
|
5707
|
-
const lineNum = content
|
|
5785
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5708
5786
|
findings.push({
|
|
5709
5787
|
rule: "VC130",
|
|
5710
5788
|
title: missingAuthRateLimit.title,
|
|
@@ -5742,7 +5820,7 @@ var vulnerableDependencies = {
|
|
|
5742
5820
|
for (const [pat, message] of vulnerablePackages) {
|
|
5743
5821
|
let m;
|
|
5744
5822
|
while ((m = pat.exec(content)) !== null) {
|
|
5745
|
-
const lineNum = content
|
|
5823
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5746
5824
|
findings.push({
|
|
5747
5825
|
rule: "VC131",
|
|
5748
5826
|
title: vulnerableDependencies.title,
|
|
@@ -5774,7 +5852,7 @@ function secretRuleCheck(content, filePath, pattern, ruleId, title, severity, fi
|
|
|
5774
5852
|
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
5775
5853
|
const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
|
|
5776
5854
|
if (PLACEHOLDER_RE.test(lineText)) continue;
|
|
5777
|
-
const lineNum = content
|
|
5855
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5778
5856
|
findings.push({
|
|
5779
5857
|
rule: ruleId,
|
|
5780
5858
|
title,
|
|
@@ -5875,7 +5953,7 @@ var hardcodedGCPServiceAccount = {
|
|
|
5875
5953
|
const findings = [];
|
|
5876
5954
|
const m = content.match(/"type"\s*:\s*"service_account"/);
|
|
5877
5955
|
if (m && m.index !== void 0) {
|
|
5878
|
-
const lineNum = content
|
|
5956
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5879
5957
|
findings.push({
|
|
5880
5958
|
rule: "VC136",
|
|
5881
5959
|
title: this.title,
|
|
@@ -5981,7 +6059,7 @@ var hardcodedDatadogKey = {
|
|
|
5981
6059
|
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
5982
6060
|
const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
|
|
5983
6061
|
if (PLACEHOLDER_RE.test(lineText)) continue;
|
|
5984
|
-
const lineNum = content
|
|
6062
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
5985
6063
|
findings.push({
|
|
5986
6064
|
rule: "VC141",
|
|
5987
6065
|
title: this.title,
|
|
@@ -6015,7 +6093,7 @@ var hardcodedVercelToken = {
|
|
|
6015
6093
|
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
6016
6094
|
const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
|
|
6017
6095
|
if (PLACEHOLDER_RE.test(lineText)) continue;
|
|
6018
|
-
const lineNum = content
|
|
6096
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6019
6097
|
findings.push({
|
|
6020
6098
|
rule: "VC142",
|
|
6021
6099
|
title: this.title,
|
|
@@ -6049,7 +6127,7 @@ var hardcodedSupabaseServiceRole = {
|
|
|
6049
6127
|
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
6050
6128
|
const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
|
|
6051
6129
|
if (PLACEHOLDER_RE.test(lineText)) continue;
|
|
6052
|
-
const lineNum = content
|
|
6130
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6053
6131
|
findings.push({
|
|
6054
6132
|
rule: "VC143",
|
|
6055
6133
|
title: this.title,
|
|
@@ -6113,7 +6191,7 @@ function contextSecretRuleCheck(content, filePath, pattern, ruleId, title, sever
|
|
|
6113
6191
|
const lineStart = content.lastIndexOf("\n", m.index - 1) + 1;
|
|
6114
6192
|
const lineText = content.substring(lineStart, content.indexOf("\n", m.index));
|
|
6115
6193
|
if (PLACEHOLDER_RE.test(lineText)) continue;
|
|
6116
|
-
const lineNum = content
|
|
6194
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6117
6195
|
findings.push({
|
|
6118
6196
|
rule: ruleId,
|
|
6119
6197
|
title,
|
|
@@ -6593,7 +6671,7 @@ var ghaPullRequestTargetCheckout = {
|
|
|
6593
6671
|
const checkoutPattern = /uses\s*:\s*actions\/checkout@[^\n]*[\s\S]{0,400}?ref\s*:\s*\$\{\{\s*github\.event\.pull_request\.head\.(?:ref|sha)\s*\}\}/g;
|
|
6594
6672
|
let m;
|
|
6595
6673
|
while ((m = checkoutPattern.exec(content)) !== null) {
|
|
6596
|
-
const lineNum = content
|
|
6674
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6597
6675
|
findings.push({
|
|
6598
6676
|
rule: "VC184",
|
|
6599
6677
|
title: ghaPullRequestTargetCheckout.title,
|
|
@@ -6677,7 +6755,7 @@ var ghaThirdPartyActionWithSecrets = {
|
|
|
6677
6755
|
const pattern = /uses\s*:\s*(?!actions\/|github\/|aws-actions\/|azure\/|google-github-actions\/|hashicorp\/)([\w\-]+\/[\w\-./]+)@[^\n]*\n[\s\S]{0,800}?with\s*:[\s\S]{0,800}?\$\{\{\s*secrets\./g;
|
|
6678
6756
|
let m;
|
|
6679
6757
|
while ((m = pattern.exec(content)) !== null) {
|
|
6680
|
-
const lineNum = content
|
|
6758
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6681
6759
|
findings.push({
|
|
6682
6760
|
rule: "VC187",
|
|
6683
6761
|
title: ghaThirdPartyActionWithSecrets.title,
|
|
@@ -6842,7 +6920,7 @@ var pyRequestsVerifyFalse = {
|
|
|
6842
6920
|
let m;
|
|
6843
6921
|
while ((m = pattern.exec(content)) !== null) {
|
|
6844
6922
|
if (isCommentLine(content, m.index)) continue;
|
|
6845
|
-
const lineNum = content
|
|
6923
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6846
6924
|
findings.push({
|
|
6847
6925
|
rule: "VC191",
|
|
6848
6926
|
title: pyRequestsVerifyFalse.title,
|
|
@@ -6873,7 +6951,7 @@ var pyJinja2AutoescapeOff = {
|
|
|
6873
6951
|
let m;
|
|
6874
6952
|
while ((m = pattern.exec(content)) !== null) {
|
|
6875
6953
|
if (isCommentLine(content, m.index)) continue;
|
|
6876
|
-
const lineNum = content
|
|
6954
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6877
6955
|
findings.push({
|
|
6878
6956
|
rule: "VC192",
|
|
6879
6957
|
title: pyJinja2AutoescapeOff.title,
|
|
@@ -6902,7 +6980,7 @@ var pyTempfileMktemp = {
|
|
|
6902
6980
|
let m;
|
|
6903
6981
|
while ((m = pattern.exec(content)) !== null) {
|
|
6904
6982
|
if (isCommentLine(content, m.index)) continue;
|
|
6905
|
-
const lineNum = content
|
|
6983
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6906
6984
|
findings.push({
|
|
6907
6985
|
rule: "VC193",
|
|
6908
6986
|
title: pyTempfileMktemp.title,
|
|
@@ -6944,7 +7022,7 @@ var pyDjangoMarkSafe = {
|
|
|
6944
7022
|
let m;
|
|
6945
7023
|
while ((m = pattern.exec(content)) !== null) {
|
|
6946
7024
|
if (isCommentLine(content, m.index)) continue;
|
|
6947
|
-
const lineNum = content
|
|
7025
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6948
7026
|
if (seenLines.has(lineNum)) continue;
|
|
6949
7027
|
seenLines.add(lineNum);
|
|
6950
7028
|
findings.push({
|
|
@@ -6981,7 +7059,7 @@ var pyParamikoAutoAdd = {
|
|
|
6981
7059
|
let m;
|
|
6982
7060
|
while ((m = pattern.exec(content)) !== null) {
|
|
6983
7061
|
if (isCommentLine(content, m.index)) continue;
|
|
6984
|
-
const lineNum = content
|
|
7062
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
6985
7063
|
if (seenLines.has(lineNum)) continue;
|
|
6986
7064
|
seenLines.add(lineNum);
|
|
6987
7065
|
findings.push({
|
|
@@ -7014,7 +7092,7 @@ var pyDjangoAllowedHostsWildcard = {
|
|
|
7014
7092
|
let m;
|
|
7015
7093
|
while ((m = pattern.exec(content)) !== null) {
|
|
7016
7094
|
if (isCommentLine(content, m.index)) continue;
|
|
7017
|
-
const lineNum = content
|
|
7095
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7018
7096
|
findings.push({
|
|
7019
7097
|
rule: "VC196",
|
|
7020
7098
|
title: pyDjangoAllowedHostsWildcard.title,
|
|
@@ -7052,7 +7130,7 @@ var pyJWTDecodeWeakConfig = {
|
|
|
7052
7130
|
if (isCommentLine(content, m.index)) continue;
|
|
7053
7131
|
const args = m[1];
|
|
7054
7132
|
if (/\balgorithms\s*=/.test(args)) continue;
|
|
7055
|
-
const lineNum = content
|
|
7133
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7056
7134
|
if (seenLines.has(lineNum)) continue;
|
|
7057
7135
|
seenLines.add(lineNum);
|
|
7058
7136
|
findings.push({
|
|
@@ -7103,7 +7181,7 @@ var llmPromptInjection = {
|
|
|
7103
7181
|
let m;
|
|
7104
7182
|
while ((m = pattern.exec(content)) !== null) {
|
|
7105
7183
|
if (isCommentLine(content, m.index)) continue;
|
|
7106
|
-
const lineNum = content
|
|
7184
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7107
7185
|
if (seenLines.has(lineNum)) continue;
|
|
7108
7186
|
seenLines.add(lineNum);
|
|
7109
7187
|
findings.push({
|
|
@@ -7142,7 +7220,7 @@ var llmSystemPromptInjection = {
|
|
|
7142
7220
|
let m;
|
|
7143
7221
|
while ((m = pattern.exec(content)) !== null) {
|
|
7144
7222
|
if (isCommentLine(content, m.index)) continue;
|
|
7145
|
-
const lineNum = content
|
|
7223
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7146
7224
|
findings.push({
|
|
7147
7225
|
rule: "VC199",
|
|
7148
7226
|
title: llmSystemPromptInjection.title,
|
|
@@ -7184,7 +7262,7 @@ var llmOutputAsHTML = {
|
|
|
7184
7262
|
let m;
|
|
7185
7263
|
while ((m = pattern.exec(content)) !== null) {
|
|
7186
7264
|
if (isCommentLine(content, m.index)) continue;
|
|
7187
|
-
const lineNum = content
|
|
7265
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7188
7266
|
if (seenLines.has(lineNum)) continue;
|
|
7189
7267
|
seenLines.add(lineNum);
|
|
7190
7268
|
findings.push({
|
|
@@ -7221,7 +7299,7 @@ var vectorStoreQueryNoUserFilter = {
|
|
|
7221
7299
|
if (/\b(?:user[_-]?id|userId|tenant[_-]?id|tenantId|org[_-]?id|orgId|owner[_-]?id|ownerId|account[_-]?id|customer[_-]?id|workspace[_-]?id)\b/i.test(args)) continue;
|
|
7222
7300
|
if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org|owner|account|customer|workspace)/i.test(args)) continue;
|
|
7223
7301
|
if (/\bfilter\s*[:=][\s\S]*?(?:\buser|\btenant|\borg|\bowner|\baccount|\bcustomer|\bworkspace)/i.test(args)) continue;
|
|
7224
|
-
const lineNum = content
|
|
7302
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7225
7303
|
findings.push({
|
|
7226
7304
|
rule: "VC201",
|
|
7227
7305
|
title: vectorStoreQueryNoUserFilter.title,
|
|
@@ -7258,7 +7336,7 @@ var vectorStoreUpsertNoMetadata = {
|
|
|
7258
7336
|
if (/\bnamespace\s*[:=]\s*[`"']?[^,)`"']*(?:user|tenant|org)/i.test(args)) {
|
|
7259
7337
|
continue;
|
|
7260
7338
|
}
|
|
7261
|
-
const lineNum = content
|
|
7339
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7262
7340
|
findings.push({
|
|
7263
7341
|
rule: "VC202",
|
|
7264
7342
|
title: vectorStoreUpsertNoMetadata.title,
|
|
@@ -7312,7 +7390,7 @@ var llmCallNoMaxTokens = {
|
|
|
7312
7390
|
if (depth !== 0) continue;
|
|
7313
7391
|
const args = content.substring(openIdx + 1, i).replace(/\/\*[\s\S]*?\*\//g, "").replace(/\/\/[^\n]*/g, "").replace(/#[^\n]*/g, "");
|
|
7314
7392
|
if (TOKEN_KW_RE.test(args)) continue;
|
|
7315
|
-
const lineNum = content
|
|
7393
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7316
7394
|
findings.push({
|
|
7317
7395
|
rule: "VC203",
|
|
7318
7396
|
title: llmCallNoMaxTokens.title,
|
|
@@ -7352,7 +7430,7 @@ var graphqlNoDepthLimit = {
|
|
|
7352
7430
|
let m;
|
|
7353
7431
|
while ((m = anchorRe.exec(content)) !== null) {
|
|
7354
7432
|
if (isCommentLine(content, m.index)) continue;
|
|
7355
|
-
const lineNum = content
|
|
7433
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7356
7434
|
findings.push({
|
|
7357
7435
|
rule: "VC204",
|
|
7358
7436
|
title: graphqlNoDepthLimit.title,
|
|
@@ -7386,7 +7464,7 @@ var graphqlNoComplexityLimit = {
|
|
|
7386
7464
|
let m;
|
|
7387
7465
|
while ((m = anchorRe.exec(content)) !== null) {
|
|
7388
7466
|
if (isCommentLine(content, m.index)) continue;
|
|
7389
|
-
const lineNum = content
|
|
7467
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7390
7468
|
findings.push({
|
|
7391
7469
|
rule: "VC205",
|
|
7392
7470
|
title: graphqlNoComplexityLimit.title,
|
|
@@ -7416,7 +7494,7 @@ var graphqlCSRFDisabled = {
|
|
|
7416
7494
|
let m;
|
|
7417
7495
|
while ((m = pattern.exec(content)) !== null) {
|
|
7418
7496
|
if (isCommentLine(content, m.index)) continue;
|
|
7419
|
-
const lineNum = content
|
|
7497
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7420
7498
|
findings.push({
|
|
7421
7499
|
rule: "VC206",
|
|
7422
7500
|
title: graphqlCSRFDisabled.title,
|
|
@@ -7512,7 +7590,7 @@ var webhookMissingIdempotency = {
|
|
|
7512
7590
|
if (/idempoten|event\.id|evt\.id|delivery_?id|alreadyProcessed|processedEvents|ON\s+CONFLICT|INSERT\s+OR\s+IGNORE|\bseen\b|\bprocessed\b/i.test(content)) return [];
|
|
7513
7591
|
const m = content.match(/constructEvent|new\s+Webhook\s*\(|verifyHeader/i);
|
|
7514
7592
|
if (!m || m.index === void 0) return [];
|
|
7515
|
-
const line = content
|
|
7593
|
+
const line = lineNumberAt(content, m.index);
|
|
7516
7594
|
return filterSilenced([{
|
|
7517
7595
|
rule: "VC209",
|
|
7518
7596
|
title: webhookMissingIdempotency.title,
|
|
@@ -7539,7 +7617,7 @@ var middlewareMatcherExcludesApi = {
|
|
|
7539
7617
|
if (!/\(\?!\s*[^)]*\bapi\b/.test(content)) return [];
|
|
7540
7618
|
const m = content.match(/matcher\s*:/);
|
|
7541
7619
|
if (!m || m.index === void 0) return [];
|
|
7542
|
-
const line = content
|
|
7620
|
+
const line = lineNumberAt(content, m.index);
|
|
7543
7621
|
return filterSilenced([{
|
|
7544
7622
|
rule: "VC210",
|
|
7545
7623
|
title: middlewareMatcherExcludesApi.title,
|
|
@@ -7552,6 +7630,113 @@ var middlewareMatcherExcludesApi = {
|
|
|
7552
7630
|
}], content, "VC210");
|
|
7553
7631
|
}
|
|
7554
7632
|
};
|
|
7633
|
+
function passesLuhn(digits) {
|
|
7634
|
+
let sum = 0;
|
|
7635
|
+
let alt = false;
|
|
7636
|
+
for (let i = digits.length - 1; i >= 0; i--) {
|
|
7637
|
+
let d = digits.charCodeAt(i) - 48;
|
|
7638
|
+
if (d < 0 || d > 9) return false;
|
|
7639
|
+
if (alt) {
|
|
7640
|
+
d *= 2;
|
|
7641
|
+
if (d > 9) d -= 9;
|
|
7642
|
+
}
|
|
7643
|
+
sum += d;
|
|
7644
|
+
alt = !alt;
|
|
7645
|
+
}
|
|
7646
|
+
return sum % 10 === 0;
|
|
7647
|
+
}
|
|
7648
|
+
function looksLikeIssuerCard(d) {
|
|
7649
|
+
if (/^4/.test(d) && (d.length === 13 || d.length === 16 || d.length === 19)) return true;
|
|
7650
|
+
if (/^5[1-5]/.test(d) && d.length === 16) return true;
|
|
7651
|
+
if (/^2(2[2-9]|[3-6]\d|7[01]|720)/.test(d) && d.length === 16) return true;
|
|
7652
|
+
if (/^3[47]/.test(d) && d.length === 15) return true;
|
|
7653
|
+
if (/^(6011|65|64[4-9])/.test(d) && d.length === 16) return true;
|
|
7654
|
+
return false;
|
|
7655
|
+
}
|
|
7656
|
+
var TEST_CARD_NUMBERS = /* @__PURE__ */ new Set([
|
|
7657
|
+
"4242424242424242",
|
|
7658
|
+
"4111111111111111",
|
|
7659
|
+
"4012888888881881",
|
|
7660
|
+
"4000056655665556",
|
|
7661
|
+
"4000000000000002",
|
|
7662
|
+
"4000000000009995",
|
|
7663
|
+
"5555555555554444",
|
|
7664
|
+
"5200828282828210",
|
|
7665
|
+
"5105105105105100",
|
|
7666
|
+
"2223003122003222",
|
|
7667
|
+
"378282246310005",
|
|
7668
|
+
"371449635398431",
|
|
7669
|
+
"6011111111111117",
|
|
7670
|
+
"6011000990139424",
|
|
7671
|
+
"3056930009020004",
|
|
7672
|
+
"38520000023237"
|
|
7673
|
+
]);
|
|
7674
|
+
var hardcodedCreditCard = {
|
|
7675
|
+
id: "VC211",
|
|
7676
|
+
title: "Hardcoded credit card number",
|
|
7677
|
+
severity: "high",
|
|
7678
|
+
category: "Information Leakage",
|
|
7679
|
+
description: "A literal credit card number (Luhn-valid, with a real issuer prefix, and not a known network test card) appears in source. Cardholder data does not belong in code \u2014 it lands in git history, logs, and backups, and is a PCI-DSS violation. Use your processor's published test cards for tests, and tokenize real cards.",
|
|
7680
|
+
check(content, filePath) {
|
|
7681
|
+
if (isTestFile(filePath)) return [];
|
|
7682
|
+
const findings = [];
|
|
7683
|
+
const pattern = /(?<![\d.])\d[\d -]{11,21}\d(?![\d.])/g;
|
|
7684
|
+
let m;
|
|
7685
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
7686
|
+
const digits = m[0].replace(/[ -]/g, "");
|
|
7687
|
+
if (digits.length < 13 || digits.length > 19) continue;
|
|
7688
|
+
if (TEST_CARD_NUMBERS.has(digits)) continue;
|
|
7689
|
+
if (!looksLikeIssuerCard(digits)) continue;
|
|
7690
|
+
if (!passesLuhn(digits)) continue;
|
|
7691
|
+
if (isCommentLine(content, m.index)) continue;
|
|
7692
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7693
|
+
findings.push({
|
|
7694
|
+
rule: "VC211",
|
|
7695
|
+
title: hardcodedCreditCard.title,
|
|
7696
|
+
severity: "high",
|
|
7697
|
+
category: "Information Leakage",
|
|
7698
|
+
file: filePath,
|
|
7699
|
+
line: lineNum,
|
|
7700
|
+
snippet: getSnippet(content, lineNum),
|
|
7701
|
+
fix: "Never store a real card number in source. Tokenize through your payment processor (Stripe, Braintree) and keep only the returned token. For tests, use the processor's published test cards (e.g. Stripe's 4242 4242 4242 4242)."
|
|
7702
|
+
});
|
|
7703
|
+
}
|
|
7704
|
+
return findings;
|
|
7705
|
+
}
|
|
7706
|
+
};
|
|
7707
|
+
var hardcodedSSN = {
|
|
7708
|
+
id: "VC212",
|
|
7709
|
+
title: "Hardcoded US Social Security Number",
|
|
7710
|
+
severity: "high",
|
|
7711
|
+
category: "Information Leakage",
|
|
7712
|
+
description: "A value in SSN format (NNN-NN-NNNN) is assigned to an SSN-named field in source. Social Security Numbers are regulated PII; a literal one leaks into git history, logs, and backups. Store SSNs encrypted at rest and load test data from generated fakes, never a code literal.",
|
|
7713
|
+
check(content, filePath) {
|
|
7714
|
+
if (isTestFile(filePath)) return [];
|
|
7715
|
+
const findings = [];
|
|
7716
|
+
const pattern = /(?:ssn|social[_-]?sec(?:urity)?(?:[_-]?(?:no|num|number))?|taxpayer[_-]?id)\b["']?\s*[:=]\s*["']?(\d{3}-\d{2}-\d{4})/gi;
|
|
7717
|
+
let m;
|
|
7718
|
+
while ((m = pattern.exec(content)) !== null) {
|
|
7719
|
+
const ssn = m[1];
|
|
7720
|
+
const [area, group, serial] = ssn.split("-");
|
|
7721
|
+
if (area === "000" || area === "666" || Number(area) >= 900) continue;
|
|
7722
|
+
if (group === "00" || serial === "0000") continue;
|
|
7723
|
+
if (ssn === "123-45-6789") continue;
|
|
7724
|
+
if (isCommentLine(content, m.index)) continue;
|
|
7725
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7726
|
+
findings.push({
|
|
7727
|
+
rule: "VC212",
|
|
7728
|
+
title: hardcodedSSN.title,
|
|
7729
|
+
severity: "high",
|
|
7730
|
+
category: "Information Leakage",
|
|
7731
|
+
file: filePath,
|
|
7732
|
+
line: lineNum,
|
|
7733
|
+
snippet: getSnippet(content, lineNum),
|
|
7734
|
+
fix: "Don't hardcode SSNs. Generate fake values for tests (a faker/factory), and store real SSNs with application-level encryption or in a secrets vault \u2014 never as a code literal."
|
|
7735
|
+
});
|
|
7736
|
+
}
|
|
7737
|
+
return findings;
|
|
7738
|
+
}
|
|
7739
|
+
};
|
|
7555
7740
|
var secretInURLParam = {
|
|
7556
7741
|
id: "VC146",
|
|
7557
7742
|
title: "Secret Passed in URL Query Parameter",
|
|
@@ -7577,7 +7762,7 @@ var secretInURLParam = {
|
|
|
7577
7762
|
if (isCommentLine(content, m.index)) continue;
|
|
7578
7763
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7579
7764
|
if (isInlineSilenced(content, m.index, "VC146")) continue;
|
|
7580
|
-
const lineNum = content
|
|
7765
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7581
7766
|
findings.push({
|
|
7582
7767
|
rule: "VC146",
|
|
7583
7768
|
title: this.title,
|
|
@@ -7608,7 +7793,7 @@ var secretLoggedToConsole = {
|
|
|
7608
7793
|
while ((m = pattern.exec(content)) !== null) {
|
|
7609
7794
|
if (isCommentLine(content, m.index)) continue;
|
|
7610
7795
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7611
|
-
const lineNum = content
|
|
7796
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7612
7797
|
findings.push({
|
|
7613
7798
|
rule: "VC147",
|
|
7614
7799
|
title: this.title,
|
|
@@ -7638,7 +7823,7 @@ var secretInErrorResponse = {
|
|
|
7638
7823
|
while ((m = pattern.exec(content)) !== null) {
|
|
7639
7824
|
if (isCommentLine(content, m.index)) continue;
|
|
7640
7825
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7641
|
-
const lineNum = content
|
|
7826
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7642
7827
|
findings.push({
|
|
7643
7828
|
rule: "VC148",
|
|
7644
7829
|
title: this.title,
|
|
@@ -7677,7 +7862,7 @@ var secretInBundleConfig = {
|
|
|
7677
7862
|
while ((m = re.exec(content)) !== null) {
|
|
7678
7863
|
if (isCommentLine(content, m.index)) continue;
|
|
7679
7864
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7680
|
-
const lineNum = content
|
|
7865
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7681
7866
|
findings.push({
|
|
7682
7867
|
rule: "VC149",
|
|
7683
7868
|
title: this.title,
|
|
@@ -7715,7 +7900,7 @@ var secretInHTMLAttribute = {
|
|
|
7715
7900
|
while ((m = re.exec(content)) !== null) {
|
|
7716
7901
|
if (isCommentLine(content, m.index)) continue;
|
|
7717
7902
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7718
|
-
const lineNum = content
|
|
7903
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7719
7904
|
findings.push({
|
|
7720
7905
|
rule: "VC150",
|
|
7721
7906
|
title: this.title,
|
|
@@ -7742,9 +7927,9 @@ var secretInCLIArgument = {
|
|
|
7742
7927
|
if (!filePath.match(/\.(js|ts|jsx|tsx|py|rb)$/)) return [];
|
|
7743
7928
|
const findings = [];
|
|
7744
7929
|
const patterns = [
|
|
7745
|
-
/(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]
|
|
7746
|
-
/(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]
|
|
7747
|
-
/(?:subprocess|os\.system|os\.popen)\s*\([^)]
|
|
7930
|
+
/(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}\$\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi,
|
|
7931
|
+
/(?:exec|execSync|spawn|spawnSync|child_process)\s*\([^)]{0,500}["']\s*\+\s*(?:api[_-]?key|secret|token|password|credentials)/gi,
|
|
7932
|
+
/(?:subprocess|os\.system|os\.popen)\s*\([^)]{0,500}\{[^}]{0,200}(?:api[_-]?key|secret|token|password|credentials)/gi
|
|
7748
7933
|
];
|
|
7749
7934
|
for (const p of patterns) {
|
|
7750
7935
|
let m;
|
|
@@ -7752,7 +7937,7 @@ var secretInCLIArgument = {
|
|
|
7752
7937
|
while ((m = re.exec(content)) !== null) {
|
|
7753
7938
|
if (isCommentLine(content, m.index)) continue;
|
|
7754
7939
|
if (isInsideFixMessage(content, m.index)) continue;
|
|
7755
|
-
const lineNum = content
|
|
7940
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7756
7941
|
findings.push({
|
|
7757
7942
|
rule: "VC151",
|
|
7758
7943
|
title: this.title,
|
|
@@ -7798,7 +7983,7 @@ var webhookSignatureVerification = {
|
|
|
7798
7983
|
if (svc.verify.test(content)) continue;
|
|
7799
7984
|
const m = content.match(/export\s+(?:async\s+)?function\s+POST/);
|
|
7800
7985
|
if (!m || m.index === void 0) continue;
|
|
7801
|
-
const lineNum = content
|
|
7986
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7802
7987
|
findings.push({
|
|
7803
7988
|
rule: "VC152",
|
|
7804
7989
|
title: `${svc.name} Webhook Missing Signature Verification`,
|
|
@@ -7865,7 +8050,7 @@ var missingRequestValidation = {
|
|
|
7865
8050
|
if (/if\s*\(\s*!(?:body|parsed|data|payload)\.|throw.*(?:missing|invalid|required)/i.test(content)) return [];
|
|
7866
8051
|
const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|PUT|PATCH)/);
|
|
7867
8052
|
if (!m || m.index === void 0) return [];
|
|
7868
|
-
const lineNum = content
|
|
8053
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7869
8054
|
return [{
|
|
7870
8055
|
rule: "VC154",
|
|
7871
8056
|
title: this.title,
|
|
@@ -7892,7 +8077,7 @@ var missingAIRateLimit = {
|
|
|
7892
8077
|
if (/rateLimit|rateLimiter|throttle|checkRateLimit|limiter|slowDown|express-rate-limit/i.test(content)) return [];
|
|
7893
8078
|
const m = content.match(/export\s+(?:async\s+)?function\s+(?:POST|GET)/i) || content.match(/\.(post|get)\s*\(/i);
|
|
7894
8079
|
if (!m || m.index === void 0) return [];
|
|
7895
|
-
const lineNum = content
|
|
8080
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7896
8081
|
return [{
|
|
7897
8082
|
rule: "VC155",
|
|
7898
8083
|
title: this.title,
|
|
@@ -7921,7 +8106,7 @@ var missingPagination = {
|
|
|
7921
8106
|
if (/findUnique|findFirst|findById|\.findOne|WHERE.*id\s*=/i.test(content)) return [];
|
|
7922
8107
|
const m = content.match(/export\s+(?:async\s+)?function\s+GET/);
|
|
7923
8108
|
if (!m || m.index === void 0) return [];
|
|
7924
|
-
const lineNum = content
|
|
8109
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7925
8110
|
return [{
|
|
7926
8111
|
rule: "VC156",
|
|
7927
8112
|
title: this.title,
|
|
@@ -7982,7 +8167,7 @@ var insecureDirectObjectReference = {
|
|
|
7982
8167
|
if (/requireUser|requireUserForApi|getServerSession|auth\(\)/i.test(content)) return [];
|
|
7983
8168
|
const m = content.match(/params\.id|params\.(?:slug|uuid)|req\.params\./);
|
|
7984
8169
|
if (!m || m.index === void 0) return [];
|
|
7985
|
-
const lineNum = content
|
|
8170
|
+
const lineNum = lineNumberAt(content, m.index);
|
|
7986
8171
|
return [{
|
|
7987
8172
|
rule: "VC158",
|
|
7988
8173
|
title: this.title,
|
|
@@ -8280,7 +8465,10 @@ var allCustomRules = [
|
|
|
8280
8465
|
// VC204–VC206: GraphQL server hardening
|
|
8281
8466
|
graphqlNoDepthLimit,
|
|
8282
8467
|
graphqlNoComplexityLimit,
|
|
8283
|
-
graphqlCSRFDisabled
|
|
8468
|
+
graphqlCSRFDisabled,
|
|
8469
|
+
// VC211–VC212: hardcoded sensitive personal data (PII)
|
|
8470
|
+
hardcodedCreditCard,
|
|
8471
|
+
hardcodedSSN
|
|
8284
8472
|
];
|
|
8285
8473
|
function runCustomRules(content, filePath, disabledRules = [], tier = "free", extraRules = []) {
|
|
8286
8474
|
const findings = [];
|
|
@@ -8288,6 +8476,17 @@ function runCustomRules(content, filePath, disabledRules = [], tier = "free", ex
|
|
|
8288
8476
|
return findings;
|
|
8289
8477
|
}
|
|
8290
8478
|
if (/pro-rules-bundle|\.bundle\./i.test(filePath)) return findings;
|
|
8479
|
+
let maxLineLen = 0;
|
|
8480
|
+
{
|
|
8481
|
+
let lineStart = 0;
|
|
8482
|
+
for (let i = 0; i <= content.length; i++) {
|
|
8483
|
+
if (i === content.length || content.charCodeAt(i) === 10) {
|
|
8484
|
+
if (i - lineStart > maxLineLen) maxLineLen = i - lineStart;
|
|
8485
|
+
lineStart = i + 1;
|
|
8486
|
+
}
|
|
8487
|
+
}
|
|
8488
|
+
}
|
|
8489
|
+
if (maxLineLen > 5e4) return findings;
|
|
8291
8490
|
if (/onboarding|demo-data|example-vulnerable|code-sample/i.test(filePath)) return findings;
|
|
8292
8491
|
const ruleset = tier === "pro" && extraRules.length > 0 ? [...freeRules, ...extraRules] : freeRules;
|
|
8293
8492
|
for (const rule of ruleset) {
|
|
@@ -8697,6 +8896,7 @@ export {
|
|
|
8697
8896
|
hardcodedAnthropicKey,
|
|
8698
8897
|
hardcodedCloudflareToken,
|
|
8699
8898
|
hardcodedCohereKey,
|
|
8899
|
+
hardcodedCreditCard,
|
|
8700
8900
|
hardcodedDatadogKey,
|
|
8701
8901
|
hardcodedDiscordToken,
|
|
8702
8902
|
hardcodedEncryptionKey,
|
|
@@ -8726,6 +8926,7 @@ export {
|
|
|
8726
8926
|
hardcodedRailwayToken,
|
|
8727
8927
|
hardcodedReplicateKey,
|
|
8728
8928
|
hardcodedResendKey,
|
|
8929
|
+
hardcodedSSN,
|
|
8729
8930
|
hardcodedSecrets,
|
|
8730
8931
|
hardcodedSendGridKey,
|
|
8731
8932
|
hardcodedSentryAuthToken,
|