oxlint-plugin-react-doctor 0.5.3 → 0.5.4-dev.e90eb7a

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -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
- const create = rule.create;
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
- let parsedProgram = null;
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$/;
@@ -21885,9 +22559,10 @@ const TANSTACK_ROUTE_CREATION_FUNCTIONS = new Set([
21885
22559
  "createRootRouteWithContext"
21886
22560
  ]);
21887
22561
  const TANSTACK_SERVER_FN_NAMES = new Set(["createServerFn"]);
22562
+ const TANSTACK_INPUT_VALIDATOR_METHOD_NAMES = new Set(["validator", "inputValidator"]);
21888
22563
  const TANSTACK_MIDDLEWARE_METHOD_ORDER = [
21889
22564
  "middleware",
21890
- "inputValidator",
22565
+ "validator",
21891
22566
  "client",
21892
22567
  "server",
21893
22568
  "handler"
@@ -24521,6 +25196,17 @@ const noZIndex9999 = defineRule({
24521
25196
  }
24522
25197
  })
24523
25198
  });
25199
+ const nosqlInjectionRisk = defineRule({
25200
+ id: "nosql-injection-risk",
25201
+ title: "NoSQL query accepts operator-shaped input",
25202
+ severity: "warn",
25203
+ recommendation: "Coerce scalar fields before querying, reject operator keys from client input, and avoid `$where` or request-derived regexes.",
25204
+ scan: scanByPattern({
25205
+ shouldScan: (file) => isProductionFilePath(file.relativePath, DATABASE_SOURCE_FILE_PATTERN),
25206
+ 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,
25207
+ message: "Code appears to pass raw JSON, regex, or `$where` style input into a NoSQL query."
25208
+ })
25209
+ });
24524
25210
  //#endregion
24525
25211
  //#region src/plugin/utils/is-framework-route-or-special-filename.ts
24526
25212
  const sourceFileExtensionGroup = NEXTJS_SOURCE_FILE_EXTENSION_GROUP;
@@ -25073,6 +25759,116 @@ const onlyExportComponents = defineRule({
25073
25759
  }
25074
25760
  });
25075
25761
  //#endregion
