eslint-plugin-jwt-security 2.3.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.
Files changed (79) hide show
  1. package/AGENTS.md +102 -0
  2. package/CHANGELOG.md +161 -0
  3. package/LICENSE +23 -0
  4. package/README.md +158 -0
  5. package/dist/AGENTS.md +102 -0
  6. package/dist/CHANGELOG.md +161 -0
  7. package/dist/LICENSE +23 -0
  8. package/dist/README.md +158 -0
  9. package/dist/package.json +125 -0
  10. package/dist/src/index.d.ts +101 -0
  11. package/dist/src/index.js +171 -0
  12. package/dist/src/oxlint.d.ts +20 -0
  13. package/dist/src/oxlint.js +21 -0
  14. package/dist/src/rules/no-algorithm-confusion/index.d.ts +24 -0
  15. package/dist/src/rules/no-algorithm-confusion/index.js +165 -0
  16. package/dist/src/rules/no-algorithm-none/index.d.ts +24 -0
  17. package/dist/src/rules/no-algorithm-none/index.js +222 -0
  18. package/dist/src/rules/no-decode-without-verify/index.d.ts +23 -0
  19. package/dist/src/rules/no-decode-without-verify/index.js +131 -0
  20. package/dist/src/rules/no-hardcoded-secret/index.d.ts +23 -0
  21. package/dist/src/rules/no-hardcoded-secret/index.js +189 -0
  22. package/dist/src/rules/no-sensitive-payload/index.d.ts +20 -0
  23. package/dist/src/rules/no-sensitive-payload/index.js +103 -0
  24. package/dist/src/rules/no-timestamp-manipulation/index.d.ts +23 -0
  25. package/dist/src/rules/no-timestamp-manipulation/index.js +97 -0
  26. package/dist/src/rules/no-weak-secret/index.d.ts +23 -0
  27. package/dist/src/rules/no-weak-secret/index.js +165 -0
  28. package/dist/src/rules/require-algorithm-whitelist/index.d.ts +24 -0
  29. package/dist/src/rules/require-algorithm-whitelist/index.js +113 -0
  30. package/dist/src/rules/require-audience-validation/index.d.ts +20 -0
  31. package/dist/src/rules/require-audience-validation/index.js +100 -0
  32. package/dist/src/rules/require-expiration/index.d.ts +23 -0
  33. package/dist/src/rules/require-expiration/index.js +136 -0
  34. package/dist/src/rules/require-issued-at/index.d.ts +23 -0
  35. package/dist/src/rules/require-issued-at/index.js +113 -0
  36. package/dist/src/rules/require-issuer-validation/index.d.ts +20 -0
  37. package/dist/src/rules/require-issuer-validation/index.js +100 -0
  38. package/dist/src/rules/require-max-age/index.d.ts +24 -0
  39. package/dist/src/rules/require-max-age/index.js +93 -0
  40. package/dist/src/types/index.d.ts +82 -0
  41. package/dist/src/types/index.js +7 -0
  42. package/dist/src/utils/index.d.ts +98 -0
  43. package/dist/src/utils/index.js +238 -0
  44. package/package.json +125 -0
  45. package/src/__compatibility__/jwt-interface.spec.ts +192 -0
  46. package/src/index.test.ts +47 -0
  47. package/src/index.ts +209 -0
  48. package/src/oxlint.test.ts +61 -0
  49. package/src/oxlint.ts +23 -0
  50. package/src/rules/no-algorithm-confusion/index.ts +208 -0
  51. package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +106 -0
  52. package/src/rules/no-algorithm-none/index.ts +270 -0
  53. package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +146 -0
  54. package/src/rules/no-decode-without-verify/index.ts +162 -0
  55. package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +76 -0
  56. package/src/rules/no-hardcoded-secret/index.ts +230 -0
  57. package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +97 -0
  58. package/src/rules/no-sensitive-payload/index.ts +123 -0
  59. package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +157 -0
  60. package/src/rules/no-timestamp-manipulation/index.ts +118 -0
  61. package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +54 -0
  62. package/src/rules/no-weak-secret/index.ts +204 -0
  63. package/src/rules/no-weak-secret/no-weak-secret.test.ts +114 -0
  64. package/src/rules/require-algorithm-whitelist/index.ts +145 -0
  65. package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +72 -0
  66. package/src/rules/require-audience-validation/index.ts +120 -0
  67. package/src/rules/require-audience-validation/require-audience-validation.test.ts +68 -0
  68. package/src/rules/require-expiration/index.ts +170 -0
  69. package/src/rules/require-expiration/require-expiration.test.ts +135 -0
  70. package/src/rules/require-issued-at/index.ts +143 -0
  71. package/src/rules/require-issued-at/require-issued-at.test.ts +64 -0
  72. package/src/rules/require-issuer-validation/index.ts +120 -0
  73. package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +68 -0
  74. package/src/rules/require-max-age/index.ts +117 -0
  75. package/src/rules/require-max-age/require-max-age.test.ts +69 -0
  76. package/src/tests/coverage-dual-layer.test.ts +323 -0
  77. package/src/types/index.ts +107 -0
  78. package/src/utils/index.ts +285 -0
  79. package/src/utils/utils.test.ts +324 -0
