@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,108 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S047 – Verify system generated initial passwords or activation codes SHOULD be securely randomly generated
|
|
5
|
+
* OWASP ASVS 2.3.1
|
|
6
|
+
* Rule ID: custom/s047
|
|
7
|
+
* SHOULD be at least 6 characters long, and MAY contain letters and numbers, and expire after a short period of time. These initial secrets must not be permitted to become the long term password.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description:
|
|
16
|
+
"Initial passwords or activation codes must be securely generated, at least 6 characters, expire soon, and not reused long-term.",
|
|
17
|
+
recommended: false,
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
weakInitialSecret:
|
|
21
|
+
"Initial secret (password or activation code) must be securely randomly generated, at least 6 characters, expire soon, and not reused long-term.",
|
|
22
|
+
},
|
|
23
|
+
schema: [],
|
|
24
|
+
},
|
|
25
|
+
|
|
26
|
+
create(context) {
|
|
27
|
+
const SECRET_NAME_REGEX = /(initial|activation|temp).*?(password|code|token|secret)/i;
|
|
28
|
+
|
|
29
|
+
function isPotentialSecretName(name) {
|
|
30
|
+
return SECRET_NAME_REGEX.test(name);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function checkInsecureGeneration(node) {
|
|
34
|
+
const callee = node.callee;
|
|
35
|
+
|
|
36
|
+
// Check for Math.random()
|
|
37
|
+
if (
|
|
38
|
+
callee.type === "MemberExpression" &&
|
|
39
|
+
callee.object.type === "Identifier" &&
|
|
40
|
+
callee.object.name === "Math" &&
|
|
41
|
+
callee.property.type === "Identifier" &&
|
|
42
|
+
callee.property.name === "random"
|
|
43
|
+
) {
|
|
44
|
+
context.report({ node, messageId: "weakInitialSecret" });
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Check for Date.now()
|
|
48
|
+
if (
|
|
49
|
+
callee.type === "MemberExpression" &&
|
|
50
|
+
callee.object.type === "Identifier" &&
|
|
51
|
+
callee.object.name === "Date" &&
|
|
52
|
+
callee.property.type === "Identifier" &&
|
|
53
|
+
callee.property.name === "now"
|
|
54
|
+
) {
|
|
55
|
+
context.report({ node, messageId: "weakInitialSecret" });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return {
|
|
60
|
+
VariableDeclarator(node) {
|
|
61
|
+
if (
|
|
62
|
+
node.id.type === "Identifier" &&
|
|
63
|
+
isPotentialSecretName(node.id.name) &&
|
|
64
|
+
node.init
|
|
65
|
+
) {
|
|
66
|
+
if (node.init.type === "Literal") {
|
|
67
|
+
const val = node.init.value;
|
|
68
|
+
if (typeof val === "string" && val.length < 6) {
|
|
69
|
+
context.report({ node, messageId: "weakInitialSecret" });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
if (
|
|
74
|
+
node.init.type === "CallExpression" ||
|
|
75
|
+
node.init.type === "NewExpression"
|
|
76
|
+
) {
|
|
77
|
+
checkInsecureGeneration(node.init);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
|
|
82
|
+
AssignmentExpression(node) {
|
|
83
|
+
const left = node.left;
|
|
84
|
+
const right = node.right;
|
|
85
|
+
|
|
86
|
+
if (
|
|
87
|
+
left.type === "MemberExpression" &&
|
|
88
|
+
left.property.type === "Identifier" &&
|
|
89
|
+
isPotentialSecretName(left.property.name)
|
|
90
|
+
) {
|
|
91
|
+
if (right.type === "Literal") {
|
|
92
|
+
const val = right.value;
|
|
93
|
+
if (typeof val === "string" && val.length < 6) {
|
|
94
|
+
context.report({ node, messageId: "weakInitialSecret" });
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (
|
|
99
|
+
right.type === "CallExpression" ||
|
|
100
|
+
right.type === "NewExpression"
|
|
101
|
+
) {
|
|
102
|
+
checkInsecureGeneration(right);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
},
|
|
108
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S048 – Password Credential Recovery
|
|
5
|
+
* OWASP ASVS 2.4.3
|
|
6
|
+
* Ensure password credential recovery does not reveal the current password in any way.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Ensure password recovery does not reveal the current password. Users must set a new password via a secure, one-time token.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
revealPassword:
|
|
20
|
+
"Never reveal or send the current password during recovery. Always require user to set a new password.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
CallExpression(node) {
|
|
27
|
+
// Example: sending current password in email (should not happen)
|
|
28
|
+
if (
|
|
29
|
+
node.callee.type === "Identifier" &&
|
|
30
|
+
node.callee.name === "sendMail" &&
|
|
31
|
+
node.arguments.length &&
|
|
32
|
+
node.arguments[0].type === "ObjectExpression"
|
|
33
|
+
) {
|
|
34
|
+
const bodyProp = node.arguments[0].properties.find(
|
|
35
|
+
(prop) =>
|
|
36
|
+
prop.key &&
|
|
37
|
+
(prop.key.name === "text" || prop.key.name === "html") &&
|
|
38
|
+
prop.value.type === "Literal" &&
|
|
39
|
+
(prop.value.value
|
|
40
|
+
.toLowerCase()
|
|
41
|
+
.includes("your current password") ||
|
|
42
|
+
prop.value.value.toLowerCase().includes("mật khẩu hiện tại"))
|
|
43
|
+
);
|
|
44
|
+
if (bodyProp) {
|
|
45
|
+
context.report({
|
|
46
|
+
node: bodyProp.value,
|
|
47
|
+
messageId: "revealPassword",
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: S050 - Session Token Weak Entropy or Algorithm
|
|
3
|
+
* Rule ID: custom/s050
|
|
4
|
+
* Description: Ensure session tokens use secure algorithms and have at least 64-bit entropy.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const WEAK_HASH_ALGOS = new Set(["md5", "sha1"]);
|
|
10
|
+
|
|
11
|
+
module.exports = {
|
|
12
|
+
meta: {
|
|
13
|
+
type: "problem",
|
|
14
|
+
docs: {
|
|
15
|
+
description:
|
|
16
|
+
"Avoid using weak hash algorithms or low entropy sources for session tokens. Use crypto.randomBytes(16), HMAC, or SHA-256.",
|
|
17
|
+
recommended: false,
|
|
18
|
+
},
|
|
19
|
+
messages: {
|
|
20
|
+
weakHash:
|
|
21
|
+
"Avoid using weak hash algorithm '{{algo}}' for session tokens. Use SHA-256, HMAC, or AES instead.",
|
|
22
|
+
lowEntropy:
|
|
23
|
+
"Session token should be generated with at least 64-bit entropy. Avoid using '{{func}}'.",
|
|
24
|
+
smallRandomBytes:
|
|
25
|
+
"Session token generated with crypto.randomBytes({{bytes}}) has insufficient entropy. Use at least 8 bytes (64-bit).",
|
|
26
|
+
},
|
|
27
|
+
schema: [],
|
|
28
|
+
},
|
|
29
|
+
|
|
30
|
+
create(context) {
|
|
31
|
+
return {
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
const callee = node.callee;
|
|
34
|
+
|
|
35
|
+
// Case 1: crypto.createHash("md5") or ("sha1")
|
|
36
|
+
if (
|
|
37
|
+
callee.type === "MemberExpression" &&
|
|
38
|
+
callee.object.name === "crypto" &&
|
|
39
|
+
callee.property.name === "createHash"
|
|
40
|
+
) {
|
|
41
|
+
const [arg] = node.arguments;
|
|
42
|
+
if (
|
|
43
|
+
arg &&
|
|
44
|
+
arg.type === "Literal" &&
|
|
45
|
+
typeof arg.value === "string"
|
|
46
|
+
) {
|
|
47
|
+
const algo = arg.value.toLowerCase();
|
|
48
|
+
if (WEAK_HASH_ALGOS.has(algo)) {
|
|
49
|
+
context.report({
|
|
50
|
+
node: arg,
|
|
51
|
+
messageId: "weakHash",
|
|
52
|
+
data: { algo },
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Case 2: crypto.randomBytes(n), n < 8
|
|
59
|
+
if (
|
|
60
|
+
callee.type === "MemberExpression" &&
|
|
61
|
+
callee.object.name === "crypto" &&
|
|
62
|
+
callee.property.name === "randomBytes"
|
|
63
|
+
) {
|
|
64
|
+
const [arg] = node.arguments;
|
|
65
|
+
if (
|
|
66
|
+
arg &&
|
|
67
|
+
arg.type === "Literal" &&
|
|
68
|
+
typeof arg.value === "number" &&
|
|
69
|
+
arg.value < 8
|
|
70
|
+
) {
|
|
71
|
+
context.report({
|
|
72
|
+
node: arg,
|
|
73
|
+
messageId: "smallRandomBytes",
|
|
74
|
+
data: { bytes: arg.value },
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Case 3: Math.random() or Date.now() used
|
|
80
|
+
if (
|
|
81
|
+
callee.type === "MemberExpression" &&
|
|
82
|
+
((callee.object.name === "Math" && callee.property.name === "random") ||
|
|
83
|
+
(callee.object.name === "Date" && callee.property.name === "now"))
|
|
84
|
+
) {
|
|
85
|
+
context.report({
|
|
86
|
+
node: callee,
|
|
87
|
+
messageId: "lowEntropy",
|
|
88
|
+
data: { func: `${callee.object.name}.${callee.property.name}` },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
},
|
|
94
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: S052 - Verify that the initial authentication code is generated by a secure random number generator, containing at least 20 bits of entropy (typically a six-digit random number is sufficient).
|
|
3
|
+
* Rule ID: custom/s052
|
|
4
|
+
* Description: Ensure that the initial authentication code is generated using a secure random number generator with at least 20 bits of entropy.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Ensure secure random number generation with sufficient entropy",
|
|
14
|
+
category: "Security",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
insecureRandom: "Insecure random number generator detected.",
|
|
19
|
+
insufficientEntropy: "Insufficient entropy detected in crypto.randomInt. Ensure at least 20 bits of entropy.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
return {
|
|
25
|
+
CallExpression(node) {
|
|
26
|
+
// Check for insufficient entropy in crypto.randomInt
|
|
27
|
+
if (
|
|
28
|
+
node.callee.type === "MemberExpression" &&
|
|
29
|
+
node.callee.object.type === "Identifier" &&
|
|
30
|
+
node.callee.object.name === "crypto" &&
|
|
31
|
+
node.callee.property.type === "Identifier" &&
|
|
32
|
+
node.callee.property.name === "randomInt"
|
|
33
|
+
) {
|
|
34
|
+
const args = node.arguments;
|
|
35
|
+
if (args.length === 2 && args[0].type === "Literal" && args[1].type === "Literal") {
|
|
36
|
+
const min = args[0].value;
|
|
37
|
+
const max = args[1].value;
|
|
38
|
+
const range = max - min;
|
|
39
|
+
|
|
40
|
+
// Check if the range provides at least 20 bits of entropy
|
|
41
|
+
if (range < Math.pow(2, 20)) {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: "insufficientEntropy",
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Check for calls to Math.random()
|
|
51
|
+
if (
|
|
52
|
+
node.callee.type === "MemberExpression" &&
|
|
53
|
+
node.callee.object.type === "Identifier" &&
|
|
54
|
+
node.callee.object.name === "Math" &&
|
|
55
|
+
node.callee.property.type === "Identifier" &&
|
|
56
|
+
node.callee.property.name === "random"
|
|
57
|
+
) {
|
|
58
|
+
context.report({
|
|
59
|
+
node,
|
|
60
|
+
messageId: "insecureRandom",
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: S054 - Verify shared or default accounts are not present (e.g. "root", "admin", or "sa").
|
|
3
|
+
* Rule ID: custom/s054
|
|
4
|
+
* Description: Ensure that shared or default accounts like "root", "admin", or "sa" are not used in the codebase.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Disallow use of default accounts without value check",
|
|
14
|
+
category: "Best Practices",
|
|
15
|
+
recommended: false,
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
missingValidation: "Default account used without validation.",
|
|
19
|
+
},
|
|
20
|
+
schema: [],
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
const defaultAccounts = ["root", "admin", "sa"];
|
|
25
|
+
const sourceCode = context.getSourceCode();
|
|
26
|
+
|
|
27
|
+
function isAccountRelatedFunction(node) {
|
|
28
|
+
const name =
|
|
29
|
+
(node.type === "FunctionDeclaration" && node.id?.name) ||
|
|
30
|
+
(node.type === "MethodDefinition" && node.key?.name) ||
|
|
31
|
+
"";
|
|
32
|
+
return /account|user|login|access/i.test(name);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Check if validation exists in code like: if (x === "admin")
|
|
36
|
+
function hasValidation(node) {
|
|
37
|
+
let validated = false;
|
|
38
|
+
|
|
39
|
+
context.getSourceCode().getTokens(node).forEach((token, idx, tokens) => {
|
|
40
|
+
const prev = tokens[idx - 1]?.value || "";
|
|
41
|
+
const next = tokens[idx + 1]?.value || "";
|
|
42
|
+
|
|
43
|
+
if (
|
|
44
|
+
token.type === "String" &&
|
|
45
|
+
defaultAccounts.includes(token.value.replace(/['"]/g, "")) &&
|
|
46
|
+
(prev === "===" || prev === "!==" || next === "===" || next === "!==")
|
|
47
|
+
) {
|
|
48
|
+
validated = true;
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
return validated;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Check if any default account string is used (directly or via const)
|
|
56
|
+
function hasDefaultAccountUsage(node) {
|
|
57
|
+
const tokens = context.getSourceCode().getTokens(node);
|
|
58
|
+
return tokens.some((token) => {
|
|
59
|
+
return (
|
|
60
|
+
token.type === "String" &&
|
|
61
|
+
defaultAccounts.includes(token.value.replace(/['"]/g, ""))
|
|
62
|
+
);
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function hasDefaultConstantUsage(node) {
|
|
67
|
+
const scope = sourceCode.scopeManager.acquire(node);
|
|
68
|
+
if (!scope) return false;
|
|
69
|
+
|
|
70
|
+
const defaultVars = scope.variables.filter((v) => {
|
|
71
|
+
const def = v.defs[0];
|
|
72
|
+
return (
|
|
73
|
+
def &&
|
|
74
|
+
def.node.init &&
|
|
75
|
+
typeof def.node.init.value === "string" &&
|
|
76
|
+
defaultAccounts.includes(def.node.init.value)
|
|
77
|
+
);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
const fnText = sourceCode.getText(node);
|
|
81
|
+
return defaultVars.some((v) => fnText.includes(v.name));
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function checkFunction(node) {
|
|
85
|
+
const usesDefaultAccount =
|
|
86
|
+
hasDefaultAccountUsage(node) || hasDefaultConstantUsage(node);
|
|
87
|
+
|
|
88
|
+
if (usesDefaultAccount && !hasValidation(node)) {
|
|
89
|
+
context.report({
|
|
90
|
+
node,
|
|
91
|
+
messageId: "missingValidation",
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
FunctionDeclaration(node) {
|
|
98
|
+
if (isAccountRelatedFunction(node)) {
|
|
99
|
+
checkFunction(node);
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
MethodDefinition(node) {
|
|
103
|
+
if (isAccountRelatedFunction(node)) {
|
|
104
|
+
checkFunction(node);
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S055 – Verify that REST services explicitly check the incoming Content-Type to be the expected one, such as application/xml or application/json.
|
|
5
|
+
* OWASP ASVS 13.2.5
|
|
6
|
+
* Rule ID: custom/s055
|
|
7
|
+
* Verify that REST services explicitly check the incoming Content-Type to be the expected one, such as application/xml or application/json.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
module.exports = {
|
|
11
|
+
meta: {
|
|
12
|
+
type: "problem",
|
|
13
|
+
docs: {
|
|
14
|
+
description:
|
|
15
|
+
"Ensure REST request handlers validate Content-Type when using req.body",
|
|
16
|
+
recommended: false,
|
|
17
|
+
},
|
|
18
|
+
messages: {
|
|
19
|
+
missingCheck:
|
|
20
|
+
"Handler uses req.body but does not validate Content-Type.",
|
|
21
|
+
},
|
|
22
|
+
schema: [],
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
function walkForReqBody(node, reqName = "req") {
|
|
27
|
+
const visited = new Set();
|
|
28
|
+
let found = false;
|
|
29
|
+
|
|
30
|
+
function walk(n) {
|
|
31
|
+
if (!n || typeof n !== "object" || visited.has(n) || found) return;
|
|
32
|
+
visited.add(n);
|
|
33
|
+
|
|
34
|
+
if (
|
|
35
|
+
n.type === "MemberExpression" &&
|
|
36
|
+
n.object.type === "Identifier" &&
|
|
37
|
+
n.object.name === reqName &&
|
|
38
|
+
n.property.type === "Identifier" &&
|
|
39
|
+
n.property.name === "body"
|
|
40
|
+
) {
|
|
41
|
+
found = true;
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
for (const key in n) {
|
|
46
|
+
if (!Object.prototype.hasOwnProperty.call(n, key)) continue;
|
|
47
|
+
const child = n[key];
|
|
48
|
+
if (Array.isArray(child)) child.forEach(walk);
|
|
49
|
+
else if (typeof child === "object" && child !== null) walk(child);
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
walk(node);
|
|
54
|
+
return found;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function walkForContentTypeCheck(node, reqName = "req") {
|
|
58
|
+
const visited = new Set();
|
|
59
|
+
|
|
60
|
+
function walk(n) {
|
|
61
|
+
if (!n || typeof n !== "object" || visited.has(n)) return false;
|
|
62
|
+
visited.add(n);
|
|
63
|
+
|
|
64
|
+
// req.is("application/json")
|
|
65
|
+
if (
|
|
66
|
+
n.type === "CallExpression" &&
|
|
67
|
+
n.callee.type === "MemberExpression" &&
|
|
68
|
+
n.callee.object.type === "Identifier" &&
|
|
69
|
+
n.callee.object.name === reqName &&
|
|
70
|
+
n.callee.property.name === "is" &&
|
|
71
|
+
n.arguments.length > 0 &&
|
|
72
|
+
n.arguments[0].type === "Literal" &&
|
|
73
|
+
typeof n.arguments[0].value === "string" &&
|
|
74
|
+
n.arguments[0].value.startsWith("application/")
|
|
75
|
+
) {
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// req.headers["content-type"]
|
|
80
|
+
if (
|
|
81
|
+
n.type === "MemberExpression" &&
|
|
82
|
+
n.object?.type === "MemberExpression" &&
|
|
83
|
+
n.object.object?.type === "Identifier" &&
|
|
84
|
+
n.object.object.name === reqName &&
|
|
85
|
+
n.object.property?.name === "headers" &&
|
|
86
|
+
(
|
|
87
|
+
(n.property.type === "Literal" && n.property.value === "content-type") ||
|
|
88
|
+
(n.property.type === "Identifier" && n.property.name === "content-type")
|
|
89
|
+
)
|
|
90
|
+
) {
|
|
91
|
+
return true;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Look through children
|
|
95
|
+
for (const key in n) {
|
|
96
|
+
if (!Object.prototype.hasOwnProperty.call(n, key)) continue;
|
|
97
|
+
const child = n[key];
|
|
98
|
+
if (Array.isArray(child)) {
|
|
99
|
+
if (child.some(walk)) return true;
|
|
100
|
+
} else if (typeof child === "object" && child !== null) {
|
|
101
|
+
if (walk(child)) return true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return false;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return walk(node);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return {
|
|
112
|
+
CallExpression(node) {
|
|
113
|
+
// Match app.post("/path", handler)
|
|
114
|
+
if (
|
|
115
|
+
node.callee.type === "MemberExpression" &&
|
|
116
|
+
["post", "put", "patch", "delete"].includes(
|
|
117
|
+
node.callee.property.name
|
|
118
|
+
)
|
|
119
|
+
) {
|
|
120
|
+
const handler = node.arguments.find(
|
|
121
|
+
(arg) =>
|
|
122
|
+
arg.type === "FunctionExpression" ||
|
|
123
|
+
arg.type === "ArrowFunctionExpression"
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
if (!handler || !handler.body) return;
|
|
127
|
+
|
|
128
|
+
const reqName = handler.params[0]?.name || "req";
|
|
129
|
+
|
|
130
|
+
const usesBody = walkForReqBody(handler.body, reqName);
|
|
131
|
+
const hasValidation = walkForContentTypeCheck(handler.body, reqName);
|
|
132
|
+
|
|
133
|
+
if (usesBody && !hasValidation) {
|
|
134
|
+
context.report({
|
|
135
|
+
node: handler,
|
|
136
|
+
messageId: "missingCheck",
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
};
|
|
142
|
+
},
|
|
143
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule: S057 – Enforce UTC usage in time formatting/logging
|
|
3
|
+
* Rule ID: custom/s057
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "suggestion",
|
|
11
|
+
docs: {
|
|
12
|
+
description:
|
|
13
|
+
"Avoid using local time formatting in logs; prefer UTC for consistency",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
avoidLocalTime:
|
|
19
|
+
"Avoid using '{{method}}' for local time formatting. Prefer UTC methods like toISOString() or moment.utc().",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
const forbiddenMethods = [
|
|
25
|
+
"toLocaleString",
|
|
26
|
+
"toLocaleDateString",
|
|
27
|
+
"toLocaleTimeString",
|
|
28
|
+
"format", // e.g., moment().format(...) → not utc by default
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
return {
|
|
32
|
+
CallExpression(node) {
|
|
33
|
+
const callee = node.callee;
|
|
34
|
+
|
|
35
|
+
// Match: date.toLocaleString(), moment().format()
|
|
36
|
+
if (callee.type === "MemberExpression") {
|
|
37
|
+
const method = callee.property.name;
|
|
38
|
+
|
|
39
|
+
if (forbiddenMethods.includes(method)) {
|
|
40
|
+
const objectText = context.getSourceCode().getText(callee.object);
|
|
41
|
+
// Optional enhancement: only warn for Date or moment() objects
|
|
42
|
+
if (/Date|moment/.test(objectText)) {
|
|
43
|
+
context.report({
|
|
44
|
+
node,
|
|
45
|
+
messageId: "avoidLocalTime",
|
|
46
|
+
data: { method },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
},
|
|
52
|
+
};
|
|
53
|
+
},
|
|
54
|
+
};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule: S058 – Detect possible SSRF via unvalidated user-controlled URLs
|
|
3
|
+
* Rule ID: custom/s058
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
"use strict";
|
|
7
|
+
|
|
8
|
+
module.exports = {
|
|
9
|
+
meta: {
|
|
10
|
+
type: "problem",
|
|
11
|
+
docs: {
|
|
12
|
+
description:
|
|
13
|
+
"Detect SSRF vulnerabilities via unvalidated user-controlled URLs",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
ssrfRisk:
|
|
19
|
+
"Possible SSRF: URL '{{url}}' comes from untrusted source. Validate or sanitize input.",
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
|
|
23
|
+
create(context) {
|
|
24
|
+
const riskySources = [
|
|
25
|
+
"req.body",
|
|
26
|
+
"req.query",
|
|
27
|
+
"req.params",
|
|
28
|
+
"input",
|
|
29
|
+
"form",
|
|
30
|
+
];
|
|
31
|
+
const riskyFunctions = [
|
|
32
|
+
"fetch",
|
|
33
|
+
"axios.get",
|
|
34
|
+
"axios.post",
|
|
35
|
+
"axios.request",
|
|
36
|
+
"http.request",
|
|
37
|
+
"https.request",
|
|
38
|
+
];
|
|
39
|
+
|
|
40
|
+
function isUserInput(nodeText) {
|
|
41
|
+
return riskySources.some((src) => nodeText.includes(src));
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function isHttpFunction(callee) {
|
|
45
|
+
if (callee.type === "Identifier")
|
|
46
|
+
return riskyFunctions.includes(callee.name);
|
|
47
|
+
if (callee.type === "MemberExpression") {
|
|
48
|
+
const fullName = `${callee.object.name}.${callee.property.name}`;
|
|
49
|
+
return riskyFunctions.includes(fullName);
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
return {
|
|
55
|
+
CallExpression(node) {
|
|
56
|
+
const callee = node.callee;
|
|
57
|
+
if (!isHttpFunction(callee)) return;
|
|
58
|
+
|
|
59
|
+
const firstArg = node.arguments[0];
|
|
60
|
+
if (!firstArg) return;
|
|
61
|
+
|
|
62
|
+
const argText = context.getSourceCode().getText(firstArg);
|
|
63
|
+
if (isUserInput(argText)) {
|
|
64
|
+
context.report({
|
|
65
|
+
node: firstArg,
|
|
66
|
+
messageId: "ssrfRisk",
|
|
67
|
+
data: { url: argText },
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
};
|