25762
+ //#region src/plugin/rules/security-scan/package-metadata-secret.ts
25763
+ const packageMetadataSecret = defineRule({
25764
+ id: "package-metadata-secret",
25765
+ title: "Secret-like package metadata",
25766
+ severity: "warn",
25767
+ recommendation: "Keep secrets out of package metadata and generated reports; they are often published to registries, logs, or browser artifacts.",
25768
+ scan: (file) => {
25769
+ if (!file.relativePath.endsWith("package.json")) return [];
25770
+ const pattern = findSuspiciousPublicEnvSecretNamePattern(file.content) ?? SECRET_VALUE_PATTERNS.find((candidate) => candidate.test(file.content));
25771
+ if (pattern === void 0) return [];
25772
+ const location = getMatchLocation(file.content, pattern);
25773
+ return [{
25774
+ message: "Package metadata contains secret-like values or public env secret names.",
25775
+ line: location.line,
25776
+ column: location.column
25777
+ }];
25778
+ }
25779
+ });
25780
+ const pathTraversalRisk = defineRule({
25781
+ id: "path-traversal-risk",
25782
+ title: "Filesystem path uses caller input",
25783
+ severity: "warn",
25784
+ recommendation: "Resolve paths against a fixed base directory, reject traversal after normalization, and map user-visible identifiers to server-owned paths.",
25785
+ scan: scanByPattern({
25786
+ shouldScan: (file) => isProductionSourcePath(file.relativePath) && !isDevToolingPath(file.relativePath),
25787
+ 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\.)/,
25788
+ message: "Filesystem access appears to use request, query, params, or body data as part of the path."
25789
+ })
25790
+ });
25791
+ //#endregion
25792
+ //#region src/plugin/rules/security-scan/plugin-update-trust-risk.ts
25793
+ 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;
25794
+ const CHECKSUM_VERIFICATION_PATTERN = /sha(?:256|512|1)sum|--checksum|checksum=|EXPECTED_SHA|gpg\s+--verify|\.sha(?:256|512)\b/i;
25795
+ 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*\(/;
25796
+ const pluginUpdateTrustRisk = defineRule({
25797
+ id: "plugin-update-trust-risk",
25798
+ title: "Plugin or updater trust boundary risk",
25799
+ severity: "warn",
25800
+ recommendation: "Require signed updates/plugins, pin trusted repositories, verify hashes before execution, and keep custom repository installs behind explicit warnings.",
25801
+ scan: (file) => {
25802
+ if (!isProductionSourcePath(file.relativePath) && !isConfigOrCiPath(file.relativePath)) return [];
25803
+ const content = getScannableContent(file);
25804
+ if (!UPDATER_TRUST_PATTERN.test(content)) return [];
25805
+ if (CHECKSUM_VERIFICATION_PATTERN.test(content)) return [];
25806
+ if (SOURCE_FILE_PATTERN.test(file.relativePath) && !EXECUTION_CONTEXT_PATTERN.test(content)) return [];
25807
+ const location = getMatchLocation(content, UPDATER_TRUST_PATTERN);
25808
+ return [{
25809
+ message: "Code appears to download, install, update, or execute plugin/updater content across a trust boundary.",
25810
+ line: location.line,
25811
+ column: location.column
25812
+ }];
25813
+ }
25814
+ });
25815
+ //#endregion
25816
+ //#region src/plugin/rules/security-scan/postmessage-origin-risk.ts
25817
+ const POSTMESSAGE_ORIGIN_CHECK_PATTERN = /origin(?!al)|\.source\s*[!=]==?/i;
25818
+ const MESSAGE_DATA_READ_PATTERN = /\b(?:event|e|evt|msg|message)\.data\b/;
25819
+ const SAME_APPLICATION_CHANNEL_TARGET_PATTERN = /port\d?\b|worker|channel|broadcast|socket|\bws\b|\bsse\b|eventsource|^self\.|^source\./i;
25820
+ const WORKER_FILE_PATH_PATTERN = /worker/i;
25821
+ const getNodeStartIndex = (node) => "start" in node && typeof node.start === "number" ? node.start : -1;
25822
+ const getNodeText = (content, node) => {
25823
+ const startIndex = getNodeStartIndex(node);
25824
+ const endIndex = "end" in node && typeof node.end === "number" ? node.end : -1;
25825
+ if (startIndex < 0 || endIndex < 0) return "";
25826
+ return content.slice(startIndex, endIndex);
25827
+ };
25828
+ const getMessageHandlerTarget = (content, node) => {
25829
+ if (node.type === "CallExpression") {
25830
+ const calleeText = isAstNode(node.callee) ? getNodeText(content, node.callee) : "";
25831
+ if (!calleeText.endsWith("addEventListener")) return null;
25832
+ const firstArgument = node.arguments[0];
25833
+ return isAstNode(firstArgument) && firstArgument.type === "Literal" && firstArgument.value === "message" ? calleeText : null;
25834
+ }
25835
+ if (node.type === "AssignmentExpression" && isAstNode(node.left)) {
25836
+ const leftText = getNodeText(content, node.left);
25837
+ return leftText.endsWith(".onmessage") ? leftText : null;
25838
+ }
25839
+ return null;
25840
+ };
25841
+ const postmessageOriginRisk = defineRule({
25842
+ id: "postmessage-origin-risk",
25843
+ title: "postMessage handler without origin check",
25844
+ severity: "warn",
25845
+ recommendation: "Validate `event.origin` against an exact allowlist before using `event.data`, especially when an iframe or parent window can be attacker-controlled.",
25846
+ scan: (file) => {
25847
+ if (!isProductionSourcePath(file.relativePath)) return [];
25848
+ if (WORKER_FILE_PATH_PATTERN.test(file.relativePath)) return [];
25849
+ const ast = parseSourceText(file.absolutePath, file.content);
25850
+ if (ast === null) return [];
25851
+ const findings = [];
25852
+ walkAst(ast, (node) => {
25853
+ const targetText = getMessageHandlerTarget(file.content, node);
25854
+ if (targetText === null) return;
25855
+ if (SAME_APPLICATION_CHANNEL_TARGET_PATTERN.test(targetText)) return;
25856
+ const nodeText = getNodeText(file.content, node);
25857
+ const messageDataIndex = nodeText.search(MESSAGE_DATA_READ_PATTERN);
25858
+ if (messageDataIndex < 0) return;
25859
+ const originCheckIndex = nodeText.search(POSTMESSAGE_ORIGIN_CHECK_PATTERN);
25860
+ if (originCheckIndex >= 0 && originCheckIndex < messageDataIndex) return;
25861
+ const location = getLocationAtIndex(file.content, getNodeStartIndex(node));
25862
+ findings.push({
25863
+ message: "A message event handler reads cross-window messages without an obvious origin check.",
25864
+ line: location.line,
25865
+ column: location.column
25866
+ });
25867
+ });
25868
+ return findings;
25869
+ }
25870
+ });
25871
+ //#endregion
25076
25872
  //#region src/plugin/rules/preact/preact-no-children-length.ts
25077
25873
  const ARRAY_READ_METHOD_NAMES = new Set([
25078
25874
  "length",
@@ -26193,6 +26989,51 @@ const preferUseReducer = defineRule({
26193
26989
  }
26194
26990
  });
26195
26991
  //#endregion
26992
+ //#region src/plugin/rules/security-scan/utils/is-public-debug-artifact-path.ts
26993
+ const LOCALE_DIRECTORY_PATTERN = /(?:^|\/)(?:locales?|i18n|lang|langs|translations?)\//i;
26994
+ 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);
26995
+ //#endregion
26996
+ //#region src/plugin/rules/security-scan/public-debug-artifact.ts
26997
+ const publicDebugArtifact = defineRule({
26998
+ id: "public-debug-artifact",
26999
+ title: "Public debug artifact",
27000
+ severity: "warn",
27001
+ recommendation: "Remove debug artifacts from public output; logs and dumps often reveal source paths, internal routes, tokens, or environment snapshots.",
27002
+ scan: (file) => {
27003
+ if (!isPublicDebugArtifactPath(file.relativePath)) return [];
27004
+ const finding = {
27005
+ message: "A browser-reachable debug, log, dump, report, or env artifact is present.",
27006
+ line: 1,
27007
+ column: 1
27008
+ };
27009
+ return [SECRET_VALUE_PATTERNS.some((pattern) => pattern.test(file.content)) ? {
27010
+ ...finding,
27011
+ severity: "error"
27012
+ } : finding];
27013
+ }
27014
+ });
27015
+ //#endregion
27016
+ //#region src/plugin/rules/security-scan/public-env-secret-name.ts
27017
+ const DOCS_DIRECTORY_PATTERN = /(?:^|\/)docs?\//i;
27018
+ const publicEnvSecretName = defineRule({
27019
+ id: "public-env-secret-name",
27020
+ title: "Secret-like public env variable",
27021
+ severity: "warn",
27022
+ 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.",
27023
+ scan: (file) => {
27024
+ if (!isClientSourcePath(file.relativePath)) return [];
27025
+ if (DOCS_DIRECTORY_PATTERN.test(file.relativePath)) return [];
27026
+ const pattern = findSuspiciousPublicEnvSecretNamePattern(file.content);
27027
+ if (pattern === void 0) return [];
27028
+ const location = getMatchLocation(file.content, pattern);
27029
+ return [{
27030
+ message: "Client code references a public env variable whose name looks like a secret or privileged credential.",
27031
+ line: location.line,
27032
+ column: location.column
27033
+ }];
27034
+ }
27035
+ });
27036
+ //#endregion
26196
27037
  //#region src/plugin/rules/tanstack-query/query-destructure-result.ts
26197
27038
  const queryDestructureResult = defineRule({
26198
27039
  id: "query-destructure-result",
@@ -26385,6 +27226,30 @@ const queryStableQueryClient = defineRule({
26385
27226
  };
26386
27227
  }
26387
27228
  });
27229
+ const rawSqlInjectionRisk = defineRule({
27230
+ id: "raw-sql-injection-risk",
27231
+ title: "Raw SQL built outside parameter binding",
27232
+ severity: "warn",
27233
+ recommendation: "Keep user input in driver parameters or ORM bind variables. Avoid unsafe/raw SQL helpers and string interpolation for queries.",
27234
+ scan: scanByPattern({
27235
+ shouldScan: (file) => isProductionScriptSourcePath(file.relativePath),
27236
+ pattern: [
27237
+ /\$queryRawUnsafe\s*\(/,
27238
+ /\$executeRawUnsafe\s*\(/,
27239
+ /\bPrisma\.raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,
27240
+ /\bsql\.\s*(?:raw|unsafe)\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,
27241
+ /\b(?:client|pool|conn)\.query\s*\(\s*['"`]\s*(?:SELECT|INSERT|UPDATE|DELETE)\b[^)]{0,400}\$\{(?!\s*[\w$.]*(?:sanitiz|escape|quote)[\w$]*\s*\()/i,
27242
+ /\.query\s*\(\s*['"`][^'"`]{0,200}['"`]\s*\+/,
27243
+ /\.(?:where|orderBy|having)Raw\s*\((?!\s*(?:["'][^"'\n]*["']\s*[,)]|`[^`$]*`))/,
27244
+ /\bcursor\.execute\s*\(\s*f['"]/,
27245
+ /\bcursor\.execute\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*(?:%|\.format\s*\(|\+)/,
27246
+ /\b(?:engine|session)\.execute\s*\(\s*(?:text\s*\(\s*)?f['"]/,
27247
+ /\$[\w]+->(?:query|exec|prepare|executeQuery|executeStatement|createQuery|createNativeQuery)\s*\(\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/,
27248
+ /mysqli_query\s*\(\s*[^,]+,\s*(?:"[^"]{0,400}"|'[^']{0,400}')\s*\.\s*\$/
27249
+ ],
27250
+ message: "Code uses a raw SQL escape hatch or string-built query shape that can bypass parameter binding."
27251
+ })
27252
+ });
26388
27253
  //#endregion
26389
27254
  //#region src/plugin/rules/architecture/react-compiler-no-manual-memoization.ts
26390
27255
  const REMOVAL_MESSAGE_BY_REACT_API_NAME = new Map([
@@ -27015,6 +27880,31 @@ const renderingUsetransitionLoading = defineRule({
27015
27880
  } })
27016
27881
  });
27017
27882
  //#endregion
27883
+ //#region src/plugin/rules/security-scan/utils/is-repository-secret-file-path.ts
27884
+ 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);
27885
+ //#endregion
27886
+ //#region src/plugin/rules/security-scan/repository-secret-file.ts
27887
+ const isRepositorySecretExamplePath = (relativePath) => /(?:^|\/)\.env\.(?:example|sample|template|dist|defaults?)$|(?:^|\/)[^/]*(?:example|sample|template)[^/]*\.(?:env|json|pem|key)$/i.test(relativePath);
27888
+ const repositorySecretFile = defineRule({
27889
+ id: "repository-secret-file",
27890
+ title: "Secret file checked into repository",
27891
+ severity: "error",
27892
+ recommendation: "Remove committed env files, service-account credentials, npm auth tokens, and webhook URLs; rotate exposed values and keep only redacted examples in source.",
27893
+ scan: (file) => {
27894
+ if (!isRepositorySecretFilePath(file.relativePath)) return [];
27895
+ if (isRepositorySecretExamplePath(file.relativePath)) return [];
27896
+ if (TEST_CONTEXT_PATTERN.test(file.relativePath)) return [];
27897
+ const pattern = SECRET_VALUE_PATTERNS.find((candidate) => candidate.test(file.content)) ?? findSuspiciousPublicEnvSecretNamePattern(file.content);
27898
+ if (pattern === void 0) return [];
27899
+ const location = getMatchLocation(file.content, pattern);
27900
+ return [{
27901
+ message: "A repository credential/config file contains secret-looking values.",
27902
+ line: location.line,
27903
+ column: location.column
27904
+ }];
27905
+ }
27906
+ });
27907
+ //#endregion
27018
27908
  //#region src/plugin/utils/function-body-has-return-with-value.ts
27019
27909
  const functionBodyHasReturnWithValue = (functionNode) => {
27020
27910
  if (functionNode.type === "ArrowFunctionExpression" && "body" in functionNode) {
@@ -34236,6 +35126,50 @@ const stylePropObject = defineRule({
34236
35126
  };
34237
35127
  }
34238
35128
  });
35129
+ const supabaseClientOwnedAuthzField = defineRule({
35130
+ id: "supabase-client-owned-authz-field",
35131
+ title: "Client writes Supabase authorization field",
35132
+ severity: "error",
35133
+ recommendation: "Use RLS policies based on `auth.uid()` and server-owned membership rows; do not trust client-provided owner, org, or role columns.",
35134
+ scan: scanByPattern({
35135
+ shouldScan: (file) => isClientSourcePath(file.relativePath),
35136
+ 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/,
35137
+ 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],
35138
+ message: "Client Supabase code appears to write user, tenant, owner, or role fields that should be enforced by RLS."
35139
+ })
35140
+ });
35141
+ //#endregion
35142
+ //#region src/plugin/rules/security-scan/utils/is-sql-path.ts
35143
+ const isSqlPath = (relativePath) => relativePath.endsWith(".sql") || /(?:^|\/)supabase\/(?:migrations|schemas)\//.test(relativePath);
35144
+ const supabaseRlsPolicyRisk = defineRule({
35145
+ id: "supabase-rls-policy-risk",
35146
+ title: "Permissive Supabase RLS policy",
35147
+ severity: "error",
35148
+ recommendation: "Keep public-read policies explicit, but gate inserts, updates, deletes, and service-role bypasses behind `auth.uid()` plus trusted tenant membership.",
35149
+ scan: scanByPattern({
35150
+ shouldScan: (file) => isSqlPath(file.relativePath),
35151
+ pattern: [
35152
+ /disable\s+row\s+level\s+security/i,
35153
+ /create\s+policy[\s\S]{0,700}auth\.role\(\)\s*=\s*["']service_role["']/i,
35154
+ /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,
35155
+ /create\s+policy(?:(?!\bfor\s+select\b)[\s\S]){0,700}\b(?:using|with\s+check)\s*\(\s*true\s*\)/i
35156
+ ],
35157
+ message: "Supabase policy SQL disables RLS, permits writes broadly, or references a service-role bypass."
35158
+ })
35159
+ });
35160
+ //#endregion
35161
+ //#region src/plugin/rules/security-scan/svg-filter-clickjacking-risk.ts
35162
+ const svgFilterClickjackingRisk = defineRule({
35163
+ id: "svg-filter-clickjacking-risk",
35164
+ title: "SVG-filtered iframe clickjacking primitive",
35165
+ severity: "warn",
35166
+ recommendation: "Avoid filtering cross-origin iframes. Use `frame-ancestors` on sensitive pages and keep SVG filters away from embedded privileged UI.",
35167
+ scan: scanByPattern({
35168
+ shouldScan: (file) => isProductionSourcePath(file.relativePath),
35169
+ 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,
35170
+ message: "An iframe is rendered through an SVG/CSS filter, which can support advanced clickjacking or visual exfiltration tricks."
35171
+ })
35172
+ });
34239
35173
  //#endregion
34240
35174
  //#region src/plugin/rules/a11y/tabindex-no-positive.ts
34241
35175
  const MESSAGE = "Keyboard users get jumped out of the normal order by a positive `tabIndex`, so use `0` or `-1`.";
@@ -34267,7 +35201,7 @@ const walkServerFnChain = (outerNode) => {
34267
35201
  const result = {
34268
35202
  isServerFnChain: false,
34269
35203
  specifiedMethod: null,
34270
- hasInputValidator: false
35204
+ hasInputValidation: false
34271
35205
  };
34272
35206
  if (!isNodeOfType(outerNode, "CallExpression")) return result;
34273
35207
  if (!isNodeOfType(outerNode.callee, "MemberExpression")) return result;
@@ -34281,7 +35215,7 @@ const walkServerFnChain = (outerNode) => {
34281
35215
  for (const property of optionsArgument.properties ?? []) if (isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "method" && isNodeOfType(property.value, "Literal") && typeof property.value.value === "string") result.specifiedMethod = property.value.value;
34282
35216
  }
34283
35217
  }
34284
- if (calleeName === "inputValidator") result.hasInputValidator = true;
35218
+ if (calleeName && TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(calleeName)) result.hasInputValidation = true;
34285
35219
  if (isNodeOfType(currentNode.callee, "MemberExpression")) currentNode = currentNode.callee.object;
34286
35220
  else break;
34287
35221
  }
@@ -34835,13 +35769,14 @@ const tanstackStartRoutePropertyOrder = defineRule({
34835
35769
  });
34836
35770
  //#endregion
34837
35771
  //#region src/plugin/rules/tanstack-start/tanstack-start-server-fn-method-order.ts
35772
+ const toMethodOrderToken = (methodName) => TANSTACK_INPUT_VALIDATOR_METHOD_NAMES.has(methodName) ? "validator" : methodName;
34838
35773
  const tanstackStartServerFnMethodOrder = defineRule({
34839
35774
  id: "tanstack-start-server-fn-method-order",
34840
35775
  title: "Server function method order breaks type inference",
34841
35776
  tags: ["test-noise"],
34842
35777
  requires: ["tanstack-start"],
34843
35778
  severity: "error",
34844
- recommendation: "Chain methods in order: .middleware() → .inputValidator() → .client() → .server() → .handler(). Types depend on this sequence.",
35779
+ recommendation: "Chain methods in order: .middleware() → .validator() → .client() → .server() → .handler(). Types depend on this sequence.",
34845
35780
  create: (context) => ({ CallExpression(node) {
34846
35781
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
34847
35782
  const methodNames = [];
@@ -34856,10 +35791,10 @@ const tanstackStartServerFnMethodOrder = defineRule({
34856
35791
  } else return;
34857
35792
  const ownMethodName = isNodeOfType(node.callee.property, "Identifier") ? node.callee.property.name : null;
34858
35793
  if (methodNames[methodNames.length - 1] !== ownMethodName) return;
34859
- const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(name));
35794
+ const orderSensitiveMethods = methodNames.filter((name) => TANSTACK_MIDDLEWARE_METHOD_ORDER.includes(toMethodOrderToken(name)));
34860
35795
  let lastIndex = -1;
34861
35796
  for (const methodName of orderSensitiveMethods) {
34862
- const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(methodName);
35797
+ const currentIndex = TANSTACK_MIDDLEWARE_METHOD_ORDER.indexOf(toMethodOrderToken(methodName));
34863
35798
  if (currentIndex < lastIndex) {
34864
35799
  const expectedBefore = TANSTACK_MIDDLEWARE_METHOD_ORDER[lastIndex];
34865
35800
  context.report({
@@ -34880,7 +35815,7 @@ const tanstackStartServerFnValidateInput = defineRule({
34880
35815
  tags: ["test-noise"],
34881
35816
  requires: ["tanstack-start"],
34882
35817
  severity: "warn",
34883
- recommendation: "Add `.inputValidator(schema)` before `.handler()`. This data crosses the network and must be validated at runtime.",
35818
+ recommendation: "Add `.validator(schema)` before `.handler()`. This data crosses the network and must be validated at runtime.",
34884
35819
  create: (context) => ({ CallExpression(node) {
34885
35820
  if (!isNodeOfType(node.callee, "MemberExpression")) return;
34886
35821
  if (!isNodeOfType(node.callee.property, "Identifier")) return;
@@ -34894,13 +35829,83 @@ const tanstackStartServerFnValidateInput = defineRule({
34894
35829
  if (isNodeOfType(child, "MemberExpression") && isNodeOfType(child.property, "Identifier") && child.property.name === "data") accessesData = true;
34895
35830
  if (isNodeOfType(child, "ObjectPattern") && child.properties?.some((property) => isNodeOfType(property, "Property") && isNodeOfType(property.key, "Identifier") && property.key.name === "data")) accessesData = true;
34896
35831
  });
34897
- if (accessesData && !chainInfo.hasInputValidator) context.report({
35832
+ if (accessesData && !chainInfo.hasInputValidation) context.report({
34898
35833
  node,
34899
- message: "This server function reads network data with no inputValidator(), so anyone can send unvalidated input."
35834
+ message: "This server function reads network data with no validator(), so anyone can send unvalidated input."
34900
35835
  });
34901
35836
  } })
34902
35837
  });
34903
35838
  //#endregion
35839
+ //#region src/plugin/rules/security-scan/utils/is-server-route-source-path.ts
35840
+ const isServerRouteSourcePath = (relativePath) => {
35841
+ if (!isProductionSourcePath(relativePath)) return false;
35842
+ if (SERVER_CONTEXT_PATTERN.test(relativePath)) return true;
35843
+ return /(?:^|\/)(?:middleware|route)\.[cm]?[jt]sx?$/.test(relativePath);
35844
+ };
35845
+ //#endregion
35846
+ //#region src/plugin/rules/security-scan/tenant-static-proxy-risk.ts
35847
+ const tenantStaticProxyRisk = defineRule({
35848
+ id: "tenant-static-proxy-risk",
35849
+ title: "Tenant-controlled static asset proxy",
35850
+ severity: "warn",
35851
+ 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.",
35852
+ scan: scanByPattern({
35853
+ shouldScan: (file) => isServerRouteSourcePath(file.relativePath),
35854
+ 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,
35855
+ message: "Route code appears to compose tenant or subdomain input into a static/CDN/object-store fetch path."
35856
+ })
35857
+ });
35858
+ //#endregion
35859
+ //#region src/plugin/rules/security-scan/untrusted-redirect-following.ts
35860
+ const OUTBOUND_FETCH_CALL_PATTERN = /(?:(?<![.\w$])fetch|\baxios\.\s*(?:get|post|put|delete|head)|\bgot|\bgot\.\s*(?:get|post))\s*\(\s*([^,)]+)/;
35861
+ 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;
35862
+ const REQUEST_INPUT_EXPRESSION_PATTERN = /\breq\.|\brequest\.(?:query|body|params|nextUrl)|\bsearchParams\b|\bparams\.|\bbody\.|\bquery\./;
35863
+ const SAFE_REDIRECT_MODE_PATTERN = /\bredirect\s*:\s*["'](?:manual|error)["']/;
35864
+ const isRequestSourcedUrlExpression = (urlExpression, fileContent) => {
35865
+ if (REQUEST_INPUT_EXPRESSION_PATTERN.test(urlExpression)) return true;
35866
+ const identifierMatch = /^[\w$]+$/.exec(urlExpression.trim());
35867
+ if (identifierMatch === null) return false;
35868
+ 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);
35869
+ };
35870
+ const untrustedRedirectFollowing = defineRule({
35871
+ id: "untrusted-redirect-following",
35872
+ title: "Server fetch follows redirects for caller-shaped URL",
35873
+ severity: "warn",
35874
+ recommendation: "Use `redirect: \"manual\"` or equivalent and re-validate every redirect target before following it to avoid SSRF redirect bypasses.",
35875
+ scan: (file) => {
35876
+ if (!isServerRouteSourcePath(file.relativePath)) return [];
35877
+ if (!OUTBOUND_FETCH_CALL_PATTERN.test(file.content)) return [];
35878
+ const findings = [];
35879
+ const lines = file.content.split("\n");
35880
+ for (let lineIndex = 0; lineIndex < lines.length; lineIndex += 1) {
35881
+ const line = lines[lineIndex] ?? "";
35882
+ const fetchMatch = line.match(OUTBOUND_FETCH_CALL_PATTERN);
35883
+ const urlExpression = fetchMatch?.[1] ?? "";
35884
+ if (!fetchMatch || !CALLER_STYLE_URL_NAME_PATTERN.test(urlExpression)) continue;
35885
+ if (!isRequestSourcedUrlExpression(urlExpression, file.content)) continue;
35886
+ const fetchWindow = lines.slice(lineIndex, lineIndex + 5).join("\n");
35887
+ if (SAFE_REDIRECT_MODE_PATTERN.test(fetchWindow)) continue;
35888
+ findings.push({
35889
+ message: "Server-side fetch code appears to follow redirects for a URL shaped like caller-controlled input.",
35890
+ line: lineIndex + 1,
35891
+ column: line.search(/\S/) + 1
35892
+ });
35893
+ }
35894
+ return findings;
35895
+ }
35896
+ });
35897
+ const urlPrefilledPrivilegedAction = defineRule({
35898
+ id: "url-prefilled-privileged-action",
35899
+ title: "URL pre-fills a privileged action",
35900
+ severity: "warn",
35901
+ recommendation: "Require server-side validation and explicit confirmation for URL-sourced invite, role, permission, redirect, or sharing parameters.",
35902
+ scan: scanByPattern({
35903
+ shouldScan: (file) => isClientSourcePath(file.relativePath),
35904
+ 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,
35905
+ message: "Client code reads sensitive action state from the URL, which can pre-fill invites, roles, redirects, or sharing flows with attacker values."
35906
+ })
35907
+ });
35908
+ //#endregion
34904
35909
  //#region src/plugin/rules/bundle-size/use-lazy-motion.ts
34905
35910
  const useLazyMotion = defineRule({
34906
35911
  id: "use-lazy-motion",
@@ -34995,6 +36000,33 @@ const voidDomElementsNoChildren = defineRule({
34995
36000
  })
34996
36001
  });
34997
36002
  //#endregion
36003
+ //#region src/plugin/rules/security-scan/webhook-signature-risk.ts
36004
+ const WEBHOOK_HANDLER_PATTERN = /(?:^|\/)[^/]*webhook[^/]*\/|(?:^|\/)[^/]*webhook[^/]*\.[cm]?[jt]s$|\bwebhook\b/i;
36005
+ const WEBHOOK_ENTRYPOINT_PATTERN = /\b(?:export\s+(?:async\s+)?function\s+POST|export\s+const\s+(?:POST|handler|webhook)|webhookHandler|webhookRoute)\b/i;
36006
+ const WEBHOOK_SIGNATURE_VERIFICATION_PATTERN = /verifySignature|verify.*signature|verify\w*(?:Webhook|Auth)|constructEvent|createHmac|timingSafeEqual|svix|webhookSecret|stripe\.webhooks|["'][\w-]*signature["']/i;
36007
+ const OUTBOUND_WEBHOOK_URL_MENTION_PATTERN = /webhook[\s_-]?ur[il]\w*/gi;
36008
+ const OUTBOUND_WEBHOOK_CONFIG_PATTERN = /process\.env\.\w*WEBHOOK_URL|\b(?:send|post|dispatch|publish|notify)\w*Webhook/;
36009
+ const REQUEST_READ_PATTERN = /\b(?:req|request)\b/;
36010
+ const COMMENT_OR_STRING_PATTERN = /\/\/[^\n]*|\/\*[\s\S]*?\*\/|"(?:\\.|[^"\\\n])*"|'(?:\\.|[^'\\\n])*'|`(?:\\.|[^`\\])*`/g;
36011
+ const webhookSignatureRisk = defineRule({
36012
+ id: "webhook-signature-risk",
36013
+ title: "Webhook handler lacks signature verification",
36014
+ severity: "warn",
36015
+ recommendation: "Verify provider signatures before parsing or acting on webhook bodies. Use provider SDK helpers or HMAC verification with timing-safe comparison.",
36016
+ scan: scanByPattern({
36017
+ shouldScan: (file) => {
36018
+ if (!isProductionSourcePath(file.relativePath)) return false;
36019
+ if (OUTBOUND_WEBHOOK_CONFIG_PATTERN.test(file.content)) return false;
36020
+ const judgeableContent = file.content.replace(COMMENT_OR_STRING_PATTERN, "").replace(OUTBOUND_WEBHOOK_URL_MENTION_PATTERN, "");
36021
+ return WEBHOOK_HANDLER_PATTERN.test(file.relativePath) || WEBHOOK_HANDLER_PATTERN.test(judgeableContent);
36022
+ },
36023
+ pattern: WEBHOOK_ENTRYPOINT_PATTERN,
36024
+ requireAll: [REQUEST_READ_PATTERN],
36025
+ suppressWhen: WEBHOOK_SIGNATURE_VERIFICATION_PATTERN,
36026
+ message: "Webhook handler code does not show an obvious signature verification step."
36027
+ })
36028
+ });
36029
+ //#endregion
34998
36030
  //#region src/plugin/rules/zod/utils/zod-ast.ts
34999
36031
  const ZOD_MODULE = "zod";
35000
36032
  const getStaticPropertyName = (member) => {
@@ -35405,6 +36437,18 @@ const zodV4PreferTopLevelStringFormats = defineRule({
35405
36437
  //#endregion
35406
36438
  //#region src/plugin/rule-registry.ts
35407
36439
  const reactDoctorRules = [
36440
+ {
36441
+ key: "react-doctor/active-static-asset",
36442
+ id: "active-static-asset",
36443
+ source: "react-doctor",
36444
+ originallyExternal: false,
36445
+ rule: {
36446
+ ...activeStaticAsset,
36447
+ framework: "global",
36448
+ category: "Security",
36449
+ tags: [...new Set(["security-scan", ...activeStaticAsset.tags ?? []])]
36450
+ }
36451
+ },
35408
36452
  {
35409
36453
  key: "react-doctor/activity-wraps-effect-heavy-subtree",
35410
36454
  id: "activity-wraps-effect-heavy-subtree",
@@ -35429,6 +36473,18 @@ const reactDoctorRules = [
35429
36473
  requires: [...new Set(["react", ...advancedEventHandlerRefs.requires ?? []])]
35430
36474
  }
35431
36475
  },
36476
+ {
36477
+ key: "react-doctor/agent-tool-capability-risk",
36478
+ id: "agent-tool-capability-risk",
36479
+ source: "react-doctor",
36480
+ originallyExternal: false,
36481
+ rule: {
36482
+ ...agentToolCapabilityRisk,
36483
+ framework: "global",
36484
+ category: "Security",
36485
+ tags: [...new Set(["security-scan", ...agentToolCapabilityRisk.tags ?? []])]
36486
+ }
36487
+ },
35432
36488
  {
35433
36489
  key: "react-doctor/alt-text",
35434
36490
  id: "alt-text",
@@ -35537,6 +36593,42 @@ const reactDoctorRules = [
35537
36593
  requires: [...new Set(["react", ...ariaUnsupportedElements.requires ?? []])]
35538
36594
  }
35539
36595
  },
36596
+ {
36597
+ key: "react-doctor/artifact-baas-authority-surface",
36598
+ id: "artifact-baas-authority-surface",
36599
+ source: "react-doctor",
36600
+ originallyExternal: false,
36601
+ rule: {
36602
+ ...artifactBaasAuthoritySurface,
36603
+ framework: "global",
36604
+ category: "Security",
36605
+ tags: [...new Set(["security-scan", ...artifactBaasAuthoritySurface.tags ?? []])]
36606
+ }
36607
+ },
36608
+ {
36609
+ key: "react-doctor/artifact-env-leak",
36610
+ id: "artifact-env-leak",
36611
+ source: "react-doctor",
36612
+ originallyExternal: false,
36613
+ rule: {
36614
+ ...artifactEnvLeak,
36615
+ framework: "global",
36616
+ category: "Security",
36617
+ tags: [...new Set(["security-scan", ...artifactEnvLeak.tags ?? []])]
36618
+ }
36619
+ },
36620
+ {
36621
+ key: "react-doctor/artifact-secret-leak",
36622
+ id: "artifact-secret-leak",
36623
+ source: "react-doctor",
36624
+ originallyExternal: false,
36625
+ rule: {
36626
+ ...artifactSecretLeak,
36627
+ framework: "global",
36628
+ category: "Security",
36629
+ tags: [...new Set(["security-scan", ...artifactSecretLeak.tags ?? []])]
36630
+ }
36631
+ },
35540
36632
  {
35541
36633
  key: "react-doctor/async-await-in-loop",
35542
36634
  id: "async-await-in-loop",
@@ -35583,6 +36675,18 @@ const reactDoctorRules = [
35583
36675
  requires: [...new Set(["react", ...autocompleteValid.requires ?? []])]
35584
36676
  }
35585
36677
  },
36678
+ {
36679
+ key: "react-doctor/build-pipeline-secret-boundary",
36680
+ id: "build-pipeline-secret-boundary",
36681
+ source: "react-doctor",
36682
+ originallyExternal: false,
36683
+ rule: {
36684
+ ...buildPipelineSecretBoundary,
36685
+ framework: "global",
36686
+ category: "Security",
36687
+ tags: [...new Set(["security-scan", ...buildPipelineSecretBoundary.tags ?? []])]
36688
+ }
36689
+ },
35586
36690
  {
35587
36691
  key: "react-doctor/button-has-type",
35588
36692
  id: "button-has-type",
@@ -35619,6 +36723,18 @@ const reactDoctorRules = [
35619
36723
  requires: [...new Set(["react", ...clickEventsHaveKeyEvents.requires ?? []])]
35620
36724
  }
35621
36725
  },
36726
+ {
36727
+ key: "react-doctor/clickjacking-redirect-risk",
36728
+ id: "clickjacking-redirect-risk",
36729
+ source: "react-doctor",
36730
+ originallyExternal: false,
36731
+ rule: {
36732
+ ...clickjackingRedirectRisk,
36733
+ framework: "global",
36734
+ category: "Security",
36735
+ tags: [...new Set(["security-scan", ...clickjackingRedirectRisk.tags ?? []])]
36736
+ }
36737
+ },
35622
36738
  {
35623
36739
  key: "react-doctor/client-localstorage-no-version",
35624
36740
  id: "client-localstorage-no-version",
@@ -35643,6 +36759,18 @@ const reactDoctorRules = [
35643
36759
  requires: [...new Set(["react", ...clientPassiveEventListeners.requires ?? []])]
35644
36760
  }
35645
36761
  },
36762
+ {
36763
+ key: "react-doctor/command-execution-input-risk",
36764
+ id: "command-execution-input-risk",
36765
+ source: "react-doctor",
36766
+ originallyExternal: false,
36767
+ rule: {
36768
+ ...commandExecutionInputRisk,
36769
+ framework: "global",
36770
+ category: "Security",
36771
+ tags: [...new Set(["security-scan", ...commandExecutionInputRisk.tags ?? []])]
36772
+ }
36773
+ },
35646
36774
  {
35647
36775
  key: "react-doctor/control-has-associated-label",
35648
36776
  id: "control-has-associated-label",
@@ -35655,6 +36783,30 @@ const reactDoctorRules = [
35655
36783
  requires: [...new Set(["react", ...controlHasAssociatedLabel.requires ?? []])]
35656
36784
  }
35657
36785
  },
36786
+ {
36787
+ key: "react-doctor/cors-cookie-trust-risk",
36788
+ id: "cors-cookie-trust-risk",
36789
+ source: "react-doctor",
36790
+ originallyExternal: false,
36791
+ rule: {
36792
+ ...corsCookieTrustRisk,
36793
+ framework: "global",
36794
+ category: "Security",
36795
+ tags: [...new Set(["security-scan", ...corsCookieTrustRisk.tags ?? []])]
36796
+ }
36797
+ },
36798
+ {
36799
+ key: "react-doctor/dangerous-html-sink",
36800
+ id: "dangerous-html-sink",
36801
+ source: "react-doctor",
36802
+ originallyExternal: false,
36803
+ rule: {
36804
+ ...dangerousHtmlSink,
36805
+ framework: "global",
36806
+ category: "Security",
36807
+ tags: [...new Set(["security-scan", ...dangerousHtmlSink.tags ?? []])]
36808
+ }
36809
+ },
35658
36810
  {
35659
36811
  key: "react-doctor/design-no-em-dash-in-jsx-text",
35660
36812
  id: "design-no-em-dash-in-jsx-text",
@@ -35775,6 +36927,42 @@ const reactDoctorRules = [
35775
36927
  tags: [...new Set(["react-native", ...expoNoNonInlinedEnv.tags ?? []])]
35776
36928
  }
35777
36929
  },
36930
+ {
36931
+ key: "react-doctor/firebase-client-owned-authz-field",
36932
+ id: "firebase-client-owned-authz-field",
36933
+ source: "react-doctor",
36934
+ originallyExternal: false,
36935
+ rule: {
36936
+ ...firebaseClientOwnedAuthzField,
36937
+ framework: "global",
36938
+ category: "Security",
36939
+ tags: [...new Set(["security-scan", ...firebaseClientOwnedAuthzField.tags ?? []])]
36940
+ }
36941
+ },
36942
+ {
36943
+ key: "react-doctor/firebase-permissive-rules",
36944
+ id: "firebase-permissive-rules",
36945
+ source: "react-doctor",
36946
+ originallyExternal: false,
36947
+ rule: {
36948
+ ...firebasePermissiveRules,
36949
+ framework: "global",
36950
+ category: "Security",
36951
+ tags: [...new Set(["security-scan", ...firebasePermissiveRules.tags ?? []])]
36952
+ }
36953
+ },
36954
+ {
36955
+ key: "react-doctor/firebase-query-filter-as-auth",
36956
+ id: "firebase-query-filter-as-auth",
36957
+ source: "react-doctor",
36958
+ originallyExternal: false,
36959
+ rule: {
36960
+ ...firebaseQueryFilterAsAuth,
36961
+ framework: "global",
36962
+ category: "Security",
36963
+ tags: [...new Set(["security-scan", ...firebaseQueryFilterAsAuth.tags ?? []])]
36964
+ }
36965
+ },
35778
36966
  {
35779
36967
  key: "react-doctor/forbid-component-props",
35780
36968
  id: "forbid-component-props",
@@ -35823,6 +37011,18 @@ const reactDoctorRules = [
35823
37011
  requires: [...new Set(["react", ...forwardRefUsesRef.requires ?? []])]
35824
37012
  }
35825
37013
  },
37014
+ {
37015
+ key: "react-doctor/git-provider-url-injection-risk",
37016
+ id: "git-provider-url-injection-risk",
37017
+ source: "react-doctor",
37018
+ originallyExternal: false,
37019
+ rule: {
37020
+ ...gitProviderUrlInjectionRisk,
37021
+ framework: "global",
37022
+ category: "Security",
37023
+ tags: [...new Set(["security-scan", ...gitProviderUrlInjectionRisk.tags ?? []])]
37024
+ }
37025
+ },
35826
37026
  {
35827
37027
  key: "react-doctor/heading-has-content",
35828
37028
  id: "heading-has-content",
@@ -35940,6 +37140,30 @@ const reactDoctorRules = [
35940
37140
  requires: [...new Set(["react", ...imgRedundantAlt.requires ?? []])]
35941
37141
  }
35942
37142
  },
37143
+ {
37144
+ key: "react-doctor/import-metadata-execution-risk",
37145
+ id: "import-metadata-execution-risk",
37146
+ source: "react-doctor",
37147
+ originallyExternal: false,
37148
+ rule: {
37149
+ ...importMetadataExecutionRisk,
37150
+ framework: "global",
37151
+ category: "Security",
37152
+ tags: [...new Set(["security-scan", ...importMetadataExecutionRisk.tags ?? []])]
37153
+ }
37154
+ },
37155
+ {
37156
+ key: "react-doctor/insecure-crypto-risk",
37157
+ id: "insecure-crypto-risk",
37158
+ source: "react-doctor",
37159
+ originallyExternal: false,
37160
+ rule: {
37161
+ ...insecureCryptoRisk,
37162
+ framework: "global",
37163
+ category: "Security",
37164
+ tags: [...new Set(["security-scan", ...insecureCryptoRisk.tags ?? []])]
37165
+ }
37166
+ },
35943
37167
  {
35944
37168
  key: "react-doctor/interactive-supports-focus",
35945
37169
  id: "interactive-supports-focus",
@@ -36382,6 +37606,18 @@ const reactDoctorRules = [
36382
37606
  requires: [...new Set(["react", ...jsxPropsNoSpreading.requires ?? []])]
36383
37607
  }
36384
37608
  },
37609
+ {
37610
+ key: "react-doctor/key-lifecycle-risk",
37611
+ id: "key-lifecycle-risk",
37612
+ source: "react-doctor",
37613
+ originallyExternal: false,
37614
+ rule: {
37615
+ ...keyLifecycleRisk,
37616
+ framework: "global",
37617
+ category: "Security",
37618
+ tags: [...new Set(["security-scan", ...keyLifecycleRisk.tags ?? []])]
37619
+ }
37620
+ },
36385
37621
  {
36386
37622
  key: "react-doctor/label-has-associated-control",
36387
37623
  id: "label-has-associated-control",
@@ -36406,6 +37642,42 @@ const reactDoctorRules = [
36406
37642
  requires: [...new Set(["react", ...lang.requires ?? []])]
36407
37643
  }
36408
37644
  },
37645
+ {
37646
+ key: "react-doctor/local-rpc-native-bridge-risk",
37647
+ id: "local-rpc-native-bridge-risk",
37648
+ source: "react-doctor",
37649
+ originallyExternal: false,
37650
+ rule: {
37651
+ ...localRpcNativeBridgeRisk,
37652
+ framework: "global",
37653
+ category: "Security",
37654
+ tags: [...new Set(["security-scan", ...localRpcNativeBridgeRisk.tags ?? []])]
37655
+ }
37656
+ },
37657
+ {
37658
+ key: "react-doctor/mcp-tool-capability-risk",
37659
+ id: "mcp-tool-capability-risk",
37660
+ source: "react-doctor",
37661
+ originallyExternal: false,
37662
+ rule: {
37663
+ ...mcpToolCapabilityRisk,
37664
+ framework: "global",
37665
+ category: "Security",
37666
+ tags: [...new Set(["security-scan", ...mcpToolCapabilityRisk.tags ?? []])]
37667
+ }
37668
+ },
37669
+ {
37670
+ key: "react-doctor/mdx-ssr-execution-risk",
37671
+ id: "mdx-ssr-execution-risk",
37672
+ source: "react-doctor",
37673
+ originallyExternal: false,
37674
+ rule: {
37675
+ ...mdxSsrExecutionRisk,
37676
+ framework: "global",
37677
+ category: "Security",
37678
+ tags: [...new Set(["security-scan", ...mdxSsrExecutionRisk.tags ?? []])]
37679
+ }
37680
+ },
36409
37681
  {
36410
37682
  key: "react-doctor/media-has-caption",
36411
37683
  id: "media-has-caption",
@@ -37951,6 +39223,18 @@ const reactDoctorRules = [
37951
39223
  category: "Maintainability"
37952
39224
  }
37953
39225
  },
39226
+ {
39227
+ key: "react-doctor/nosql-injection-risk",
39228
+ id: "nosql-injection-risk",
39229
+ source: "react-doctor",
39230
+ originallyExternal: false,
39231
+ rule: {
39232
+ ...nosqlInjectionRisk,
39233
+ framework: "global",
39234
+ category: "Security",
39235
+ tags: [...new Set(["security-scan", ...nosqlInjectionRisk.tags ?? []])]
39236
+ }
39237
+ },
37954
39238
  {
37955
39239
  key: "react-doctor/only-export-components",
37956
39240
  id: "only-export-components",
@@ -37963,6 +39247,54 @@ const reactDoctorRules = [
37963
39247
  requires: [...new Set(["react", ...onlyExportComponents.requires ?? []])]
37964
39248
  }
37965
39249
  },
39250
+ {
39251
+ key: "react-doctor/package-metadata-secret",
39252
+ id: "package-metadata-secret",
39253
+ source: "react-doctor",
39254
+ originallyExternal: false,
39255
+ rule: {
39256
+ ...packageMetadataSecret,
39257
+ framework: "global",
39258
+ category: "Security",
39259
+ tags: [...new Set(["security-scan", ...packageMetadataSecret.tags ?? []])]
39260
+ }
39261
+ },
39262
+ {
39263
+ key: "react-doctor/path-traversal-risk",
39264
+ id: "path-traversal-risk",
39265
+ source: "react-doctor",
39266
+ originallyExternal: false,
39267
+ rule: {
39268
+ ...pathTraversalRisk,
39269
+ framework: "global",
39270
+ category: "Security",
39271
+ tags: [...new Set(["security-scan", ...pathTraversalRisk.tags ?? []])]
39272
+ }
39273
+ },
39274
+ {
39275
+ key: "react-doctor/plugin-update-trust-risk",
39276
+ id: "plugin-update-trust-risk",
39277
+ source: "react-doctor",
39278
+ originallyExternal: false,
39279
+ rule: {
39280
+ ...pluginUpdateTrustRisk,
39281
+ framework: "global",
39282
+ category: "Security",
39283
+ tags: [...new Set(["security-scan", ...pluginUpdateTrustRisk.tags ?? []])]
39284
+ }
39285
+ },
39286
+ {
39287
+ key: "react-doctor/postmessage-origin-risk",
39288
+ id: "postmessage-origin-risk",
39289
+ source: "react-doctor",
39290
+ originallyExternal: false,
39291
+ rule: {
39292
+ ...postmessageOriginRisk,
39293
+ framework: "global",
39294
+ category: "Security",
39295
+ tags: [...new Set(["security-scan", ...postmessageOriginRisk.tags ?? []])]
39296
+ }
39297
+ },
37966
39298
  {
37967
39299
  key: "react-doctor/preact-no-children-length",
37968
39300
  id: "preact-no-children-length",
@@ -38158,6 +39490,30 @@ const reactDoctorRules = [
38158
39490
  requires: [...new Set(["react", ...preferUseReducer.requires ?? []])]
38159
39491
  }
38160
39492
  },
39493
+ {
39494
+ key: "react-doctor/public-debug-artifact",
39495
+ id: "public-debug-artifact",
39496
+ source: "react-doctor",
39497
+ originallyExternal: false,
39498
+ rule: {
39499
+ ...publicDebugArtifact,
39500
+ framework: "global",
39501
+ category: "Security",
39502
+ tags: [...new Set(["security-scan", ...publicDebugArtifact.tags ?? []])]
39503
+ }
39504
+ },
39505
+ {
39506
+ key: "react-doctor/public-env-secret-name",
39507
+ id: "public-env-secret-name",
39508
+ source: "react-doctor",
39509
+ originallyExternal: false,
39510
+ rule: {
39511
+ ...publicEnvSecretName,
39512
+ framework: "global",
39513
+ category: "Security",
39514
+ tags: [...new Set(["security-scan", ...publicEnvSecretName.tags ?? []])]
39515
+ }
39516
+ },
38161
39517
  {
38162
39518
  key: "react-doctor/query-destructure-result",
38163
39519
  id: "query-destructure-result",
@@ -38235,6 +39591,18 @@ const reactDoctorRules = [
38235
39591
  category: "Bugs"
38236
39592
  }
38237
39593
  },
39594
+ {
39595
+ key: "react-doctor/raw-sql-injection-risk",
39596
+ id: "raw-sql-injection-risk",
39597
+ source: "react-doctor",
39598
+ originallyExternal: false,
39599
+ rule: {
39600
+ ...rawSqlInjectionRisk,
39601
+ framework: "global",
39602
+ category: "Security",
39603
+ tags: [...new Set(["security-scan", ...rawSqlInjectionRisk.tags ?? []])]
39604
+ }
39605
+ },
38238
39606
  {
38239
39607
  key: "react-doctor/react-compiler-no-manual-memoization",
38240
39608
  id: "react-compiler-no-manual-memoization",
@@ -38376,6 +39744,18 @@ const reactDoctorRules = [
38376
39744
  requires: [...new Set(["react", ...renderingUsetransitionLoading.requires ?? []])]
38377
39745
  }
38378
39746
  },
39747
+ {
39748
+ key: "react-doctor/repository-secret-file",
39749
+ id: "repository-secret-file",
39750
+ source: "react-doctor",
39751
+ originallyExternal: false,
39752
+ rule: {
39753
+ ...repositorySecretFile,
39754
+ framework: "global",
39755
+ category: "Security",
39756
+ tags: [...new Set(["security-scan", ...repositorySecretFile.tags ?? []])]
39757
+ }
39758
+ },
38379
39759
  {
38380
39760
  key: "react-doctor/require-render-return",
38381
39761
  id: "require-render-return",
@@ -39096,6 +40476,42 @@ const reactDoctorRules = [
39096
40476
  requires: [...new Set(["react", ...stylePropObject.requires ?? []])]
39097
40477
  }
39098
40478
  },
40479
+ {
40480
+ key: "react-doctor/supabase-client-owned-authz-field",
40481
+ id: "supabase-client-owned-authz-field",
40482
+ source: "react-doctor",
40483
+ originallyExternal: false,
40484
+ rule: {
40485
+ ...supabaseClientOwnedAuthzField,
40486
+ framework: "global",
40487
+ category: "Security",
40488
+ tags: [...new Set(["security-scan", ...supabaseClientOwnedAuthzField.tags ?? []])]
40489
+ }
40490
+ },
40491
+ {
40492
+ key: "react-doctor/supabase-rls-policy-risk",
40493
+ id: "supabase-rls-policy-risk",
40494
+ source: "react-doctor",
40495
+ originallyExternal: false,
40496
+ rule: {
40497
+ ...supabaseRlsPolicyRisk,
40498
+ framework: "global",
40499
+ category: "Security",
40500
+ tags: [...new Set(["security-scan", ...supabaseRlsPolicyRisk.tags ?? []])]
40501
+ }
40502
+ },
40503
+ {
40504
+ key: "react-doctor/svg-filter-clickjacking-risk",
40505
+ id: "svg-filter-clickjacking-risk",
40506
+ source: "react-doctor",
40507
+ originallyExternal: false,
40508
+ rule: {
40509
+ ...svgFilterClickjackingRisk,
40510
+ framework: "global",
40511
+ category: "Security",
40512
+ tags: [...new Set(["security-scan", ...svgFilterClickjackingRisk.tags ?? []])]
40513
+ }
40514
+ },
39099
40515
  {
39100
40516
  key: "react-doctor/tabindex-no-positive",
39101
40517
  id: "tabindex-no-positive",
@@ -39262,6 +40678,42 @@ const reactDoctorRules = [
39262
40678
  category: "Bugs"
39263
40679
  }
39264
40680
  },
40681
+ {
40682
+ key: "react-doctor/tenant-static-proxy-risk",
40683
+ id: "tenant-static-proxy-risk",
40684
+ source: "react-doctor",
40685
+ originallyExternal: false,
40686
+ rule: {
40687
+ ...tenantStaticProxyRisk,
40688
+ framework: "global",
40689
+ category: "Security",
40690
+ tags: [...new Set(["security-scan", ...tenantStaticProxyRisk.tags ?? []])]
40691
+ }
40692
+ },
40693
+ {
40694
+ key: "react-doctor/untrusted-redirect-following",
40695
+ id: "untrusted-redirect-following",
40696
+ source: "react-doctor",
40697
+ originallyExternal: false,
40698
+ rule: {
40699
+ ...untrustedRedirectFollowing,
40700
+ framework: "global",
40701
+ category: "Security",
40702
+ tags: [...new Set(["security-scan", ...untrustedRedirectFollowing.tags ?? []])]
40703
+ }
40704
+ },
40705
+ {
40706
+ key: "react-doctor/url-prefilled-privileged-action",
40707
+ id: "url-prefilled-privileged-action",
40708
+ source: "react-doctor",
40709
+ originallyExternal: false,
40710
+ rule: {
40711
+ ...urlPrefilledPrivilegedAction,
40712
+ framework: "global",
40713
+ category: "Security",
40714
+ tags: [...new Set(["security-scan", ...urlPrefilledPrivilegedAction.tags ?? []])]
40715
+ }
40716
+ },
39265
40717
  {
39266
40718
  key: "react-doctor/use-lazy-motion",
39267
40719
  id: "use-lazy-motion",
@@ -39285,6 +40737,18 @@ const reactDoctorRules = [
39285
40737
  requires: [...new Set(["react", ...voidDomElementsNoChildren.requires ?? []])]
39286
40738
  }
39287
40739
  },
40740
+ {
40741
+ key: "react-doctor/webhook-signature-risk",
40742
+ id: "webhook-signature-risk",
40743
+ source: "react-doctor",
40744
+ originallyExternal: false,
40745
+ rule: {
40746
+ ...webhookSignatureRisk,
40747
+ framework: "global",
40748
+ category: "Security",
40749
+ tags: [...new Set(["security-scan", ...webhookSignatureRisk.tags ?? []])]
40750
+ }
40751
+ },
39288
40752
  {
39289
40753
  key: "react-doctor/zod-v4-no-deprecated-error-apis",
39290
40754
  id: "zod-v4-no-deprecated-error-apis",
@@ -39855,7 +41319,8 @@ const toKeyedSeverity = (entries) => entries.map((entry) => ({
39855
41319
  severity: entry.rule.severity
39856
41320
  }));
39857
41321
  const isRecommendedByDefault = (entry) => entry.rule.defaultEnabled !== false;
39858
- const collectReactDoctorRulesByFramework = (frameworkName) => reactDoctorRules.filter((entry) => entry.rule.framework === frameworkName && isRecommendedByDefault(entry));
41322
+ const isScanRule = (entry) => entry.rule.scan !== void 0;
41323
+ const collectReactDoctorRulesByFramework = (frameworkName) => reactDoctorRules.filter((entry) => entry.rule.framework === frameworkName && isRecommendedByDefault(entry) && !isScanRule(entry));
39859
41324
  const collectExternalRulesBySource = (source) => EXTERNAL_RULES.filter((rule) => rule.source === source);
39860
41325
  const collectFrameworkSpecificRuleKeys = () => {
39861
41326
  const collected = /* @__PURE__ */ new Set();
@@ -39952,14 +41417,36 @@ const REACT_NATIVE_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFr
39952
41417
  const TANSTACK_START_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-start")));
39953
41418
  const TANSTACK_QUERY_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("tanstack-query")));
39954
41419
  const PREACT_RULES = toRuleMap(toKeyedSeverity(collectReactDoctorRulesByFramework("preact")));
39955
- const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES));
41420
+ const ALL_REACT_DOCTOR_RULES = toRuleMap(toKeyedSeverity(REACT_DOCTOR_RULES.filter((entry) => !isScanRule(entry))));
39956
41421
  const ALL_REACT_DOCTOR_RULE_KEYS = new Set(REACT_DOCTOR_RULES.map((rule) => rule.key));
39957
41422
  const FRAMEWORK_SPECIFIC_RULE_KEYS = collectFrameworkSpecificRuleKeys();
39958
41423
  const REACT_COMPILER_RULES = toRuleMap(collectExternalRulesBySource("react-compiler"));
39959
41424
  //#endregion
41425
+ //#region src/plugin/rules/security-scan/utils/is-probably-text-file.ts
41426
+ const isProbablyTextFile = (relativePath) => TEXT_FILE_PATTERN.test(relativePath) || DOTENV_FILE_PATTERN.test(relativePath);
41427
+ //#endregion
41428
+ //#region src/plugin/rules/security-scan/utils/classify-security-scan-file.ts
41429
+ const classifySecurityScanFile = (relativePath) => {
41430
+ const isGeneratedBundleByName = GENERATED_BUNDLE_FILE_PATTERN.test(relativePath);
41431
+ if (isRepositorySecretFilePath(relativePath) || isSqlPath(relativePath) || isFirebaseRulesPath(relativePath) || isConfigOrCiPath(relativePath)) return {
41432
+ bucket: "priority",
41433
+ isGeneratedBundleByName
41434
+ };
41435
+ if (isBrowserArtifactPath(relativePath, isGeneratedBundleByName)) return {
41436
+ bucket: "artifact",
41437
+ isGeneratedBundleByName
41438
+ };
41439
+ if (isProbablyTextFile(relativePath)) return {
41440
+ bucket: "other",
41441
+ isGeneratedBundleByName
41442
+ };
41443
+ return null;
41444
+ };
41445
+ const shouldReadSecurityScanContent = (relativePath, isGeneratedBundle) => isGeneratedBundle || isProbablyTextFile(relativePath) || isConfigOrCiPath(relativePath) || isRepositorySecretFilePath(relativePath);
41446
+ //#endregion
39960
41447
  //#region src/index.ts
39961
41448
  var src_default = plugin;
39962
41449
  //#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 };
41450
+ 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
41451
 
39965
41452
  //# sourceMappingURL=index.js.map