@sun-asterisk/sunlint 1.0.7 → 1.1.4

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 (219) hide show
  1. package/.sunlint.json +35 -0
  2. package/CHANGELOG.md +30 -3
  3. package/CONTRIBUTING.md +235 -0
  4. package/PROJECT_STRUCTURE.md +60 -0
  5. package/README.md +146 -58
  6. package/cli.js +1 -0
  7. package/config/README.md +88 -0
  8. package/config/defaults/ai-rules-context.json +231 -0
  9. package/config/engines/engines.json +49 -0
  10. package/config/engines/eslint-rule-mapping.json +74 -0
  11. package/config/eslint-rule-mapping.json +126 -0
  12. package/config/integrations/eslint/base.config.js +125 -0
  13. package/config/integrations/eslint/simple.config.js +24 -0
  14. package/config/presets/strict.json +0 -1
  15. package/config/rule-analysis-strategies.js +74 -0
  16. package/config/{rules-registry.json → rules/rules-registry.json} +30 -7
  17. package/core/analysis-orchestrator.js +383 -591
  18. package/core/ast-modules/README.md +103 -0
  19. package/core/ast-modules/base-parser.js +90 -0
  20. package/core/ast-modules/index.js +97 -0
  21. package/core/ast-modules/package.json +37 -0
  22. package/core/ast-modules/parsers/eslint-js-parser.js +153 -0
  23. package/core/ast-modules/parsers/eslint-ts-parser.js +98 -0
  24. package/core/ast-modules/parsers/javascript-parser.js +187 -0
  25. package/core/ast-modules/parsers/typescript-parser.js +187 -0
  26. package/core/cli-action-handler.js +271 -255
  27. package/core/cli-program.js +18 -4
  28. package/core/config-manager.js +9 -3
  29. package/core/config-merger.js +40 -1
  30. package/core/config-validator.js +2 -2
  31. package/core/dependency-checker.js +125 -0
  32. package/core/enhanced-rules-registry.js +331 -0
  33. package/core/file-targeting-service.js +92 -23
  34. package/core/interfaces/analysis-engine.interface.js +100 -0
  35. package/core/multi-rule-runner.js +0 -221
  36. package/core/output-service.js +1 -1
  37. package/core/rule-mapping-service.js +1 -1
  38. package/core/rule-selection-service.js +10 -2
  39. package/core/smart-installer.js +164 -0
  40. package/docs/AI.md +163 -0
  41. package/docs/ARCHITECTURE.md +78 -0
  42. package/docs/CI-CD-GUIDE.md +315 -0
  43. package/docs/COMMAND-EXAMPLES.md +256 -0
  44. package/docs/CONFIGURATION.md +414 -0
  45. package/docs/DEBUG.md +86 -0
  46. package/docs/DEPENDENCIES.md +90 -0
  47. package/docs/DEPLOYMENT-STRATEGIES.md +270 -0
  48. package/docs/DISTRIBUTION.md +153 -0
  49. package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
  50. package/docs/ESLINT_INTEGRATION.md +238 -0
  51. package/docs/FOLDER_STRUCTURE.md +59 -0
  52. package/docs/FUTURE_PACKAGES.md +83 -0
  53. package/docs/HEURISTIC_VS_AI.md +113 -0
  54. package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +112 -0
  55. package/docs/PRODUCTION_SIZE_IMPACT.md +183 -0
  56. package/docs/README.md +32 -0
  57. package/docs/RELEASE_GUIDE.md +230 -0
  58. package/engines/eslint-engine.js +610 -0
  59. package/engines/heuristic-engine.js +864 -0
  60. package/engines/openai-engine.js +374 -0
  61. package/engines/tree-sitter-parser.js +0 -0
  62. package/engines/universal-ast-engine.js +0 -0
  63. package/integrations/eslint/README.md +99 -0
  64. package/integrations/eslint/configs/.eslintrc.js +98 -0
  65. package/integrations/eslint/configs/eslint.config.js +133 -0
  66. package/integrations/eslint/configs/eslint.config.simple.js +24 -0
  67. package/integrations/eslint/package.json +23 -0
  68. package/integrations/eslint/plugin/index.js +164 -0
  69. package/integrations/eslint/plugin/package.json +13 -0
  70. package/integrations/eslint/plugin/rules/common/c002-no-duplicate-code.js +204 -0
  71. package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +246 -0
  72. package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +216 -0
  73. package/integrations/eslint/plugin/rules/common/c010-limit-block-nesting.js +90 -0
  74. package/integrations/eslint/plugin/rules/common/c013-no-dead-code.js +78 -0
  75. package/integrations/eslint/plugin/rules/common/c014-abstract-dependency-preferred.js +38 -0
  76. package/integrations/eslint/plugin/rules/common/c017-limit-constructor-logic.js +146 -0
  77. package/integrations/eslint/plugin/rules/common/c018-no-generic-throw.js +335 -0
  78. package/integrations/eslint/plugin/rules/common/c023-no-duplicate-variable-name-in-scope.js +142 -0
  79. package/integrations/eslint/plugin/rules/common/c029-catch-block-logging.js +115 -0
  80. package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +294 -0
  81. package/integrations/eslint/plugin/rules/common/c035-no-empty-catch.js +162 -0
  82. package/integrations/eslint/plugin/rules/common/c041-no-config-inline.js +122 -0
  83. package/integrations/eslint/plugin/rules/common/c042-boolean-name-prefix.js +406 -0
  84. package/integrations/eslint/plugin/rules/common/c043-no-console-or-print.js +300 -0
  85. package/integrations/eslint/plugin/rules/common/c047-no-duplicate-retry-logic.js +239 -0
  86. package/integrations/eslint/plugin/rules/common/c072-one-assert-per-test.js +184 -0
  87. package/integrations/eslint/plugin/rules/common/c075-explicit-function-return-types.js +168 -0
  88. package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +254 -0
  89. package/integrations/eslint/plugin/rules/security/s001-fail-securely.js +381 -0
  90. package/integrations/eslint/plugin/rules/security/s002-idor-check.js +945 -0
  91. package/integrations/eslint/plugin/rules/security/s003-no-unvalidated-redirect.js +86 -0
  92. package/integrations/eslint/plugin/rules/security/s007-no-plaintext-otp.js +74 -0
  93. package/integrations/eslint/plugin/rules/security/s013-verify-tls-connection.js +47 -0
  94. package/integrations/eslint/plugin/rules/security/s047-secure-random-passwords.js +108 -0
  95. package/integrations/eslint/plugin/rules/security/s055-verification-rest-check-the-incoming-content-type.js +143 -0
  96. package/integrations/eslint/plugin/rules/typescript/t002-interface-prefix-i.js +42 -0
  97. package/integrations/eslint/plugin/rules/typescript/t003-ts-ignore-reason.js +48 -0
  98. package/integrations/eslint/plugin/rules/typescript/t004-no-empty-type.js +95 -0
  99. package/integrations/eslint/plugin/rules/typescript/t007-no-fn-in-constructor.js +52 -0
  100. package/integrations/eslint/plugin/rules/typescript/t010-no-nested-union-tuple.js +48 -0
  101. package/integrations/eslint/plugin/rules/typescript/t019-no-this-assign.js +81 -0
  102. package/integrations/eslint/plugin/rules/typescript/t020-no-default-multi-export.js +127 -0
  103. package/integrations/eslint/plugin/rules/typescript/t021-limit-nested-generics.js +150 -0
  104. package/integrations/eslint/tsconfig.json +27 -0
  105. package/package.json +61 -21
  106. package/rules/README.md +252 -0
  107. package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
  108. package/rules/common/C002_no_duplicate_code/config.json +23 -0
  109. package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
  110. package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
  111. package/rules/{C006_function_naming → common/C006_function_naming}/analyzer.js +13 -2
  112. package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
  113. package/rules/common/C013_no_dead_code/analyzer.js +206 -0
  114. package/rules/common/C014_dependency_injection/analyzer.js +338 -0
  115. package/rules/common/C017_constructor_logic/analyzer.js +314 -0
  116. package/rules/{C019_log_level_usage → common/C019_log_level_usage}/analyzer.js +5 -2
  117. package/rules/{C029_catch_block_logging → common/C029_catch_block_logging}/analyzer.js +49 -15
  118. package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
  119. package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
  120. package/rules/common/C043_no_console_or_print/analyzer.js +304 -0
  121. package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +351 -0
  122. package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
  123. package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
  124. package/rules/docs/C002_no_duplicate_code.md +57 -0
  125. package/rules/index.js +149 -0
  126. package/rules/migration/converter.js +385 -0
  127. package/rules/migration/mapping.json +164 -0
  128. package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
  129. package/rules/security/S026_json_schema_validation/config.json +27 -0
  130. package/rules/security/S027_no_hardcoded_secrets/analyzer.js +263 -0
  131. package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
  132. package/rules/security/S029_csrf_protection/analyzer.js +264 -0
  133. package/rules/tests/C002_no_duplicate_code.test.js +50 -0
  134. package/rules/universal/C010/generic.js +0 -0
  135. package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
  136. package/rules/utils/ast-utils.js +191 -0
  137. package/rules/utils/base-analyzer.js +98 -0
  138. package/rules/utils/pattern-matchers.js +239 -0
  139. package/rules/utils/rule-helpers.js +264 -0
  140. package/rules/utils/severity-constants.js +93 -0
  141. package/scripts/build-release.sh +117 -0
  142. package/scripts/ci-report.js +179 -0
  143. package/scripts/install.sh +196 -0
  144. package/scripts/manual-release.sh +338 -0
  145. package/scripts/merge-reports.js +424 -0
  146. package/scripts/pre-release-test.sh +175 -0
  147. package/scripts/prepare-release.sh +202 -0
  148. package/scripts/setup-github-registry.sh +42 -0
  149. package/scripts/test-scripts/README.md +22 -0
  150. package/scripts/test-scripts/test-c041-comparison.js +114 -0
  151. package/scripts/test-scripts/test-c041-eslint.js +67 -0
  152. package/scripts/test-scripts/test-eslint-rules.js +146 -0
  153. package/scripts/test-scripts/test-real-world.js +44 -0
  154. package/scripts/test-scripts/test-rules-on-real-projects.js +86 -0
  155. package/scripts/trigger-release.sh +285 -0
  156. package/scripts/validate-rule-structure.js +148 -0
  157. package/scripts/verify-install.sh +82 -0
  158. package/config/sunlint-schema.json +0 -159
  159. package/config/typescript/custom-rules.js +0 -9
  160. package/config/typescript/package-lock.json +0 -1585
  161. package/config/typescript/package.json +0 -13
  162. package/config/typescript/security-rules/index.js +0 -90
  163. package/config/typescript/tsconfig.json +0 -29
  164. package/core/ai-analyzer.js +0 -169
  165. package/core/eslint-engine-service.js +0 -312
  166. package/core/eslint-instance-manager.js +0 -104
  167. package/core/eslint-integration-service.js +0 -363
  168. package/core/sunlint-engine-service.js +0 -23
  169. package/core/typescript-analyzer.js +0 -262
  170. package/core/typescript-engine.js +0 -313
  171. /package/config/{default.json → defaults/default.json} +0 -0
  172. /package/config/{typescript/eslint.config.js → integrations/eslint/typescript.config.js} +0 -0
  173. /package/config/{typescript/custom-rules-new.js → schemas/sunlint-schema.json} +0 -0
  174. /package/config/{typescript → testing}/test-s005-working.ts +0 -0
  175. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s005-no-origin-auth.js +0 -0
  176. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s006-activation-recovery-secret-not-plaintext.js +0 -0
  177. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s008-crypto-agility.js +0 -0
  178. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s009-no-insecure-crypto.js +0 -0
  179. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s010-no-insecure-random-in-sensitive-context.js +0 -0
  180. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s011-no-insecure-uuid.js +0 -0
  181. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s012-hardcode-secret.js +0 -0
  182. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s014-insecure-tls-version.js +0 -0
  183. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s015-insecure-tls-certificate.js +0 -0
  184. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s016-sensitive-query-parameter.js +0 -0
  185. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s017-no-sql-injection.js +0 -0
  186. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s018-positive-input-validation.js +0 -0
  187. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s019-no-raw-user-input-in-email.js +0 -0
  188. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s020-no-eval-dynamic-execution.js +0 -0
  189. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s022-output-encoding.js +0 -0
  190. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s023-no-json-injection.js +0 -0
  191. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s025-server-side-input-validation.js +0 -0
  192. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s026-json-schema-validation.js +0 -0
  193. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s027-no-hardcoded-secrets.js +0 -0
  194. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s029-require-csrf-protection.js +0 -0
  195. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s030-no-directory-browsing.js +0 -0
  196. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s033-require-samesite-cookie.js +0 -0
  197. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s034-require-host-cookie-prefix.js +0 -0
  198. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s035-cookie-specific-path.js +0 -0
  199. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s036-no-unsafe-file-include.js +0 -0
  200. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s037-require-anti-cache-headers.js +0 -0
  201. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s038-no-version-disclosure.js +0 -0
  202. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s039-no-session-token-in-url.js +0 -0
  203. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s041-require-session-invalidate-on-logout.js +0 -0
  204. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s042-require-periodic-reauthentication.js +0 -0
  205. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s043-terminate-sessions-on-password-change.js +0 -0
  206. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s044-require-full-session-for-sensitive-operations.js +0 -0
  207. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s045-anti-automation-controls.js +0 -0
  208. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s046-secure-notification-on-auth-change.js +0 -0
  209. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s048-password-credential-recovery.js +0 -0
  210. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s050-session-token-weak-hash.js +0 -0
  211. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s052-secure-random-authentication-code.js +0 -0
  212. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s054-verification-default-account.js +0 -0
  213. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s057-utc-logging.js +0 -0
  214. /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s058-no-ssrf.js +0 -0
  215. /package/rules/{C006_function_naming → common/C006_function_naming}/config.json +0 -0
  216. /package/rules/{C019_log_level_usage → common/C019_log_level_usage}/config.json +0 -0
  217. /package/rules/{C029_catch_block_logging → common/C029_catch_block_logging}/config.json +0 -0
  218. /package/rules/{C031_validation_separation → common/C031_validation_separation}/analyzer.js +0 -0
  219. /package/rules/{C031_validation_separation/README.md → docs/C031_validation_separation.md} +0 -0
