@sun-asterisk/sunlint 1.0.5

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.
Files changed (192) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LICENSE +21 -0
  3. package/README.md +490 -0
  4. package/cli-legacy.js +355 -0
  5. package/cli.js +35 -0
  6. package/config/default.json +22 -0
  7. package/config/presets/beginner.json +36 -0
  8. package/config/presets/ci.json +46 -0
  9. package/config/presets/recommended.json +24 -0
  10. package/config/presets/strict.json +32 -0
  11. package/config/rules-registry.json +681 -0
  12. package/config/sunlint-schema.json +166 -0
  13. package/config/typescript/custom-rules-new.js +0 -0
  14. package/config/typescript/custom-rules.js +9 -0
  15. package/config/typescript/eslint.config.js +110 -0
  16. package/config/typescript/package-lock.json +1585 -0
  17. package/config/typescript/package.json +13 -0
  18. package/config/typescript/security-rules/index.js +90 -0
  19. package/config/typescript/security-rules/s005-no-origin-auth.js +95 -0
  20. package/config/typescript/security-rules/s006-activation-recovery-secret-not-plaintext.js +69 -0
  21. package/config/typescript/security-rules/s008-crypto-agility.js +62 -0
  22. package/config/typescript/security-rules/s009-no-insecure-crypto.js +103 -0
  23. package/config/typescript/security-rules/s010-no-insecure-random-in-sensitive-context.js +123 -0
  24. package/config/typescript/security-rules/s011-no-insecure-uuid.js +66 -0
  25. package/config/typescript/security-rules/s012-hardcode-secret.js +71 -0
  26. package/config/typescript/security-rules/s014-insecure-tls-version.js +50 -0
  27. package/config/typescript/security-rules/s015-insecure-tls-certificate.js +43 -0
  28. package/config/typescript/security-rules/s016-sensitive-query-parameter.js +59 -0
  29. package/config/typescript/security-rules/s017-no-sql-injection.js +193 -0
  30. package/config/typescript/security-rules/s018-positive-input-validation.js +56 -0
  31. package/config/typescript/security-rules/s019-no-raw-user-input-in-email.js +113 -0
  32. package/config/typescript/security-rules/s020-no-eval-dynamic-execution.js +89 -0
  33. package/config/typescript/security-rules/s022-output-encoding.js +78 -0
  34. package/config/typescript/security-rules/s023-no-json-injection.js +300 -0
  35. package/config/typescript/security-rules/s025-server-side-input-validation.js +217 -0
  36. package/config/typescript/security-rules/s026-json-schema-validation.js +68 -0
  37. package/config/typescript/security-rules/s027-no-hardcoded-secrets.js +80 -0
  38. package/config/typescript/security-rules/s029-require-csrf-protection.js +79 -0
  39. package/config/typescript/security-rules/s030-no-directory-browsing.js +78 -0
  40. package/config/typescript/security-rules/s033-require-samesite-cookie.js +80 -0
  41. package/config/typescript/security-rules/s034-require-host-cookie-prefix.js +77 -0
  42. package/config/typescript/security-rules/s035-cookie-specific-path.js +74 -0
  43. package/config/typescript/security-rules/s036-no-unsafe-file-include.js +68 -0
  44. package/config/typescript/security-rules/s037-require-anti-cache-headers.js +70 -0
  45. package/config/typescript/security-rules/s038-no-version-disclosure.js +74 -0
  46. package/config/typescript/security-rules/s039-no-session-token-in-url.js +63 -0
  47. package/config/typescript/security-rules/s041-require-session-invalidate-on-logout.js +211 -0
  48. package/config/typescript/security-rules/s042-require-periodic-reauthentication.js +294 -0
  49. package/config/typescript/security-rules/s043-terminate-sessions-on-password-change.js +254 -0
  50. package/config/typescript/security-rules/s044-require-full-session-for-sensitive-operations.js +292 -0
  51. package/config/typescript/security-rules/s045-anti-automation-controls.js +46 -0
  52. package/config/typescript/security-rules/s046-secure-notification-on-auth-change.js +44 -0
  53. package/config/typescript/security-rules/s048-password-credential-recovery.js +54 -0
  54. package/config/typescript/security-rules/s050-session-token-weak-hash.js +94 -0
  55. package/config/typescript/security-rules/s052-secure-random-authentication-code.js +66 -0
  56. package/config/typescript/security-rules/s054-verification-default-account.js +109 -0
  57. package/config/typescript/security-rules/s057-utc-logging.js +54 -0
  58. package/config/typescript/security-rules/s058-no-ssrf.js +73 -0
  59. package/config/typescript/test-s005-working.ts +22 -0
  60. package/config/typescript/tsconfig.json +29 -0
  61. package/core/ai-analyzer.js +169 -0
  62. package/core/analysis-orchestrator.js +705 -0
  63. package/core/cli-action-handler.js +230 -0
  64. package/core/cli-program.js +106 -0
  65. package/core/config-manager.js +396 -0
  66. package/core/config-merger.js +136 -0
  67. package/core/config-override-processor.js +74 -0
  68. package/core/config-preset-resolver.js +65 -0
  69. package/core/config-source-loader.js +152 -0
  70. package/core/config-validator.js +126 -0
  71. package/core/dependency-manager.js +105 -0
  72. package/core/eslint-engine-service.js +312 -0
  73. package/core/eslint-instance-manager.js +104 -0
  74. package/core/eslint-integration-service.js +363 -0
  75. package/core/git-utils.js +170 -0
  76. package/core/multi-rule-runner.js +239 -0
  77. package/core/output-service.js +250 -0
  78. package/core/report-generator.js +320 -0
  79. package/core/rule-mapping-service.js +309 -0
  80. package/core/rule-selection-service.js +121 -0
  81. package/core/sunlint-engine-service.js +23 -0
  82. package/core/typescript-analyzer.js +262 -0
  83. package/core/typescript-engine.js +313 -0
  84. package/docs/AI.md +163 -0
  85. package/docs/ARCHITECTURE.md +78 -0
  86. package/docs/CI-CD-GUIDE.md +315 -0
  87. package/docs/COMMAND-EXAMPLES.md +256 -0
  88. package/docs/DEBUG.md +86 -0
  89. package/docs/DISTRIBUTION.md +153 -0
  90. package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
  91. package/docs/ESLINT_INTEGRATION.md +238 -0
  92. package/docs/FOLDER_STRUCTURE.md +59 -0
  93. package/docs/HEURISTIC_VS_AI.md +113 -0
  94. package/docs/README.md +32 -0
  95. package/docs/RELEASE_GUIDE.md +230 -0
  96. package/docs/RULE-RESPONSIBILITY-MATRIX.md +204 -0
  97. package/eslint-integration/.eslintrc.js +98 -0
  98. package/eslint-integration/cli.js +35 -0
  99. package/eslint-integration/eslint-plugin-custom/c002-no-duplicate-code.js +204 -0
  100. package/eslint-integration/eslint-plugin-custom/c003-no-vague-abbreviations.js +246 -0
  101. package/eslint-integration/eslint-plugin-custom/c006-function-name-verb-noun.js +207 -0
  102. package/eslint-integration/eslint-plugin-custom/c010-limit-block-nesting.js +90 -0
  103. package/eslint-integration/eslint-plugin-custom/c013-no-dead-code.js +43 -0
  104. package/eslint-integration/eslint-plugin-custom/c014-abstract-dependency-preferred.js +38 -0
  105. package/eslint-integration/eslint-plugin-custom/c017-limit-constructor-logic.js +39 -0
  106. package/eslint-integration/eslint-plugin-custom/c018-no-generic-throw.js +335 -0
  107. package/eslint-integration/eslint-plugin-custom/c023-no-duplicate-variable-name-in-scope.js +142 -0
  108. package/eslint-integration/eslint-plugin-custom/c027-limit-function-nesting.js +50 -0
  109. package/eslint-integration/eslint-plugin-custom/c029-catch-block-logging.js +80 -0
  110. package/eslint-integration/eslint-plugin-custom/c030-use-custom-error-classes.js +294 -0
  111. package/eslint-integration/eslint-plugin-custom/c034-no-implicit-return.js +34 -0
  112. package/eslint-integration/eslint-plugin-custom/c035-no-empty-catch.js +32 -0
  113. package/eslint-integration/eslint-plugin-custom/c041-no-config-inline.js +64 -0
  114. package/eslint-integration/eslint-plugin-custom/c042-boolean-name-prefix.js +406 -0
  115. package/eslint-integration/eslint-plugin-custom/c043-no-console-or-print.js +300 -0
  116. package/eslint-integration/eslint-plugin-custom/c047-no-duplicate-retry-logic.js +239 -0
  117. package/eslint-integration/eslint-plugin-custom/c048-no-var-declaration.js +31 -0
  118. package/eslint-integration/eslint-plugin-custom/c076-one-assert-per-test.js +184 -0
  119. package/eslint-integration/eslint-plugin-custom/index.js +155 -0
  120. package/eslint-integration/eslint-plugin-custom/package.json +13 -0
  121. package/eslint-integration/eslint-plugin-custom/package.json.bak +9 -0
  122. package/eslint-integration/eslint-plugin-custom/s003-no-unvalidated-redirect.js +86 -0
  123. package/eslint-integration/eslint-plugin-custom/s005-no-origin-auth.js +95 -0
  124. package/eslint-integration/eslint-plugin-custom/s006-activation-recovery-secret-not-plaintext.js +69 -0
  125. package/eslint-integration/eslint-plugin-custom/s008-crypto-agility.js +62 -0
  126. package/eslint-integration/eslint-plugin-custom/s009-no-insecure-crypto.js +103 -0
  127. package/eslint-integration/eslint-plugin-custom/s010-no-insecure-random-in-sensitive-context.js +123 -0
  128. package/eslint-integration/eslint-plugin-custom/s011-no-insecure-uuid.js +66 -0
  129. package/eslint-integration/eslint-plugin-custom/s012-hardcode-secret.js +71 -0
  130. package/eslint-integration/eslint-plugin-custom/s014-insecure-tls-version.js +50 -0
  131. package/eslint-integration/eslint-plugin-custom/s015-insecure-tls-certificate.js +43 -0
  132. package/eslint-integration/eslint-plugin-custom/s016-sensitive-query-parameter.js +59 -0
  133. package/eslint-integration/eslint-plugin-custom/s017-no-sql-injection.js +193 -0
  134. package/eslint-integration/eslint-plugin-custom/s018-positive-input-validation.js +56 -0
  135. package/eslint-integration/eslint-plugin-custom/s019-no-raw-user-input-in-email.js +113 -0
  136. package/eslint-integration/eslint-plugin-custom/s020-no-eval-dynamic-execution.js +89 -0
  137. package/eslint-integration/eslint-plugin-custom/s022-output-encoding.js +78 -0
  138. package/eslint-integration/eslint-plugin-custom/s023-no-json-injection.js +300 -0
  139. package/eslint-integration/eslint-plugin-custom/s025-server-side-input-validation.js +217 -0
  140. package/eslint-integration/eslint-plugin-custom/s026-json-schema-validation.js +68 -0
  141. package/eslint-integration/eslint-plugin-custom/s027-no-hardcoded-secrets.js +80 -0
  142. package/eslint-integration/eslint-plugin-custom/s029-require-csrf-protection.js +79 -0
  143. package/eslint-integration/eslint-plugin-custom/s030-no-directory-browsing.js +78 -0
  144. package/eslint-integration/eslint-plugin-custom/s033-require-samesite-cookie.js +80 -0
  145. package/eslint-integration/eslint-plugin-custom/s034-require-host-cookie-prefix.js +77 -0
  146. package/eslint-integration/eslint-plugin-custom/s035-cookie-specific-path.js +74 -0
  147. package/eslint-integration/eslint-plugin-custom/s036-no-unsafe-file-include.js +68 -0
  148. package/eslint-integration/eslint-plugin-custom/s037-require-anti-cache-headers.js +70 -0
  149. package/eslint-integration/eslint-plugin-custom/s038-no-version-disclosure.js +74 -0
  150. package/eslint-integration/eslint-plugin-custom/s039-no-session-token-in-url.js +63 -0
  151. package/eslint-integration/eslint-plugin-custom/s041-require-session-invalidate-on-logout.js +211 -0
  152. package/eslint-integration/eslint-plugin-custom/s042-require-periodic-reauthentication.js +294 -0
  153. package/eslint-integration/eslint-plugin-custom/s043-terminate-sessions-on-password-change.js +254 -0
  154. package/eslint-integration/eslint-plugin-custom/s044-require-full-session-for-sensitive-operations.js +292 -0
  155. package/eslint-integration/eslint-plugin-custom/s045-anti-automation-controls.js +46 -0
  156. package/eslint-integration/eslint-plugin-custom/s046-secure-notification-on-auth-change.js +44 -0
  157. package/eslint-integration/eslint-plugin-custom/s047-secure-random-passwords.js +108 -0
  158. package/eslint-integration/eslint-plugin-custom/s048-password-credential-recovery.js +54 -0
  159. package/eslint-integration/eslint-plugin-custom/s050-session-token-weak-hash.js +94 -0
  160. package/eslint-integration/eslint-plugin-custom/s052-secure-random-authentication-code.js +66 -0
  161. package/eslint-integration/eslint-plugin-custom/s054-verification-default-account.js +109 -0
  162. package/eslint-integration/eslint-plugin-custom/s055-verification-rest-check-the-incoming-content-type.js +143 -0
  163. package/eslint-integration/eslint-plugin-custom/s057-utc-logging.js +54 -0
  164. package/eslint-integration/eslint-plugin-custom/s058-no-ssrf.js +73 -0
  165. package/eslint-integration/eslint-plugin-custom/t002-interface-prefix-i.js +42 -0
  166. package/eslint-integration/eslint-plugin-custom/t003-ts-ignore-reason.js +48 -0
  167. package/eslint-integration/eslint-plugin-custom/t004-interface-public-only.js +160 -0
  168. package/eslint-integration/eslint-plugin-custom/t007-no-fn-in-constructor.js +52 -0
  169. package/eslint-integration/eslint-plugin-custom/t011-no-real-time-dependency.js +175 -0
  170. package/eslint-integration/eslint-plugin-custom/t019-no-empty-type.js +95 -0
  171. package/eslint-integration/eslint-plugin-custom/t025-no-nested-union-tuple.js +48 -0
  172. package/eslint-integration/eslint-plugin-custom/t026-limit-nested-generics.js +377 -0
  173. package/eslint-integration/eslint.config.js +125 -0
  174. package/eslint-integration/eslint.config.simple.js +24 -0
  175. package/eslint-integration/node_modules/eslint-plugin-custom/package.json +0 -0
  176. package/eslint-integration/package.json +23 -0
  177. package/eslint-integration/sample.ts +53 -0
  178. package/eslint-integration/test-s003.js +5 -0
  179. package/eslint-integration/tsconfig.json +27 -0
  180. package/examples/.github/workflows/code-quality.yml +111 -0
  181. package/examples/.sunlint.json +42 -0
  182. package/examples/README.md +47 -0
  183. package/examples/package.json +33 -0
  184. package/package.json +100 -0
  185. package/rules/C006_function_naming/analyzer.js +338 -0
  186. package/rules/C006_function_naming/config.json +86 -0
  187. package/rules/C019_log_level_usage/analyzer.js +359 -0
  188. package/rules/C019_log_level_usage/config.json +121 -0
  189. package/rules/C029_catch_block_logging/analyzer.js +339 -0
  190. package/rules/C029_catch_block_logging/config.json +59 -0
  191. package/rules/C031_validation_separation/README.md +72 -0
  192. package/rules/C031_validation_separation/analyzer.js +186 -0
