@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,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "sunlint-eslint-typescript",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "ESLint integration for SunLint TypeScript analysis",
|
|
5
|
+
"dependencies": {
|
|
6
|
+
"eslint": "^9.16.0",
|
|
7
|
+
"@typescript-eslint/parser": "^8.15.0",
|
|
8
|
+
"@typescript-eslint/eslint-plugin": "^8.15.0"
|
|
9
|
+
},
|
|
10
|
+
"devDependencies": {
|
|
11
|
+
"typescript": "^5.7.2"
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint plugin: custom rules index
|
|
3
|
+
* Export toàn bộ rule tự định nghĩa dùng cho plugin "custom"
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
const s005 = require("./s005-no-origin-auth.js");
|
|
7
|
+
const s006 = require("./s006-activation-recovery-secret-not-plaintext.js");
|
|
8
|
+
const s008 = require("./s008-crypto-agility.js");
|
|
9
|
+
const s009 = require("./s009-no-insecure-crypto.js");
|
|
10
|
+
const s010 = require("./s010-no-insecure-random-in-sensitive-context.js");
|
|
11
|
+
const s011 = require("./s011-no-insecure-uuid.js");
|
|
12
|
+
const s012 = require("./s012-hardcode-secret.js");
|
|
13
|
+
const s014 = require("./s014-insecure-tls-version.js");
|
|
14
|
+
const s015 = require("./s015-insecure-tls-certificate.js");
|
|
15
|
+
const s016 = require("./s016-sensitive-query-parameter.js");
|
|
16
|
+
const s017 = require("./s017-no-sql-injection.js");
|
|
17
|
+
const s018 = require("./s018-positive-input-validation.js");
|
|
18
|
+
const s019 = require("./s019-no-raw-user-input-in-email.js");
|
|
19
|
+
const s020 = require("./s020-no-eval-dynamic-execution.js");
|
|
20
|
+
const s022 = require("./s022-output-encoding.js");
|
|
21
|
+
const s023 = require("./s023-no-json-injection.js");
|
|
22
|
+
const s025 = require("./s025-server-side-input-validation.js");
|
|
23
|
+
const s026 = require("./s026-json-schema-validation.js");
|
|
24
|
+
const s027 = require("./s027-no-hardcoded-secrets.js");
|
|
25
|
+
const s029 = require("./s029-require-csrf-protection.js");
|
|
26
|
+
const s030 = require("./s030-no-directory-browsing.js");
|
|
27
|
+
const s033 = require("./s033-require-samesite-cookie.js");
|
|
28
|
+
const s034 = require("./s034-require-host-cookie-prefix.js");
|
|
29
|
+
const s035 = require("./s035-cookie-specific-path.js");
|
|
30
|
+
const s036 = require("./s036-no-unsafe-file-include.js");
|
|
31
|
+
const s037 = require("./s037-require-anti-cache-headers.js");
|
|
32
|
+
const s038 = require("./s038-no-version-disclosure.js");
|
|
33
|
+
const s039 = require("./s039-no-session-token-in-url.js");
|
|
34
|
+
const s041 = require("./s041-require-session-invalidate-on-logout.js");
|
|
35
|
+
const s042 = require("./s042-require-periodic-reauthentication.js");
|
|
36
|
+
const s043 = require("./s043-terminate-sessions-on-password-change.js");
|
|
37
|
+
const s044 = require("./s044-require-full-session-for-sensitive-operations.js");
|
|
38
|
+
const s045 = require("./s045-anti-automation-controls.js");
|
|
39
|
+
const s046 = require("./s046-secure-notification-on-auth-change.js");
|
|
40
|
+
const s048 = require("./s048-password-credential-recovery.js");
|
|
41
|
+
const s050 = require("./s050-session-token-weak-hash.js");
|
|
42
|
+
const s052 = require("./s052-secure-random-authentication-code.js");
|
|
43
|
+
const s054 = require("./s054-verification-default-account.js");
|
|
44
|
+
const s057 = require("./s057-utc-logging.js");
|
|
45
|
+
const s058 = require("./s058-no-ssrf.js");
|
|
46
|
+
|
|
47
|
+
module.exports = {
|
|
48
|
+
rules: {
|
|
49
|
+
typescript_s005: s005,
|
|
50
|
+
typescript_s006: s006,
|
|
51
|
+
typescript_s008: s008,
|
|
52
|
+
typescript_s009: s009,
|
|
53
|
+
typescript_s010: s010,
|
|
54
|
+
typescript_s011: s011,
|
|
55
|
+
typescript_s012: s012,
|
|
56
|
+
typescript_s014: s014,
|
|
57
|
+
typescript_s015: s015,
|
|
58
|
+
typescript_s016: s016,
|
|
59
|
+
typescript_s017: s017,
|
|
60
|
+
typescript_s018: s018,
|
|
61
|
+
typescript_s019: s019,
|
|
62
|
+
typescript_s020: s020,
|
|
63
|
+
typescript_s022: s022,
|
|
64
|
+
typescript_s023: s023,
|
|
65
|
+
typescript_s025: s025,
|
|
66
|
+
typescript_s026: s026,
|
|
67
|
+
typescript_s027: s027,
|
|
68
|
+
typescript_s029: s029,
|
|
69
|
+
typescript_s030: s030,
|
|
70
|
+
typescript_s033: s033,
|
|
71
|
+
typescript_s034: s034,
|
|
72
|
+
typescript_s035: s035,
|
|
73
|
+
typescript_s036: s036,
|
|
74
|
+
typescript_s037: s037,
|
|
75
|
+
typescript_s038: s038,
|
|
76
|
+
typescript_s039: s039,
|
|
77
|
+
typescript_s041: s041,
|
|
78
|
+
typescript_s042: s042,
|
|
79
|
+
typescript_s043: s043,
|
|
80
|
+
typescript_s044: s044,
|
|
81
|
+
typescript_s045: s045,
|
|
82
|
+
typescript_s046: s046,
|
|
83
|
+
typescript_s048: s048,
|
|
84
|
+
typescript_s050: s050,
|
|
85
|
+
typescript_s052: s052,
|
|
86
|
+
typescript_s054: s054,
|
|
87
|
+
typescript_s057: s057,
|
|
88
|
+
typescript_s058: s058,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ESLint rule: S005 – Do not use Origin header for authentication/access control
|
|
3
|
+
* Rule ID: custom/s005
|
|
4
|
+
* Description: Prevent usage of request.getHeader("Origin") for security decisions.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Do not use the Origin header to make authentication or access control decisions.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
noOriginCheck:
|
|
20
|
+
"Do not use the Origin header for authentication or access control decisions. It can be easily spoofed.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
const trackedVariables = new Set();
|
|
26
|
+
|
|
27
|
+
function isOriginHeaderCall(node) {
|
|
28
|
+
// Match: request.getHeader("Origin")
|
|
29
|
+
return (
|
|
30
|
+
node.type === "CallExpression" &&
|
|
31
|
+
node.callee.type === "MemberExpression" &&
|
|
32
|
+
node.callee.property.name === "getHeader" &&
|
|
33
|
+
node.arguments.length &&
|
|
34
|
+
node.arguments[0].type === "Literal" &&
|
|
35
|
+
node.arguments[0].value === "Origin"
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
VariableDeclarator(node) {
|
|
41
|
+
// Track: const origin = request.getHeader("Origin");
|
|
42
|
+
if (node.init && isOriginHeaderCall(node.init)) {
|
|
43
|
+
trackedVariables.add(node.id.name);
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
|
|
47
|
+
AssignmentExpression(node) {
|
|
48
|
+
// Track: origin = request.getHeader("Origin");
|
|
49
|
+
if (
|
|
50
|
+
node.left.type === "Identifier" &&
|
|
51
|
+
isOriginHeaderCall(node.right)
|
|
52
|
+
) {
|
|
53
|
+
trackedVariables.add(node.left.name);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
|
|
57
|
+
CallExpression(node) {
|
|
58
|
+
// Detect: origin.equals(...) or origin.includes(...) or "xyz" === origin
|
|
59
|
+
if (
|
|
60
|
+
node.callee.type === "MemberExpression" &&
|
|
61
|
+
["includes", "startsWith", "endsWith", "indexOf", "equals"].includes(
|
|
62
|
+
node.callee.property.name
|
|
63
|
+
)
|
|
64
|
+
) {
|
|
65
|
+
const arg = node.callee.object;
|
|
66
|
+
if (arg.type === "Identifier" && trackedVariables.has(arg.name)) {
|
|
67
|
+
context.report({ node, messageId: "noOriginCheck" });
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Detect: "admin" === origin (binary expression inside if)
|
|
72
|
+
if (
|
|
73
|
+
node.parent &&
|
|
74
|
+
node.parent.type === "IfStatement" &&
|
|
75
|
+
node.arguments.some(
|
|
76
|
+
(arg) =>
|
|
77
|
+
arg.type === "Identifier" && trackedVariables.has(arg.name)
|
|
78
|
+
)
|
|
79
|
+
) {
|
|
80
|
+
context.report({ node, messageId: "noOriginCheck" });
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
BinaryExpression(node) {
|
|
85
|
+
// Detect: if (origin === "some-origin")
|
|
86
|
+
const ids = [node.left, node.right].filter(
|
|
87
|
+
(e) => e.type === "Identifier" && trackedVariables.has(e.name)
|
|
88
|
+
);
|
|
89
|
+
if (ids.length > 0) {
|
|
90
|
+
context.report({ node, messageId: "noOriginCheck" });
|
|
91
|
+
}
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S006 – Activation/Recovery Secret Not Plaintext
|
|
5
|
+
* OWASP ASVS 2.5.1
|
|
6
|
+
* Ensure that system-generated activation or recovery secrets are not sent in plaintext to the user.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Disallow sending activation or recovery secrets in plaintext to the user (e.g., via email/SMS). Use secure channels or one-time tokens.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
plaintextSecret:
|
|
20
|
+
"Do not send activation or recovery secrets in plaintext. Use a hashed or one-time token instead.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
function isActivationOrRecovery(text) {
|
|
26
|
+
if (!text || typeof text !== "string") return false;
|
|
27
|
+
const keywords = [
|
|
28
|
+
"activation code",
|
|
29
|
+
"activation token",
|
|
30
|
+
"recovery code",
|
|
31
|
+
"recovery token",
|
|
32
|
+
];
|
|
33
|
+
return keywords.some((kw) => text.toLowerCase().includes(kw));
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
return {
|
|
37
|
+
CallExpression(node) {
|
|
38
|
+
// Check for sendMail({ text: ... }) or sendSMS(...)
|
|
39
|
+
if (
|
|
40
|
+
node.callee.type === "Identifier" &&
|
|
41
|
+
(node.callee.name === "sendMail" || node.callee.name === "sendSMS") &&
|
|
42
|
+
node.arguments.length
|
|
43
|
+
) {
|
|
44
|
+
const arg = node.arguments[0];
|
|
45
|
+
if (arg.type === "ObjectExpression") {
|
|
46
|
+
const bodyProp = arg.properties.find(
|
|
47
|
+
(prop) =>
|
|
48
|
+
prop.key &&
|
|
49
|
+
(prop.key.name === "text" || prop.key.name === "html") &&
|
|
50
|
+
prop.value.type === "Literal"
|
|
51
|
+
);
|
|
52
|
+
if (bodyProp && isActivationOrRecovery(bodyProp.value.value)) {
|
|
53
|
+
context.report({
|
|
54
|
+
node: bodyProp.value,
|
|
55
|
+
messageId: "plaintextSecret",
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (arg.type === "Literal" && isActivationOrRecovery(arg.value)) {
|
|
60
|
+
context.report({
|
|
61
|
+
node: arg,
|
|
62
|
+
messageId: "plaintextSecret",
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S008 – Crypto Agility
|
|
5
|
+
* OWASP ASVS 6.2.4
|
|
6
|
+
* Ensure that cryptographic algorithms and their parameters can be reconfigured or upgraded.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Ensure cryptographic algorithms, key lengths, and modes are not hardcoded and can be configured or upgraded easily.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
hardcodedAlgo:
|
|
20
|
+
"Do not hardcode cryptographic algorithm or parameters. Use configurable values instead.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
function isHardcodedCrypto(node) {
|
|
26
|
+
if (node.type === "Literal" && typeof node.value === "string") {
|
|
27
|
+
const lower = node.value.toLowerCase();
|
|
28
|
+
const algos = [
|
|
29
|
+
"aes-128-cbc",
|
|
30
|
+
"aes-256-cbc",
|
|
31
|
+
"aes-256-gcm",
|
|
32
|
+
"sha1",
|
|
33
|
+
"sha256",
|
|
34
|
+
"sha512",
|
|
35
|
+
"md5",
|
|
36
|
+
"des",
|
|
37
|
+
"rc4",
|
|
38
|
+
"blowfish",
|
|
39
|
+
];
|
|
40
|
+
return algos.some((algo) => lower.includes(algo));
|
|
41
|
+
}
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
CallExpression(node) {
|
|
47
|
+
// Detect hardcoded crypto algorithm in common Node.js crypto calls
|
|
48
|
+
if (
|
|
49
|
+
node.callee.type === "MemberExpression" &&
|
|
50
|
+
node.callee.object.name === "crypto" &&
|
|
51
|
+
node.arguments.length > 0 &&
|
|
52
|
+
isHardcodedCrypto(node.arguments[0])
|
|
53
|
+
) {
|
|
54
|
+
context.report({
|
|
55
|
+
node: node.arguments[0],
|
|
56
|
+
messageId: "hardcodedAlgo",
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
},
|
|
62
|
+
};
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
module.exports = {
|
|
4
|
+
meta: {
|
|
5
|
+
type: "problem",
|
|
6
|
+
docs: {
|
|
7
|
+
description:
|
|
8
|
+
"Disallow use of insecure cryptographic algorithms, cipher modes, paddings, and hash functions",
|
|
9
|
+
recommended: true,
|
|
10
|
+
},
|
|
11
|
+
schema: [],
|
|
12
|
+
messages: {
|
|
13
|
+
insecureCipher: "Do not use insecure cipher '{{algo}}'. Use AES with 128-bit block size or higher.",
|
|
14
|
+
insecureMode: "Do not use insecure cipher mode '{{mode}}'. Use CBC or GCM instead.",
|
|
15
|
+
insecurePadding: "Do not use insecure padding '{{padding}}'. Use OAEP or other secure paddings.",
|
|
16
|
+
weakHash: "Do not use weak hash function '{{hash}}'. Use SHA-256 or stronger.",
|
|
17
|
+
},
|
|
18
|
+
},
|
|
19
|
+
|
|
20
|
+
create(context) {
|
|
21
|
+
const insecureCiphers = ["DES", "DESede", "Blowfish"];
|
|
22
|
+
const insecureModes = ["ECB"];
|
|
23
|
+
const insecurePaddings = ["PKCS1Padding"];
|
|
24
|
+
const weakHashes = ["MD5", "SHA1", "SHA-1"];
|
|
25
|
+
|
|
26
|
+
function checkAlgorithmString(node, rawValue) {
|
|
27
|
+
const value = rawValue.toUpperCase();
|
|
28
|
+
|
|
29
|
+
for (const cipher of insecureCiphers) {
|
|
30
|
+
if (value.includes(cipher.toUpperCase())) {
|
|
31
|
+
context.report({
|
|
32
|
+
node,
|
|
33
|
+
messageId: "insecureCipher",
|
|
34
|
+
data: { algo: cipher },
|
|
35
|
+
});
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
for (const mode of insecureModes) {
|
|
41
|
+
if (value.includes(mode.toUpperCase())) {
|
|
42
|
+
context.report({
|
|
43
|
+
node,
|
|
44
|
+
messageId: "insecureMode",
|
|
45
|
+
data: { mode },
|
|
46
|
+
});
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
for (const padding of insecurePaddings) {
|
|
52
|
+
if (value.includes(padding.toUpperCase())) {
|
|
53
|
+
context.report({
|
|
54
|
+
node,
|
|
55
|
+
messageId: "insecurePadding",
|
|
56
|
+
data: { padding },
|
|
57
|
+
});
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
for (const hash of weakHashes) {
|
|
63
|
+
if (value === hash.toUpperCase()) {
|
|
64
|
+
context.report({
|
|
65
|
+
node,
|
|
66
|
+
messageId: "weakHash",
|
|
67
|
+
data: { hash },
|
|
68
|
+
});
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
CallExpression(node) {
|
|
76
|
+
if (
|
|
77
|
+
node.callee.type === "MemberExpression" &&
|
|
78
|
+
node.callee.object.name === "crypto"
|
|
79
|
+
) {
|
|
80
|
+
const method = node.callee.property.name;
|
|
81
|
+
|
|
82
|
+
// Check hash functions
|
|
83
|
+
if (
|
|
84
|
+
method === "createHash" &&
|
|
85
|
+
node.arguments.length &&
|
|
86
|
+
node.arguments[0].type === "Literal"
|
|
87
|
+
) {
|
|
88
|
+
checkAlgorithmString(node.arguments[0], node.arguments[0].value);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Check ciphers and paddings
|
|
92
|
+
if (
|
|
93
|
+
(method === "createCipher" || method === "createCipheriv") &&
|
|
94
|
+
node.arguments.length &&
|
|
95
|
+
node.arguments[0].type === "Literal"
|
|
96
|
+
) {
|
|
97
|
+
checkAlgorithmString(node.arguments[0], node.arguments[0].value);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
},
|
|
101
|
+
};
|
|
102
|
+
},
|
|
103
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: S010 – Insecure random generator used in sensitive context
|
|
3
|
+
* Rule ID: custom/s010
|
|
4
|
+
* Purpose: Disallow Math.random(), UUID, or insecure Random-like usage in sensitive variables (token, password, etc.)
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Disallow insecure random generators (Math.random, UUID, etc.) for sensitive values like tokens, passwords",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
insecureRandom:
|
|
20
|
+
"Do not use insecure random generator '{{name}}' for sensitive values. Use crypto.randomBytes or a CSPRNG instead.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
const sensitiveKeywords = [
|
|
26
|
+
"token",
|
|
27
|
+
"otp",
|
|
28
|
+
"session",
|
|
29
|
+
"reset",
|
|
30
|
+
"password",
|
|
31
|
+
"link",
|
|
32
|
+
"key",
|
|
33
|
+
"auth",
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
// Check if a string contains any sensitive keyword
|
|
37
|
+
function isSensitiveName(name) {
|
|
38
|
+
return sensitiveKeywords.some((kw) => name.toLowerCase().includes(kw));
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function isInSensitiveContext(node) {
|
|
42
|
+
const variable = findNearestVariable(node);
|
|
43
|
+
const func = findNearestFunction(node);
|
|
44
|
+
|
|
45
|
+
return (
|
|
46
|
+
(variable && isSensitiveName(variable.name)) ||
|
|
47
|
+
(func && isSensitiveName(func.name))
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function findNearestVariable(node) {
|
|
52
|
+
while (node) {
|
|
53
|
+
if (node.type === "VariableDeclarator" && node.id.type === "Identifier") {
|
|
54
|
+
return node.id;
|
|
55
|
+
}
|
|
56
|
+
node = node.parent;
|
|
57
|
+
}
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function findNearestFunction(node) {
|
|
62
|
+
while (node) {
|
|
63
|
+
if (
|
|
64
|
+
(node.type === "FunctionDeclaration" || node.type === "FunctionExpression") &&
|
|
65
|
+
node.id
|
|
66
|
+
) {
|
|
67
|
+
return node.id;
|
|
68
|
+
}
|
|
69
|
+
node = node.parent;
|
|
70
|
+
}
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
CallExpression(node) {
|
|
76
|
+
const callee = node.callee;
|
|
77
|
+
|
|
78
|
+
// Math.random()
|
|
79
|
+
if (
|
|
80
|
+
callee.type === "MemberExpression" &&
|
|
81
|
+
callee.object.name === "Math" &&
|
|
82
|
+
callee.property.name === "random"
|
|
83
|
+
) {
|
|
84
|
+
if (isInSensitiveContext(node)) {
|
|
85
|
+
context.report({
|
|
86
|
+
node,
|
|
87
|
+
messageId: "insecureRandom",
|
|
88
|
+
data: { name: "Math.random" },
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// UUID.v4() or uuid()
|
|
94
|
+
if (
|
|
95
|
+
callee.type === "Identifier" &&
|
|
96
|
+
(callee.name === "uuid" || callee.name === "uuidv4")
|
|
97
|
+
) {
|
|
98
|
+
if (isInSensitiveContext(node)) {
|
|
99
|
+
context.report({
|
|
100
|
+
node,
|
|
101
|
+
messageId: "insecureRandom",
|
|
102
|
+
data: { name: callee.name },
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
},
|
|
107
|
+
NewExpression(node) {
|
|
108
|
+
// new Random() – simulate Java-like pattern in JS context if exists
|
|
109
|
+
if (
|
|
110
|
+
node.callee.type === "Identifier" &&
|
|
111
|
+
node.callee.name === "Random" &&
|
|
112
|
+
isInSensitiveContext(node)
|
|
113
|
+
) {
|
|
114
|
+
context.report({
|
|
115
|
+
node,
|
|
116
|
+
messageId: "insecureRandom",
|
|
117
|
+
data: { name: "Random" },
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
},
|
|
121
|
+
};
|
|
122
|
+
},
|
|
123
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Custom ESLint rule for: S011 – UUID must be version 4 and use CSPRNG
|
|
3
|
+
* Rule ID: custom/s011
|
|
4
|
+
* Purpose: Prevent usage of UUID.randomUUID() in security-sensitive contexts.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
"use strict";
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description: "Disallow use of UUID.randomUUID() for sensitive identifiers. Use SecureRandom-based UUIDs instead.",
|
|
14
|
+
recommended: true,
|
|
15
|
+
},
|
|
16
|
+
schema: [],
|
|
17
|
+
messages: {
|
|
18
|
+
insecureUuid: "Do not use UUID.randomUUID() for '{{context}}'. Use a UUID v4 generator with SecureRandom instead.",
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
|
|
22
|
+
create(context) {
|
|
23
|
+
const sensitiveKeywords = [
|
|
24
|
+
"token", "session", "reset", "password", "link", "key", "auth", "uuid", "guid",
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
function containsSensitiveKeyword(name = "") {
|
|
28
|
+
const lower = name.toLowerCase();
|
|
29
|
+
return sensitiveKeywords.some(keyword => lower.includes(keyword));
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function getEnclosingFunctionOrVariableName(node) {
|
|
33
|
+
let current = node;
|
|
34
|
+
while (current) {
|
|
35
|
+
if (current.type === "FunctionDeclaration" || current.type === "FunctionExpression" || current.type === "ArrowFunctionExpression") {
|
|
36
|
+
return current.id?.name || "<anonymous>";
|
|
37
|
+
}
|
|
38
|
+
if (current.type === "VariableDeclarator" && current.id?.type === "Identifier") {
|
|
39
|
+
return current.id.name;
|
|
40
|
+
}
|
|
41
|
+
current = current.parent;
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return {
|
|
47
|
+
CallExpression(node) {
|
|
48
|
+
if (
|
|
49
|
+
node.callee &&
|
|
50
|
+
node.callee.type === "MemberExpression" &&
|
|
51
|
+
node.callee.object.name === "UUID" &&
|
|
52
|
+
node.callee.property.name === "randomUUID"
|
|
53
|
+
) {
|
|
54
|
+
const contextName = getEnclosingFunctionOrVariableName(node);
|
|
55
|
+
if (containsSensitiveKeyword(contextName)) {
|
|
56
|
+
context.report({
|
|
57
|
+
node,
|
|
58
|
+
messageId: "insecureUuid",
|
|
59
|
+
data: { context: contextName },
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
},
|
|
66
|
+
};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* S012 – Hardcoded Secret
|
|
5
|
+
* OWASP ASVS 1.6.2
|
|
6
|
+
* Ensure that secrets such as passwords, API keys, and cryptographic keys are not hardcoded in source code.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: "problem",
|
|
12
|
+
docs: {
|
|
13
|
+
description:
|
|
14
|
+
"Do not hardcode any secrets (API keys, passwords, cryptographic keys) in the source code.",
|
|
15
|
+
recommended: true,
|
|
16
|
+
},
|
|
17
|
+
schema: [],
|
|
18
|
+
messages: {
|
|
19
|
+
hardcodedSecret:
|
|
20
|
+
"Do not hardcode secrets (API keys, passwords, cryptographic keys) in source code.",
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
|
|
24
|
+
create(context) {
|
|
25
|
+
const secretKeywords = [
|
|
26
|
+
"SECRET",
|
|
27
|
+
"TOKEN",
|
|
28
|
+
"APIKEY",
|
|
29
|
+
"PASSWORD",
|
|
30
|
+
"PRIVATEKEY",
|
|
31
|
+
"JWT_SECRET",
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
VariableDeclarator(node) {
|
|
35
|
+
if (
|
|
36
|
+
node.id &&
|
|
37
|
+
node.id.type === "Identifier" &&
|
|
38
|
+
secretKeywords.some((kw) => node.id.name.toUpperCase().includes(kw))
|
|
39
|
+
) {
|
|
40
|
+
if (
|
|
41
|
+
node.init &&
|
|
42
|
+
node.init.type === "Literal" &&
|
|
43
|
+
typeof node.init.value === "string" &&
|
|
44
|
+
node.init.value.length > 0
|
|
45
|
+
) {
|
|
46
|
+
context.report({
|
|
47
|
+
node: node.init,
|
|
48
|
+
messageId: "hardcodedSecret",
|
|
49
|
+
});
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
},
|
|
53
|
+
AssignmentExpression(node) {
|
|
54
|
+
if (
|
|
55
|
+
node.left.type === "Identifier" &&
|
|
56
|
+
secretKeywords.some((kw) =>
|
|
57
|
+
node.left.name.toUpperCase().includes(kw)
|
|
58
|
+
) &&
|
|
59
|
+
node.right.type === "Literal" &&
|
|
60
|
+
typeof node.right.value === "string" &&
|
|
61
|
+
node.right.value.length > 0
|
|
62
|
+
) {
|
|
63
|
+
context.report({
|
|
64
|
+
node: node.right,
|
|
65
|
+
messageId: "hardcodedSecret",
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
},
|
|
71
|
+
};
|