oxlint-plugin-react-doctor 0.5.3-dev.0b30023 → 0.5.3-dev.eacdcf2
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/README.md +2 -0
- package/dist/index.d.ts +2222 -80
- package/dist/index.js +1674 -189
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -267,19 +267,127 @@ const wrapCreateForReactJsxOnly = (create) => ((context) => {
|
|
|
267
267
|
return wrappedVisitors;
|
|
268
268
|
});
|
|
269
269
|
const defineRule = (rule) => {
|
|
270
|
+
if (!("create" in rule)) return {
|
|
271
|
+
...rule,
|
|
272
|
+
create: () => ({})
|
|
273
|
+
};
|
|
270
274
|
const tags = rule.tags;
|
|
271
|
-
|
|
272
|
-
if (typeof create !== "function") return rule;
|
|
273
|
-
let wrappedCreate = create;
|
|
275
|
+
let wrappedCreate = rule.create;
|
|
274
276
|
if (tags?.includes("test-noise") && !tags?.includes("migration-hint")) wrappedCreate = wrapCreateForTestNoise(wrappedCreate);
|
|
275
277
|
if (tags?.includes("react-jsx-only")) wrappedCreate = wrapCreateForReactJsxOnly(wrappedCreate);
|
|
276
|
-
if (wrappedCreate === create) return rule;
|
|
278
|
+
if (wrappedCreate === rule.create) return rule;
|
|
277
279
|
return {
|
|
278
280
|
...rule,
|
|
279
281
|
create: wrappedCreate
|
|
280
282
|
};
|
|
281
283
|
};
|
|
282
284
|
//#endregion
|
|
285
|
+
//#region src/plugin/rules/security-scan/utils/get-location-at-index.ts
|
|
286
|
+
const getLocationAtIndex = (content, matchIndex) => {
|
|
287
|
+
if (matchIndex < 0) return {
|
|
288
|
+
line: 1,
|
|
289
|
+
column: 1
|
|
290
|
+
};
|
|
291
|
+
const lines = content.slice(0, matchIndex).split(/\r?\n/);
|
|
292
|
+
return {
|
|
293
|
+
line: lines.length,
|
|
294
|
+
column: (lines[lines.length - 1]?.length ?? 0) + 1
|
|
295
|
+
};
|
|
296
|
+
};
|
|
297
|
+
//#endregion
|
|
298
|
+
//#region src/plugin/rules/security-scan/utils/get-match-location.ts
|
|
299
|
+
const getMatchLocation = (content, pattern) => getLocationAtIndex(content, content.search(pattern));
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/plugin/constants/security-scan.ts
|
|
302
|
+
const TEXT_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|json|jsonc|map|html?|mdx?|ya?ml|toml|sql|rules|env|txt|log|svg|xml|pem|key|crt|cert|pub|py|php)$/i;
|
|
303
|
+
const DOTENV_FILE_PATTERN = /(?:^|\/)\.env(?:\.|$)/;
|
|
304
|
+
const SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?)$/i;
|
|
305
|
+
const SCRIPT_SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|py|php)$/i;
|
|
306
|
+
const DATABASE_SOURCE_FILE_PATTERN = /\.(?:[cm]?[jt]sx?|py)$/i;
|
|
307
|
+
const SERVER_CONTEXT_PATTERN = /(?:^|\/)(?:api|backend|server|servers|middleware|route|routes|functions|lambdas|workers)(?:\/|$)|(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/i;
|
|
308
|
+
const TEST_CONTEXT_PATTERN = /(?:^|\/)(?:__fixtures__|__mocks__|__tests__|fixtures|mocks|test|tests|testdata|test-data|e2e|playwright)(?:\/|$)|\.(?:test|spec|e2e|e2e-spec|integration-test|fixture|fixtures|stories|story)\.[cm]?[jt]sx?$|(?:^|\/)(?:test_[^/]+|[^/]+_test|conftest)\.py$|\.env\.[^/]*(?:test|e2e)[^/]*$/i;
|
|
309
|
+
const BUILD_SCRIPT_CONTEXT_PATTERN = /(?:^|\/)scripts(?:\/|$)/i;
|
|
310
|
+
const DEMO_CONTEXT_PATTERN = /(?:^|\/)(?:examples?|tutorials?|demos?|samples?|playgrounds?)(?:\/|$)/i;
|
|
311
|
+
const DOCUMENTATION_CONTEXT_PATTERN = /(?:^|\/)(?:README|CHANGELOG|CONTRIBUTING|PUBLISHING|DOCS)\.mdx?$|\.mdx?$/i;
|
|
312
|
+
const GENERATED_SOURCE_CONTEXT_PATTERN = /(?:^|\/)(?:generated|__generated__|dist|build|coverage|out|storybook-static|vendor|vendors|third[-_]?party|libraries)(?:\/|$)|(?:^|\/)\.next\/|(?:^|\/)\.yarn\/|(?:^|\/)public\/(?:chunks?|assets?|build|dist|static)\/|(?:generated|\.gen)\.[cm]?[jt]sx?$|@\d+\.\d+\.\d+(?:[-.][\w.]+)?\.[cm]?js$|[.-]min\.[cm]?js$|\.asm\.js$|(?:^|\/)[\w-]+[.@-]\d+\.\d+\.\d+(?:[-.][\w.]+)?\//i;
|
|
313
|
+
const GENERATED_BUNDLE_FILE_PATTERN = /\.(iife|umd|global|min)\.js$/i;
|
|
314
|
+
const BROWSER_ARTIFACT_PATH_PATTERNS = [
|
|
315
|
+
/(?:^|\/)\.next\/static\//,
|
|
316
|
+
/(?:^|\/)\.output\/public\//,
|
|
317
|
+
/(?:^|\/)build\/static\//,
|
|
318
|
+
/(?:^|\/)dist\/assets\//,
|
|
319
|
+
/(?:^|\/)public\//,
|
|
320
|
+
/(?:^|\/)out\//,
|
|
321
|
+
/(?:^|\/)storybook-static\//
|
|
322
|
+
];
|
|
323
|
+
const AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN = /\b(?:exec|execSync|spawn|child_process|eval|new Function|vm\.run|readFile|writeFile|fs\.read|fs\.write|fetch|axios|http\.request|sandbox|runCode|executeCode)\b/;
|
|
324
|
+
//#endregion
|
|
325
|
+
//#region src/plugin/rules/security-scan/utils/is-browser-artifact-path.ts
|
|
326
|
+
const isServerOnlyBuildArtifactPath = (relativePath) => /(?:^|\/)(?:\.next\/server|\.output\/server)\//.test(relativePath);
|
|
327
|
+
const isBrowserArtifactPath = (relativePath, isGeneratedBundle) => {
|
|
328
|
+
if (isServerOnlyBuildArtifactPath(relativePath)) return false;
|
|
329
|
+
if (isGeneratedBundle) return true;
|
|
330
|
+
if (relativePath.endsWith(".map")) return true;
|
|
331
|
+
return BROWSER_ARTIFACT_PATH_PATTERNS.some((pattern) => pattern.test(relativePath));
|
|
332
|
+
};
|
|
333
|
+
//#endregion
|
|
334
|
+
//#region src/plugin/rules/security-scan/utils/is-config-or-ci-path.ts
|
|
335
|
+
const isConfigOrCiPath = (relativePath) => /(?:^|\/)(?:package\.json|Dockerfile|docker-compose\.ya?ml|\.github\/workflows\/[^/]+\.ya?ml|vercel\.json|next\.config\.[cm]?[jt]s|netlify\.toml)$/i.test(relativePath);
|
|
336
|
+
//#endregion
|
|
337
|
+
//#region src/plugin/rules/security-scan/utils/is-production-file-path.ts
|
|
338
|
+
const isProductionFilePath = (relativePath, sourceFilePattern) => {
|
|
339
|
+
if (!sourceFilePattern.test(relativePath)) return false;
|
|
340
|
+
if (TEST_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
341
|
+
if (BUILD_SCRIPT_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
342
|
+
if (DOCUMENTATION_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
343
|
+
if (GENERATED_SOURCE_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
344
|
+
return true;
|
|
345
|
+
};
|
|
346
|
+
//#endregion
|
|
347
|
+
//#region src/plugin/rules/security-scan/utils/is-production-source-path.ts
|
|
348
|
+
const isProductionSourcePath = (relativePath) => {
|
|
349
|
+
return isProductionFilePath(relativePath, SOURCE_FILE_PATTERN);
|
|
350
|
+
};
|
|
351
|
+
//#endregion
|
|
352
|
+
//#region src/plugin/rules/security-scan/active-static-asset.ts
|
|
353
|
+
const SVG_ACTIVE_PATTERN = /<script\b|on(?:load|error|click|mouseover)\s*=/i;
|
|
354
|
+
const DANGEROUS_ALLOW_SVG_PATTERN = /dangerouslyAllowSVG\s*:\s*true/i;
|
|
355
|
+
const EXECUTABLE_SVG_EMBED_PATTERN = /<(?:object|embed|iframe)\b[^>]+(?:data|src)=["'][^"']+\.svg(?:\?[^"']*)?["']/i;
|
|
356
|
+
const activeStaticAsset = defineRule({
|
|
357
|
+
id: "active-static-asset",
|
|
358
|
+
title: "Executable SVG exposure",
|
|
359
|
+
severity: "warn",
|
|
360
|
+
recommendation: "Prefer `<img>` for SVG images; if SVG must be served directly, use attachment disposition and a CSP that blocks scripts and objects.",
|
|
361
|
+
scan: (file) => {
|
|
362
|
+
const findings = [];
|
|
363
|
+
if (file.relativePath.endsWith(".svg") && isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle)) {
|
|
364
|
+
if (SVG_ACTIVE_PATTERN.test(file.content)) {
|
|
365
|
+
const location = getMatchLocation(file.content, SVG_ACTIVE_PATTERN);
|
|
366
|
+
findings.push({
|
|
367
|
+
message: "A browser-reachable SVG contains script or event-handler code.",
|
|
368
|
+
line: location.line,
|
|
369
|
+
column: location.column,
|
|
370
|
+
severity: "error",
|
|
371
|
+
title: "Active SVG in public assets",
|
|
372
|
+
help: "Serve untrusted SVG as downloads, sanitize it, or isolate it on a cookieless asset origin with a restrictive CSP."
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
return findings;
|
|
376
|
+
}
|
|
377
|
+
if (!isProductionSourcePath(file.relativePath) && !isConfigOrCiPath(file.relativePath)) return findings;
|
|
378
|
+
const pattern = [DANGEROUS_ALLOW_SVG_PATTERN, EXECUTABLE_SVG_EMBED_PATTERN].find((candidate) => candidate.test(file.content));
|
|
379
|
+
if (pattern !== void 0) {
|
|
380
|
+
const location = getMatchLocation(file.content, pattern);
|
|
381
|
+
findings.push({
|
|
382
|
+
message: "The app enables or embeds SVG in an executable browser context.",
|
|
383
|
+
line: location.line,
|
|
384
|
+
column: location.column
|
|
385
|
+
});
|
|
386
|
+
}
|
|
387
|
+
return findings;
|
|
388
|
+
}
|
|
389
|
+
});
|
|
390
|
+
//#endregion
|
|
283
391
|
//#region src/plugin/constants/library.ts
|
|
284
392
|
const HEAVY_LIBRARIES = new Set([
|
|
285
393
|
"@monaco-editor/react",
|
|
@@ -772,6 +880,94 @@ const advancedEventHandlerRefs = defineRule({
|
|
|
772
880
|
} })
|
|
773
881
|
});
|
|
774
882
|
//#endregion
|
|
883
|
+
//#region src/plugin/rules/security-scan/utils/strip-comments-preserving-positions.ts
|
|
884
|
+
const stripCommentsPreservingPositions = (content) => {
|
|
885
|
+
const characters = content.split("");
|
|
886
|
+
let stringDelimiter = null;
|
|
887
|
+
let index = 0;
|
|
888
|
+
while (index < content.length) {
|
|
889
|
+
const character = content[index];
|
|
890
|
+
const nextCharacter = content[index + 1];
|
|
891
|
+
if (stringDelimiter !== null) {
|
|
892
|
+
if (character === "\\") {
|
|
893
|
+
index += 2;
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
if (character === stringDelimiter) stringDelimiter = null;
|
|
897
|
+
index += 1;
|
|
898
|
+
continue;
|
|
899
|
+
}
|
|
900
|
+
if (character === "\"" || character === "'" || character === "`") {
|
|
901
|
+
stringDelimiter = character;
|
|
902
|
+
index += 1;
|
|
903
|
+
continue;
|
|
904
|
+
}
|
|
905
|
+
if (character === "/" && nextCharacter === "/") {
|
|
906
|
+
while (index < content.length && content[index] !== "\n") {
|
|
907
|
+
characters[index] = " ";
|
|
908
|
+
index += 1;
|
|
909
|
+
}
|
|
910
|
+
continue;
|
|
911
|
+
}
|
|
912
|
+
if (character === "/" && nextCharacter === "*") {
|
|
913
|
+
while (index < content.length) {
|
|
914
|
+
if (content[index] === "*" && content[index + 1] === "/") {
|
|
915
|
+
characters[index] = " ";
|
|
916
|
+
characters[index + 1] = " ";
|
|
917
|
+
index += 2;
|
|
918
|
+
break;
|
|
919
|
+
}
|
|
920
|
+
if (content[index] !== "\n") characters[index] = " ";
|
|
921
|
+
index += 1;
|
|
922
|
+
}
|
|
923
|
+
continue;
|
|
924
|
+
}
|
|
925
|
+
index += 1;
|
|
926
|
+
}
|
|
927
|
+
return characters.join("");
|
|
928
|
+
};
|
|
929
|
+
//#endregion
|
|
930
|
+
//#region src/plugin/rules/security-scan/utils/scan-by-pattern.ts
|
|
931
|
+
const strippedContentCache = /* @__PURE__ */ new WeakMap();
|
|
932
|
+
const getScannableContent = (file) => {
|
|
933
|
+
if (!SOURCE_FILE_PATTERN.test(file.relativePath)) return file.content;
|
|
934
|
+
const cachedContent = strippedContentCache.get(file);
|
|
935
|
+
if (cachedContent !== void 0) return cachedContent;
|
|
936
|
+
const strippedContent = stripCommentsPreservingPositions(file.content);
|
|
937
|
+
strippedContentCache.set(file, strippedContent);
|
|
938
|
+
return strippedContent;
|
|
939
|
+
};
|
|
940
|
+
const scanByPattern = ({ shouldScan, pattern, requireAll, suppressWhen, message }) => (file) => {
|
|
941
|
+
if (!shouldScan(file)) return [];
|
|
942
|
+
const content = getScannableContent(file);
|
|
943
|
+
if (requireAll !== void 0 && !requireAll.every((gate) => gate.test(content))) return [];
|
|
944
|
+
const matchedPattern = (pattern instanceof RegExp ? [pattern] : pattern).find((candidate) => candidate.test(content));
|
|
945
|
+
if (matchedPattern === void 0) return [];
|
|
946
|
+
if (suppressWhen !== void 0 && suppressWhen.test(content)) return [];
|
|
947
|
+
const { line, column } = getMatchLocation(content, matchedPattern);
|
|
948
|
+
return [{
|
|
949
|
+
message,
|
|
950
|
+
line,
|
|
951
|
+
column
|
|
952
|
+
}];
|
|
953
|
+
};
|
|
954
|
+
//#endregion
|
|
955
|
+
//#region src/plugin/rules/security-scan/agent-tool-capability-risk.ts
|
|
956
|
+
const AGENT_TOOL_DEFINITION_PATTERN = /\b(?:tool\s*\(\s*\{|createTool\s*\(|defineTool\s*\(|new\s+(?:DynamicTool|StructuredTool)\s*\()/;
|
|
957
|
+
const AGENT_TOOL_CONTEXT_PATH_PATTERN = /(?:^|\/)(?:agents?|tools?|mcp)(?:\/|$)|(?:agent|tool|mcp)[^/]*\.[cm]?[jt]sx?$/i;
|
|
958
|
+
const agentToolCapabilityRisk = defineRule({
|
|
959
|
+
id: "agent-tool-capability-risk",
|
|
960
|
+
title: "Agent tool exposes dangerous capability",
|
|
961
|
+
severity: "warn",
|
|
962
|
+
recommendation: "Treat tool inputs as prompt-injection controlled. Validate arguments, scope permissions per call, and avoid exposing shell/file/network primitives directly to agents.",
|
|
963
|
+
scan: scanByPattern({
|
|
964
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath) && AGENT_TOOL_CONTEXT_PATH_PATTERN.test(file.relativePath),
|
|
965
|
+
pattern: AGENT_TOOL_DEFINITION_PATTERN,
|
|
966
|
+
requireAll: [AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
967
|
+
message: "An agent-callable tool appears to expose network, filesystem, shell, or code-execution capability."
|
|
968
|
+
})
|
|
969
|
+
});
|
|
970
|
+
//#endregion
|
|
775
971
|
//#region src/plugin/utils/get-jsx-prop-string-value.ts
|
|
776
972
|
const getJsxPropStringValue = (attribute) => {
|
|
777
973
|
const value = attribute.value;
|
|
@@ -2867,6 +3063,260 @@ const ariaUnsupportedElements = defineRule({
|
|
|
2867
3063
|
}
|
|
2868
3064
|
} })
|
|
2869
3065
|
});
|
|
3066
|
+
const artifactBaasAuthoritySurface = defineRule({
|
|
3067
|
+
id: "artifact-baas-authority-surface",
|
|
3068
|
+
title: "BaaS authority map shipped in browser artifact",
|
|
3069
|
+
severity: "warn",
|
|
3070
|
+
recommendation: "Client BaaS config is often public, but shipped collection names plus owner, role, tenant, or admin fields give attackers a precise authorization map. Verify rules/RLS enforce every boundary server-side.",
|
|
3071
|
+
scan: scanByPattern({
|
|
3072
|
+
shouldScan: (file) => isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle),
|
|
3073
|
+
pattern: /\b(?:collection\s*\(\s*["'](?:boosts|sessions|sessions_admin|users|orgs|candidateJobs|conversations|documents|profiles)|from\s*\(\s*["'](?:users|profiles|documents|organizations|memberships)|creatorID|creatorId|providerId|ghostOrg|ownerId|orgId|tenantId|workspaceId|role|roles|isAdmin|SuperAdmin)\b/i,
|
|
3074
|
+
requireAll: [/\b(?:initializeApp|firebase|firestore|getFirestore|createClient)\b[\s\S]{0,700}\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket|supabase|SUPABASE_URL)\b|\b(?:apiKey|authDomain|projectId|databaseURL|storageBucket)\b[\s\S]{0,700}\b(?:firebase|firestore|getFirestore|initializeApp)\b/i],
|
|
3075
|
+
message: "A browser artifact exposes Firebase/Supabase config together with sensitive collections or authorization fields."
|
|
3076
|
+
})
|
|
3077
|
+
});
|
|
3078
|
+
//#endregion
|
|
3079
|
+
//#region src/plugin/constants/security.ts
|
|
3080
|
+
const AUTH_FUNCTION_NAMES = new Set([
|
|
3081
|
+
"auth",
|
|
3082
|
+
"getSession",
|
|
3083
|
+
"getServerSession",
|
|
3084
|
+
"getUser",
|
|
3085
|
+
"requireAuth",
|
|
3086
|
+
"checkAuth",
|
|
3087
|
+
"verifyAuth",
|
|
3088
|
+
"authenticate",
|
|
3089
|
+
"currentUser",
|
|
3090
|
+
"getAuth",
|
|
3091
|
+
"validateSession"
|
|
3092
|
+
]);
|
|
3093
|
+
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
3094
|
+
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
3095
|
+
const SECRET_PATTERNS = [
|
|
3096
|
+
/^sk_live_/,
|
|
3097
|
+
/^sk_test_/,
|
|
3098
|
+
/^AKIA[0-9A-Z]{16}$/,
|
|
3099
|
+
/^ghp_[a-zA-Z0-9]{36}$/,
|
|
3100
|
+
/^gho_[a-zA-Z0-9]{36}$/,
|
|
3101
|
+
/^github_pat_/,
|
|
3102
|
+
/^glpat-/,
|
|
3103
|
+
/^xox[bporas]-/,
|
|
3104
|
+
/^sk-[a-zA-Z0-9]{32,}$/
|
|
3105
|
+
];
|
|
3106
|
+
const SECRET_VALUE_PATTERNS = [
|
|
3107
|
+
/\b(?:AKIA|ASIA)[0-9A-Z]{16}\b/,
|
|
3108
|
+
/\bAWS_SECRET_ACCESS_KEY\s*[:=]\s*["']?[A-Za-z0-9/+=]{35,}["']?/,
|
|
3109
|
+
/\bgithub_pat_[A-Za-z0-9_]{30,}\b/,
|
|
3110
|
+
/\bgh[pousr]_[A-Za-z0-9]{30,}\b/,
|
|
3111
|
+
/\bglpat-[A-Za-z0-9_-]{20,}\b/,
|
|
3112
|
+
/\bxox[baprs]-[A-Za-z0-9-]{20,}\b/,
|
|
3113
|
+
/\bsk_(?:live|test)_[A-Za-z0-9]{16,}\b/,
|
|
3114
|
+
/\brk_(?:live|test)_[A-Za-z0-9]{16,}\b/,
|
|
3115
|
+
/\bsk-[A-Za-z0-9_-]{32,}\b/,
|
|
3116
|
+
/\bsk-ant-api\d{2}-[A-Za-z0-9_-]{20,}\b/,
|
|
3117
|
+
/\blin_(?:api|oauth)_[A-Za-z0-9]{20,}\b/,
|
|
3118
|
+
/\bvercel_[A-Za-z0-9]{20,}\b/,
|
|
3119
|
+
/\bsntrys_[A-Za-z0-9_-]{20,}\b/,
|
|
3120
|
+
/\bkey-[a-f0-9]{32}\b/i,
|
|
3121
|
+
/\bnpm_[A-Za-z0-9]{30,}\b/,
|
|
3122
|
+
/\bSG\.[A-Za-z0-9_-]{20,}\.[A-Za-z0-9_-]{20,}\b/,
|
|
3123
|
+
/https:\/\/hooks\.slack\.com\/services\/T[A-Z0-9]+\/B[A-Z0-9]+\/[A-Za-z0-9]+/,
|
|
3124
|
+
/https:\/\/discord(?:app)?\.com\/api\/webhooks\/\d+\/[A-Za-z0-9_-]+/,
|
|
3125
|
+
/\bsb_secret_[A-Za-z0-9_]{20,}\b/,
|
|
3126
|
+
/\bservice_role\b/i,
|
|
3127
|
+
/"private_key"\s*:\s*"-----BEGIN PRIVATE KEY-----/,
|
|
3128
|
+
/-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----/,
|
|
3129
|
+
/\b(?:postgres|mysql|mongodb(?:\+srv)?|redis):\/\/[^:\s/@]+:(?!(?:pass(?:word)?|my[a-z]*pass(?:word)?|mysecretpassword|myusername|postgres|mysql|redis|root|admin|minioadmin|secret|example|changeme|change_me|test|guest|placeholder|default|user(?:name)?|x{3,}|\*{2,}|\$\{[^}]*\}|\$[A-Z_]+|<[^>]*>|%[\w.]+%|\{\{[^}]*\}\})@)[^@\s/]+@(?!(?:localhost|127\.0\.0\.1|0\.0\.0\.0|host\.docker\.internal)(?:[:/\s]|$))[^\s:/@]*\./i
|
|
3130
|
+
];
|
|
3131
|
+
const PUBLIC_ENV_SECRET_NAME_PATTERN = /\b(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_[A-Z0-9_]*(?:SECRET|TOKEN|PASSWORD|PRIVATE|DATABASE_URL|SERVICE_ROLE|AWS_ACCESS_KEY|AWS_SECRET)[A-Z0-9_]*\b/i;
|
|
3132
|
+
const FULL_ENV_LEAK_CONTEXT_PATTERN = /\b(?:process\.env|import\.meta\.env|window\.__[A-Z0-9_]*ENV[A-Z0-9_]*__|__[A-Z0-9_]*ENV[A-Z0-9_]*__)\b/;
|
|
3133
|
+
const FULL_ENV_LEAK_SECRET_NAME_PATTERN = /\b(?:DATABASE_URL|AWS_SECRET_ACCESS_KEY|AWS_ACCESS_KEY_ID|MAILGUN_API_KEY|SALESFORCE_CLIENT_SECRET|OKTA_CLIENT_SECRET|SESSION_SECRET|COOKIE_SECRET|PRIVATE_KEY|SERVICE_ROLE)\b/;
|
|
3134
|
+
const TRUSTED_PUBLIC_SECRET_NAME_PATTERN = /(?:SENTRY_DSN|PUBLIC_KEY|PUBLISHABLE|ANON_KEY|POSTHOG_(?:PROJECT_)?TOKEN|POSTHOG_KEY|TLDRAW_LICENSE_KEY|CLERK_PUBLISHABLE_KEY|ALGOLIA_SEARCH_KEY|GC_API_KEY|GOOGLE_MAPS_API_KEY|MAPBOX_TOKEN|MIXPANEL_TOKEN|(?:NEXT_PUBLIC|VITE|REACT_APP|EXPO_PUBLIC)_(?:DISABLE|ENABLE|ALLOW|REQUIRE)_)/i;
|
|
3135
|
+
const PUBLIC_CLIENT_KEY_PATTERNS = [
|
|
3136
|
+
/^appl_/,
|
|
3137
|
+
/^goog_/,
|
|
3138
|
+
/^amzn_/,
|
|
3139
|
+
/^strp_/,
|
|
3140
|
+
/^pk_(?:live|test)_/,
|
|
3141
|
+
/^sb_publishable_/,
|
|
3142
|
+
/^phc_/,
|
|
3143
|
+
/^public-token-(?:live|test)-/,
|
|
3144
|
+
/^pk\.eyJ/
|
|
3145
|
+
];
|
|
3146
|
+
const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
3147
|
+
/^[\s._\-*\u2022xX]{8,}$/,
|
|
3148
|
+
/(?:\.{3,}|\u2026|[*\u2022]{3,})/,
|
|
3149
|
+
/(?:^|[_\-\s])(?:your|redacted|masked|placeholder|replace[_\-\s]?me|changeme)(?:$|[_\-\s])/i,
|
|
3150
|
+
/<[^>]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^>]*>/i,
|
|
3151
|
+
/\[[^\]]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^\]]*\]/i,
|
|
3152
|
+
/\{[^}]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^}]*\}/i
|
|
3153
|
+
];
|
|
3154
|
+
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
3155
|
+
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
3156
|
+
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
3157
|
+
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
3158
|
+
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
3159
|
+
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
3160
|
+
const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
|
|
3161
|
+
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
|
|
3162
|
+
const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
|
|
3163
|
+
const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
|
|
3164
|
+
const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
|
|
3165
|
+
const SECRET_SERVER_DIRECTORY_NAMES = new Set([
|
|
3166
|
+
"backend",
|
|
3167
|
+
"functions",
|
|
3168
|
+
"lambdas",
|
|
3169
|
+
"lambda",
|
|
3170
|
+
"middleware",
|
|
3171
|
+
"server",
|
|
3172
|
+
"servers"
|
|
3173
|
+
]);
|
|
3174
|
+
const SECRET_SERVER_SOURCE_ROOT_OWNER_NAMES = new Set([
|
|
3175
|
+
"api",
|
|
3176
|
+
"backend",
|
|
3177
|
+
"edge",
|
|
3178
|
+
"function",
|
|
3179
|
+
"functions",
|
|
3180
|
+
"lambda",
|
|
3181
|
+
"lambdas",
|
|
3182
|
+
"server",
|
|
3183
|
+
"servers",
|
|
3184
|
+
"worker",
|
|
3185
|
+
"workers"
|
|
3186
|
+
]);
|
|
3187
|
+
const SECRET_TEST_DIRECTORY_NAMES = new Set([
|
|
3188
|
+
"__fixtures__",
|
|
3189
|
+
"__mocks__",
|
|
3190
|
+
"__tests__",
|
|
3191
|
+
"fixtures",
|
|
3192
|
+
"mocks",
|
|
3193
|
+
"test",
|
|
3194
|
+
"tests"
|
|
3195
|
+
]);
|
|
3196
|
+
const SECRET_TOOLING_DIRECTORY_NAMES = new Set([
|
|
3197
|
+
"bin",
|
|
3198
|
+
"config",
|
|
3199
|
+
"configs",
|
|
3200
|
+
"script",
|
|
3201
|
+
"scripts",
|
|
3202
|
+
"tooling",
|
|
3203
|
+
"tools"
|
|
3204
|
+
]);
|
|
3205
|
+
const SECRET_CLIENT_SOURCE_DIRECTORY_NAMES = new Set([
|
|
3206
|
+
"components",
|
|
3207
|
+
"features",
|
|
3208
|
+
"hooks",
|
|
3209
|
+
"pages",
|
|
3210
|
+
"ui",
|
|
3211
|
+
"views",
|
|
3212
|
+
"widgets"
|
|
3213
|
+
]);
|
|
3214
|
+
const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
3215
|
+
"modal",
|
|
3216
|
+
"label",
|
|
3217
|
+
"text",
|
|
3218
|
+
"title",
|
|
3219
|
+
"name",
|
|
3220
|
+
"id",
|
|
3221
|
+
"url",
|
|
3222
|
+
"path",
|
|
3223
|
+
"route",
|
|
3224
|
+
"page",
|
|
3225
|
+
"param",
|
|
3226
|
+
"field",
|
|
3227
|
+
"column",
|
|
3228
|
+
"header",
|
|
3229
|
+
"placeholder",
|
|
3230
|
+
"prefix",
|
|
3231
|
+
"description",
|
|
3232
|
+
"type",
|
|
3233
|
+
"icon",
|
|
3234
|
+
"class",
|
|
3235
|
+
"style",
|
|
3236
|
+
"variant",
|
|
3237
|
+
"event",
|
|
3238
|
+
"action",
|
|
3239
|
+
"status",
|
|
3240
|
+
"state",
|
|
3241
|
+
"mode",
|
|
3242
|
+
"flag",
|
|
3243
|
+
"option",
|
|
3244
|
+
"config",
|
|
3245
|
+
"message",
|
|
3246
|
+
"error",
|
|
3247
|
+
"display",
|
|
3248
|
+
"view",
|
|
3249
|
+
"component",
|
|
3250
|
+
"element",
|
|
3251
|
+
"container",
|
|
3252
|
+
"wrapper",
|
|
3253
|
+
"button",
|
|
3254
|
+
"link",
|
|
3255
|
+
"input",
|
|
3256
|
+
"select",
|
|
3257
|
+
"dialog",
|
|
3258
|
+
"menu",
|
|
3259
|
+
"form",
|
|
3260
|
+
"step",
|
|
3261
|
+
"index",
|
|
3262
|
+
"count",
|
|
3263
|
+
"length",
|
|
3264
|
+
"role",
|
|
3265
|
+
"scope",
|
|
3266
|
+
"context",
|
|
3267
|
+
"provider",
|
|
3268
|
+
"ref",
|
|
3269
|
+
"handler",
|
|
3270
|
+
"query",
|
|
3271
|
+
"schema",
|
|
3272
|
+
"constant"
|
|
3273
|
+
]);
|
|
3274
|
+
//#endregion
|
|
3275
|
+
//#region src/plugin/rules/security-scan/utils/escape-reg-exp.ts
|
|
3276
|
+
const escapeRegExp = (value) => value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
3277
|
+
//#endregion
|
|
3278
|
+
//#region src/plugin/rules/security-scan/utils/find-suspicious-public-env-secret-name.ts
|
|
3279
|
+
const findSuspiciousPublicEnvSecretNamePattern = (content) => {
|
|
3280
|
+
for (const match of content.matchAll(new RegExp(PUBLIC_ENV_SECRET_NAME_PATTERN.source, "gi"))) {
|
|
3281
|
+
const value = match[0] ?? "";
|
|
3282
|
+
if (!TRUSTED_PUBLIC_SECRET_NAME_PATTERN.test(value)) return new RegExp(escapeRegExp(value));
|
|
3283
|
+
}
|
|
3284
|
+
};
|
|
3285
|
+
//#endregion
|
|
3286
|
+
//#region src/plugin/rules/security-scan/utils/has-full-env-leak-shape.ts
|
|
3287
|
+
const hasFullEnvLeakShape = (content) => FULL_ENV_LEAK_CONTEXT_PATTERN.test(content) && FULL_ENV_LEAK_SECRET_NAME_PATTERN.test(content);
|
|
3288
|
+
//#endregion
|
|
3289
|
+
//#region src/plugin/rules/security-scan/utils/scan-artifact-leak.ts
|
|
3290
|
+
const scanArtifactLeak = (file, findLeakPattern, message) => {
|
|
3291
|
+
if (DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
3292
|
+
if (!isBrowserArtifactPath(file.relativePath, file.isGeneratedBundle)) return [];
|
|
3293
|
+
const leakPattern = findLeakPattern(file.content);
|
|
3294
|
+
if (leakPattern === void 0) return [];
|
|
3295
|
+
const location = getMatchLocation(file.content, leakPattern);
|
|
3296
|
+
return [{
|
|
3297
|
+
message,
|
|
3298
|
+
line: location.line,
|
|
3299
|
+
column: location.column
|
|
3300
|
+
}];
|
|
3301
|
+
};
|
|
3302
|
+
//#endregion
|
|
3303
|
+
//#region src/plugin/rules/security-scan/artifact-env-leak.ts
|
|
3304
|
+
const artifactEnvLeak = defineRule({
|
|
3305
|
+
id: "artifact-env-leak",
|
|
3306
|
+
title: "Server env leaked to browser artifact",
|
|
3307
|
+
severity: "error",
|
|
3308
|
+
recommendation: "Treat public env prefixes as publication, not secrecy; keep secret env vars server-only and rebuild after rotating leaked keys.",
|
|
3309
|
+
scan: (file) => scanArtifactLeak(file, (content) => findSuspiciousPublicEnvSecretNamePattern(content) ?? (hasFullEnvLeakShape(content) ? FULL_ENV_LEAK_SECRET_NAME_PATTERN : void 0), "A browser artifact contains server-secret environment names or a full environment dump shape.")
|
|
3310
|
+
});
|
|
3311
|
+
//#endregion
|
|
3312
|
+
//#region src/plugin/rules/security-scan/artifact-secret-leak.ts
|
|
3313
|
+
const artifactSecretLeak = defineRule({
|
|
3314
|
+
id: "artifact-secret-leak",
|
|
3315
|
+
title: "Secret shipped in browser artifact",
|
|
3316
|
+
severity: "error",
|
|
3317
|
+
recommendation: "Remove the secret from client bundles/static assets, rotate it, and route privileged service calls through server-only code.",
|
|
3318
|
+
scan: (file) => scanArtifactLeak(file, (content) => SECRET_VALUE_PATTERNS.find((pattern) => pattern.test(content)), "A browser-delivered artifact contains a secret-looking credential value.")
|
|
3319
|
+
});
|
|
2870
3320
|
//#endregion
|
|
2871
3321
|
//#region src/plugin/constants/js.ts
|
|
2872
3322
|
const LOOP_TYPES = [
|
|
@@ -3835,6 +4285,17 @@ const autocompleteValid = defineRule({
|
|
|
3835
4285
|
} };
|
|
3836
4286
|
}
|
|
3837
4287
|
});
|
|
4288
|
+
const buildPipelineSecretBoundary = defineRule({
|
|
4289
|
+
id: "build-pipeline-secret-boundary",
|
|
4290
|
+
title: "Build pipeline runs code near secrets",
|
|
4291
|
+
severity: "warn",
|
|
4292
|
+
recommendation: "Run dependency installs with scripts disabled before exposing secrets, isolate untrusted build code, and move signing/deploy authority into a narrow privileged step.",
|
|
4293
|
+
scan: scanByPattern({
|
|
4294
|
+
shouldScan: (file) => isConfigOrCiPath(file.relativePath) && !file.relativePath.endsWith("package.json"),
|
|
4295
|
+
pattern: /(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b(?:(?!--ignore-scripts)[\s\S]){0,700}\bsecrets\.[A-Z0-9_]+|\bsecrets\.[A-Z0-9_]+(?:(?!--ignore-scripts)[\s\S]){0,700}(?:npm|pnpm|yarn|bun)\s+(?:install|ci)\b/i,
|
|
4296
|
+
message: "The build or install pipeline can execute package lifecycle code while CI secrets may be present."
|
|
4297
|
+
})
|
|
4298
|
+
});
|
|
3838
4299
|
//#endregion
|
|
3839
4300
|
//#region src/plugin/utils/is-create-element-call.ts
|
|
3840
4301
|
const memberChainContainsDocument = (memberExpression) => {
|
|
@@ -4135,6 +4596,19 @@ const clickEventsHaveKeyEvents = defineRule({
|
|
|
4135
4596
|
}
|
|
4136
4597
|
});
|
|
4137
4598
|
//#endregion
|
|
4599
|
+
//#region src/plugin/rules/security-scan/clickjacking-redirect-risk.ts
|
|
4600
|
+
const clickjackingRedirectRisk = defineRule({
|
|
4601
|
+
id: "clickjacking-redirect-risk",
|
|
4602
|
+
title: "Redirect or frame boundary risk",
|
|
4603
|
+
severity: "warn",
|
|
4604
|
+
recommendation: "Allowlist redirect origins/paths, set `frame-ancestors` for privileged pages, and avoid URL-prefilled privileged dialogs.",
|
|
4605
|
+
scan: scanByPattern({
|
|
4606
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath) || isConfigOrCiPath(file.relativePath),
|
|
4607
|
+
pattern: /\bredirect\s*\((?!\s*(?:await\s+)?[\w$]*(?:safe|valid|sanitiz|allowlist|whitelist)[\w$]*\s*\()[^)'"`\n]*\b(?:searchParams\.get|nextUrl\.searchParams|returnTo|callbackUrl|continue|next)\b|<iframe\b[\s\S]{0,700}\b(?:next=|continue=|redirect=|redirect_uri|userstoinvite|sharingaction|role=|\.\.)|frame-ancestors\s+(?:\*|'self'\s+\*)|X-Frame-Options["']?\s*:\s*["']?ALLOW/i,
|
|
4608
|
+
message: "Redirect or framing configuration may let attacker-controlled URLs chain into privileged UI or clickjacking."
|
|
4609
|
+
})
|
|
4610
|
+
});
|
|
4611
|
+
//#endregion
|
|
4138
4612
|
//#region src/plugin/rules/client/client-localstorage-no-version.ts
|
|
4139
4613
|
const VERSIONED_KEY_PATTERN = /(?:[._:-]v\d+|@\d+|\bv\d+\b)/i;
|
|
4140
4614
|
const STORAGE_OBJECTS = new Set(["localStorage", "sessionStorage"]);
|
|
@@ -4203,6 +4677,23 @@ const clientPassiveEventListeners = defineRule({
|
|
|
4203
4677
|
} })
|
|
4204
4678
|
});
|
|
4205
4679
|
//#endregion
|
|
4680
|
+
//#region src/plugin/rules/security-scan/utils/is-dev-tooling-path.ts
|
|
4681
|
+
const isDevToolingPath = (relativePath) => /(?:^|\/)(?:tools?|scripts?)\/|(?:^|\/)management\/commands\/|(?:^|\/)(?:build|make|gulpfile|gruntfile)\.[cm]?[jt]s$/i.test(relativePath);
|
|
4682
|
+
//#endregion
|
|
4683
|
+
//#region src/plugin/rules/security-scan/utils/is-production-script-source-path.ts
|
|
4684
|
+
const isProductionScriptSourcePath = (relativePath) => isProductionFilePath(relativePath, SCRIPT_SOURCE_FILE_PATTERN);
|
|
4685
|
+
const commandExecutionInputRisk = defineRule({
|
|
4686
|
+
id: "command-execution-input-risk",
|
|
4687
|
+
title: "Command execution uses caller-shaped input",
|
|
4688
|
+
severity: "error",
|
|
4689
|
+
recommendation: "Avoid shell execution for caller-controlled values. Use fixed commands, argument arrays, strict allowlists, and no shell interpolation.",
|
|
4690
|
+
scan: scanByPattern({
|
|
4691
|
+
shouldScan: (file) => isProductionScriptSourcePath(file.relativePath) && !isDevToolingPath(file.relativePath),
|
|
4692
|
+
pattern: /(?:(?<![.\w$])(?:exec(?:Sync)?|spawn(?:Sync)?|system|passthru|proc_open|shell_exec)|\b(?:os\.system|subprocess\.(?:run|Popen|call)|(?:child_process|childProcess|cp)\.(?:exec|spawn)\w*))\s*\([^)]{0,220}(?:req\.|request\.|params\.|query\.|body\.|searchParams|\$_(?:GET|POST|REQUEST)|shell\s*=\s*true|f['"`][^'"`]*\{)/i,
|
|
4693
|
+
message: "Command execution appears to include request, query, body, or shell-interpolated input."
|
|
4694
|
+
})
|
|
4695
|
+
});
|
|
4696
|
+
//#endregion
|
|
4206
4697
|
//#region src/plugin/utils/is-interactive-role.ts
|
|
4207
4698
|
const isInteractiveRole = (role) => INTERACTIVE_ROLES.has(role);
|
|
4208
4699
|
//#endregion
|
|
@@ -4380,6 +4871,130 @@ const controlHasAssociatedLabel = defineRule({
|
|
|
4380
4871
|
} };
|
|
4381
4872
|
}
|
|
4382
4873
|
});
|
|
4874
|
+
//#endregion
|
|
4875
|
+
//#region src/plugin/rules/security-scan/cors-cookie-trust-risk.ts
|
|
4876
|
+
const corsCookieTrustRisk = defineRule({
|
|
4877
|
+
id: "cors-cookie-trust-risk",
|
|
4878
|
+
title: "Broad cookie or credentialed CORS trust",
|
|
4879
|
+
severity: "warn",
|
|
4880
|
+
recommendation: "Keep auth cookies host-only and HttpOnly, avoid credentialed CORS for less-trusted docs/vendor origins, and isolate documentation domains from app sessions.",
|
|
4881
|
+
scan: scanByPattern({
|
|
4882
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath) || isConfigOrCiPath(file.relativePath),
|
|
4883
|
+
pattern: /Access-Control-Allow-Credentials["']?\s*[:,]\s*["']?true[\s\S]{0,700}Access-Control-Allow-Origin["']?\s*[:,]\s*["']?(?:\*|https:\/\/docs\.|https:\/\/.*mintlify)|\b(?:session|auth|token|jwt)[^=\n]{0,80}\bDomain=\./i,
|
|
4884
|
+
message: "Credentialed CORS or broad auth-cookie scope can make a docs/custom-domain XSS become account compromise."
|
|
4885
|
+
})
|
|
4886
|
+
});
|
|
4887
|
+
//#endregion
|
|
4888
|
+
//#region src/plugin/rules/security-scan/dangerous-html-sink.ts
|
|
4889
|
+
const DANGEROUS_HTML_PATTERN = /dangerouslySetInnerHTML|\.(?:inner|outer)HTML\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\(/;
|
|
4890
|
+
const HTML_VALUE_START_PATTERN = /(?:__html\s*:|\.(?:inner|outer)HTML\s*[+]?=(?!=)|\.insertAdjacentHTML\s*\(\s*[^,]*,|\bdocument\.write(?:ln)?\s*\(|\.(?:createContextualFragment|setHTMLUnsafe)\s*\()\s*([\s\S]*)/;
|
|
4891
|
+
const HTML_TAINT_PATTERN = /searchParams|query|params|request|req\.|response\.|result\.|data\.|await|fetch|props\.|children|content|html|body|text|message|\blocation\b|document\.cookie|\breferrer\b|\blocalStorage\b|\bsessionStorage\b|URLSearchParams|window\.name/i;
|
|
4892
|
+
const STRING_LITERAL_VALUE_PATTERN = /^(?:["'][^"']*["']|`[^`$]*`)\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
|
|
4893
|
+
const MODULE_CONSTANT_VALUE_PATTERN = /^[A-Z][A-Z0-9_]*\s*(?:\/\/[^\n]*)?\s*(?:[;,})\n]|$)/;
|
|
4894
|
+
const DOM_CONTENT_SOURCE_VALUE_PATTERN = /^[\w$]+(?:\??\.[\w$]+)*\??\.(?:inner|outer)HTML\b/;
|
|
4895
|
+
const SANITIZER_PATTERN = /\b(?:DOMPurify|sanitize\w*|purify|(?:escape|encode)[A-Za-z]*(?:Html|HTML|Entit\w*)|insane|xss)\b|(?<!un)safe|(?<!un)saniti[sz]/i;
|
|
4896
|
+
const SANITIZED_ASSIGNMENT_PATTERN = /=\s*[^\n;]*\b(?:DOMPurify\b|sanitize\w*\s*\(|purify\w*\s*\()/i;
|
|
4897
|
+
const DOM_CONTENT_ASSIGNMENT_PATTERN = /=\s*[\w$.?[\]]*\.(?:inner|outer)HTML\s*(?:[;,)\n]|$)/;
|
|
4898
|
+
const ENV_CONFIG_VALUE_PATTERN = /process\.env/;
|
|
4899
|
+
const I18N_VALUE_PATTERN = /\b(?:t|i18n|translate|formatMessage|intl)\s*[.(]/;
|
|
4900
|
+
const ESCAPING_SERIALIZER_CALL_PATTERN = /^(?:[\w$.]+\.)?(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast|highlight[A-Za-z]*)\s*\(/;
|
|
4901
|
+
const HIGHLIGHTER_LIBRARY_PATTERN = /\b(?:shiki|prism|hljs|highlightjs|getHighlighter|codeToHtml|codeToHast|refractor|lowlight|starry-night)\b|highlight\.js/i;
|
|
4902
|
+
const SERIALIZER_ASSIGNMENT_PATTERN = /=\s*[^\n;]*(?:\b(?:katex|shiki|hljs|prism|mermaid)\b|hast-util-to-html|renderHtmlFromRichText|(?:toHtml|render[A-Za-z]*(?:Html|HTML)|renderToString|renderToStaticMarkup|codeToHtml|codeToHast)\s*\()/i;
|
|
4903
|
+
const BARE_IDENTIFIER_VALUE_PATTERN = /^[\w$]+\s*(?:[;,})\n]|$)/;
|
|
4904
|
+
const MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN = /^[\w$]+(?:\.[\w$]+|\[[^\]]*\])+\s*(?:[;,})\n]|$)/;
|
|
4905
|
+
const STYLE_TAG_BEFORE_SINK_PATTERN = /<style\b[^<>]*$/;
|
|
4906
|
+
const STYLE_TAG_LOOKBEHIND_LINES = 5;
|
|
4907
|
+
const EMAIL_TEMPLATE_PATH_PATTERN = /(?:^|\/)emails?(?:\/|$)|email[-_.]templates?(?:\/|$)|RawHtml|[A-Za-z]*[Ee]mail[A-Za-z]*\.(?:t|j)sx?/i;
|
|
4908
|
+
const INNERHTML_TARGET_PATTERN = /(?:^|[^\w$.])([\w$]+(?:\.[\w$]+)*)\.(?:(?:inner|outer)HTML\s*[+]?=(?!=)|insertAdjacentHTML\s*\()/;
|
|
4909
|
+
const LIVE_DOM_ATTACH_PATTERN = /\b(?:appendChild|append|prepend|before|after|replaceWith|replaceChild|replaceChildren|insertBefore|insertAdjacentElement)\s*\(/;
|
|
4910
|
+
const VALUE_LOOKAHEAD_LINES = 4;
|
|
4911
|
+
const VALUE_EXPRESSION_MAX_CHARS = 300;
|
|
4912
|
+
const STATIC_TEMPLATE_LOOKAHEAD_LINES = 60;
|
|
4913
|
+
const STATIC_TEMPLATE_MAX_CHARS = 5e3;
|
|
4914
|
+
const getTemplateInterpolations = (valueTail) => {
|
|
4915
|
+
if (!valueTail.startsWith("`")) return null;
|
|
4916
|
+
const closingBacktickIndex = valueTail.indexOf("`", 1);
|
|
4917
|
+
if (closingBacktickIndex < 0 || closingBacktickIndex > STATIC_TEMPLATE_MAX_CHARS) return null;
|
|
4918
|
+
const interpolations = valueTail.slice(1, closingBacktickIndex).match(/\$\{[^}]*\}/g);
|
|
4919
|
+
return interpolations === null ? "" : interpolations.join(" ");
|
|
4920
|
+
};
|
|
4921
|
+
const isInertParseTarget = (target, fileContent) => {
|
|
4922
|
+
const escapedTarget = escapeRegExp(target);
|
|
4923
|
+
const escapedRoot = escapeRegExp(target.split(".")[0] ?? target);
|
|
4924
|
+
if (new RegExp(`\\b${escapedRoot}\\s*=\\s*[^\\n;]*(?:getElementById|querySelector|getElementsBy|\\.current\\b|document\\.(?:body|head|documentElement))`).test(fileContent)) return false;
|
|
4925
|
+
if (new RegExp(`${escapedTarget}\\s*=\\s*document\\.createElement\\(\\s*["'\`]template["'\`]`).test(fileContent)) return true;
|
|
4926
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\(\\s*["'\`](?:style|textarea)["'\`]`).test(fileContent)) return true;
|
|
4927
|
+
if (new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateHTMLDocument\\s*\\(`).test(fileContent)) return true;
|
|
4928
|
+
if (!new RegExp(`${escapedRoot}\\s*=\\s*[^\\n;]*\\bcreateElement\\s*\\(`).test(fileContent)) return false;
|
|
4929
|
+
const attachedToLiveTreePattern = new RegExp(`${LIVE_DOM_ATTACH_PATTERN.source}[^)]*\\b${escapedRoot}\\b`);
|
|
4930
|
+
const returnedAsNodePattern = new RegExp(`\\breturn\\b[^\\n]*\\b${escapedRoot}\\b(?!\\s*\\.\\s*(?:textContent|innerText|innerHTML|outerHTML))`);
|
|
4931
|
+
if (attachedToLiveTreePattern.test(fileContent) || returnedAsNodePattern.test(fileContent)) return false;
|
|
4932
|
+
return new RegExp(`\\b${escapedRoot}\\.(?:textContent|innerText|querySelector|querySelectorAll|children|childNodes)\\b`).test(fileContent);
|
|
4933
|
+
};
|
|
4934
|
+
const dangerousHtmlSink = defineRule({
|
|
4935
|
+
id: "dangerous-html-sink",
|
|
4936
|
+
title: "HTML injection sink with dynamic content",
|
|
4937
|
+
severity: "warn",
|
|
4938
|
+
recommendation: "Prefer rendering structured React nodes. If HTML is required, sanitize with a well-reviewed sanitizer and keep the trust boundary close to the sink.",
|
|
4939
|
+
scan: (file) => {
|
|
4940
|
+
if (file.isGeneratedBundle) return [];
|
|
4941
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
4942
|
+
if (EMAIL_TEMPLATE_PATH_PATTERN.test(file.relativePath)) return [];
|
|
4943
|
+
if (!DANGEROUS_HTML_PATTERN.test(file.content)) return [];
|
|
4944
|
+
const findings = [];
|
|
4945
|
+
const lines = file.content.split("\n");
|
|
4946
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
4947
|
+
const line = lines[lineIndex] ?? "";
|
|
4948
|
+
if (!DANGEROUS_HTML_PATTERN.test(line)) continue;
|
|
4949
|
+
const codeBeforeSinkOnLine = line.slice(0, line.search(DANGEROUS_HTML_PATTERN)).replace(/"[^"]*"|'[^']*'|`[^`]*`/g, "");
|
|
4950
|
+
if (/(?:^|[^:])\/\//.test(codeBeforeSinkOnLine) || /^\s*[/*]/.test(line)) continue;
|
|
4951
|
+
const sinkWindow = lines.slice(lineIndex, lineIndex + 1 + VALUE_LOOKAHEAD_LINES).join("\n");
|
|
4952
|
+
const valueMatch = HTML_VALUE_START_PATTERN.exec(sinkWindow);
|
|
4953
|
+
if (valueMatch === null) continue;
|
|
4954
|
+
const fullValueTail = (valueMatch[1] ?? "").trimStart();
|
|
4955
|
+
const valueTail = fullValueTail.slice(0, VALUE_EXPRESSION_MAX_CHARS);
|
|
4956
|
+
const terminatorIndex = valueTail.search(/[;}]/);
|
|
4957
|
+
const valueExpression = terminatorIndex >= 0 ? valueTail.slice(0, terminatorIndex + 1) : valueTail;
|
|
4958
|
+
if (STRING_LITERAL_VALUE_PATTERN.test(valueExpression)) continue;
|
|
4959
|
+
if (MODULE_CONSTANT_VALUE_PATTERN.test(valueExpression)) continue;
|
|
4960
|
+
if (DOM_CONTENT_SOURCE_VALUE_PATTERN.test(valueExpression) && !valueExpression.includes("+")) {
|
|
4961
|
+
const afterDomRead = valueExpression.replace(DOM_CONTENT_SOURCE_VALUE_PATTERN, "");
|
|
4962
|
+
if (!HTML_TAINT_PATTERN.test(afterDomRead)) continue;
|
|
4963
|
+
}
|
|
4964
|
+
const longValueTail = HTML_VALUE_START_PATTERN.exec(lines.slice(lineIndex, lineIndex + 1 + STATIC_TEMPLATE_LOOKAHEAD_LINES).join("\n"))?.[1]?.trimStart();
|
|
4965
|
+
const templateInterpolations = getTemplateInterpolations(longValueTail ?? fullValueTail);
|
|
4966
|
+
if (templateInterpolations === "") continue;
|
|
4967
|
+
const judgedExpression = templateInterpolations ?? valueExpression;
|
|
4968
|
+
if (SANITIZER_PATTERN.test(judgedExpression)) continue;
|
|
4969
|
+
if (ENV_CONFIG_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
4970
|
+
if (I18N_VALUE_PATTERN.test(judgedExpression)) continue;
|
|
4971
|
+
if (!HTML_TAINT_PATTERN.test(judgedExpression)) continue;
|
|
4972
|
+
if (ESCAPING_SERIALIZER_CALL_PATTERN.test(valueExpression)) continue;
|
|
4973
|
+
if (/highlighted/i.test(valueExpression)) continue;
|
|
4974
|
+
if (/highlight/i.test(valueExpression) && HIGHLIGHTER_LIBRARY_PATTERN.test(file.content)) continue;
|
|
4975
|
+
if (BARE_IDENTIFIER_VALUE_PATTERN.test(valueExpression) || MEMBER_OR_INDEX_ACCESS_VALUE_PATTERN.test(valueExpression)) {
|
|
4976
|
+
const valueIdentifier = valueExpression.match(/^[\w$]+/)?.[0];
|
|
4977
|
+
if (valueIdentifier !== void 0) {
|
|
4978
|
+
const escapedIdentifier = escapeRegExp(valueIdentifier);
|
|
4979
|
+
const fromSerializer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SERIALIZER_ASSIGNMENT_PATTERN.source}`, "i");
|
|
4980
|
+
const fromSanitizer = new RegExp(`\\b${escapedIdentifier}\\b\\s*${SANITIZED_ASSIGNMENT_PATTERN.source}`, "i");
|
|
4981
|
+
const fromDomContent = new RegExp(`\\b${escapedIdentifier}\\b\\s*${DOM_CONTENT_ASSIGNMENT_PATTERN.source}`);
|
|
4982
|
+
if (fromSerializer.test(file.content) || fromSanitizer.test(file.content) || fromDomContent.test(file.content)) continue;
|
|
4983
|
+
}
|
|
4984
|
+
}
|
|
4985
|
+
const sinkTargetMatch = INNERHTML_TARGET_PATTERN.exec(line);
|
|
4986
|
+
if (sinkTargetMatch?.[1] !== void 0 && isInertParseTarget(sinkTargetMatch[1], file.content)) continue;
|
|
4987
|
+
const textBeforeSink = lines.slice(Math.max(0, lineIndex - STYLE_TAG_LOOKBEHIND_LINES), lineIndex + 1).join("\n").slice(0, -line.length + line.search(DANGEROUS_HTML_PATTERN));
|
|
4988
|
+
if (STYLE_TAG_BEFORE_SINK_PATTERN.test(textBeforeSink)) continue;
|
|
4989
|
+
findings.push({
|
|
4990
|
+
message: "HTML is injected from a dynamic-looking source, which can become XSS if the value is user-controlled or unsanitized.",
|
|
4991
|
+
line: lineIndex + 1,
|
|
4992
|
+
column: line.search(/\S/) + 1
|
|
4993
|
+
});
|
|
4994
|
+
}
|
|
4995
|
+
return findings;
|
|
4996
|
+
}
|
|
4997
|
+
});
|
|
4383
4998
|
const LONG_TRANSITION_DURATION_THRESHOLD_MS = 1e3;
|
|
4384
4999
|
const VAGUE_BUTTON_LABELS = new Set([
|
|
4385
5000
|
"continue",
|
|
@@ -6774,6 +7389,56 @@ const expoNoNonInlinedEnv = defineRule({
|
|
|
6774
7389
|
}
|
|
6775
7390
|
});
|
|
6776
7391
|
//#endregion
|
|
7392
|
+
//#region src/plugin/rules/security-scan/utils/is-client-source-path.ts
|
|
7393
|
+
const isClientSourcePath = (relativePath) => {
|
|
7394
|
+
if (!isProductionSourcePath(relativePath)) return false;
|
|
7395
|
+
if (SERVER_CONTEXT_PATTERN.test(relativePath)) return false;
|
|
7396
|
+
return true;
|
|
7397
|
+
};
|
|
7398
|
+
//#endregion
|
|
7399
|
+
//#region src/plugin/rules/security-scan/firebase-client-owned-authz-field.ts
|
|
7400
|
+
const CLIENT_DATABASE_EVIDENCE_PATTERN = /firebase|firestore|supabase|\b(?:setDoc|addDoc)\s*\(/i;
|
|
7401
|
+
const firebaseClientOwnedAuthzField = defineRule({
|
|
7402
|
+
id: "firebase-client-owned-authz-field",
|
|
7403
|
+
title: "Client writes authorization field",
|
|
7404
|
+
severity: "error",
|
|
7405
|
+
recommendation: "Derive authority fields on the server or enforce them in Firebase/Supabase rules; never trust client-provided owner, org, or role values.",
|
|
7406
|
+
scan: scanByPattern({
|
|
7407
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath) && (CLIENT_DATABASE_EVIDENCE_PATTERN.test(file.content) || CLIENT_DATABASE_EVIDENCE_PATTERN.test(file.relativePath)),
|
|
7408
|
+
pattern: /(?:\b(?:setDoc|updateDoc|addDoc)\s*\(|(?:\b(?:firebase|firestore|getFirestore)\b|\bcollection\s*\(|\.collection\s*\()[\s\S]{0,500}\.(?:set|update|add)\s*\()[\s\S]{0,700}\b(?:ownerId|ownerID|creatorId|creatorID|providerId|providerID|orgId|orgID|tenantId|tenantID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin)\b/i,
|
|
7409
|
+
message: "Client code writes an ownership, tenant, or role field that should be server-owned and immutable."
|
|
7410
|
+
})
|
|
7411
|
+
});
|
|
7412
|
+
//#endregion
|
|
7413
|
+
//#region src/plugin/rules/security-scan/utils/is-firebase-rules-path.ts
|
|
7414
|
+
const isFirebaseRulesPath = (relativePath) => /(?:^|\/)(?:firestore\.rules|storage\.rules|database\.rules\.json)$/.test(relativePath);
|
|
7415
|
+
//#endregion
|
|
7416
|
+
//#region src/plugin/rules/security-scan/firebase-permissive-rules.ts
|
|
7417
|
+
const firebasePermissiveRules = defineRule({
|
|
7418
|
+
id: "firebase-permissive-rules",
|
|
7419
|
+
title: "Permissive Firebase security rule",
|
|
7420
|
+
severity: "error",
|
|
7421
|
+
recommendation: "Bind every read/write to `request.auth.uid`, immutable ownership, and tenant membership instead of treating sign-in as authorization.",
|
|
7422
|
+
scan: scanByPattern({
|
|
7423
|
+
shouldScan: (file) => isFirebaseRulesPath(file.relativePath),
|
|
7424
|
+
pattern: /allow\s+(?:read|write|create|update|delete|list|get|read,\s*write)\s*:\s*if\s+(?:true|request\.auth\s*!=\s*null)\s*;?/i,
|
|
7425
|
+
message: "Firebase rules grant broad access to everyone or to any signed-in user, which is the Chattr/Firewreck failure mode."
|
|
7426
|
+
})
|
|
7427
|
+
});
|
|
7428
|
+
//#endregion
|
|
7429
|
+
//#region src/plugin/rules/security-scan/firebase-query-filter-as-auth.ts
|
|
7430
|
+
const firebaseQueryFilterAsAuth = defineRule({
|
|
7431
|
+
id: "firebase-query-filter-as-auth",
|
|
7432
|
+
title: "Firestore query filter used as authorization",
|
|
7433
|
+
severity: "warn",
|
|
7434
|
+
recommendation: "Make sure Firestore rules compare the requested document against `request.auth.uid` and trusted membership data.",
|
|
7435
|
+
scan: scanByPattern({
|
|
7436
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
7437
|
+
pattern: /\.where\s*\(\s*["'](?:uid|userId|userID|ownerId|ownerID|orgId|orgID|tenantId|tenantID|role)["']\s*,\s*["']==["']/i,
|
|
7438
|
+
message: "Firestore query code filters by an auth-shaped field; filtering is not authorization unless rules enforce the same boundary."
|
|
7439
|
+
})
|
|
7440
|
+
});
|
|
7441
|
+
//#endregion
|
|
6777
7442
|
//#region src/plugin/utils/compile-glob.ts
|
|
6778
7443
|
/**
|
|
6779
7444
|
* Compiles a simple glob pattern (only `*` as a wildcard) into an
|
|
@@ -7037,6 +7702,37 @@ const forwardRefUsesRef = defineRule({
|
|
|
7037
7702
|
} })
|
|
7038
7703
|
});
|
|
7039
7704
|
//#endregion
|
|
7705
|
+
//#region src/plugin/rules/security-scan/git-provider-url-injection-risk.ts
|
|
7706
|
+
const GIT_PROVIDER_HOST_PATTERN = /api\.github\.com|github\.com|gitlab\.com|bitbucket\.org/gi;
|
|
7707
|
+
const TEMPLATE_INTERPOLATION_PATTERN = /\$\{([^}]*)\}/g;
|
|
7708
|
+
const EXTERNAL_INPUT_PATTERN = /\b(?:params|searchParams|query|req|request|input|payload)\s*[.[]|\buntrusted|\bdecodeURI\w*/;
|
|
7709
|
+
const ENCODED_INTERPOLATION_PATTERN = /encodeURIComponent\s*\(/;
|
|
7710
|
+
const INTERPOLATION_LOOKAHEAD_CHARS = 200;
|
|
7711
|
+
const gitProviderUrlInjectionRisk = defineRule({
|
|
7712
|
+
id: "git-provider-url-injection-risk",
|
|
7713
|
+
title: "Git provider URL built from interpolation",
|
|
7714
|
+
severity: "warn",
|
|
7715
|
+
recommendation: "Validate owner, repo, org, and branch identifiers against strict slugs and build URLs with URL/path encoders instead of raw interpolation.",
|
|
7716
|
+
scan: (file) => {
|
|
7717
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
7718
|
+
const findings = [];
|
|
7719
|
+
for (const hostMatch of file.content.matchAll(GIT_PROVIDER_HOST_PATTERN)) {
|
|
7720
|
+
const rawTail = file.content.slice(hostMatch.index, hostMatch.index + INTERPOLATION_LOOKAHEAD_CHARS);
|
|
7721
|
+
const templateEndIndex = rawTail.indexOf("`");
|
|
7722
|
+
const urlTail = templateEndIndex >= 0 ? rawTail.slice(0, templateEndIndex) : rawTail;
|
|
7723
|
+
if (!Array.from(urlTail.matchAll(TEMPLATE_INTERPOLATION_PATTERN)).some((interpolation) => EXTERNAL_INPUT_PATTERN.test(interpolation[1] ?? "") && !ENCODED_INTERPOLATION_PATTERN.test(interpolation[1] ?? ""))) continue;
|
|
7724
|
+
const location = getLocationAtIndex(file.content, hostMatch.index);
|
|
7725
|
+
findings.push({
|
|
7726
|
+
message: "GitHub/GitLab/Bitbucket URL construction interpolates path components that may be attacker-controlled.",
|
|
7727
|
+
line: location.line,
|
|
7728
|
+
column: location.column
|
|
7729
|
+
});
|
|
7730
|
+
break;
|
|
7731
|
+
}
|
|
7732
|
+
return findings;
|
|
7733
|
+
}
|
|
7734
|
+
});
|
|
7735
|
+
//#endregion
|
|
7040
7736
|
//#region src/plugin/rules/a11y/heading-has-content.ts
|
|
7041
7737
|
const MESSAGE$45 = "Blind users can't use this heading to navigate because screen readers skip it empty, so add text, `aria-label`, or `aria-labelledby`.";
|
|
7042
7738
|
const DEFAULT_HEADING_TAGS = [
|
|
@@ -7707,6 +8403,98 @@ const imgRedundantAlt = defineRule({
|
|
|
7707
8403
|
}
|
|
7708
8404
|
});
|
|
7709
8405
|
//#endregion
|
|
8406
|
+
//#region src/plugin/rules/security-scan/import-metadata-execution-risk.ts
|
|
8407
|
+
const PROCESS_MODULE_EVIDENCE_PATTERN = /child_process|childProcess|execa|subprocess|Deno\.run/;
|
|
8408
|
+
const EXECUTION_WITH_BARE_CALLS_PATTERN = /(?:\b(?:eval|new\s+Function|vm\.runIn\w*)|(?<![.\w$])(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)|\b(?:child_process|childProcess|cp)\.(?:exec|spawn)\w*)\s*\([^;]{0,200}(?<!["'])\b(?:exif|metadata|manifest|preset|plugin|upload|drop(?:ped|s)?\b|archive|zip|unzip|untar)(?!\w*["'])/;
|
|
8409
|
+
const EXECUTION_WITHOUT_BARE_CALLS_PATTERN = /(?:\b(?:eval|new\s+Function|vm\.runIn\w*)|\b(?:child_process|childProcess|cp)\.(?:exec|spawn)\w*)\s*\([^;]{0,200}(?<!["'])\b(?:exif|metadata|manifest|preset|plugin|upload|drop(?:ped|s)?\b|archive|zip|unzip|untar)(?!\w*["'])/;
|
|
8410
|
+
const EXECUTION_RISK_MESSAGE = "Imported metadata, uploads, or plugin manifests appear to reach code execution.";
|
|
8411
|
+
const scanWithBareCalls = scanByPattern({
|
|
8412
|
+
shouldScan: () => true,
|
|
8413
|
+
pattern: EXECUTION_WITH_BARE_CALLS_PATTERN,
|
|
8414
|
+
message: EXECUTION_RISK_MESSAGE
|
|
8415
|
+
});
|
|
8416
|
+
const scanWithoutBareCalls = scanByPattern({
|
|
8417
|
+
shouldScan: () => true,
|
|
8418
|
+
pattern: EXECUTION_WITHOUT_BARE_CALLS_PATTERN,
|
|
8419
|
+
message: EXECUTION_RISK_MESSAGE
|
|
8420
|
+
});
|
|
8421
|
+
const importMetadataExecutionRisk = defineRule({
|
|
8422
|
+
id: "import-metadata-execution-risk",
|
|
8423
|
+
title: "Imported metadata reaches code execution",
|
|
8424
|
+
severity: "error",
|
|
8425
|
+
recommendation: "Parse imported metadata as data with strict schemas; do not evaluate EXIF, manifests, presets, dropped files, or archives.",
|
|
8426
|
+
scan: (file) => {
|
|
8427
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
8428
|
+
return PROCESS_MODULE_EVIDENCE_PATTERN.test(file.content) ? scanWithBareCalls(file) : scanWithoutBareCalls(file);
|
|
8429
|
+
}
|
|
8430
|
+
});
|
|
8431
|
+
//#endregion
|
|
8432
|
+
//#region src/plugin/rules/security-scan/insecure-crypto-risk.ts
|
|
8433
|
+
const WEAK_HASH_PATTERN = /createHash\s*\(\s*["'](?:md5|sha1)["']|\bmd5\s*\(/gi;
|
|
8434
|
+
const SECURITY_CONTEXT_PATTERN = /\b(?:password|token|secret|signature|signing|auth|credential|session|cookie|csrf|api.?key)\b/i;
|
|
8435
|
+
const DEPRECATED_CIPHER_API_PATTERN = /(?<!cipher\.)\bcreate(?:Cipher|Decipher)\s*\(/;
|
|
8436
|
+
const WEAK_CIPHER_ALGORITHM_PATTERN = /\bcreate(?:Cipher|Decipher)iv\s*\(\s*["'](?:des|des3|des-?ede3?|rc4|rc2|bf|blowfish)\b/i;
|
|
8437
|
+
const WEAK_CIPHER_NAME_PATTERN = /\b(?:DES|RC4|Blowfish)\b/;
|
|
8438
|
+
const CIPHER_CONTEXT_PATTERN = /\b(?:cipher|decipher|encrypt|decrypt|crypto)\b/i;
|
|
8439
|
+
const UNSAFE_SIGNATURE_COMPARISON_PATTERN = /[A-Za-z_$][\w$.]*signature[\w$]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*(?:\([^)]*\))?|[A-Za-z_$][\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)\s*[A-Za-z_$][\w$.]*signature[\w$]*(?:\([^)]*\))?/i;
|
|
8440
|
+
const ENUM_MEMBER_COMPARAND_PATTERN = /(?:===?|!==?)\s*[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))|^[A-Z](?:[a-z]|[A-Z0-9_]*\b(?!\s*[.(]))[\w$.]*(?:\([^)]*\))?\s*(?:===?|!==?)/;
|
|
8441
|
+
const SIGNATURE_METADATA_IDENTIFIER_PATTERN = /signature(?:Method|Type|Status|Algorithm|Kind|Mode|Version)\b/i;
|
|
8442
|
+
const BOOLEAN_COMPARAND_PATTERN = /(?:===?|!==?)\s*(?:true|false|null|undefined)\b/;
|
|
8443
|
+
const CLIENT_COMPONENT_FILE_PATTERN = /\.[cm]?[jt]sx$/i;
|
|
8444
|
+
const TIMING_SAFE_COMPARISON_PATTERN = /timingSafeEqual|timing.?safe/i;
|
|
8445
|
+
const PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN = /gravatar|digest[-_ ]?auth|oauth[-_ ]?1|\b_id\b|\betag\b|checksum|cache[-_ ]?key|fingerprint/i;
|
|
8446
|
+
const SECURITY_RANDOM_CONTEXT_PATTERN = /token|secret|password|nonce|salt|csrf|credential|otp/i;
|
|
8447
|
+
const UI_NONCE_CONTEXT_PATTERN = /(?:focus|render|refresh|remount|redraw|animation|layout|cache|update)[-_]?nonce/i;
|
|
8448
|
+
const MATH_RANDOM_CALL_PATTERN = /Math\.random\s*\(/g;
|
|
8449
|
+
const SECURITY_CONTEXT_WINDOW_CHARS = 250;
|
|
8450
|
+
const findMatchIndexNearContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
8451
|
+
for (const callMatch of content.matchAll(pattern)) {
|
|
8452
|
+
const surroundingText = content.slice(Math.max(0, callMatch.index - SECURITY_CONTEXT_WINDOW_CHARS), callMatch.index + SECURITY_CONTEXT_WINDOW_CHARS);
|
|
8453
|
+
if (!contextPattern.test(surroundingText)) continue;
|
|
8454
|
+
if (excludeContextPattern?.test(surroundingText)) continue;
|
|
8455
|
+
return callMatch.index;
|
|
8456
|
+
}
|
|
8457
|
+
return -1;
|
|
8458
|
+
};
|
|
8459
|
+
const findRandomCallIndexWithSameLineContext = (content, pattern, contextPattern, excludeContextPattern) => {
|
|
8460
|
+
for (const callMatch of content.matchAll(pattern)) {
|
|
8461
|
+
const lineStartIndex = content.lastIndexOf("\n", callMatch.index) + 1;
|
|
8462
|
+
const lineEndCandidate = content.indexOf("\n", callMatch.index);
|
|
8463
|
+
const lineEndIndex = lineEndCandidate < 0 ? content.length : lineEndCandidate;
|
|
8464
|
+
const lineText = content.slice(lineStartIndex, lineEndIndex);
|
|
8465
|
+
if (excludeContextPattern.test(lineText)) continue;
|
|
8466
|
+
if (contextPattern.test(lineText)) return callMatch.index;
|
|
8467
|
+
}
|
|
8468
|
+
return -1;
|
|
8469
|
+
};
|
|
8470
|
+
const insecureCryptoRisk = defineRule({
|
|
8471
|
+
id: "insecure-crypto-risk",
|
|
8472
|
+
title: "Weak cryptography in security context",
|
|
8473
|
+
severity: "warn",
|
|
8474
|
+
recommendation: "Use modern primitives, `crypto.randomBytes` / Web Crypto randomness, and timing-safe comparisons for signatures, digests, tokens, and auth material.",
|
|
8475
|
+
scan: (file) => {
|
|
8476
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
8477
|
+
if (DEMO_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
8478
|
+
if (PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
8479
|
+
let matchIndex = findMatchIndexNearContext(file.content, WEAK_HASH_PATTERN, SECURITY_CONTEXT_PATTERN, PROTOCOL_MANDATED_HASH_CONTEXT_PATTERN);
|
|
8480
|
+
if (matchIndex < 0) matchIndex = file.content.search(WEAK_CIPHER_ALGORITHM_PATTERN);
|
|
8481
|
+
if (matchIndex < 0) matchIndex = file.content.search(DEPRECATED_CIPHER_API_PATTERN);
|
|
8482
|
+
if (matchIndex < 0 && CIPHER_CONTEXT_PATTERN.test(file.content)) matchIndex = file.content.search(WEAK_CIPHER_NAME_PATTERN);
|
|
8483
|
+
if (matchIndex < 0 && !TIMING_SAFE_COMPARISON_PATTERN.test(file.content) && !CLIENT_COMPONENT_FILE_PATTERN.test(file.relativePath)) {
|
|
8484
|
+
const comparisonMatch = UNSAFE_SIGNATURE_COMPARISON_PATTERN.exec(file.content);
|
|
8485
|
+
if (comparisonMatch !== null && !ENUM_MEMBER_COMPARAND_PATTERN.test(comparisonMatch[0]) && !SIGNATURE_METADATA_IDENTIFIER_PATTERN.test(comparisonMatch[0]) && !BOOLEAN_COMPARAND_PATTERN.test(comparisonMatch[0])) matchIndex = comparisonMatch.index;
|
|
8486
|
+
}
|
|
8487
|
+
if (matchIndex < 0) matchIndex = findRandomCallIndexWithSameLineContext(file.content, MATH_RANDOM_CALL_PATTERN, SECURITY_RANDOM_CONTEXT_PATTERN, UI_NONCE_CONTEXT_PATTERN);
|
|
8488
|
+
if (matchIndex < 0) return [];
|
|
8489
|
+
const location = getLocationAtIndex(file.content, matchIndex);
|
|
8490
|
+
return [{
|
|
8491
|
+
message: "Code uses weak hashes, deprecated ciphers, timing-unsafe comparisons, or Math.random in a security-shaped context.",
|
|
8492
|
+
line: location.line,
|
|
8493
|
+
column: location.column
|
|
8494
|
+
}];
|
|
8495
|
+
}
|
|
8496
|
+
});
|
|
8497
|
+
//#endregion
|
|
7710
8498
|
//#region src/plugin/constants/event-handlers.ts
|
|
7711
8499
|
const MOUSE_EVENT_HANDLERS = [
|
|
7712
8500
|
"onClick",
|
|
@@ -11912,6 +12700,19 @@ const jsxPropsNoSpreading = defineRule({
|
|
|
11912
12700
|
}
|
|
11913
12701
|
});
|
|
11914
12702
|
//#endregion
|
|
12703
|
+
//#region src/plugin/rules/security-scan/key-lifecycle-risk.ts
|
|
12704
|
+
const keyLifecycleRisk = defineRule({
|
|
12705
|
+
id: "key-lifecycle-risk",
|
|
12706
|
+
title: "Long-lived key material in repository",
|
|
12707
|
+
severity: "error",
|
|
12708
|
+
recommendation: "Remove private keys from source, rotate exposed credentials, prefer short-lived deploy credentials, and document revocation/expiry for release keys.",
|
|
12709
|
+
scan: scanByPattern({
|
|
12710
|
+
shouldScan: (file) => !TEST_CONTEXT_PATTERN.test(file.relativePath) && !DOCUMENTATION_CONTEXT_PATTERN.test(file.relativePath),
|
|
12711
|
+
pattern: /(?<!(?:placeholder|example|sample|dummy|fake)[\s\S]{0,40})-----BEGIN (?:RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----(?:\s|\\r|\\n)*[A-Za-z0-9+/=][A-Za-z0-9+/=\s]{38,}(?![^-]{0,160}\.\.\.)|\b(?:SSH_PRIVATE_KEY|GPG_PRIVATE_KEY|DEPLOY_KEY|SIGNING_KEY)\b\s*[:=]\s*["'][^"'\n]{16,}["']/i,
|
|
12712
|
+
message: "Private or long-lived release key material appears in the repository."
|
|
12713
|
+
})
|
|
12714
|
+
});
|
|
12715
|
+
//#endregion
|
|
11915
12716
|
//#region src/plugin/rules/a11y/label-has-associated-control.ts
|
|
11916
12717
|
const MESSAGE_NO_LABEL = "Blind users can't identify this field because screen readers find no label text, so add visible text, `aria-label`, or `aria-labelledby`.";
|
|
11917
12718
|
const MESSAGE_NO_CONTROL = "Screen reader users can't tell which input this label names because it's tied to none, so add `htmlFor` or wrap the input inside it.";
|
|
@@ -12287,6 +13088,44 @@ const lang = defineRule({
|
|
|
12287
13088
|
} })
|
|
12288
13089
|
});
|
|
12289
13090
|
//#endregion
|
|
13091
|
+
//#region src/plugin/rules/security-scan/local-rpc-native-bridge-risk.ts
|
|
13092
|
+
const localRpcNativeBridgeRisk = defineRule({
|
|
13093
|
+
id: "local-rpc-native-bridge-risk",
|
|
13094
|
+
title: "Weak localhost native bridge boundary",
|
|
13095
|
+
severity: "warn",
|
|
13096
|
+
recommendation: "Use exact origin allowlists after URL parsing, per-request nonces, narrow methods, and never expose install/update commands to arbitrary web pages.",
|
|
13097
|
+
scan: scanByPattern({
|
|
13098
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13099
|
+
pattern: /\b(?:127\.0\.0\.1|localhost|Access-Control-Allow-Origin|websocket|WebSocket)\b[\s\S]{0,700}(?:\b(?:UpdateApp|InstallApp|child_process)\b|(?<![.\w$])(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)\s*\()/i,
|
|
13100
|
+
message: "Code appears to bridge browser code to localhost/native capabilities with weak origin or update/install checks."
|
|
13101
|
+
})
|
|
13102
|
+
});
|
|
13103
|
+
const mcpToolCapabilityRisk = defineRule({
|
|
13104
|
+
id: "mcp-tool-capability-risk",
|
|
13105
|
+
title: "MCP tool exposes dangerous capability",
|
|
13106
|
+
severity: "warn",
|
|
13107
|
+
recommendation: "MCP tool calls run with the connecting client's authority. Validate inputs, enforce per-tool authorization, and avoid raw filesystem/shell/network access where possible.",
|
|
13108
|
+
scan: scanByPattern({
|
|
13109
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13110
|
+
pattern: /\bserver\.\s*tool\s*\(|\bregisterTool\s*\(|\bsetRequestHandler\s*\(\s*CallToolRequestSchema/,
|
|
13111
|
+
requireAll: [/\bfrom\s+["']@modelcontextprotocol\/sdk[^"']*["']|\bMcpServer\b|\bMcpAgent\b/, AGENT_TOOL_DANGEROUS_CAPABILITY_PATTERN],
|
|
13112
|
+
message: "An MCP tool/resource/prompt handler appears to expose file, shell, network, or code-execution capability."
|
|
13113
|
+
})
|
|
13114
|
+
});
|
|
13115
|
+
//#endregion
|
|
13116
|
+
//#region src/plugin/rules/security-scan/mdx-ssr-execution-risk.ts
|
|
13117
|
+
const mdxSsrExecutionRisk = defineRule({
|
|
13118
|
+
id: "mdx-ssr-execution-risk",
|
|
13119
|
+
title: "Server-rendered MDX can execute code",
|
|
13120
|
+
severity: "warn",
|
|
13121
|
+
recommendation: "Use a constrained compiler for untrusted content, disable expressions/raw HTML, sandbox renderers, and avoid caching attacker-controlled output across tenants.",
|
|
13122
|
+
scan: scanByPattern({
|
|
13123
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
13124
|
+
pattern: /(?:@mdx-js\/mdx|next-mdx-remote|\b(?:MDXRemote|compileMDX|evaluateMdx)\b)[\s\S]{0,700}\b(?:repo|customer|tenant|user[-_]?(?:content|markdown|mdx|input|provided|generated|submitted)|untrusted|searchParams|req\.|request\.|fetch\s*\(|prisma\.|db\.|database|rehypeRaw|allowDangerousHtml)/i,
|
|
13125
|
+
message: "MDX/markdown rendering code may evaluate user or repository content during SSR or static generation."
|
|
13126
|
+
})
|
|
13127
|
+
});
|
|
13128
|
+
//#endregion
|
|
12290
13129
|
//#region src/plugin/rules/a11y/media-has-caption.ts
|
|
12291
13130
|
const MESSAGE$32 = "Deaf and hard-of-hearing users need captions for this media. Add a `<track kind=\"captions\">` inside the `<audio>` or `<video>`.";
|
|
12292
13131
|
const DEFAULT_AUDIO = ["audio"];
|
|
@@ -13484,6 +14323,20 @@ const FILENAME_TO_LANG = {
|
|
|
13484
14323
|
const resolveLang = (filename) => {
|
|
13485
14324
|
return FILENAME_TO_LANG[path.extname(filename).toLowerCase()] ?? "tsx";
|
|
13486
14325
|
};
|
|
14326
|
+
const parseSourceText = (filename, sourceText) => {
|
|
14327
|
+
try {
|
|
14328
|
+
const result = parseSync(filename, sourceText, {
|
|
14329
|
+
astType: "ts",
|
|
14330
|
+
lang: resolveLang(filename)
|
|
14331
|
+
});
|
|
14332
|
+
if (result.errors.some((parseError) => parseError.severity === "Error")) return null;
|
|
14333
|
+
const parsedProgram = result.program;
|
|
14334
|
+
attachParentReferences(parsedProgram);
|
|
14335
|
+
return parsedProgram;
|
|
14336
|
+
} catch {
|
|
14337
|
+
return null;
|
|
14338
|
+
}
|
|
14339
|
+
};
|
|
13487
14340
|
const parseCache = /* @__PURE__ */ new Map();
|
|
13488
14341
|
const parseSourceFile = (absoluteFilePath) => {
|
|
13489
14342
|
let fileStat;
|
|
@@ -13515,19 +14368,7 @@ const parseSourceFile = (absoluteFilePath) => {
|
|
|
13515
14368
|
});
|
|
13516
14369
|
return null;
|
|
13517
14370
|
}
|
|
13518
|
-
|
|
13519
|
-
try {
|
|
13520
|
-
const result = parseSync(absoluteFilePath, sourceText, {
|
|
13521
|
-
astType: "ts",
|
|
13522
|
-
lang: resolveLang(absoluteFilePath)
|
|
13523
|
-
});
|
|
13524
|
-
if (!result.errors.some((parseError) => parseError.severity === "Error")) {
|
|
13525
|
-
parsedProgram = result.program;
|
|
13526
|
-
attachParentReferences(parsedProgram);
|
|
13527
|
-
}
|
|
13528
|
-
} catch {
|
|
13529
|
-
parsedProgram = null;
|
|
13530
|
-
}
|
|
14371
|
+
const parsedProgram = parseSourceText(absoluteFilePath, sourceText);
|
|
13531
14372
|
parseCache.set(absoluteFilePath, {
|
|
13532
14373
|
mtimeMs: fileStat.mtimeMs,
|
|
13533
14374
|
size: fileStat.size,
|
|
@@ -21536,7 +22377,7 @@ const isUndefinedNode = (node) => {
|
|
|
21536
22377
|
if (node === null || node === void 0) return true;
|
|
21537
22378
|
return isNodeOfType(node, "Identifier") && node.name === "undefined";
|
|
21538
22379
|
};
|
|
21539
|
-
const getNodeText = (node) => {
|
|
22380
|
+
const getNodeText$1 = (node) => {
|
|
21540
22381
|
if (!node) return "";
|
|
21541
22382
|
return JSON.stringify(node, (key, value) => {
|
|
21542
22383
|
if (key === "parent" || key === "loc" || key === "range" || key === "start" || key === "end") return;
|
|
@@ -21554,7 +22395,7 @@ const isSetStateToInitialValue = (analysis, setterRef) => {
|
|
|
21554
22395
|
if (isUndefinedNode(setStateToValue) && isUndefinedNode(stateInitialValue)) return true;
|
|
21555
22396
|
if (setStateToValue == null && stateInitialValue == null) return true;
|
|
21556
22397
|
if (setStateToValue && !stateInitialValue || !setStateToValue && stateInitialValue) return false;
|
|
21557
|
-
return getNodeText(setStateToValue) === getNodeText(stateInitialValue);
|
|
22398
|
+
return getNodeText$1(setStateToValue) === getNodeText$1(stateInitialValue);
|
|
21558
22399
|
};
|
|
21559
22400
|
const countUseStates = (analysis, componentNode) => {
|
|
21560
22401
|
if (!componentNode) return 0;
|
|
@@ -21618,173 +22459,6 @@ const noScaleFromZero = defineRule({
|
|
|
21618
22459
|
} })
|
|
21619
22460
|
});
|
|
21620
22461
|
//#endregion
|
|
21621
|
-
//#region src/plugin/constants/security.ts
|
|
21622
|
-
const AUTH_FUNCTION_NAMES = new Set([
|
|
21623
|
-
"auth",
|
|
21624
|
-
"getSession",
|
|
21625
|
-
"getServerSession",
|
|
21626
|
-
"getUser",
|
|
21627
|
-
"requireAuth",
|
|
21628
|
-
"checkAuth",
|
|
21629
|
-
"verifyAuth",
|
|
21630
|
-
"authenticate",
|
|
21631
|
-
"currentUser",
|
|
21632
|
-
"getAuth",
|
|
21633
|
-
"validateSession"
|
|
21634
|
-
]);
|
|
21635
|
-
const GENERIC_AUTH_METHOD_NAMES = new Set(["getUser"]);
|
|
21636
|
-
const AUTH_OBJECT_PATTERN = /(?:^|[._])(?:auth|authn|authz|clerk|session|jwt|firebase|supabase|nextauth|kinde|workos|stytch|descope|cognito|propelauth|lucia)/i;
|
|
21637
|
-
const SECRET_PATTERNS = [
|
|
21638
|
-
/^sk_live_/,
|
|
21639
|
-
/^sk_test_/,
|
|
21640
|
-
/^AKIA[0-9A-Z]{16}$/,
|
|
21641
|
-
/^ghp_[a-zA-Z0-9]{36}$/,
|
|
21642
|
-
/^gho_[a-zA-Z0-9]{36}$/,
|
|
21643
|
-
/^github_pat_/,
|
|
21644
|
-
/^glpat-/,
|
|
21645
|
-
/^xox[bporas]-/,
|
|
21646
|
-
/^sk-[a-zA-Z0-9]{32,}$/
|
|
21647
|
-
];
|
|
21648
|
-
const PUBLIC_CLIENT_KEY_PATTERNS = [
|
|
21649
|
-
/^appl_/,
|
|
21650
|
-
/^goog_/,
|
|
21651
|
-
/^amzn_/,
|
|
21652
|
-
/^strp_/,
|
|
21653
|
-
/^pk_(?:live|test)_/,
|
|
21654
|
-
/^sb_publishable_/,
|
|
21655
|
-
/^phc_/,
|
|
21656
|
-
/^public-token-(?:live|test)-/,
|
|
21657
|
-
/^pk\.eyJ/
|
|
21658
|
-
];
|
|
21659
|
-
const SECRET_UNAMBIGUOUS_PLACEHOLDER_VALUE_PATTERNS = [
|
|
21660
|
-
/^[\s._\-*\u2022xX]{8,}$/,
|
|
21661
|
-
/(?:\.{3,}|\u2026|[*\u2022]{3,})/,
|
|
21662
|
-
/(?:^|[_\-\s])(?:your|redacted|masked|placeholder|replace[_\-\s]?me|changeme)(?:$|[_\-\s])/i,
|
|
21663
|
-
/<[^>]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^>]*>/i,
|
|
21664
|
-
/\[[^\]]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^\]]*\]/i,
|
|
21665
|
-
/\{[^}]*(?:auth|credential|key|password|secret|token|your|redacted|placeholder|masked)[^}]*\}/i
|
|
21666
|
-
];
|
|
21667
|
-
const SECRET_CONTEXTUAL_PLACEHOLDER_VALUE_PATTERNS = [/(?:^|[_\-\s])(?:example|sample|dummy)(?:$|[_\-\s])/i];
|
|
21668
|
-
const SECRET_PLACEHOLDER_CONTEXT_PATTERN = /(?:placeholder|example|sample|dummy|masked|redacted|mask)/i;
|
|
21669
|
-
const SECRET_VARIABLE_PATTERN = /(?:api_?key|secret|token|password|credential|auth)/i;
|
|
21670
|
-
const SECRET_TOOLING_FILE_PATTERN = /(?:^|\/)[^/]+\.config\.[cm]?[jt]s$/;
|
|
21671
|
-
const SECRET_TOOLING_RC_FILE_PATTERN = /(?:^|\/)(?:\.[a-z-]+rc|[a-z-]+\.rc)\.[cm]?[jt]s$/;
|
|
21672
|
-
const SECRET_TEST_FILE_PATTERN = /(?:^|\/)[^/]+\.(?:test|spec|stories|story|fixture|fixtures)\.[cm]?[jt]sx?$/;
|
|
21673
|
-
const SECRET_SERVER_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.server\.[cm]?[jt]sx?$/;
|
|
21674
|
-
const SECRET_SERVER_ENTRY_FILE_PATTERN = /(?:^|\/)(?:middleware|proxy|route)\.[cm]?[jt]sx?$/;
|
|
21675
|
-
const SECRET_NEXT_PAGES_API_FILE_PATTERN = /(?:^|\/)pages\/api\/.+\.[cm]?[jt]sx?$/;
|
|
21676
|
-
const SECRET_CLIENT_FILE_SUFFIX_PATTERN = /(?:^|\/)[^/]+\.(?:client|browser|web)\.[cm]?[jt]sx?$/;
|
|
21677
|
-
const SECRET_CLIENT_ENTRY_FILE_PATTERN = /(?:^|\/)(?:src\/)?(?:main|index|[Aa]pp|client)\.[cm]?[jt]sx?$/;
|
|
21678
|
-
const SECRET_SERVER_DIRECTORY_NAMES = new Set([
|
|
21679
|
-
"backend",
|
|
21680
|
-
"functions",
|
|
21681
|
-
"lambdas",
|
|
21682
|
-
"lambda",
|
|
21683
|
-
"middleware",
|
|
21684
|
-
"server",
|
|
21685
|
-
"servers"
|
|
21686
|
-
]);
|
|
21687
|
-
const SECRET_SERVER_SOURCE_ROOT_OWNER_NAMES = new Set([
|
|
21688
|
-
"api",
|
|
21689
|
-
"backend",
|
|
21690
|
-
"edge",
|
|
21691
|
-
"function",
|
|
21692
|
-
"functions",
|
|
21693
|
-
"lambda",
|
|
21694
|
-
"lambdas",
|
|
21695
|
-
"server",
|
|
21696
|
-
"servers",
|
|
21697
|
-
"worker",
|
|
21698
|
-
"workers"
|
|
21699
|
-
]);
|
|
21700
|
-
const SECRET_TEST_DIRECTORY_NAMES = new Set([
|
|
21701
|
-
"__fixtures__",
|
|
21702
|
-
"__mocks__",
|
|
21703
|
-
"__tests__",
|
|
21704
|
-
"fixtures",
|
|
21705
|
-
"mocks",
|
|
21706
|
-
"test",
|
|
21707
|
-
"tests"
|
|
21708
|
-
]);
|
|
21709
|
-
const SECRET_TOOLING_DIRECTORY_NAMES = new Set([
|
|
21710
|
-
"bin",
|
|
21711
|
-
"config",
|
|
21712
|
-
"configs",
|
|
21713
|
-
"script",
|
|
21714
|
-
"scripts",
|
|
21715
|
-
"tooling",
|
|
21716
|
-
"tools"
|
|
21717
|
-
]);
|
|
21718
|
-
const SECRET_CLIENT_SOURCE_DIRECTORY_NAMES = new Set([
|
|
21719
|
-
"components",
|
|
21720
|
-
"features",
|
|
21721
|
-
"hooks",
|
|
21722
|
-
"pages",
|
|
21723
|
-
"ui",
|
|
21724
|
-
"views",
|
|
21725
|
-
"widgets"
|
|
21726
|
-
]);
|
|
21727
|
-
const SECRET_FALSE_POSITIVE_SUFFIXES = new Set([
|
|
21728
|
-
"modal",
|
|
21729
|
-
"label",
|
|
21730
|
-
"text",
|
|
21731
|
-
"title",
|
|
21732
|
-
"name",
|
|
21733
|
-
"id",
|
|
21734
|
-
"url",
|
|
21735
|
-
"path",
|
|
21736
|
-
"route",
|
|
21737
|
-
"page",
|
|
21738
|
-
"param",
|
|
21739
|
-
"field",
|
|
21740
|
-
"column",
|
|
21741
|
-
"header",
|
|
21742
|
-
"placeholder",
|
|
21743
|
-
"prefix",
|
|
21744
|
-
"description",
|
|
21745
|
-
"type",
|
|
21746
|
-
"icon",
|
|
21747
|
-
"class",
|
|
21748
|
-
"style",
|
|
21749
|
-
"variant",
|
|
21750
|
-
"event",
|
|
21751
|
-
"action",
|
|
21752
|
-
"status",
|
|
21753
|
-
"state",
|
|
21754
|
-
"mode",
|
|
21755
|
-
"flag",
|
|
21756
|
-
"option",
|
|
21757
|
-
"config",
|
|
21758
|
-
"message",
|
|
21759
|
-
"error",
|
|
21760
|
-
"display",
|
|
21761
|
-
"view",
|
|
21762
|
-
"component",
|
|
21763
|
-
"element",
|
|
21764
|
-
"container",
|
|
21765
|
-
"wrapper",
|
|
21766
|
-
"button",
|
|
21767
|
-
"link",
|
|
21768
|
-
"input",
|
|
21769
|
-
"select",
|
|
21770
|
-
"dialog",
|
|
21771
|
-
"menu",
|
|
21772
|
-
"form",
|
|
21773
|
-
"step",
|
|
21774
|
-
"index",
|
|
21775
|
-
"count",
|
|
21776
|
-
"length",
|
|
21777
|
-
"role",
|
|
21778
|
-
"scope",
|
|
21779
|
-
"context",
|
|
21780
|
-
"provider",
|
|
21781
|
-
"ref",
|
|
21782
|
-
"handler",
|
|
21783
|
-
"query",
|
|
21784
|
-
"schema",
|
|
21785
|
-
"constant"
|
|
21786
|
-
]);
|
|
21787
|
-
//#endregion
|
|
21788
22462
|
//#region src/plugin/utils/classify-secret-file-exposure.ts
|
|
21789
22463
|
const SOURCE_FILE_EXTENSION_PATTERN = /\.[cm]?[jt]sx?$/;
|
|
21790
22464
|
const CLIENT_SOURCE_FILE_EXTENSION_PATTERN = /\.[cm]?[jt]sx$/;
|
|
@@ -24521,6 +25195,17 @@ const noZIndex9999 = defineRule({
|
|
|
24521
25195
|
}
|
|
24522
25196
|
})
|
|
24523
25197
|
});
|
|
25198
|
+
const nosqlInjectionRisk = defineRule({
|
|
25199
|
+
id: "nosql-injection-risk",
|
|
25200
|
+
title: "NoSQL query accepts operator-shaped input",
|
|
25201
|
+
severity: "warn",
|
|
25202
|
+
recommendation: "Coerce scalar fields before querying, reject operator keys from client input, and avoid `$where` or request-derived regexes.",
|
|
25203
|
+
scan: scanByPattern({
|
|
25204
|
+
shouldScan: (file) => isProductionFilePath(file.relativePath, DATABASE_SOURCE_FILE_PATTERN),
|
|
25205
|
+
pattern: /\$where\s*['"]?\s*:\s*(?:f?['"`][^'"`]{0,200}\$\{|function|f['"])|\.find\s*\(\s*JSON\.parse\s*\(\s*(?:req|request)\.|\.aggregate\s*\(\s*\[?\s*\{[^}]{0,400}\$where|\bnew\s+RegExp\s*\(\s*(?:req|request)\.|\$regex['"]?\s*:\s*(?:req|request)\./i,
|
|
25206
|
+
message: "Code appears to pass raw JSON, regex, or `$where` style input into a NoSQL query."
|
|
25207
|
+
})
|
|
25208
|
+
});
|
|
24524
25209
|
//#endregion
|
|
24525
25210
|
//#region src/plugin/utils/is-framework-route-or-special-filename.ts
|
|
24526
25211
|
const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
|
|
@@ -25073,6 +25758,116 @@ const onlyExportComponents = defineRule({
|
|
|
25073
25758
|
}
|
|
25074
25759
|
});
|
|
25075
25760
|
//#endregion
|
|
25761
|
+
//#region src/plugin/rules/security-scan/package-metadata-secret.ts
|
|
25762
|
+
const packageMetadataSecret = defineRule({
|
|
25763
|
+
id: "package-metadata-secret",
|
|
25764
|
+
title: "Secret-like package metadata",
|
|
25765
|
+
severity: "warn",
|
|
25766
|
+
recommendation: "Keep secrets out of package metadata and generated reports; they are often published to registries, logs, or browser artifacts.",
|
|
25767
|
+
scan: (file) => {
|
|
25768
|
+
if (!file.relativePath.endsWith("package.json")) return [];
|
|
25769
|
+
const pattern = findSuspiciousPublicEnvSecretNamePattern(file.content) ?? SECRET_VALUE_PATTERNS.find((candidate) => candidate.test(file.content));
|
|
25770
|
+
if (pattern === void 0) return [];
|
|
25771
|
+
const location = getMatchLocation(file.content, pattern);
|
|
25772
|
+
return [{
|
|
25773
|
+
message: "Package metadata contains secret-like values or public env secret names.",
|
|
25774
|
+
line: location.line,
|
|
25775
|
+
column: location.column
|
|
25776
|
+
}];
|
|
25777
|
+
}
|
|
25778
|
+
});
|
|
25779
|
+
const pathTraversalRisk = defineRule({
|
|
25780
|
+
id: "path-traversal-risk",
|
|
25781
|
+
title: "Filesystem path uses caller input",
|
|
25782
|
+
severity: "warn",
|
|
25783
|
+
recommendation: "Resolve paths against a fixed base directory, reject traversal after normalization, and map user-visible identifiers to server-owned paths.",
|
|
25784
|
+
scan: scanByPattern({
|
|
25785
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath) && !isDevToolingPath(file.relativePath),
|
|
25786
|
+
pattern: /\b(?:readFile|readFileSync|writeFile|writeFileSync)\s*\(\s*(?:req\.|request\.|params\.|query\.|body\.|parsed\.|`[^`]*(?<![-.\w$'"])(?:req\.|request\.|params\.|query\.|body\.))|\bpath\.(?:join|resolve)\s*\([^)]*(?<![-.\w$'"])(?:req\.|request\.|params\.|query\.|body\.|parsed\.)/,
|
|
25787
|
+
message: "Filesystem access appears to use request, query, params, or body data as part of the path."
|
|
25788
|
+
})
|
|
25789
|
+
});
|
|
25790
|
+
//#endregion
|
|
25791
|
+
//#region src/plugin/rules/security-scan/plugin-update-trust-risk.ts
|
|
25792
|
+
const UPDATER_TRUST_PATTERN = /\b(?:repoUrl|updateUrl|UpdateApp|InstallApp|auto.?updater?|installer|curl(?!\s+(?:-T\b|--upload-file\b))|wget)\b[\s\S]{0,250}(?:\.(?:zip|exe|dmg|appimage|msi|deb|rpm)\b|\.tar\.gz\b|\|\s*(?:bash|sh)\b)/i;
|
|
25793
|
+
const CHECKSUM_VERIFICATION_PATTERN = /sha(?:256|512|1)sum|--checksum|checksum=|EXPECTED_SHA|gpg\s+--verify|\.sha(?:256|512)\b/i;
|
|
25794
|
+
const EXECUTION_CONTEXT_PATTERN = /\b(?:child_process|childProcess|execa|os\.system|subprocess\.|Deno\.run|autoUpdater|electron-updater)\b|\b(?:exec(?:File)?(?:Sync)?|spawn(?:Sync)?)\s*\(/;
|
|
25795
|
+
const pluginUpdateTrustRisk = defineRule({
|
|
25796
|
+
id: "plugin-update-trust-risk",
|
|
25797
|
+
title: "Plugin or updater trust boundary risk",
|
|
25798
|
+
severity: "warn",
|
|
25799
|
+
recommendation: "Require signed updates/plugins, pin trusted repositories, verify hashes before execution, and keep custom repository installs behind explicit warnings.",
|
|
25800
|
+
scan: (file) => {
|
|
25801
|
+
if (!isProductionSourcePath(file.relativePath) && !isConfigOrCiPath(file.relativePath)) return [];
|
|
25802
|
+
const content = getScannableContent(file);
|
|
25803
|
+
if (!UPDATER_TRUST_PATTERN.test(content)) return [];
|
|
25804
|
+
if (CHECKSUM_VERIFICATION_PATTERN.test(content)) return [];
|
|
25805
|
+
if (SOURCE_FILE_PATTERN.test(file.relativePath) && !EXECUTION_CONTEXT_PATTERN.test(content)) return [];
|
|
25806
|
+
const location = getMatchLocation(content, UPDATER_TRUST_PATTERN);
|
|
25807
|
+
return [{
|
|
25808
|
+
message: "Code appears to download, install, update, or execute plugin/updater content across a trust boundary.",
|
|
25809
|
+
line: location.line,
|
|
25810
|
+
column: location.column
|
|
25811
|
+
}];
|
|
25812
|
+
}
|
|
25813
|
+
});
|
|
25814
|
+
//#endregion
|
|
25815
|
+
//#region src/plugin/rules/security-scan/postmessage-origin-risk.ts
|
|
25816
|
+
const POSTMESSAGE_ORIGIN_CHECK_PATTERN = /origin(?!al)|\.source\s*[!=]==?/i;
|
|
25817
|
+
const MESSAGE_DATA_READ_PATTERN = /\b(?:event|e|evt|msg|message)\.data\b/;
|
|
25818
|
+
const SAME_APPLICATION_CHANNEL_TARGET_PATTERN = /port\d?\b|worker|channel|broadcast|socket|\bws\b|\bsse\b|eventsource|^self\.|^source\./i;
|
|
25819
|
+
const WORKER_FILE_PATH_PATTERN = /worker/i;
|
|
25820
|
+
const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
|
|
25821
|
+
const getNodeText = (content, node) => {
|
|
25822
|
+
const startIndex = getNodeStartIndex(node);
|
|
25823
|
+
const endIndex = "end" in node && typeof node.end === "number" ? node.end : -1;
|
|
25824
|
+
if (startIndex < 0 || endIndex < 0) return "";
|
|
25825
|
+
return content.slice(startIndex, endIndex);
|
|
25826
|
+
};
|
|
25827
|
+
const getMessageHandlerTarget = (content, node) => {
|
|
25828
|
+
if (node.type === "CallExpression") {
|
|
25829
|
+
const calleeText = isAstNode(node.callee) ? getNodeText(content, node.callee) : "";
|
|
25830
|
+
if (!calleeText.endsWith("addEventListener")) return null;
|
|
25831
|
+
const firstArgument = node.arguments[0];
|
|
25832
|
+
return isAstNode(firstArgument) && firstArgument.type === "Literal" && firstArgument.value === "message" ? calleeText : null;
|
|
25833
|
+
}
|
|
25834
|
+
if (node.type === "AssignmentExpression" && isAstNode(node.left)) {
|
|
25835
|
+
const leftText = getNodeText(content, node.left);
|
|
25836
|
+
return leftText.endsWith(".onmessage") ? leftText : null;
|
|
25837
|
+
}
|
|
25838
|
+
return null;
|
|
25839
|
+
};
|
|
25840
|
+
const postmessageOriginRisk = defineRule({
|
|
25841
|
+
id: "postmessage-origin-risk",
|
|
25842
|
+
title: "postMessage handler without origin check",
|
|
25843
|
+
severity: "warn",
|
|
25844
|
+
recommendation: "Validate `event.origin` against an exact allowlist before using `event.data`, especially when an iframe or parent window can be attacker-controlled.",
|
|
25845
|
+
scan: (file) => {
|
|
25846
|
+
if (!isProductionSourcePath(file.relativePath)) return [];
|
|
25847
|
+
if (WORKER_FILE_PATH_PATTERN.test(file.relativePath)) return [];
|
|
25848
|
+
const ast = parseSourceText(file.absolutePath, file.content);
|
|
25849
|
+
if (ast === null) return [];
|
|
25850
|
+
const findings = [];
|
|
25851
|
+
walkAst(ast, (node) => {
|
|
25852
|
+
const targetText = getMessageHandlerTarget(file.content, node);
|
|
25853
|
+
if (targetText === null) return;
|
|
25854
|
+
if (SAME_APPLICATION_CHANNEL_TARGET_PATTERN.test(targetText)) return;
|
|
25855
|
+
const nodeText = getNodeText(file.content, node);
|
|
25856
|
+
const messageDataIndex = nodeText.search(MESSAGE_DATA_READ_PATTERN);
|
|
25857
|
+
if (messageDataIndex < 0) return;
|
|
25858
|
+
const originCheckIndex = nodeText.search(POSTMESSAGE_ORIGIN_CHECK_PATTERN);
|
|
25859
|
+
if (originCheckIndex >= 0 && originCheckIndex < messageDataIndex) return;
|
|
25860
|
+
const location = getLocationAtIndex(file.content, getNodeStartIndex(node));
|
|
25861
|
+
findings.push({
|
|
25862
|
+
message: "A message event handler reads cross-window messages without an obvious origin check.",
|
|
25863
|
+
line: location.line,
|
|
25864
|
+
column: location.column
|
|
25865
|
+
});
|
|
25866
|
+
});
|
|
25867
|
+
return findings;
|
|
25868
|
+
}
|
|
25869
|
+
});
|
|
25870
|
+
//#endregion
|
|
25076
25871
|
//#region src/plugin/rules/preact/preact-no-children-length.ts
|
|
25077
25872
|
const ARRAY_READ_METHOD_NAMES = new Set([
|
|
25078
25873
|
"length",
|
|
@@ -26193,6 +26988,51 @@ const preferUseReducer = defineRule({
|
|
|
26193
26988
|
}
|
|
26194
26989
|
});
|
|
26195
26990
|
//#endregion
|
|
26991
|
+
//#region src/plugin/rules/security-scan/utils/is-public-debug-artifact-path.ts
|
|
26992
|
+
const LOCALE_DIRECTORY_PATTERN = /(?:^|\/)(?:locales?|i18n|lang|langs|translations?)\//i;
|
|
26993
|
+
const isPublicDebugArtifactPath = (relativePath) => isBrowserArtifactPath(relativePath, GENERATED_BUNDLE_FILE_PATTERN.test(relativePath)) && !LOCALE_DIRECTORY_PATTERN.test(relativePath) && /(?:^|\/)(?:\.env(?:\.[^/]*)?|[^/]*(?:debug|crash|trace|stack|report|dump|phpinfo)[^/]*\.(?:txt|log|json|html?)|[^/]+\.log)$/i.test(relativePath);
|
|
26994
|
+
//#endregion
|
|
26995
|
+
//#region src/plugin/rules/security-scan/public-debug-artifact.ts
|
|
26996
|
+
const publicDebugArtifact = defineRule({
|
|
26997
|
+
id: "public-debug-artifact",
|
|
26998
|
+
title: "Public debug artifact",
|
|
26999
|
+
severity: "warn",
|
|
27000
|
+
recommendation: "Remove debug artifacts from public output; logs and dumps often reveal source paths, internal routes, tokens, or environment snapshots.",
|
|
27001
|
+
scan: (file) => {
|
|
27002
|
+
if (!isPublicDebugArtifactPath(file.relativePath)) return [];
|
|
27003
|
+
const finding = {
|
|
27004
|
+
message: "A browser-reachable debug, log, dump, report, or env artifact is present.",
|
|
27005
|
+
line: 1,
|
|
27006
|
+
column: 1
|
|
27007
|
+
};
|
|
27008
|
+
return [SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(file.content)) ? {
|
|
27009
|
+
...finding,
|
|
27010
|
+
severity: "error"
|
|
27011
|
+
} : finding];
|
|
27012
|
+
}
|
|
27013
|
+
});
|
|
27014
|
+
//#endregion
|
|
27015
|
+
//#region src/plugin/rules/security-scan/public-env-secret-name.ts
|
|
27016
|
+
const DOCS_DIRECTORY_PATTERN = /(?:^|\/)docs?\//i;
|
|
27017
|
+
const publicEnvSecretName = defineRule({
|
|
27018
|
+
id: "public-env-secret-name",
|
|
27019
|
+
title: "Secret-like public env variable",
|
|
27020
|
+
severity: "warn",
|
|
27021
|
+
recommendation: "Public env prefixes are inlined into browser bundles. Rename public values to non-secret names, and keep tokens, passwords, private keys, and service-role credentials server-only.",
|
|
27022
|
+
scan: (file) => {
|
|
27023
|
+
if (!isClientSourcePath(file.relativePath)) return [];
|
|
27024
|
+
if (DOCS_DIRECTORY_PATTERN.test(file.relativePath)) return [];
|
|
27025
|
+
const pattern = findSuspiciousPublicEnvSecretNamePattern(file.content);
|
|
27026
|
+
if (pattern === void 0) return [];
|
|
27027
|
+
const location = getMatchLocation(file.content, pattern);
|
|
27028
|
+
return [{
|
|
27029
|
+
message: "Client code references a public env variable whose name looks like a secret or privileged credential.",
|
|
27030
|
+
line: location.line,
|
|
27031
|
+
column: location.column
|
|
27032
|
+
}];
|
|
27033
|
+
}
|
|
27034
|
+
});
|
|
27035
|
+
//#endregion
|
|
26196
27036
|
//#region src/plugin/rules/tanstack-query/query-destructure-result.ts
|
|
26197
27037
|
const queryDestructureResult = defineRule({
|
|
26198
27038
|
id: "query-destructure-result",
|
|
@@ -26385,6 +27225,30 @@ const queryStableQueryClient = defineRule({
|
|
|
26385
27225
|
};
|
|
26386
27226
|
}
|
|
26387
27227
|
});
|
|
27228
|
+
const rawSqlInjectionRisk = defineRule({
|
|
27229
|
+
id: "raw-sql-injection-risk",
|
|
27230
|
+
title: "Raw SQL built outside parameter binding",
|
|
27231
|
+
severity: "warn",
|
|
27232
|
+
recommendation: "Keep user input in driver parameters or ORM bind variables. Avoid unsafe/raw SQL helpers and string interpolation for queries.",
|
|
27233
|
+
scan: scanByPattern({
|
|
27234
|
+
shouldScan: (file) => isProductionScriptSourcePath(file.relativePath),
|
|
27235
|
+
pattern: [
|
|
27236
|
+
/\$queryRawUnsafe\s*\(/,
|
|
27237
|
+
/\$executeRawUnsafe\s*\(/,
|
|
27238
|
+
/\bPrisma\.raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,
|
|
27239
|
+
/\bsql\.\s*(?:raw|unsafe)\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,
|
|
27240
|
+
/\b(?:client|pool|conn)\.query\s*\(\s*['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^)]{0,400}\$\{(?!\s*[\w$.]*(?:sanitiz|escape|quote)[\w$]*\s*\()/i,
|
|
27241
|
+
/\.query\s*\(\s*['"`][^'"`]{0,200}['"`]\s*\+/,
|
|
27242
|
+
/\.(?:where|orderBy|having)Raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,
|
|
27243
|
+
/\bcursor\.execute\s*\(\s*f['"]/,
|
|
27244
|
+
/\bcursor\.execute\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*(?:%|\.format\s*\(|\+)/,
|
|
27245
|
+
/\b(?:engine|session)\.execute\s*\(\s*(?:text\s*\(\s*)?f['"]/,
|
|
27246
|
+
/\$[\w]+->(?:query|exec|prepare|executeQuery|executeStatement|createQuery|createNativeQuery)\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/,
|
|
27247
|
+
/mysqli_query\s*\(\s*[^,]+,\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/
|
|
27248
|
+
],
|
|
27249
|
+
message: "Code uses a raw SQL escape hatch or string-built query shape that can bypass parameter binding."
|
|
27250
|
+
})
|
|
27251
|
+
});
|
|
26388
27252
|
//#endregion
|
|
26389
27253
|
//#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
|
|
26390
27254
|
const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
|
|
@@ -27015,6 +27879,31 @@ const renderingUsetransitionLoading = defineRule({
|
|
|
27015
27879
|
} })
|
|
27016
27880
|
});
|
|
27017
27881
|
//#endregion
|
|
27882
|
+
//#region src/plugin/rules/security-scan/utils/is-repository-secret-file-path.ts
|
|
27883
|
+
const isRepositorySecretFilePath = (relativePath) => DOTENV_FILE_PATTERN.test(relativePath) || /(?:^|\/)\.npmrc$/.test(relativePath) || /(?:^|\/)[^/]*(?:credential|credentials|service-account|serviceAccount|firebase-admin|google-service-account|gcp-service-account)[^/]*\.(?:json|env|pem|key)$/i.test(relativePath);
|
|
27884
|
+
//#endregion
|
|
27885
|
+
//#region src/plugin/rules/security-scan/repository-secret-file.ts
|
|
27886
|
+
const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(relativePath);
|
|
27887
|
+
const repositorySecretFile = defineRule({
|
|
27888
|
+
id: "repository-secret-file",
|
|
27889
|
+
title: "Secret file checked into repository",
|
|
27890
|
+
severity: "error",
|
|
27891
|
+
recommendation: "Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.",
|
|
27892
|
+
scan: (file) => {
|
|
27893
|
+
if (!isRepositorySecretFilePath(file.relativePath)) return [];
|
|
27894
|
+
if (isRepositorySecretExamplePath(file.relativePath)) return [];
|
|
27895
|
+
if (TEST_CONTEXT_PATTERN.test(file.relativePath)) return [];
|
|
27896
|
+
const pattern = SECRET_VALUE_PATTERNS.find((candidate) => candidate.test(file.content)) ?? findSuspiciousPublicEnvSecretNamePattern(file.content);
|
|
27897
|
+
if (pattern === void 0) return [];
|
|
27898
|
+
const location = getMatchLocation(file.content, pattern);
|
|
27899
|
+
return [{
|
|
27900
|
+
message: "A repository credential/config file contains secret-looking values.",
|
|
27901
|
+
line: location.line,
|
|
27902
|
+
column: location.column
|
|
27903
|
+
}];
|
|
27904
|
+
}
|
|
27905
|
+
});
|
|
27906
|
+
//#endregion
|
|
27018
27907
|
//#region src/plugin/utils/function-body-has-return-with-value.ts
|
|
27019
27908
|
const functionBodyHasReturnWithValue = (functionNode) => {
|
|
27020
27909
|
if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
|
|
@@ -34236,6 +35125,50 @@ const stylePropObject = defineRule({
|
|
|
34236
35125
|
};
|
|
34237
35126
|
}
|
|
34238
35127
|
});
|
|
35128
|
+
const supabaseClientOwnedAuthzField = defineRule({
|
|
35129
|
+
id: "supabase-client-owned-authz-field",
|
|
35130
|
+
title: "Client writes Supabase authorization field",
|
|
35131
|
+
severity: "error",
|
|
35132
|
+
recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
|
|
35133
|
+
scan: scanByPattern({
|
|
35134
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
35135
|
+
pattern: /\b(?:ownerId|ownerID|creatorId|creatorID|userId|userID|uid|providerId|providerID|orgId|orgID|tenantId|tenantID|teamId|teamID|workspaceId|workspaceID|ghostOrg|role|roles|isAdmin|admin)\b/,
|
|
35136
|
+
requireAll: [/\b(?:supabase\b|\.from\s*\(\s*["'][^"']+["']\s*\))[\s\S]{0,700}\b(?:insert|upsert|update)\s*\(\s*(?:\{|\[?\s*\{)[\s\S]{0,700}\b(?:ownerId|creatorId|userId|orgId|tenantId|role|isAdmin)\b/i],
|
|
35137
|
+
message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
|
|
35138
|
+
})
|
|
35139
|
+
});
|
|
35140
|
+
//#endregion
|
|
35141
|
+
//#region src/plugin/rules/security-scan/utils/is-sql-path.ts
|
|
35142
|
+
const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
|
|
35143
|
+
const supabaseRlsPolicyRisk = defineRule({
|
|
35144
|
+
id: "supabase-rls-policy-risk",
|
|
35145
|
+
title: "Permissive Supabase RLS policy",
|
|
35146
|
+
severity: "error",
|
|
35147
|
+
recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
|
|
35148
|
+
scan: scanByPattern({
|
|
35149
|
+
shouldScan: (file) => isSqlPath(file.relativePath),
|
|
35150
|
+
pattern: [
|
|
35151
|
+
/disable\s+row\s+level\s+security/i,
|
|
35152
|
+
/create\s+policy[\s\S]{0,700}auth\.role\(\)\s*=\s*["']service_role["']/i,
|
|
35153
|
+
/create\s+policy[\s\S]{0,700}\bfor\s+(?:all|insert|update|delete)\b[\s\S]{0,500}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i,
|
|
35154
|
+
/create\s+policy(?:(?!\bfor\s+select\b)[\s\S]){0,700}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i
|
|
35155
|
+
],
|
|
35156
|
+
message: "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass."
|
|
35157
|
+
})
|
|
35158
|
+
});
|
|
35159
|
+
//#endregion
|
|
35160
|
+
//#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
|
|
35161
|
+
const svgFilterClickjackingRisk = defineRule({
|
|
35162
|
+
id: "svg-filter-clickjacking-risk",
|
|
35163
|
+
title: "SVG-filtered iframe clickjacking primitive",
|
|
35164
|
+
severity: "warn",
|
|
35165
|
+
recommendation: "Avoid filtering cross-origin iframes. Use `frame-ancestors` on sensitive pages and keep SVG filters away from embedded privileged UI.",
|
|
35166
|
+
scan: scanByPattern({
|
|
35167
|
+
shouldScan: (file) => isProductionSourcePath(file.relativePath),
|
|
35168
|
+
pattern: /<iframe\b[\s\S]{0,700}\bfilter\s*:\s*["']?url\(#|filter\s*:\s*url\(#.*[\s\S]{0,700}<iframe\b|<fe(?:DisplacementMap|ColorMatrix|Composite|Tile|Morphology)\b[\s\S]{0,700}<iframe\b/i,
|
|
35169
|
+
message: "An iframe is rendered through an SVG/CSS filter, which can support advanced clickjacking or visual exfiltration tricks."
|
|
35170
|
+
})
|
|
35171
|
+
});
|
|
34239
35172
|
//#endregion
|
|
34240
35173
|
//#region src/plugin/rules/a11y/tabindex-no-positive.ts
|
|
34241
35174
|
const MESSAGE = "Keyboard users get jumped out of the normal order by a positive `tabIndex`, so use `0` or `-1`.";
|
|
@@ -34901,6 +35834,76 @@ const tanstackStartServerFnValidateInput = defineRule({
|
|
|
34901
35834
|
} })
|
|
34902
35835
|
});
|
|
34903
35836
|
//#endregion
|
|
35837
|
+
//#region src/plugin/rules/security-scan/utils/is-server-route-source-path.ts
|
|
35838
|
+
const isServerRouteSourcePath = (relativePath) => {
|
|
35839
|
+
if (!isProductionSourcePath(relativePath)) return false;
|
|
35840
|
+
if (SERVER_CONTEXT_PATTERN.test(relativePath)) return true;
|
|
35841
|
+
return /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/.test(relativePath);
|
|
35842
|
+
};
|
|
35843
|
+
//#endregion
|
|
35844
|
+
//#region src/plugin/rules/security-scan/tenant-static-proxy-risk.ts
|
|
35845
|
+
const tenantStaticProxyRisk = defineRule({
|
|
35846
|
+
id: "tenant-static-proxy-risk",
|
|
35847
|
+
title: "Tenant-controlled static asset proxy",
|
|
35848
|
+
severity: "warn",
|
|
35849
|
+
recommendation: "Bind tenant identity to the trusted host or authenticated org, canonicalize after decoding, reject traversal, and never let one tenant choose another tenant's asset prefix.",
|
|
35850
|
+
scan: scanByPattern({
|
|
35851
|
+
shouldScan: (file) => isServerRouteSourcePath(file.relativePath),
|
|
35852
|
+
pattern: /\b(?:fetch|path\.join|getObject\w*|GetObjectCommand|getSignedUrl|createReadStream)\s*\([^;]{0,200}(?:\$\{[^}]{0,100}\b(?:tenant|subdomain|workspace|hostPattern|(?<!\.)organization(?:Id|Slug)?)\b|\b(?:tenant|subdomain|workspace)(?:Id|Slug|Name)?\b\s*[,)+\].])/i,
|
|
35853
|
+
message: "Route code appears to compose tenant or subdomain input into a static/CDN/object-store fetch path."
|
|
35854
|
+
})
|
|
35855
|
+
});
|
|
35856
|
+
//#endregion
|
|
35857
|
+
//#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
|
|
35858
|
+
const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
|
|
35859
|
+
const CALLER_STYLE_URL_NAME_PATTERN = /\b(?:url|targetUrl|callbackUrl|redirectUrl|webhookUrl|companyUrl|websiteUrl|domainUrl|imageUrl|fetchUrl|next|return_to|returnTo|destination|location)\b/i;
|
|
35860
|
+
const REQUEST_INPUT_EXPRESSION_PATTERN = /\breq\.|\brequest\.(?:query|body|params|nextUrl)|\bsearchParams\b|\bparams\.|\bbody\.|\bquery\./;
|
|
35861
|
+
const SAFE_REDIRECT_MODE_PATTERN = /\bredirect\s*:\s*["'](?:manual|error)["']/;
|
|
35862
|
+
const isRequestSourcedUrlExpression = (urlExpression, fileContent) => {
|
|
35863
|
+
if (REQUEST_INPUT_EXPRESSION_PATTERN.test(urlExpression)) return true;
|
|
35864
|
+
const identifierMatch = /^[\w$]+$/.exec(urlExpression.trim());
|
|
35865
|
+
if (identifierMatch === null) return false;
|
|
35866
|
+
return new RegExp(`(?:const|let|var)[^=;\\n]{0,80}\\b${identifierMatch[0]}\\b[^=;\\n]{0,80}=[^;\\n]*(?:\\breq\\.|\\brequest\\.|\\bsearchParams\\b|\\bparams\\.|\\bbody\\.|\\bquery\\.|\\$_(?:GET|POST|REQUEST))`).test(fileContent);
|
|
35867
|
+
};
|
|
35868
|
+
const untrustedRedirectFollowing = defineRule({
|
|
35869
|
+
id: "untrusted-redirect-following",
|
|
35870
|
+
title: "Server fetch follows redirects for caller-shaped URL",
|
|
35871
|
+
severity: "warn",
|
|
35872
|
+
recommendation: "Use `redirect: \"manual\"` or equivalent and re-validate every redirect target before following it to avoid SSRF redirect bypasses.",
|
|
35873
|
+
scan: (file) => {
|
|
35874
|
+
if (!isServerRouteSourcePath(file.relativePath)) return [];
|
|
35875
|
+
if (!OUTBOUND_FETCH_CALL_PATTERN.test(file.content)) return [];
|
|
35876
|
+
const findings = [];
|
|
35877
|
+
const lines = file.content.split("\n");
|
|
35878
|
+
for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
|
|
35879
|
+
const line = lines[lineIndex] ?? "";
|
|
35880
|
+
const fetchMatch = line.match(OUTBOUND_FETCH_CALL_PATTERN);
|
|
35881
|
+
const urlExpression = fetchMatch?.[1] ?? "";
|
|
35882
|
+
if (!fetchMatch || !CALLER_STYLE_URL_NAME_PATTERN.test(urlExpression)) continue;
|
|
35883
|
+
if (!isRequestSourcedUrlExpression(urlExpression, file.content)) continue;
|
|
35884
|
+
const fetchWindow = lines.slice(lineIndex, lineIndex + 5).join("\n");
|
|
35885
|
+
if (SAFE_REDIRECT_MODE_PATTERN.test(fetchWindow)) continue;
|
|
35886
|
+
findings.push({
|
|
35887
|
+
message: "Server-side fetch code appears to follow redirects for a URL shaped like caller-controlled input.",
|
|
35888
|
+
line: lineIndex + 1,
|
|
35889
|
+
column: line.search(/\S/) + 1
|
|
35890
|
+
});
|
|
35891
|
+
}
|
|
35892
|
+
return findings;
|
|
35893
|
+
}
|
|
35894
|
+
});
|
|
35895
|
+
const urlPrefilledPrivilegedAction = defineRule({
|
|
35896
|
+
id: "url-prefilled-privileged-action",
|
|
35897
|
+
title: "URL pre-fills a privileged action",
|
|
35898
|
+
severity: "warn",
|
|
35899
|
+
recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
|
|
35900
|
+
scan: scanByPattern({
|
|
35901
|
+
shouldScan: (file) => isClientSourcePath(file.relativePath),
|
|
35902
|
+
pattern: /(?<!(?:safe|valid|sanitiz|relativ|allowlist|whitelist)[\w$]*\(\s*(?:new\s+)?)\b(?:searchParams|useSearchParams\s*\(\s*\)|URLSearchParams\s*\([^)]{0,120}\))(?:[?!])?\.get(?:All)?\s*\(\s*["'](?:userstoinvite|role|permission|sharingaction|invite|admin|next|continue|returnTo|redirect_uri|callbackUrl)["']|\bsearchParams\.(?:userstoinvite|role|permission|sharingaction|invite|admin|returnTo|redirect_uri|callbackUrl)\b/i,
|
|
35903
|
+
message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
|
|
35904
|
+
})
|
|
35905
|
+
});
|
|
35906
|
+
//#endregion
|
|
34904
35907
|
//#region src/plugin/rules/bundle-size/use-lazy-motion.ts
|
|
34905
35908
|
const useLazyMotion = defineRule({
|
|
34906
35909
|
id: "use-lazy-motion",
|
|
@@ -34995,6 +35998,33 @@ const voidDomElementsNoChildren = defineRule({
|
|
|
34995
35998
|
})
|
|
34996
35999
|
});
|
|
34997
36000
|
//#endregion
|
|
36001
|
+
//#region src/plugin/rules/security-scan/webhook-signature-risk.ts
|
|
36002
|
+
const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
|
|
36003
|
+
const WEBHOOK_ENTRYPOINT_PATTERN = /\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i;
|
|
36004
|
+
const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = /verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']/i;
|
|
36005
|
+
const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
|
|
36006
|
+
const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
|
|
36007
|
+
const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
|
|
36008
|
+
const COMMENT_OR_STRING_PATTERN = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g;
|
|
36009
|
+
const webhookSignatureRisk = defineRule({
|
|
36010
|
+
id: "webhook-signature-risk",
|
|
36011
|
+
title: "Webhook handler lacks signature verification",
|
|
36012
|
+
severity: "warn",
|
|
36013
|
+
recommendation: "Verify provider signatures before parsing or acting on webhook bodies. Use provider SDK helpers or HMAC verification with timing-safe comparison.",
|
|
36014
|
+
scan: scanByPattern({
|
|
36015
|
+
shouldScan: (file) => {
|
|
36016
|
+
if (!isProductionSourcePath(file.relativePath)) return false;
|
|
36017
|
+
if (OUTBOUND_WEBHOOK_CONFIG_PATTERN.test(file.content)) return false;
|
|
36018
|
+
const judgeableContent = file.content.replace(COMMENT_OR_STRING_PATTERN, "").replace(OUTBOUND_WEBHOOK_URL_MENTION_PATTERN, "");
|
|
36019
|
+
return WEBHOOK_HANDLER_PATTERN.test(file.relativePath) || WEBHOOK_HANDLER_PATTERN.test(judgeableContent);
|
|
36020
|
+
},
|
|
36021
|
+
pattern: WEBHOOK_ENTRYPOINT_PATTERN,
|
|
36022
|
+
requireAll: [REQUEST_READ_PATTERN],
|
|
36023
|
+
suppressWhen: WEBHOOK_SIGNATURE_VERIFICATION_PATTERN,
|
|
36024
|
+
message: "Webhook handler code does not show an obvious signature verification step."
|
|
36025
|
+
})
|
|
36026
|
+
});
|
|
36027
|
+
//#endregion
|
|
34998
36028
|
//#region src/plugin/rules/zod/utils/zod-ast.ts
|
|
34999
36029
|
const ZOD_MODULE = "zod";
|
|
35000
36030
|
const getStaticPropertyName = (member) => {
|
|
@@ -35405,6 +36435,18 @@ const zodV4PreferTopLevelStringFormats = defineRule({
|
|
|
35405
36435
|
//#endregion
|
|
35406
36436
|
//#region src/plugin/rule-registry.ts
|
|
35407
36437
|
const reactDoctorRules = [
|
|
36438
|
+
{
|
|
36439
|
+
key: "react-doctor/active-static-asset",
|
|
36440
|
+
id: "active-static-asset",
|
|
36441
|
+
source: "react-doctor",
|
|
36442
|
+
originallyExternal: false,
|
|
36443
|
+
rule: {
|
|
36444
|
+
...activeStaticAsset,
|
|
36445
|
+
framework: "global",
|
|
36446
|
+
category: "Security",
|
|
36447
|
+
tags: [...new Set(["security-scan", ...activeStaticAsset.tags ?? []])]
|
|
36448
|
+
}
|
|
36449
|
+
},
|
|
35408
36450
|
{
|
|
35409
36451
|
key: "react-doctor/activity-wraps-effect-heavy-subtree",
|
|
35410
36452
|
id: "activity-wraps-effect-heavy-subtree",
|
|
@@ -35429,6 +36471,18 @@ const reactDoctorRules = [
|
|
|
35429
36471
|
requires: [...new Set(["react", ...advancedEventHandlerRefs.requires ?? []])]
|
|
35430
36472
|
}
|
|
35431
36473
|
},
|
|
36474
|
+
{
|
|
36475
|
+
key: "react-doctor/agent-tool-capability-risk",
|
|
36476
|
+
id: "agent-tool-capability-risk",
|
|
36477
|
+
source: "react-doctor",
|
|
36478
|
+
originallyExternal: false,
|
|
36479
|
+
rule: {
|
|
36480
|
+
...agentToolCapabilityRisk,
|
|
36481
|
+
framework: "global",
|
|
36482
|
+
category: "Security",
|
|
36483
|
+
tags: [...new Set(["security-scan", ...agentToolCapabilityRisk.tags ?? []])]
|
|
36484
|
+
}
|
|
36485
|
+
},
|
|
35432
36486
|
{
|
|
35433
36487
|
key: "react-doctor/alt-text",
|
|
35434
36488
|
id: "alt-text",
|
|
@@ -35537,6 +36591,42 @@ const reactDoctorRules = [
|
|
|
35537
36591
|
requires: [...new Set(["react", ...ariaUnsupportedElements.requires ?? []])]
|
|
35538
36592
|
}
|
|
35539
36593
|
},
|
|
36594
|
+
{
|
|
36595
|
+
key: "react-doctor/artifact-baas-authority-surface",
|
|
36596
|
+
id: "artifact-baas-authority-surface",
|
|
36597
|
+
source: "react-doctor",
|
|
36598
|
+
originallyExternal: false,
|
|
36599
|
+
rule: {
|
|
36600
|
+
...artifactBaasAuthoritySurface,
|
|
36601
|
+
framework: "global",
|
|
36602
|
+
category: "Security",
|
|
36603
|
+
tags: [...new Set(["security-scan", ...artifactBaasAuthoritySurface.tags ?? []])]
|
|
36604
|
+
}
|
|
36605
|
+
},
|
|
36606
|
+
{
|
|
36607
|
+
key: "react-doctor/artifact-env-leak",
|
|
36608
|
+
id: "artifact-env-leak",
|
|
36609
|
+
source: "react-doctor",
|
|
36610
|
+
originallyExternal: false,
|
|
36611
|
+
rule: {
|
|
36612
|
+
...artifactEnvLeak,
|
|
36613
|
+
framework: "global",
|
|
36614
|
+
category: "Security",
|
|
36615
|
+
tags: [...new Set(["security-scan", ...artifactEnvLeak.tags ?? []])]
|
|
36616
|
+
}
|
|
36617
|
+
},
|
|
36618
|
+
{
|
|
36619
|
+
key: "react-doctor/artifact-secret-leak",
|
|
36620
|
+
id: "artifact-secret-leak",
|
|
36621
|
+
source: "react-doctor",
|
|
36622
|
+
originallyExternal: false,
|
|
36623
|
+
rule: {
|
|
36624
|
+
...artifactSecretLeak,
|
|
36625
|
+
framework: "global",
|
|
36626
|
+
category: "Security",
|
|
36627
|
+
tags: [...new Set(["security-scan", ...artifactSecretLeak.tags ?? []])]
|
|
36628
|
+
}
|
|
36629
|
+
},
|
|
35540
36630
|
{
|
|
35541
36631
|
key: "react-doctor/async-await-in-loop",
|
|
35542
36632
|
id: "async-await-in-loop",
|
|
@@ -35583,6 +36673,18 @@ const reactDoctorRules = [
|
|
|
35583
36673
|
requires: [...new Set(["react", ...autocompleteValid.requires ?? []])]
|
|
35584
36674
|
}
|
|
35585
36675
|
},
|
|
36676
|
+
{
|
|
36677
|
+
key: "react-doctor/build-pipeline-secret-boundary",
|
|
36678
|
+
id: "build-pipeline-secret-boundary",
|
|
36679
|
+
source: "react-doctor",
|
|
36680
|
+
originallyExternal: false,
|
|
36681
|
+
rule: {
|
|
36682
|
+
...buildPipelineSecretBoundary,
|
|
36683
|
+
framework: "global",
|
|
36684
|
+
category: "Security",
|
|
36685
|
+
tags: [...new Set(["security-scan", ...buildPipelineSecretBoundary.tags ?? []])]
|
|
36686
|
+
}
|
|
36687
|
+
},
|
|
35586
36688
|
{
|
|
35587
36689
|
key: "react-doctor/button-has-type",
|
|
35588
36690
|
id: "button-has-type",
|
|
@@ -35619,6 +36721,18 @@ const reactDoctorRules = [
|
|
|
35619
36721
|
requires: [...new Set(["react", ...clickEventsHaveKeyEvents.requires ?? []])]
|
|
35620
36722
|
}
|
|
35621
36723
|
},
|
|
36724
|
+
{
|
|
36725
|
+
key: "react-doctor/clickjacking-redirect-risk",
|
|
36726
|
+
id: "clickjacking-redirect-risk",
|
|
36727
|
+
source: "react-doctor",
|
|
36728
|
+
originallyExternal: false,
|
|
36729
|
+
rule: {
|
|
36730
|
+
...clickjackingRedirectRisk,
|
|
36731
|
+
framework: "global",
|
|
36732
|
+
category: "Security",
|
|
36733
|
+
tags: [...new Set(["security-scan", ...clickjackingRedirectRisk.tags ?? []])]
|
|
36734
|
+
}
|
|
36735
|
+
},
|
|
35622
36736
|
{
|
|
35623
36737
|
key: "react-doctor/client-localstorage-no-version",
|
|
35624
36738
|
id: "client-localstorage-no-version",
|
|
@@ -35643,6 +36757,18 @@ const reactDoctorRules = [
|
|
|
35643
36757
|
requires: [...new Set(["react", ...clientPassiveEventListeners.requires ?? []])]
|
|
35644
36758
|
}
|
|
35645
36759
|
},
|
|
36760
|
+
{
|
|
36761
|
+
key: "react-doctor/command-execution-input-risk",
|
|
36762
|
+
id: "command-execution-input-risk",
|
|
36763
|
+
source: "react-doctor",
|
|
36764
|
+
originallyExternal: false,
|
|
36765
|
+
rule: {
|
|
36766
|
+
...commandExecutionInputRisk,
|
|
36767
|
+
framework: "global",
|
|
36768
|
+
category: "Security",
|
|
36769
|
+
tags: [...new Set(["security-scan", ...commandExecutionInputRisk.tags ?? []])]
|
|
36770
|
+
}
|
|
36771
|
+
},
|
|
35646
36772
|
{
|
|
35647
36773
|
key: "react-doctor/control-has-associated-label",
|
|
35648
36774
|
id: "control-has-associated-label",
|
|
@@ -35655,6 +36781,30 @@ const reactDoctorRules = [
|
|
|
35655
36781
|
requires: [...new Set(["react", ...controlHasAssociatedLabel.requires ?? []])]
|
|
35656
36782
|
}
|
|
35657
36783
|
},
|
|
36784
|
+
{
|
|
36785
|
+
key: "react-doctor/cors-cookie-trust-risk",
|
|
36786
|
+
id: "cors-cookie-trust-risk",
|
|
36787
|
+
source: "react-doctor",
|
|
36788
|
+
originallyExternal: false,
|
|
36789
|
+
rule: {
|
|
36790
|
+
...corsCookieTrustRisk,
|
|
36791
|
+
framework: "global",
|
|
36792
|
+
category: "Security",
|
|
36793
|
+
tags: [...new Set(["security-scan", ...corsCookieTrustRisk.tags ?? []])]
|
|
36794
|
+
}
|
|
36795
|
+
},
|
|
36796
|
+
{
|
|
36797
|
+
key: "react-doctor/dangerous-html-sink",
|
|
36798
|
+
id: "dangerous-html-sink",
|
|
36799
|
+
source: "react-doctor",
|
|
36800
|
+
originallyExternal: false,
|
|
36801
|
+
rule: {
|
|
36802
|
+
...dangerousHtmlSink,
|
|
36803
|
+
framework: "global",
|
|
36804
|
+
category: "Security",
|
|
36805
|
+
tags: [...new Set(["security-scan", ...dangerousHtmlSink.tags ?? []])]
|
|
36806
|
+
}
|
|
36807
|
+
},
|
|
35658
36808
|
{
|
|
35659
36809
|
key: "react-doctor/design-no-em-dash-in-jsx-text",
|
|
35660
36810
|
id: "design-no-em-dash-in-jsx-text",
|
|
@@ -35775,6 +36925,42 @@ const reactDoctorRules = [
|
|
|
35775
36925
|
tags: [...new Set(["react-native", ...expoNoNonInlinedEnv.tags ?? []])]
|
|
35776
36926
|
}
|
|
35777
36927
|
},
|
|
36928
|
+
{
|
|
36929
|
+
key: "react-doctor/firebase-client-owned-authz-field",
|
|
36930
|
+
id: "firebase-client-owned-authz-field",
|
|
36931
|
+
source: "react-doctor",
|
|
36932
|
+
originallyExternal: false,
|
|
36933
|
+
rule: {
|
|
36934
|
+
...firebaseClientOwnedAuthzField,
|
|
36935
|
+
framework: "global",
|
|
36936
|
+
category: "Security",
|
|
36937
|
+
tags: [...new Set(["security-scan", ...firebaseClientOwnedAuthzField.tags ?? []])]
|
|
36938
|
+
}
|
|
36939
|
+
},
|
|
36940
|
+
{
|
|
36941
|
+
key: "react-doctor/firebase-permissive-rules",
|
|
36942
|
+
id: "firebase-permissive-rules",
|
|
36943
|
+
source: "react-doctor",
|
|
36944
|
+
originallyExternal: false,
|
|
36945
|
+
rule: {
|
|
36946
|
+
...firebasePermissiveRules,
|
|
36947
|
+
framework: "global",
|
|
36948
|
+
category: "Security",
|
|
36949
|
+
tags: [...new Set(["security-scan", ...firebasePermissiveRules.tags ?? []])]
|
|
36950
|
+
}
|
|
36951
|
+
},
|
|
36952
|
+
{
|
|
36953
|
+
key: "react-doctor/firebase-query-filter-as-auth",
|
|
36954
|
+
id: "firebase-query-filter-as-auth",
|
|
36955
|
+
source: "react-doctor",
|
|
36956
|
+
originallyExternal: false,
|
|
36957
|
+
rule: {
|
|
36958
|
+
...firebaseQueryFilterAsAuth,
|
|
36959
|
+
framework: "global",
|
|
36960
|
+
category: "Security",
|
|
36961
|
+
tags: [...new Set(["security-scan", ...firebaseQueryFilterAsAuth.tags ?? []])]
|
|
36962
|
+
}
|
|
36963
|
+
},
|
|
35778
36964
|
{
|
|
35779
36965
|
key: "react-doctor/forbid-component-props",
|
|
35780
36966
|
id: "forbid-component-props",
|
|
@@ -35823,6 +37009,18 @@ const reactDoctorRules = [
|
|
|
35823
37009
|
requires: [...new Set(["react", ...forwardRefUsesRef.requires ?? []])]
|
|
35824
37010
|
}
|
|
35825
37011
|
},
|
|
37012
|
+
{
|
|
37013
|
+
key: "react-doctor/git-provider-url-injection-risk",
|
|
37014
|
+
id: "git-provider-url-injection-risk",
|
|
37015
|
+
source: "react-doctor",
|
|
37016
|
+
originallyExternal: false,
|
|
37017
|
+
rule: {
|
|
37018
|
+
...gitProviderUrlInjectionRisk,
|
|
37019
|
+
framework: "global",
|
|
37020
|
+
category: "Security",
|
|
37021
|
+
tags: [...new Set(["security-scan", ...gitProviderUrlInjectionRisk.tags ?? []])]
|
|
37022
|
+
}
|
|
37023
|
+
},
|
|
35826
37024
|
{
|
|
35827
37025
|
key: "react-doctor/heading-has-content",
|
|
35828
37026
|
id: "heading-has-content",
|
|
@@ -35940,6 +37138,30 @@ const reactDoctorRules = [
|
|
|
35940
37138
|
requires: [...new Set(["react", ...imgRedundantAlt.requires ?? []])]
|
|
35941
37139
|
}
|
|
35942
37140
|
},
|
|
37141
|
+
{
|
|
37142
|
+
key: "react-doctor/import-metadata-execution-risk",
|
|
37143
|
+
id: "import-metadata-execution-risk",
|
|
37144
|
+
source: "react-doctor",
|
|
37145
|
+
originallyExternal: false,
|
|
37146
|
+
rule: {
|
|
37147
|
+
...importMetadataExecutionRisk,
|
|
37148
|
+
framework: "global",
|
|
37149
|
+
category: "Security",
|
|
37150
|
+
tags: [...new Set(["security-scan", ...importMetadataExecutionRisk.tags ?? []])]
|
|
37151
|
+
}
|
|
37152
|
+
},
|
|
37153
|
+
{
|
|
37154
|
+
key: "react-doctor/insecure-crypto-risk",
|
|
37155
|
+
id: "insecure-crypto-risk",
|
|
37156
|
+
source: "react-doctor",
|
|
37157
|
+
originallyExternal: false,
|
|
37158
|
+
rule: {
|
|
37159
|
+
...insecureCryptoRisk,
|
|
37160
|
+
framework: "global",
|
|
37161
|
+
category: "Security",
|
|
37162
|
+
tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
|
|
37163
|
+
}
|
|
37164
|
+
},
|
|
35943
37165
|
{
|
|
35944
37166
|
key: "react-doctor/interactive-supports-focus",
|
|
35945
37167
|
id: "interactive-supports-focus",
|
|
@@ -36382,6 +37604,18 @@ const reactDoctorRules = [
|
|
|
36382
37604
|
requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
|
|
36383
37605
|
}
|
|
36384
37606
|
},
|
|
37607
|
+
{
|
|
37608
|
+
key: "react-doctor/key-lifecycle-risk",
|
|
37609
|
+
id: "key-lifecycle-risk",
|
|
37610
|
+
source: "react-doctor",
|
|
37611
|
+
originallyExternal: false,
|
|
37612
|
+
rule: {
|
|
37613
|
+
...keyLifecycleRisk,
|
|
37614
|
+
framework: "global",
|
|
37615
|
+
category: "Security",
|
|
37616
|
+
tags: [...new Set(["security-scan", ...keyLifecycleRisk.tags ?? []])]
|
|
37617
|
+
}
|
|
37618
|
+
},
|
|
36385
37619
|
{
|
|
36386
37620
|
key: "react-doctor/label-has-associated-control",
|
|
36387
37621
|
id: "label-has-associated-control",
|
|
@@ -36406,6 +37640,42 @@ const reactDoctorRules = [
|
|
|
36406
37640
|
requires: [...new Set(["react", ...lang.requires ?? []])]
|
|
36407
37641
|
}
|
|
36408
37642
|
},
|
|
37643
|
+
{
|
|
37644
|
+
key: "react-doctor/local-rpc-native-bridge-risk",
|
|
37645
|
+
id: "local-rpc-native-bridge-risk",
|
|
37646
|
+
source: "react-doctor",
|
|
37647
|
+
originallyExternal: false,
|
|
37648
|
+
rule: {
|
|
37649
|
+
...localRpcNativeBridgeRisk,
|
|
37650
|
+
framework: "global",
|
|
37651
|
+
category: "Security",
|
|
37652
|
+
tags: [...new Set(["security-scan", ...localRpcNativeBridgeRisk.tags ?? []])]
|
|
37653
|
+
}
|
|
37654
|
+
},
|
|
37655
|
+
{
|
|
37656
|
+
key: "react-doctor/mcp-tool-capability-risk",
|
|
37657
|
+
id: "mcp-tool-capability-risk",
|
|
37658
|
+
source: "react-doctor",
|
|
37659
|
+
originallyExternal: false,
|
|
37660
|
+
rule: {
|
|
37661
|
+
...mcpToolCapabilityRisk,
|
|
37662
|
+
framework: "global",
|
|
37663
|
+
category: "Security",
|
|
37664
|
+
tags: [...new Set(["security-scan", ...mcpToolCapabilityRisk.tags ?? []])]
|
|
37665
|
+
}
|
|
37666
|
+
},
|
|
37667
|
+
{
|
|
37668
|
+
key: "react-doctor/mdx-ssr-execution-risk",
|
|
37669
|
+
id: "mdx-ssr-execution-risk",
|
|
37670
|
+
source: "react-doctor",
|
|
37671
|
+
originallyExternal: false,
|
|
37672
|
+
rule: {
|
|
37673
|
+
...mdxSsrExecutionRisk,
|
|
37674
|
+
framework: "global",
|
|
37675
|
+
category: "Security",
|
|
37676
|
+
tags: [...new Set(["security-scan", ...mdxSsrExecutionRisk.tags ?? []])]
|
|
37677
|
+
}
|
|
37678
|
+
},
|
|
36409
37679
|
{
|
|
36410
37680
|
key: "react-doctor/media-has-caption",
|
|
36411
37681
|
id: "media-has-caption",
|
|
@@ -37951,6 +39221,18 @@ const reactDoctorRules = [
|
|
|
37951
39221
|
category: "Maintainability"
|
|
37952
39222
|
}
|
|
37953
39223
|
},
|
|
39224
|
+
{
|
|
39225
|
+
key: "react-doctor/nosql-injection-risk",
|
|
39226
|
+
id: "nosql-injection-risk",
|
|
39227
|
+
source: "react-doctor",
|
|
39228
|
+
originallyExternal: false,
|
|
39229
|
+
rule: {
|
|
39230
|
+
...nosqlInjectionRisk,
|
|
39231
|
+
framework: "global",
|
|
39232
|
+
category: "Security",
|
|
39233
|
+
tags: [...new Set(["security-scan", ...nosqlInjectionRisk.tags ?? []])]
|
|
39234
|
+
}
|
|
39235
|
+
},
|
|
37954
39236
|
{
|
|
37955
39237
|
key: "react-doctor/only-export-components",
|
|
37956
39238
|
id: "only-export-components",
|
|
@@ -37963,6 +39245,54 @@ const reactDoctorRules = [
|
|
|
37963
39245
|
requires: [...new Set(["react", ...onlyExportComponents.requires ?? []])]
|
|
37964
39246
|
}
|
|
37965
39247
|
},
|
|
39248
|
+
{
|
|
39249
|
+
key: "react-doctor/package-metadata-secret",
|
|
39250
|
+
id: "package-metadata-secret",
|
|
39251
|
+
source: "react-doctor",
|
|
39252
|
+
originallyExternal: false,
|
|
39253
|
+
rule: {
|
|
39254
|
+
...packageMetadataSecret,
|
|
39255
|
+
framework: "global",
|
|
39256
|
+
category: "Security",
|
|
39257
|
+
tags: [...new Set(["security-scan", ...packageMetadataSecret.tags ?? []])]
|
|
39258
|
+
}
|
|
39259
|
+
},
|
|
39260
|
+
{
|
|
39261
|
+
key: "react-doctor/path-traversal-risk",
|
|
39262
|
+
id: "path-traversal-risk",
|
|
39263
|
+
source: "react-doctor",
|
|
39264
|
+
originallyExternal: false,
|
|
39265
|
+
rule: {
|
|
39266
|
+
...pathTraversalRisk,
|
|
39267
|
+
framework: "global",
|
|
39268
|
+
category: "Security",
|
|
39269
|
+
tags: [...new Set(["security-scan", ...pathTraversalRisk.tags ?? []])]
|
|
39270
|
+
}
|
|
39271
|
+
},
|
|
39272
|
+
{
|
|
39273
|
+
key: "react-doctor/plugin-update-trust-risk",
|
|
39274
|
+
id: "plugin-update-trust-risk",
|
|
39275
|
+
source: "react-doctor",
|
|
39276
|
+
originallyExternal: false,
|
|
39277
|
+
rule: {
|
|
39278
|
+
...pluginUpdateTrustRisk,
|
|
39279
|
+
framework: "global",
|
|
39280
|
+
category: "Security",
|
|
39281
|
+
tags: [...new Set(["security-scan", ...pluginUpdateTrustRisk.tags ?? []])]
|
|
39282
|
+
}
|
|
39283
|
+
},
|
|
39284
|
+
{
|
|
39285
|
+
key: "react-doctor/postmessage-origin-risk",
|
|
39286
|
+
id: "postmessage-origin-risk",
|
|
39287
|
+
source: "react-doctor",
|
|
39288
|
+
originallyExternal: false,
|
|
39289
|
+
rule: {
|
|
39290
|
+
...postmessageOriginRisk,
|
|
39291
|
+
framework: "global",
|
|
39292
|
+
category: "Security",
|
|
39293
|
+
tags: [...new Set(["security-scan", ...postmessageOriginRisk.tags ?? []])]
|
|
39294
|
+
}
|
|
39295
|
+
},
|
|
37966
39296
|
{
|
|
37967
39297
|
key: "react-doctor/preact-no-children-length",
|
|
37968
39298
|
id: "preact-no-children-length",
|
|
@@ -38158,6 +39488,30 @@ const reactDoctorRules = [
|
|
|
38158
39488
|
requires: [...new Set(["react", ...preferUseReducer.requires ?? []])]
|
|
38159
39489
|
}
|
|
38160
39490
|
},
|
|
39491
|
+
{
|
|
39492
|
+
key: "react-doctor/public-debug-artifact",
|
|
39493
|
+
id: "public-debug-artifact",
|
|
39494
|
+
source: "react-doctor",
|
|
39495
|
+
originallyExternal: false,
|
|
39496
|
+
rule: {
|
|
39497
|
+
...publicDebugArtifact,
|
|
39498
|
+
framework: "global",
|
|
39499
|
+
category: "Security",
|
|
39500
|
+
tags: [...new Set(["security-scan", ...publicDebugArtifact.tags ?? []])]
|
|
39501
|
+
}
|
|
39502
|
+
},
|
|
39503
|
+
{
|
|
39504
|
+
key: "react-doctor/public-env-secret-name",
|
|
39505
|
+
id: "public-env-secret-name",
|
|
39506
|
+
source: "react-doctor",
|
|
39507
|
+
originallyExternal: false,
|
|
39508
|
+
rule: {
|
|
39509
|
+
...publicEnvSecretName,
|
|
39510
|
+
framework: "global",
|
|
39511
|
+
category: "Security",
|
|
39512
|
+
tags: [...new Set(["security-scan", ...publicEnvSecretName.tags ?? []])]
|
|
39513
|
+
}
|
|
39514
|
+
},
|
|
38161
39515
|
{
|
|
38162
39516
|
key: "react-doctor/query-destructure-result",
|
|
38163
39517
|
id: "query-destructure-result",
|
|
@@ -38235,6 +39589,18 @@ const reactDoctorRules = [
|
|
|
38235
39589
|
category: "Bugs"
|
|
38236
39590
|
}
|
|
38237
39591
|
},
|
|
39592
|
+
{
|
|
39593
|
+
key: "react-doctor/raw-sql-injection-risk",
|
|
39594
|
+
id: "raw-sql-injection-risk",
|
|
39595
|
+
source: "react-doctor",
|
|
39596
|
+
originallyExternal: false,
|
|
39597
|
+
rule: {
|
|
39598
|
+
...rawSqlInjectionRisk,
|
|
39599
|
+
framework: "global",
|
|
39600
|
+
category: "Security",
|
|
39601
|
+
tags: [...new Set(["security-scan", ...rawSqlInjectionRisk.tags ?? []])]
|
|
39602
|
+
}
|
|
39603
|
+
},
|
|
38238
39604
|
{
|
|
38239
39605
|
key: "react-doctor/react-compiler-no-manual-memoization",
|
|
38240
39606
|
id: "react-compiler-no-manual-memoization",
|
|
@@ -38376,6 +39742,18 @@ const reactDoctorRules = [
|
|
|
38376
39742
|
requires: [...new Set(["react", ...renderingUsetransitionLoading.requires ?? []])]
|
|
38377
39743
|
}
|
|
38378
39744
|
},
|
|
39745
|
+
{
|
|
39746
|
+
key: "react-doctor/repository-secret-file",
|
|
39747
|
+
id: "repository-secret-file",
|
|
39748
|
+
source: "react-doctor",
|
|
39749
|
+
originallyExternal: false,
|
|
39750
|
+
rule: {
|
|
39751
|
+
...repositorySecretFile,
|
|
39752
|
+
framework: "global",
|
|
39753
|
+
category: "Security",
|
|
39754
|
+
tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
|
|
39755
|
+
}
|
|
39756
|
+
},
|
|
38379
39757
|
{
|
|
38380
39758
|
key: "react-doctor/require-render-return",
|
|
38381
39759
|
id: "require-render-return",
|
|
@@ -39096,6 +40474,42 @@ const reactDoctorRules = [
|
|
|
39096
40474
|
requires: [...new Set(["react", ...stylePropObject.requires ?? []])]
|
|
39097
40475
|
}
|
|
39098
40476
|
},
|
|
40477
|
+
{
|
|
40478
|
+
key: "react-doctor/supabase-client-owned-authz-field",
|
|
40479
|
+
id: "supabase-client-owned-authz-field",
|
|
40480
|
+
source: "react-doctor",
|
|
40481
|
+
originallyExternal: false,
|
|
40482
|
+
rule: {
|
|
40483
|
+
...supabaseClientOwnedAuthzField,
|
|
40484
|
+
framework: "global",
|
|
40485
|
+
category: "Security",
|
|
40486
|
+
tags: [...new Set(["security-scan", ...supabaseClientOwnedAuthzField.tags ?? []])]
|
|
40487
|
+
}
|
|
40488
|
+
},
|
|
40489
|
+
{
|
|
40490
|
+
key: "react-doctor/supabase-rls-policy-risk",
|
|
40491
|
+
id: "supabase-rls-policy-risk",
|
|
40492
|
+
source: "react-doctor",
|
|
40493
|
+
originallyExternal: false,
|
|
40494
|
+
rule: {
|
|
40495
|
+
...supabaseRlsPolicyRisk,
|
|
40496
|
+
framework: "global",
|
|
40497
|
+
category: "Security",
|
|
40498
|
+
tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
|
|
40499
|
+
}
|
|
40500
|
+
},
|
|
40501
|
+
{
|
|
40502
|
+
key: "react-doctor/svg-filter-clickjacking-risk",
|
|
40503
|
+
id: "svg-filter-clickjacking-risk",
|
|
40504
|
+
source: "react-doctor",
|
|
40505
|
+
originallyExternal: false,
|
|
40506
|
+
rule: {
|
|
40507
|
+
...svgFilterClickjackingRisk,
|
|
40508
|
+
framework: "global",
|
|
40509
|
+
category: "Security",
|
|
40510
|
+
tags: [...new Set(["security-scan", ...svgFilterClickjackingRisk.tags ?? []])]
|
|
40511
|
+
}
|
|
40512
|
+
},
|
|
39099
40513
|
{
|
|
39100
40514
|
key: "react-doctor/tabindex-no-positive",
|
|
39101
40515
|
id: "tabindex-no-positive",
|
|
@@ -39262,6 +40676,42 @@ const reactDoctorRules = [
|
|
|
39262
40676
|
category: "Bugs"
|
|
39263
40677
|
}
|
|
39264
40678
|
},
|
|
40679
|
+
{
|
|
40680
|
+
key: "react-doctor/tenant-static-proxy-risk",
|
|
40681
|
+
id: "tenant-static-proxy-risk",
|
|
40682
|
+
source: "react-doctor",
|
|
40683
|
+
originallyExternal: false,
|
|
40684
|
+
rule: {
|
|
40685
|
+
...tenantStaticProxyRisk,
|
|
40686
|
+
framework: "global",
|
|
40687
|
+
category: "Security",
|
|
40688
|
+
tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
|
|
40689
|
+
}
|
|
40690
|
+
},
|
|
40691
|
+
{
|
|
40692
|
+
key: "react-doctor/untrusted-redirect-following",
|
|
40693
|
+
id: "untrusted-redirect-following",
|
|
40694
|
+
source: "react-doctor",
|
|
40695
|
+
originallyExternal: false,
|
|
40696
|
+
rule: {
|
|
40697
|
+
...untrustedRedirectFollowing,
|
|
40698
|
+
framework: "global",
|
|
40699
|
+
category: "Security",
|
|
40700
|
+
tags: [...new Set(["security-scan", ...untrustedRedirectFollowing.tags ?? []])]
|
|
40701
|
+
}
|
|
40702
|
+
},
|
|
40703
|
+
{
|
|
40704
|
+
key: "react-doctor/url-prefilled-privileged-action",
|
|
40705
|
+
id: "url-prefilled-privileged-action",
|
|
40706
|
+
source: "react-doctor",
|
|
40707
|
+
originallyExternal: false,
|
|
40708
|
+
rule: {
|
|
40709
|
+
...urlPrefilledPrivilegedAction,
|
|
40710
|
+
framework: "global",
|
|
40711
|
+
category: "Security",
|
|
40712
|
+
tags: [...new Set(["security-scan", ...urlPrefilledPrivilegedAction.tags ?? []])]
|
|
40713
|
+
}
|
|
40714
|
+
},
|
|
39265
40715
|
{
|
|
39266
40716
|
key: "react-doctor/use-lazy-motion",
|
|
39267
40717
|
id: "use-lazy-motion",
|
|
@@ -39285,6 +40735,18 @@ const reactDoctorRules = [
|
|
|
39285
40735
|
requires: [...new Set(["react", ...voidDomElementsNoChildren.requires ?? []])]
|
|
39286
40736
|
}
|
|
39287
40737
|
},
|
|
40738
|
+
{
|
|
40739
|
+
key: "react-doctor/webhook-signature-risk",
|
|
40740
|
+
id: "webhook-signature-risk",
|
|
40741
|
+
source: "react-doctor",
|
|
40742
|
+
originallyExternal: false,
|
|
40743
|
+
rule: {
|
|
40744
|
+
...webhookSignatureRisk,
|
|
40745
|
+
framework: "global",
|
|
40746
|
+
category: "Security",
|
|
40747
|
+
tags: [...new Set(["security-scan", ...webhookSignatureRisk.tags ?? []])]
|
|
40748
|
+
}
|
|
40749
|
+
},
|
|
39288
40750
|
{
|
|
39289
40751
|
key: "react-doctor/zod-v4-no-deprecated-error-apis",
|
|
39290
40752
|
id: "zod-v4-no-deprecated-error-apis",
|
|
@@ -39855,7 +41317,8 @@ const toKeyedSeverity = (entries) => entries.map((entry) => ({
|
|
|
39855
41317
|
severity: entry.rule.severity
|
|
39856
41318
|
}));
|
|
39857
41319
|
const isRecommendedByDefault = (entry) => entry.rule.defaultEnabled !== false;
|
|
39858
|
-
const
|
|
41320
|
+
const isScanRule = (entry) => entry.rule.scan !== void 0;
|
|
41321
|
+
const collectReactDoctorRulesByFramework = (frameworkName) => reactDoctorRules.filter((entry) => entry.rule.framework === frameworkName && isRecommendedByDefault(entry) && !isScanRule(entry));
|
|
39859
41322
|
const collectExternalRulesBySource = (source) => EXTERNAL_RULES.filter((rule) => rule.source === source);
|
|
39860
41323
|
const collectFrameworkSpecificRuleKeys = () => {
|
|
39861
41324
|
const collected = /* @__PURE__ */ new Set();
|
|
@@ -39952,14 +41415,36 @@ const REACT_NATIVE_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFr
|
|
|
39952
41415
|
const TANSTACK_START_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-start")));
|
|
39953
41416
|
const TANSTACK_QUERY_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-query")));
|
|
39954
41417
|
const PREACT_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("preact")));
|
|
39955
|
-
const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES));
|
|
41418
|
+
const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES.filter((entry) => !isScanRule(entry))));
|
|
39956
41419
|
const ALL_REACT_DOCTOR_RULE_KEYS = new Set(REACT_DOCTOR_RULES.map((rule) => rule.key));
|
|
39957
41420
|
const FRAMEWORK_SPECIFIC_RULE_KEYS = collectFrameworkSpecificRuleKeys();
|
|
39958
41421
|
const REACT_COMPILER_RULES = toRuleMap(collectExternalRulesBySource("react-compiler"));
|
|
39959
41422
|
//#endregion
|
|
41423
|
+
//#region src/plugin/rules/security-scan/utils/is-probably-text-file.ts
|
|
41424
|
+
const isProbablyTextFile = (relativePath) => TEXT_FILE_PATTERN.test(relativePath) || DOTENV_FILE_PATTERN.test(relativePath);
|
|
41425
|
+
//#endregion
|
|
41426
|
+
//#region src/plugin/rules/security-scan/utils/classify-security-scan-file.ts
|
|
41427
|
+
const classifySecurityScanFile = (relativePath) => {
|
|
41428
|
+
const isGeneratedBundleByName = GENERATED_BUNDLE_FILE_PATTERN.test(relativePath);
|
|
41429
|
+
if (isRepositorySecretFilePath(relativePath) || isSqlPath(relativePath) || isFirebaseRulesPath(relativePath) || isConfigOrCiPath(relativePath)) return {
|
|
41430
|
+
bucket: "priority",
|
|
41431
|
+
isGeneratedBundleByName
|
|
41432
|
+
};
|
|
41433
|
+
if (isBrowserArtifactPath(relativePath, isGeneratedBundleByName)) return {
|
|
41434
|
+
bucket: "artifact",
|
|
41435
|
+
isGeneratedBundleByName
|
|
41436
|
+
};
|
|
41437
|
+
if (isProbablyTextFile(relativePath)) return {
|
|
41438
|
+
bucket: "other",
|
|
41439
|
+
isGeneratedBundleByName
|
|
41440
|
+
};
|
|
41441
|
+
return null;
|
|
41442
|
+
};
|
|
41443
|
+
const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGeneratedBundle || isProbablyTextFile(relativePath) || isConfigOrCiPath(relativePath) || isRepositorySecretFilePath(relativePath);
|
|
41444
|
+
//#endregion
|
|
39960
41445
|
//#region src/index.ts
|
|
39961
41446
|
var src_default = plugin;
|
|
39962
41447
|
//#endregion
|
|
39963
|
-
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, src_default as default, isReactNativeDependencyName };
|
|
41448
|
+
export { ALL_REACT_DOCTOR_RULES, ALL_REACT_DOCTOR_RULE_KEYS, EXTERNAL_RULES, FRAMEWORK_SPECIFIC_RULE_KEYS, MOTION_LIBRARY_PACKAGES, NEXTJS_RULES, PREACT_RULES, REACT_COMPILER_RULES, REACT_DOCTOR_RULES, REACT_NATIVE_DEPENDENCY_NAMES, REACT_NATIVE_DEPENDENCY_PREFIXES, REACT_NATIVE_RULES, RECOMMENDED_RULES, RULES, TANSTACK_QUERY_RULES, TANSTACK_START_RULES, classifySecurityScanFile, src_default as default, isReactNativeDependencyName, shouldReadSecurityScanContent };
|
|
39964
41449
|
|
|
39965
41450
|
//# sourceMappingURL=index.js.map
|