@@ -0,0 +1,79 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Custom ESLint rule: S029 – Require CSRF protection on routes
5
+ * Rule ID: custom/s029
6
+ * Purpose: Ensure CSRF protection is applied to route handlers
7
+ */
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description: "Ensure CSRF protection is applied to route handlers",
14
+ recommended: true,
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ missingCsrf:
19
+ "CSRF protection is missing on this route handler '{{route}}'. Apply csurf() or equivalent middleware.",
20
+ },
21
+ },
22
+
23
+ create(context) {
24
+ const csrfFunctions = [
25
+ "csurf",
26
+ "csrfProtection",
27
+ "verifyCsrfToken",
28
+ "checkCsrf",
29
+ ];
30
+ const routeMethods = ["post", "put", "delete"];
31
+
32
+ const protectedInstances = new Set(); // e.g. app, router
33
+
34
+ return {
35
+ CallExpression(node) {
36
+ const callee = node.callee;
37
+
38
+ // Detect app.use(csurf()) → mark 'app' as protected
39
+ if (
40
+ callee.type === "MemberExpression" &&
41
+ callee.property.name === "use" &&
42
+ node.arguments.some(
43
+ (arg) =>
44
+ (arg.type === "CallExpression" &&
45
+ arg.callee.type === "Identifier" &&
46
+ csrfFunctions.includes(arg.callee.name)) ||
47
+ (arg.type === "Identifier" && csrfFunctions.includes(arg.name))
48
+ )
49
+ ) {
50
+ const instance = callee.object.name;
51
+ if (instance) {
52
+ protectedInstances.add(instance);
53
+ }
54
+ }
55
+
56
+ // Detect route registrations
57
+ if (
58
+ callee.type === "MemberExpression" &&
59
+ routeMethods.includes(callee.property.name)
60
+ ) {
61
+ const instance = callee.object.name;
62
+ const args = node.arguments;
63
+
64
+ if (!protectedInstances.has(instance)) {
65
+ const path =
66
+ args[0] && args[0].type === "Literal"
67
+ ? args[0].value
68
+ : "<unknown>";
69
+ context.report({
70
+ node,
71
+ messageId: "missingCsrf",
72
+ data: { route: path },
73
+ });
74
+ }
75
+ }
76
+ },
77
+ };
78
+ },
79
+ };
@@ -0,0 +1,78 @@
1
+ /**
2
+ * Custom ESLint rule: S030 – Prevent directory browsing and metadata disclosure
3
+ * Rule ID: custom/s030
4
+ * Purpose: Disallow unsafe static serving or exposure of internal metadata files
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description: "Prevent directory browsing and metadata file disclosure",
14
+ recommended: true,
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ unsafeStatic:
19
+ "Directory browsing might be enabled via static file serving. Explicitly set `index: false` if not intended.",
20
+ exposeMetaFile:
21
+ "Avoid exposing internal files or folders such as '.git', '.svn', '.DS_Store', or 'Thumbs.db'.",
22
+ },
23
+ },
24
+
25
+ create(context) {
26
+ const metaFileNames = [".git", ".svn", ".DS_Store", "Thumbs.db"];
27
+
28
+ return {
29
+ CallExpression(node) {
30
+ const callee = node.callee;
31
+
32
+ // Detect express.static(...) or serveStatic(...)
33
+ if (
34
+ (callee.type === "MemberExpression" &&
35
+ callee.property.name === "static") ||
36
+ (callee.type === "Identifier" && callee.name === "serveStatic")
37
+ ) {
38
+ const optionsArg = node.arguments[1];
39
+
40
+ let hasIndexFalse = false;
41
+ if (optionsArg && optionsArg.type === "ObjectExpression") {
42
+ hasIndexFalse = optionsArg.properties.some((prop) => {
43
+ return (
44
+ prop.key.name === "index" &&
45
+ prop.value.type === "Literal" &&
46
+ prop.value.value === false
47
+ );
48
+ });
49
+ }
50
+
51
+ if (!hasIndexFalse) {
52
+ context.report({
53
+ node,
54
+ messageId: "unsafeStatic",
55
+ });
56
+ }
57
+ }
58
+
59
+ // Detect serving meta files directly
60
+ if (
61
+ node.arguments &&
62
+ node.arguments.some((arg) => {
63
+ return (
64
+ arg.type === "Literal" &&
65
+ typeof arg.value === "string" &&
66
+ metaFileNames.some((name) => arg.value.includes(name))
67
+ );
68
+ })
69
+ ) {
70
+ context.report({
71
+ node,
72
+ messageId: "exposeMetaFile",
73
+ });
74
+ }
75
+ },
76
+ };
77
+ },
78
+ };
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Custom ESLint rule: S033 – Enforce SameSite on cookies
3
+ * Rule ID: custom/s033
4
+ * Purpose: Ensure SameSite attribute is set when setting cookies
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description: "Ensure SameSite is set when using cookies to prevent CSRF",
14
+ recommended: true,
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ missingSameSite:
19
+ "Cookie does not set 'SameSite' attribute. This may expose it to CSRF.",
20
+ },
21
+ },
22
+
23
+ create(context) {
24
+ return {
25
+ CallExpression(node) {
26
+ const callee = node.callee;
27
+
28
+ // Detect res.cookie("name", "value", options)
29
+ if (
30
+ callee.type === "MemberExpression" &&
31
+ callee.property.name === "cookie"
32
+ ) {
33
+ const options = node.arguments[2];
34
+ if (options && options.type === "ObjectExpression") {
35
+ const hasSameSite = options.properties.some((prop) => {
36
+ return (
37
+ prop.key &&
38
+ prop.key.name === "sameSite" &&
39
+ prop.value.type === "Literal" &&
40
+ ["strict", "lax", "none"].includes(
41
+ prop.value.value.toLowerCase()
42
+ )
43
+ );
44
+ });
45
+
46
+ if (!hasSameSite) {
47
+ context.report({
48
+ node,
49
+ messageId: "missingSameSite",
50
+ });
51
+ }
52
+ } else {
53
+ // No options object passed at all
54
+ context.report({
55
+ node,
56
+ messageId: "missingSameSite",
57
+ });
58
+ }
59
+ }
60
+
61
+ // Detect res.setHeader('Set-Cookie', '...') → doesn't include SameSite
62
+ if (
63
+ callee.type === "MemberExpression" &&
64
+ callee.property.name === "setHeader" &&
65
+ node.arguments.length >= 2 &&
66
+ node.arguments[0].type === "Literal" &&
67
+ node.arguments[0].value === "Set-Cookie" &&
68
+ node.arguments[1].type === "Literal" &&
69
+ typeof node.arguments[1].value === "string" &&
70
+ !node.arguments[1].value.includes("SameSite")
71
+ ) {
72
+ context.report({
73
+ node,
74
+ messageId: "missingSameSite",
75
+ });
76
+ }
77
+ },
78
+ };
79
+ },
80
+ };
@@ -0,0 +1,77 @@
1
+ /**
2
+ * Custom ESLint rule: S034 – Enforce '__Host-' prefix on secure cookies
3
+ * Rule ID: custom/s034
4
+ * Purpose: Ensure cookies use the '__Host-' prefix for added security
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description:
14
+ "Ensure cookies use the '__Host-' prefix for secure session cookies",
15
+ recommended: true,
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ missingHostPrefix:
20
+ "Cookie name '{{name}}' should use the '__Host-' prefix for stronger security.",
21
+ },
22
+ },
23
+
24
+ create(context) {
25
+ return {
26
+ CallExpression(node) {
27
+ const callee = node.callee;
28
+
29
+ // Match res.cookie("name", ...)
30
+ if (
31
+ callee.type === "MemberExpression" &&
32
+ callee.property.name === "cookie" &&
33
+ node.arguments.length >= 1
34
+ ) {
35
+ const cookieNameArg = node.arguments[0];
36
+
37
+ if (
38
+ cookieNameArg.type === "Literal" &&
39
+ typeof cookieNameArg.value === "string"
40
+ ) {
41
+ const name = cookieNameArg.value;
42
+
43
+ // If cookie name does not start with '__Host-' → warn
44
+ if (!name.startsWith("__Host-")) {
45
+ context.report({
46
+ node: cookieNameArg,
47
+ messageId: "missingHostPrefix",
48
+ data: { name },
49
+ });
50
+ }
51
+ }
52
+ }
53
+
54
+ // Optional: check res.setHeader('Set-Cookie', 'session=value; ...')
55
+ if (
56
+ callee.type === "MemberExpression" &&
57
+ callee.property.name === "setHeader" &&
58
+ node.arguments.length >= 2 &&
59
+ node.arguments[0].type === "Literal" &&
60
+ node.arguments[0].value === "Set-Cookie" &&
61
+ node.arguments[1].type === "Literal" &&
62
+ typeof node.arguments[1].value === "string"
63
+ ) {
64
+ const cookieString = node.arguments[1].value;
65
+ const cookieName = cookieString.split("=")[0].trim();
66
+ if (!cookieName.startsWith("__Host-")) {
67
+ context.report({
68
+ node: node.arguments[1],
69
+ messageId: "missingHostPrefix",
70
+ data: { name: cookieName },
71
+ });
72
+ }
73
+ }
74
+ },
75
+ };
76
+ },
77
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Custom ESLint rule: S035 – Require specific path in cookies
3
+ * Rule ID: custom/s035
4
+ * Purpose: Ensure cookies set a specific path, not the root `/`
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description:
14
+ "Ensure cookies use a specific path (not `/`) to reduce exposure to sibling apps under same domain",
15
+ recommended: true,
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ pathTooBroad:
20
+ "Cookie uses path `'/'`, which may expose it across unrelated applications. Use a more specific path.",
21
+ },
22
+ },
23
+
24
+ create(context) {
25
+ return {
26
+ CallExpression(node) {
27
+ const callee = node.callee;
28
+
29
+ // Match res.cookie("name", "value", { ... })
30
+ if (
31
+ callee.type === "MemberExpression" &&
32
+ callee.property.name === "cookie" &&
33
+ node.arguments.length >= 3
34
+ ) {
35
+ const options = node.arguments[2];
36
+
37
+ if (options.type === "ObjectExpression") {
38
+ const pathProp = options.properties.find(
39
+ (prop) => prop.key.name === "path"
40
+ );
41
+
42
+ if (
43
+ pathProp &&
44
+ pathProp.value.type === "Literal" &&
45
+ pathProp.value.value === "/"
46
+ ) {
47
+ context.report({
48
+ node: pathProp,
49
+ messageId: "pathTooBroad",
50
+ });
51
+ }
52
+ }
53
+ }
54
+
55
+ // Optional: check res.setHeader("Set-Cookie", "...Path=/...")
56
+ if (
57
+ callee.type === "MemberExpression" &&
58
+ callee.property.name === "setHeader" &&
59
+ node.arguments.length >= 2 &&
60
+ node.arguments[0].type === "Literal" &&
61
+ node.arguments[0].value === "Set-Cookie" &&
62
+ node.arguments[1].type === "Literal" &&
63
+ typeof node.arguments[1].value === "string" &&
64
+ /path=\/(;|\s|$)/i.test(node.arguments[1].value)
65
+ ) {
66
+ context.report({
67
+ node: node.arguments[1],
68
+ messageId: "pathTooBroad",
69
+ });
70
+ }
71
+ },
72
+ };
73
+ },
74
+ };
@@ -0,0 +1,68 @@
1
+ /**
2
+ * Custom ESLint rule: S036 – Prevent LFI and RFI vulnerabilities
3
+ * Rule ID: custom/s036
4
+ * Purpose: Detect unvalidated user input passed to file system or dynamic import/require
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description:
14
+ "Detect possible Local/Remote File Inclusion vulnerabilities",
15
+ recommended: true,
16
+ },
17
+ schema: [],
18
+ messages: {
19
+ unsafeFilePath:
20
+ "Potential LFI/RFI: Do not pass user input directly to file or import functions.",
21
+ },
22
+ },
23
+
24
+ create(context) {
25
+ const fileFunctions = ["readFile", "readFileSync", "createReadStream"];
26
+ const importFunctions = ["require", "import"];
27
+
28
+ function isUserInput(argNode) {
29
+ const code = context.getSourceCode().getText(argNode);
30
+ return /req\.|input\.|params\.|query\.|body\./.test(code);
31
+ }
32
+
33
+ return {
34
+ CallExpression(node) {
35
+ const callee = node.callee;
36
+
37
+ // Handle fs.readFile(...), fs.createReadStream(...)
38
+ if (
39
+ callee.type === "MemberExpression" &&
40
+ fileFunctions.includes(callee.property.name)
41
+ ) {
42
+ const [arg] = node.arguments;
43
+ if (arg && isUserInput(arg)) {
44
+ context.report({
45
+ node: arg,
46
+ messageId: "unsafeFilePath",
47
+ });
48
+ }
49
+ }
50
+
51
+ // Handle require(...) and import(...)
52
+ if (
53
+ (callee.type === "Identifier" &&
54
+ importFunctions.includes(callee.name)) ||
55
+ node.type === "ImportExpression"
56
+ ) {
57
+ const arg = node.arguments?.[0] || node.source;
58
+ if (arg && isUserInput(arg)) {
59
+ context.report({
60
+ node: arg,
61
+ messageId: "unsafeFilePath",
62
+ });
63
+ }
64
+ }
65
+ },
66
+ };
67
+ },
68
+ };
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Custom ESLint rule: S037 – Require anti-caching headers on responses
3
+ */
4
+
5
+ "use strict";
6
+
7
+ module.exports = {
8
+ meta: {
9
+ type: "suggestion",
10
+ docs: {
11
+ description:
12
+ "Ensure anti-cache headers are set to prevent sensitive data caching",
13
+ recommended: true,
14
+ },
15
+ schema: [],
16
+ messages: {
17
+ missingCacheHeader:
18
+ "Missing anti-cache header for response '{{resName}}'. Consider setting 'Cache-Control: no-store'.",
19
+ },
20
+ },
21
+
22
+ create(context) {
23
+ // Track which response variables have Cache-Control: no-store/no-cache
24
+ const secureResNames = new Set();
25
+
26
+ return {
27
+ CallExpression(node) {
28
+ const callee = node.callee;
29
+
30
+ // Match res.setHeader('Cache-Control', '...')
31
+ if (
32
+ callee.type === "MemberExpression" &&
33
+ callee.property.name === "setHeader" &&
34
+ node.arguments.length >= 2 &&
35
+ node.arguments[0].type === "Literal" &&
36
+ node.arguments[0].value === "Cache-Control"
37
+ ) {
38
+ const resVar = callee.object;
39
+ const valNode = node.arguments[1];
40
+
41
+ if (
42
+ resVar.type === "Identifier" &&
43
+ valNode.type === "Literal" &&
44
+ typeof valNode.value === "string" &&
45
+ /no-store|no-cache/i.test(valNode.value)
46
+ ) {
47
+ secureResNames.add(resVar.name);
48
+ }
49
+ }
50
+
51
+ // Match res.send / res.json / res.end
52
+ if (
53
+ callee.type === "MemberExpression" &&
54
+ ["send", "json", "end"].includes(callee.property.name) &&
55
+ callee.object.type === "Identifier"
56
+ ) {
57
+ const resName = callee.object.name;
58
+
59
+ if (!secureResNames.has(resName)) {
60
+ context.report({
61
+ node,
62
+ messageId: "missingCacheHeader",
63
+ data: { resName },
64
+ });
65
+ }
66
+ }
67
+ },
68
+ };
69
+ },
70
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * Custom ESLint rule: S038 – Prevent version disclosure in HTTP headers or response
3
+ * Rule ID: custom/s038
4
+ */
5
+
6
+ "use strict";
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description:
13
+ "Prevent exposing version info via HTTP headers or response bodies",
14
+ recommended: true,
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ headerLeak:
19
+ "Do not expose version information via HTTP header '{{name}}'.",
20
+ responseLeak: "Do not send version information in response: '{{text}}'.",
21
+ },
22
+ },
23
+
24
+ create(context) {
25
+ const riskyHeaders = ["x-powered-by", "server", "x-runtime", "x-version"];
26
+
27
+ return {
28
+ CallExpression(node) {
29
+ const callee = node.callee;
30
+
31
+ // res.setHeader("Header-Name", "value")
32
+ if (
33
+ callee.type === "MemberExpression" &&
34
+ callee.property.name === "setHeader" &&
35
+ node.arguments.length >= 2 &&
36
+ node.arguments[0].type === "Literal" &&
37
+ typeof node.arguments[0].value === "string"
38
+ ) {
39
+ const headerName = node.arguments[0].value.toLowerCase();
40
+ const headerValue = node.arguments[1];
41
+
42
+ if (
43
+ riskyHeaders.includes(headerName) &&
44
+ headerValue.type === "Literal" &&
45
+ typeof headerValue.value === "string" &&
46
+ /\d+\.\d+/.test(headerValue.value) // has version pattern
47
+ ) {
48
+ context.report({
49
+ node,
50
+ messageId: "headerLeak",
51
+ data: { name: headerName },
52
+ });
53
+ }
54
+ }
55
+
56
+ // res.send("Express 4.17.1") or res.end("NestJS 9.0")
57
+ if (
58
+ callee.type === "MemberExpression" &&
59
+ ["send", "end", "json"].includes(callee.property.name) &&
60
+ node.arguments.length &&
61
+ node.arguments[0].type === "Literal" &&
62
+ typeof node.arguments[0].value === "string" &&
63
+ /\d+\.\d+/.test(node.arguments[0].value)
64
+ ) {
65
+ context.report({
66
+ node,
67
+ messageId: "responseLeak",
68
+ data: { text: node.arguments[0].value },
69
+ });
70
+ }
71
+ },
72
+ };
73
+ },
74
+ };
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Custom ESLint rule: S039 – Do not include session tokens in URL parameters
3
+ * Rule ID: custom/s039
4
+ */
5
+
6
+ "use strict";
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description:
13
+ "Ensure session tokens are not exposed in URL query parameters",
14
+ recommended: true,
15
+ },
16
+ schema: [],
17
+ messages: {
18
+ tokenInUrl: "Do not expose session token '{{param}}' in URL parameters.",
19
+ },
20
+ },
21
+
22
+ create(context) {
23
+ const tokenKeywords = ["token", "session", "auth", "jwt", "sid"];
24
+
25
+ return {
26
+ Literal(node) {
27
+ if (typeof node.value === "string") {
28
+ const url = node.value;
29
+
30
+ // Simple match ?token=abc123 or &sid=xyz or ?session=...
31
+ const regex = /[?&]([a-zA-Z0-9_-]+)=/g;
32
+ let match;
33
+ while ((match = regex.exec(url)) !== null) {
34
+ const param = match[1].toLowerCase();
35
+ if (tokenKeywords.some((key) => param.includes(key))) {
36
+ context.report({
37
+ node,
38
+ messageId: "tokenInUrl",
39
+ data: { param: match[1] },
40
+ });
41
+ }
42
+ }
43
+ }
44
+ },
45
+
46
+ TemplateLiteral(node) {
47
+ const raw = context.getSourceCode().getText(node);
48
+ const regex = /[?&]([a-zA-Z0-9_-]+)=/g;
49
+ let match;
50
+ while ((match = regex.exec(raw)) !== null) {
51
+ const param = match[1].toLowerCase();
52
+ if (tokenKeywords.some((key) => param.includes(key))) {
53
+ context.report({
54
+ node,
55
+ messageId: "tokenInUrl",
56
+ data: { param: match[1] },
57
+ });
58
+ }
59
+ }
60
+ },
61
+ };
62
+ },
63
+ };