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.
- package/AGENTS.md +102 -0
- package/CHANGELOG.md +161 -0
- package/LICENSE +23 -0
- package/README.md +158 -0
- package/dist/AGENTS.md +102 -0
- package/dist/CHANGELOG.md +161 -0
- package/dist/LICENSE +23 -0
- package/dist/README.md +158 -0
- package/dist/package.json +125 -0
- package/dist/src/index.d.ts +101 -0
- package/dist/src/index.js +171 -0
- package/dist/src/oxlint.d.ts +20 -0
- package/dist/src/oxlint.js +21 -0
- package/dist/src/rules/no-algorithm-confusion/index.d.ts +24 -0
- package/dist/src/rules/no-algorithm-confusion/index.js +165 -0
- package/dist/src/rules/no-algorithm-none/index.d.ts +24 -0
- package/dist/src/rules/no-algorithm-none/index.js +222 -0
- package/dist/src/rules/no-decode-without-verify/index.d.ts +23 -0
- package/dist/src/rules/no-decode-without-verify/index.js +131 -0
- package/dist/src/rules/no-hardcoded-secret/index.d.ts +23 -0
- package/dist/src/rules/no-hardcoded-secret/index.js +189 -0
- package/dist/src/rules/no-sensitive-payload/index.d.ts +20 -0
- package/dist/src/rules/no-sensitive-payload/index.js +103 -0
- package/dist/src/rules/no-timestamp-manipulation/index.d.ts +23 -0
- package/dist/src/rules/no-timestamp-manipulation/index.js +97 -0
- package/dist/src/rules/no-weak-secret/index.d.ts +23 -0
- package/dist/src/rules/no-weak-secret/index.js +165 -0
- package/dist/src/rules/require-algorithm-whitelist/index.d.ts +24 -0
- package/dist/src/rules/require-algorithm-whitelist/index.js +113 -0
- package/dist/src/rules/require-audience-validation/index.d.ts +20 -0
- package/dist/src/rules/require-audience-validation/index.js +100 -0
- package/dist/src/rules/require-expiration/index.d.ts +23 -0
- package/dist/src/rules/require-expiration/index.js +136 -0
- package/dist/src/rules/require-issued-at/index.d.ts +23 -0
- package/dist/src/rules/require-issued-at/index.js +113 -0
- package/dist/src/rules/require-issuer-validation/index.d.ts +20 -0
- package/dist/src/rules/require-issuer-validation/index.js +100 -0
- package/dist/src/rules/require-max-age/index.d.ts +24 -0
- package/dist/src/rules/require-max-age/index.js +93 -0
- package/dist/src/types/index.d.ts +82 -0
- package/dist/src/types/index.js +7 -0
- package/dist/src/utils/index.d.ts +98 -0
- package/dist/src/utils/index.js +238 -0
- package/package.json +125 -0
- package/src/__compatibility__/jwt-interface.spec.ts +192 -0
- package/src/index.test.ts +47 -0
- package/src/index.ts +209 -0
- package/src/oxlint.test.ts +61 -0
- package/src/oxlint.ts +23 -0
- package/src/rules/no-algorithm-confusion/index.ts +208 -0
- package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +106 -0
- package/src/rules/no-algorithm-none/index.ts +270 -0
- package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +146 -0
- package/src/rules/no-decode-without-verify/index.ts +162 -0
- package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +76 -0
- package/src/rules/no-hardcoded-secret/index.ts +230 -0
- package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +97 -0
- package/src/rules/no-sensitive-payload/index.ts +123 -0
- package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +157 -0
- package/src/rules/no-timestamp-manipulation/index.ts +118 -0
- package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +54 -0
- package/src/rules/no-weak-secret/index.ts +204 -0
- package/src/rules/no-weak-secret/no-weak-secret.test.ts +114 -0
- package/src/rules/require-algorithm-whitelist/index.ts +145 -0
- package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +72 -0
- package/src/rules/require-audience-validation/index.ts +120 -0
- package/src/rules/require-audience-validation/require-audience-validation.test.ts +68 -0
- package/src/rules/require-expiration/index.ts +170 -0
- package/src/rules/require-expiration/require-expiration.test.ts +135 -0
- package/src/rules/require-issued-at/index.ts +143 -0
- package/src/rules/require-issued-at/require-issued-at.test.ts +64 -0
- package/src/rules/require-issuer-validation/index.ts +120 -0
- package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +68 -0
- package/src/rules/require-max-age/index.ts +117 -0
- package/src/rules/require-max-age/require-max-age.test.ts +69 -0
- package/src/tests/coverage-dual-layer.test.ts +323 -0
- package/src/types/index.ts +107 -0
- package/src/utils/index.ts +285 -0
- package/src/utils/utils.test.ts +324 -0
|
@@ -0,0 +1,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for JWT utilities
|
|
3
|
+
*/
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import {
|
|
6
|
+
isJwtLibraryCall,
|
|
7
|
+
isStringLiteral,
|
|
8
|
+
extractAlgorithms,
|
|
9
|
+
hasOption,
|
|
10
|
+
getOptionValue,
|
|
11
|
+
isWeakSecret,
|
|
12
|
+
isEnvVariable,
|
|
13
|
+
isSignOperation,
|
|
14
|
+
isVerifyOperation,
|
|
15
|
+
isDecodeOperation,
|
|
16
|
+
getOptionsArgument,
|
|
17
|
+
isTestFile,
|
|
18
|
+
SENSITIVE_PAYLOAD_FIELDS,
|
|
19
|
+
SYMMETRIC_ALGORITHMS,
|
|
20
|
+
SECURE_ALGORITHMS,
|
|
21
|
+
INSECURE_ALGORITHMS,
|
|
22
|
+
JWT_LIBRARIES,
|
|
23
|
+
} from './index';
|
|
24
|
+
import type { TSESTree } from '@interlace/eslint-devkit';
|
|
25
|
+
|
|
26
|
+
// Helper to create mock nodes
|
|
27
|
+
function mockCallExpression(calleeName: string): TSESTree.CallExpression {
|
|
28
|
+
return {
|
|
29
|
+
type: 'CallExpression',
|
|
30
|
+
callee: { type: 'Identifier', name: calleeName } as TSESTree.Identifier,
|
|
31
|
+
arguments: [],
|
|
32
|
+
} as unknown as TSESTree.CallExpression;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function mockMemberCallExpression(objectName: string, methodName: string): TSESTree.CallExpression {
|
|
36
|
+
return {
|
|
37
|
+
type: 'CallExpression',
|
|
38
|
+
callee: {
|
|
39
|
+
type: 'MemberExpression',
|
|
40
|
+
object: { type: 'Identifier', name: objectName } as TSESTree.Identifier,
|
|
41
|
+
property: { type: 'Identifier', name: methodName } as TSESTree.Identifier,
|
|
42
|
+
},
|
|
43
|
+
arguments: [],
|
|
44
|
+
} as unknown as TSESTree.CallExpression;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function mockLiteral(value: string | number | boolean): TSESTree.Literal {
|
|
48
|
+
return {
|
|
49
|
+
type: 'Literal',
|
|
50
|
+
value,
|
|
51
|
+
} as TSESTree.Literal;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function mockObjectExpression(props: Record<string, TSESTree.Node>): TSESTree.ObjectExpression {
|
|
55
|
+
return {
|
|
56
|
+
type: 'ObjectExpression',
|
|
57
|
+
properties: Object.entries(props).map(([key, value]) => ({
|
|
58
|
+
type: 'Property',
|
|
59
|
+
key: { type: 'Identifier', name: key } as TSESTree.Identifier,
|
|
60
|
+
value,
|
|
61
|
+
})),
|
|
62
|
+
} as unknown as TSESTree.ObjectExpression;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('JWT Utils', () => {
|
|
66
|
+
describe('isJwtLibraryCall', () => {
|
|
67
|
+
it('should detect direct calls', () => {
|
|
68
|
+
const call = mockCallExpression('verify');
|
|
69
|
+
expect(isJwtLibraryCall(call, new Set(['verify']))).toBe(true);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should detect member calls', () => {
|
|
73
|
+
const call = mockMemberCallExpression('jwt', 'verify');
|
|
74
|
+
expect(isJwtLibraryCall(call, new Set(['verify']))).toBe(true);
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should return false for non-matching calls', () => {
|
|
78
|
+
const call = mockCallExpression('somethingElse');
|
|
79
|
+
expect(isJwtLibraryCall(call, new Set(['verify']))).toBe(false);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it('should return false for non-identifier/member callee (line 117)', () => {
|
|
83
|
+
// This covers the `return false` at line 117 for CallExpression, ArrowFunction, etc.
|
|
84
|
+
const call = {
|
|
85
|
+
type: 'CallExpression',
|
|
86
|
+
callee: { type: 'ArrowFunctionExpression' }, // Not Identifier or MemberExpression
|
|
87
|
+
arguments: [],
|
|
88
|
+
} as unknown as TSESTree.CallExpression;
|
|
89
|
+
expect(isJwtLibraryCall(call, new Set(['verify']))).toBe(false);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should return false for member with non-identifier property', () => {
|
|
93
|
+
const call = {
|
|
94
|
+
type: 'CallExpression',
|
|
95
|
+
callee: {
|
|
96
|
+
type: 'MemberExpression',
|
|
97
|
+
object: { type: 'Identifier', name: 'jwt' },
|
|
98
|
+
property: { type: 'Literal', value: 'verify' }, // Literal, not Identifier
|
|
99
|
+
},
|
|
100
|
+
arguments: [],
|
|
101
|
+
} as unknown as TSESTree.CallExpression;
|
|
102
|
+
expect(isJwtLibraryCall(call, new Set(['verify']))).toBe(false);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
describe('isStringLiteral', () => {
|
|
107
|
+
it('should return true for string literals', () => {
|
|
108
|
+
const literal = mockLiteral('hello');
|
|
109
|
+
expect(isStringLiteral(literal)).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should return true when value matches', () => {
|
|
113
|
+
const literal = mockLiteral('test');
|
|
114
|
+
expect(isStringLiteral(literal, 'test')).toBe(true);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should return false when value does not match', () => {
|
|
118
|
+
const literal = mockLiteral('test');
|
|
119
|
+
expect(isStringLiteral(literal, 'other')).toBe(false);
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should return false for non-string literals', () => {
|
|
123
|
+
const literal = mockLiteral(123);
|
|
124
|
+
expect(isStringLiteral(literal)).toBe(false);
|
|
125
|
+
});
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
describe('extractAlgorithms', () => {
|
|
129
|
+
it('should extract single algorithm', () => {
|
|
130
|
+
const obj = mockObjectExpression({
|
|
131
|
+
algorithm: mockLiteral('HS256'),
|
|
132
|
+
});
|
|
133
|
+
expect(extractAlgorithms(obj)).toEqual(['HS256']);
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it('should extract algorithms from array', () => {
|
|
137
|
+
const obj = {
|
|
138
|
+
type: 'ObjectExpression',
|
|
139
|
+
properties: [{
|
|
140
|
+
type: 'Property',
|
|
141
|
+
key: { type: 'Identifier', name: 'algorithms' },
|
|
142
|
+
value: {
|
|
143
|
+
type: 'ArrayExpression',
|
|
144
|
+
elements: [mockLiteral('RS256'), mockLiteral('ES256')],
|
|
145
|
+
},
|
|
146
|
+
}],
|
|
147
|
+
} as unknown as TSESTree.ObjectExpression;
|
|
148
|
+
expect(extractAlgorithms(obj)).toEqual(['RS256', 'ES256']);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return empty array for no algorithms', () => {
|
|
152
|
+
const obj = mockObjectExpression({ other: mockLiteral('value') });
|
|
153
|
+
expect(extractAlgorithms(obj)).toEqual([]);
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
it('should handle alg key', () => {
|
|
157
|
+
const obj = mockObjectExpression({ alg: mockLiteral('ES512') });
|
|
158
|
+
expect(extractAlgorithms(obj)).toEqual(['ES512']);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should skip SpreadElement properties (line 143)', () => {
|
|
162
|
+
// This covers the `continue` at line 143 for non-Property types
|
|
163
|
+
const obj = {
|
|
164
|
+
type: 'ObjectExpression',
|
|
165
|
+
properties: [
|
|
166
|
+
{ type: 'SpreadElement', argument: { type: 'Identifier', name: 'opts' } },
|
|
167
|
+
{
|
|
168
|
+
type: 'Property',
|
|
169
|
+
key: { type: 'Identifier', name: 'algorithm' },
|
|
170
|
+
value: mockLiteral('RS256'),
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
} as unknown as TSESTree.ObjectExpression;
|
|
174
|
+
expect(extractAlgorithms(obj)).toEqual(['RS256']);
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should skip properties with non-identifier keys', () => {
|
|
178
|
+
// This also covers line 142 - when key.type !== 'Identifier'
|
|
179
|
+
const obj = {
|
|
180
|
+
type: 'ObjectExpression',
|
|
181
|
+
properties: [{
|
|
182
|
+
type: 'Property',
|
|
183
|
+
key: { type: 'Literal', value: 'algorithm' }, // Literal key, not Identifier
|
|
184
|
+
value: mockLiteral('RS256'),
|
|
185
|
+
}],
|
|
186
|
+
} as unknown as TSESTree.ObjectExpression;
|
|
187
|
+
expect(extractAlgorithms(obj)).toEqual([]);
|
|
188
|
+
});
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
describe('hasOption', () => {
|
|
192
|
+
it('should return true when option exists', () => {
|
|
193
|
+
const obj = mockObjectExpression({ expiresIn: mockLiteral('1h') });
|
|
194
|
+
expect(hasOption(obj, 'expiresIn')).toBe(true);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should return false when option missing', () => {
|
|
198
|
+
const obj = mockObjectExpression({});
|
|
199
|
+
expect(hasOption(obj, 'expiresIn')).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
describe('getOptionValue', () => {
|
|
204
|
+
it('should return option value', () => {
|
|
205
|
+
const value = mockLiteral('1h');
|
|
206
|
+
const obj = mockObjectExpression({ expiresIn: value });
|
|
207
|
+
expect(getOptionValue(obj, 'expiresIn')).toBe(value);
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('should return undefined for missing option', () => {
|
|
211
|
+
const obj = mockObjectExpression({});
|
|
212
|
+
expect(getOptionValue(obj, 'expiresIn')).toBeUndefined();
|
|
213
|
+
});
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
describe('isWeakSecret', () => {
|
|
217
|
+
it('should return true for short secrets', () => {
|
|
218
|
+
const node = mockLiteral('short');
|
|
219
|
+
expect(isWeakSecret(node, 32)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should return false for long secrets', () => {
|
|
223
|
+
const node = mockLiteral('a'.repeat(64));
|
|
224
|
+
expect(isWeakSecret(node, 32)).toBe(false);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('should return false for non-literals', () => {
|
|
228
|
+
const node = { type: 'Identifier', name: 'secret' } as TSESTree.Identifier;
|
|
229
|
+
expect(isWeakSecret(node)).toBe(false);
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
describe('isEnvVariable', () => {
|
|
234
|
+
it('should return true for process.env.X', () => {
|
|
235
|
+
const node = {
|
|
236
|
+
type: 'MemberExpression',
|
|
237
|
+
object: {
|
|
238
|
+
type: 'MemberExpression',
|
|
239
|
+
object: { type: 'Identifier', name: 'process' },
|
|
240
|
+
property: { type: 'Identifier', name: 'env' },
|
|
241
|
+
},
|
|
242
|
+
property: { type: 'Identifier', name: 'JWT_SECRET' },
|
|
243
|
+
} as unknown as TSESTree.MemberExpression;
|
|
244
|
+
expect(isEnvVariable(node)).toBe(true);
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
it('should return false for other nodes', () => {
|
|
248
|
+
const node = { type: 'Identifier', name: 'secret' } as TSESTree.Identifier;
|
|
249
|
+
expect(isEnvVariable(node)).toBe(false);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
describe('operation checkers', () => {
|
|
254
|
+
it('isSignOperation detects sign calls', () => {
|
|
255
|
+
expect(isSignOperation(mockCallExpression('sign'))).toBe(true);
|
|
256
|
+
expect(isSignOperation(mockCallExpression('other'))).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it('isVerifyOperation detects verify calls', () => {
|
|
260
|
+
expect(isVerifyOperation(mockCallExpression('verify'))).toBe(true);
|
|
261
|
+
expect(isVerifyOperation(mockCallExpression('other'))).toBe(false);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it('isDecodeOperation detects decode calls', () => {
|
|
265
|
+
expect(isDecodeOperation(mockCallExpression('decode'))).toBe(true);
|
|
266
|
+
expect(isDecodeOperation(mockCallExpression('other'))).toBe(false);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
describe('getOptionsArgument', () => {
|
|
271
|
+
it('should return options object at default index', () => {
|
|
272
|
+
const options = mockObjectExpression({});
|
|
273
|
+
const call = {
|
|
274
|
+
...mockCallExpression('verify'),
|
|
275
|
+
arguments: [mockLiteral('token'), mockLiteral('secret'), options],
|
|
276
|
+
} as unknown as TSESTree.CallExpression;
|
|
277
|
+
expect(getOptionsArgument(call)).toBe(options);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('should return undefined if no options', () => {
|
|
281
|
+
const call = mockCallExpression('verify');
|
|
282
|
+
expect(getOptionsArgument(call)).toBeUndefined();
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
|
|
286
|
+
describe('isTestFile', () => {
|
|
287
|
+
it('should return true for test files', () => {
|
|
288
|
+
expect(isTestFile('file.test.ts')).toBe(true);
|
|
289
|
+
expect(isTestFile('file.spec.js')).toBe(true);
|
|
290
|
+
expect(isTestFile('/path/__tests__/file.ts')).toBe(true);
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should return false for non-test files', () => {
|
|
294
|
+
expect(isTestFile('index.ts')).toBe(false);
|
|
295
|
+
expect(isTestFile('utils.js')).toBe(false);
|
|
296
|
+
});
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
describe('constants', () => {
|
|
300
|
+
it('should have sensitive fields', () => {
|
|
301
|
+
expect(SENSITIVE_PAYLOAD_FIELDS.has('password')).toBe(true);
|
|
302
|
+
expect(SENSITIVE_PAYLOAD_FIELDS.has('ssn')).toBe(true);
|
|
303
|
+
expect(SENSITIVE_PAYLOAD_FIELDS.has('api_key')).toBe(true);
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
it('should have symmetric algorithms', () => {
|
|
307
|
+
expect(SYMMETRIC_ALGORITHMS.has('HS256')).toBe(true);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should have secure algorithms', () => {
|
|
311
|
+
expect(SECURE_ALGORITHMS.has('RS256')).toBe(true);
|
|
312
|
+
expect(SECURE_ALGORITHMS.has('ES256')).toBe(true);
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('should have insecure algorithms', () => {
|
|
316
|
+
expect(INSECURE_ALGORITHMS.has('none')).toBe(true);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should have JWT libraries', () => {
|
|
320
|
+
expect(JWT_LIBRARIES.JSONWEBTOKEN).toBe('jsonwebtoken');
|
|
321
|
+
expect(JWT_LIBRARIES.JOSE).toBe('jose');
|
|
322
|
+
});
|
|
323
|
+
});
|
|
324
|
+
});
|