eslint-plugin-jwt-security 2.3.0 → 2.3.2
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/README.md +5 -5
- package/package.json +16 -24
- package/src/index.d.ts +17 -0
- package/src/index.js +1 -0
- package/src/oxlint.d.ts +17 -0
- package/src/oxlint.js +1 -0
- package/src/rules/no-algorithm-confusion/index.js +1 -0
- package/src/rules/no-algorithm-none/index.js +1 -0
- package/src/rules/no-decode-without-verify/index.js +1 -0
- package/src/rules/no-hardcoded-secret/index.js +1 -0
- package/src/rules/no-sensitive-payload/index.js +1 -0
- package/src/rules/no-timestamp-manipulation/index.js +1 -0
- package/src/rules/no-weak-secret/index.js +1 -0
- package/src/rules/require-algorithm-whitelist/index.js +1 -0
- package/src/rules/require-audience-validation/index.js +1 -0
- package/src/rules/require-expiration/index.js +1 -0
- package/src/rules/require-issued-at/index.js +1 -0
- package/src/rules/require-issuer-validation/index.js +1 -0
- package/src/rules/require-max-age/index.js +1 -0
- package/src/types/index.js +1 -0
- package/src/utils/index.js +1 -0
- package/AGENTS.md +0 -102
- package/CHANGELOG.md +0 -161
- package/dist/AGENTS.md +0 -102
- package/dist/CHANGELOG.md +0 -161
- package/dist/LICENSE +0 -23
- package/dist/README.md +0 -158
- package/dist/package.json +0 -125
- package/dist/src/index.d.ts +0 -101
- package/dist/src/index.js +0 -171
- package/dist/src/oxlint.d.ts +0 -20
- package/dist/src/oxlint.js +0 -21
- package/dist/src/rules/no-algorithm-confusion/index.d.ts +0 -24
- package/dist/src/rules/no-algorithm-confusion/index.js +0 -165
- package/dist/src/rules/no-algorithm-none/index.d.ts +0 -24
- package/dist/src/rules/no-algorithm-none/index.js +0 -222
- package/dist/src/rules/no-decode-without-verify/index.d.ts +0 -23
- package/dist/src/rules/no-decode-without-verify/index.js +0 -131
- package/dist/src/rules/no-hardcoded-secret/index.d.ts +0 -23
- package/dist/src/rules/no-hardcoded-secret/index.js +0 -189
- package/dist/src/rules/no-sensitive-payload/index.d.ts +0 -20
- package/dist/src/rules/no-sensitive-payload/index.js +0 -103
- package/dist/src/rules/no-timestamp-manipulation/index.d.ts +0 -23
- package/dist/src/rules/no-timestamp-manipulation/index.js +0 -97
- package/dist/src/rules/no-weak-secret/index.d.ts +0 -23
- package/dist/src/rules/no-weak-secret/index.js +0 -165
- package/dist/src/rules/require-algorithm-whitelist/index.d.ts +0 -24
- package/dist/src/rules/require-algorithm-whitelist/index.js +0 -113
- package/dist/src/rules/require-audience-validation/index.d.ts +0 -20
- package/dist/src/rules/require-audience-validation/index.js +0 -100
- package/dist/src/rules/require-expiration/index.d.ts +0 -23
- package/dist/src/rules/require-expiration/index.js +0 -136
- package/dist/src/rules/require-issued-at/index.d.ts +0 -23
- package/dist/src/rules/require-issued-at/index.js +0 -113
- package/dist/src/rules/require-issuer-validation/index.d.ts +0 -20
- package/dist/src/rules/require-issuer-validation/index.js +0 -100
- package/dist/src/rules/require-max-age/index.d.ts +0 -24
- package/dist/src/rules/require-max-age/index.js +0 -93
- package/dist/src/types/index.js +0 -7
- package/dist/src/utils/index.d.ts +0 -98
- package/dist/src/utils/index.js +0 -238
- package/src/__compatibility__/jwt-interface.spec.ts +0 -192
- package/src/index.test.ts +0 -47
- package/src/index.ts +0 -209
- package/src/oxlint.test.ts +0 -61
- package/src/oxlint.ts +0 -23
- package/src/rules/no-algorithm-confusion/index.ts +0 -208
- package/src/rules/no-algorithm-confusion/no-algorithm-confusion.test.ts +0 -106
- package/src/rules/no-algorithm-none/index.ts +0 -270
- package/src/rules/no-algorithm-none/no-algorithm-none.test.ts +0 -146
- package/src/rules/no-decode-without-verify/index.ts +0 -162
- package/src/rules/no-decode-without-verify/no-decode-without-verify.test.ts +0 -76
- package/src/rules/no-hardcoded-secret/index.ts +0 -230
- package/src/rules/no-hardcoded-secret/no-hardcoded-secret.test.ts +0 -97
- package/src/rules/no-sensitive-payload/index.ts +0 -123
- package/src/rules/no-sensitive-payload/no-sensitive-payload.test.ts +0 -157
- package/src/rules/no-timestamp-manipulation/index.ts +0 -118
- package/src/rules/no-timestamp-manipulation/no-timestamp-manipulation.test.ts +0 -54
- package/src/rules/no-weak-secret/index.ts +0 -204
- package/src/rules/no-weak-secret/no-weak-secret.test.ts +0 -114
- package/src/rules/require-algorithm-whitelist/index.ts +0 -145
- package/src/rules/require-algorithm-whitelist/require-algorithm-whitelist.test.ts +0 -72
- package/src/rules/require-audience-validation/index.ts +0 -120
- package/src/rules/require-audience-validation/require-audience-validation.test.ts +0 -68
- package/src/rules/require-expiration/index.ts +0 -170
- package/src/rules/require-expiration/require-expiration.test.ts +0 -135
- package/src/rules/require-issued-at/index.ts +0 -143
- package/src/rules/require-issued-at/require-issued-at.test.ts +0 -64
- package/src/rules/require-issuer-validation/index.ts +0 -120
- package/src/rules/require-issuer-validation/require-issuer-validation.test.ts +0 -68
- package/src/rules/require-max-age/index.ts +0 -117
- package/src/rules/require-max-age/require-max-age.test.ts +0 -69
- package/src/tests/coverage-dual-layer.test.ts +0 -323
- package/src/types/index.ts +0 -107
- package/src/utils/index.ts +0 -285
- package/src/utils/utils.test.ts +0 -324
- /package/{dist/src → src}/types/index.d.ts +0 -0
package/src/utils/index.ts
DELETED
|
@@ -1,285 +0,0 @@
|
|
|
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
|
-
* Shared utilities for JWT security rules
|
|
9
|
-
*
|
|
10
|
-
* Library detection, pattern matching, and common helpers.
|
|
11
|
-
*/
|
|
12
|
-
import type { TSESTree } from '@interlace/eslint-devkit';
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Supported JWT libraries
|
|
16
|
-
*/
|
|
17
|
-
export const JWT_LIBRARIES = {
|
|
18
|
-
JSONWEBTOKEN: 'jsonwebtoken',
|
|
19
|
-
JOSE: 'jose',
|
|
20
|
-
EXPRESS_JWT: 'express-jwt',
|
|
21
|
-
NESTJS_JWT: '@nestjs/jwt',
|
|
22
|
-
JWKS_RSA: 'jwks-rsa',
|
|
23
|
-
JWT_DECODE: 'jwt-decode',
|
|
24
|
-
} as const;
|
|
25
|
-
|
|
26
|
-
export type JwtLibrary = (typeof JWT_LIBRARIES)[keyof typeof JWT_LIBRARIES];
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Insecure algorithms that should be flagged
|
|
30
|
-
*/
|
|
31
|
-
export const INSECURE_ALGORITHMS = new Set([
|
|
32
|
-
'none',
|
|
33
|
-
'None',
|
|
34
|
-
'NONE',
|
|
35
|
-
'HS256', // Only insecure when used with public keys
|
|
36
|
-
'HS384',
|
|
37
|
-
'HS512',
|
|
38
|
-
]);
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* Algorithms vulnerable to confusion attacks when used with asymmetric keys
|
|
42
|
-
*/
|
|
43
|
-
export const SYMMETRIC_ALGORITHMS = new Set(['HS256', 'HS384', 'HS512']);
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Recommended secure algorithms
|
|
47
|
-
*/
|
|
48
|
-
export const SECURE_ALGORITHMS = new Set([
|
|
49
|
-
'RS256',
|
|
50
|
-
'RS384',
|
|
51
|
-
'RS512',
|
|
52
|
-
'ES256',
|
|
53
|
-
'ES384',
|
|
54
|
-
'ES512',
|
|
55
|
-
'PS256',
|
|
56
|
-
'PS384',
|
|
57
|
-
'PS512',
|
|
58
|
-
'EdDSA',
|
|
59
|
-
]);
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Sensitive field names that should not be in JWT payload
|
|
63
|
-
* All lowercase for case-insensitive matching
|
|
64
|
-
*/
|
|
65
|
-
export const SENSITIVE_PAYLOAD_FIELDS = new Set([
|
|
66
|
-
// Passwords
|
|
67
|
-
'password', 'passwd', 'pwd', 'pass', 'secret',
|
|
68
|
-
// API Keys
|
|
69
|
-
'apikey', 'api_key', 'api-key',
|
|
70
|
-
'apisecret', 'api_secret', 'api-secret',
|
|
71
|
-
// Tokens
|
|
72
|
-
'accesstoken', 'access_token', 'access-token',
|
|
73
|
-
'refreshtoken', 'refresh_token', 'refresh-token',
|
|
74
|
-
'bearertoken', 'bearer_token', 'bearer-token',
|
|
75
|
-
// Keys
|
|
76
|
-
'privatekey', 'private_key', 'private-key',
|
|
77
|
-
'secretkey', 'secret_key', 'secret-key',
|
|
78
|
-
// PII - Personal Identifiable Information
|
|
79
|
-
'email', 'emailaddress', 'email_address', 'email-address',
|
|
80
|
-
'phone', 'phonenumber', 'phone_number', 'phone-number',
|
|
81
|
-
'ssn', 'socialsecuritynumber', 'social_security_number',
|
|
82
|
-
'dob', 'dateofbirth', 'date_of_birth', 'birthdate',
|
|
83
|
-
'address', 'streetaddress', 'street_address',
|
|
84
|
-
// Financial
|
|
85
|
-
'creditcard', 'credit_card', 'credit-card',
|
|
86
|
-
'cardnumber', 'card_number', 'card-number',
|
|
87
|
-
'cvv', 'cvc', 'securitycode', 'security_code',
|
|
88
|
-
'pin', 'pincode', 'pin_code',
|
|
89
|
-
'bankaccount', 'bank_account', 'bank-account',
|
|
90
|
-
'accountnumber', 'account_number', 'account-number',
|
|
91
|
-
'routingnumber', 'routing_number', 'routing-number',
|
|
92
|
-
]);
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* JWT method patterns for different operations
|
|
96
|
-
*/
|
|
97
|
-
export const JWT_METHODS = {
|
|
98
|
-
SIGN: new Set(['sign', 'signJWT', 'SignJWT']),
|
|
99
|
-
VERIFY: new Set(['verify', 'verifyJWT', 'jwtVerify']),
|
|
100
|
-
DECODE: new Set(['decode', 'jwtDecode', 'decodeJWT']),
|
|
101
|
-
} as const;
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Check if a node represents a call to a JWT library method
|
|
105
|
-
*/
|
|
106
|
-
export function isJwtLibraryCall(
|
|
107
|
-
node: TSESTree.CallExpression,
|
|
108
|
-
targetMethods: Set<string>
|
|
109
|
-
): boolean {
|
|
110
|
-
// Check member expression: jwt.verify(), jose.jwtVerify()
|
|
111
|
-
if (node.callee.type === 'MemberExpression') {
|
|
112
|
-
const property = node.callee.property;
|
|
113
|
-
if (property.type === 'Identifier') {
|
|
114
|
-
return targetMethods.has(property.name);
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
// Check direct calls: jwtVerify(), jwtDecode()
|
|
119
|
-
if (node.callee.type === 'Identifier') {
|
|
120
|
-
return targetMethods.has(node.callee.name);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
/**
|
|
127
|
-
* Check if a node is a string literal with a specific value
|
|
128
|
-
*/
|
|
129
|
-
export function isStringLiteral(
|
|
130
|
-
node: TSESTree.Node,
|
|
131
|
-
value?: string
|
|
132
|
-
): node is TSESTree.Literal {
|
|
133
|
-
if (node.type !== 'Literal' || typeof node.value !== 'string') {
|
|
134
|
-
return false;
|
|
135
|
-
}
|
|
136
|
-
return value === undefined || node.value === value;
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
/**
|
|
140
|
-
* Extract algorithm from options object
|
|
141
|
-
*/
|
|
142
|
-
export function extractAlgorithms(
|
|
143
|
-
optionsNode: TSESTree.ObjectExpression
|
|
144
|
-
): string[] {
|
|
145
|
-
const algorithms: string[] = [];
|
|
146
|
-
|
|
147
|
-
for (const prop of optionsNode.properties) {
|
|
148
|
-
if (prop.type !== 'Property' || prop.key.type !== 'Identifier') {
|
|
149
|
-
continue;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const keyName = prop.key.name;
|
|
153
|
-
if (keyName !== 'algorithms' && keyName !== 'algorithm' && keyName !== 'alg') {
|
|
154
|
-
continue;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
// Single algorithm: { algorithm: 'HS256' }
|
|
158
|
-
if (prop.value.type === 'Literal' && typeof prop.value.value === 'string') {
|
|
159
|
-
algorithms.push(prop.value.value);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// Array of algorithms: { algorithms: ['RS256', 'ES256'] }
|
|
163
|
-
if (prop.value.type === 'ArrayExpression') {
|
|
164
|
-
for (const elem of prop.value.elements) {
|
|
165
|
-
if (elem && elem.type === 'Literal' && typeof elem.value === 'string') {
|
|
166
|
-
algorithms.push(elem.value);
|
|
167
|
-
}
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
return algorithms;
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
/**
|
|
176
|
-
* Check if options object has a specific property set
|
|
177
|
-
*/
|
|
178
|
-
export function hasOption(
|
|
179
|
-
optionsNode: TSESTree.ObjectExpression,
|
|
180
|
-
optionName: string
|
|
181
|
-
): boolean {
|
|
182
|
-
return optionsNode.properties.some(
|
|
183
|
-
(prop): prop is TSESTree.Property =>
|
|
184
|
-
prop.type === 'Property' &&
|
|
185
|
-
prop.key.type === 'Identifier' &&
|
|
186
|
-
prop.key.name === optionName
|
|
187
|
-
);
|
|
188
|
-
}
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Get the value of a specific option from options object
|
|
192
|
-
*/
|
|
193
|
-
export function getOptionValue(
|
|
194
|
-
optionsNode: TSESTree.ObjectExpression,
|
|
195
|
-
optionName: string
|
|
196
|
-
): TSESTree.Node | undefined {
|
|
197
|
-
for (const prop of optionsNode.properties) {
|
|
198
|
-
if (
|
|
199
|
-
prop.type === 'Property' &&
|
|
200
|
-
prop.key.type === 'Identifier' &&
|
|
201
|
-
prop.key.name === optionName
|
|
202
|
-
) {
|
|
203
|
-
return prop.value;
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
return undefined;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
/**
|
|
210
|
-
* Check if a literal appears to be a weak secret (short string)
|
|
211
|
-
*/
|
|
212
|
-
export function isWeakSecret(
|
|
213
|
-
node: TSESTree.Node,
|
|
214
|
-
minLength = 32
|
|
215
|
-
): boolean {
|
|
216
|
-
if (node.type === 'Literal' && typeof node.value === 'string') {
|
|
217
|
-
return node.value.length < minLength;
|
|
218
|
-
}
|
|
219
|
-
return false;
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Check if a node is an environment variable access (safe pattern)
|
|
224
|
-
*/
|
|
225
|
-
export function isEnvVariable(node: TSESTree.Node): boolean {
|
|
226
|
-
// process.env.JWT_SECRET
|
|
227
|
-
if (
|
|
228
|
-
node.type === 'MemberExpression' &&
|
|
229
|
-
node.object.type === 'MemberExpression' &&
|
|
230
|
-
node.object.object.type === 'Identifier' &&
|
|
231
|
-
node.object.object.name === 'process' &&
|
|
232
|
-
node.object.property.type === 'Identifier' &&
|
|
233
|
-
node.object.property.name === 'env'
|
|
234
|
-
) {
|
|
235
|
-
return true;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
return false;
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
/**
|
|
242
|
-
* Check if this call looks like a JWT sign operation
|
|
243
|
-
*/
|
|
244
|
-
export function isSignOperation(node: TSESTree.CallExpression): boolean {
|
|
245
|
-
return isJwtLibraryCall(node, JWT_METHODS.SIGN);
|
|
246
|
-
}
|
|
247
|
-
|
|
248
|
-
/**
|
|
249
|
-
* Check if this call looks like a JWT verify operation
|
|
250
|
-
*/
|
|
251
|
-
export function isVerifyOperation(node: TSESTree.CallExpression): boolean {
|
|
252
|
-
return isJwtLibraryCall(node, JWT_METHODS.VERIFY);
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
/**
|
|
256
|
-
* Check if this call looks like a JWT decode operation (no verification)
|
|
257
|
-
*/
|
|
258
|
-
export function isDecodeOperation(node: TSESTree.CallExpression): boolean {
|
|
259
|
-
return isJwtLibraryCall(node, JWT_METHODS.DECODE);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* Get the options argument from a JWT call
|
|
264
|
-
* For jwt.verify(token, secret, options) -> returns options
|
|
265
|
-
* For jwt.sign(payload, secret, options) -> returns options
|
|
266
|
-
*/
|
|
267
|
-
export function getOptionsArgument(
|
|
268
|
-
node: TSESTree.CallExpression,
|
|
269
|
-
optionsIndex = 2
|
|
270
|
-
): TSESTree.ObjectExpression | undefined {
|
|
271
|
-
const arg = node.arguments[optionsIndex];
|
|
272
|
-
if (arg && arg.type === 'ObjectExpression') {
|
|
273
|
-
return arg;
|
|
274
|
-
}
|
|
275
|
-
return undefined;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
/**
|
|
279
|
-
* Check if file is a test file
|
|
280
|
-
*/
|
|
281
|
-
export function isTestFile(filename: string): boolean {
|
|
282
|
-
return /\.(test|spec)\.[jt]sx?$/.test(filename) ||
|
|
283
|
-
/__(tests?|mocks?)__/.test(filename) ||
|
|
284
|
-
/\/(tests?|specs?|__tests__)\//.test(filename);
|
|
285
|
-
}
|
package/src/utils/utils.test.ts
DELETED
|
@@ -1,324 +0,0 @@
|
|
|
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
|
-
});
|
|
File without changes
|