@@ -0,0 +1,86 @@
1
+ /**
2
+ * Custom ESLint rule: S003 – Prevent unvalidated redirects
3
+ * Rule ID: custom/s003
4
+ */
5
+
6
+ "use strict";
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: "problem",
11
+ docs: {
12
+ description: "Prevent unvalidated redirects to user-controlled URLs",
13
+ recommended: true,
14
+ },
15
+ schema: [],
16
+ messages: {
17
+ unvalidatedRedirect:
18
+ "Redirect to user-controlled URL '{{target}}' without validation. Use allow list or warning page.",
19
+ },
20
+ },
21
+
22
+ create(context) {
23
+ const userInputSources = [
24
+ "req.query",
25
+ "req.body",
26
+ "input",
27
+ "form",
28
+ "params",
29
+ ];
30
+
31
+ function isUserControlled(nodeText) {
32
+ return userInputSources.some((source) => nodeText.includes(source));
33
+ }
34
+
35
+ function reportIfUserControlled(node, nodeText) {
36
+ if (isUserControlled(nodeText)) {
37
+ context.report({
38
+ node,
39
+ messageId: "unvalidatedRedirect",
40
+ data: { target: nodeText },
41
+ });
42
+ }
43
+ }
44
+
45
+ return {
46
+ CallExpression(node) {
47
+ const callee = node.callee;
48
+
49
+ // res.redirect(...)
50
+ if (
51
+ callee.type === "MemberExpression" &&
52
+ callee.property.name === "redirect" &&
53
+ node.arguments.length > 0
54
+ ) {
55
+ const argText = context.getSourceCode().getText(node.arguments[0]);
56
+ reportIfUserControlled(node, argText);
57
+ }
58
+ },
59
+
60
+ AssignmentExpression(node) {
61
+ const left = node.left;
62
+ const right = node.right;
63
+
64
+ if (
65
+ left.type === "MemberExpression" &&
66
+ left.object.name === "window" &&
67
+ ["location", "location.href"].includes(left.property.name || "") &&
68
+ right
69
+ ) {
70
+ const text = context.getSourceCode().getText(right);
71
+ reportIfUserControlled(node, text);
72
+ }
73
+
74
+ if (
75
+ left.type === "MemberExpression" &&
76
+ left.object.name === "location" &&
77
+ ["href", "replace"].includes(left.property.name || "") &&
78
+ right
79
+ ) {
80
+ const text = context.getSourceCode().getText(right);
81
+ reportIfUserControlled(node, text);
82
+ }
83
+ },
84
+ };
85
+ },
86
+ };
@@ -0,0 +1,74 @@
1
+ /**
2
+ * ESLint rule: S007 – Plaintext OTP Check
3
+ * Rule ID: custom/s007
4
+ * Description: Verify that OTPs are not stored or transmitted in plaintext form.
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description: 'Verify that OTPs are not stored or transmitted in plaintext form.',
14
+ recommended: true,
15
+ url: 'https://owasp.org/www-community/vulnerabilities/Insecure_Storage',
16
+ },
17
+ messages: {
18
+ plaintextOtp:
19
+ 'OTP should not be stored or transmitted in plaintext. Consider hashing or encrypting it.',
20
+ },
21
+ schema: [],
22
+ },
23
+
24
+ create(context) {
25
+ return {
26
+ Property(node) {
27
+ if (
28
+ node.key &&
29
+ node.key.type === 'Identifier' &&
30
+ /otp/i.test(node.key.name) &&
31
+ node.value &&
32
+ node.value.type === 'Identifier' &&
33
+ !/hash|encrypt|bcrypt|sha/i.test(node.value.name)
34
+ ) {
35
+ context.report({
36
+ node,
37
+ messageId: 'plaintextOtp',
38
+ });
39
+ }
40
+ },
41
+
42
+ CallExpression(node) {
43
+ const calleeName = getCalleeName(node.callee);
44
+ if (!calleeName) return;
45
+
46
+ if (/(insert|update|save|query|create)/i.test(calleeName)) {
47
+ for (const arg of node.arguments) {
48
+ const text = context.getSourceCode().getText(arg);
49
+ if (/otp/i.test(text) && !/hash|encrypt|bcrypt|sha/i.test(text)) {
50
+ context.report({
51
+ node: arg,
52
+ messageId: 'plaintextOtp',
53
+ });
54
+ }
55
+ }
56
+ }
57
+ },
58
+ };
59
+ },
60
+ };
61
+
62
+ // ===== Helper function =====
63
+
64
+ function getCalleeName(callee) {
65
+ if (callee.type === 'Identifier') {
66
+ return callee.name;
67
+ } else if (
68
+ callee.type === 'MemberExpression' &&
69
+ callee.property.type === 'Identifier'
70
+ ) {
71
+ return callee.property.name;
72
+ }
73
+ return null;
74
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * ESLint rule: S013 – Enforce TLS Usage
3
+ * Rule ID: custom-tls/s013
4
+ */
5
+ "use strict";
6
+
7
+ const HTTP_REGEX = /^http:\/\//i;
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "problem",
12
+ docs: {
13
+ description:
14
+ "Ensure all client connections use TLS (HTTPS) and prevent unencrypted (HTTP) connections.",
15
+ recommended: true,
16
+ },
17
+ messages: {
18
+ insecureUrl:
19
+ "Unencrypted connection detected (http://). Always use HTTPS (TLS).",
20
+ },
21
+ schema: [],
22
+ },
23
+
24
+ create(context) {
25
+ /** Báo lỗi nếu node là string chứa http:// */
26
+ function reportIfHttp(node, text) {
27
+ if (HTTP_REGEX.test(text)) {
28
+ context.report({ node, messageId: "insecureUrl" });
29
+ }
30
+ }
31
+
32
+ return {
33
+ // String literal
34
+ Literal(node) {
35
+ if (typeof node.value === "string") reportIfHttp(node, node.value);
36
+ },
37
+
38
+ // Template “thuần” (không có ${...})
39
+ TemplateLiteral(node) {
40
+ if (node.expressions.length === 0) {
41
+ const raw = node.quasis.map(q => q.value.raw).join("");
42
+ reportIfHttp(node, raw);
43
+ }
44
+ },
45
+ };
46
+ },
47
+ };
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * S047 – Verify system generated initial passwords or activation codes SHOULD be securely randomly generated
5
+ * OWASP ASVS 2.3.1
6
+ * Rule ID: custom/s047
7
+ * SHOULD be at least 6 characters long, and MAY contain letters and numbers, and expire after a short period of time. These initial secrets must not be permitted to become the long term password.
8
+ */
9
+
10
+
11
+ module.exports = {
12
+ meta: {
13
+ type: "problem",
14
+ docs: {
15
+ description:
16
+ "Initial passwords or activation codes must be securely generated, at least 6 characters, expire soon, and not reused long-term.",
17
+ recommended: false,
18
+ },
19
+ messages: {
20
+ weakInitialSecret:
21
+ "Initial secret (password or activation code) must be securely randomly generated, at least 6 characters, expire soon, and not reused long-term.",
22
+ },
23
+ schema: [],
24
+ },
25
+
26
+ create(context) {
27
+ const SECRET_NAME_REGEX = /(initial|activation|temp).*?(password|code|token|secret)/i;
28
+
29
+ function isPotentialSecretName(name) {
30
+ return SECRET_NAME_REGEX.test(name);
31
+ }
32
+
33
+ function checkInsecureGeneration(node) {
34
+ const callee = node.callee;
35
+
36
+ // Check for Math.random()
37
+ if (
38
+ callee.type === "MemberExpression" &&
39
+ callee.object.type === "Identifier" &&
40
+ callee.object.name === "Math" &&
41
+ callee.property.type === "Identifier" &&
42
+ callee.property.name === "random"
43
+ ) {
44
+ context.report({ node, messageId: "weakInitialSecret" });
45
+ }
46
+
47
+ // Check for Date.now()
48
+ if (
49
+ callee.type === "MemberExpression" &&
50
+ callee.object.type === "Identifier" &&
51
+ callee.object.name === "Date" &&
52
+ callee.property.type === "Identifier" &&
53
+ callee.property.name === "now"
54
+ ) {
55
+ context.report({ node, messageId: "weakInitialSecret" });
56
+ }
57
+ }
58
+
59
+ return {
60
+ VariableDeclarator(node) {
61
+ if (
62
+ node.id.type === "Identifier" &&
63
+ isPotentialSecretName(node.id.name) &&
64
+ node.init
65
+ ) {
66
+ if (node.init.type === "Literal") {
67
+ const val = node.init.value;
68
+ if (typeof val === "string" && val.length < 6) {
69
+ context.report({ node, messageId: "weakInitialSecret" });
70
+ }
71
+ }
72
+
73
+ if (
74
+ node.init.type === "CallExpression" ||
75
+ node.init.type === "NewExpression"
76
+ ) {
77
+ checkInsecureGeneration(node.init);
78
+ }
79
+ }
80
+ },
81
+
82
+ AssignmentExpression(node) {
83
+ const left = node.left;
84
+ const right = node.right;
85
+
86
+ if (
87
+ left.type === "MemberExpression" &&
88
+ left.property.type === "Identifier" &&
89
+ isPotentialSecretName(left.property.name)
90
+ ) {
91
+ if (right.type === "Literal") {
92
+ const val = right.value;
93
+ if (typeof val === "string" && val.length < 6) {
94
+ context.report({ node, messageId: "weakInitialSecret" });
95
+ }
96
+ }
97
+
98
+ if (
99
+ right.type === "CallExpression" ||
100
+ right.type === "NewExpression"
101
+ ) {
102
+ checkInsecureGeneration(right);
103
+ }
104
+ }
105
+ },
106
+ };
107
+ },
108
+ };
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * S055 – Verify that REST services explicitly check the incoming Content-Type to be the expected one, such as application/xml or application/json.
5
+ * OWASP ASVS 13.2.5
6
+ * Rule ID: custom/s055
7
+ * Verify that REST services explicitly check the incoming Content-Type to be the expected one, such as application/xml or application/json.
8
+ */
9
+
10
+ module.exports = {
11
+ meta: {
12
+ type: "problem",
13
+ docs: {
14
+ description:
15
+ "Ensure REST request handlers validate Content-Type when using req.body",
16
+ recommended: false,
17
+ },
18
+ messages: {
19
+ missingCheck:
20
+ "Handler uses req.body but does not validate Content-Type.",
21
+ },
22
+ schema: [],
23
+ },
24
+
25
+ create(context) {
26
+ function walkForReqBody(node, reqName = "req") {
27
+ const visited = new Set();
28
+ let found = false;
29
+
30
+ function walk(n) {
31
+ if (!n || typeof n !== "object" || visited.has(n) || found) return;
32
+ visited.add(n);
33
+
34
+ if (
35
+ n.type === "MemberExpression" &&
36
+ n.object.type === "Identifier" &&
37
+ n.object.name === reqName &&
38
+ n.property.type === "Identifier" &&
39
+ n.property.name === "body"
40
+ ) {
41
+ found = true;
42
+ return;
43
+ }
44
+
45
+ for (const key in n) {
46
+ if (!Object.prototype.hasOwnProperty.call(n, key)) continue;
47
+ const child = n[key];
48
+ if (Array.isArray(child)) child.forEach(walk);
49
+ else if (typeof child === "object" && child !== null) walk(child);
50
+ }
51
+ }
52
+
53
+ walk(node);
54
+ return found;
55
+ }
56
+
57
+ function walkForContentTypeCheck(node, reqName = "req") {
58
+ const visited = new Set();
59
+
60
+ function walk(n) {
61
+ if (!n || typeof n !== "object" || visited.has(n)) return false;
62
+ visited.add(n);
63
+
64
+ // req.is("application/json")
65
+ if (
66
+ n.type === "CallExpression" &&
67
+ n.callee.type === "MemberExpression" &&
68
+ n.callee.object.type === "Identifier" &&
69
+ n.callee.object.name === reqName &&
70
+ n.callee.property.name === "is" &&
71
+ n.arguments.length > 0 &&
72
+ n.arguments[0].type === "Literal" &&
73
+ typeof n.arguments[0].value === "string" &&
74
+ n.arguments[0].value.startsWith("application/")
75
+ ) {
76
+ return true;
77
+ }
78
+
79
+ // req.headers["content-type"]
80
+ if (
81
+ n.type === "MemberExpression" &&
82
+ n.object?.type === "MemberExpression" &&
83
+ n.object.object?.type === "Identifier" &&
84
+ n.object.object.name === reqName &&
85
+ n.object.property?.name === "headers" &&
86
+ (
87
+ (n.property.type === "Literal" && n.property.value === "content-type") ||
88
+ (n.property.type === "Identifier" && n.property.name === "content-type")
89
+ )
90
+ ) {
91
+ return true;
92
+ }
93
+
94
+ // Look through children
95
+ for (const key in n) {
96
+ if (!Object.prototype.hasOwnProperty.call(n, key)) continue;
97
+ const child = n[key];
98
+ if (Array.isArray(child)) {
99
+ if (child.some(walk)) return true;
100
+ } else if (typeof child === "object" && child !== null) {
101
+ if (walk(child)) return true;
102
+ }
103
+ }
104
+
105
+ return false;
106
+ }
107
+
108
+ return walk(node);
109
+ }
110
+
111
+ return {
112
+ CallExpression(node) {
113
+ // Match app.post("/path", handler)
114
+ if (
115
+ node.callee.type === "MemberExpression" &&
116
+ ["post", "put", "patch", "delete"].includes(
117
+ node.callee.property.name
118
+ )
119
+ ) {
120
+ const handler = node.arguments.find(
121
+ (arg) =>
122
+ arg.type === "FunctionExpression" ||
123
+ arg.type === "ArrowFunctionExpression"
124
+ );
125
+
126
+ if (!handler || !handler.body) return;
127
+
128
+ const reqName = handler.params[0]?.name || "req";
129
+
130
+ const usesBody = walkForReqBody(handler.body, reqName);
131
+ const hasValidation = walkForContentTypeCheck(handler.body, reqName);
132
+
133
+ if (usesBody && !hasValidation) {
134
+ context.report({
135
+ node: handler,
136
+ messageId: "missingCheck",
137
+ });
138
+ }
139
+ }
140
+ },
141
+ };
142
+ },
143
+ };
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Custom ESLint rule for: T002 – Interface names should start with 'I'
3
+ * Rule ID: custom/t002
4
+ * Purpose: Enforce consistent interface naming convention by requiring the 'I' prefix
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "Interface names should start with 'I'",
14
+ recommended: false
15
+ },
16
+ fixable: "code",
17
+ schema: [],
18
+ messages: {
19
+ interfacePrefix: "Interface name '{{name}}' should start with 'I'"
20
+ }
21
+ },
22
+
23
+ create(context) {
24
+ return {
25
+ TSInterfaceDeclaration(node) {
26
+ const interfaceName = node.id.name;
27
+ if (!interfaceName.startsWith("I")) {
28
+ context.report({
29
+ node: node.id,
30
+ messageId: "interfacePrefix",
31
+ data: {
32
+ name: interfaceName,
33
+ },
34
+ fix(fixer) {
35
+ return fixer.replaceText(node.id, `I${interfaceName}`);
36
+ },
37
+ });
38
+ }
39
+ },
40
+ };
41
+ },
42
+ };
@@ -0,0 +1,48 @@
1
+ /**
2
+ * Custom ESLint rule for: T003 – Avoid using @ts-ignore without justification
3
+ * Rule ID: custom/t003
4
+ * Purpose: Require clear justification when using @ts-ignore comments
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "Avoid using @ts-ignore without a justification",
14
+ recommended: false
15
+ },
16
+ fixable: null,
17
+ schema: [],
18
+ messages: {
19
+ tsIgnoreReason: "Please provide a reason for using @ts-ignore"
20
+ }
21
+ },
22
+
23
+ create(context) {
24
+ return {
25
+ Program() {
26
+ const sourceCode = context.getSourceCode();
27
+ const comments = sourceCode.getAllComments();
28
+
29
+ comments.forEach((comment) => {
30
+ if (comment.type === "Line" && comment.value.includes("@ts-ignore")) {
31
+ // Check if there's a justification after @ts-ignore
32
+ const hasJustification = comment.value
33
+ .replace("@ts-ignore", "")
34
+ .trim()
35
+ .length > 0;
36
+
37
+ if (!hasJustification) {
38
+ context.report({
39
+ node: comment,
40
+ messageId: "tsIgnoreReason",
41
+ });
42
+ }
43
+ }
44
+ });
45
+ },
46
+ };
47
+ },
48
+ };
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Custom ESLint rule for: T019 – Disallow empty type definitions
3
+ * Rule ID: custom/t019
4
+ * Purpose: Prevent empty type definitions and suggest more specific types
5
+ */
6
+
7
+ "use strict";
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: "suggestion",
12
+ docs: {
13
+ description: "Disallow empty type definitions",
14
+ category: "TypeScript",
15
+ recommended: true,
16
+ },
17
+ fixable: "code",
18
+ schema: [],
19
+ messages: {
20
+ emptyType: "Empty type definition is not allowed. Add at least one property or use a more specific type.",
21
+ },
22
+ },
23
+
24
+ create(context) {
25
+ // Helper function to get a more specific type suggestion based on the name
26
+ function getSuggestedType(name) {
27
+ const lowerName = name.toLowerCase();
28
+
29
+ // Suggest more specific types based on common naming patterns
30
+ if (lowerName.includes("config") || lowerName.includes("options")) {
31
+ return "Record<string, unknown>";
32
+ }
33
+ if (lowerName.includes("props")) {
34
+ return "Record<string, React.ReactNode>";
35
+ }
36
+ if (lowerName.includes("state")) {
37
+ return "Record<string, unknown>";
38
+ }
39
+ if (lowerName.includes("params")) {
40
+ return "Record<string, string | number | boolean>";
41
+ }
42
+ if (lowerName.includes("result") || lowerName.includes("response")) {
43
+ return "Record<string, unknown>";
44
+ }
45
+ if (lowerName.includes("event") || lowerName.includes("handler")) {
46
+ return "(...args: unknown[]) => void";
47
+ }
48
+ if (lowerName.includes("callback") || lowerName.includes("fn")) {
49
+ return "(...args: unknown[]) => unknown";
50
+ }
51
+ if (lowerName.includes("data") || lowerName.includes("item")) {
52
+ return "Record<string, unknown>";
53
+ }
54
+
55
+ // Default suggestion
56
+ return "Record<string, unknown>";
57
+ }
58
+
59
+ return {
60
+ TSInterfaceDeclaration(node) {
61
+ if (node.body.body.length === 0) {
62
+ const suggestedType = getSuggestedType(node.id.name);
63
+ context.report({
64
+ node: node.body,
65
+ messageId: "emptyType",
66
+ fix(fixer) {
67
+ return fixer.replaceText(
68
+ node,
69
+ `type ${node.id.name} = ${suggestedType};`
70
+ );
71
+ },
72
+ });
73
+ }
74
+ },
75
+ TSTypeAliasDeclaration(node) {
76
+ if (
77
+ node.typeAnnotation.type === "TSTypeLiteral" &&
78
+ node.typeAnnotation.members.length === 0
79
+ ) {
80
+ const suggestedType = getSuggestedType(node.id.name);
81
+ context.report({
82
+ node: node.typeAnnotation,
83
+ messageId: "emptyType",
84
+ fix(fixer) {
85
+ return fixer.replaceText(
86
+ node.typeAnnotation,
87
+ suggestedType
88
+ );
89
+ },
90
+ });
91
+ }
92
+ },
93
+ };
94
+ },
95
+ };
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Custom ESLint rule for: T007 – Avoid declaring functions inside constructors or class bodies
3
+ * Rule ID: custom/t007
4
+ * Purpose: Discourage function declarations within constructors or class methods
5
+ */
6
+
7
+ module.exports = {
8
+ meta: {
9
+ type: "problem",
10
+ docs: {
11
+ description: "Avoid declaring functions inside constructors or class bodies",
12
+ recommended: false
13
+ },
14
+ schema: [],
15
+ messages: {
16
+ noFunctionInConstructor: "Avoid declaring functions inside class constructors.",
17
+ noFunctionInClassBody: "Avoid declaring nested functions inside class body."
18
+ }
19
+ },
20
+ create(context) {
21
+ return {
22
+ MethodDefinition(node) {
23
+ if (node.kind === "constructor" && node.value && node.value.body && node.value.body.body) {
24
+ const constructorBody = node.value.body.body;
25
+ constructorBody.forEach(element => {
26
+ if (element.type === "FunctionDeclaration" || element.type === "FunctionExpression") {
27
+ context.report({
28
+ node: element,
29
+ messageId: "noFunctionInConstructor"
30
+ });
31
+ }
32
+ });
33
+ }
34
+ },
35
+ ClassBody(node) {
36
+ node.body.forEach(element => {
37
+ if (element.type === "MethodDefinition" && element.value && element.value.body && element.value.body.body) {
38
+ const methodBody = element.value.body.body;
39
+ methodBody.forEach(subNode => {
40
+ if (subNode.type === "FunctionDeclaration") {
41
+ context.report({
42
+ node: subNode,
43
+ messageId: "noFunctionInClassBody"
44
+ });
45
+ }
46
+ });
47
+ }
48
+ });
49
+ }
50
+ };
51
+ }
52
+ };