@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.
- package/.sunlint.json +35 -0
- package/CHANGELOG.md +30 -3
- package/CONTRIBUTING.md +235 -0
- package/PROJECT_STRUCTURE.md +60 -0
- package/README.md +146 -58
- package/cli.js +1 -0
- package/config/README.md +88 -0
- package/config/defaults/ai-rules-context.json +231 -0
- package/config/engines/engines.json +49 -0
- package/config/engines/eslint-rule-mapping.json +74 -0
- package/config/eslint-rule-mapping.json +126 -0
- package/config/integrations/eslint/base.config.js +125 -0
- package/config/integrations/eslint/simple.config.js +24 -0
- package/config/presets/strict.json +0 -1
- package/config/rule-analysis-strategies.js +74 -0
- package/config/{rules-registry.json → rules/rules-registry.json} +30 -7
- package/core/analysis-orchestrator.js +383 -591
- package/core/ast-modules/README.md +103 -0
- package/core/ast-modules/base-parser.js +90 -0
- package/core/ast-modules/index.js +97 -0
- package/core/ast-modules/package.json +37 -0
- package/core/ast-modules/parsers/eslint-js-parser.js +153 -0
- package/core/ast-modules/parsers/eslint-ts-parser.js +98 -0
- package/core/ast-modules/parsers/javascript-parser.js +187 -0
- package/core/ast-modules/parsers/typescript-parser.js +187 -0
- package/core/cli-action-handler.js +271 -255
- package/core/cli-program.js +18 -4
- package/core/config-manager.js +9 -3
- package/core/config-merger.js +40 -1
- package/core/config-validator.js +2 -2
- package/core/dependency-checker.js +125 -0
- package/core/enhanced-rules-registry.js +331 -0
- package/core/file-targeting-service.js +92 -23
- package/core/interfaces/analysis-engine.interface.js +100 -0
- package/core/multi-rule-runner.js +0 -221
- package/core/output-service.js +1 -1
- package/core/rule-mapping-service.js +1 -1
- package/core/rule-selection-service.js +10 -2
- package/core/smart-installer.js +164 -0
- package/docs/AI.md +163 -0
- package/docs/ARCHITECTURE.md +78 -0
- package/docs/CI-CD-GUIDE.md +315 -0
- package/docs/COMMAND-EXAMPLES.md +256 -0
- package/docs/CONFIGURATION.md +414 -0
- package/docs/DEBUG.md +86 -0
- package/docs/DEPENDENCIES.md +90 -0
- package/docs/DEPLOYMENT-STRATEGIES.md +270 -0
- package/docs/DISTRIBUTION.md +153 -0
- package/docs/ESLINT-INTEGRATION-STRATEGY.md +392 -0
- package/docs/ESLINT_INTEGRATION.md +238 -0
- package/docs/FOLDER_STRUCTURE.md +59 -0
- package/docs/FUTURE_PACKAGES.md +83 -0
- package/docs/HEURISTIC_VS_AI.md +113 -0
- package/docs/PRODUCTION_DEPLOYMENT_ANALYSIS.md +112 -0
- package/docs/PRODUCTION_SIZE_IMPACT.md +183 -0
- package/docs/README.md +32 -0
- package/docs/RELEASE_GUIDE.md +230 -0
- package/engines/eslint-engine.js +610 -0
- package/engines/heuristic-engine.js +864 -0
- package/engines/openai-engine.js +374 -0
- package/engines/tree-sitter-parser.js +0 -0
- package/engines/universal-ast-engine.js +0 -0
- package/integrations/eslint/README.md +99 -0
- package/integrations/eslint/configs/.eslintrc.js +98 -0
- package/integrations/eslint/configs/eslint.config.js +133 -0
- package/integrations/eslint/configs/eslint.config.simple.js +24 -0
- package/integrations/eslint/package.json +23 -0
- package/integrations/eslint/plugin/index.js +164 -0
- package/integrations/eslint/plugin/package.json +13 -0
- package/integrations/eslint/plugin/rules/common/c002-no-duplicate-code.js +204 -0
- package/integrations/eslint/plugin/rules/common/c003-no-vague-abbreviations.js +246 -0
- package/integrations/eslint/plugin/rules/common/c006-function-name-verb-noun.js +216 -0
- package/integrations/eslint/plugin/rules/common/c010-limit-block-nesting.js +90 -0
- package/integrations/eslint/plugin/rules/common/c013-no-dead-code.js +78 -0
- package/integrations/eslint/plugin/rules/common/c014-abstract-dependency-preferred.js +38 -0
- package/integrations/eslint/plugin/rules/common/c017-limit-constructor-logic.js +146 -0
- package/integrations/eslint/plugin/rules/common/c018-no-generic-throw.js +335 -0
- package/integrations/eslint/plugin/rules/common/c023-no-duplicate-variable-name-in-scope.js +142 -0
- package/integrations/eslint/plugin/rules/common/c029-catch-block-logging.js +115 -0
- package/integrations/eslint/plugin/rules/common/c030-use-custom-error-classes.js +294 -0
- package/integrations/eslint/plugin/rules/common/c035-no-empty-catch.js +162 -0
- package/integrations/eslint/plugin/rules/common/c041-no-config-inline.js +122 -0
- package/integrations/eslint/plugin/rules/common/c042-boolean-name-prefix.js +406 -0
- package/integrations/eslint/plugin/rules/common/c043-no-console-or-print.js +300 -0
- package/integrations/eslint/plugin/rules/common/c047-no-duplicate-retry-logic.js +239 -0
- package/integrations/eslint/plugin/rules/common/c072-one-assert-per-test.js +184 -0
- package/integrations/eslint/plugin/rules/common/c075-explicit-function-return-types.js +168 -0
- package/integrations/eslint/plugin/rules/common/c076-single-behavior-per-test.js +254 -0
- package/integrations/eslint/plugin/rules/security/s001-fail-securely.js +381 -0
- package/integrations/eslint/plugin/rules/security/s002-idor-check.js +945 -0
- package/integrations/eslint/plugin/rules/security/s003-no-unvalidated-redirect.js +86 -0
- package/integrations/eslint/plugin/rules/security/s007-no-plaintext-otp.js +74 -0
- package/integrations/eslint/plugin/rules/security/s013-verify-tls-connection.js +47 -0
- package/integrations/eslint/plugin/rules/security/s047-secure-random-passwords.js +108 -0
- package/integrations/eslint/plugin/rules/security/s055-verification-rest-check-the-incoming-content-type.js +143 -0
- package/integrations/eslint/plugin/rules/typescript/t002-interface-prefix-i.js +42 -0
- package/integrations/eslint/plugin/rules/typescript/t003-ts-ignore-reason.js +48 -0
- package/integrations/eslint/plugin/rules/typescript/t004-no-empty-type.js +95 -0
- package/integrations/eslint/plugin/rules/typescript/t007-no-fn-in-constructor.js +52 -0
- package/integrations/eslint/plugin/rules/typescript/t010-no-nested-union-tuple.js +48 -0
- package/integrations/eslint/plugin/rules/typescript/t019-no-this-assign.js +81 -0
- package/integrations/eslint/plugin/rules/typescript/t020-no-default-multi-export.js +127 -0
- package/integrations/eslint/plugin/rules/typescript/t021-limit-nested-generics.js +150 -0
- package/integrations/eslint/tsconfig.json +27 -0
- package/package.json +61 -21
- package/rules/README.md +252 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +65 -0
- package/rules/common/C002_no_duplicate_code/config.json +23 -0
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +418 -0
- package/rules/common/C003_no_vague_abbreviations/config.json +35 -0
- package/rules/{C006_function_naming → common/C006_function_naming}/analyzer.js +13 -2
- package/rules/common/C010_limit_block_nesting/analyzer.js +389 -0
- package/rules/common/C013_no_dead_code/analyzer.js +206 -0
- package/rules/common/C014_dependency_injection/analyzer.js +338 -0
- package/rules/common/C017_constructor_logic/analyzer.js +314 -0
- package/rules/{C019_log_level_usage → common/C019_log_level_usage}/analyzer.js +5 -2
- package/rules/{C029_catch_block_logging → common/C029_catch_block_logging}/analyzer.js +49 -15
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +292 -0
- package/rules/common/C042_boolean_name_prefix/analyzer.js +300 -0
- package/rules/common/C043_no_console_or_print/analyzer.js +304 -0
- package/rules/common/C047_no_duplicate_retry_logic/analyzer.js +351 -0
- package/rules/common/C075_explicit_return_types/analyzer.js +103 -0
- package/rules/common/C076_single_test_behavior/analyzer.js +121 -0
- package/rules/docs/C002_no_duplicate_code.md +57 -0
- package/rules/index.js +149 -0
- package/rules/migration/converter.js +385 -0
- package/rules/migration/mapping.json +164 -0
- package/rules/security/S026_json_schema_validation/analyzer.js +251 -0
- package/rules/security/S026_json_schema_validation/config.json +27 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +263 -0
- package/rules/security/S027_no_hardcoded_secrets/config.json +29 -0
- package/rules/security/S029_csrf_protection/analyzer.js +264 -0
- package/rules/tests/C002_no_duplicate_code.test.js +50 -0
- package/rules/universal/C010/generic.js +0 -0
- package/rules/universal/C010/tree-sitter-analyzer.js +0 -0
- package/rules/utils/ast-utils.js +191 -0
- package/rules/utils/base-analyzer.js +98 -0
- package/rules/utils/pattern-matchers.js +239 -0
- package/rules/utils/rule-helpers.js +264 -0
- package/rules/utils/severity-constants.js +93 -0
- package/scripts/build-release.sh +117 -0
- package/scripts/ci-report.js +179 -0
- package/scripts/install.sh +196 -0
- package/scripts/manual-release.sh +338 -0
- package/scripts/merge-reports.js +424 -0
- package/scripts/pre-release-test.sh +175 -0
- package/scripts/prepare-release.sh +202 -0
- package/scripts/setup-github-registry.sh +42 -0
- package/scripts/test-scripts/README.md +22 -0
- package/scripts/test-scripts/test-c041-comparison.js +114 -0
- package/scripts/test-scripts/test-c041-eslint.js +67 -0
- package/scripts/test-scripts/test-eslint-rules.js +146 -0
- package/scripts/test-scripts/test-real-world.js +44 -0
- package/scripts/test-scripts/test-rules-on-real-projects.js +86 -0
- package/scripts/trigger-release.sh +285 -0
- package/scripts/validate-rule-structure.js +148 -0
- package/scripts/verify-install.sh +82 -0
- package/config/sunlint-schema.json +0 -159
- package/config/typescript/custom-rules.js +0 -9
- package/config/typescript/package-lock.json +0 -1585
- package/config/typescript/package.json +0 -13
- package/config/typescript/security-rules/index.js +0 -90
- package/config/typescript/tsconfig.json +0 -29
- package/core/ai-analyzer.js +0 -169
- package/core/eslint-engine-service.js +0 -312
- package/core/eslint-instance-manager.js +0 -104
- package/core/eslint-integration-service.js +0 -363
- package/core/sunlint-engine-service.js +0 -23
- package/core/typescript-analyzer.js +0 -262
- package/core/typescript-engine.js +0 -313
- /package/config/{default.json → defaults/default.json} +0 -0
- /package/config/{typescript/eslint.config.js → integrations/eslint/typescript.config.js} +0 -0
- /package/config/{typescript/custom-rules-new.js → schemas/sunlint-schema.json} +0 -0
- /package/config/{typescript → testing}/test-s005-working.ts +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s005-no-origin-auth.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s006-activation-recovery-secret-not-plaintext.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s008-crypto-agility.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s009-no-insecure-crypto.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s010-no-insecure-random-in-sensitive-context.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s011-no-insecure-uuid.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s012-hardcode-secret.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s014-insecure-tls-version.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s015-insecure-tls-certificate.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s016-sensitive-query-parameter.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s017-no-sql-injection.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s018-positive-input-validation.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s019-no-raw-user-input-in-email.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s020-no-eval-dynamic-execution.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s022-output-encoding.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s023-no-json-injection.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s025-server-side-input-validation.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s026-json-schema-validation.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s027-no-hardcoded-secrets.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s029-require-csrf-protection.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s030-no-directory-browsing.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s033-require-samesite-cookie.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s034-require-host-cookie-prefix.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s035-cookie-specific-path.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s036-no-unsafe-file-include.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s037-require-anti-cache-headers.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s038-no-version-disclosure.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s039-no-session-token-in-url.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s041-require-session-invalidate-on-logout.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s042-require-periodic-reauthentication.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s043-terminate-sessions-on-password-change.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s044-require-full-session-for-sensitive-operations.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s045-anti-automation-controls.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s046-secure-notification-on-auth-change.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s048-password-credential-recovery.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s050-session-token-weak-hash.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s052-secure-random-authentication-code.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s054-verification-default-account.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s057-utc-logging.js +0 -0
- /package/{config/typescript/security-rules → integrations/eslint/plugin/rules/security}/s058-no-ssrf.js +0 -0
- /package/rules/{C006_function_naming → common/C006_function_naming}/config.json +0 -0
- /package/rules/{C019_log_level_usage → common/C019_log_level_usage}/config.json +0 -0
- /package/rules/{C029_catch_block_logging → common/C029_catch_block_logging}/config.json +0 -0
- /package/rules/{C031_validation_separation → common/C031_validation_separation}/analyzer.js +0 -0
- /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
|
+
};
|