@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,254 @@
1
+ /**
2
+ * Custom ESLint rule for: C076 – Each test should assert only one behavior
3
+ * Rule ID: custom/c076
4
+ * Purpose: Ensure test functions focus on testing a single behavior/aspect
5
+ * Note: Similar to C072 but with broader scope and different focus
6
+ */
7
+
8
+ module.exports = {
9
+ meta: {
10
+ type: "suggestion",
11
+ docs: {
12
+ description: "Each test should assert only one behavior to maintain focus and clarity",
13
+ recommended: true,
14
+ category: "Testing"
15
+ },
16
+ schema: [
17
+ {
18
+ type: "object",
19
+ properties: {
20
+ maxAssertions: {
21
+ type: "integer",
22
+ minimum: 1,
23
+ default: 1
24
+ },
25
+ allowSetupAssertions: {
26
+ type: "boolean",
27
+ default: true
28
+ },
29
+ assertionPatterns: {
30
+ type: "array",
31
+ items: {
32
+ type: "string"
33
+ },
34
+ default: ["expect", "assert", "should"]
35
+ }
36
+ },
37
+ additionalProperties: false
38
+ }
39
+ ],
40
+ messages: {
41
+ tooManyAssertions: "Test '{{testName}}' has {{count}} assertions. Each test should focus on one behavior (max {{max}} assertions)",
42
+ multipleBehaviors: "Test '{{testName}}' appears to test multiple behaviors. Consider splitting into separate test cases"
43
+ }
44
+ },
45
+ create(context) {
46
+ const options = context.options[0] || {};
47
+ const maxAssertions = options.maxAssertions || 1;
48
+ const allowSetupAssertions = options.allowSetupAssertions !== false;
49
+ const assertionPatterns = options.assertionPatterns || ["expect", "assert", "should"];
50
+
51
+ function isTestFunction(node) {
52
+ if (!node || !node.callee) return false;
53
+
54
+ if (node.type === "CallExpression") {
55
+ // Direct test/it/describe calls
56
+ if (node.callee.type === "Identifier") {
57
+ return ["test", "it", "describe", "context"].includes(node.callee.name);
58
+ }
59
+
60
+ // Method calls like jest.test, mocha.it, etc.
61
+ if (node.callee.type === "MemberExpression" &&
62
+ node.callee.property &&
63
+ ["test", "it", "describe", "context"].includes(node.callee.property.name)) {
64
+ return true;
65
+ }
66
+ }
67
+ return false;
68
+ }
69
+
70
+ function isSetupFunction(node) {
71
+ if (!node || !node.callee) return false;
72
+
73
+ const setupFunctions = ["beforeEach", "afterEach", "beforeAll", "afterAll", "before", "after"];
74
+
75
+ if (node.type === "CallExpression") {
76
+ if (node.callee.type === "Identifier") {
77
+ return setupFunctions.includes(node.callee.name);
78
+ }
79
+
80
+ if (node.callee.type === "MemberExpression" &&
81
+ node.callee.property &&
82
+ setupFunctions.includes(node.callee.property.name)) {
83
+ return true;
84
+ }
85
+ }
86
+ return false;
87
+ }
88
+
89
+ function isAssertionCall(node) {
90
+ if (!node || node.type !== "CallExpression" || !node.callee) {
91
+ return false;
92
+ }
93
+
94
+ // Direct assertion calls
95
+ if (node.callee.type === "Identifier") {
96
+ return assertionPatterns.includes(node.callee.name);
97
+ }
98
+
99
+ // Method calls like chai.expect, jest.expect, etc.
100
+ if (node.callee.type === "MemberExpression" && node.callee.property) {
101
+ return assertionPatterns.includes(node.callee.property.name);
102
+ }
103
+
104
+ return false;
105
+ }
106
+
107
+ function countAssertions(testBody, testName) {
108
+ let assertionCount = 0;
109
+ let setupAssertionCount = 0;
110
+ let hasMultipleBehaviorIndicators = false;
111
+
112
+ function traverse(node) {
113
+ if (!node || typeof node !== 'object') return;
114
+
115
+ // Count assertions
116
+ if (isAssertionCall(node)) {
117
+ // Check if this assertion is in setup/teardown
118
+ let parent = node;
119
+ let inSetup = false;
120
+ while (parent && parent.parent) {
121
+ parent = parent.parent;
122
+ if (parent.type === "CallExpression" && isSetupFunction(parent)) {
123
+ inSetup = true;
124
+ break;
125
+ }
126
+ }
127
+
128
+ if (inSetup) {
129
+ setupAssertionCount++;
130
+ } else {
131
+ assertionCount++;
132
+ }
133
+ }
134
+
135
+ // Look for multiple behavior indicators
136
+ if (node.type === "CallExpression") {
137
+ // Multiple nested test functions indicate multiple behaviors
138
+ if (isTestFunction(node) && node !== testBody.parent) {
139
+ hasMultipleBehaviorIndicators = true;
140
+ }
141
+ }
142
+
143
+ // Look for comment patterns that suggest multiple behaviors
144
+ if (node.type === "ExpressionStatement" && node.leadingComments) {
145
+ const comments = node.leadingComments.map(c => c.value.toLowerCase());
146
+ const behaviorKeywords = ["and", "also", "then", "next", "additionally", "furthermore"];
147
+ if (comments.some(comment =>
148
+ behaviorKeywords.some(keyword => comment.includes(keyword)))) {
149
+ hasMultipleBehaviorIndicators = true;
150
+ }
151
+ }
152
+
153
+ // Recursively check child nodes, but don't go into nested test functions
154
+ for (const key in node) {
155
+ if (key === 'parent' || key === 'range' || key === 'loc') continue;
156
+
157
+ const child = node[key];
158
+ if (Array.isArray(child)) {
159
+ child.forEach(item => {
160
+ if (item && typeof item === 'object' && item.type) {
161
+ if (!(item.type === "CallExpression" && isTestFunction(item))) {
162
+ traverse(item);
163
+ }
164
+ }
165
+ });
166
+ } else if (child && typeof child === 'object' && child.type) {
167
+ if (!(child.type === "CallExpression" && isTestFunction(child))) {
168
+ traverse(child);
169
+ }
170
+ }
171
+ }
172
+ }
173
+
174
+ traverse(testBody);
175
+
176
+ return {
177
+ assertions: assertionCount,
178
+ setupAssertions: setupAssertionCount,
179
+ hasMultipleBehaviors: hasMultipleBehaviorIndicators
180
+ };
181
+ }
182
+
183
+ return {
184
+ CallExpression(node) {
185
+ // Only check test function calls, not describe blocks
186
+ if (!isTestFunction(node) || (node.callee.type === "Identifier" && node.callee.name === "describe")) {
187
+ return;
188
+ }
189
+
190
+ // Skip describe blocks
191
+ if (node.callee.type === "Identifier" && ["describe", "context"].includes(node.callee.name)) {
192
+ return;
193
+ }
194
+
195
+ // Must have test name and callback
196
+ if (!node.arguments || node.arguments.length < 2) return;
197
+
198
+ const testName = node.arguments[0];
199
+ const testCallback = node.arguments[1];
200
+
201
+ if (!testCallback ||
202
+ (testCallback.type !== "FunctionExpression" &&
203
+ testCallback.type !== "ArrowFunctionExpression")) {
204
+ return;
205
+ }
206
+
207
+ const testNameStr = testName.type === "Literal" ? testName.value :
208
+ testName.type === "TemplateLiteral" ? "template" : "unnamed";
209
+
210
+ // Get function body
211
+ const fnBody = testCallback.body;
212
+ if (!fnBody) return;
213
+
214
+ // Handle both block statements and expression bodies
215
+ let bodyToCheck = fnBody;
216
+ if (testCallback.type === "ArrowFunctionExpression" && fnBody.type !== "BlockStatement") {
217
+ bodyToCheck = { type: "BlockStatement", body: [{ type: "ExpressionStatement", expression: fnBody }] };
218
+ }
219
+
220
+ // Count assertions and analyze behavior
221
+ const analysis = countAssertions(bodyToCheck, testNameStr);
222
+
223
+ // Calculate effective assertions (exclude setup if allowed)
224
+ const effectiveAssertions = allowSetupAssertions ?
225
+ analysis.assertions :
226
+ analysis.assertions + analysis.setupAssertions;
227
+
228
+ // Report if too many assertions
229
+ if (effectiveAssertions > maxAssertions) {
230
+ context.report({
231
+ node,
232
+ messageId: "tooManyAssertions",
233
+ data: {
234
+ testName: testNameStr,
235
+ count: effectiveAssertions,
236
+ max: maxAssertions
237
+ }
238
+ });
239
+ }
240
+
241
+ // Report if multiple behaviors detected
242
+ if (analysis.hasMultipleBehaviors) {
243
+ context.report({
244
+ node,
245
+ messageId: "multipleBehaviors",
246
+ data: {
247
+ testName: testNameStr
248
+ }
249
+ });
250
+ }
251
+ }
252
+ };
253
+ }
254
+ };
@@ -0,0 +1,381 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * S001 – Verify that if there is an error in access control, the system fails securely
5
+ * OWASP ASVS 4.14
6
+ * Verify that the principle of least privilege exists - users should only be able to access functions, data files, URLs, controllers, services, and other resources, for which they possess specific authorization. This implies protection against spoofing and elevation of privilege.
7
+ */
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: 'problem',
12
+ docs: {
13
+ description: 'Verify that if there is an error in access control, the system fails securely (Fail Securely)',
14
+ category: 'Security',
15
+ recommended: true,
16
+ url: 'https://owasp.org/www-project-top-ten/2017/A5_2017-Broken_Access_Control'
17
+ },
18
+ fixable: 'code',
19
+ schema: [
20
+ {
21
+ type: 'object',
22
+ properties: {
23
+ checkMethods: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'string'
27
+ },
28
+ default: ['authenticate', 'authorize', 'checkPermission', 'validateAccess']
29
+ },
30
+ allowedFailureMethods: {
31
+ type: 'array',
32
+ items: {
33
+ type: 'string'
34
+ },
35
+ default: ['deny', 'reject', 'forbid', 'unauthorized', 'forbidden']
36
+ }
37
+ },
38
+ additionalProperties: false
39
+ }
40
+ ],
41
+ messages: {
42
+ failInsecurely: 'Access control does not fail securely. On error, the system should deny access instead of allowing.',
43
+ missingErrorHandling: 'Missing error handling in access control. There must be a try-catch block or error handling.',
44
+ defaultAllowAccess: 'Should not allow access by default. The system should deny access when unsure.',
45
+ catchAllowsAccess: 'Catch block should not allow access. Deny access on error.',
46
+ missingFallback: 'Missing fallback case. There should be a default case to deny access.',
47
+ unsafeDefaultReturn: 'Default return statement is not secure. Should return false or deny access.'
48
+ }
49
+ },
50
+
51
+ create(context) {
52
+ const options = context.options[0] || {};
53
+ const checkMethods = options.checkMethods || ['authenticate', 'authorize', 'checkPermission', 'validateAccess'];
54
+ const allowedFailureMethods = options.allowedFailureMethods || ['deny', 'reject', 'forbid', 'unauthorized', 'forbidden'];
55
+
56
+ // Keywords related to access control
57
+ const accessControlKeywords = [
58
+ 'authenticate', 'authorize', 'permission', 'access', 'role', 'auth',
59
+ 'login', 'verify', 'check', 'validate', 'guard', 'middleware',
60
+ 'canAccess', 'isAuthorized', 'hasPermission', 'checkAccess'
61
+ ];
62
+
63
+ // Keywords for allowing access
64
+ const allowKeywords = [
65
+ 'allow', 'permit', 'grant', 'enable', 'accept', 'approve',
66
+ 'authorized', 'authenticated', 'allowed', 'granted', 'success'
67
+ ];
68
+
69
+ // Keywords for denying access
70
+ const denyKeywords = [
71
+ 'deny', 'reject', 'forbid', 'block', 'refuse', 'unauthorized',
72
+ 'forbidden', 'denied', 'blocked', 'refused', 'error', 'failed'
73
+ ];
74
+
75
+ function isAccessControlFunction(node) {
76
+ const functionName = getFunctionName(node);
77
+ if (!functionName) return false;
78
+
79
+ return accessControlKeywords.some(keyword =>
80
+ functionName.toLowerCase().includes(keyword.toLowerCase())
81
+ );
82
+ }
83
+
84
+ function getFunctionName(node) {
85
+ if (!node || !node.type) {
86
+ return null;
87
+ }
88
+
89
+ if (node.type === 'FunctionDeclaration' && node.id) {
90
+ return node.id.name;
91
+ }
92
+ if (node.type === 'FunctionExpression' || node.type === 'ArrowFunctionExpression') {
93
+ const parent = node.parent;
94
+ if (!parent || !parent.type) {
95
+ return null;
96
+ }
97
+ if (parent.type === 'VariableDeclarator' && parent.id) {
98
+ return parent.id.name;
99
+ }
100
+ if (parent.type === 'Property' && parent.key) {
101
+ return parent.key.name;
102
+ }
103
+ if (parent.type === 'MethodDefinition' && parent.key) {
104
+ return parent.key.name;
105
+ }
106
+ }
107
+ return null;
108
+ }
109
+
110
+ function containsKeywords(text, keywords) {
111
+ return keywords.some(keyword =>
112
+ text.toLowerCase().includes(keyword.toLowerCase())
113
+ );
114
+ }
115
+
116
+ function getReturnValue(node) {
117
+ if (!node || !node.type) {
118
+ return null;
119
+ }
120
+
121
+ switch (node.type) {
122
+ case 'Literal':
123
+ return String(node.value);
124
+ case 'Identifier':
125
+ return node.name;
126
+ case 'MemberExpression':
127
+ return getFullMemberExpression(node);
128
+ case 'ObjectExpression':
129
+ return getObjectExpressionValue(node);
130
+ case 'ConditionalExpression':
131
+ return 'conditional';
132
+ default:
133
+ return null;
134
+ }
135
+ }
136
+
137
+ function getFullMemberExpression(node) {
138
+ const object = node.object.type === 'Identifier' ? node.object.name : 'unknown';
139
+ const property = node.property.type === 'Identifier' ? node.property.name : 'unknown';
140
+ return `${object}.${property}`;
141
+ }
142
+
143
+ function getObjectExpressionValue(node) {
144
+ const properties = node.properties.map(prop => {
145
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
146
+ return prop.key.name;
147
+ }
148
+ return '';
149
+ }).filter(Boolean);
150
+ return properties.join('.');
151
+ }
152
+
153
+ function isInAccessControlFunction(node) {
154
+ let parent = node.parent;
155
+ while (parent) {
156
+ if (isAccessControlFunction(parent)) {
157
+ return true;
158
+ }
159
+ parent = parent.parent;
160
+ }
161
+ return false;
162
+ }
163
+
164
+ function isUnsafeReturn(returnValue) {
165
+ if (!returnValue) return false;
166
+
167
+ return returnValue === 'true' ||
168
+ returnValue === 'success' ||
169
+ containsKeywords(returnValue, allowKeywords);
170
+ }
171
+
172
+ function isSafeReturn(returnValue) {
173
+ if (!returnValue) return false;
174
+
175
+ return returnValue === 'false' ||
176
+ returnValue === 'error' ||
177
+ containsKeywords(returnValue, denyKeywords);
178
+ }
179
+
180
+ function checkTryStatement(node) {
181
+ if (!isAccessControlFunction(getParentFunction(node))) return;
182
+
183
+ const catchClause = node.handler;
184
+ if (!catchClause) {
185
+ context.report({
186
+ node,
187
+ messageId: 'missingErrorHandling'
188
+ });
189
+ return;
190
+ }
191
+
192
+ // Check catch block
193
+ const catchBody = catchClause.body;
194
+ if (catchBody.type === 'BlockStatement') {
195
+ const hasUnsafeReturn = catchBody.body.some(stmt => {
196
+ if (stmt.type === 'ReturnStatement' && stmt.argument) {
197
+ const returnValue = getReturnValue(stmt.argument);
198
+ return isUnsafeReturn(returnValue);
199
+ }
200
+ return false;
201
+ });
202
+
203
+ if (hasUnsafeReturn) {
204
+ context.report({
205
+ node: catchClause,
206
+ messageId: 'catchAllowsAccess'
207
+ });
208
+ }
209
+
210
+ // Check for safe handling (throw or safe return)
211
+ const hasSafeHandling = catchBody.body.some(stmt => {
212
+ if (stmt.type === 'ThrowStatement') return true;
213
+ if (stmt.type === 'ReturnStatement' && stmt.argument) {
214
+ const returnValue = getReturnValue(stmt.argument);
215
+ return isSafeReturn(returnValue);
216
+ }
217
+ return false;
218
+ });
219
+
220
+ if (!hasSafeHandling) {
221
+ context.report({
222
+ node: catchClause,
223
+ messageId: 'catchAllowsAccess'
224
+ });
225
+ }
226
+ }
227
+ }
228
+
229
+ function getParentFunction(node) {
230
+ let parent = node.parent;
231
+ while (parent) {
232
+ if (parent.type === 'FunctionDeclaration' ||
233
+ parent.type === 'FunctionExpression' ||
234
+ parent.type === 'ArrowFunctionExpression') {
235
+ return parent;
236
+ }
237
+ parent = parent.parent;
238
+ }
239
+ return null;
240
+ }
241
+
242
+ function checkReturnStatement(node) {
243
+ if (!node.argument || !isInAccessControlFunction(node)) return;
244
+
245
+ const returnValue = getReturnValue(node.argument);
246
+ if (isUnsafeReturn(returnValue)) {
247
+ // Check if inside try block
248
+ const tryBlock = findParentTryBlock(node);
249
+ if (!tryBlock) {
250
+ // Check for guard condition
251
+ if (!hasProperGuardCondition(node)) {
252
+ context.report({
253
+ node,
254
+ messageId: 'unsafeDefaultReturn'
255
+ });
256
+ }
257
+ }
258
+ }
259
+ }
260
+
261
+ function findParentTryBlock(node) {
262
+ let parent = node.parent;
263
+ while (parent) {
264
+ if (parent.type === 'TryStatement') {
265
+ return parent;
266
+ }
267
+ parent = parent.parent;
268
+ }
269
+ return null;
270
+ }
271
+
272
+ function hasProperGuardCondition(node) {
273
+ // Look for if/else conditions before the return statement
274
+ let parent = node.parent;
275
+ while (parent && parent.type === 'BlockStatement') {
276
+ const block = parent;
277
+ const returnIndex = block.body.indexOf(node);
278
+
279
+ if (returnIndex > 0) {
280
+ // Check statements before return
281
+ for (let i = returnIndex - 1; i >= 0; i--) {
282
+ const stmt = block.body[i];
283
+ if (stmt.type === 'IfStatement') {
284
+ const stmtText = context.getSourceCode().getText(stmt);
285
+ if (containsKeywords(stmtText, ['check', 'verify', 'validate', 'auth', 'permission'])) {
286
+ return true;
287
+ }
288
+ }
289
+ }
290
+ }
291
+
292
+ // Check parent if statement
293
+ if (parent.parent && parent.parent.type === 'IfStatement') {
294
+ const ifStmt = parent.parent;
295
+ const ifText = context.getSourceCode().getText(ifStmt.test);
296
+ if (containsKeywords(ifText, ['check', 'verify', 'validate', 'auth', 'permission'])) {
297
+ return true;
298
+ }
299
+ }
300
+
301
+ parent = parent.parent;
302
+ }
303
+ return false;
304
+ }
305
+
306
+ function checkFunction(node) {
307
+ if (!isAccessControlFunction(node)) return;
308
+
309
+ // Check if function has try-catch
310
+ const functionBody = node.body;
311
+ if (functionBody && functionBody.type === 'BlockStatement') {
312
+ const hasTryCatch = functionBody.body.some(stmt => stmt.type === 'TryStatement');
313
+
314
+ if (!hasTryCatch) {
315
+ // Check for throw or error handling
316
+ const hasErrorHandling = functionBody.body.some(stmt => {
317
+ if (stmt.type === 'ThrowStatement') return true;
318
+ if (stmt.type === 'IfStatement') {
319
+ const stmtText = context.getSourceCode().getText(stmt);
320
+ return containsKeywords(stmtText, ['error', 'throw', 'reject']);
321
+ }
322
+ return false;
323
+ });
324
+
325
+ if (!hasErrorHandling) {
326
+ context.report({
327
+ node,
328
+ messageId: 'missingErrorHandling'
329
+ });
330
+ }
331
+ }
332
+
333
+ // Check last return statement
334
+ const lastStatement = functionBody.body[functionBody.body.length - 1];
335
+ if (lastStatement && lastStatement.type === 'ReturnStatement' && lastStatement.argument) {
336
+ const returnValue = getReturnValue(lastStatement.argument);
337
+ if (isUnsafeReturn(returnValue)) {
338
+ context.report({
339
+ node: lastStatement,
340
+ messageId: 'defaultAllowAccess'
341
+ });
342
+ }
343
+ }
344
+ }
345
+ }
346
+
347
+ function checkIfStatement(node) {
348
+ if (!isInAccessControlFunction(node)) return;
349
+
350
+ // Check if there is no else clause
351
+ if (!node.alternate) {
352
+ const consequent = node.consequent;
353
+ if (consequent.type === 'BlockStatement') {
354
+ const hasUnsafeReturn = consequent.body.some(stmt => {
355
+ if (stmt.type === 'ReturnStatement' && stmt.argument) {
356
+ const returnValue = getReturnValue(stmt.argument);
357
+ return isUnsafeReturn(returnValue);
358
+ }
359
+ return false;
360
+ });
361
+
362
+ if (hasUnsafeReturn) {
363
+ context.report({
364
+ node,
365
+ messageId: 'missingFallback'
366
+ });
367
+ }
368
+ }
369
+ }
370
+ }
371
+
372
+ return {
373
+ 'FunctionDeclaration': checkFunction,
374
+ 'FunctionExpression': checkFunction,
375
+ 'ArrowFunctionExpression': checkFunction,
376
+ 'TryStatement': checkTryStatement,
377
+ 'ReturnStatement': checkReturnStatement,
378
+ 'IfStatement': checkIfStatement
379
+ };
380
+ }
381
+ };