@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,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,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
|
+
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
function doAdminTask() {}
|
|
2
|
+
function doAction() {}
|
|
3
|
+
function isAuthenticated(req: any): boolean {
|
|
4
|
+
return true;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
function doPost(request: any, response: any) { // ❌
|
|
8
|
+
const origin = request.getHeader("Origin");
|
|
9
|
+
|
|
10
|
+
if (origin === "https://admin.example.com") {
|
|
11
|
+
doAdminTask();
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function doPost_safe(request: any, response: any) { // ✅
|
|
16
|
+
const origin = request.getHeader("Origin");
|
|
17
|
+
console.log("Origin:", origin);
|
|
18
|
+
|
|
19
|
+
if (isAuthenticated(request)) {
|
|
20
|
+
doAction();
|
|
21
|
+
}
|
|
22
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "commonjs",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"allowJs": true,
|
|
7
|
+
"outDir": "./dist",
|
|
8
|
+
"rootDir": "./",
|
|
9
|
+
"strict": true,
|
|
10
|
+
"esModuleInterop": true,
|
|
11
|
+
"skipLibCheck": true,
|
|
12
|
+
"forceConsistentCasingInFileNames": true,
|
|
13
|
+
"declaration": true,
|
|
14
|
+
"declarationMap": true,
|
|
15
|
+
"sourceMap": true,
|
|
16
|
+
"resolveJsonModule": true,
|
|
17
|
+
"moduleResolution": "node"
|
|
18
|
+
},
|
|
19
|
+
"include": [
|
|
20
|
+
"**/*.ts",
|
|
21
|
+
"**/*.tsx",
|
|
22
|
+
"**/*.js",
|
|
23
|
+
"**/*.jsx"
|
|
24
|
+
],
|
|
25
|
+
"exclude": [
|
|
26
|
+
"node_modules",
|
|
27
|
+
"dist"
|
|
28
|
+
]
|
|
29
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
|
|
4
|
+
class AIAnalyzer {
|
|
5
|
+
constructor(config = {}) {
|
|
6
|
+
this.config = config;
|
|
7
|
+
this.apiKey = process.env.OPENAI_API_KEY || config.apiKey;
|
|
8
|
+
this.model = config.model || 'gpt-4o-mini';
|
|
9
|
+
this.provider = config.provider || 'openai';
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async analyzeWithAI(filePath, content, ruleConfig) {
|
|
13
|
+
if (!this.apiKey) {
|
|
14
|
+
console.warn('⚠️ AI API key not found, falling back to pattern analysis');
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
try {
|
|
19
|
+
const prompt = this.buildPrompt(content, ruleConfig);
|
|
20
|
+
const response = await this.callAI(prompt);
|
|
21
|
+
return this.parseAIResponse(response, filePath);
|
|
22
|
+
} catch (error) {
|
|
23
|
+
console.error('AI Analysis failed:', error.message);
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
buildPrompt(content, ruleConfig) {
|
|
29
|
+
return `You are a code quality expert analyzing code ONLY for logging best practices.
|
|
30
|
+
|
|
31
|
+
RULE: ${ruleConfig.ruleId} - ${ruleConfig.name}
|
|
32
|
+
DESCRIPTION: ${ruleConfig.description}
|
|
33
|
+
|
|
34
|
+
IMPORTANT: You are ONLY checking for LOG LEVEL VIOLATIONS. Do NOT analyze function naming, variable naming, or other code quality issues.
|
|
35
|
+
|
|
36
|
+
ANALYZE THIS CODE FOR LOG LEVEL VIOLATIONS ONLY:
|
|
37
|
+
\`\`\`
|
|
38
|
+
${content}
|
|
39
|
+
\`\`\`
|
|
40
|
+
|
|
41
|
+
SPECIFIC CRITERIA FOR LOG LEVELS ONLY:
|
|
42
|
+
1. Don't use console.error() or logger.error() for non-critical issues (should use console.warn() or console.log())
|
|
43
|
+
2. Avoid generic error messages without context
|
|
44
|
+
3. Error logs should be reserved for actual errors/exceptions that need immediate attention
|
|
45
|
+
4. Info/debug logs should use appropriate levels
|
|
46
|
+
5. Validation errors should typically use warn level, not error level
|
|
47
|
+
|
|
48
|
+
FOCUS ONLY ON:
|
|
49
|
+
- console.error() usage
|
|
50
|
+
- logger.error() usage
|
|
51
|
+
- this.logger.error() usage
|
|
52
|
+
- Other error-level logging calls
|
|
53
|
+
|
|
54
|
+
DO NOT CHECK:
|
|
55
|
+
- Function names
|
|
56
|
+
- Variable names
|
|
57
|
+
- Code structure
|
|
58
|
+
- Other code quality issues
|
|
59
|
+
|
|
60
|
+
RESPOND WITH JSON FORMAT:
|
|
61
|
+
{
|
|
62
|
+
"violations": [
|
|
63
|
+
{
|
|
64
|
+
"line": <line_number>,
|
|
65
|
+
"column": <column_number>,
|
|
66
|
+
"message": "<specific_logging_violation_description>",
|
|
67
|
+
"severity": "warning|error|info",
|
|
68
|
+
"code": "<code_snippet>",
|
|
69
|
+
"suggestion": "<how_to_fix_logging>"
|
|
70
|
+
}
|
|
71
|
+
],
|
|
72
|
+
"summary": "<overall_logging_assessment>"
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
Be precise about line numbers and provide actionable suggestions for LOG LEVEL issues only.`;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async callAI(prompt) {
|
|
79
|
+
if (this.provider === 'openai') {
|
|
80
|
+
return await this.callOpenAI(prompt);
|
|
81
|
+
} else if (this.provider === 'copilot') {
|
|
82
|
+
return await this.callCopilot(prompt);
|
|
83
|
+
}
|
|
84
|
+
throw new Error(`Unsupported AI provider: ${this.provider}`);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async callOpenAI(prompt) {
|
|
88
|
+
const fetch = (await import('node-fetch')).default;
|
|
89
|
+
|
|
90
|
+
const response = await fetch('https://api.openai.com/v1/chat/completions', {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
headers: {
|
|
93
|
+
'Content-Type': 'application/json',
|
|
94
|
+
'Authorization': `Bearer ${this.apiKey}`
|
|
95
|
+
},
|
|
96
|
+
body: JSON.stringify({
|
|
97
|
+
model: this.model,
|
|
98
|
+
messages: [
|
|
99
|
+
{
|
|
100
|
+
role: 'system',
|
|
101
|
+
content: 'You are an expert code analyzer specializing in logging best practices. Provide precise, actionable feedback.'
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
role: 'user',
|
|
105
|
+
content: prompt
|
|
106
|
+
}
|
|
107
|
+
],
|
|
108
|
+
temperature: 0.1,
|
|
109
|
+
max_tokens: 2000
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
if (!response.ok) {
|
|
114
|
+
throw new Error(`OpenAI API error: ${response.status} ${response.statusText}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const data = await response.json();
|
|
118
|
+
return data.choices[0].message.content;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async callCopilot(prompt) {
|
|
122
|
+
// TODO: Implement GitHub Copilot API integration
|
|
123
|
+
// This would require GitHub Copilot API access
|
|
124
|
+
throw new Error('GitHub Copilot API integration not yet implemented');
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
parseAIResponse(aiResponse, filePath) {
|
|
128
|
+
try {
|
|
129
|
+
// Extract JSON from AI response
|
|
130
|
+
const jsonMatch = aiResponse.match(/\{[\s\S]*\}/);
|
|
131
|
+
if (!jsonMatch) {
|
|
132
|
+
throw new Error('No JSON found in AI response');
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const parsed = JSON.parse(jsonMatch[0]);
|
|
136
|
+
|
|
137
|
+
return parsed.violations.map(violation => ({
|
|
138
|
+
line: violation.line,
|
|
139
|
+
column: violation.column || 1,
|
|
140
|
+
message: violation.message,
|
|
141
|
+
severity: violation.severity || 'warning',
|
|
142
|
+
ruleId: 'C019',
|
|
143
|
+
code: violation.code,
|
|
144
|
+
suggestion: violation.suggestion,
|
|
145
|
+
file: filePath,
|
|
146
|
+
source: 'ai'
|
|
147
|
+
}));
|
|
148
|
+
} catch (error) {
|
|
149
|
+
console.error('Failed to parse AI response:', error.message);
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async testConnection() {
|
|
155
|
+
if (!this.apiKey) {
|
|
156
|
+
return { success: false, error: 'API key not configured' };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const testPrompt = 'Respond with: {"status": "ok"}';
|
|
161
|
+
await this.callAI(testPrompt);
|
|
162
|
+
return { success: true, provider: this.provider, model: this.model };
|
|
163
|
+
} catch (error) {
|
|
164
|
+
return { success: false, error: error.message };
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
module.exports = AIAnalyzer;
|