@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,50 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S014 – Insecure TLS Version
|
|
5
|
+
* OWASP ASVS 9.1.3
|
|
6
|
+
* Ensure that only secure versions of TLS (1.2 and 1.3) are enabled.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Ensure that only secure versions of TLS are enabled. Do not use SSLv3, TLS 1.0, or TLS 1.1.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
insecureTLS:
|
|
20
|
+
"Insecure TLS/SSL version detected. Only TLS 1.2 or TLS 1.3 should be used.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
Property(node) {
|
|
27
|
+
// Detect insecure minVersion in HTTPS server options
|
|
28
|
+
if (
|
|
29
|
+
node.key &&
|
|
30
|
+
node.key.name === "minVersion" &&
|
|
31
|
+
node.value.type === "Literal" &&
|
|
32
|
+
typeof node.value.value === "string"
|
|
33
|
+
) {
|
|
34
|
+
const version = node.value.value.toLowerCase();
|
|
35
|
+
if (
|
|
36
|
+
version === "tlsv1" ||
|
|
37
|
+
version === "tlsv1.0" ||
|
|
38
|
+
version === "tlsv1.1" ||
|
|
39
|
+
version === "sslv3"
|
|
40
|
+
) {
|
|
41
|
+
context.report({
|
|
42
|
+
node: node.value,
|
|
43
|
+
messageId: "insecureTLS",
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
};
|
|
49
|
+
},
|
|
50
|
+
};
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S015 – Insecure TLS Certificate
|
|
5
|
+
* OWASP ASVS 9.2.1
|
|
6
|
+
* Ensure that only trusted TLS certificates are accepted.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Ensure only trusted TLS certificates are used. Reject self-signed, expired, or untrusted certificates unless explicitly allowed for internal use.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
untrustedCert:
|
|
20
|
+
"Untrusted/self-signed/expired certificate accepted. Only use trusted certificates in production.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
return {
|
|
26
|
+
Property(node) {
|
|
27
|
+
// Check if rejectUnauthorized: false is used (should not be used in production)
|
|
28
|
+
if (
|
|
29
|
+
node.key &&
|
|
30
|
+
(node.key.name === "rejectUnauthorized" ||
|
|
31
|
+
node.key.value === "rejectUnauthorized") &&
|
|
32
|
+
node.value.type === "Literal" &&
|
|
33
|
+
node.value.value === false
|
|
34
|
+
) {
|
|
35
|
+
context.report({
|
|
36
|
+
node: node.value,
|
|
37
|
+
messageId: "untrustedCert",
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
},
|
|
43
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint Rule: S016 - Sensitive Data in Query Parameters
|
|
3
|
+
* Rule ID: custom/s016
|
|
4
|
+
* Description: Sensitive data should not be passed via query parameters (e.g., @Query('password')).
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
const SENSITIVE_TERMS = [
|
|
10
|
+
"password",
|
|
11
|
+
"otp",
|
|
12
|
+
"token",
|
|
13
|
+
"creditCard",
|
|
14
|
+
"ssn",
|
|
15
|
+
"apiKey",
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
module.exports = {
|
|
19
|
+
meta: {
|
|
20
|
+
type: "problem",
|
|
21
|
+
docs: {
|
|
22
|
+
description: "Detect sensitive parameters passed via query string.",
|
|
23
|
+
},
|
|
24
|
+
messages: {
|
|
25
|
+
sensitiveParam:
|
|
26
|
+
"Avoid passing sensitive data via query parameters: '{{param}}'",
|
|
27
|
+
},
|
|
28
|
+
schema: [],
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
create(context) {
|
|
32
|
+
return {
|
|
33
|
+
Decorator(node) {
|
|
34
|
+
if (
|
|
35
|
+
node.expression &&
|
|
36
|
+
node.expression.type === "CallExpression" &&
|
|
37
|
+
node.expression.callee &&
|
|
38
|
+
node.expression.callee.type === "Identifier" &&
|
|
39
|
+
node.expression.callee.name === "Query"
|
|
40
|
+
) {
|
|
41
|
+
const arg = node.expression.arguments[0];
|
|
42
|
+
if (arg && arg.type === "Literal" && typeof arg.value === "string") {
|
|
43
|
+
const paramName = arg.value.toLowerCase();
|
|
44
|
+
for (const sensitive of SENSITIVE_TERMS) {
|
|
45
|
+
if (paramName.includes(sensitive.toLowerCase())) {
|
|
46
|
+
context.report({
|
|
47
|
+
node: arg,
|
|
48
|
+
messageId: "sensitiveParam",
|
|
49
|
+
data: { param: arg.value },
|
|
50
|
+
});
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
};
|
|
58
|
+
},
|
|
59
|
+
};
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
"Verify that data selection or database queries use parameterized queries, ORMs, entity frameworks, or are otherwise protected from database injection attacks",
|
|
9
|
+
recommended: true,
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
sqlInjection: "Potential SQL injection vulnerability detected. Use parameterized queries instead of string concatenation in '{{method}}'.",
|
|
14
|
+
unsafeQuery: "Unsafe database query detected in '{{method}}'. Consider using ORM or parameterized queries.",
|
|
15
|
+
stringConcatenation: "Avoid string concatenation in SQL queries. Use parameterized queries in '{{method}}'.",
|
|
16
|
+
templateLiteralQuery: "Template literals in SQL queries can be vulnerable to injection. Use parameterized queries in '{{method}}'.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
// Database methods that should use parameterized queries
|
|
22
|
+
const databaseMethods = [
|
|
23
|
+
// MySQL
|
|
24
|
+
"query", "execute", "prepare",
|
|
25
|
+
// PostgreSQL
|
|
26
|
+
"query", "execute",
|
|
27
|
+
// MongoDB
|
|
28
|
+
"find", "findOne", "aggregate", "updateOne", "updateMany", "deleteOne", "deleteMany",
|
|
29
|
+
// SQLite
|
|
30
|
+
"run", "get", "all", "each", "prepare",
|
|
31
|
+
// General ORM methods
|
|
32
|
+
"where", "whereRaw", "raw", "knex",
|
|
33
|
+
// Sequelize
|
|
34
|
+
"findAll", "findOne", "create", "update", "destroy",
|
|
35
|
+
// TypeORM
|
|
36
|
+
"createQueryBuilder", "query", "manager",
|
|
37
|
+
// Prisma
|
|
38
|
+
"findMany", "findUnique", "create", "update", "delete",
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
// Dangerous string methods that suggest concatenation
|
|
42
|
+
const dangerousStringMethods = [
|
|
43
|
+
"concat", "replace", "replaceAll", "substring", "slice"
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// SQL keywords that when used with string operations are suspicious
|
|
47
|
+
const sqlKeywords = [
|
|
48
|
+
"SELECT", "INSERT", "UPDATE", "DELETE", "DROP", "CREATE", "ALTER",
|
|
49
|
+
"WHERE", "FROM", "JOIN", "UNION", "ORDER", "GROUP", "HAVING",
|
|
50
|
+
"select", "insert", "update", "delete", "drop", "create", "alter",
|
|
51
|
+
"where", "from", "join", "union", "order", "group", "having"
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
function containsSqlKeywords(str) {
|
|
55
|
+
return sqlKeywords.some(keyword => str.includes(keyword));
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function checkStringConcatenation(node) {
|
|
59
|
+
if (node.type === "BinaryExpression" && node.operator === "+") {
|
|
60
|
+
const leftStr = getStringValue(node.left);
|
|
61
|
+
const rightStr = getStringValue(node.right);
|
|
62
|
+
|
|
63
|
+
if ((leftStr && containsSqlKeywords(leftStr)) ||
|
|
64
|
+
(rightStr && containsSqlKeywords(rightStr))) {
|
|
65
|
+
return true;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function getStringValue(node) {
|
|
72
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
73
|
+
return node.value;
|
|
74
|
+
}
|
|
75
|
+
if (node.type === "TemplateLiteral") {
|
|
76
|
+
return node.quasis.map(q => q.value.raw).join("");
|
|
77
|
+
}
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function checkTemplateLiteral(node) {
|
|
82
|
+
if (node.type === "TemplateLiteral") {
|
|
83
|
+
const templateString = node.quasis.map(q => q.value.raw).join("");
|
|
84
|
+
return containsSqlKeywords(templateString) && node.expressions.length > 0;
|
|
85
|
+
}
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function isDatabaseCall(node) {
|
|
90
|
+
if (node.type === "CallExpression") {
|
|
91
|
+
// Check for method calls like db.query(), connection.execute()
|
|
92
|
+
if (node.callee.type === "MemberExpression") {
|
|
93
|
+
const methodName = node.callee.property.name;
|
|
94
|
+
return databaseMethods.includes(methodName);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// Check for direct function calls like query()
|
|
98
|
+
if (node.callee.type === "Identifier") {
|
|
99
|
+
return databaseMethods.includes(node.callee.name);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
return false;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function getMethodName(node) {
|
|
106
|
+
if (node.callee.type === "MemberExpression") {
|
|
107
|
+
return node.callee.property.name;
|
|
108
|
+
}
|
|
109
|
+
if (node.callee.type === "Identifier") {
|
|
110
|
+
return node.callee.name;
|
|
111
|
+
}
|
|
112
|
+
return "unknown";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function checkCallExpression(node) {
|
|
116
|
+
if (!isDatabaseCall(node)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const methodName = getMethodName(node);
|
|
121
|
+
|
|
122
|
+
// Check each argument
|
|
123
|
+
for (const arg of node.arguments) {
|
|
124
|
+
// Check for string concatenation
|
|
125
|
+
if (checkStringConcatenation(arg)) {
|
|
126
|
+
context.report({
|
|
127
|
+
node: arg,
|
|
128
|
+
messageId: "stringConcatenation",
|
|
129
|
+
data: { method: methodName },
|
|
130
|
+
});
|
|
131
|
+
continue;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// Check for template literals with expressions
|
|
135
|
+
if (checkTemplateLiteral(arg)) {
|
|
136
|
+
context.report({
|
|
137
|
+
node: arg,
|
|
138
|
+
messageId: "templateLiteralQuery",
|
|
139
|
+
data: { method: methodName },
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Check for potentially unsafe patterns
|
|
145
|
+
if (arg.type === "Identifier" || arg.type === "MemberExpression") {
|
|
146
|
+
// This is a variable or property access - could be unsafe
|
|
147
|
+
// We'll be more lenient here and only warn if it's clearly a string concatenation
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return {
|
|
154
|
+
CallExpression: checkCallExpression,
|
|
155
|
+
|
|
156
|
+
// Also check assignment expressions that might build SQL strings
|
|
157
|
+
AssignmentExpression(node) {
|
|
158
|
+
if (node.operator === "=" || node.operator === "+=") {
|
|
159
|
+
if (checkStringConcatenation(node.right)) {
|
|
160
|
+
const leftStr = getStringValue(node.right.left);
|
|
161
|
+
const rightStr = getStringValue(node.right.right);
|
|
162
|
+
|
|
163
|
+
if ((leftStr && containsSqlKeywords(leftStr)) ||
|
|
164
|
+
(rightStr && containsSqlKeywords(rightStr))) {
|
|
165
|
+
context.report({
|
|
166
|
+
node: node.right,
|
|
167
|
+
messageId: "sqlInjection",
|
|
168
|
+
data: { method: "string assignment" },
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
},
|
|
174
|
+
|
|
175
|
+
// Check variable declarations
|
|
176
|
+
VariableDeclarator(node) {
|
|
177
|
+
if (node.init && checkStringConcatenation(node.init)) {
|
|
178
|
+
const leftStr = getStringValue(node.init.left);
|
|
179
|
+
const rightStr = getStringValue(node.init.right);
|
|
180
|
+
|
|
181
|
+
if ((leftStr && containsSqlKeywords(leftStr)) ||
|
|
182
|
+
(rightStr && containsSqlKeywords(rightStr))) {
|
|
183
|
+
context.report({
|
|
184
|
+
node: node.init,
|
|
185
|
+
messageId: "sqlInjection",
|
|
186
|
+
data: { method: "variable declaration" },
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
};
|
|
192
|
+
},
|
|
193
|
+
};
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S018 – Positive Input Validation
|
|
5
|
+
* OWASP ASVS 5.1.3
|
|
6
|
+
* Ensure that all input is validated using positive validation (allow lists).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Ensure input validation uses allow lists (whitelisting), not deny lists (blacklisting) wherever possible.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
denyList:
|
|
20
|
+
"Do not use deny lists (blacklisting) for input validation. Use allow lists (whitelisting) instead.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
// Detect use of common denylist patterns
|
|
26
|
+
function isDenyListCheck(node) {
|
|
27
|
+
// Example: if (input.includes('badword')) { ... }
|
|
28
|
+
return (
|
|
29
|
+
node &&
|
|
30
|
+
node.type === "CallExpression" &&
|
|
31
|
+
node.callee.type === "MemberExpression" &&
|
|
32
|
+
(node.callee.property.name === "includes" ||
|
|
33
|
+
node.callee.property.name === "indexOf") &&
|
|
34
|
+
node.arguments.length &&
|
|
35
|
+
node.arguments[0].type === "Literal" &&
|
|
36
|
+
typeof node.arguments[0].value === "string"
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
IfStatement(node) {
|
|
42
|
+
if (
|
|
43
|
+
node.test &&
|
|
44
|
+
((node.test.type === "UnaryExpression" &&
|
|
45
|
+
isDenyListCheck(node.test.argument)) ||
|
|
46
|
+
(node.test.type === "CallExpression" && isDenyListCheck(node.test)))
|
|
47
|
+
) {
|
|
48
|
+
context.report({
|
|
49
|
+
node: node.test,
|
|
50
|
+
messageId: "denyList",
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
},
|
|
56
|
+
};
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: S019 – Sanitize input before using in email systems
|
|
3
|
+
* Rule ID: custom/s019
|
|
4
|
+
* Purpose: Prevent SMTP/IMAP injection via unvalidated user input
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Ensure user input is sanitized before use in mail functions",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
unsanitizedEmailInput:
|
|
20
|
+
"Unsanitized user input '{{input}}' should not be passed directly to mail systems.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
const mailFunctionNames = ["sendMail", "sendEmail", "mailer.send"];
|
|
26
|
+
const sanitizeFunctions = [
|
|
27
|
+
"sanitize",
|
|
28
|
+
"escape",
|
|
29
|
+
"validateEmail",
|
|
30
|
+
"cleanInput",
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
// Identify common patterns of user input
|
|
34
|
+
function isUserInput(name) {
|
|
35
|
+
return (
|
|
36
|
+
name.includes("req.body") ||
|
|
37
|
+
name.includes("input") ||
|
|
38
|
+
name.includes("form")
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Check if a value has been sanitized
|
|
43
|
+
function isSanitized(node) {
|
|
44
|
+
return (
|
|
45
|
+
node.type === "CallExpression" &&
|
|
46
|
+
node.callee &&
|
|
47
|
+
sanitizeFunctions.includes(node.callee.name)
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return {
|
|
52
|
+
CallExpression(node) {
|
|
53
|
+
const calleeName =
|
|
54
|
+
(node.callee.type === "MemberExpression" &&
|
|
55
|
+
node.callee.property.name) ||
|
|
56
|
+
node.callee.name;
|
|
57
|
+
|
|
58
|
+
// Only analyze known mail function calls
|
|
59
|
+
if (!mailFunctionNames.includes(calleeName)) return;
|
|
60
|
+
|
|
61
|
+
for (const arg of node.arguments) {
|
|
62
|
+
// Direct use of user input in function arguments
|
|
63
|
+
if (arg.type === "MemberExpression") {
|
|
64
|
+
const fullName = context.getSourceCode().getText(arg);
|
|
65
|
+
if (isUserInput(fullName)) {
|
|
66
|
+
context.report({
|
|
67
|
+
node: arg,
|
|
68
|
+
messageId: "unsanitizedEmailInput",
|
|
69
|
+
data: { input: fullName },
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// If email parameters are passed in an object
|
|
75
|
+
if (arg.type === "ObjectExpression") {
|
|
76
|
+
for (const prop of arg.properties) {
|
|
77
|
+
const value = prop.value;
|
|
78
|
+
|
|
79
|
+
// User input used directly
|
|
80
|
+
if (
|
|
81
|
+
value.type === "MemberExpression" &&
|
|
82
|
+
isUserInput(context.getSourceCode().getText(value))
|
|
83
|
+
) {
|
|
84
|
+
context.report({
|
|
85
|
+
node: value,
|
|
86
|
+
messageId: "unsanitizedEmailInput",
|
|
87
|
+
data: { input: context.getSourceCode().getText(value) },
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// User input passed through function without sanitization
|
|
92
|
+
if (
|
|
93
|
+
value.type === "CallExpression" &&
|
|
94
|
+
!isSanitized(value) &&
|
|
95
|
+
value.arguments.some(
|
|
96
|
+
(a) =>
|
|
97
|
+
a.type === "MemberExpression" &&
|
|
98
|
+
isUserInput(context.getSourceCode().getText(a))
|
|
99
|
+
)
|
|
100
|
+
) {
|
|
101
|
+
context.report({
|
|
102
|
+
node: value,
|
|
103
|
+
messageId: "unsanitizedEmailInput",
|
|
104
|
+
data: { input: context.getSourceCode().getText(value) },
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
},
|
|
113
|
+
};
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule S020: Avoid eval() and dynamic code execution
|
|
3
|
+
* Prevents Remote Code Execution vulnerabilities
|
|
4
|
+
* OWASP ASVS 5.2.4 compliance
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
const ZERO_INDEX = 0;
|
|
8
|
+
|
|
9
|
+
const s020Rule = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Avoid eval() and dynamic code execution to prevent RCE vulnerabilities",
|
|
14
|
+
category: "Security",
|
|
15
|
+
recommended: "error"
|
|
16
|
+
},
|
|
17
|
+
messages: {
|
|
18
|
+
avoidEval: "Avoid eval() - use JSON.parse() or safer alternatives to prevent RCE",
|
|
19
|
+
avoidFunction: "Avoid Function constructor - use regular functions to prevent code injection",
|
|
20
|
+
avoidStringTimeout: "Avoid string arguments in setTimeout/setInterval - use function references",
|
|
21
|
+
avoidDynamicExecution: "Avoid dynamic code execution - sanitize and validate all inputs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
create(context) {
|
|
26
|
+
return {
|
|
27
|
+
CallExpression(node) {
|
|
28
|
+
// Check for eval() usage
|
|
29
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'eval') {
|
|
30
|
+
context.report({
|
|
31
|
+
node,
|
|
32
|
+
messageId: 'avoidEval'
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
// Check for setTimeout/setInterval with string arguments
|
|
37
|
+
if (node.callee.type === 'Identifier' &&
|
|
38
|
+
(node.callee.name === 'setTimeout' || node.callee.name === 'setInterval')) {
|
|
39
|
+
if (node.arguments.length > ZERO_INDEX &&
|
|
40
|
+
node.arguments[ZERO_INDEX].type === 'Literal' &&
|
|
41
|
+
typeof node.arguments[ZERO_INDEX].value === 'string') {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: 'avoidStringTimeout'
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Check for dynamic script execution methods
|
|
50
|
+
if (node.callee.type === 'MemberExpression') {
|
|
51
|
+
const methodName = node.callee.property.name;
|
|
52
|
+
if (methodName === 'executeScript' || methodName === 'executeJavaScript') {
|
|
53
|
+
context.report({
|
|
54
|
+
node,
|
|
55
|
+
messageId: 'avoidDynamicExecution'
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
NewExpression(node) {
|
|
62
|
+
// Check for new Function() constructor
|
|
63
|
+
if (node.callee.type === 'Identifier' && node.callee.name === 'Function') {
|
|
64
|
+
context.report({
|
|
65
|
+
node,
|
|
66
|
+
messageId: 'avoidFunction'
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
MemberExpression(node) {
|
|
72
|
+
// Check for indirect eval access via window/global
|
|
73
|
+
if (node.property.type === 'Identifier' && node.property.name === 'eval') {
|
|
74
|
+
if (node.object.type === 'Identifier') {
|
|
75
|
+
const objectName = node.object.name;
|
|
76
|
+
if (objectName === 'window' || objectName === 'global' || objectName === 'globalThis') {
|
|
77
|
+
context.report({
|
|
78
|
+
node,
|
|
79
|
+
messageId: 'avoidEval'
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
module.exports = s020Rule;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description: "Ensure HTML output is properly encoded to prevent XSS.",
|
|
8
|
+
recommended: true,
|
|
9
|
+
},
|
|
10
|
+
schema: [],
|
|
11
|
+
messages: {
|
|
12
|
+
unsafeOutput:
|
|
13
|
+
"Output to HTML must use proper encoding (e.g., escapeHtml) to prevent XSS.",
|
|
14
|
+
},
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
create(context) {
|
|
18
|
+
const outputMethods = new Set(["write", "print", "println", "log"]);
|
|
19
|
+
const outputObjects = new Set(["out", "writer", "response", "console", "System"]);
|
|
20
|
+
const riskyVarNames = ["input", "user", "html", "token", "param", "data"];
|
|
21
|
+
|
|
22
|
+
function isEscapeFunction(node) {
|
|
23
|
+
return (
|
|
24
|
+
node.type === "CallExpression" &&
|
|
25
|
+
node.callee.type === "Identifier" &&
|
|
26
|
+
(node.callee.name.includes("escape") || node.callee.name.includes("encode"))
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function isUnescapedArg(node) {
|
|
31
|
+
if (!node) return false;
|
|
32
|
+
|
|
33
|
+
if (node.type === "Literal") return false;
|
|
34
|
+
if (isEscapeFunction(node)) return false;
|
|
35
|
+
|
|
36
|
+
if (node.type === "BinaryExpression") {
|
|
37
|
+
return isUnescapedArg(node.left) || isUnescapedArg(node.right);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (node.type === "Identifier") {
|
|
41
|
+
const varName = node.name.toLowerCase();
|
|
42
|
+
return riskyVarNames.some((risky) => varName.includes(risky));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
CallExpression(node) {
|
|
50
|
+
const callee = node.callee;
|
|
51
|
+
|
|
52
|
+
if (
|
|
53
|
+
callee.type === "MemberExpression" &&
|
|
54
|
+
outputMethods.has(callee.property.name)
|
|
55
|
+
) {
|
|
56
|
+
const object = callee.object;
|
|
57
|
+
|
|
58
|
+
const isLikelyHtmlOutput =
|
|
59
|
+
(object.type === "Identifier" && outputObjects.has(object.name)) ||
|
|
60
|
+
(object.type === "MemberExpression" &&
|
|
61
|
+
object.property?.name === "getWriter");
|
|
62
|
+
|
|
63
|
+
if (!isLikelyHtmlOutput) return;
|
|
64
|
+
|
|
65
|
+
for (const arg of node.arguments) {
|
|
66
|
+
if (isUnescapedArg(arg)) {
|
|
67
|
+
context.report({
|
|
68
|
+
node: arg,
|
|
69
|
+
messageId: "unsafeOutput",
|
|
70
|
+
});
|
|
71
|
+
break;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
},
|
|
78
|
+
};
|