@sun-asterisk/sunlint 1.3.16 → 1.3.17
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/config/rule-analysis-strategies.js +3 -3
- package/config/rules/enhanced-rules-registry.json +40 -20
- package/core/cli-action-handler.js +2 -2
- package/core/config-merger.js +28 -6
- package/core/constants/defaults.js +1 -1
- package/core/file-targeting-service.js +72 -4
- package/core/output-service.js +21 -4
- package/engines/heuristic-engine.js +5 -0
- package/package.json +1 -1
- package/rules/common/C002_no_duplicate_code/README.md +115 -0
- package/rules/common/C002_no_duplicate_code/analyzer.js +615 -219
- package/rules/common/C002_no_duplicate_code/test-cases/api-handlers.ts +64 -0
- package/rules/common/C002_no_duplicate_code/test-cases/data-processor.ts +46 -0
- package/rules/common/C002_no_duplicate_code/test-cases/good-example.tsx +40 -0
- package/rules/common/C002_no_duplicate_code/test-cases/product-service.ts +57 -0
- package/rules/common/C002_no_duplicate_code/test-cases/user-service.ts +49 -0
- package/rules/common/C008/analyzer.js +40 -0
- package/rules/common/C008/config.json +20 -0
- package/rules/common/C008/ts-morph-analyzer.js +1067 -0
- package/rules/common/C018_no_throw_generic_error/analyzer.js +1 -1
- package/rules/common/C018_no_throw_generic_error/symbol-based-analyzer.js +27 -3
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +504 -162
- package/rules/common/C029_catch_block_logging/analyzer.js +499 -89
- package/rules/common/C033_separate_service_repository/README.md +131 -20
- package/rules/common/C033_separate_service_repository/analyzer.js +1 -1
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +417 -274
- package/rules/common/C041_no_sensitive_hardcode/analyzer.js +144 -254
- package/rules/common/C041_no_sensitive_hardcode/config.json +50 -0
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +575 -0
- package/rules/common/C067_no_hardcoded_config/analyzer.js +17 -16
- package/rules/common/C067_no_hardcoded_config/symbol-based-analyzer.js +3477 -659
- package/rules/docs/C002_no_duplicate_code.md +276 -11
- package/rules/index.js +5 -1
- package/rules/security/S006_no_plaintext_recovery_codes/analyzer.js +266 -88
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +805 -0
- package/rules/security/S010_no_insecure_encryption/README.md +78 -0
- package/rules/security/S010_no_insecure_encryption/analyzer.js +463 -398
- package/rules/security/S013_tls_enforcement/README.md +51 -0
- package/rules/security/S013_tls_enforcement/analyzer.js +99 -0
- package/rules/security/S013_tls_enforcement/config.json +41 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +339 -0
- package/rules/security/S014_tls_version_enforcement/README.md +354 -0
- package/rules/security/S014_tls_version_enforcement/analyzer.js +118 -0
- package/rules/security/S014_tls_version_enforcement/config.json +56 -0
- package/rules/security/S014_tls_version_enforcement/symbol-based-analyzer.js +194 -0
- package/rules/security/S055_content_type_validation/analyzer.js +121 -279
- package/rules/security/S055_content_type_validation/symbol-based-analyzer.js +346 -0
- package/rules/tests/C002_no_duplicate_code.test.js +111 -22
- package/rules/common/C029_catch_block_logging/analyzer-smart-pipeline.js +0 -755
- package/rules/common/C041_no_sensitive_hardcode/ast-analyzer.js +0 -296
|
@@ -0,0 +1,805 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* S006 Symbol-Based Analyzer - No Plaintext Recovery/Activation Codes
|
|
3
|
+
*
|
|
4
|
+
* Uses ts-morph AST analysis to detect plaintext recovery/activation codes
|
|
5
|
+
* in various contexts: API responses, logging, email templates, etc.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { SyntaxKind } = require("ts-morph");
|
|
9
|
+
|
|
10
|
+
class S006SymbolBasedAnalyzer {
|
|
11
|
+
constructor(semanticEngine = null) {
|
|
12
|
+
this.semanticEngine = semanticEngine;
|
|
13
|
+
this.ruleId = "S006";
|
|
14
|
+
|
|
15
|
+
// Sensitive code-related identifiers (security/auth related only)
|
|
16
|
+
this.sensitiveIdentifiers = new Set([
|
|
17
|
+
"activationcode",
|
|
18
|
+
"recoverycode",
|
|
19
|
+
"resetcode",
|
|
20
|
+
"verificationcode",
|
|
21
|
+
"confirmationcode",
|
|
22
|
+
"otp",
|
|
23
|
+
"otpcode",
|
|
24
|
+
"totp",
|
|
25
|
+
"pin",
|
|
26
|
+
"pincode",
|
|
27
|
+
// Removed generic "code" - too many false positives with business codes
|
|
28
|
+
// Only specific security-related codes above
|
|
29
|
+
"secret",
|
|
30
|
+
"password",
|
|
31
|
+
"passphrase",
|
|
32
|
+
]);
|
|
33
|
+
|
|
34
|
+
// Business/technical identifiers that should NOT be flagged (not security codes)
|
|
35
|
+
this.safeBusinessIdentifiers = new Set([
|
|
36
|
+
"statuscode",
|
|
37
|
+
"httpstatuscode",
|
|
38
|
+
"errorcode",
|
|
39
|
+
"responsecode",
|
|
40
|
+
"agencycode",
|
|
41
|
+
"companycode",
|
|
42
|
+
"eventcode",
|
|
43
|
+
"productcode",
|
|
44
|
+
"itemcode",
|
|
45
|
+
"categorycode",
|
|
46
|
+
"departmentcode",
|
|
47
|
+
"locationcode",
|
|
48
|
+
"branchcode",
|
|
49
|
+
"regioncode",
|
|
50
|
+
"countrycode",
|
|
51
|
+
"languagecode",
|
|
52
|
+
"currencycode",
|
|
53
|
+
"timecode",
|
|
54
|
+
"zipcode",
|
|
55
|
+
"postalcode",
|
|
56
|
+
"areacode",
|
|
57
|
+
"dialcode",
|
|
58
|
+
"apiname",
|
|
59
|
+
"adminapiname",
|
|
60
|
+
"appinstall",
|
|
61
|
+
"externalpointeventid", // External point system event identifier
|
|
62
|
+
"externalserviceid", // External service identifier
|
|
63
|
+
"pointstatus", // User point status (business state, not security code)
|
|
64
|
+
"memberstatus", // User member status
|
|
65
|
+
"friendshipstatus", // User friendship status
|
|
66
|
+
]);
|
|
67
|
+
|
|
68
|
+
// Safe password-related patterns (template names, route names, not actual passwords)
|
|
69
|
+
this.safePasswordPatterns = [
|
|
70
|
+
"forgotpassword",
|
|
71
|
+
"resetpassword",
|
|
72
|
+
"changepassword",
|
|
73
|
+
"updatepassword",
|
|
74
|
+
"passwordreset",
|
|
75
|
+
"passwordchange",
|
|
76
|
+
"passwordupdate",
|
|
77
|
+
"passwordrecovery",
|
|
78
|
+
];
|
|
79
|
+
|
|
80
|
+
// Safe token types (JWT, session tokens, etc.)
|
|
81
|
+
this.safeTokenTypes = new Set([
|
|
82
|
+
"resettoken",
|
|
83
|
+
"accesstoken",
|
|
84
|
+
"refreshtoken",
|
|
85
|
+
"sessiontoken",
|
|
86
|
+
"authtoken",
|
|
87
|
+
"jwttoken",
|
|
88
|
+
"bearertoken",
|
|
89
|
+
]);
|
|
90
|
+
|
|
91
|
+
// Transmission/exposure contexts
|
|
92
|
+
this.exposureContexts = new Set([
|
|
93
|
+
"send",
|
|
94
|
+
"email",
|
|
95
|
+
"sms",
|
|
96
|
+
"text",
|
|
97
|
+
"message",
|
|
98
|
+
"mail",
|
|
99
|
+
"push",
|
|
100
|
+
"notify",
|
|
101
|
+
"response",
|
|
102
|
+
"body",
|
|
103
|
+
"json",
|
|
104
|
+
"data",
|
|
105
|
+
"payload",
|
|
106
|
+
"log",
|
|
107
|
+
"console",
|
|
108
|
+
"debug",
|
|
109
|
+
"info",
|
|
110
|
+
"warn",
|
|
111
|
+
"error",
|
|
112
|
+
"trace",
|
|
113
|
+
"render",
|
|
114
|
+
"template",
|
|
115
|
+
"view",
|
|
116
|
+
"html",
|
|
117
|
+
]);
|
|
118
|
+
|
|
119
|
+
// Safe patterns to exclude
|
|
120
|
+
this.safePatterns = [
|
|
121
|
+
"hash",
|
|
122
|
+
"encrypt",
|
|
123
|
+
"cipher",
|
|
124
|
+
"bcrypt",
|
|
125
|
+
"crypto",
|
|
126
|
+
"secure",
|
|
127
|
+
"hashed",
|
|
128
|
+
"encrypted",
|
|
129
|
+
"sendsecure",
|
|
130
|
+
"hashcode",
|
|
131
|
+
"encryptcode",
|
|
132
|
+
"savehashed",
|
|
133
|
+
"storehashed",
|
|
134
|
+
"validatecode",
|
|
135
|
+
"verifycode",
|
|
136
|
+
"checkcode",
|
|
137
|
+
// Audit/internal logging (not external exposure)
|
|
138
|
+
"logadminpointhistory",
|
|
139
|
+
"adminpointhistory",
|
|
140
|
+
"auditlog",
|
|
141
|
+
"systemlog",
|
|
142
|
+
"internallog",
|
|
143
|
+
"logger.warn", // Warning logs are typically business messages, not sensitive data exposure
|
|
144
|
+
"logger.debug", // Debug logs are for development, not production exposure
|
|
145
|
+
"logger.info", // Info logs are for general information
|
|
146
|
+
];
|
|
147
|
+
|
|
148
|
+
// Safe return/response patterns (just success messages, no actual codes)
|
|
149
|
+
this.safeResponsePatterns = [
|
|
150
|
+
"success",
|
|
151
|
+
"message",
|
|
152
|
+
"sent",
|
|
153
|
+
"instructions",
|
|
154
|
+
"check your",
|
|
155
|
+
"please enter",
|
|
156
|
+
"has been sent",
|
|
157
|
+
"successfully sent",
|
|
158
|
+
"sent to your email",
|
|
159
|
+
"sent to email",
|
|
160
|
+
"sent successfully",
|
|
161
|
+
"generated successfully",
|
|
162
|
+
];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async initialize(semanticEngine) {
|
|
166
|
+
this.semanticEngine = semanticEngine;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async analyze(sourceFile, filePath) {
|
|
170
|
+
const violations = [];
|
|
171
|
+
|
|
172
|
+
try {
|
|
173
|
+
// Check return statements with sensitive codes
|
|
174
|
+
this.checkReturnStatements(sourceFile, filePath, violations);
|
|
175
|
+
|
|
176
|
+
// Check property assignments in objects
|
|
177
|
+
this.checkObjectLiterals(sourceFile, filePath, violations);
|
|
178
|
+
|
|
179
|
+
// Check call expressions (res.json, console.log, etc.)
|
|
180
|
+
this.checkCallExpressions(sourceFile, filePath, violations);
|
|
181
|
+
|
|
182
|
+
// Check template literals with codes
|
|
183
|
+
this.checkTemplateLiterals(sourceFile, filePath, violations);
|
|
184
|
+
|
|
185
|
+
// Check interface/type definitions
|
|
186
|
+
this.checkTypeDefinitions(sourceFile, filePath, violations);
|
|
187
|
+
|
|
188
|
+
// Removed: checkVariableDeclarations - too many false positives
|
|
189
|
+
// Variables can be used safely in many contexts (hashing, validation, etc.)
|
|
190
|
+
} catch (error) {
|
|
191
|
+
console.warn(`⚠️ [S006] Analysis error in ${filePath}: ${error.message}`);
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return violations;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check return statements that expose sensitive codes
|
|
199
|
+
* e.g., return { activationCode };
|
|
200
|
+
*/
|
|
201
|
+
checkReturnStatements(sourceFile, filePath, violations) {
|
|
202
|
+
const returnStatements = sourceFile.getDescendantsOfKind(
|
|
203
|
+
SyntaxKind.ReturnStatement
|
|
204
|
+
);
|
|
205
|
+
|
|
206
|
+
for (const returnStmt of returnStatements) {
|
|
207
|
+
const expression = returnStmt.getExpression();
|
|
208
|
+
if (!expression) continue;
|
|
209
|
+
|
|
210
|
+
// Check if returning object with sensitive properties
|
|
211
|
+
if (expression.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
212
|
+
const objLiteral = expression;
|
|
213
|
+
const properties = objLiteral.getProperties();
|
|
214
|
+
|
|
215
|
+
// Skip if return object contains only safe messages (success, message, etc.)
|
|
216
|
+
const hasOnlySafeProperties = properties.every((prop) => {
|
|
217
|
+
const name = prop.getName?.() || "";
|
|
218
|
+
const normalizedName = name.toLowerCase();
|
|
219
|
+
return (
|
|
220
|
+
normalizedName === "success" ||
|
|
221
|
+
normalizedName === "message" ||
|
|
222
|
+
normalizedName === "expiresat" ||
|
|
223
|
+
normalizedName === "timestamp" ||
|
|
224
|
+
normalizedName === "status"
|
|
225
|
+
);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
if (hasOnlySafeProperties) {
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
for (const prop of properties) {
|
|
233
|
+
if (
|
|
234
|
+
prop.getKind() === SyntaxKind.PropertyAssignment ||
|
|
235
|
+
prop.getKind() === SyntaxKind.ShorthandPropertyAssignment
|
|
236
|
+
) {
|
|
237
|
+
const name = prop.getName?.() || "";
|
|
238
|
+
const normalizedName = this.normalizeIdentifier(name);
|
|
239
|
+
|
|
240
|
+
// Skip statusCode - it's HTTP status code, not sensitive
|
|
241
|
+
if (normalizedName === "statuscode") {
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
if (
|
|
246
|
+
this.isSensitiveIdentifier(normalizedName) &&
|
|
247
|
+
!this.isInSafeContext(prop)
|
|
248
|
+
) {
|
|
249
|
+
violations.push({
|
|
250
|
+
ruleId: this.ruleId,
|
|
251
|
+
severity: "error",
|
|
252
|
+
message: `Returning sensitive code '${name}' in plaintext - codes should be encrypted or excluded from responses`,
|
|
253
|
+
line: prop.getStartLineNumber(),
|
|
254
|
+
column: prop.getStart() - prop.getStartLinePos() + 1,
|
|
255
|
+
filePath: filePath,
|
|
256
|
+
file: filePath,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Check object literals for sensitive code exposure
|
|
267
|
+
* e.g., { resetCode: code, ... }
|
|
268
|
+
*/
|
|
269
|
+
checkObjectLiterals(sourceFile, filePath, violations) {
|
|
270
|
+
const objectLiterals = sourceFile.getDescendantsOfKind(
|
|
271
|
+
SyntaxKind.ObjectLiteralExpression
|
|
272
|
+
);
|
|
273
|
+
|
|
274
|
+
for (const objLiteral of objectLiterals) {
|
|
275
|
+
// Skip if in safe context
|
|
276
|
+
if (this.isInSafeContext(objLiteral)) continue;
|
|
277
|
+
|
|
278
|
+
const properties = objLiteral.getProperties();
|
|
279
|
+
|
|
280
|
+
for (const prop of properties) {
|
|
281
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
282
|
+
const name = prop.getName?.() || "";
|
|
283
|
+
const normalizedName = this.normalizeIdentifier(name);
|
|
284
|
+
|
|
285
|
+
if (this.isSensitiveIdentifier(normalizedName)) {
|
|
286
|
+
// Check if in exposure context (e.g., res.json, send, etc.)
|
|
287
|
+
const parent = this.findParentContext(objLiteral);
|
|
288
|
+
if (parent && this.isExposureContext(parent)) {
|
|
289
|
+
// Check if this is request input (not exposure)
|
|
290
|
+
// Look for JSON.stringify in parent chain with body/data context
|
|
291
|
+
let currentParent = parent;
|
|
292
|
+
let depth = 0;
|
|
293
|
+
let isRequestBody = false;
|
|
294
|
+
|
|
295
|
+
while (currentParent && depth < 10) {
|
|
296
|
+
const parentText = currentParent.getText().toLowerCase();
|
|
297
|
+
|
|
298
|
+
// Check if this object is being passed to JSON.stringify
|
|
299
|
+
// and is part of a fetch/axios request body
|
|
300
|
+
if (parentText.includes("json.stringify")) {
|
|
301
|
+
// Look further up for body: or data: context
|
|
302
|
+
let upperParent = currentParent.getParent();
|
|
303
|
+
let upperDepth = 0;
|
|
304
|
+
|
|
305
|
+
while (upperParent && upperDepth < 5) {
|
|
306
|
+
const upperText = upperParent.getText().toLowerCase();
|
|
307
|
+
if (
|
|
308
|
+
(upperText.includes("body:") ||
|
|
309
|
+
upperText.includes("data:")) &&
|
|
310
|
+
(upperText.includes("fetch") ||
|
|
311
|
+
upperText.includes("axios") ||
|
|
312
|
+
upperText.includes("method:") ||
|
|
313
|
+
upperText.includes("post") ||
|
|
314
|
+
upperText.includes("put"))
|
|
315
|
+
) {
|
|
316
|
+
isRequestBody = true;
|
|
317
|
+
break;
|
|
318
|
+
}
|
|
319
|
+
upperParent = upperParent.getParent();
|
|
320
|
+
upperDepth++;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
if (isRequestBody) break;
|
|
325
|
+
currentParent = currentParent.getParent();
|
|
326
|
+
depth++;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
if (isRequestBody) {
|
|
330
|
+
// This is request body - user sending code to server for verification
|
|
331
|
+
// Not an exposure, so skip
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// Use warning for JWT config properties (common pattern in DTOs)
|
|
336
|
+
const isJwtConfig =
|
|
337
|
+
normalizedName === "secret" &&
|
|
338
|
+
(objLiteral.getText().includes("expiresIn") ||
|
|
339
|
+
objLiteral.getText().includes("JWT") ||
|
|
340
|
+
objLiteral.getText().includes("process.env"));
|
|
341
|
+
|
|
342
|
+
violations.push({
|
|
343
|
+
ruleId: this.ruleId,
|
|
344
|
+
severity: isJwtConfig ? "warning" : "error",
|
|
345
|
+
message: isJwtConfig
|
|
346
|
+
? `Object property '${name}' in JWT config - consider refactoring signing logic to service layer`
|
|
347
|
+
: `Object property '${name}' exposes sensitive code in plaintext - use encrypted transmission or exclude from response`,
|
|
348
|
+
line: prop.getStartLineNumber(),
|
|
349
|
+
column: prop.getStart() - prop.getStartLinePos() + 1,
|
|
350
|
+
filePath: filePath,
|
|
351
|
+
file: filePath,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Check call expressions like res.json(), console.log(), etc.
|
|
362
|
+
* e.g., res.json({ code: verificationCode })
|
|
363
|
+
*/
|
|
364
|
+
checkCallExpressions(sourceFile, filePath, violations) {
|
|
365
|
+
const callExpressions = sourceFile.getDescendantsOfKind(
|
|
366
|
+
SyntaxKind.CallExpression
|
|
367
|
+
);
|
|
368
|
+
|
|
369
|
+
for (const callExpr of callExpressions) {
|
|
370
|
+
const expression = callExpr.getExpression();
|
|
371
|
+
const expressionText = expression.getText().toLowerCase();
|
|
372
|
+
|
|
373
|
+
// Skip safe methods
|
|
374
|
+
if (
|
|
375
|
+
this.safePatterns.some((pattern) => expressionText.includes(pattern))
|
|
376
|
+
) {
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
// Check for exposure methods
|
|
381
|
+
const isExposureMethod =
|
|
382
|
+
expressionText.includes("json") ||
|
|
383
|
+
expressionText.includes("send") ||
|
|
384
|
+
expressionText.includes("log") ||
|
|
385
|
+
expressionText.includes("console") ||
|
|
386
|
+
expressionText.includes("email") ||
|
|
387
|
+
expressionText.includes("sms") ||
|
|
388
|
+
expressionText.includes("notify") ||
|
|
389
|
+
expressionText.includes("render");
|
|
390
|
+
|
|
391
|
+
if (!isExposureMethod) continue;
|
|
392
|
+
|
|
393
|
+
// Skip JSON.stringify in request body context (client sending code to server for verification)
|
|
394
|
+
if (expressionText.includes("json.stringify")) {
|
|
395
|
+
// Check if this is part of fetch/axios request body
|
|
396
|
+
let parent = callExpr.getParent();
|
|
397
|
+
let depth = 0;
|
|
398
|
+
let isRequestBody = false;
|
|
399
|
+
|
|
400
|
+
while (parent && depth < 10) {
|
|
401
|
+
const parentText = parent.getText().toLowerCase();
|
|
402
|
+
if (
|
|
403
|
+
(parentText.includes("body:") || parentText.includes("data:")) &&
|
|
404
|
+
(parentText.includes("fetch") ||
|
|
405
|
+
parentText.includes("axios") ||
|
|
406
|
+
parentText.includes("method:") ||
|
|
407
|
+
parentText.includes("post"))
|
|
408
|
+
) {
|
|
409
|
+
isRequestBody = true;
|
|
410
|
+
break;
|
|
411
|
+
}
|
|
412
|
+
parent = parent.getParent();
|
|
413
|
+
depth++;
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
if (isRequestBody) {
|
|
417
|
+
continue; // Skip - this is client sending code for verification, not exposure
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
// Check arguments for sensitive codes
|
|
422
|
+
const args = callExpr.getArguments();
|
|
423
|
+
for (const arg of args) {
|
|
424
|
+
// Skip if argument is in safe context
|
|
425
|
+
if (this.isInSafeContext(arg)) {
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Skip safe string messages
|
|
430
|
+
if (
|
|
431
|
+
arg.getKind() === SyntaxKind.StringLiteral ||
|
|
432
|
+
arg.getKind() === SyntaxKind.NoSubstitutionTemplateLiteral
|
|
433
|
+
) {
|
|
434
|
+
const text = arg.getText().toLowerCase();
|
|
435
|
+
if (
|
|
436
|
+
this.safeResponsePatterns.some((pattern) => text.includes(pattern))
|
|
437
|
+
) {
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
if (this.containsSensitiveCode(arg)) {
|
|
443
|
+
const argText = arg.getText();
|
|
444
|
+
const match = argText.match(
|
|
445
|
+
/\b(activation|recovery|reset|verification|otp|code)\w*/i
|
|
446
|
+
);
|
|
447
|
+
const codeName = match ? match[0] : "sensitive code";
|
|
448
|
+
|
|
449
|
+
// Check if this is OTP/SMS transmission (accepted functional requirement)
|
|
450
|
+
const isOtpSms =
|
|
451
|
+
(expressionText.includes("sendmessage") ||
|
|
452
|
+
expressionText.includes("sendsms") ||
|
|
453
|
+
expressionText.includes("sendotp")) &&
|
|
454
|
+
(codeName.toLowerCase().includes("otp") ||
|
|
455
|
+
argText.toLowerCase().includes("otp"));
|
|
456
|
+
|
|
457
|
+
violations.push({
|
|
458
|
+
ruleId: this.ruleId,
|
|
459
|
+
severity: isOtpSms ? "warning" : "error",
|
|
460
|
+
message: isOtpSms
|
|
461
|
+
? `OTP transmission via ${expressionText}() - ensure secure channel and proper expiration/rate limiting`
|
|
462
|
+
: `Exposing '${codeName}' via ${expressionText}() - codes should not be transmitted in plaintext`,
|
|
463
|
+
line: arg.getStartLineNumber(),
|
|
464
|
+
column: arg.getStart() - arg.getStartLinePos() + 1,
|
|
465
|
+
filePath: filePath,
|
|
466
|
+
file: filePath,
|
|
467
|
+
});
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* Check template literals for code exposure
|
|
475
|
+
* e.g., `Your code is: ${activationCode}`
|
|
476
|
+
*/
|
|
477
|
+
checkTemplateLiterals(sourceFile, filePath, violations) {
|
|
478
|
+
const templates = sourceFile.getDescendantsOfKind(
|
|
479
|
+
SyntaxKind.TemplateExpression
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
for (const template of templates) {
|
|
483
|
+
const spans = template.getTemplateSpans();
|
|
484
|
+
|
|
485
|
+
for (const span of spans) {
|
|
486
|
+
const expression = span.getExpression();
|
|
487
|
+
const expressionText = expression.getText().toLowerCase();
|
|
488
|
+
const normalizedExpr = this.normalizeIdentifier(expressionText);
|
|
489
|
+
|
|
490
|
+
// Skip JWT signing operations - these produce signed tokens (safe), not plaintext secrets
|
|
491
|
+
if (
|
|
492
|
+
expressionText.includes("jwtsecret") ||
|
|
493
|
+
expressionText.includes("jwtsign") ||
|
|
494
|
+
expressionText.includes("tokensign") ||
|
|
495
|
+
expressionText.includes("sign(") ||
|
|
496
|
+
expressionText.includes(".sign(")
|
|
497
|
+
) {
|
|
498
|
+
continue;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
if (this.isSensitiveIdentifier(normalizedExpr)) {
|
|
502
|
+
// Check if in exposure context
|
|
503
|
+
const parent = this.findParentContext(template);
|
|
504
|
+
if (parent && this.isExposureContext(parent)) {
|
|
505
|
+
violations.push({
|
|
506
|
+
ruleId: this.ruleId,
|
|
507
|
+
severity: "error",
|
|
508
|
+
message: `Template literal exposes sensitive code '${expression.getText()}' - avoid including codes in templates`,
|
|
509
|
+
line: expression.getStartLineNumber(),
|
|
510
|
+
column: expression.getStart() - expression.getStartLinePos() + 1,
|
|
511
|
+
filePath: filePath,
|
|
512
|
+
file: filePath,
|
|
513
|
+
});
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Check interface/type definitions
|
|
522
|
+
* e.g., interface Response { verificationCode: string }
|
|
523
|
+
*/
|
|
524
|
+
checkTypeDefinitions(sourceFile, filePath, violations) {
|
|
525
|
+
const interfaces = sourceFile.getDescendantsOfKind(
|
|
526
|
+
SyntaxKind.InterfaceDeclaration
|
|
527
|
+
);
|
|
528
|
+
|
|
529
|
+
for (const iface of interfaces) {
|
|
530
|
+
const name = iface.getName();
|
|
531
|
+
const normalizedName = name.toLowerCase();
|
|
532
|
+
|
|
533
|
+
// Check if it's a response/DTO type
|
|
534
|
+
if (
|
|
535
|
+
normalizedName.includes("response") ||
|
|
536
|
+
normalizedName.includes("result") ||
|
|
537
|
+
normalizedName.includes("dto") ||
|
|
538
|
+
normalizedName.includes("payload")
|
|
539
|
+
) {
|
|
540
|
+
const properties = iface.getProperties();
|
|
541
|
+
|
|
542
|
+
for (const prop of properties) {
|
|
543
|
+
const propName = prop.getName();
|
|
544
|
+
const normalizedPropName = this.normalizeIdentifier(propName);
|
|
545
|
+
|
|
546
|
+
if (this.isSensitiveIdentifier(normalizedPropName)) {
|
|
547
|
+
violations.push({
|
|
548
|
+
ruleId: this.ruleId,
|
|
549
|
+
severity: "warning",
|
|
550
|
+
message: `Interface '${name}' exposes sensitive property '${propName}' - consider excluding codes from response types`,
|
|
551
|
+
line: prop.getStartLineNumber(),
|
|
552
|
+
column: prop.getStart() - prop.getStartLinePos() + 1,
|
|
553
|
+
filePath: filePath,
|
|
554
|
+
file: filePath,
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
/**
|
|
563
|
+
* Check variable declarations for sensitive code handling
|
|
564
|
+
*/
|
|
565
|
+
checkVariableDeclarations(sourceFile, filePath, violations) {
|
|
566
|
+
const varDecls = sourceFile.getDescendantsOfKind(
|
|
567
|
+
SyntaxKind.VariableDeclaration
|
|
568
|
+
);
|
|
569
|
+
|
|
570
|
+
for (const varDecl of varDecls) {
|
|
571
|
+
const name = varDecl.getName();
|
|
572
|
+
const normalizedName = this.normalizeIdentifier(name);
|
|
573
|
+
|
|
574
|
+
if (!this.isSensitiveIdentifier(normalizedName)) continue;
|
|
575
|
+
|
|
576
|
+
const initializer = varDecl.getInitializer();
|
|
577
|
+
if (!initializer) continue;
|
|
578
|
+
|
|
579
|
+
// Check if variable is used in exposure context
|
|
580
|
+
const varStatement = varDecl.getFirstAncestorByKind(
|
|
581
|
+
SyntaxKind.VariableStatement
|
|
582
|
+
);
|
|
583
|
+
if (!varStatement) continue;
|
|
584
|
+
|
|
585
|
+
const scope = varStatement.getParent();
|
|
586
|
+
if (!scope) continue;
|
|
587
|
+
|
|
588
|
+
// Look for usage in exposure contexts
|
|
589
|
+
const identifiers = scope.getDescendantsOfKind(SyntaxKind.Identifier);
|
|
590
|
+
for (const identifier of identifiers) {
|
|
591
|
+
if (identifier.getText() === name) {
|
|
592
|
+
const parent = identifier.getParent();
|
|
593
|
+
if (parent && this.isExposureContext(parent)) {
|
|
594
|
+
violations.push({
|
|
595
|
+
ruleId: this.ruleId,
|
|
596
|
+
severity: "warning",
|
|
597
|
+
message: `Variable '${name}' containing sensitive code is used in exposure context - ensure proper encryption`,
|
|
598
|
+
line: identifier.getStartLineNumber(),
|
|
599
|
+
column: identifier.getStart() - identifier.getStartLinePos() + 1,
|
|
600
|
+
filePath: filePath,
|
|
601
|
+
file: filePath,
|
|
602
|
+
});
|
|
603
|
+
break; // Only report once per variable
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Helper: Normalize identifier by removing non-alphanumeric and lowercase
|
|
612
|
+
*/
|
|
613
|
+
normalizeIdentifier(name) {
|
|
614
|
+
return name.replace(/[^a-zA-Z0-9]/g, "").toLowerCase();
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
/**
|
|
618
|
+
* Helper: Check if identifier is sensitive
|
|
619
|
+
*/
|
|
620
|
+
isSensitiveIdentifier(normalizedName) {
|
|
621
|
+
// First check if it's a safe token type (JWT, session token, etc.)
|
|
622
|
+
if (this.safeTokenTypes.has(normalizedName)) {
|
|
623
|
+
return false;
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
// Check if it's a business/technical identifier (not security code)
|
|
627
|
+
if (this.safeBusinessIdentifiers.has(normalizedName)) {
|
|
628
|
+
return false;
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
// Check if it's a safe password pattern (template/route names, not actual passwords)
|
|
632
|
+
if (
|
|
633
|
+
this.safePasswordPatterns.some((pattern) =>
|
|
634
|
+
normalizedName.includes(pattern)
|
|
635
|
+
)
|
|
636
|
+
) {
|
|
637
|
+
return false;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
// Check if it ends with "token" (resetToken, authToken, etc. are secure JWT tokens)
|
|
641
|
+
if (normalizedName.endsWith("token")) {
|
|
642
|
+
return false;
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Check if it's just generic "code" without security context
|
|
646
|
+
// Only flag if it's specifically security-related
|
|
647
|
+
if (normalizedName === "code") {
|
|
648
|
+
// Check if it has security context in variable name
|
|
649
|
+
// e.g., "verificationCode", "otpCode" are sensitive
|
|
650
|
+
// but "errorCode", "statusCode" are not
|
|
651
|
+
return false; // Generic "code" alone is not sensitive
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
// Skip if it's "password" alone in object key context (likely config/route name)
|
|
655
|
+
if (normalizedName === "password") {
|
|
656
|
+
return false; // Will be caught by containsSensitiveCode if it's actual password value
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
return (
|
|
660
|
+
this.sensitiveIdentifiers.has(normalizedName) ||
|
|
661
|
+
Array.from(this.sensitiveIdentifiers).some((sensitive) =>
|
|
662
|
+
normalizedName.includes(sensitive)
|
|
663
|
+
)
|
|
664
|
+
);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* Helper: Check if node is in safe context (hashed, encrypted, etc.)
|
|
669
|
+
*/
|
|
670
|
+
isInSafeContext(node) {
|
|
671
|
+
// Check the node itself
|
|
672
|
+
const nodeText = node.getText().toLowerCase();
|
|
673
|
+
if (this.safePatterns.some((pattern) => nodeText.includes(pattern))) {
|
|
674
|
+
return true;
|
|
675
|
+
}
|
|
676
|
+
|
|
677
|
+
// Check if it's part of a safe message response (success: true, message: "...")
|
|
678
|
+
if (
|
|
679
|
+
this.safeResponsePatterns.some((pattern) => nodeText.includes(pattern))
|
|
680
|
+
) {
|
|
681
|
+
return true;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
// Check parent context
|
|
685
|
+
let current = node.getParent();
|
|
686
|
+
let depth = 0;
|
|
687
|
+
|
|
688
|
+
while (current && depth < 5) {
|
|
689
|
+
const text = current.getText().toLowerCase();
|
|
690
|
+
|
|
691
|
+
// Check for safe patterns
|
|
692
|
+
if (this.safePatterns.some((pattern) => text.includes(pattern))) {
|
|
693
|
+
return true;
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// Check if parent is a variable with "hashed" or "encrypted" in name
|
|
697
|
+
if (current.getKind() === SyntaxKind.VariableDeclaration) {
|
|
698
|
+
const varName = current.getName?.() || "";
|
|
699
|
+
const normalizedVarName = varName.toLowerCase();
|
|
700
|
+
if (
|
|
701
|
+
normalizedVarName.includes("hashed") ||
|
|
702
|
+
normalizedVarName.includes("encrypted") ||
|
|
703
|
+
normalizedVarName.includes("secure") ||
|
|
704
|
+
normalizedVarName.includes("token")
|
|
705
|
+
) {
|
|
706
|
+
return true;
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
// Check if parent is a safe function call (e.g., sendSecure, hash, encrypt)
|
|
711
|
+
if (current.getKind() === SyntaxKind.CallExpression) {
|
|
712
|
+
const callExpr = current;
|
|
713
|
+
const expression = callExpr.getExpression();
|
|
714
|
+
const exprText = expression.getText().toLowerCase();
|
|
715
|
+
if (this.safePatterns.some((pattern) => exprText.includes(pattern))) {
|
|
716
|
+
return true;
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
current = current.getParent();
|
|
721
|
+
depth++;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
return false;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Helper: Find parent context (call expression, return, etc.)
|
|
729
|
+
*/
|
|
730
|
+
findParentContext(node) {
|
|
731
|
+
let current = node.getParent();
|
|
732
|
+
let depth = 0;
|
|
733
|
+
|
|
734
|
+
while (current && depth < 10) {
|
|
735
|
+
const kind = current.getKind();
|
|
736
|
+
if (
|
|
737
|
+
kind === SyntaxKind.CallExpression ||
|
|
738
|
+
kind === SyntaxKind.ReturnStatement ||
|
|
739
|
+
kind === SyntaxKind.VariableDeclaration
|
|
740
|
+
) {
|
|
741
|
+
return current;
|
|
742
|
+
}
|
|
743
|
+
current = current.getParent();
|
|
744
|
+
depth++;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
return null;
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
/**
|
|
751
|
+
* Helper: Check if parent context is exposure context
|
|
752
|
+
*/
|
|
753
|
+
isExposureContext(node) {
|
|
754
|
+
const text = node.getText().toLowerCase();
|
|
755
|
+
return Array.from(this.exposureContexts).some((context) =>
|
|
756
|
+
text.includes(context)
|
|
757
|
+
);
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
/**
|
|
761
|
+
* Helper: Check if argument contains sensitive code
|
|
762
|
+
*/
|
|
763
|
+
containsSensitiveCode(node) {
|
|
764
|
+
const text = node.getText().toLowerCase();
|
|
765
|
+
const normalized = this.normalizeIdentifier(text);
|
|
766
|
+
|
|
767
|
+
// Check for sensitive identifiers
|
|
768
|
+
if (this.isSensitiveIdentifier(normalized)) {
|
|
769
|
+
return true;
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
// Check child nodes (for object literals, etc.)
|
|
773
|
+
if (node.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
774
|
+
const properties = node.getProperties();
|
|
775
|
+
for (const prop of properties) {
|
|
776
|
+
const propName = prop.getName?.() || "";
|
|
777
|
+
const normalizedPropName = this.normalizeIdentifier(propName);
|
|
778
|
+
if (this.isSensitiveIdentifier(normalizedPropName)) {
|
|
779
|
+
return true;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
// Check template spans
|
|
785
|
+
if (node.getKind() === SyntaxKind.TemplateExpression) {
|
|
786
|
+
const spans = node.getTemplateSpans();
|
|
787
|
+
for (const span of spans) {
|
|
788
|
+
const expr = span.getExpression();
|
|
789
|
+
const exprText = expr.getText().toLowerCase();
|
|
790
|
+
const normalizedExpr = this.normalizeIdentifier(exprText);
|
|
791
|
+
if (this.isSensitiveIdentifier(normalizedExpr)) {
|
|
792
|
+
return true;
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
return false;
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
cleanup() {
|
|
801
|
+
// No cleanup needed
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
module.exports = S006SymbolBasedAnalyzer;
|