eslint-plugin-node-security 4.0.0
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 +83 -0
- package/README.md +50 -0
- package/package.json +79 -0
- package/src/index.d.ts +10 -0
- package/src/index.js +118 -0
- package/src/index.js.map +1 -0
- package/src/rules/detect-child-process/index.d.ts +30 -0
- package/src/rules/detect-child-process/index.js +535 -0
- package/src/rules/detect-child-process/index.js.map +1 -0
- package/src/rules/detect-eval-with-expression/index.d.ts +28 -0
- package/src/rules/detect-eval-with-expression/index.js +398 -0
- package/src/rules/detect-eval-with-expression/index.js.map +1 -0
- package/src/rules/detect-non-literal-fs-filename/index.d.ts +26 -0
- package/src/rules/detect-non-literal-fs-filename/index.js +460 -0
- package/src/rules/detect-non-literal-fs-filename/index.js.map +1 -0
- package/src/rules/detect-suspicious-dependencies/index.d.ts +12 -0
- package/src/rules/detect-suspicious-dependencies/index.js +77 -0
- package/src/rules/detect-suspicious-dependencies/index.js.map +1 -0
- package/src/rules/lock-file/index.d.ts +13 -0
- package/src/rules/lock-file/index.js +94 -0
- package/src/rules/lock-file/index.js.map +1 -0
- package/src/rules/no-arbitrary-file-access/index.d.ts +12 -0
- package/src/rules/no-arbitrary-file-access/index.js +201 -0
- package/src/rules/no-arbitrary-file-access/index.js.map +1 -0
- package/src/rules/no-buffer-overread/index.d.ts +39 -0
- package/src/rules/no-buffer-overread/index.js +612 -0
- package/src/rules/no-buffer-overread/index.js.map +1 -0
- package/src/rules/no-cryptojs/index.d.ts +24 -0
- package/src/rules/no-cryptojs/index.js +104 -0
- package/src/rules/no-cryptojs/index.js.map +1 -0
- package/src/rules/no-cryptojs-weak-random/index.d.ts +24 -0
- package/src/rules/no-cryptojs-weak-random/index.js +112 -0
- package/src/rules/no-cryptojs-weak-random/index.js.map +1 -0
- package/src/rules/no-data-in-temp-storage/index.d.ts +14 -0
- package/src/rules/no-data-in-temp-storage/index.js +99 -0
- package/src/rules/no-data-in-temp-storage/index.js.map +1 -0
- package/src/rules/no-deprecated-cipher-method/index.d.ts +23 -0
- package/src/rules/no-deprecated-cipher-method/index.js +118 -0
- package/src/rules/no-deprecated-cipher-method/index.js.map +1 -0
- package/src/rules/no-dynamic-dependency-loading/index.d.ts +12 -0
- package/src/rules/no-dynamic-dependency-loading/index.js +55 -0
- package/src/rules/no-dynamic-dependency-loading/index.js.map +1 -0
- package/src/rules/no-dynamic-require/index.d.ts +21 -0
- package/src/rules/no-dynamic-require/index.js +122 -0
- package/src/rules/no-dynamic-require/index.js.map +1 -0
- package/src/rules/no-ecb-mode/index.d.ts +23 -0
- package/src/rules/no-ecb-mode/index.js +113 -0
- package/src/rules/no-ecb-mode/index.js.map +1 -0
- package/src/rules/no-insecure-key-derivation/index.d.ts +24 -0
- package/src/rules/no-insecure-key-derivation/index.js +116 -0
- package/src/rules/no-insecure-key-derivation/index.js.map +1 -0
- package/src/rules/no-insecure-rsa-padding/index.d.ts +24 -0
- package/src/rules/no-insecure-rsa-padding/index.js +110 -0
- package/src/rules/no-insecure-rsa-padding/index.js.map +1 -0
- package/src/rules/no-pii-in-logs/index.d.ts +12 -0
- package/src/rules/no-pii-in-logs/index.js +74 -0
- package/src/rules/no-pii-in-logs/index.js.map +1 -0
- package/src/rules/no-self-signed-certs/index.d.ts +23 -0
- package/src/rules/no-self-signed-certs/index.js +116 -0
- package/src/rules/no-self-signed-certs/index.js.map +1 -0
- package/src/rules/no-sha1-hash/index.d.ts +24 -0
- package/src/rules/no-sha1-hash/index.js +128 -0
- package/src/rules/no-sha1-hash/index.js.map +1 -0
- package/src/rules/no-static-iv/index.d.ts +23 -0
- package/src/rules/no-static-iv/index.js +147 -0
- package/src/rules/no-static-iv/index.js.map +1 -0
- package/src/rules/no-timing-unsafe-compare/index.d.ts +23 -0
- package/src/rules/no-timing-unsafe-compare/index.js +114 -0
- package/src/rules/no-timing-unsafe-compare/index.js.map +1 -0
- package/src/rules/no-toctou-vulnerability/index.d.ts +26 -0
- package/src/rules/no-toctou-vulnerability/index.js +214 -0
- package/src/rules/no-toctou-vulnerability/index.js.map +1 -0
- package/src/rules/no-unsafe-dynamic-require/index.d.ts +19 -0
- package/src/rules/no-unsafe-dynamic-require/index.js +112 -0
- package/src/rules/no-unsafe-dynamic-require/index.js.map +1 -0
- package/src/rules/no-weak-cipher-algorithm/index.d.ts +25 -0
- package/src/rules/no-weak-cipher-algorithm/index.js +190 -0
- package/src/rules/no-weak-cipher-algorithm/index.js.map +1 -0
- package/src/rules/no-weak-hash-algorithm/index.d.ts +25 -0
- package/src/rules/no-weak-hash-algorithm/index.js +218 -0
- package/src/rules/no-weak-hash-algorithm/index.js.map +1 -0
- package/src/rules/no-zip-slip/index.d.ts +35 -0
- package/src/rules/no-zip-slip/index.js +451 -0
- package/src/rules/no-zip-slip/index.js.map +1 -0
- package/src/rules/prefer-native-crypto/index.d.ts +23 -0
- package/src/rules/prefer-native-crypto/index.js +124 -0
- package/src/rules/prefer-native-crypto/index.js.map +1 -0
- package/src/rules/require-dependency-integrity/index.d.ts +12 -0
- package/src/rules/require-dependency-integrity/index.js +70 -0
- package/src/rules/require-dependency-integrity/index.js.map +1 -0
- package/src/rules/require-secure-credential-storage/index.d.ts +12 -0
- package/src/rules/require-secure-credential-storage/index.js +54 -0
- package/src/rules/require-secure-credential-storage/index.js.map +1 -0
- package/src/rules/require-secure-deletion/index.d.ts +12 -0
- package/src/rules/require-secure-deletion/index.js +46 -0
- package/src/rules/require-secure-deletion/index.js.map +1 -0
- package/src/rules/require-storage-encryption/index.d.ts +12 -0
- package/src/rules/require-storage-encryption/index.js +54 -0
- package/src/rules/require-storage-encryption/index.js.map +1 -0
- package/src/types/index.d.ts +24 -0
- package/src/types/index.js +8 -0
- package/src/types/index.js.map +1 -0
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noDynamicRequire = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
const eslint_devkit_2 = require("@interlace/eslint-devkit");
|
|
11
|
+
exports.noDynamicRequire = (0, eslint_devkit_1.createRule)({
|
|
12
|
+
name: 'no-dynamic-require',
|
|
13
|
+
meta: {
|
|
14
|
+
type: 'problem',
|
|
15
|
+
docs: {
|
|
16
|
+
description: 'Forbid `require()` calls with expressions',
|
|
17
|
+
},
|
|
18
|
+
hasSuggestions: false,
|
|
19
|
+
messages: {
|
|
20
|
+
dynamicRequire: (0, eslint_devkit_2.formatLLMMessage)({
|
|
21
|
+
icon: eslint_devkit_2.MessageIcons.WARNING,
|
|
22
|
+
issueName: 'Dynamic Require',
|
|
23
|
+
description: 'Require call uses dynamic expression',
|
|
24
|
+
severity: 'HIGH',
|
|
25
|
+
fix: 'Use static string literals for require() calls',
|
|
26
|
+
documentationLink: 'https://github.com/import-js/eslint-plugin-import/blob/main/docs/rules/no-dynamic-require.md',
|
|
27
|
+
}),
|
|
28
|
+
},
|
|
29
|
+
schema: [
|
|
30
|
+
{
|
|
31
|
+
type: 'object',
|
|
32
|
+
properties: {
|
|
33
|
+
allowContexts: {
|
|
34
|
+
type: 'array',
|
|
35
|
+
items: {
|
|
36
|
+
type: 'string',
|
|
37
|
+
enum: ['test', 'config', 'build', 'runtime'],
|
|
38
|
+
},
|
|
39
|
+
description: 'Allow dynamic requires in specific contexts.',
|
|
40
|
+
},
|
|
41
|
+
allowPatterns: {
|
|
42
|
+
type: 'array',
|
|
43
|
+
items: { type: 'string' },
|
|
44
|
+
description: 'Regex patterns for allowed dynamic require paths.',
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
additionalProperties: false,
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
defaultOptions: [{
|
|
52
|
+
allowContexts: [],
|
|
53
|
+
allowPatterns: []
|
|
54
|
+
}],
|
|
55
|
+
create(context) {
|
|
56
|
+
const [options] = context.options;
|
|
57
|
+
const { allowContexts = [], allowPatterns = [], } = options || {};
|
|
58
|
+
const filename = context.getFilename() || '';
|
|
59
|
+
function isInAllowedContext() {
|
|
60
|
+
if (allowContexts.includes('test') && (filename.includes('.test.') || filename.includes('.spec.') || filename.includes('/__tests__/'))) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (allowContexts.includes('config') && (filename.includes('config') || filename.includes('webpack') || filename.includes('rollup'))) {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
if (allowContexts.includes('build') && (filename.includes('build') || filename.includes('scripts') || filename.includes('tools'))) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
if (allowContexts.includes('runtime') && (filename.includes('runtime') || filename.includes('dynamic'))) {
|
|
70
|
+
return true;
|
|
71
|
+
}
|
|
72
|
+
return false;
|
|
73
|
+
}
|
|
74
|
+
/* v8 ignore start -- allowPatterns check is unreachable: static literals return early before this check */
|
|
75
|
+
function isAllowedPattern(requirePath) {
|
|
76
|
+
return allowPatterns.some((pattern) => {
|
|
77
|
+
try {
|
|
78
|
+
const regex = new RegExp(pattern);
|
|
79
|
+
return regex.test(requirePath);
|
|
80
|
+
}
|
|
81
|
+
catch {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/* v8 ignore stop */
|
|
87
|
+
function isStaticLiteral(node) {
|
|
88
|
+
return node.type === 'Literal' && typeof node.value === 'string';
|
|
89
|
+
}
|
|
90
|
+
return {
|
|
91
|
+
CallExpression(node) {
|
|
92
|
+
if (node.callee.type === 'Identifier' &&
|
|
93
|
+
node.callee.name === 'require' &&
|
|
94
|
+
node.arguments.length === 1) {
|
|
95
|
+
const requireArg = node.arguments[0];
|
|
96
|
+
if (isInAllowedContext()) {
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
// Check if it's a static literal
|
|
100
|
+
if (isStaticLiteral(requireArg)) {
|
|
101
|
+
// Static requires are allowed
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
/* v8 ignore start -- unreachable: static literals already returned above */
|
|
105
|
+
// Check allow patterns
|
|
106
|
+
if (requireArg.type === 'Literal' && typeof requireArg.value === 'string') {
|
|
107
|
+
if (isAllowedPattern(requireArg.value)) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/* v8 ignore stop */
|
|
112
|
+
// Report dynamic require
|
|
113
|
+
context.report({
|
|
114
|
+
node: requireArg,
|
|
115
|
+
messageId: 'dynamicRequire',
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/eslint-plugin-node-security/src/rules/no-dynamic-require/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: no-ecb-mode
|
|
8
|
+
* Detects use of ECB encryption mode which leaks data patterns
|
|
9
|
+
* CWE-327: ECB mode encrypts identical blocks identically
|
|
10
|
+
*
|
|
11
|
+
* @see https://blog.cloudflare.com/why-are-some-images-more-secure-than-others/
|
|
12
|
+
*/
|
|
13
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
14
|
+
type MessageIds = 'ecbMode' | 'useGcm' | 'useCbc';
|
|
15
|
+
export interface Options {
|
|
16
|
+
/** Allow ECB in test files. Default: false */
|
|
17
|
+
allowInTests?: boolean;
|
|
18
|
+
}
|
|
19
|
+
type RuleOptions = [Options?];
|
|
20
|
+
export declare const noEcbMode: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
21
|
+
name: string;
|
|
22
|
+
};
|
|
23
|
+
export type { Options as NoEcbModeOptions };
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noEcbMode = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
exports.noEcbMode = (0, eslint_devkit_1.createRule)({
|
|
11
|
+
name: 'no-ecb-mode',
|
|
12
|
+
meta: {
|
|
13
|
+
type: 'problem',
|
|
14
|
+
docs: {
|
|
15
|
+
description: 'Disallow ECB encryption mode (use GCM or CBC instead)',
|
|
16
|
+
},
|
|
17
|
+
hasSuggestions: true,
|
|
18
|
+
messages: {
|
|
19
|
+
ecbMode: (0, eslint_devkit_1.formatLLMMessage)({
|
|
20
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
21
|
+
issueName: 'ECB mode detected',
|
|
22
|
+
cwe: 'CWE-327',
|
|
23
|
+
description: 'ECB mode encrypts identical plaintext blocks to identical ciphertext, leaking data patterns. Famous example: the "ECB penguin".',
|
|
24
|
+
severity: 'HIGH',
|
|
25
|
+
fix: 'Use GCM mode for authenticated encryption: crypto.createCipheriv("aes-256-gcm", key, iv)',
|
|
26
|
+
documentationLink: 'https://blog.cloudflare.com/why-are-some-images-more-secure-than-others/',
|
|
27
|
+
}),
|
|
28
|
+
useGcm: (0, eslint_devkit_1.formatLLMMessage)({
|
|
29
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
30
|
+
issueName: 'Use GCM mode',
|
|
31
|
+
description: 'GCM provides authenticated encryption (confidentiality + integrity)',
|
|
32
|
+
severity: 'LOW',
|
|
33
|
+
fix: 'crypto.createCipheriv("aes-256-gcm", key, iv)',
|
|
34
|
+
documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options',
|
|
35
|
+
}),
|
|
36
|
+
useCbc: (0, eslint_devkit_1.formatLLMMessage)({
|
|
37
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
38
|
+
issueName: 'Use CBC mode',
|
|
39
|
+
description: 'CBC with HMAC provides confidentiality (add separate MAC for integrity)',
|
|
40
|
+
severity: 'LOW',
|
|
41
|
+
fix: 'crypto.createCipheriv("aes-256-cbc", key, iv)',
|
|
42
|
+
documentationLink: 'https://nodejs.org/api/crypto.html#cryptocreatecipherivalgorithm-key-iv-options',
|
|
43
|
+
}),
|
|
44
|
+
},
|
|
45
|
+
schema: [
|
|
46
|
+
{
|
|
47
|
+
type: 'object',
|
|
48
|
+
properties: {
|
|
49
|
+
allowInTests: {
|
|
50
|
+
type: 'boolean',
|
|
51
|
+
default: false,
|
|
52
|
+
description: 'Allow ECB mode in test files',
|
|
53
|
+
},
|
|
54
|
+
},
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
defaultOptions: [
|
|
60
|
+
{
|
|
61
|
+
allowInTests: false,
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
create(context, [options = {}]) {
|
|
65
|
+
const { allowInTests = false } = options;
|
|
66
|
+
const filename = context.filename;
|
|
67
|
+
const isTestFile = allowInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
68
|
+
function checkCallExpression(node) {
|
|
69
|
+
if (isTestFile)
|
|
70
|
+
return;
|
|
71
|
+
const cipherMethods = new Set(['createCipher', 'createCipheriv', 'createDecipher', 'createDecipheriv']);
|
|
72
|
+
// Check for crypto.createCipher*() pattern
|
|
73
|
+
const isCipherCall = (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
74
|
+
node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
75
|
+
cipherMethods.has(node.callee.property.name)) ||
|
|
76
|
+
(node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
77
|
+
cipherMethods.has(node.callee.name));
|
|
78
|
+
if (isCipherCall && node.arguments.length >= 1) {
|
|
79
|
+
const algorithmArg = node.arguments[0];
|
|
80
|
+
if (algorithmArg.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof algorithmArg.value === 'string') {
|
|
81
|
+
const algorithm = algorithmArg.value.toLowerCase();
|
|
82
|
+
if (algorithm.includes('-ecb') || algorithm.endsWith('ecb')) {
|
|
83
|
+
const gcmReplacement = algorithm.replace(/-?ecb$/, '-gcm');
|
|
84
|
+
/* c8 ignore next 18 -- suggestions require output assertions which are impractical for dynamic fixes */
|
|
85
|
+
context.report({
|
|
86
|
+
node: algorithmArg,
|
|
87
|
+
messageId: 'ecbMode',
|
|
88
|
+
suggest: [
|
|
89
|
+
{
|
|
90
|
+
messageId: 'useGcm',
|
|
91
|
+
fix: (fixer) => {
|
|
92
|
+
return fixer.replaceText(algorithmArg, `"${gcmReplacement}"`);
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
messageId: 'useCbc',
|
|
97
|
+
fix: (fixer) => {
|
|
98
|
+
const cbcReplacement = algorithm.replace(/-?ecb$/, '-cbc');
|
|
99
|
+
return fixer.replaceText(algorithmArg, `"${cbcReplacement}"`);
|
|
100
|
+
},
|
|
101
|
+
},
|
|
102
|
+
],
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
CallExpression: checkCallExpression,
|
|
110
|
+
};
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/eslint-plugin-node-security/src/rules/no-ecb-mode/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: no-insecure-key-derivation
|
|
8
|
+
* Detects PBKDF2 with insufficient iterations
|
|
9
|
+
* CWE-916: Use of Password Hash With Insufficient Computational Effort
|
|
10
|
+
*
|
|
11
|
+
* OWASP 2023 recommends minimum 600,000 iterations for PBKDF2-SHA256
|
|
12
|
+
* @see https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html
|
|
13
|
+
*/
|
|
14
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
15
|
+
type MessageIds = 'insufficientIterations' | 'useMinIterations' | 'useScrypt' | 'useArgon2';
|
|
16
|
+
export interface Options {
|
|
17
|
+
/** Minimum PBKDF2 iterations. Default: 100000 */
|
|
18
|
+
minIterations?: number;
|
|
19
|
+
}
|
|
20
|
+
type RuleOptions = [Options?];
|
|
21
|
+
export declare const noInsecureKeyDerivation: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export type { Options as NoInsecureKeyDerivationOptions };
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noInsecureKeyDerivation = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
// OWASP 2023 recommendations
|
|
11
|
+
const DEFAULT_MIN_ITERATIONS = 100000;
|
|
12
|
+
exports.noInsecureKeyDerivation = (0, eslint_devkit_1.createRule)({
|
|
13
|
+
name: 'no-insecure-key-derivation',
|
|
14
|
+
meta: {
|
|
15
|
+
type: 'problem',
|
|
16
|
+
docs: {
|
|
17
|
+
description: 'Disallow PBKDF2 with insufficient iterations (< 100,000)',
|
|
18
|
+
},
|
|
19
|
+
hasSuggestions: true,
|
|
20
|
+
messages: {
|
|
21
|
+
insufficientIterations: (0, eslint_devkit_1.formatLLMMessage)({
|
|
22
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
23
|
+
issueName: 'Insufficient PBKDF2 iterations',
|
|
24
|
+
cwe: 'CWE-916',
|
|
25
|
+
description: 'PBKDF2 with {{actual}} iterations is too low. Minimum recommended: {{minimum}} iterations (OWASP 2023).',
|
|
26
|
+
severity: 'HIGH',
|
|
27
|
+
fix: 'Increase iterations to at least {{minimum}}, or use scrypt/Argon2',
|
|
28
|
+
documentationLink: 'https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html',
|
|
29
|
+
}),
|
|
30
|
+
useMinIterations: (0, eslint_devkit_1.formatLLMMessage)({
|
|
31
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
32
|
+
issueName: 'Use minimum iterations',
|
|
33
|
+
description: 'Use at least {{minimum}} iterations for PBKDF2',
|
|
34
|
+
severity: 'LOW',
|
|
35
|
+
fix: 'crypto.pbkdf2(password, salt, {{minimum}}, keylen, digest)',
|
|
36
|
+
documentationLink: 'https://nodejs.org/api/crypto.html#cryptopbkdf2password-salt-iterations-keylen-digest-callback',
|
|
37
|
+
}),
|
|
38
|
+
useScrypt: (0, eslint_devkit_1.formatLLMMessage)({
|
|
39
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
40
|
+
issueName: 'Use scrypt',
|
|
41
|
+
description: 'scrypt is memory-hard and resistant to GPU/ASIC attacks',
|
|
42
|
+
severity: 'LOW',
|
|
43
|
+
fix: 'crypto.scrypt(password, salt, keylen)',
|
|
44
|
+
documentationLink: 'https://nodejs.org/api/crypto.html#cryptoscryptpassword-salt-keylen-options-callback',
|
|
45
|
+
}),
|
|
46
|
+
useArgon2: (0, eslint_devkit_1.formatLLMMessage)({
|
|
47
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
48
|
+
issueName: 'Use Argon2',
|
|
49
|
+
description: 'Argon2id is the winner of the Password Hashing Competition',
|
|
50
|
+
severity: 'LOW',
|
|
51
|
+
fix: 'argon2.hash(password, { type: argon2.argon2id })',
|
|
52
|
+
documentationLink: 'https://github.com/ranisalt/node-argon2',
|
|
53
|
+
}),
|
|
54
|
+
},
|
|
55
|
+
schema: [
|
|
56
|
+
{
|
|
57
|
+
type: 'object',
|
|
58
|
+
properties: {
|
|
59
|
+
minIterations: {
|
|
60
|
+
type: 'number',
|
|
61
|
+
default: DEFAULT_MIN_ITERATIONS,
|
|
62
|
+
description: 'Minimum required PBKDF2 iterations',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
additionalProperties: false,
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
},
|
|
69
|
+
defaultOptions: [
|
|
70
|
+
{
|
|
71
|
+
minIterations: DEFAULT_MIN_ITERATIONS,
|
|
72
|
+
},
|
|
73
|
+
],
|
|
74
|
+
create(context, [options = {}]) {
|
|
75
|
+
const { minIterations = DEFAULT_MIN_ITERATIONS } = options;
|
|
76
|
+
function checkCallExpression(node) {
|
|
77
|
+
// Check for crypto.pbkdf2() or crypto.pbkdf2Sync()
|
|
78
|
+
const isPbkdf2Call = (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
79
|
+
node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
80
|
+
(node.callee.property.name === 'pbkdf2' || node.callee.property.name === 'pbkdf2Sync')) ||
|
|
81
|
+
(node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
82
|
+
(node.callee.name === 'pbkdf2' || node.callee.name === 'pbkdf2Sync'));
|
|
83
|
+
if (isPbkdf2Call) {
|
|
84
|
+
// pbkdf2(password, salt, iterations, keylen, digest, callback)
|
|
85
|
+
// iterations is the 3rd argument (index 2)
|
|
86
|
+
const iterationsArg = node.arguments[2];
|
|
87
|
+
if (iterationsArg?.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof iterationsArg.value === 'number') {
|
|
88
|
+
const iterations = iterationsArg.value;
|
|
89
|
+
if (iterations < minIterations) {
|
|
90
|
+
context.report({
|
|
91
|
+
node: iterationsArg,
|
|
92
|
+
messageId: 'insufficientIterations',
|
|
93
|
+
data: {
|
|
94
|
+
actual: String(iterations),
|
|
95
|
+
minimum: String(minIterations),
|
|
96
|
+
},
|
|
97
|
+
suggest: [
|
|
98
|
+
{
|
|
99
|
+
messageId: 'useMinIterations',
|
|
100
|
+
data: { minimum: String(minIterations) },
|
|
101
|
+
fix: (fixer) => {
|
|
102
|
+
return fixer.replaceText(iterationsArg, String(minIterations));
|
|
103
|
+
},
|
|
104
|
+
},
|
|
105
|
+
],
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
CallExpression: checkCallExpression,
|
|
113
|
+
};
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/eslint-plugin-node-security/src/rules/no-insecure-key-derivation/index.ts"],"names":[],"mappings":";AAAA;;;;GAIG;;;AAWH,4DAAsG;AAetG,6BAA6B;AAC7B,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAEzB,QAAA,uBAAuB,GAAG,IAAA,0BAAU,EAA0B;IACzE,IAAI,EAAE,4BAA4B;IAClC,IAAI,EAAE;QACJ,IAAI,EAAE,SAAS;QACf,IAAI,EAAE;YACJ,WAAW,EAAE,0DAA0D;SACxE;QACD,cAAc,EAAE,IAAI;QACpB,QAAQ,EAAE;YACR,sBAAsB,EAAE,IAAA,gCAAgB,EAAC;gBACvC,IAAI,EAAE,4BAAY,CAAC,QAAQ;gBAC3B,SAAS,EAAE,gCAAgC;gBAC3C,GAAG,EAAE,SAAS;gBACd,WAAW,EAAE,yGAAyG;gBACtH,QAAQ,EAAE,MAAM;gBAChB,GAAG,EAAE,mEAAmE;gBACxE,iBAAiB,EAAE,kFAAkF;aACtG,CAAC;YACF,gBAAgB,EAAE,IAAA,gCAAgB,EAAC;gBACjC,IAAI,EAAE,4BAAY,CAAC,IAAI;gBACvB,SAAS,EAAE,wBAAwB;gBACnC,WAAW,EAAE,gDAAgD;gBAC7D,QAAQ,EAAE,KAAK;gBACf,GAAG,EAAE,4DAA4D;gBACjE,iBAAiB,EAAE,gGAAgG;aACpH,CAAC;YACF,SAAS,EAAE,IAAA,gCAAgB,EAAC;gBAC1B,IAAI,EAAE,4BAAY,CAAC,IAAI;gBACvB,SAAS,EAAE,YAAY;gBACvB,WAAW,EAAE,yDAAyD;gBACtE,QAAQ,EAAE,KAAK;gBACf,GAAG,EAAE,uCAAuC;gBAC5C,iBAAiB,EAAE,sFAAsF;aAC1G,CAAC;YACF,SAAS,EAAE,IAAA,gCAAgB,EAAC;gBAC1B,IAAI,EAAE,4BAAY,CAAC,IAAI;gBACvB,SAAS,EAAE,YAAY;gBACvB,WAAW,EAAE,4DAA4D;gBACzE,QAAQ,EAAE,KAAK;gBACf,GAAG,EAAE,kDAAkD;gBACvD,iBAAiB,EAAE,yCAAyC;aAC7D,CAAC;SACH;QACD,MAAM,EAAE;YACN;gBACE,IAAI,EAAE,QAAQ;gBACd,UAAU,EAAE;oBACV,aAAa,EAAE;wBACb,IAAI,EAAE,QAAQ;wBACd,OAAO,EAAE,sBAAsB;wBAC/B,WAAW,EAAE,oCAAoC;qBAClD;iBACF;gBACD,oBAAoB,EAAE,KAAK;aAC5B;SACF;KACF;IACD,cAAc,EAAE;QACd;YACE,aAAa,EAAE,sBAAsB;SACtC;KACF;IACD,MAAM,CACJ,OAAsD,EACtD,CAAC,OAAO,GAAG,EAAE,CAAC;QAEd,MAAM,EAAE,aAAa,GAAG,sBAAsB,EAAE,GAAG,OAAkB,CAAC;QAEtE,SAAS,mBAAmB,CAAC,IAA6B;YACxD,mDAAmD;YACnD,MAAM,YAAY,GAChB,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,8BAAc,CAAC,gBAAgB;gBACnD,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,8BAAc,CAAC,UAAU;gBACvD,CAAC,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,QAAQ,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC;gBACzF,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,8BAAc,CAAC,UAAU;oBAC7C,CAAC,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,QAAQ,IAAI,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,YAAY,CAAC,CAAC,CAAC;YAE1E,IAAI,YAAY,EAAE,CAAC;gBACjB,+DAA+D;gBAC/D,2CAA2C;gBAC3C,MAAM,aAAa,GAAG,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;gBAExC,IAAI,aAAa,EAAE,IAAI,KAAK,8BAAc,CAAC,OAAO,IAAI,OAAO,aAAa,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;oBAC9F,MAAM,UAAU,GAAG,aAAa,CAAC,KAAK,CAAC;oBAEvC,IAAI,UAAU,GAAG,aAAa,EAAE,CAAC;wBAC/B,OAAO,CAAC,MAAM,CAAC;4BACb,IAAI,EAAE,aAAa;4BACnB,SAAS,EAAE,wBAAwB;4BACnC,IAAI,EAAE;gCACJ,MAAM,EAAE,MAAM,CAAC,UAAU,CAAC;gCAC1B,OAAO,EAAE,MAAM,CAAC,aAAa,CAAC;6BAC/B;4BACD,OAAO,EAAE;gCACP;oCACE,SAAS,EAAE,kBAAkB;oCAC7B,IAAI,EAAE,EAAE,OAAO,EAAE,MAAM,CAAC,aAAa,CAAC,EAAE;oCACxC,GAAG,EAAE,CAAC,KAAyB,EAAE,EAAE;wCACjC,OAAO,KAAK,CAAC,WAAW,CAAC,aAAa,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC,CAAC;oCACjE,CAAC;iCACF;6BACF;yBACF,CAAC,CAAC;oBACL,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,OAAO;YACL,cAAc,EAAE,mBAAmB;SACpC,CAAC;IACJ,CAAC;CACF,CAAC,CAAC"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* ESLint Rule: no-insecure-rsa-padding
|
|
8
|
+
* Detects RSA with PKCS#1 v1.5 padding (Marvin Attack vulnerable)
|
|
9
|
+
* CVE-2023-46809: Timing side-channel in Node.js privateDecrypt()
|
|
10
|
+
*
|
|
11
|
+
* @see https://nvd.nist.gov/vuln/detail/CVE-2023-46809
|
|
12
|
+
* @see https://people.redhat.com/~hkario/marvin/
|
|
13
|
+
*/
|
|
14
|
+
import type { TSESLint } from '@interlace/eslint-devkit';
|
|
15
|
+
type MessageIds = 'insecureRsaPadding' | 'useOaep';
|
|
16
|
+
export interface Options {
|
|
17
|
+
/** Allow in test files. Default: false */
|
|
18
|
+
allowInTests?: boolean;
|
|
19
|
+
}
|
|
20
|
+
type RuleOptions = [Options?];
|
|
21
|
+
export declare const noInsecureRsaPadding: TSESLint.RuleModule<MessageIds, RuleOptions, unknown, TSESLint.RuleListener> & {
|
|
22
|
+
name: string;
|
|
23
|
+
};
|
|
24
|
+
export type { Options as NoInsecureRsaPaddingOptions };
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noInsecureRsaPadding = void 0;
|
|
9
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
10
|
+
// Constants for PKCS#1 v1.5 padding (insecure)
|
|
11
|
+
const PKCS1_PADDING_NAMES = new Set([
|
|
12
|
+
'RSA_PKCS1_PADDING',
|
|
13
|
+
'constants.RSA_PKCS1_PADDING',
|
|
14
|
+
'crypto.constants.RSA_PKCS1_PADDING',
|
|
15
|
+
]);
|
|
16
|
+
exports.noInsecureRsaPadding = (0, eslint_devkit_1.createRule)({
|
|
17
|
+
name: 'no-insecure-rsa-padding',
|
|
18
|
+
meta: {
|
|
19
|
+
type: 'problem',
|
|
20
|
+
docs: {
|
|
21
|
+
description: 'Disallow RSA PKCS#1 v1.5 padding (CVE-2023-46809 Marvin Attack)',
|
|
22
|
+
},
|
|
23
|
+
hasSuggestions: true,
|
|
24
|
+
messages: {
|
|
25
|
+
insecureRsaPadding: (0, eslint_devkit_1.formatLLMMessage)({
|
|
26
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
27
|
+
issueName: 'Insecure RSA padding',
|
|
28
|
+
cwe: 'CWE-327',
|
|
29
|
+
description: 'RSA PKCS#1 v1.5 padding is vulnerable to the Marvin Attack (CVE-2023-46809). Timing side-channels allow attackers to decrypt ciphertexts or forge signatures.',
|
|
30
|
+
severity: 'HIGH',
|
|
31
|
+
fix: 'Use RSA_PKCS1_OAEP_PADDING instead',
|
|
32
|
+
documentationLink: 'https://people.redhat.com/~hkario/marvin/',
|
|
33
|
+
}),
|
|
34
|
+
useOaep: (0, eslint_devkit_1.formatLLMMessage)({
|
|
35
|
+
icon: eslint_devkit_1.MessageIcons.INFO,
|
|
36
|
+
issueName: 'Use OAEP padding',
|
|
37
|
+
description: 'Use RSA-OAEP which is not vulnerable to padding oracle attacks',
|
|
38
|
+
severity: 'LOW',
|
|
39
|
+
fix: 'padding: crypto.constants.RSA_PKCS1_OAEP_PADDING',
|
|
40
|
+
documentationLink: 'https://nodejs.org/api/crypto.html#cryptopublicdecryptkey-buffer',
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
schema: [
|
|
44
|
+
{
|
|
45
|
+
type: 'object',
|
|
46
|
+
properties: {
|
|
47
|
+
allowInTests: {
|
|
48
|
+
type: 'boolean',
|
|
49
|
+
default: false,
|
|
50
|
+
description: 'Allow in test files',
|
|
51
|
+
},
|
|
52
|
+
},
|
|
53
|
+
additionalProperties: false,
|
|
54
|
+
},
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
defaultOptions: [
|
|
58
|
+
{
|
|
59
|
+
allowInTests: false,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
create(context, [options = {}]) {
|
|
63
|
+
const { allowInTests = false } = options;
|
|
64
|
+
const filename = context.filename;
|
|
65
|
+
const isTestFile = allowInTests && /\.(test|spec)\.(ts|tsx|js|jsx)$/.test(filename);
|
|
66
|
+
const sourceCode = context.sourceCode;
|
|
67
|
+
function checkCallExpression(node) {
|
|
68
|
+
if (isTestFile)
|
|
69
|
+
return;
|
|
70
|
+
// Check for privateDecrypt, publicDecrypt, privateEncrypt, publicEncrypt
|
|
71
|
+
const rsaMethods = new Set(['privateDecrypt', 'publicDecrypt', 'privateEncrypt', 'publicEncrypt']);
|
|
72
|
+
const isRsaCall = (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
73
|
+
node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
74
|
+
rsaMethods.has(node.callee.property.name)) ||
|
|
75
|
+
(node.callee.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
76
|
+
rsaMethods.has(node.callee.name));
|
|
77
|
+
if (isRsaCall && node.arguments.length >= 1) {
|
|
78
|
+
const keyArg = node.arguments[0];
|
|
79
|
+
// Check if first argument is an object with padding property
|
|
80
|
+
if (keyArg.type === eslint_devkit_1.AST_NODE_TYPES.ObjectExpression) {
|
|
81
|
+
for (const prop of keyArg.properties) {
|
|
82
|
+
if (prop.type === eslint_devkit_1.AST_NODE_TYPES.Property &&
|
|
83
|
+
prop.key.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
84
|
+
prop.key.name === 'padding') {
|
|
85
|
+
const paddingText = sourceCode.getText(prop.value);
|
|
86
|
+
if (PKCS1_PADDING_NAMES.has(paddingText) || paddingText.includes('RSA_PKCS1_PADDING')) {
|
|
87
|
+
context.report({
|
|
88
|
+
node: prop,
|
|
89
|
+
messageId: 'insecureRsaPadding',
|
|
90
|
+
suggest: [
|
|
91
|
+
{
|
|
92
|
+
messageId: 'useOaep',
|
|
93
|
+
fix: (fixer) => {
|
|
94
|
+
return fixer.replaceText(prop.value, 'crypto.constants.RSA_PKCS1_OAEP_PADDING');
|
|
95
|
+
},
|
|
96
|
+
},
|
|
97
|
+
],
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
return {
|
|
106
|
+
CallExpression: checkCallExpression,
|
|
107
|
+
};
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../../../packages/eslint-plugin-node-security/src/rules/no-insecure-rsa-padding/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
3
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
4
|
+
* MIT license that can be found in the LICENSE file.
|
|
5
|
+
*/
|
|
6
|
+
export interface Options {
|
|
7
|
+
}
|
|
8
|
+
type RuleOptions = [Options?];
|
|
9
|
+
export declare const noPiiInLogs: import("@typescript-eslint/utils/ts-eslint").RuleModule<"violationDetected", RuleOptions, unknown, import("@typescript-eslint/utils/ts-eslint").RuleListener> & {
|
|
10
|
+
name: string;
|
|
11
|
+
};
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Copyright (c) 2025 Ofri Peretz
|
|
4
|
+
* Licensed under the MIT License. Use of this source code is governed by the
|
|
5
|
+
* MIT license that can be found in the LICENSE file.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.noPiiInLogs = void 0;
|
|
9
|
+
/**
|
|
10
|
+
* @fileoverview Prevent PII (email, SSN, credit cards) in console logs
|
|
11
|
+
* @see https://owasp.org/www-project-mobile-top-10/
|
|
12
|
+
* @see https://cwe.mitre.org/data/definitions/532.html
|
|
13
|
+
*/
|
|
14
|
+
const eslint_devkit_1 = require("@interlace/eslint-devkit");
|
|
15
|
+
exports.noPiiInLogs = (0, eslint_devkit_1.createRule)({
|
|
16
|
+
name: 'no-pii-in-logs',
|
|
17
|
+
meta: {
|
|
18
|
+
type: 'problem',
|
|
19
|
+
docs: {
|
|
20
|
+
description: 'Prevent PII (email, SSN, credit cards) in console logs',
|
|
21
|
+
},
|
|
22
|
+
messages: {
|
|
23
|
+
violationDetected: (0, eslint_devkit_1.formatLLMMessage)({
|
|
24
|
+
icon: eslint_devkit_1.MessageIcons.SECURITY,
|
|
25
|
+
issueName: 'violation Detected',
|
|
26
|
+
cwe: 'CWE-359',
|
|
27
|
+
description: 'Prevent PII (email, SSN, credit cards) in console logs detected - this is a security risk',
|
|
28
|
+
severity: 'HIGH',
|
|
29
|
+
fix: 'Review and apply secure practices',
|
|
30
|
+
documentationLink: 'https://cwe.mitre.org/data/definitions/359.html',
|
|
31
|
+
})
|
|
32
|
+
},
|
|
33
|
+
schema: [],
|
|
34
|
+
},
|
|
35
|
+
defaultOptions: [],
|
|
36
|
+
create(context) {
|
|
37
|
+
function report(node) {
|
|
38
|
+
context.report({
|
|
39
|
+
node,
|
|
40
|
+
messageId: 'violationDetected',
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
return {
|
|
44
|
+
CallExpression(node) {
|
|
45
|
+
// Check console.log/error/warn calls
|
|
46
|
+
if (node.callee.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
47
|
+
node.callee.object.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
48
|
+
node.callee.object.name === 'console' &&
|
|
49
|
+
node.callee.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier &&
|
|
50
|
+
['log', 'error', 'warn', 'info'].includes(node.callee.property.name)) {
|
|
51
|
+
// Check arguments for PII-related property access
|
|
52
|
+
for (const arg of node.arguments) {
|
|
53
|
+
if (arg.type === eslint_devkit_1.AST_NODE_TYPES.MemberExpression &&
|
|
54
|
+
arg.property.type === eslint_devkit_1.AST_NODE_TYPES.Identifier) {
|
|
55
|
+
const propName = arg.property.name.toLowerCase();
|
|
56
|
+
const piiProps = ['email', 'ssn', 'password', 'creditcard', 'phone'];
|
|
57
|
+
if (piiProps.some(p => propName.includes(p))) {
|
|
58
|
+
report(node);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
// Check string literals mentioning PII
|
|
62
|
+
if (arg.type === eslint_devkit_1.AST_NODE_TYPES.Literal && typeof arg.value === 'string') {
|
|
63
|
+
const text = arg.value.toLowerCase();
|
|
64
|
+
if (text.includes('email:') || text.includes('ssn:') || text.includes('password:')) {
|
|
65
|
+
report(node);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
//# sourceMappingURL=index.js.map
|