@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,945 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: S002 – IDOR Check
|
|
3
|
+
* Rule ID: custom/s002
|
|
4
|
+
* Description: Verify IDOR (Insecure Direct Object Reference) security in REST APIs.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
docs: {
|
|
13
|
+
description: 'Verify IDOR (Insecure Direct Object Reference) security in REST APIs',
|
|
14
|
+
recommended: true,
|
|
15
|
+
url: 'https://owasp.org/www-project-top-ten/2017/A5_2017-Broken_Access_Control'
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
restEndpointMissingAuth: 'IDOR Risk: REST endpoint with ID parameter lacks authorization check. Add proper authentication middleware or manual ownership validation.',
|
|
20
|
+
repositoryMissingConstraint: 'IDOR Risk: Database query should include user/owner constraint (e.g., WHERE user_id = $userId) to prevent unauthorized data access.',
|
|
21
|
+
directEntityReturn: 'IDOR Risk: Avoid returning Entity/Model directly from REST endpoints. Use DTO/ResponseModel to control data exposure.',
|
|
22
|
+
sequentialIdUsage: 'IDOR Risk: Consider using UUID instead of sequential ID (number) to make resource identifiers unpredictable.',
|
|
23
|
+
directRepositoryAccess: 'IDOR Risk: Direct repository/database access with ID from URL without authorization check. Verify user ownership before accessing resources.',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
|
|
27
|
+
create(context) {
|
|
28
|
+
return {
|
|
29
|
+
// Check REST endpoints (Express.js, NestJS, etc.)
|
|
30
|
+
CallExpression(node) {
|
|
31
|
+
checkRestEndpointAuthorization(node, context);
|
|
32
|
+
checkDirectRepositoryAccess(node, context);
|
|
33
|
+
},
|
|
34
|
+
|
|
35
|
+
// Check method definitions
|
|
36
|
+
MethodDefinition(node) {
|
|
37
|
+
checkRepositorySecurityConstraint(node, context);
|
|
38
|
+
checkDirectEntityReturn(node, context);
|
|
39
|
+
},
|
|
40
|
+
|
|
41
|
+
// Check function declarations
|
|
42
|
+
FunctionDeclaration(node) {
|
|
43
|
+
checkRepositorySecurityConstraint(node, context);
|
|
44
|
+
checkDirectEntityReturn(node, context);
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
// Check variable declarations for ID fields
|
|
48
|
+
VariableDeclarator(node) {
|
|
49
|
+
checkSequentialIdUsage(node, context);
|
|
50
|
+
},
|
|
51
|
+
|
|
52
|
+
// Check property definitions (for class properties)
|
|
53
|
+
PropertyDefinition(node) {
|
|
54
|
+
checkSequentialIdUsage(node, context);
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
// Check arrow function expressions
|
|
58
|
+
ArrowFunctionExpression(node) {
|
|
59
|
+
checkRepositorySecurityConstraint(node, context);
|
|
60
|
+
checkDirectEntityReturn(node, context);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
// Check function expressions
|
|
64
|
+
FunctionExpression(node) {
|
|
65
|
+
checkRepositorySecurityConstraint(node, context);
|
|
66
|
+
checkDirectEntityReturn(node, context);
|
|
67
|
+
},
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
// =================== CHECK METHODS ===================
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Check if the endpoint has security middleware
|
|
74
|
+
*/
|
|
75
|
+
function hasSecurityMiddleware(node) {
|
|
76
|
+
if (node.type !== 'CallExpression') return false;
|
|
77
|
+
|
|
78
|
+
// Check for middleware functions like auth, authenticate, authorize
|
|
79
|
+
return node.arguments.some(arg => {
|
|
80
|
+
if (arg.type === 'Identifier') {
|
|
81
|
+
const argName = arg.name.toLowerCase();
|
|
82
|
+
return argName.includes('auth') ||
|
|
83
|
+
argName.includes('guard') ||
|
|
84
|
+
argName.includes('protect') ||
|
|
85
|
+
argName.includes('secure') ||
|
|
86
|
+
argName.includes('jwt') ||
|
|
87
|
+
argName.includes('token');
|
|
88
|
+
}
|
|
89
|
+
return false;
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Check if the function has manual security check
|
|
95
|
+
*/
|
|
96
|
+
function hasManualSecurityCheck(node) {
|
|
97
|
+
let bodyNode;
|
|
98
|
+
|
|
99
|
+
if (node.type === 'CallExpression') {
|
|
100
|
+
// Check callback function in Express.js
|
|
101
|
+
const callback = node.arguments.find(arg =>
|
|
102
|
+
arg.type === 'FunctionExpression' ||
|
|
103
|
+
arg.type === 'ArrowFunctionExpression'
|
|
104
|
+
);
|
|
105
|
+
if (callback && callback.body) {
|
|
106
|
+
bodyNode = callback.body;
|
|
107
|
+
}
|
|
108
|
+
} else if (node.body) {
|
|
109
|
+
bodyNode = node.body;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!bodyNode) return false;
|
|
113
|
+
|
|
114
|
+
// If arrow function with expression body
|
|
115
|
+
if (bodyNode.type !== 'BlockStatement') {
|
|
116
|
+
const bodyText = context.getSourceCode().getText(bodyNode).toLowerCase();
|
|
117
|
+
return containsSecurityCheck(bodyText);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Check block statement
|
|
121
|
+
return bodyNode.body.some(statement => {
|
|
122
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
123
|
+
return containsSecurityCheck(statementText);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Check REST endpoint with ID parameter but missing authorization
|
|
129
|
+
*/
|
|
130
|
+
function checkRestEndpointAuthorization(node, context) {
|
|
131
|
+
if (!isRestEndpoint(node)) return;
|
|
132
|
+
|
|
133
|
+
const hasIdParameter = hasPathParameterId(node);
|
|
134
|
+
const hasSecurityMiddlewareResult = hasSecurityMiddleware(node);
|
|
135
|
+
const hasManualSecurityCheckResult = hasManualSecurityCheck(node);
|
|
136
|
+
|
|
137
|
+
if (hasIdParameter && !hasSecurityMiddlewareResult && !hasManualSecurityCheckResult) {
|
|
138
|
+
context.report({
|
|
139
|
+
node,
|
|
140
|
+
messageId: 'restEndpointMissingAuth',
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Check if the method has custom query
|
|
147
|
+
*/
|
|
148
|
+
function hasCustomQuery(node) {
|
|
149
|
+
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
150
|
+
|
|
151
|
+
return node.body.body.some(statement => {
|
|
152
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
153
|
+
return statementText.includes('query') ||
|
|
154
|
+
statementText.includes('sql') ||
|
|
155
|
+
statementText.includes('select') ||
|
|
156
|
+
statementText.includes('from') ||
|
|
157
|
+
statementText.includes('where') ||
|
|
158
|
+
statementText.includes('findby') ||
|
|
159
|
+
statementText.includes('createquerybuilder') ||
|
|
160
|
+
statementText.includes('rawquery');
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Check if query contains user constraint
|
|
166
|
+
*/
|
|
167
|
+
function hasUserConstraintInQuery(node) {
|
|
168
|
+
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
169
|
+
|
|
170
|
+
return node.body.body.some(statement => {
|
|
171
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
172
|
+
return statementText.includes('user_id') ||
|
|
173
|
+
statementText.includes('owner_id') ||
|
|
174
|
+
statementText.includes('created_by') ||
|
|
175
|
+
statementText.includes('userid') ||
|
|
176
|
+
statementText.includes('ownerid') ||
|
|
177
|
+
statementText.includes('createdby') ||
|
|
178
|
+
(statementText.includes('where') &&
|
|
179
|
+
(statementText.includes('user') || statementText.includes('owner')));
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Check Repository query missing user/owner constraint
|
|
185
|
+
*/
|
|
186
|
+
function checkRepositorySecurityConstraint(node, context) {
|
|
187
|
+
if (!isRepositoryMethod(node)) return;
|
|
188
|
+
|
|
189
|
+
const hasCustomQueryResult = hasCustomQuery(node);
|
|
190
|
+
const hasUserConstraint = hasUserConstraintInQuery(node);
|
|
191
|
+
|
|
192
|
+
if (hasCustomQueryResult && !hasUserConstraint) {
|
|
193
|
+
context.report({
|
|
194
|
+
node,
|
|
195
|
+
messageId: 'repositoryMissingConstraint',
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Check if method returns Entity directly instead of DTO
|
|
202
|
+
*/
|
|
203
|
+
function checkDirectEntityReturn(node, context) {
|
|
204
|
+
if (!isRestEndpoint(node)) return;
|
|
205
|
+
|
|
206
|
+
const returnsEntity = returnsEntityDirectly(node);
|
|
207
|
+
|
|
208
|
+
if (returnsEntity) {
|
|
209
|
+
context.report({
|
|
210
|
+
node,
|
|
211
|
+
messageId: 'directEntityReturn',
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Check if ID field uses sequential number
|
|
218
|
+
*/
|
|
219
|
+
function checkSequentialIdUsage(node, context) {
|
|
220
|
+
const isEntityId = isEntityIdField(node);
|
|
221
|
+
const isSequentialType = isSequentialIdType(node);
|
|
222
|
+
|
|
223
|
+
if (isEntityId && isSequentialType) {
|
|
224
|
+
context.report({
|
|
225
|
+
node,
|
|
226
|
+
messageId: 'sequentialIdUsage',
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Check direct repository access without authorization check
|
|
233
|
+
*/
|
|
234
|
+
function checkDirectRepositoryAccess(node, context) {
|
|
235
|
+
if (!isRepositoryFindMethod(node)) return;
|
|
236
|
+
|
|
237
|
+
const enclosingFunction = getEnclosingFunction(node);
|
|
238
|
+
if (!enclosingFunction) return;
|
|
239
|
+
|
|
240
|
+
const isInRestControllerResult = isInRestController(enclosingFunction);
|
|
241
|
+
const hasIdParameter = hasPathParameterId(enclosingFunction);
|
|
242
|
+
const hasAuthCheck = hasAuthorizationLogicNearby(node, enclosingFunction);
|
|
243
|
+
|
|
244
|
+
if (isInRestControllerResult && hasIdParameter && !hasAuthCheck) {
|
|
245
|
+
context.report({
|
|
246
|
+
node,
|
|
247
|
+
messageId: 'directRepositoryAccess',
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// =================== HELPER METHODS ===================
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Check if the call expression is a REST endpoint
|
|
256
|
+
*/
|
|
257
|
+
function isRestEndpoint(node) {
|
|
258
|
+
// Express.js style: app.get(), router.post(), etc.
|
|
259
|
+
if (node.type === 'CallExpression') {
|
|
260
|
+
const callee = node.callee;
|
|
261
|
+
if (callee.type === 'MemberExpression') {
|
|
262
|
+
const property = callee.property;
|
|
263
|
+
if (property.type === 'Identifier') {
|
|
264
|
+
const methodName = property.name;
|
|
265
|
+
return ['get', 'post', 'put', 'delete', 'patch', 'use', 'all'].includes(methodName);
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// NestJS style: @Get(), @Post(), etc.
|
|
271
|
+
if (node.decorators) {
|
|
272
|
+
return node.decorators.some(decorator => {
|
|
273
|
+
if (decorator.expression.type === 'Identifier') {
|
|
274
|
+
const decoratorName = decorator.expression.name;
|
|
275
|
+
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
|
|
276
|
+
}
|
|
277
|
+
if (decorator.expression.type === 'CallExpression' &&
|
|
278
|
+
decorator.expression.callee.type === 'Identifier') {
|
|
279
|
+
const decoratorName = decorator.expression.callee.name;
|
|
280
|
+
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
|
|
281
|
+
}
|
|
282
|
+
return false;
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return false;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/**
|
|
290
|
+
* Check if the endpoint has a path parameter ID
|
|
291
|
+
*/
|
|
292
|
+
function hasPathParameterId(node) {
|
|
293
|
+
// Express.js style: app.get('/users/:id', ...)
|
|
294
|
+
if (node.type === 'CallExpression') {
|
|
295
|
+
const firstArg = node.arguments[0];
|
|
296
|
+
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
297
|
+
return firstArg.value.includes(':id') ||
|
|
298
|
+
firstArg.value.includes(':uuid') ||
|
|
299
|
+
firstArg.value.match(/:[\w]*id/i);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
// NestJS style: @Get(':id')
|
|
304
|
+
if (node.decorators) {
|
|
305
|
+
return node.decorators.some(decorator => {
|
|
306
|
+
if (decorator.expression.type === 'CallExpression') {
|
|
307
|
+
const firstArg = decorator.expression.arguments[0];
|
|
308
|
+
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
309
|
+
return firstArg.value.includes(':id') ||
|
|
310
|
+
firstArg.value.includes(':uuid') ||
|
|
311
|
+
firstArg.value.match(/:[\w]*id/i);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
return false;
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check function parameters
|
|
319
|
+
if (node.params) {
|
|
320
|
+
return node.params.some(param => {
|
|
321
|
+
if (param.type === 'Identifier') {
|
|
322
|
+
const paramName = param.name.toLowerCase();
|
|
323
|
+
return paramName.includes('id') || paramName === 'uuid';
|
|
324
|
+
}
|
|
325
|
+
// Destructured parameter: { id }
|
|
326
|
+
if (param.type === 'ObjectPattern') {
|
|
327
|
+
return param.properties.some(prop => {
|
|
328
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
329
|
+
const propName = prop.key.name.toLowerCase();
|
|
330
|
+
return propName.includes('id') || propName === 'uuid';
|
|
331
|
+
}
|
|
332
|
+
return false;
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
return false;
|
|
336
|
+
});
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
return false;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Check if the endpoint has security middleware
|
|
344
|
+
*/
|
|
345
|
+
function hasSecurityMiddleware(node) {
|
|
346
|
+
if (node.type !== 'CallExpression') return false;
|
|
347
|
+
|
|
348
|
+
// Check for middleware functions like auth, authenticate, authorize
|
|
349
|
+
return node.arguments.some(arg => {
|
|
350
|
+
if (arg.type === 'Identifier') {
|
|
351
|
+
const argName = arg.name.toLowerCase();
|
|
352
|
+
return argName.includes('auth') ||
|
|
353
|
+
argName.includes('guard') ||
|
|
354
|
+
argName.includes('protect') ||
|
|
355
|
+
argName.includes('secure') ||
|
|
356
|
+
argName.includes('jwt') ||
|
|
357
|
+
argName.includes('token');
|
|
358
|
+
}
|
|
359
|
+
return false;
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Check if the method has custom query
|
|
365
|
+
*/
|
|
366
|
+
function hasCustomQuery(node) {
|
|
367
|
+
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
368
|
+
|
|
369
|
+
return node.body.body.some(statement => {
|
|
370
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
371
|
+
return statementText.includes('query') ||
|
|
372
|
+
statementText.includes('sql') ||
|
|
373
|
+
statementText.includes('select') ||
|
|
374
|
+
statementText.includes('from') ||
|
|
375
|
+
statementText.includes('where') ||
|
|
376
|
+
statementText.includes('findby') ||
|
|
377
|
+
statementText.includes('createquerybuilder') ||
|
|
378
|
+
statementText.includes('rawquery');
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Check if query contains user constraint
|
|
384
|
+
*/
|
|
385
|
+
function hasUserConstraintInQuery(node) {
|
|
386
|
+
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
387
|
+
|
|
388
|
+
return node.body.body.some(statement => {
|
|
389
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
390
|
+
return statementText.includes('user_id') ||
|
|
391
|
+
statementText.includes('owner_id') ||
|
|
392
|
+
statementText.includes('created_by') ||
|
|
393
|
+
statementText.includes('userid') ||
|
|
394
|
+
statementText.includes('ownerid') ||
|
|
395
|
+
statementText.includes('createdby') ||
|
|
396
|
+
(statementText.includes('where') &&
|
|
397
|
+
(statementText.includes('user') || statementText.includes('owner')));
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Check Repository query missing user/owner constraint
|
|
403
|
+
*/
|
|
404
|
+
function checkRepositorySecurityConstraint(node, context) {
|
|
405
|
+
if (!isRepositoryMethod(node)) return;
|
|
406
|
+
|
|
407
|
+
const hasCustomQueryResult = hasCustomQuery(node);
|
|
408
|
+
const hasUserConstraint = hasUserConstraintInQuery(node);
|
|
409
|
+
|
|
410
|
+
if (hasCustomQueryResult && !hasUserConstraint) {
|
|
411
|
+
context.report({
|
|
412
|
+
node,
|
|
413
|
+
messageId: 'repositoryMissingConstraint',
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Check if method returns Entity directly instead of DTO
|
|
420
|
+
*/
|
|
421
|
+
function checkDirectEntityReturn(node, context) {
|
|
422
|
+
if (!isRestEndpoint(node)) return;
|
|
423
|
+
|
|
424
|
+
const returnsEntity = returnsEntityDirectly(node);
|
|
425
|
+
|
|
426
|
+
if (returnsEntity) {
|
|
427
|
+
context.report({
|
|
428
|
+
node,
|
|
429
|
+
messageId: 'directEntityReturn',
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* Check if ID field uses sequential number
|
|
436
|
+
*/
|
|
437
|
+
function checkSequentialIdUsage(node, context) {
|
|
438
|
+
const isEntityId = isEntityIdField(node);
|
|
439
|
+
const isSequentialType = isSequentialIdType(node);
|
|
440
|
+
|
|
441
|
+
if (isEntityId && isSequentialType) {
|
|
442
|
+
context.report({
|
|
443
|
+
node,
|
|
444
|
+
messageId: 'sequentialIdUsage',
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
/**
|
|
450
|
+
* Check direct repository access without authorization check
|
|
451
|
+
*/
|
|
452
|
+
function checkDirectRepositoryAccess(node, context) {
|
|
453
|
+
if (!isRepositoryFindMethod(node)) return;
|
|
454
|
+
|
|
455
|
+
const enclosingFunction = getEnclosingFunction(node);
|
|
456
|
+
if (!enclosingFunction) return;
|
|
457
|
+
|
|
458
|
+
const isInRestControllerResult = isInRestController(enclosingFunction);
|
|
459
|
+
const hasIdParameter = hasPathParameterId(enclosingFunction);
|
|
460
|
+
const hasAuthCheck = hasAuthorizationLogicNearby(node, enclosingFunction);
|
|
461
|
+
|
|
462
|
+
if (isInRestControllerResult && hasIdParameter && !hasAuthCheck) {
|
|
463
|
+
context.report({
|
|
464
|
+
node,
|
|
465
|
+
messageId: 'directRepositoryAccess',
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
// =================== HELPER METHODS ===================
|
|
471
|
+
|
|
472
|
+
/**
|
|
473
|
+
* Check if the call expression is a REST endpoint
|
|
474
|
+
*/
|
|
475
|
+
function isRestEndpoint(node) {
|
|
476
|
+
// Express.js style: app.get(), router.post(), etc.
|
|
477
|
+
if (node.type === 'CallExpression') {
|
|
478
|
+
const callee = node.callee;
|
|
479
|
+
if (callee.type === 'MemberExpression') {
|
|
480
|
+
const property = callee.property;
|
|
481
|
+
if (property.type === 'Identifier') {
|
|
482
|
+
const methodName = property.name;
|
|
483
|
+
return ['get', 'post', 'put', 'delete', 'patch', 'use', 'all'].includes(methodName);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// NestJS style: @Get(), @Post(), etc.
|
|
489
|
+
if (node.decorators) {
|
|
490
|
+
return node.decorators.some(decorator => {
|
|
491
|
+
if (decorator.expression.type === 'Identifier') {
|
|
492
|
+
const decoratorName = decorator.expression.name;
|
|
493
|
+
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
|
|
494
|
+
}
|
|
495
|
+
if (decorator.expression.type === 'CallExpression' &&
|
|
496
|
+
decorator.expression.callee.type === 'Identifier') {
|
|
497
|
+
const decoratorName = decorator.expression.callee.name;
|
|
498
|
+
return ['Get', 'Post', 'Put', 'Delete', 'Patch', 'All'].includes(decoratorName);
|
|
499
|
+
}
|
|
500
|
+
return false;
|
|
501
|
+
});
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
/**
|
|
508
|
+
* Check if the endpoint has a path parameter ID
|
|
509
|
+
*/
|
|
510
|
+
function hasPathParameterId(node) {
|
|
511
|
+
// Express.js style: app.get('/users/:id', ...)
|
|
512
|
+
if (node.type === 'CallExpression') {
|
|
513
|
+
const firstArg = node.arguments[0];
|
|
514
|
+
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
515
|
+
return firstArg.value.includes(':id') ||
|
|
516
|
+
firstArg.value.includes(':uuid') ||
|
|
517
|
+
firstArg.value.match(/:[\w]*id/i);
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
// NestJS style: @Get(':id')
|
|
522
|
+
if (node.decorators) {
|
|
523
|
+
return node.decorators.some(decorator => {
|
|
524
|
+
if (decorator.expression.type === 'CallExpression') {
|
|
525
|
+
const firstArg = decorator.expression.arguments[0];
|
|
526
|
+
if (firstArg?.type === 'Literal' && typeof firstArg.value === 'string') {
|
|
527
|
+
return firstArg.value.includes(':id') ||
|
|
528
|
+
firstArg.value.includes(':uuid') ||
|
|
529
|
+
firstArg.value.match(/:[\w]*id/i);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
return false;
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// Check function parameters
|
|
537
|
+
if (node.params) {
|
|
538
|
+
return node.params.some(param => {
|
|
539
|
+
if (param.type === 'Identifier') {
|
|
540
|
+
const paramName = param.name.toLowerCase();
|
|
541
|
+
return paramName.includes('id') || paramName === 'uuid';
|
|
542
|
+
}
|
|
543
|
+
// Destructured parameter: { id }
|
|
544
|
+
if (param.type === 'ObjectPattern') {
|
|
545
|
+
return param.properties.some(prop => {
|
|
546
|
+
if (prop.type === 'Property' && prop.key.type === 'Identifier') {
|
|
547
|
+
const propName = prop.key.name.toLowerCase();
|
|
548
|
+
return propName.includes('id') || propName === 'uuid';
|
|
549
|
+
}
|
|
550
|
+
return false;
|
|
551
|
+
});
|
|
552
|
+
}
|
|
553
|
+
return false;
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
return false;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
/**
|
|
561
|
+
* Check if the endpoint has security middleware
|
|
562
|
+
*/
|
|
563
|
+
function hasSecurityMiddleware(node) {
|
|
564
|
+
if (node.type !== 'CallExpression') return false;
|
|
565
|
+
|
|
566
|
+
// Check for middleware functions like auth, authenticate, authorize
|
|
567
|
+
return node.arguments.some(arg => {
|
|
568
|
+
if (arg.type === 'Identifier') {
|
|
569
|
+
const argName = arg.name.toLowerCase();
|
|
570
|
+
return argName.includes('auth') ||
|
|
571
|
+
argName.includes('guard') ||
|
|
572
|
+
argName.includes('protect') ||
|
|
573
|
+
argName.includes('secure') ||
|
|
574
|
+
argName.includes('jwt') ||
|
|
575
|
+
argName.includes('token');
|
|
576
|
+
}
|
|
577
|
+
return false;
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
/**
|
|
582
|
+
* Check if the method has custom query
|
|
583
|
+
*/
|
|
584
|
+
function hasCustomQuery(node) {
|
|
585
|
+
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
586
|
+
|
|
587
|
+
return node.body.body.some(statement => {
|
|
588
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
589
|
+
return statementText.includes('query') ||
|
|
590
|
+
statementText.includes('sql') ||
|
|
591
|
+
statementText.includes('select') ||
|
|
592
|
+
statementText.includes('from') ||
|
|
593
|
+
statementText.includes('where') ||
|
|
594
|
+
statementText.includes('findby') ||
|
|
595
|
+
statementText.includes('createquerybuilder') ||
|
|
596
|
+
statementText.includes('rawquery');
|
|
597
|
+
});
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Check if query contains user constraint
|
|
602
|
+
*/
|
|
603
|
+
function hasUserConstraintInQuery(node) {
|
|
604
|
+
if (!node.body || node.body.type !== 'BlockStatement') return false;
|
|
605
|
+
|
|
606
|
+
return node.body.body.some(statement => {
|
|
607
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
608
|
+
return statementText.includes('user_id') ||
|
|
609
|
+
statementText.includes('owner_id') ||
|
|
610
|
+
statementText.includes('created_by') ||
|
|
611
|
+
statementText.includes('userid') ||
|
|
612
|
+
statementText.includes('ownerid') ||
|
|
613
|
+
statementText.includes('createdby') ||
|
|
614
|
+
(statementText.includes('where') &&
|
|
615
|
+
(statementText.includes('user') || statementText.includes('owner')));
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/**
|
|
620
|
+
* Check Repository query missing user/owner constraint
|
|
621
|
+
*/
|
|
622
|
+
function checkRepositorySecurityConstraint(node, context) {
|
|
623
|
+
if (!isRepositoryMethod(node)) return;
|
|
624
|
+
|
|
625
|
+
const hasCustomQueryResult = hasCustomQuery(node);
|
|
626
|
+
const hasUserConstraint = hasUserConstraintInQuery(node);
|
|
627
|
+
|
|
628
|
+
if (hasCustomQueryResult && !hasUserConstraint) {
|
|
629
|
+
context.report({
|
|
630
|
+
node,
|
|
631
|
+
messageId: 'repositoryMissingConstraint',
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Check if method returns Entity directly instead of DTO
|
|
638
|
+
*/
|
|
639
|
+
function checkDirectEntityReturn(node, context) {
|
|
640
|
+
if (!isRestEndpoint(node)) return;
|
|
641
|
+
|
|
642
|
+
const returnsEntity = returnsEntityDirectly(node);
|
|
643
|
+
|
|
644
|
+
if (returnsEntity) {
|
|
645
|
+
context.report({
|
|
646
|
+
node,
|
|
647
|
+
messageId: 'directEntityReturn',
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/**
|
|
653
|
+
* Check if ID field uses sequential number
|
|
654
|
+
*/
|
|
655
|
+
function checkSequentialIdUsage(node, context) {
|
|
656
|
+
const isEntityId = isEntityIdField(node);
|
|
657
|
+
const isSequentialType = isSequentialIdType(node);
|
|
658
|
+
|
|
659
|
+
if (isEntityId && isSequentialType) {
|
|
660
|
+
context.report({
|
|
661
|
+
node,
|
|
662
|
+
messageId: 'sequentialIdUsage',
|
|
663
|
+
});
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Check direct repository access without authorization check
|
|
669
|
+
*/
|
|
670
|
+
function checkDirectRepositoryAccess(node, context) {
|
|
671
|
+
if (!isRepositoryFindMethod(node)) return;
|
|
672
|
+
|
|
673
|
+
const enclosingFunction = getEnclosingFunction(node);
|
|
674
|
+
if (!enclosingFunction) return;
|
|
675
|
+
|
|
676
|
+
const isInRestControllerResult = isInRestController(enclosingFunction);
|
|
677
|
+
const hasIdParameter = hasPathParameterId(enclosingFunction);
|
|
678
|
+
const hasAuthCheck = hasAuthorizationLogicNearby(node, enclosingFunction);
|
|
679
|
+
|
|
680
|
+
if (isInRestControllerResult && hasIdParameter && !hasAuthCheck) {
|
|
681
|
+
context.report({
|
|
682
|
+
node,
|
|
683
|
+
messageId: 'directRepositoryAccess',
|
|
684
|
+
});
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
// =================== HELPER METHODS ===================
|
|
689
|
+
|
|
690
|
+
/**
|
|
691
|
+
* Check if the method is a Repository method
|
|
692
|
+
*/
|
|
693
|
+
function isRepositoryMethod(node) {
|
|
694
|
+
const className = getEnclosingClassName(node);
|
|
695
|
+
if (className) {
|
|
696
|
+
const lowerClassName = className.toLowerCase();
|
|
697
|
+
return lowerClassName.includes('repository') ||
|
|
698
|
+
lowerClassName.includes('service') ||
|
|
699
|
+
lowerClassName.includes('dao') ||
|
|
700
|
+
lowerClassName.includes('model');
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
// Check for repository-related decorators
|
|
704
|
+
if (node.decorators) {
|
|
705
|
+
return node.decorators.some(decorator => {
|
|
706
|
+
if (decorator.expression.type === 'Identifier') {
|
|
707
|
+
const decoratorName = decorator.expression.name.toLowerCase();
|
|
708
|
+
return decoratorName.includes('repository') ||
|
|
709
|
+
decoratorName.includes('injectable') ||
|
|
710
|
+
decoratorName.includes('service');
|
|
711
|
+
}
|
|
712
|
+
return false;
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
return false;
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/**
|
|
720
|
+
* Check if the method returns Entity directly
|
|
721
|
+
*/
|
|
722
|
+
function returnsEntityDirectly(node) {
|
|
723
|
+
// Check TypeScript return type annotation
|
|
724
|
+
if (node.returnType) {
|
|
725
|
+
const returnTypeText = context.getSourceCode().getText(node.returnType).toLowerCase();
|
|
726
|
+
|
|
727
|
+
// Exclude safe types
|
|
728
|
+
if (returnTypeText.includes('dto') ||
|
|
729
|
+
returnTypeText.includes('response') ||
|
|
730
|
+
returnTypeText.includes('view') ||
|
|
731
|
+
returnTypeText.includes('model') ||
|
|
732
|
+
returnTypeText.includes('string') ||
|
|
733
|
+
returnTypeText.includes('number') ||
|
|
734
|
+
returnTypeText.includes('boolean') ||
|
|
735
|
+
returnTypeText.includes('void') ||
|
|
736
|
+
returnTypeText.includes('promise') ||
|
|
737
|
+
returnTypeText.includes('observable')) {
|
|
738
|
+
return false;
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
// Check if it's likely an Entity type
|
|
742
|
+
return isLikelyEntityType(returnTypeText);
|
|
743
|
+
}
|
|
744
|
+
|
|
745
|
+
// Check return statements
|
|
746
|
+
if (node.body && node.body.type === 'BlockStatement') {
|
|
747
|
+
return node.body.body.some(statement => {
|
|
748
|
+
if (statement.type === 'ReturnStatement' && statement.argument) {
|
|
749
|
+
const returnText = context.getSourceCode().getText(statement.argument);
|
|
750
|
+
return !returnText.includes('dto') &&
|
|
751
|
+
!returnText.includes('response') &&
|
|
752
|
+
!returnText.includes('view');
|
|
753
|
+
}
|
|
754
|
+
return false;
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
return false;
|
|
759
|
+
}
|
|
760
|
+
|
|
761
|
+
/**
|
|
762
|
+
* Check if the type is likely an Entity
|
|
763
|
+
*/
|
|
764
|
+
function isLikelyEntityType(returnType) {
|
|
765
|
+
// Entity usually starts with uppercase and is not a primitive type
|
|
766
|
+
const cleanType = returnType.replace(/[<>[\]{}]/g, '');
|
|
767
|
+
return /^[A-Z][a-zA-Z]*$/.test(cleanType) &&
|
|
768
|
+
!['Boolean', 'Number', 'String', 'Date', 'Array', 'Object', 'Promise', 'Observable'].includes(cleanType);
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
/**
|
|
772
|
+
* Check if the variable is an Entity ID field
|
|
773
|
+
*/
|
|
774
|
+
function isEntityIdField(node) {
|
|
775
|
+
let name;
|
|
776
|
+
|
|
777
|
+
if (node.type === 'VariableDeclarator' && node.id.type === 'Identifier') {
|
|
778
|
+
name = node.id.name.toLowerCase();
|
|
779
|
+
} else if (node.type === 'PropertyDefinition' && node.key.type === 'Identifier') {
|
|
780
|
+
name = node.key.name.toLowerCase();
|
|
781
|
+
} else {
|
|
782
|
+
return false;
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
return name === 'id' || name === '_id' || name.endsWith('id');
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
/**
|
|
789
|
+
* Check if the ID type is sequential
|
|
790
|
+
*/
|
|
791
|
+
function isSequentialIdType(node) {
|
|
792
|
+
// Check TypeScript type annotation
|
|
793
|
+
if (node.typeAnnotation) {
|
|
794
|
+
const typeText = context.getSourceCode().getText(node.typeAnnotation).toLowerCase();
|
|
795
|
+
return typeText.includes('number') || typeText.includes('int');
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// Check initializer value
|
|
799
|
+
if (node.init && node.init.type === 'Literal') {
|
|
800
|
+
return typeof node.init.value === 'number';
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
return false;
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
/**
|
|
807
|
+
* Check if the method invocation is a repository find method
|
|
808
|
+
*/
|
|
809
|
+
function isRepositoryFindMethod(node) {
|
|
810
|
+
if (node.type !== 'CallExpression') return false;
|
|
811
|
+
|
|
812
|
+
if (node.callee.type === 'MemberExpression') {
|
|
813
|
+
const property = node.callee.property;
|
|
814
|
+
if (property.type === 'Identifier') {
|
|
815
|
+
const methodName = property.name.toLowerCase();
|
|
816
|
+
return methodName.startsWith('findby') ||
|
|
817
|
+
methodName.startsWith('getby') ||
|
|
818
|
+
methodName.startsWith('find') ||
|
|
819
|
+
methodName.startsWith('get') ||
|
|
820
|
+
methodName.startsWith('delete') ||
|
|
821
|
+
methodName.includes('id');
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
return false;
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
/**
|
|
828
|
+
* Check if the method is in a RestController
|
|
829
|
+
*/
|
|
830
|
+
function isInRestController(node) {
|
|
831
|
+
const className = getEnclosingClassName(node);
|
|
832
|
+
if (className) {
|
|
833
|
+
const lowerClassName = className.toLowerCase();
|
|
834
|
+
return lowerClassName.includes('controller') ||
|
|
835
|
+
lowerClassName.includes('route') ||
|
|
836
|
+
lowerClassName.includes('handler');
|
|
837
|
+
}
|
|
838
|
+
return false;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
/**
|
|
842
|
+
* Check if there is authorization logic near the repository call
|
|
843
|
+
*/
|
|
844
|
+
function hasAuthorizationLogicNearby(repoCall, method) {
|
|
845
|
+
const enclosingFunction = getEnclosingFunction(repoCall);
|
|
846
|
+
if (!enclosingFunction || !enclosingFunction.body) return false;
|
|
847
|
+
|
|
848
|
+
const bodyNode = enclosingFunction.body;
|
|
849
|
+
if (bodyNode.type !== 'BlockStatement') return false;
|
|
850
|
+
|
|
851
|
+
return bodyNode.body.some(statement => {
|
|
852
|
+
const stmtText = context.getSourceCode().getText(statement).toLowerCase();
|
|
853
|
+
return containsSecurityCheck(stmtText);
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Check if text contains security-related keywords
|
|
859
|
+
*/
|
|
860
|
+
function containsSecurityCheck(text) {
|
|
861
|
+
return text.includes('getcurrentuser') ||
|
|
862
|
+
text.includes('req.user') ||
|
|
863
|
+
text.includes('request.user') ||
|
|
864
|
+
text.includes('authentication') ||
|
|
865
|
+
text.includes('checkowner') ||
|
|
866
|
+
text.includes('haspermission') ||
|
|
867
|
+
text.includes('canaccess') ||
|
|
868
|
+
text.includes('isowner') ||
|
|
869
|
+
text.includes('validateaccess') ||
|
|
870
|
+
text.includes('authorize') ||
|
|
871
|
+
text.includes('jwt') ||
|
|
872
|
+
text.includes('token') ||
|
|
873
|
+
text.includes('permission') ||
|
|
874
|
+
text.includes('role') ||
|
|
875
|
+
text.includes('guard');
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
/**
|
|
879
|
+
* Check if the function has manual security check
|
|
880
|
+
*/
|
|
881
|
+
function hasManualSecurityCheck(node) {
|
|
882
|
+
let bodyNode;
|
|
883
|
+
|
|
884
|
+
if (node.type === 'CallExpression') {
|
|
885
|
+
// Check callback function in Express.js
|
|
886
|
+
const callback = node.arguments.find(arg =>
|
|
887
|
+
arg.type === 'FunctionExpression' ||
|
|
888
|
+
arg.type === 'ArrowFunctionExpression'
|
|
889
|
+
);
|
|
890
|
+
if (callback && callback.body) {
|
|
891
|
+
bodyNode = callback.body;
|
|
892
|
+
}
|
|
893
|
+
} else if (node.body) {
|
|
894
|
+
bodyNode = node.body;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
if (!bodyNode) return false;
|
|
898
|
+
|
|
899
|
+
// If arrow function with expression body
|
|
900
|
+
if (bodyNode.type !== 'BlockStatement') {
|
|
901
|
+
const bodyText = context.getSourceCode().getText(bodyNode).toLowerCase();
|
|
902
|
+
return containsSecurityCheck(bodyText);
|
|
903
|
+
}
|
|
904
|
+
|
|
905
|
+
// Check block statement
|
|
906
|
+
return bodyNode.body.some(statement => {
|
|
907
|
+
const statementText = context.getSourceCode().getText(statement).toLowerCase();
|
|
908
|
+
return containsSecurityCheck(statementText);
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
// =================== UTILITY METHODS ===================
|
|
913
|
+
|
|
914
|
+
/**
|
|
915
|
+
* Get enclosing function of a node
|
|
916
|
+
*/
|
|
917
|
+
function getEnclosingFunction(node) {
|
|
918
|
+
let parent = node.parent;
|
|
919
|
+
while (parent) {
|
|
920
|
+
if (parent.type === 'FunctionDeclaration' ||
|
|
921
|
+
parent.type === 'MethodDefinition' ||
|
|
922
|
+
parent.type === 'FunctionExpression' ||
|
|
923
|
+
parent.type === 'ArrowFunctionExpression') {
|
|
924
|
+
return parent;
|
|
925
|
+
}
|
|
926
|
+
parent = parent.parent;
|
|
927
|
+
}
|
|
928
|
+
return null;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Get enclosing class name
|
|
933
|
+
*/
|
|
934
|
+
function getEnclosingClassName(node) {
|
|
935
|
+
let parent = node.parent;
|
|
936
|
+
while (parent) {
|
|
937
|
+
if (parent.type === 'ClassDeclaration' && parent.id) {
|
|
938
|
+
return parent.id.name;
|
|
939
|
+
}
|
|
940
|
+
parent = parent.parent;
|
|
941
|
+
}
|
|
942
|
+
return null;
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
};
|