@@ -0,0 +1,230 @@
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
+ /**
8
+ * ESLint Rule: no-hardcoded-secret
9
+ *
10
+ * Detects hardcoded secrets in JWT sign/verify operations.
11
+ * Hardcoded secrets in source code are a critical security vulnerability.
12
+ *
13
+ * CWE-798: Use of Hard-coded Credentials
14
+ *
15
+ * @see https://cwe.mitre.org/data/definitions/798.html
16
+ */
17
+ import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
18
+ import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
19
+ import {
20
+ isSignOperation,
21
+ isVerifyOperation,
22
+ isEnvVariable,
23
+ } from '../../utils';
24
+ import type { NoHardcodedSecretOptions } from '../../types';
25
+
26
+ type MessageIds =
27
+ | 'hardcodedSecret'
28
+ | 'useEnvVariable';
29
+
30
+ type RuleOptions = [NoHardcodedSecretOptions?];
31
+
32
+ export const noHardcodedSecret = createRule<RuleOptions, MessageIds>({
33
+ name: 'no-hardcoded-secret',
34
+ meta: {
35
+ type: 'problem',
36
+ docs: {
37
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-hardcoded-secret.md',
38
+ description:
39
+ 'Disallow hardcoded secrets in JWT sign/verify operations',
40
+ cwe: 'CWE-798',
41
+ cvss: 9.8,
42
+ },
43
+ fixable: undefined,
44
+ hasSuggestions: false,
45
+ messages: {
46
+ hardcodedSecret: formatLLMMessage({
47
+ icon: MessageIcons.SECURITY,
48
+ issueName: 'Hardcoded JWT Secret',
49
+ cwe: 'CWE-798',
50
+ description:
51
+ 'Secret key hardcoded in source code can be extracted from repositories',
52
+ severity: 'HIGH',
53
+ fix: 'Use process.env.JWT_SECRET or secure secret management',
54
+ documentationLink: 'https://cwe.mitre.org/data/definitions/798.html',
55
+ }),
56
+ useEnvVariable: formatLLMMessage({
57
+ icon: MessageIcons.INFO,
58
+ issueName: 'Use Environment Variable',
59
+ description: 'Replace hardcoded secret with environment variable',
60
+ severity: 'LOW',
61
+ fix: 'process.env.JWT_SECRET',
62
+ documentationLink:
63
+ 'https://12factor.net/config',
64
+ }),
65
+ },
66
+ schema: [
67
+ {
68
+ type: 'object',
69
+ properties: {
70
+ envPatterns: {
71
+ type: 'array',
72
+ items: { type: 'string' },
73
+ default: [],
74
+ description: 'Patterns that indicate safe environment variable usage',
75
+ },
76
+ trustedSanitizers: {
77
+ type: 'array',
78
+ items: { type: 'string' },
79
+ default: [],
80
+ },
81
+ trustedAnnotations: {
82
+ type: 'array',
83
+ items: { type: 'string' },
84
+ default: [],
85
+ },
86
+ strictMode: {
87
+ type: 'boolean',
88
+ default: false,
89
+ },
90
+ },
91
+ additionalProperties: false,
92
+ },
93
+ ],
94
+ },
95
+ defaultOptions: [
96
+ {
97
+ envPatterns: [],
98
+ trustedSanitizers: [],
99
+ trustedAnnotations: [],
100
+ strictMode: false,
101
+ },
102
+ ],
103
+ create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
104
+ /**
105
+ * Check if a node is a hardcoded string literal
106
+ */
107
+ // oxlint-disable-next-line consistent-function-scoping
108
+ const isHardcodedString = (node: TSESTree.Node): boolean => {
109
+ // String literal
110
+ if (node.type === 'Literal' && typeof node.value === 'string') {
111
+ return true;
112
+ }
113
+
114
+ // Template literal without expressions
115
+ if (
116
+ node.type === 'TemplateLiteral' &&
117
+ node.expressions.length === 0
118
+ ) {
119
+ return true;
120
+ }
121
+
122
+ return false;
123
+ };
124
+
125
+ /**
126
+ * Resolve an Identifier node to its initializer (one frame of indirection).
127
+ * Closes the audit FN where `const SECRET = 'x'; jwt.sign(p, SECRET)`
128
+ * was treated as safe — the literal-via-const pattern hides the
129
+ * hardcoded secret behind one declaration. See benchmarks/AUDIT_PATTERNS.md
130
+ * §3.2 ("Indirection through one level of variable").
131
+ */
132
+ const resolveConstLiteralValue = (
133
+ node: TSESTree.Node,
134
+ ): TSESTree.Node | null => {
135
+ if (node.type !== 'Identifier') return null;
136
+ const scope = context.sourceCode.getScope?.(node) ?? null;
137
+ if (!scope) return null;
138
+ const variable = scope.references.find(
139
+ (r) => r.identifier === node,
140
+ )?.resolved;
141
+ const def = variable?.defs[0];
142
+ if (!def || def.type !== 'Variable') return null;
143
+ const decl = def.parent;
144
+ if (decl?.type !== 'VariableDeclaration' || decl.kind !== 'const') return null;
145
+ return def.node.init ?? null;
146
+ };
147
+
148
+ /**
149
+ * Check if node is a safe key source
150
+ */
151
+ const isSafeKeySource = (node: TSESTree.Node): boolean => {
152
+ // Environment variable
153
+ if (isEnvVariable(node)) {
154
+ return true;
155
+ }
156
+
157
+ // Function call (getSecret(), loadKey(), etc.)
158
+ if (node.type === 'CallExpression') {
159
+ return true;
160
+ }
161
+
162
+ // await expression (async key loading)
163
+ if (node.type === 'AwaitExpression') {
164
+ return true;
165
+ }
166
+
167
+ // Variable reference: resolve `const X = ...` one level. If X is a
168
+ // hardcoded literal, treat as unsafe — fall through to the
169
+ // hardcoded check. Otherwise (env var, call, identifier chain)
170
+ // it's safe.
171
+ if (node.type === 'Identifier') {
172
+ const init = resolveConstLiteralValue(node);
173
+ if (init && isHardcodedString(init)) {
174
+ // Unsafe — let the caller's `isHardcodedString` (with the
175
+ // resolved init) flag it.
176
+ return false;
177
+ }
178
+ return true;
179
+ }
180
+
181
+ // Member expression but not literal (config.secret, etc.)
182
+ if (node.type === 'MemberExpression' && !isHardcodedString(node)) {
183
+ return true;
184
+ }
185
+
186
+ return false;
187
+ };
188
+
189
+ /**
190
+ * Like isHardcodedString but resolves single-level `const X = '...'`
191
+ * indirection so the caller fires on `const SECRET = 'x'; jwt.sign(p, SECRET)`.
192
+ */
193
+ const isHardcodedStringOrResolvedConst = (node: TSESTree.Node): boolean => {
194
+ if (isHardcodedString(node)) return true;
195
+ const init = resolveConstLiteralValue(node);
196
+ return init ? isHardcodedString(init) : false;
197
+ };
198
+
199
+ return {
200
+ CallExpression(node: TSESTree.CallExpression) {
201
+ // Check both sign and verify operations
202
+ if (!isSignOperation(node) && !isVerifyOperation(node)) {
203
+ return;
204
+ }
205
+
206
+ // Secret/key is the second argument
207
+ if (node.arguments.length < 2) {
208
+ return;
209
+ }
210
+
211
+ const secretArg = node.arguments[1];
212
+
213
+ // Skip if safe source
214
+ if (isSafeKeySource(secretArg)) {
215
+ return;
216
+ }
217
+
218
+ // Flag hardcoded strings (also follows single-frame `const X = '...'`).
219
+ if (isHardcodedStringOrResolvedConst(secretArg)) {
220
+ context.report({
221
+ node: secretArg,
222
+ messageId: 'hardcodedSecret',
223
+ });
224
+ }
225
+ },
226
+ };
227
+ },
228
+ });
229
+
230
+ export default noHardcodedSecret;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * Tests for no-hardcoded-secret rule
3
+ * Security: CWE-798 (Hardcoded Credentials)
4
+ */
5
+ import { RuleTester } from '@typescript-eslint/rule-tester';
6
+ import { describe, it, afterAll } from 'vitest';
7
+ import parser from '@typescript-eslint/parser';
8
+ import { noHardcodedSecret } from './index';
9
+
10
+ RuleTester.afterAll = afterAll;
11
+ RuleTester.it = it;
12
+ RuleTester.itOnly = it.only;
13
+ RuleTester.describe = describe;
14
+
15
+ const ruleTester = new RuleTester({
16
+ languageOptions: {
17
+ parser,
18
+ ecmaVersion: 2022,
19
+ sourceType: 'module',
20
+ },
21
+ });
22
+
23
+ describe('no-hardcoded-secret', () => {
24
+ describe('Valid Code - Safe Key Sources', () => {
25
+ ruleTester.run('valid - environment and config', noHardcodedSecret, {
26
+ valid: [
27
+ // Environment variable
28
+ { code: `jwt.sign(payload, process.env.JWT_SECRET);` },
29
+ { code: `jwt.verify(token, process.env.JWT_SECRET);` },
30
+ // Variable reference
31
+ { code: `jwt.sign(payload, secret);` },
32
+ { code: `jwt.verify(token, privateKey);` },
33
+ // Function call
34
+ { code: `jwt.sign(payload, getSecretKey());` },
35
+ { code: `jwt.verify(token, await loadPublicKey());` },
36
+ // Config object
37
+ { code: `jwt.sign(payload, config.jwtSecret);` },
38
+ { code: `jwt.verify(token, settings.publicKey);` },
39
+ // Async key loading
40
+ { code: `jwt.verify(token, await importJWK(jwk));` },
41
+ // crypto operations
42
+ { code: `jwt.sign(payload, crypto.randomBytes(32));` },
43
+ // Only 1 argument - edge case (line 156 coverage)
44
+ { code: `jwt.sign(payload);` },
45
+ { code: `jwt.verify(token);` },
46
+ // MemberExpression that's not hardcoded
47
+ { code: `jwt.sign(payload, keys.current);` },
48
+ // signJWT with env var
49
+ { code: `signJWT(payload, process.env.SECRET);` },
50
+ ],
51
+ invalid: [],
52
+ });
53
+ });
54
+
55
+ describe('Invalid Code - Hardcoded Secrets', () => {
56
+ ruleTester.run('invalid - hardcoded strings', noHardcodedSecret, {
57
+ valid: [],
58
+ invalid: [
59
+ // String literal
60
+ {
61
+ code: `jwt.sign(payload, 'my-secret-key');`,
62
+ errors: [{ messageId: 'hardcodedSecret' }],
63
+ },
64
+ // Long string (still hardcoded)
65
+ {
66
+ code: `jwt.sign(payload, 'this-is-a-very-long-secret-but-still-hardcoded');`,
67
+ errors: [{ messageId: 'hardcodedSecret' }],
68
+ },
69
+ // Template literal without variables
70
+ {
71
+ code: 'jwt.sign(payload, `my-secret-key`);',
72
+ errors: [{ messageId: 'hardcodedSecret' }],
73
+ },
74
+ // Verify with hardcoded
75
+ {
76
+ code: `jwt.verify(token, 'hardcoded-public-key');`,
77
+ errors: [{ messageId: 'hardcodedSecret' }],
78
+ },
79
+ // PEM key inline
80
+ {
81
+ code: `jwt.verify(token, '-----BEGIN PUBLIC KEY-----\\\\nMIIBIj...');`,
82
+ errors: [{ messageId: 'hardcodedSecret' }],
83
+ },
84
+ // signJWT with hardcoded
85
+ {
86
+ code: `signJWT(payload, 'hardcoded');`,
87
+ errors: [{ messageId: 'hardcodedSecret' }],
88
+ },
89
+ // sign() direct call with hardcoded
90
+ {
91
+ code: `sign(payload, 'secret123');`,
92
+ errors: [{ messageId: 'hardcodedSecret' }],
93
+ },
94
+ ],
95
+ });
96
+ });
97
+ });
@@ -0,0 +1,123 @@
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
+ /**
8
+ * ESLint Rule: no-sensitive-payload
9
+ *
10
+ * Detects storage of sensitive data in JWT payload.
11
+ * JWT payloads are only base64-encoded, not encrypted.
12
+ *
13
+ * CWE-359: Exposure of Private Personal Information
14
+ */
15
+ import type { TSESLint, TSESTree } from '@interlace/eslint-devkit';
16
+ import { createRule, formatLLMMessage, MessageIcons } from '@interlace/eslint-devkit';
17
+ import { isSignOperation, SENSITIVE_PAYLOAD_FIELDS } from '../../utils';
18
+ import type { NoSensitivePayloadOptions } from '../../types';
19
+
20
+ type MessageIds = 'sensitivePayloadField';
21
+
22
+ type RuleOptions = [NoSensitivePayloadOptions?];
23
+
24
+ export const noSensitivePayload = createRule<RuleOptions, MessageIds>({
25
+ name: 'no-sensitive-payload',
26
+ meta: {
27
+ type: 'problem',
28
+ docs: {
29
+ url: 'https://github.com/ofri-peretz/eslint/blob/main/packages/eslint-plugin-jwt-security/docs/rules/no-sensitive-payload.md',
30
+ description:
31
+ 'Prevent storing sensitive data in JWT payload which is only base64-encoded',
32
+ cwe: 'CWE-359',
33
+ cvss: 5,
34
+ },
35
+ fixable: undefined,
36
+ hasSuggestions: false,
37
+ messages: {
38
+ sensitivePayloadField: formatLLMMessage({
39
+ icon: MessageIcons.SECURITY,
40
+ issueName: 'Sensitive Data in JWT Payload',
41
+ cwe: 'CWE-359',
42
+ description:
43
+ 'JWT payloads are not encrypted - sensitive data like "{{fieldName}}" can be read by anyone',
44
+ severity: 'MEDIUM',
45
+ fix: 'Store sensitive data server-side, reference by ID in token',
46
+ documentationLink: 'https://tools.ietf.org/html/rfc8725',
47
+ }),
48
+ },
49
+ schema: [
50
+ {
51
+ type: 'object',
52
+ properties: {
53
+ additionalSensitiveFields: {
54
+ type: 'array',
55
+ items: { type: 'string' },
56
+ default: [],
57
+ description: 'Additional field names to flag as sensitive',
58
+ },
59
+ trustedSanitizers: {
60
+ type: 'array',
61
+ items: { type: 'string' },
62
+ default: [],
63
+ },
64
+ trustedAnnotations: {
65
+ type: 'array',
66
+ items: { type: 'string' },
67
+ default: [],
68
+ },
69
+ strictMode: {
70
+ type: 'boolean',
71
+ default: false,
72
+ },
73
+ },
74
+ additionalProperties: false,
75
+ },
76
+ ],
77
+ },
78
+ defaultOptions: [
79
+ {
80
+ additionalSensitiveFields: [],
81
+ trustedSanitizers: [],
82
+ trustedAnnotations: [],
83
+ strictMode: false,
84
+ },
85
+ ],
86
+ create(context: TSESLint.RuleContext<MessageIds, RuleOptions>) {
87
+ const options = context.options[0] ?? {};
88
+ const additionalFields = options.additionalSensitiveFields ?? [];
89
+ const allSensitiveFields = new Set([...SENSITIVE_PAYLOAD_FIELDS, ...additionalFields]);
90
+
91
+ return {
92
+ CallExpression(node: TSESTree.CallExpression) {
93
+ if (!isSignOperation(node)) {
94
+ return;
95
+ }
96
+
97
+ if (node.arguments.length < 1) {
98
+ return;
99
+ }
100
+
101
+ const payloadArg = node.arguments[0];
102
+
103
+ // Check object literal payload
104
+ if (payloadArg.type === 'ObjectExpression') {
105
+ for (const prop of payloadArg.properties) {
106
+ if (prop.type === 'Property' && prop.key.type === 'Identifier') {
107
+ const fieldName = prop.key.name.toLowerCase();
108
+ if (allSensitiveFields.has(fieldName)) {
109
+ context.report({
110
+ node: prop,
111
+ messageId: 'sensitivePayloadField',
112
+ data: { fieldName: prop.key.name },
113
+ });
114
+ }
115
+ }
116
+ }
117
+ }
118
+ },
119
+ };
120
+ },
121
+ });
122
+
123
+ export default noSensitivePayload;
@@ -0,0 +1,157 @@
1
+ /**
2
+ * Tests for no-sensitive-payload rule
3
+ */
4
+ import { RuleTester } from '@typescript-eslint/rule-tester';
5
+ import { describe, it, afterAll } from 'vitest';
6
+ import parser from '@typescript-eslint/parser';
7
+ import { noSensitivePayload } from './index';
8
+
9
+ RuleTester.afterAll = afterAll;
10
+ RuleTester.it = it;
11
+ RuleTester.itOnly = it.only;
12
+ RuleTester.describe = describe;
13
+
14
+ const ruleTester = new RuleTester({
15
+ languageOptions: {
16
+ parser,
17
+ ecmaVersion: 2022,
18
+ sourceType: 'module',
19
+ },
20
+ });
21
+
22
+ describe('no-sensitive-payload', () => {
23
+ describe('Valid Code - Safe Payloads', () => {
24
+ ruleTester.run('valid - standard claims', noSensitivePayload, {
25
+ valid: [
26
+ // Standard JWT claims
27
+ { code: `jwt.sign({ sub: 'user123', role: 'admin' }, secret);` },
28
+ { code: `jwt.sign({ userId: '123', permissions: [] }, secret);` },
29
+ { code: `jwt.sign({ sub: 'user', iss: 'auth', aud: 'api', exp: 123 }, secret);` },
30
+ // Variable reference (cannot analyze)
31
+ { code: `jwt.sign(payload, secret);` },
32
+ // Verify operation (not checked)
33
+ { code: `jwt.verify(token, secret);` },
34
+ // No arguments
35
+ { code: `jwt.sign();` },
36
+ // Non-JWT sign function
37
+ { code: `sign(payload, secret);` },
38
+ // Similar but non-sensitive names
39
+ { code: `jwt.sign({ passwordResetToken: false }, secret);` },
40
+ { code: `jwt.sign({ emailVerified: true }, secret);` },
41
+ { code: `jwt.sign({ phoneVerified: true }, secret);` },
42
+ ],
43
+ invalid: [],
44
+ });
45
+ });
46
+
47
+ describe('Invalid Code - Password Fields', () => {
48
+ ruleTester.run('invalid - password variations', noSensitivePayload, {
49
+ valid: [],
50
+ invalid: [
51
+ { code: `jwt.sign({ password: 'secret123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
52
+ { code: `jwt.sign({ passwd: 'abc' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
53
+ { code: `jwt.sign({ pwd: '123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
54
+ { code: `jwt.sign({ pass: 'xyz' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
55
+ ],
56
+ });
57
+ });
58
+
59
+ describe('Invalid Code - PII Fields', () => {
60
+ ruleTester.run('invalid - personal identifiable information', noSensitivePayload, {
61
+ valid: [],
62
+ invalid: [
63
+ // Email
64
+ { code: `jwt.sign({ email: 'user@example.com' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
65
+ { code: `jwt.sign({ emailAddress: 'user@test.com' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
66
+ // Phone
67
+ { code: `jwt.sign({ phone: '555-1234' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
68
+ { code: `jwt.sign({ phoneNumber: '1234567890' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
69
+ // SSN
70
+ { code: `jwt.sign({ ssn: '123-45-6789' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
71
+ // Address
72
+ { code: `jwt.sign({ address: '123 Main St' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
73
+ // DOB
74
+ { code: `jwt.sign({ dob: '1990-01-01' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
75
+ { code: `jwt.sign({ dateOfBirth: '1990-01-01' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
76
+ ],
77
+ });
78
+ });
79
+
80
+ describe('Invalid Code - Financial Fields', () => {
81
+ ruleTester.run('invalid - financial data', noSensitivePayload, {
82
+ valid: [],
83
+ invalid: [
84
+ // Credit card (camelCase)
85
+ { code: `jwt.sign({ creditCard: '4111111111111111' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
86
+ // Credit card (snake_case)
87
+ { code: `jwt.sign({ credit_card: '4111111111111111' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
88
+ // Card number
89
+ { code: `jwt.sign({ cardNumber: '1234' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
90
+ // CVV
91
+ { code: `jwt.sign({ cvv: '123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
92
+ { code: `jwt.sign({ cvc: '456' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
93
+ // PIN
94
+ { code: `jwt.sign({ pin: '1234' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
95
+ // Bank account
96
+ { code: `jwt.sign({ bankAccount: '123456' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
97
+ { code: `jwt.sign({ accountNumber: '789' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
98
+ { code: `jwt.sign({ routingNumber: '111' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
99
+ ],
100
+ });
101
+ });
102
+
103
+ describe('Invalid Code - API Keys and Secrets', () => {
104
+ ruleTester.run('invalid - secrets and keys', noSensitivePayload, {
105
+ valid: [],
106
+ invalid: [
107
+ // API key variations
108
+ { code: `jwt.sign({ apiKey: 'sk_live_123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
109
+ { code: `jwt.sign({ api_key: 'sk_live_123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
110
+ // Secret
111
+ { code: `jwt.sign({ secret: 'abc123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
112
+ // Token fields
113
+ { code: `jwt.sign({ accessToken: 'abc123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
114
+ { code: `jwt.sign({ refreshToken: 'xyz789' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
115
+ { code: `jwt.sign({ bearerToken: 'bearer123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
116
+ // Private key
117
+ { code: `jwt.sign({ privateKey: '-----BEGIN RSA' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
118
+ { code: `jwt.sign({ secretKey: 'key123' }, secret);`, errors: [{ messageId: 'sensitivePayloadField' }] },
119
+ ],
120
+ });
121
+ });
122
+
123
+ describe('Invalid Code - Multiple Sensitive Fields', () => {
124
+ ruleTester.run('invalid - multiple violations', noSensitivePayload, {
125
+ valid: [],
126
+ invalid: [
127
+ // Two sensitive fields - should report both
128
+ {
129
+ code: `jwt.sign({ email: 'user@test.com', phone: '555-1234' }, secret);`,
130
+ errors: [
131
+ { messageId: 'sensitivePayloadField' },
132
+ { messageId: 'sensitivePayloadField' },
133
+ ],
134
+ },
135
+ // Mixed valid and invalid
136
+ {
137
+ code: `jwt.sign({ sub: 'user', password: 'secret' }, secret);`,
138
+ errors: [{ messageId: 'sensitivePayloadField' }],
139
+ },
140
+ ],
141
+ });
142
+ });
143
+
144
+ describe('Edge Cases - Custom Configuration', () => {
145
+ ruleTester.run('custom sensitive fields', noSensitivePayload, {
146
+ valid: [],
147
+ invalid: [
148
+ // Test additionalSensitiveFields option
149
+ {
150
+ code: `jwt.sign({ customSecret: 'value' }, secret);`,
151
+ options: [{ additionalSensitiveFields: ['customsecret'] }],
152
+ errors: [{ messageId: 'sensitivePayloadField' }],
153
+ },
154
+ ],
155
+ });
156
+ });
157
+ });