@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,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule: S041 – Require session invalidation on logout
|
|
3
|
+
* Rule ID: custom/s041
|
|
4
|
+
* Purpose: Ensure logout handlers properly invalidate session tokens and clear cookies
|
|
5
|
+
* OWASP 3.3.1: Verify that logout and expiration invalidate the session token
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
"use strict";
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description: "Ensure logout handlers properly invalidate session tokens and prevent session reuse",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
missingSessionInvalidation: "Logout method '{{method}}' must invalidate session token. Use session.invalidate(), req.session.destroy(), or equivalent session cleanup.",
|
|
20
|
+
missingCookieClear: "Logout method '{{method}}' should clear authentication cookies to prevent session reuse.",
|
|
21
|
+
missingCacheControl: "Logout method '{{method}}' should set cache-control headers to prevent back button authentication.",
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
// Keywords that indicate logout functionality
|
|
27
|
+
const logoutKeywords = [
|
|
28
|
+
"logout", "signout", "sign-out", "logoff", "signoff",
|
|
29
|
+
"disconnect", "terminate", "exit", "end-session"
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Session invalidation methods
|
|
33
|
+
const sessionInvalidationMethods = [
|
|
34
|
+
"invalidate", "destroy", "remove", "clear", "delete",
|
|
35
|
+
"expire", "revoke", "blacklist"
|
|
36
|
+
];
|
|
37
|
+
|
|
38
|
+
// Cookie clearing methods
|
|
39
|
+
const cookieClearMethods = [
|
|
40
|
+
"clearCookie", "removeCookie", "deleteCookie", "expireCookie"
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
// Cache control methods
|
|
44
|
+
const cacheControlMethods = [
|
|
45
|
+
"setHeader", "header", "set", "no-cache", "no-store"
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
function isLogoutMethod(name) {
|
|
49
|
+
if (!name) return false;
|
|
50
|
+
const lowerName = name.toLowerCase();
|
|
51
|
+
return logoutKeywords.some(keyword => lowerName.includes(keyword));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function checkLogoutMethodBody(node, methodName) {
|
|
55
|
+
let hasSessionInvalidation = false;
|
|
56
|
+
let hasCookieClearing = false;
|
|
57
|
+
let hasCacheControl = false;
|
|
58
|
+
|
|
59
|
+
function checkNode(n, visited = new Set()) {
|
|
60
|
+
if (!n || visited.has(n)) return;
|
|
61
|
+
visited.add(n);
|
|
62
|
+
|
|
63
|
+
// Check for session invalidation
|
|
64
|
+
if (n.type === "CallExpression") {
|
|
65
|
+
const callee = n.callee;
|
|
66
|
+
|
|
67
|
+
// session.invalidate(), req.session.destroy(), etc.
|
|
68
|
+
if (callee.type === "MemberExpression") {
|
|
69
|
+
const property = callee.property.name;
|
|
70
|
+
const object = callee.object;
|
|
71
|
+
|
|
72
|
+
if (sessionInvalidationMethods.includes(property)) {
|
|
73
|
+
// Check if it's session-related: session.invalidate(), req.session.destroy()
|
|
74
|
+
if (object.type === "Identifier" && object.name === "session") {
|
|
75
|
+
hasSessionInvalidation = true;
|
|
76
|
+
} else if (object.type === "MemberExpression" &&
|
|
77
|
+
object.property && object.property.name === "session") {
|
|
78
|
+
hasSessionInvalidation = true;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check for cookie clearing: res.clearCookie()
|
|
83
|
+
if (cookieClearMethods.includes(property)) {
|
|
84
|
+
hasCookieClearing = true;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Check for cache control headers
|
|
88
|
+
if (cacheControlMethods.includes(property)) {
|
|
89
|
+
const args = n.arguments;
|
|
90
|
+
if (args.length > 0) {
|
|
91
|
+
const firstArg = args[0];
|
|
92
|
+
if (firstArg.type === "Literal") {
|
|
93
|
+
const header = firstArg.value;
|
|
94
|
+
if (typeof header === "string" &&
|
|
95
|
+
(header.toLowerCase().includes("cache-control") ||
|
|
96
|
+
header.toLowerCase().includes("pragma"))) {
|
|
97
|
+
hasCacheControl = true;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Recursively check specific node types to avoid infinite loops
|
|
106
|
+
const nodeTypesToCheck = [
|
|
107
|
+
'BlockStatement', 'ExpressionStatement', 'CallExpression',
|
|
108
|
+
'MemberExpression', 'ArrowFunctionExpression', 'FunctionExpression'
|
|
109
|
+
];
|
|
110
|
+
|
|
111
|
+
for (const key in n) {
|
|
112
|
+
if (n[key] && typeof n[key] === "object" && key !== 'parent') {
|
|
113
|
+
if (Array.isArray(n[key])) {
|
|
114
|
+
n[key].forEach(child => {
|
|
115
|
+
if (child && child.type && nodeTypesToCheck.includes(child.type)) {
|
|
116
|
+
checkNode(child, visited);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
} else if (n[key].type && nodeTypesToCheck.includes(n[key].type)) {
|
|
120
|
+
checkNode(n[key], visited);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check method body
|
|
127
|
+
if (node.body) {
|
|
128
|
+
checkNode(node.body);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Report missing requirements
|
|
132
|
+
if (!hasSessionInvalidation) {
|
|
133
|
+
context.report({
|
|
134
|
+
node,
|
|
135
|
+
messageId: "missingSessionInvalidation",
|
|
136
|
+
data: { method: methodName }
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!hasCookieClearing) {
|
|
141
|
+
context.report({
|
|
142
|
+
node,
|
|
143
|
+
messageId: "missingCookieClear",
|
|
144
|
+
data: { method: methodName }
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
if (!hasCacheControl) {
|
|
149
|
+
context.report({
|
|
150
|
+
node,
|
|
151
|
+
messageId: "missingCacheControl",
|
|
152
|
+
data: { method: methodName }
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return {
|
|
158
|
+
// Check class methods (NestJS controllers)
|
|
159
|
+
MethodDefinition(node) {
|
|
160
|
+
const methodName = node.key.name;
|
|
161
|
+
if (isLogoutMethod(methodName)) {
|
|
162
|
+
checkLogoutMethodBody(node.value, methodName);
|
|
163
|
+
}
|
|
164
|
+
},
|
|
165
|
+
|
|
166
|
+
// Check function declarations
|
|
167
|
+
FunctionDeclaration(node) {
|
|
168
|
+
const functionName = node.id?.name;
|
|
169
|
+
if (isLogoutMethod(functionName)) {
|
|
170
|
+
checkLogoutMethodBody(node, functionName);
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
|
|
174
|
+
// Check arrow functions and function expressions assigned to variables
|
|
175
|
+
VariableDeclarator(node) {
|
|
176
|
+
if (node.id.type === "Identifier" && node.init) {
|
|
177
|
+
const varName = node.id.name;
|
|
178
|
+
if (isLogoutMethod(varName)) {
|
|
179
|
+
if (node.init.type === "ArrowFunctionExpression" ||
|
|
180
|
+
node.init.type === "FunctionExpression") {
|
|
181
|
+
checkLogoutMethodBody(node.init, varName);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
},
|
|
186
|
+
|
|
187
|
+
// Check route handlers with logout paths
|
|
188
|
+
CallExpression(node) {
|
|
189
|
+
const callee = node.callee;
|
|
190
|
+
|
|
191
|
+
// Express/NestJS route: app.post('/logout', handler)
|
|
192
|
+
if (callee.type === "MemberExpression" &&
|
|
193
|
+
["post", "get", "put", "delete"].includes(callee.property.name) &&
|
|
194
|
+
node.arguments.length >= 2) {
|
|
195
|
+
|
|
196
|
+
const pathArg = node.arguments[0];
|
|
197
|
+
if (pathArg.type === "Literal" && typeof pathArg.value === "string") {
|
|
198
|
+
const path = pathArg.value.toLowerCase();
|
|
199
|
+
if (logoutKeywords.some(keyword => path.includes(keyword))) {
|
|
200
|
+
const handler = node.arguments[node.arguments.length - 1];
|
|
201
|
+
if (handler.type === "ArrowFunctionExpression" ||
|
|
202
|
+
handler.type === "FunctionExpression") {
|
|
203
|
+
checkLogoutMethodBody(handler, `route handler for ${pathArg.value}`);
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
};
|
|
210
|
+
},
|
|
211
|
+
};
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule: S042 – Require periodic re-authentication
|
|
3
|
+
* Rule ID: custom/s042
|
|
4
|
+
* Purpose: Verify that if authenticators permit users to remain logged in,
|
|
5
|
+
* re-authentication occurs periodically both when actively used or after an idle period
|
|
6
|
+
* OWASP 3.3.2: If authenticators permit users to remain logged in, verify that
|
|
7
|
+
* re-authentication occurs periodically both when actively used or after an idle period
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
"use strict";
|
|
11
|
+
|
|
12
|
+
module.exports = {
|
|
13
|
+
meta: {
|
|
14
|
+
type: "problem",
|
|
15
|
+
docs: {
|
|
16
|
+
description: "Ensure periodic re-authentication is implemented for long-lived sessions",
|
|
17
|
+
recommended: true,
|
|
18
|
+
},
|
|
19
|
+
schema: [],
|
|
20
|
+
messages: {
|
|
21
|
+
missingReauthentication: "Authentication method '{{method}}' should implement periodic re-authentication for long-lived sessions.",
|
|
22
|
+
missingIdleTimeout: "Session configuration should include idle timeout mechanism for automatic logout.",
|
|
23
|
+
missingActiveTimeout: "Session configuration should include maximum active session duration (e.g., 12 hours).",
|
|
24
|
+
missing2FAForSensitive: "Sensitive operations should require two-factor authentication (2FA) regardless of session state.",
|
|
25
|
+
missingReauthForSensitive: "Sensitive operations should require re-authentication even for active sessions.",
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
create(context) {
|
|
30
|
+
// Keywords that indicate authentication/session functionality
|
|
31
|
+
const authKeywords = [
|
|
32
|
+
"auth", "login", "signin", "sign-in", "authenticate", "session",
|
|
33
|
+
"passport", "jwt", "token", "guard", "middleware"
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Session configuration keywords
|
|
37
|
+
const sessionConfigKeywords = [
|
|
38
|
+
"session", "maxage", "expires", "timeout", "idle", "duration",
|
|
39
|
+
"lifetime", "ttl", "expiry"
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
// Sensitive operation keywords
|
|
43
|
+
const sensitiveOperationKeywords = [
|
|
44
|
+
"payment", "transfer", "withdraw", "deposit", "transaction",
|
|
45
|
+
"delete", "remove", "destroy", "admin", "privilege", "role",
|
|
46
|
+
"password", "email", "profile", "settings", "config"
|
|
47
|
+
];
|
|
48
|
+
|
|
49
|
+
// Two-factor authentication keywords
|
|
50
|
+
const tfaKeywords = ["2fa", "mfa", "totp", "otp", "authenticator"];
|
|
51
|
+
|
|
52
|
+
// Re-authentication keywords
|
|
53
|
+
const reauthKeywords = ["reauth", "re-auth", "verify", "confirm"];
|
|
54
|
+
|
|
55
|
+
function isAuthenticationRelated(name) {
|
|
56
|
+
if (!name) return false;
|
|
57
|
+
const lowerName = name.toLowerCase();
|
|
58
|
+
return authKeywords.some(keyword => lowerName.includes(keyword));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function isSensitiveOperation(name) {
|
|
62
|
+
if (!name) return false;
|
|
63
|
+
const lowerName = name.toLowerCase();
|
|
64
|
+
return sensitiveOperationKeywords.some(keyword => lowerName.includes(keyword));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hasSessionConfiguration(node) {
|
|
68
|
+
let hasIdleTimeout = false;
|
|
69
|
+
let hasMaxAge = false;
|
|
70
|
+
|
|
71
|
+
function checkConfigObject(obj) {
|
|
72
|
+
if (obj.type === "ObjectExpression") {
|
|
73
|
+
obj.properties.forEach(prop => {
|
|
74
|
+
if (prop.key && prop.key.name) {
|
|
75
|
+
const keyName = prop.key.name.toLowerCase();
|
|
76
|
+
if (keyName.includes("idle") || keyName.includes("timeout")) {
|
|
77
|
+
hasIdleTimeout = true;
|
|
78
|
+
}
|
|
79
|
+
if (keyName.includes("maxage") || keyName.includes("expires") ||
|
|
80
|
+
keyName.includes("duration") || keyName.includes("lifetime")) {
|
|
81
|
+
hasMaxAge = true;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Check for session configuration in various patterns
|
|
89
|
+
if (node.type === "CallExpression") {
|
|
90
|
+
node.arguments.forEach(arg => {
|
|
91
|
+
checkConfigObject(arg);
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return { hasIdleTimeout, hasMaxAge };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function hasReauthenticationLogic(node, visited = new Set()) {
|
|
99
|
+
if (!node || visited.has(node)) return { hasReauth: false, has2FA: false };
|
|
100
|
+
visited.add(node);
|
|
101
|
+
|
|
102
|
+
let hasReauth = false;
|
|
103
|
+
let has2FA = false;
|
|
104
|
+
|
|
105
|
+
function checkNode(n) {
|
|
106
|
+
if (!n || visited.has(n)) return;
|
|
107
|
+
visited.add(n);
|
|
108
|
+
|
|
109
|
+
// Check for re-authentication calls
|
|
110
|
+
if (n.type === "CallExpression" && n.callee && n.callee.type === "MemberExpression") {
|
|
111
|
+
const methodName = n.callee.property && n.callee.property.name;
|
|
112
|
+
if (methodName && reauthKeywords.some(keyword =>
|
|
113
|
+
methodName.toLowerCase().includes(keyword))) {
|
|
114
|
+
hasReauth = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Check for 2FA implementation
|
|
119
|
+
if (n.type === "CallExpression") {
|
|
120
|
+
const callText = context.getSourceCode().getText(n).toLowerCase();
|
|
121
|
+
if (tfaKeywords.some(keyword => callText.includes(keyword))) {
|
|
122
|
+
has2FA = true;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Check identifier names
|
|
127
|
+
if (n.type === "Identifier") {
|
|
128
|
+
const name = n.name.toLowerCase();
|
|
129
|
+
if (reauthKeywords.some(keyword => name.includes(keyword))) {
|
|
130
|
+
hasReauth = true;
|
|
131
|
+
}
|
|
132
|
+
if (tfaKeywords.some(keyword => name.includes(keyword))) {
|
|
133
|
+
has2FA = true;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Only check direct children to avoid deep recursion
|
|
138
|
+
if (n.type === "BlockStatement" && n.body) {
|
|
139
|
+
n.body.forEach(stmt => checkNode(stmt));
|
|
140
|
+
} else if (n.type === "ExpressionStatement" && n.expression) {
|
|
141
|
+
checkNode(n.expression);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
checkNode(node);
|
|
146
|
+
return { hasReauth, has2FA };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return {
|
|
150
|
+
// Check class methods (NestJS controllers/guards)
|
|
151
|
+
MethodDefinition(node) {
|
|
152
|
+
const methodName = node.key.name;
|
|
153
|
+
|
|
154
|
+
if (isAuthenticationRelated(methodName)) {
|
|
155
|
+
const { hasIdleTimeout, hasMaxAge } = hasSessionConfiguration(node);
|
|
156
|
+
|
|
157
|
+
if (!hasIdleTimeout) {
|
|
158
|
+
context.report({
|
|
159
|
+
node,
|
|
160
|
+
messageId: "missingIdleTimeout",
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!hasMaxAge) {
|
|
165
|
+
context.report({
|
|
166
|
+
node,
|
|
167
|
+
messageId: "missingActiveTimeout",
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
if (isSensitiveOperation(methodName)) {
|
|
173
|
+
const { hasReauth, has2FA } = hasReauthenticationLogic(node.value);
|
|
174
|
+
|
|
175
|
+
if (!hasReauth) {
|
|
176
|
+
context.report({
|
|
177
|
+
node,
|
|
178
|
+
messageId: "missingReauthForSensitive",
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (!has2FA) {
|
|
183
|
+
context.report({
|
|
184
|
+
node,
|
|
185
|
+
messageId: "missing2FAForSensitive",
|
|
186
|
+
});
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
},
|
|
190
|
+
|
|
191
|
+
// Check function declarations
|
|
192
|
+
FunctionDeclaration(node) {
|
|
193
|
+
const functionName = node.id ? node.id.name : null;
|
|
194
|
+
|
|
195
|
+
if (isAuthenticationRelated(functionName)) {
|
|
196
|
+
const { hasIdleTimeout, hasMaxAge } = hasSessionConfiguration(node);
|
|
197
|
+
|
|
198
|
+
if (!hasIdleTimeout) {
|
|
199
|
+
context.report({
|
|
200
|
+
node,
|
|
201
|
+
messageId: "missingIdleTimeout",
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (!hasMaxAge) {
|
|
206
|
+
context.report({
|
|
207
|
+
node,
|
|
208
|
+
messageId: "missingActiveTimeout",
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
if (isSensitiveOperation(functionName)) {
|
|
214
|
+
const { hasReauth, has2FA } = hasReauthenticationLogic(node);
|
|
215
|
+
|
|
216
|
+
if (!hasReauth) {
|
|
217
|
+
context.report({
|
|
218
|
+
node,
|
|
219
|
+
messageId: "missingReauthForSensitive",
|
|
220
|
+
});
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
// Check arrow functions assigned to variables
|
|
226
|
+
VariableDeclarator(node) {
|
|
227
|
+
if (node.id.type === "Identifier" && node.init) {
|
|
228
|
+
const varName = node.id.name;
|
|
229
|
+
|
|
230
|
+
if (isAuthenticationRelated(varName) &&
|
|
231
|
+
(node.init.type === "ArrowFunctionExpression" ||
|
|
232
|
+
node.init.type === "FunctionExpression")) {
|
|
233
|
+
|
|
234
|
+
const { hasIdleTimeout, hasMaxAge } = hasSessionConfiguration(node.init);
|
|
235
|
+
|
|
236
|
+
if (!hasIdleTimeout) {
|
|
237
|
+
context.report({
|
|
238
|
+
node,
|
|
239
|
+
messageId: "missingIdleTimeout",
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (!hasMaxAge) {
|
|
244
|
+
context.report({
|
|
245
|
+
node,
|
|
246
|
+
messageId: "missingActiveTimeout",
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
if (isSensitiveOperation(varName) &&
|
|
252
|
+
(node.init.type === "ArrowFunctionExpression" ||
|
|
253
|
+
node.init.type === "FunctionExpression")) {
|
|
254
|
+
|
|
255
|
+
const { hasReauth, has2FA } = hasReauthenticationLogic(node.init);
|
|
256
|
+
|
|
257
|
+
if (!hasReauth) {
|
|
258
|
+
context.report({
|
|
259
|
+
node,
|
|
260
|
+
messageId: "missingReauthForSensitive",
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
// Check session configuration calls
|
|
268
|
+
CallExpression(node) {
|
|
269
|
+
const sourceCode = context.getSourceCode().getText(node).toLowerCase();
|
|
270
|
+
|
|
271
|
+
// Check for session middleware configuration
|
|
272
|
+
if (sourceCode.includes("session") &&
|
|
273
|
+
(sourceCode.includes("express") || sourceCode.includes("app.use"))) {
|
|
274
|
+
|
|
275
|
+
const { hasIdleTimeout, hasMaxAge } = hasSessionConfiguration(node);
|
|
276
|
+
|
|
277
|
+
if (!hasIdleTimeout) {
|
|
278
|
+
context.report({
|
|
279
|
+
node,
|
|
280
|
+
messageId: "missingIdleTimeout",
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (!hasMaxAge) {
|
|
285
|
+
context.report({
|
|
286
|
+
node,
|
|
287
|
+
messageId: "missingActiveTimeout",
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
};
|
|
293
|
+
},
|
|
294
|
+
};
|