@sun-asterisk/sunlint 1.0.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +202 -0
- package/LICENSE +21 -0
- package/README.md +490 -0
- package/cli-legacy.js +355 -0
- package/cli.js +35 -0
- package/config/default.json +22 -0
- package/config/presets/beginner.json +36 -0
- package/config/presets/ci.json +46 -0
- package/config/presets/recommended.json +24 -0
- package/config/presets/strict.json +32 -0
- package/config/rules-registry.json +681 -0
- package/config/sunlint-schema.json +166 -0
- package/config/typescript/custom-rules-new.js +0 -0
- package/config/typescript/custom-rules.js +9 -0
- package/config/typescript/eslint.config.js +110 -0
- package/config/typescript/package-lock.json +1585 -0
- package/config/typescript/package.json +13 -0
- package/config/typescript/security-rules/index.js +90 -0
- package/config/typescript/security-rules/s005-no-origin-auth.js +95 -0
- package/config/typescript/security-rules/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/config/typescript/security-rules/s008-crypto-agility.js +62 -0
- package/config/typescript/security-rules/s009-no-insecure-crypto.js +103 -0
- package/config/typescript/security-rules/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/config/typescript/security-rules/s011-no-insecure-uuid.js +66 -0
- package/config/typescript/security-rules/s012-hardcode-secret.js +71 -0
- package/config/typescript/security-rules/s014-insecure-tls-version.js +50 -0
- package/config/typescript/security-rules/s015-insecure-tls-certificate.js +43 -0
- package/config/typescript/security-rules/s016-sensitive-query-parameter.js +59 -0
- package/config/typescript/security-rules/s017-no-sql-injection.js +193 -0
- package/config/typescript/security-rules/s018-positive-input-validation.js +56 -0
- package/config/typescript/security-rules/s019-no-raw-user-input-in-email.js +113 -0
- package/config/typescript/security-rules/s020-no-eval-dynamic-execution.js +89 -0
- package/config/typescript/security-rules/s022-output-encoding.js +78 -0
- package/config/typescript/security-rules/s023-no-json-injection.js +300 -0
- package/config/typescript/security-rules/s025-server-side-input-validation.js +217 -0
- package/config/typescript/security-rules/s026-json-schema-validation.js +68 -0
- package/config/typescript/security-rules/s027-no-hardcoded-secrets.js +80 -0
- package/config/typescript/security-rules/s029-require-csrf-protection.js +79 -0
- package/config/typescript/security-rules/s030-no-directory-browsing.js +78 -0
- package/config/typescript/security-rules/s033-require-samesite-cookie.js +80 -0
- package/config/typescript/security-rules/s034-require-host-cookie-prefix.js +77 -0
- package/config/typescript/security-rules/s035-cookie-specific-path.js +74 -0
- package/config/typescript/security-rules/s036-no-unsafe-file-include.js +68 -0
- package/config/typescript/security-rules/s037-require-anti-cache-headers.js +70 -0
- package/config/typescript/security-rules/s038-no-version-disclosure.js +74 -0
- package/config/typescript/security-rules/s039-no-session-token-in-url.js +63 -0
- package/config/typescript/security-rules/s041-require-session-invalidate-on-logout.js +211 -0
- package/config/typescript/security-rules/s042-require-periodic-reauthentication.js +294 -0
- package/config/typescript/security-rules/s043-terminate-sessions-on-password-change.js +254 -0
- package/config/typescript/security-rules/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/config/typescript/security-rules/s045-anti-automation-controls.js +46 -0
- package/config/typescript/security-rules/s046-secure-notification-on-auth-change.js +44 -0
- package/config/typescript/security-rules/s048-password-credential-recovery.js +54 -0
- package/config/typescript/security-rules/s050-session-token-weak-hash.js +94 -0
- package/config/typescript/security-rules/s052-secure-random-authentication-code.js +66 -0
- package/config/typescript/security-rules/s054-verification-default-account.js +109 -0
- package/config/typescript/security-rules/s057-utc-logging.js +54 -0
- package/config/typescript/security-rules/s058-no-ssrf.js +73 -0
- package/config/typescript/test-s005-working.ts +22 -0
- package/config/typescript/tsconfig.json +29 -0
- package/core/ai-analyzer.js +169 -0
- package/core/analysis-orchestrator.js +705 -0
- package/core/cli-action-handler.js +230 -0
- package/core/cli-program.js +106 -0
- package/core/config-manager.js +396 -0
- package/core/config-merger.js +136 -0
- package/core/config-override-processor.js +74 -0
- package/core/config-preset-resolver.js +65 -0
- package/core/config-source-loader.js +152 -0
- package/core/config-validator.js +126 -0
- package/core/dependency-manager.js +105 -0
- package/core/eslint-engine-service.js +312 -0
- package/core/eslint-instance-manager.js +104 -0
- package/core/eslint-integration-service.js +363 -0
- package/core/git-utils.js +170 -0
- package/core/multi-rule-runner.js +239 -0
- package/core/output-service.js +250 -0
- package/core/report-generator.js +320 -0
- package/core/rule-mapping-service.js +309 -0
- package/core/rule-selection-service.js +121 -0
- package/core/sunlint-engine-service.js +23 -0
- package/core/typescript-analyzer.js +262 -0
- package/core/typescript-engine.js +313 -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/DEBUG.md +86 -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/HEURISTIC_VS_AI.md +113 -0
- package/docs/README.md +32 -0
- package/docs/RELEASE_GUIDE.md +230 -0
- package/docs/RULE-RESPONSIBILITY-MATRIX.md +204 -0
- package/eslint-integration/.eslintrc.js +98 -0
- package/eslint-integration/cli.js +35 -0
- package/eslint-integration/eslint-plugin-custom/c002-no-duplicate-code.js +204 -0
- package/eslint-integration/eslint-plugin-custom/c003-no-vague-abbreviations.js +246 -0
- package/eslint-integration/eslint-plugin-custom/c006-function-name-verb-noun.js +207 -0
- package/eslint-integration/eslint-plugin-custom/c010-limit-block-nesting.js +90 -0
- package/eslint-integration/eslint-plugin-custom/c013-no-dead-code.js +43 -0
- package/eslint-integration/eslint-plugin-custom/c014-abstract-dependency-preferred.js +38 -0
- package/eslint-integration/eslint-plugin-custom/c017-limit-constructor-logic.js +39 -0
- package/eslint-integration/eslint-plugin-custom/c018-no-generic-throw.js +335 -0
- package/eslint-integration/eslint-plugin-custom/c023-no-duplicate-variable-name-in-scope.js +142 -0
- package/eslint-integration/eslint-plugin-custom/c027-limit-function-nesting.js +50 -0
- package/eslint-integration/eslint-plugin-custom/c029-catch-block-logging.js +80 -0
- package/eslint-integration/eslint-plugin-custom/c030-use-custom-error-classes.js +294 -0
- package/eslint-integration/eslint-plugin-custom/c034-no-implicit-return.js +34 -0
- package/eslint-integration/eslint-plugin-custom/c035-no-empty-catch.js +32 -0
- package/eslint-integration/eslint-plugin-custom/c041-no-config-inline.js +64 -0
- package/eslint-integration/eslint-plugin-custom/c042-boolean-name-prefix.js +406 -0
- package/eslint-integration/eslint-plugin-custom/c043-no-console-or-print.js +300 -0
- package/eslint-integration/eslint-plugin-custom/c047-no-duplicate-retry-logic.js +239 -0
- package/eslint-integration/eslint-plugin-custom/c048-no-var-declaration.js +31 -0
- package/eslint-integration/eslint-plugin-custom/c076-one-assert-per-test.js +184 -0
- package/eslint-integration/eslint-plugin-custom/index.js +155 -0
- package/eslint-integration/eslint-plugin-custom/package.json +13 -0
- package/eslint-integration/eslint-plugin-custom/package.json.bak +9 -0
- package/eslint-integration/eslint-plugin-custom/s003-no-unvalidated-redirect.js +86 -0
- package/eslint-integration/eslint-plugin-custom/s005-no-origin-auth.js +95 -0
- package/eslint-integration/eslint-plugin-custom/s006-activation-recovery-secret-not-plaintext.js +69 -0
- package/eslint-integration/eslint-plugin-custom/s008-crypto-agility.js +62 -0
- package/eslint-integration/eslint-plugin-custom/s009-no-insecure-crypto.js +103 -0
- package/eslint-integration/eslint-plugin-custom/s010-no-insecure-random-in-sensitive-context.js +123 -0
- package/eslint-integration/eslint-plugin-custom/s011-no-insecure-uuid.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s012-hardcode-secret.js +71 -0
- package/eslint-integration/eslint-plugin-custom/s014-insecure-tls-version.js +50 -0
- package/eslint-integration/eslint-plugin-custom/s015-insecure-tls-certificate.js +43 -0
- package/eslint-integration/eslint-plugin-custom/s016-sensitive-query-parameter.js +59 -0
- package/eslint-integration/eslint-plugin-custom/s017-no-sql-injection.js +193 -0
- package/eslint-integration/eslint-plugin-custom/s018-positive-input-validation.js +56 -0
- package/eslint-integration/eslint-plugin-custom/s019-no-raw-user-input-in-email.js +113 -0
- package/eslint-integration/eslint-plugin-custom/s020-no-eval-dynamic-execution.js +89 -0
- package/eslint-integration/eslint-plugin-custom/s022-output-encoding.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s023-no-json-injection.js +300 -0
- package/eslint-integration/eslint-plugin-custom/s025-server-side-input-validation.js +217 -0
- package/eslint-integration/eslint-plugin-custom/s026-json-schema-validation.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s027-no-hardcoded-secrets.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s029-require-csrf-protection.js +79 -0
- package/eslint-integration/eslint-plugin-custom/s030-no-directory-browsing.js +78 -0
- package/eslint-integration/eslint-plugin-custom/s033-require-samesite-cookie.js +80 -0
- package/eslint-integration/eslint-plugin-custom/s034-require-host-cookie-prefix.js +77 -0
- package/eslint-integration/eslint-plugin-custom/s035-cookie-specific-path.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s036-no-unsafe-file-include.js +68 -0
- package/eslint-integration/eslint-plugin-custom/s037-require-anti-cache-headers.js +70 -0
- package/eslint-integration/eslint-plugin-custom/s038-no-version-disclosure.js +74 -0
- package/eslint-integration/eslint-plugin-custom/s039-no-session-token-in-url.js +63 -0
- package/eslint-integration/eslint-plugin-custom/s041-require-session-invalidate-on-logout.js +211 -0
- package/eslint-integration/eslint-plugin-custom/s042-require-periodic-reauthentication.js +294 -0
- package/eslint-integration/eslint-plugin-custom/s043-terminate-sessions-on-password-change.js +254 -0
- package/eslint-integration/eslint-plugin-custom/s044-require-full-session-for-sensitive-operations.js +292 -0
- package/eslint-integration/eslint-plugin-custom/s045-anti-automation-controls.js +46 -0
- package/eslint-integration/eslint-plugin-custom/s046-secure-notification-on-auth-change.js +44 -0
- package/eslint-integration/eslint-plugin-custom/s047-secure-random-passwords.js +108 -0
- package/eslint-integration/eslint-plugin-custom/s048-password-credential-recovery.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s050-session-token-weak-hash.js +94 -0
- package/eslint-integration/eslint-plugin-custom/s052-secure-random-authentication-code.js +66 -0
- package/eslint-integration/eslint-plugin-custom/s054-verification-default-account.js +109 -0
- package/eslint-integration/eslint-plugin-custom/s055-verification-rest-check-the-incoming-content-type.js +143 -0
- package/eslint-integration/eslint-plugin-custom/s057-utc-logging.js +54 -0
- package/eslint-integration/eslint-plugin-custom/s058-no-ssrf.js +73 -0
- package/eslint-integration/eslint-plugin-custom/t002-interface-prefix-i.js +42 -0
- package/eslint-integration/eslint-plugin-custom/t003-ts-ignore-reason.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t004-interface-public-only.js +160 -0
- package/eslint-integration/eslint-plugin-custom/t007-no-fn-in-constructor.js +52 -0
- package/eslint-integration/eslint-plugin-custom/t011-no-real-time-dependency.js +175 -0
- package/eslint-integration/eslint-plugin-custom/t019-no-empty-type.js +95 -0
- package/eslint-integration/eslint-plugin-custom/t025-no-nested-union-tuple.js +48 -0
- package/eslint-integration/eslint-plugin-custom/t026-limit-nested-generics.js +377 -0
- package/eslint-integration/eslint.config.js +125 -0
- package/eslint-integration/eslint.config.simple.js +24 -0
- package/eslint-integration/node_modules/eslint-plugin-custom/package.json +0 -0
- package/eslint-integration/package.json +23 -0
- package/eslint-integration/sample.ts +53 -0
- package/eslint-integration/test-s003.js +5 -0
- package/eslint-integration/tsconfig.json +27 -0
- package/examples/.github/workflows/code-quality.yml +111 -0
- package/examples/.sunlint.json +42 -0
- package/examples/README.md +47 -0
- package/examples/package.json +33 -0
- package/package.json +100 -0
- package/rules/C006_function_naming/analyzer.js +338 -0
- package/rules/C006_function_naming/config.json +86 -0
- package/rules/C019_log_level_usage/analyzer.js +359 -0
- package/rules/C019_log_level_usage/config.json +121 -0
- package/rules/C029_catch_block_logging/analyzer.js +339 -0
- package/rules/C029_catch_block_logging/config.json +59 -0
- package/rules/C031_validation_separation/README.md +72 -0
- package/rules/C031_validation_separation/analyzer.js +186 -0
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: C047 – Logic retry không được viết lặp lại nhiều nơi
|
|
3
|
+
* Rule ID: custom/c047
|
|
4
|
+
* Purpose: Detect duplicate retry logic patterns and enforce centralized retry utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Logic retry không được viết lặp lại nhiều nơi - use centralized retry utility instead",
|
|
12
|
+
recommended: false
|
|
13
|
+
},
|
|
14
|
+
schema: [
|
|
15
|
+
{
|
|
16
|
+
type: "object",
|
|
17
|
+
properties: {
|
|
18
|
+
maxRetryPatterns: {
|
|
19
|
+
type: "number",
|
|
20
|
+
minimum: 1,
|
|
21
|
+
default: 2,
|
|
22
|
+
description: "Maximum number of retry patterns allowed before suggesting centralization"
|
|
23
|
+
},
|
|
24
|
+
allowedRetryUtils: {
|
|
25
|
+
type: "array",
|
|
26
|
+
items: { type: "string" },
|
|
27
|
+
default: ["RetryUtil", "retryWithBackoff", "withRetry"],
|
|
28
|
+
description: "Names of allowed centralized retry utilities"
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
additionalProperties: false
|
|
32
|
+
}
|
|
33
|
+
],
|
|
34
|
+
messages: {
|
|
35
|
+
duplicateRetryLogic: "Duplicate retry logic detected ({{count}} occurrences). Consider using a centralized retry utility like RetryUtil.withRetry()",
|
|
36
|
+
inlineRetryLogic: "Inline retry logic found. Consider using a centralized retry utility for consistency and maintainability.",
|
|
37
|
+
suggestRetryUtil: "Use centralized retry utility instead of custom retry logic."
|
|
38
|
+
}
|
|
39
|
+
},
|
|
40
|
+
create(context) {
|
|
41
|
+
const options = context.options[0] || {};
|
|
42
|
+
const maxRetryPatterns = options.maxRetryPatterns || 2;
|
|
43
|
+
const allowedRetryUtils = options.allowedRetryUtils || ["RetryUtil", "retryWithBackoff", "withRetry"];
|
|
44
|
+
|
|
45
|
+
const retryPatterns = [];
|
|
46
|
+
const sourceCode = context.getSourceCode();
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Check if a node represents a retry pattern
|
|
50
|
+
*/
|
|
51
|
+
function isRetryPattern(node) {
|
|
52
|
+
// Pattern 1: for/while loop with try-catch for retry
|
|
53
|
+
if ((node.type === "ForStatement" || node.type === "WhileStatement") &&
|
|
54
|
+
node.body && node.body.type === "BlockStatement") {
|
|
55
|
+
const hasRetryLogic = node.body.body.some(stmt =>
|
|
56
|
+
stmt.type === "TryStatement" ||
|
|
57
|
+
(stmt.type === "IfStatement" && hasRetryCondition(stmt))
|
|
58
|
+
);
|
|
59
|
+
return hasRetryLogic;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pattern 2: do-while with try-catch
|
|
63
|
+
if (node.type === "DoWhileStatement" &&
|
|
64
|
+
node.body && node.body.type === "BlockStatement") {
|
|
65
|
+
return node.body.body.some(stmt => stmt.type === "TryStatement");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Pattern 3: recursive function with retry logic
|
|
69
|
+
if (node.type === "FunctionDeclaration" || node.type === "FunctionExpression") {
|
|
70
|
+
return hasRecursiveRetryPattern(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if statement has retry-related conditions
|
|
78
|
+
*/
|
|
79
|
+
function hasRetryCondition(ifStmt) {
|
|
80
|
+
if (!ifStmt.test) return false;
|
|
81
|
+
|
|
82
|
+
const testText = sourceCode.getText(ifStmt.test).toLowerCase();
|
|
83
|
+
return testText.includes('retry') ||
|
|
84
|
+
testText.includes('attempt') ||
|
|
85
|
+
testText.includes('tries') ||
|
|
86
|
+
testText.includes('maxretries') ||
|
|
87
|
+
testText.includes('maxattempts');
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Check if function has recursive retry pattern
|
|
92
|
+
*/
|
|
93
|
+
function hasRecursiveRetryPattern(funcNode) {
|
|
94
|
+
if (!funcNode.body || !funcNode.body.body) return false;
|
|
95
|
+
|
|
96
|
+
const funcName = funcNode.id ? funcNode.id.name : null;
|
|
97
|
+
if (!funcName) return false;
|
|
98
|
+
|
|
99
|
+
// Look for recursive calls with retry logic
|
|
100
|
+
const hasRecursiveCall = funcNode.body.body.some(stmt => {
|
|
101
|
+
if (stmt.type === "TryStatement" && stmt.handler) {
|
|
102
|
+
// Check if catch block has recursive call
|
|
103
|
+
return containsRecursiveCall(stmt.handler.body, funcName);
|
|
104
|
+
}
|
|
105
|
+
return false;
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
return hasRecursiveCall;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Check if block contains recursive call to the function
|
|
113
|
+
*/
|
|
114
|
+
function containsRecursiveCall(block, funcName) {
|
|
115
|
+
if (!block || !block.body) return false;
|
|
116
|
+
|
|
117
|
+
return block.body.some(stmt => {
|
|
118
|
+
if (stmt.type === "ReturnStatement" && stmt.argument) {
|
|
119
|
+
return containsCallExpression(stmt.argument, funcName);
|
|
120
|
+
}
|
|
121
|
+
if (stmt.type === "ExpressionStatement") {
|
|
122
|
+
return containsCallExpression(stmt.expression, funcName);
|
|
123
|
+
}
|
|
124
|
+
return false;
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if expression contains call to specific function
|
|
130
|
+
*/
|
|
131
|
+
function containsCallExpression(expr, funcName) {
|
|
132
|
+
if (expr.type === "CallExpression" &&
|
|
133
|
+
expr.callee && expr.callee.name === funcName) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (expr.type === "AwaitExpression" && expr.argument) {
|
|
138
|
+
return containsCallExpression(expr.argument, funcName);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return false;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Check if node uses allowed retry utilities
|
|
146
|
+
*/
|
|
147
|
+
function usesAllowedRetryUtil(node) {
|
|
148
|
+
const nodeText = sourceCode.getText(node);
|
|
149
|
+
return allowedRetryUtils.some(utilName => nodeText.includes(utilName));
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Get hash for retry pattern to detect duplicates
|
|
154
|
+
*/
|
|
155
|
+
function getRetryPatternHash(node) {
|
|
156
|
+
let text = sourceCode.getText(node);
|
|
157
|
+
// Normalize text for comparison (remove variable names, whitespace)
|
|
158
|
+
text = text
|
|
159
|
+
.replace(/\b[a-zA-Z_$][a-zA-Z0-9_$]*\b/g, 'VAR') // Replace identifiers
|
|
160
|
+
.replace(/\s+/g, ' ') // Normalize whitespace
|
|
161
|
+
.replace(/\/\*.*?\*\//g, '') // Remove block comments
|
|
162
|
+
.replace(/\/\/.*$/gm, ''); // Remove line comments
|
|
163
|
+
return text;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
// Check for retry patterns in various constructs
|
|
168
|
+
"ForStatement, WhileStatement, DoWhileStatement"(node) {
|
|
169
|
+
if (isRetryPattern(node) && !usesAllowedRetryUtil(node)) {
|
|
170
|
+
const hash = getRetryPatternHash(node);
|
|
171
|
+
const existing = retryPatterns.find(p => p.hash === hash);
|
|
172
|
+
|
|
173
|
+
if (existing) {
|
|
174
|
+
existing.count++;
|
|
175
|
+
existing.nodes.push(node);
|
|
176
|
+
} else {
|
|
177
|
+
retryPatterns.push({
|
|
178
|
+
hash,
|
|
179
|
+
count: 1,
|
|
180
|
+
nodes: [node],
|
|
181
|
+
type: node.type
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
"FunctionDeclaration, FunctionExpression"(node) {
|
|
188
|
+
if (isRetryPattern(node) && !usesAllowedRetryUtil(node)) {
|
|
189
|
+
context.report({
|
|
190
|
+
node,
|
|
191
|
+
messageId: "inlineRetryLogic"
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
},
|
|
195
|
+
|
|
196
|
+
// Check for inline setTimeout/setInterval retry patterns
|
|
197
|
+
"CallExpression"(node) {
|
|
198
|
+
if (node.callee &&
|
|
199
|
+
(node.callee.name === "setTimeout" || node.callee.name === "setInterval")) {
|
|
200
|
+
const parent = node.parent;
|
|
201
|
+
// Check if this setTimeout is part of retry logic
|
|
202
|
+
if (parent && parent.type === "ExpressionStatement") {
|
|
203
|
+
let current = parent.parent;
|
|
204
|
+
while (current) {
|
|
205
|
+
if (current.type === "TryStatement" ||
|
|
206
|
+
(current.type === "IfStatement" && hasRetryCondition(current))) {
|
|
207
|
+
if (!usesAllowedRetryUtil(current)) {
|
|
208
|
+
context.report({
|
|
209
|
+
node: current,
|
|
210
|
+
messageId: "inlineRetryLogic"
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
break;
|
|
214
|
+
}
|
|
215
|
+
current = current.parent;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
// Report duplicates at end of program
|
|
222
|
+
"Program:exit"() {
|
|
223
|
+
retryPatterns.forEach(pattern => {
|
|
224
|
+
if (pattern.count >= maxRetryPatterns) {
|
|
225
|
+
pattern.nodes.forEach(node => {
|
|
226
|
+
context.report({
|
|
227
|
+
node,
|
|
228
|
+
messageId: "duplicateRetryLogic",
|
|
229
|
+
data: {
|
|
230
|
+
count: pattern.count
|
|
231
|
+
}
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: C048 – Avoid using `var` declarations, use `let` or `const` instead
|
|
3
|
+
* Rule ID: custom/c048
|
|
4
|
+
* Purpose: Prevent usage of var due to hoisting issues, use let/const for clear scoping
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "problem",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Avoid using `var` declarations, use `let` or `const` instead",
|
|
12
|
+
recommended: true
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
noVar: "Do not use `var`. Use `let` or `const` instead."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
return {
|
|
21
|
+
VariableDeclaration(node) {
|
|
22
|
+
if (node.kind === "var") {
|
|
23
|
+
context.report({
|
|
24
|
+
node,
|
|
25
|
+
messageId: "noVar"
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: C076 – Each test should assert only one behavior (Single Assert Rule)
|
|
3
|
+
* Rule ID: custom/c076
|
|
4
|
+
* Purpose: A test case should only have one main behavior to test (one expect statement)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
module.exports = {
|
|
8
|
+
meta: {
|
|
9
|
+
type: "suggestion",
|
|
10
|
+
docs: {
|
|
11
|
+
description: "Each test should assert only one behavior (Single Assert Rule)",
|
|
12
|
+
recommended: false
|
|
13
|
+
},
|
|
14
|
+
schema: [],
|
|
15
|
+
messages: {
|
|
16
|
+
tooMany: "Test contains too many expect statements ({{count}}). Each test should have only one main behavior to verify."
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
create(context) {
|
|
20
|
+
function isTestFunction(node) {
|
|
21
|
+
// Safety check for node structure
|
|
22
|
+
if (!node || !node.callee) {
|
|
23
|
+
return false;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Check for test/it calls
|
|
27
|
+
if (node.type === "CallExpression") {
|
|
28
|
+
// Handle direct test/it calls
|
|
29
|
+
if (node.callee.type === "Identifier") {
|
|
30
|
+
return ["test", "it"].includes(node.callee.name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Handle imported test/it calls
|
|
34
|
+
if (node.callee.type === "MemberExpression" &&
|
|
35
|
+
node.callee.object.type === "Identifier" &&
|
|
36
|
+
["test", "it"].includes(node.callee.property.name)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return false;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isDescribeBlock(node) {
|
|
45
|
+
if (!node || !node.callee) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (node.type === "CallExpression") {
|
|
50
|
+
// Handle direct describe calls
|
|
51
|
+
if (node.callee.type === "Identifier") {
|
|
52
|
+
return node.callee.name === "describe";
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Handle imported describe calls
|
|
56
|
+
if (node.callee.type === "MemberExpression" &&
|
|
57
|
+
node.callee.object.type === "Identifier" &&
|
|
58
|
+
node.callee.property.name === "describe") {
|
|
59
|
+
return true;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function isSetupOrTeardown(node) {
|
|
67
|
+
if (!node || !node.callee) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (node.type === "CallExpression") {
|
|
72
|
+
const name = node.callee.type === "Identifier"
|
|
73
|
+
? node.callee.name
|
|
74
|
+
: node.callee.type === "MemberExpression"
|
|
75
|
+
? node.callee.property.name
|
|
76
|
+
: null;
|
|
77
|
+
|
|
78
|
+
return ["beforeEach", "afterEach", "beforeAll", "afterAll"].includes(name);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return false;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function countExpectCalls(body) {
|
|
85
|
+
let count = 0;
|
|
86
|
+
|
|
87
|
+
function traverse(node) {
|
|
88
|
+
// Safety check to ensure node exists and has type property
|
|
89
|
+
if (!node || typeof node !== 'object' || !node.type) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if this node is an expect call
|
|
94
|
+
if (
|
|
95
|
+
node.type === "CallExpression" &&
|
|
96
|
+
node.callee &&
|
|
97
|
+
node.callee.type === "Identifier" &&
|
|
98
|
+
node.callee.name === "expect"
|
|
99
|
+
) {
|
|
100
|
+
count++;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Safely traverse child nodes, but don't go into nested test functions
|
|
104
|
+
for (const key in node) {
|
|
105
|
+
if (key === 'parent' || key === 'range' || key === 'loc') {
|
|
106
|
+
continue; // Skip circular references and metadata
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const child = node[key];
|
|
110
|
+
if (Array.isArray(child)) {
|
|
111
|
+
child.forEach(item => {
|
|
112
|
+
if (item && typeof item === 'object' && item.type) {
|
|
113
|
+
// Don't traverse into nested test functions
|
|
114
|
+
if (!(item.type === "CallExpression" && isTestFunction(item))) {
|
|
115
|
+
traverse(item);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
} else if (child && typeof child === 'object' && child.type) {
|
|
120
|
+
// Don't traverse into nested test functions
|
|
121
|
+
if (!(child.type === "CallExpression" && isTestFunction(child))) {
|
|
122
|
+
traverse(child);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
traverse(body);
|
|
129
|
+
return count;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
CallExpression(node) {
|
|
134
|
+
// Only check test/it function calls
|
|
135
|
+
if (!isTestFunction(node)) {
|
|
136
|
+
return;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Ensure we have the required arguments (name and callback)
|
|
140
|
+
if (!node.arguments || node.arguments.length < 2) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const testCallback = node.arguments[1];
|
|
145
|
+
|
|
146
|
+
// Check if the second argument is a function (test body)
|
|
147
|
+
if (
|
|
148
|
+
!testCallback ||
|
|
149
|
+
(testCallback.type !== "FunctionExpression" &&
|
|
150
|
+
testCallback.type !== "ArrowFunctionExpression")
|
|
151
|
+
) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Get the function body
|
|
156
|
+
const fnBody = testCallback.body;
|
|
157
|
+
if (!fnBody) {
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// Handle both block statements and expression bodies
|
|
162
|
+
let bodyToCheck = fnBody;
|
|
163
|
+
if (testCallback.type === "ArrowFunctionExpression" && fnBody.type !== "BlockStatement") {
|
|
164
|
+
// For arrow functions with expression bodies, wrap in a virtual block
|
|
165
|
+
bodyToCheck = { type: "BlockStatement", body: [{ type: "ExpressionStatement", expression: fnBody }] };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// Count expect calls in the test body
|
|
169
|
+
const expectCount = countExpectCalls(bodyToCheck);
|
|
170
|
+
|
|
171
|
+
// Report if more than one expect statement
|
|
172
|
+
if (expectCount > 1) {
|
|
173
|
+
context.report({
|
|
174
|
+
node,
|
|
175
|
+
messageId: "tooMany",
|
|
176
|
+
data: {
|
|
177
|
+
count: expectCount
|
|
178
|
+
}
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint plugin: custom rules index
|
|
3
|
+
* Export toàn bộ rule tự định nghĩa dùng cho plugin "custom"
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const c002 = require("./c002-no-duplicate-code.js");
|
|
7
|
+
const c003 = require("./c003-no-vague-abbreviations.js");
|
|
8
|
+
const c006 = require("./c006-function-name-verb-noun.js");
|
|
9
|
+
const c010 = require("./c010-limit-block-nesting.js");
|
|
10
|
+
const c013 = require("./c013-no-dead-code.js");
|
|
11
|
+
const c014 = require("./c014-abstract-dependency-preferred.js");
|
|
12
|
+
const c017 = require("./c017-limit-constructor-logic.js");
|
|
13
|
+
const c018 = require("./c018-no-generic-throw.js");
|
|
14
|
+
const c023 = require("./c023-no-duplicate-variable-name-in-scope.js");
|
|
15
|
+
const c041 = require("./c041-no-config-inline.js");
|
|
16
|
+
const c027 = require("./c027-limit-function-nesting.js");
|
|
17
|
+
const c029 = require("./c029-catch-block-logging.js");
|
|
18
|
+
const c030 = require("./c030-use-custom-error-classes.js");
|
|
19
|
+
const c034 = require("./c034-no-implicit-return.js");
|
|
20
|
+
const c035 = require("./c035-no-empty-catch.js");
|
|
21
|
+
const c042 = require("./c042-boolean-name-prefix.js");
|
|
22
|
+
const c043 = require("./c043-no-console-or-print.js");
|
|
23
|
+
const c047 = require("./c047-no-duplicate-retry-logic.js");
|
|
24
|
+
const c048 = require("./c048-no-var-declaration.js");
|
|
25
|
+
const t011 = require("./t011-no-real-time-dependency.js");
|
|
26
|
+
const c076 = require("./c076-one-assert-per-test.js");
|
|
27
|
+
const t002 = require("./t002-interface-prefix-i.js");
|
|
28
|
+
const t003 = require("./t003-ts-ignore-reason.js");
|
|
29
|
+
const t004 = require("./t004-interface-public-only.js");
|
|
30
|
+
const t019 = require("./t019-no-empty-type.js");
|
|
31
|
+
const t007 = require("./t007-no-fn-in-constructor.js");
|
|
32
|
+
const t025 = require("./t025-no-nested-union-tuple.js");
|
|
33
|
+
const t026 = require("./t026-limit-nested-generics.js");
|
|
34
|
+
|
|
35
|
+
// Security rules
|
|
36
|
+
const s003 = require("./s003-no-unvalidated-redirect.js");
|
|
37
|
+
const s005 = require("./s005-no-origin-auth.js");
|
|
38
|
+
const s006 = require("./s006-activation-recovery-secret-not-plaintext.js");
|
|
39
|
+
const s008 = require("./s008-crypto-agility.js");
|
|
40
|
+
const s009 = require("./s009-no-insecure-crypto.js");
|
|
41
|
+
const s010 = require("./s010-no-insecure-random-in-sensitive-context.js");
|
|
42
|
+
const s011 = require("./s011-no-insecure-uuid.js");
|
|
43
|
+
const s012 = require("./s012-hardcode-secret.js");
|
|
44
|
+
const s014 = require("./s014-insecure-tls-version.js");
|
|
45
|
+
const s015 = require("./s015-insecure-tls-certificate.js");
|
|
46
|
+
const s016 = require("./s016-sensitive-query-parameter.js");
|
|
47
|
+
const s017 = require("./s017-no-sql-injection.js");
|
|
48
|
+
const s018 = require("./s018-positive-input-validation.js");
|
|
49
|
+
const s019 = require("./s019-no-raw-user-input-in-email.js");
|
|
50
|
+
const s020 = require("./s020-no-eval-dynamic-execution.js");
|
|
51
|
+
const s022 = require("./s022-output-encoding.js");
|
|
52
|
+
const s023 = require("./s023-no-json-injection.js");
|
|
53
|
+
const s025 = require("./s025-server-side-input-validation.js");
|
|
54
|
+
const s026 = require("./s026-json-schema-validation.js");
|
|
55
|
+
const s027 = require("./s027-no-hardcoded-secrets.js");
|
|
56
|
+
const s029 = require("./s029-require-csrf-protection.js");
|
|
57
|
+
const s030 = require("./s030-no-directory-browsing.js");
|
|
58
|
+
const s033 = require("./s033-require-samesite-cookie.js");
|
|
59
|
+
const s034 = require("./s034-require-host-cookie-prefix.js");
|
|
60
|
+
const s035 = require("./s035-cookie-specific-path.js");
|
|
61
|
+
const s036 = require("./s036-no-unsafe-file-include.js");
|
|
62
|
+
const s037 = require("./s037-require-anti-cache-headers.js");
|
|
63
|
+
const s038 = require("./s038-no-version-disclosure.js");
|
|
64
|
+
const s039 = require("./s039-no-session-token-in-url.js");
|
|
65
|
+
const s041 = require("./s041-require-session-invalidate-on-logout.js");
|
|
66
|
+
const s042 = require("./s042-require-periodic-reauthentication.js");
|
|
67
|
+
const s043 = require("./s043-terminate-sessions-on-password-change.js");
|
|
68
|
+
const s044 = require("./s044-require-full-session-for-sensitive-operations.js");
|
|
69
|
+
const s045 = require("./s045-anti-automation-controls.js");
|
|
70
|
+
const s046 = require("./s046-secure-notification-on-auth-change.js");
|
|
71
|
+
const s047 = require("./s047-secure-random-passwords.js");
|
|
72
|
+
const s048 = require("./s048-password-credential-recovery.js");
|
|
73
|
+
const s050 = require("./s050-session-token-weak-hash.js");
|
|
74
|
+
const s052 = require("./s052-secure-random-authentication-code.js");
|
|
75
|
+
const s054 = require("./s054-verification-default-account.js");
|
|
76
|
+
const s055 = require("./s055-verification-rest-check-the-incoming-content-type.js");
|
|
77
|
+
const s057 = require("./s057-utc-logging.js");
|
|
78
|
+
const s058 = require("./s058-no-ssrf.js");
|
|
79
|
+
|
|
80
|
+
module.exports = {
|
|
81
|
+
rules: {
|
|
82
|
+
"c002": c002,
|
|
83
|
+
"c003": c003,
|
|
84
|
+
"c006": c006,
|
|
85
|
+
"c010": c010,
|
|
86
|
+
"c013": c013,
|
|
87
|
+
"c014": c014,
|
|
88
|
+
"c017": c017,
|
|
89
|
+
"c018": c018,
|
|
90
|
+
"c030": c030,
|
|
91
|
+
"c035": c035,
|
|
92
|
+
"c023": c023,
|
|
93
|
+
"c027": c027,
|
|
94
|
+
"c029": c029,
|
|
95
|
+
"c034": c034,
|
|
96
|
+
"c041": c041,
|
|
97
|
+
"c042": c042,
|
|
98
|
+
"c043": c043,
|
|
99
|
+
"c047": c047,
|
|
100
|
+
"c048": c048,
|
|
101
|
+
"c076": c076,
|
|
102
|
+
"t002": t002,
|
|
103
|
+
"t003": t003,
|
|
104
|
+
"t019": t019,
|
|
105
|
+
"t004": t004,
|
|
106
|
+
"t007": t007,
|
|
107
|
+
"t025": t025,
|
|
108
|
+
"t011": t011,
|
|
109
|
+
"t026": t026,
|
|
110
|
+
// Security rules
|
|
111
|
+
"typescript_s003": s003,
|
|
112
|
+
"typescript_s005": s005,
|
|
113
|
+
"typescript_s006": s006,
|
|
114
|
+
"typescript_s008": s008,
|
|
115
|
+
"typescript_s009": s009,
|
|
116
|
+
"typescript_s010": s010,
|
|
117
|
+
"typescript_s011": s011,
|
|
118
|
+
"typescript_s012": s012,
|
|
119
|
+
"typescript_s014": s014,
|
|
120
|
+
"typescript_s015": s015,
|
|
121
|
+
"typescript_s016": s016,
|
|
122
|
+
"typescript_s017": s017,
|
|
123
|
+
"typescript_s018": s018,
|
|
124
|
+
"typescript_s019": s019,
|
|
125
|
+
"typescript_s020": s020,
|
|
126
|
+
"typescript_s022": s022,
|
|
127
|
+
"typescript_s023": s023,
|
|
128
|
+
"typescript_s025": s025,
|
|
129
|
+
"typescript_s026": s026,
|
|
130
|
+
"typescript_s027": s027,
|
|
131
|
+
"typescript_s029": s029,
|
|
132
|
+
"typescript_s030": s030,
|
|
133
|
+
"typescript_s033": s033,
|
|
134
|
+
"typescript_s034": s034,
|
|
135
|
+
"typescript_s035": s035,
|
|
136
|
+
"typescript_s036": s036,
|
|
137
|
+
"typescript_s037": s037,
|
|
138
|
+
"typescript_s038": s038,
|
|
139
|
+
"typescript_s039": s039,
|
|
140
|
+
"typescript_s041": s041,
|
|
141
|
+
"typescript_s042": s042,
|
|
142
|
+
"typescript_s043": s043,
|
|
143
|
+
"typescript_s044": s044,
|
|
144
|
+
"typescript_s045": s045,
|
|
145
|
+
"typescript_s046": s046,
|
|
146
|
+
"typescript_s047": s047,
|
|
147
|
+
"typescript_s048": s048,
|
|
148
|
+
"typescript_s050": s050,
|
|
149
|
+
"typescript_s052": s052,
|
|
150
|
+
"typescript_s054": s054,
|
|
151
|
+
"typescript_s055": s055,
|
|
152
|
+
"typescript_s057": s057,
|
|
153
|
+
"typescript_s058": s058
|
|
154
|
+
}
|
|
155
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "eslint-plugin-custom",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Custom ESLint rules for Sun Lint security and quality checks",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "commonjs",
|
|
7
|
+
"engines": {
|
|
8
|
+
"node": ">=14.0.0"
|
|
9
|
+
},
|
|
10
|
+
"keywords": ["eslint", "security", "coding-standards"],
|
|
11
|
+
"author": "Sun* Engineering",
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|