@sun-asterisk/sunlint 1.3.18 → 1.3.19
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/rules/enhanced-rules-registry.json +77 -18
- package/core/cli-program.js +2 -1
- package/core/github-annotate-service.js +89 -0
- package/core/output-service.js +25 -0
- package/core/summary-report-service.js +30 -30
- package/package.json +3 -2
- package/rules/common/C014_dependency_injection/symbol-based-analyzer.js +392 -280
- package/rules/common/C017_constructor_logic/analyzer.js +137 -503
- package/rules/common/C017_constructor_logic/config.json +50 -0
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +463 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +463 -21
- package/rules/security/S011_secure_guid_generation/README.md +255 -0
- package/rules/security/S011_secure_guid_generation/analyzer.js +135 -0
- package/rules/security/S011_secure_guid_generation/config.json +56 -0
- package/rules/security/S011_secure_guid_generation/symbol-based-analyzer.js +609 -0
- package/rules/security/S028_file_upload_size_limits/README.md +537 -0
- package/rules/security/S028_file_upload_size_limits/analyzer.js +202 -0
- package/rules/security/S028_file_upload_size_limits/config.json +186 -0
- package/rules/security/S028_file_upload_size_limits/symbol-based-analyzer.js +530 -0
- package/rules/security/S041_session_token_invalidation/README.md +303 -0
- package/rules/security/S041_session_token_invalidation/analyzer.js +242 -0
- package/rules/security/S041_session_token_invalidation/config.json +175 -0
- package/rules/security/S041_session_token_invalidation/regex-based-analyzer.js +411 -0
- package/rules/security/S041_session_token_invalidation/symbol-based-analyzer.js +674 -0
- package/rules/security/S044_re_authentication_required/README.md +136 -0
- package/rules/security/S044_re_authentication_required/analyzer.js +242 -0
- package/rules/security/S044_re_authentication_required/config.json +161 -0
- package/rules/security/S044_re_authentication_required/regex-based-analyzer.js +329 -0
- package/rules/security/S044_re_authentication_required/symbol-based-analyzer.js +537 -0
- package/rules/security/S045_brute_force_protection/README.md +345 -0
- package/rules/security/S045_brute_force_protection/analyzer.js +336 -0
- package/rules/security/S045_brute_force_protection/config.json +139 -0
- package/rules/security/S045_brute_force_protection/symbol-based-analyzer.js +646 -0
- package/rules/common/C017_constructor_logic/semantic-analyzer.js +0 -340
|
@@ -63,9 +63,56 @@ class S006SymbolBasedAnalyzer {
|
|
|
63
63
|
"pointstatus", // User point status (business state, not security code)
|
|
64
64
|
"memberstatus", // User member status
|
|
65
65
|
"friendshipstatus", // User friendship status
|
|
66
|
+
"groupinvoicedetailid", // Invoice detail identifier (business ID, not security code)
|
|
67
|
+
"invoiceid", // Invoice identifier
|
|
68
|
+
"orderid", // Order identifier
|
|
69
|
+
"detailid", // Detail record identifier
|
|
70
|
+
"transactionid", // Transaction identifier
|
|
71
|
+
"paymentid", // Payment identifier
|
|
72
|
+
"subscriptionid", // Subscription identifier
|
|
73
|
+
"customerid", // Customer identifier
|
|
74
|
+
"userid", // User identifier (not sensitive in business context)
|
|
75
|
+
"accountid", // Account identifier
|
|
76
|
+
// API/Endpoint related (not sensitive codes)
|
|
77
|
+
"generateotpapi", // API endpoint name
|
|
78
|
+
"adduserbyemailnopasswordapi", // API endpoint name
|
|
79
|
+
"setpasswordurl", // URL configuration
|
|
80
|
+
"responseotp", // Response field name in config
|
|
81
|
+
"apidocsauthbasicpassword", // API docs authentication (config)
|
|
82
|
+
"basicauthpassword", // Basic auth config (not actual password)
|
|
83
|
+
// Time/interval configurations (not sensitive codes)
|
|
84
|
+
"dedupinginterval", // SWR deduping interval
|
|
85
|
+
"refreshinterval", // Refresh interval
|
|
86
|
+
"retryinterval", // Retry interval
|
|
87
|
+
"pollinginterval", // Polling interval
|
|
88
|
+
"timeoutinterval", // Timeout interval
|
|
89
|
+
// Message/validation constants (not actual codes/passwords)
|
|
90
|
+
"invalidpassword", // Error message constant
|
|
91
|
+
"incorrectpassword", // Error message constant
|
|
92
|
+
"incorrectcurrentpassword", // Error message constant
|
|
93
|
+
"confirmationpasswordnotmatch", // Error message constant
|
|
94
|
+
// Form input fields (user input, not server response with actual codes)
|
|
95
|
+
// Note: These are commonly used as field names in registration/verification forms
|
|
96
|
+
// The actual verification should happen server-side with hashed codes
|
|
97
|
+
// Database metadata fields (counters, flags, not sensitive data)
|
|
98
|
+
"failedpasswordattempts", // Password attempt counter
|
|
99
|
+
"failedloginattempts", // Login attempt counter
|
|
100
|
+
"passwordattempts", // Attempt counter
|
|
101
|
+
"loginattempts", // Attempt counter
|
|
102
|
+
// Swagger/Validation schema properties (not actual data)
|
|
103
|
+
"ispassword", // Swagger schema validation flag
|
|
104
|
+
"isotp", // Swagger schema validation flag
|
|
105
|
+
"isotpcode", // Swagger schema validation flag
|
|
106
|
+
"isactivationcode", // Swagger schema validation flag
|
|
107
|
+
"isverificationcode", // Swagger schema validation flag
|
|
108
|
+
// Validation constants
|
|
109
|
+
"password_min_length", // Validation constant
|
|
110
|
+
"password_max_length", // Validation constant
|
|
111
|
+
"otp_length", // Validation constant
|
|
112
|
+
"code_length", // Validation constant
|
|
66
113
|
]);
|
|
67
114
|
|
|
68
|
-
// Safe password-related patterns (template names, route names, not actual passwords)
|
|
115
|
+
// Safe password-related patterns (template names, route names, form fields, not actual passwords)
|
|
69
116
|
this.safePasswordPatterns = [
|
|
70
117
|
"forgotpassword",
|
|
71
118
|
"resetpassword",
|
|
@@ -75,6 +122,12 @@ class S006SymbolBasedAnalyzer {
|
|
|
75
122
|
"passwordchange",
|
|
76
123
|
"passwordupdate",
|
|
77
124
|
"passwordrecovery",
|
|
125
|
+
"registerpassword", // Registration route/form
|
|
126
|
+
"currentpassword", // Form field for current password
|
|
127
|
+
"newpassword", // Form field for new password
|
|
128
|
+
"confirmpassword", // Form field for password confirmation
|
|
129
|
+
"confirmationpassword", // Form field for password confirmation
|
|
130
|
+
"passwordconfirm", // Form field for password confirmation
|
|
78
131
|
];
|
|
79
132
|
|
|
80
133
|
// Safe token types (JWT, session tokens, etc.)
|
|
@@ -134,6 +187,16 @@ class S006SymbolBasedAnalyzer {
|
|
|
134
187
|
"validatecode",
|
|
135
188
|
"verifycode",
|
|
136
189
|
"checkcode",
|
|
190
|
+
// Encoding operations (base64, etc.)
|
|
191
|
+
"btoa", // Base64 encoding (often used with env vars for Basic Auth)
|
|
192
|
+
"atob", // Base64 decoding
|
|
193
|
+
"base64",
|
|
194
|
+
// Parsing operations (input processing, not output exposure)
|
|
195
|
+
"json.parse",
|
|
196
|
+
"json.stringify",
|
|
197
|
+
"parse(",
|
|
198
|
+
"parseint",
|
|
199
|
+
"parsefloat",
|
|
137
200
|
// Audit/internal logging (not external exposure)
|
|
138
201
|
"logadminpointhistory",
|
|
139
202
|
"adminpointhistory",
|
|
@@ -143,6 +206,15 @@ class S006SymbolBasedAnalyzer {
|
|
|
143
206
|
"logger.warn", // Warning logs are typically business messages, not sensitive data exposure
|
|
144
207
|
"logger.debug", // Debug logs are for development, not production exposure
|
|
145
208
|
"logger.info", // Info logs are for general information
|
|
209
|
+
"logger.error", // Error logs are internal, not user-facing
|
|
210
|
+
// Configuration and endpoint definitions
|
|
211
|
+
"config.",
|
|
212
|
+
"endpoint.",
|
|
213
|
+
"_url",
|
|
214
|
+
"_api",
|
|
215
|
+
"_endpoint",
|
|
216
|
+
// Environment variables (should be managed separately)
|
|
217
|
+
"process.env",
|
|
146
218
|
];
|
|
147
219
|
|
|
148
220
|
// Safe return/response patterns (just success messages, no actual codes)
|
|
@@ -170,6 +242,11 @@ class S006SymbolBasedAnalyzer {
|
|
|
170
242
|
const violations = [];
|
|
171
243
|
|
|
172
244
|
try {
|
|
245
|
+
// Skip configuration files - they contain config structures, not actual sensitive data
|
|
246
|
+
if (this.isConfigFile(filePath)) {
|
|
247
|
+
return violations; // Config files are safe
|
|
248
|
+
}
|
|
249
|
+
|
|
173
250
|
// Check return statements with sensitive codes
|
|
174
251
|
this.checkReturnStatements(sourceFile, filePath, violations);
|
|
175
252
|
|
|
@@ -194,6 +271,32 @@ class S006SymbolBasedAnalyzer {
|
|
|
194
271
|
return violations;
|
|
195
272
|
}
|
|
196
273
|
|
|
274
|
+
/**
|
|
275
|
+
* Check if file is a configuration file or Swagger schema file
|
|
276
|
+
*/
|
|
277
|
+
isConfigFile(filePath) {
|
|
278
|
+
const configPatterns = [
|
|
279
|
+
"/config/",
|
|
280
|
+
"/configs/",
|
|
281
|
+
"/configuration/",
|
|
282
|
+
"config.ts",
|
|
283
|
+
"config.js",
|
|
284
|
+
".config.ts",
|
|
285
|
+
".config.js",
|
|
286
|
+
"Config.ts",
|
|
287
|
+
"Config.js",
|
|
288
|
+
// Swagger/OpenAPI schema files - contain validation schema, not actual data
|
|
289
|
+
"/swagger/",
|
|
290
|
+
".swagger.ts",
|
|
291
|
+
".swagger.js",
|
|
292
|
+
"swagger.config",
|
|
293
|
+
"/openapi/",
|
|
294
|
+
".openapi.ts",
|
|
295
|
+
".openapi.js",
|
|
296
|
+
];
|
|
297
|
+
return configPatterns.some((pattern) => filePath.includes(pattern));
|
|
298
|
+
}
|
|
299
|
+
|
|
197
300
|
/**
|
|
198
301
|
* Check return statements that expose sensitive codes
|
|
199
302
|
* e.g., return { activationCode };
|
|
@@ -212,6 +315,22 @@ class S006SymbolBasedAnalyzer {
|
|
|
212
315
|
const objLiteral = expression;
|
|
213
316
|
const properties = objLiteral.getProperties();
|
|
214
317
|
|
|
318
|
+
// Skip if in infrastructure config builder function
|
|
319
|
+
const parentFunc = this.findParentFunctionName(returnStmt);
|
|
320
|
+
if (parentFunc) {
|
|
321
|
+
const funcNameLower = parentFunc.toLowerCase();
|
|
322
|
+
if (
|
|
323
|
+
funcNameLower.includes("buildenv") ||
|
|
324
|
+
funcNameLower.includes("getenv") ||
|
|
325
|
+
funcNameLower.includes("loadenv") ||
|
|
326
|
+
funcNameLower.includes("config") ||
|
|
327
|
+
funcNameLower.includes("setup") ||
|
|
328
|
+
funcNameLower === "env"
|
|
329
|
+
) {
|
|
330
|
+
continue; // Infrastructure config builders are safe
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
215
334
|
// Skip if return object contains only safe messages (success, message, etc.)
|
|
216
335
|
const hasOnlySafeProperties = properties.every((prop) => {
|
|
217
336
|
const name = prop.getName?.() || "";
|
|
@@ -283,6 +402,41 @@ class S006SymbolBasedAnalyzer {
|
|
|
283
402
|
const normalizedName = this.normalizeIdentifier(name);
|
|
284
403
|
|
|
285
404
|
if (this.isSensitiveIdentifier(normalizedName)) {
|
|
405
|
+
// Skip error constants, configuration objects, and form states
|
|
406
|
+
const parentVarName = this.findParentVariableName(objLiteral);
|
|
407
|
+
if (
|
|
408
|
+
parentVarName &&
|
|
409
|
+
(parentVarName.includes("ERROR") ||
|
|
410
|
+
parentVarName.includes("ERRORS") ||
|
|
411
|
+
parentVarName.includes("MESSAGE") ||
|
|
412
|
+
parentVarName.includes("MESSAGES") ||
|
|
413
|
+
parentVarName.includes("MAPPING") ||
|
|
414
|
+
parentVarName.includes("FIELDS") ||
|
|
415
|
+
parentVarName.includes("CONFIG") ||
|
|
416
|
+
parentVarName.includes("CONSTANT") ||
|
|
417
|
+
parentVarName.includes("ENDPOINT") ||
|
|
418
|
+
parentVarName.includes("API") ||
|
|
419
|
+
parentVarName.includes("URL") ||
|
|
420
|
+
parentVarName.includes("ROUTE") ||
|
|
421
|
+
parentVarName.includes("ROUTES") ||
|
|
422
|
+
parentVarName.includes("INITIAL") ||
|
|
423
|
+
parentVarName.includes("DEFAULT") ||
|
|
424
|
+
parentVarName.includes("STATE") ||
|
|
425
|
+
parentVarName.includes("FORM"))
|
|
426
|
+
) {
|
|
427
|
+
continue; // Error constants, configs, routes, and form states are safe
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
// Skip URL/endpoint configurations (e.g., set_password_url, generate_otp_api)
|
|
431
|
+
if (
|
|
432
|
+
normalizedName.endsWith("url") ||
|
|
433
|
+
normalizedName.endsWith("api") ||
|
|
434
|
+
normalizedName.endsWith("endpoint") ||
|
|
435
|
+
normalizedName.includes("apiname")
|
|
436
|
+
) {
|
|
437
|
+
continue; // URL/API endpoint configurations are not sensitive data
|
|
438
|
+
}
|
|
439
|
+
|
|
286
440
|
// Check if in exposure context (e.g., res.json, send, etc.)
|
|
287
441
|
const parent = this.findParentContext(objLiteral);
|
|
288
442
|
if (parent && this.isExposureContext(parent)) {
|
|
@@ -291,10 +445,75 @@ class S006SymbolBasedAnalyzer {
|
|
|
291
445
|
let currentParent = parent;
|
|
292
446
|
let depth = 0;
|
|
293
447
|
let isRequestBody = false;
|
|
448
|
+
let isFormState = false;
|
|
449
|
+
let isStateDispatch = false;
|
|
450
|
+
let isServiceCall = false;
|
|
294
451
|
|
|
295
452
|
while (currentParent && depth < 10) {
|
|
296
453
|
const parentText = currentParent.getText().toLowerCase();
|
|
297
454
|
|
|
455
|
+
// Check if this is a service/API call (client sending data to server)
|
|
456
|
+
// Method calls: AuthService.verify(), apiClient.post()
|
|
457
|
+
if (
|
|
458
|
+
parentText.includes("service.") ||
|
|
459
|
+
parentText.includes("api.") ||
|
|
460
|
+
parentText.includes("client.") ||
|
|
461
|
+
parentText.match(/\w+service\./i)
|
|
462
|
+
) {
|
|
463
|
+
isServiceCall = true;
|
|
464
|
+
break;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
// Function calls for authentication/verification (client to server)
|
|
468
|
+
// e.g., confirmResetPassword(), verifyOtp(), loginUser(), registerUser()
|
|
469
|
+
if (currentParent.getKind() === SyntaxKind.CallExpression) {
|
|
470
|
+
const callExpr = currentParent;
|
|
471
|
+
const expression = callExpr.getExpression();
|
|
472
|
+
const funcName = expression.getText().toLowerCase();
|
|
473
|
+
|
|
474
|
+
// Check if function name indicates API call
|
|
475
|
+
if (
|
|
476
|
+
funcName.includes("verify") ||
|
|
477
|
+
funcName.includes("confirm") ||
|
|
478
|
+
funcName.includes("reset") ||
|
|
479
|
+
funcName.includes("login") ||
|
|
480
|
+
funcName.includes("signup") ||
|
|
481
|
+
funcName.includes("register") ||
|
|
482
|
+
funcName.includes("authenticate") ||
|
|
483
|
+
funcName.includes("validate") ||
|
|
484
|
+
funcName.includes("check")
|
|
485
|
+
) {
|
|
486
|
+
isServiceCall = true;
|
|
487
|
+
break;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
// Check if this is state management dispatch
|
|
492
|
+
// e.g., dispatch(setRegisterInfo({})), setState({})
|
|
493
|
+
if (
|
|
494
|
+
parentText.includes("dispatch(") ||
|
|
495
|
+
parentText.includes("setstate") ||
|
|
496
|
+
parentText.includes("updatestate") ||
|
|
497
|
+
parentText.includes("setregister") ||
|
|
498
|
+
parentText.includes("setuser") ||
|
|
499
|
+
parentText.includes("setform")
|
|
500
|
+
) {
|
|
501
|
+
isStateDispatch = true;
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
// Check if this is form state (useState, useForm, etc.)
|
|
506
|
+
if (
|
|
507
|
+
parentText.includes("usestate") ||
|
|
508
|
+
parentText.includes("useform") ||
|
|
509
|
+
parentText.includes("initialstate") ||
|
|
510
|
+
parentText.includes("defaultvalues") ||
|
|
511
|
+
parentText.includes("formdata")
|
|
512
|
+
) {
|
|
513
|
+
isFormState = true;
|
|
514
|
+
break;
|
|
515
|
+
}
|
|
516
|
+
|
|
298
517
|
// Check if this object is being passed to JSON.stringify
|
|
299
518
|
// and is part of a fetch/axios request body
|
|
300
519
|
if (parentText.includes("json.stringify")) {
|
|
@@ -321,13 +540,24 @@ class S006SymbolBasedAnalyzer {
|
|
|
321
540
|
}
|
|
322
541
|
}
|
|
323
542
|
|
|
324
|
-
if (
|
|
543
|
+
if (
|
|
544
|
+
isRequestBody ||
|
|
545
|
+
isFormState ||
|
|
546
|
+
isStateDispatch ||
|
|
547
|
+
isServiceCall
|
|
548
|
+
)
|
|
549
|
+
break;
|
|
325
550
|
currentParent = currentParent.getParent();
|
|
326
551
|
depth++;
|
|
327
552
|
}
|
|
328
553
|
|
|
329
|
-
if (
|
|
330
|
-
|
|
554
|
+
if (
|
|
555
|
+
isRequestBody ||
|
|
556
|
+
isFormState ||
|
|
557
|
+
isStateDispatch ||
|
|
558
|
+
isServiceCall
|
|
559
|
+
) {
|
|
560
|
+
// This is request body, form state, state dispatch, or service call - user input data
|
|
331
561
|
// Not an exposure, so skip
|
|
332
562
|
continue;
|
|
333
563
|
}
|
|
@@ -370,23 +600,69 @@ class S006SymbolBasedAnalyzer {
|
|
|
370
600
|
const expression = callExpr.getExpression();
|
|
371
601
|
const expressionText = expression.getText().toLowerCase();
|
|
372
602
|
|
|
373
|
-
// Skip safe methods
|
|
603
|
+
// Skip safe methods (parsing, configuration, internal logging)
|
|
374
604
|
if (
|
|
375
605
|
this.safePatterns.some((pattern) => expressionText.includes(pattern))
|
|
376
606
|
) {
|
|
377
607
|
continue;
|
|
378
608
|
}
|
|
379
609
|
|
|
380
|
-
//
|
|
610
|
+
// Skip parsing operations (JSON.parse, parseInt, etc.) - these are INPUT processing, not OUTPUT exposure
|
|
611
|
+
if (
|
|
612
|
+
expressionText.includes("parse") ||
|
|
613
|
+
expressionText.includes("json.stringify") ||
|
|
614
|
+
expressionText.includes(".map(") ||
|
|
615
|
+
expressionText.includes(".filter(") ||
|
|
616
|
+
expressionText.includes(".reduce(")
|
|
617
|
+
) {
|
|
618
|
+
continue; // Data processing/transformation, not exposure
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
// Skip internal logging methods
|
|
622
|
+
if (
|
|
623
|
+
expressionText.includes("logger.") ||
|
|
624
|
+
expressionText.includes("httplog") ||
|
|
625
|
+
expressionText.includes("requestlog") ||
|
|
626
|
+
expressionText.includes("console.debug") ||
|
|
627
|
+
expressionText.includes("console.info")
|
|
628
|
+
) {
|
|
629
|
+
continue; // Internal logging is not external exposure
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
// Skip AWS Cognito admin operations (internal service management, not client exposure)
|
|
633
|
+
if (
|
|
634
|
+
expressionText.includes("cognito") &&
|
|
635
|
+
(expressionText.includes("adminsetuserpassword") ||
|
|
636
|
+
expressionText.includes("admindeleteuser") ||
|
|
637
|
+
expressionText.includes("adminresetuser") ||
|
|
638
|
+
expressionText.includes("deleteuserbyemail") ||
|
|
639
|
+
expressionText.includes("createuser") ||
|
|
640
|
+
expressionText.includes("updateuser"))
|
|
641
|
+
) {
|
|
642
|
+
continue; // AWS Cognito admin SDK calls - server-side operations, not client exposure
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
// Skip UI rendering functions (not data exposure)
|
|
646
|
+
if (
|
|
647
|
+
expressionText.startsWith("render") &&
|
|
648
|
+
!expressionText.includes("response") &&
|
|
649
|
+
!expressionText.includes("send") &&
|
|
650
|
+
!expressionText.includes("json")
|
|
651
|
+
) {
|
|
652
|
+
continue; // UI rendering components are safe
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
// Check for exposure methods (only actual external transmission)
|
|
381
656
|
const isExposureMethod =
|
|
382
|
-
expressionText.includes("json") ||
|
|
383
|
-
expressionText.includes("send") ||
|
|
384
|
-
expressionText.includes("
|
|
385
|
-
expressionText.includes("
|
|
657
|
+
expressionText.includes("res.json") ||
|
|
658
|
+
expressionText.includes("res.send") ||
|
|
659
|
+
expressionText.includes("response.json") ||
|
|
660
|
+
expressionText.includes("response.send") ||
|
|
386
661
|
expressionText.includes("email") ||
|
|
387
662
|
expressionText.includes("sms") ||
|
|
388
663
|
expressionText.includes("notify") ||
|
|
389
|
-
expressionText.includes("
|
|
664
|
+
expressionText.includes("ws.send") ||
|
|
665
|
+
expressionText.includes("websocket.send");
|
|
390
666
|
|
|
391
667
|
if (!isExposureMethod) continue;
|
|
392
668
|
|
|
@@ -440,6 +716,7 @@ class S006SymbolBasedAnalyzer {
|
|
|
440
716
|
}
|
|
441
717
|
|
|
442
718
|
if (this.containsSensitiveCode(arg)) {
|
|
719
|
+
// For other methods, use general check
|
|
443
720
|
const argText = arg.getText();
|
|
444
721
|
const match = argText.match(
|
|
445
722
|
/\b(activation|recovery|reset|verification|otp|code)\w*/i
|
|
@@ -498,10 +775,81 @@ class S006SymbolBasedAnalyzer {
|
|
|
498
775
|
continue;
|
|
499
776
|
}
|
|
500
777
|
|
|
778
|
+
// Skip safe patterns (btoa, process.env, etc.)
|
|
779
|
+
if (
|
|
780
|
+
this.safePatterns.some((pattern) => expressionText.includes(pattern))
|
|
781
|
+
) {
|
|
782
|
+
continue; // Safe encoding/config patterns
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Skip metadata access (.name, .length, .type, etc.) - these are not sensitive data
|
|
786
|
+
if (
|
|
787
|
+
expressionText.includes(".name") ||
|
|
788
|
+
expressionText.includes(".length") ||
|
|
789
|
+
expressionText.includes(".type") ||
|
|
790
|
+
expressionText.includes(".id") ||
|
|
791
|
+
expressionText.includes(".status") ||
|
|
792
|
+
expressionText.includes(".method")
|
|
793
|
+
) {
|
|
794
|
+
continue; // Metadata properties are not sensitive codes
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
// Skip if this is user input data (registerInfo, formData, etc.)
|
|
798
|
+
if (
|
|
799
|
+
expressionText.includes("registerinfo.") ||
|
|
800
|
+
expressionText.includes("formdata.") ||
|
|
801
|
+
expressionText.includes("formvalues.") ||
|
|
802
|
+
expressionText.includes("inputdata.")
|
|
803
|
+
) {
|
|
804
|
+
continue; // User input data, not server response
|
|
805
|
+
}
|
|
806
|
+
|
|
501
807
|
if (this.isSensitiveIdentifier(normalizedExpr)) {
|
|
502
808
|
// Check if in exposure context
|
|
503
809
|
const parent = this.findParentContext(template);
|
|
504
810
|
if (parent && this.isExposureContext(parent)) {
|
|
811
|
+
// Check if this is in fetch/axios body (server-to-server API call)
|
|
812
|
+
let currentParent = parent;
|
|
813
|
+
let depth = 0;
|
|
814
|
+
let isServerToServerCall = false;
|
|
815
|
+
|
|
816
|
+
while (currentParent && depth < 10) {
|
|
817
|
+
const parentText = currentParent.getText().toLowerCase();
|
|
818
|
+
|
|
819
|
+
// Check if this is fetch/axios body for external API
|
|
820
|
+
if (
|
|
821
|
+
(parentText.includes("body:") ||
|
|
822
|
+
parentText.includes("data:")) &&
|
|
823
|
+
(parentText.includes("fetch(") ||
|
|
824
|
+
parentText.includes("axios.") ||
|
|
825
|
+
parentText.includes("http.") ||
|
|
826
|
+
parentText.includes("https.") ||
|
|
827
|
+
parentText.includes("request("))
|
|
828
|
+
) {
|
|
829
|
+
// Check if calling external API (not internal endpoint)
|
|
830
|
+
if (
|
|
831
|
+
parentText.includes("http://") ||
|
|
832
|
+
parentText.includes("https://") ||
|
|
833
|
+
parentText.includes("api.") ||
|
|
834
|
+
parentText.includes("siteverify") ||
|
|
835
|
+
parentText.includes("oauth") ||
|
|
836
|
+
parentText.includes("recaptcha")
|
|
837
|
+
) {
|
|
838
|
+
isServerToServerCall = true;
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
currentParent = currentParent.getParent();
|
|
844
|
+
depth++;
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
if (isServerToServerCall) {
|
|
848
|
+
// This is server-to-server API call (e.g., verify captcha with Google)
|
|
849
|
+
// Secret keys in external API calls are acceptable
|
|
850
|
+
continue;
|
|
851
|
+
}
|
|
852
|
+
|
|
505
853
|
violations.push({
|
|
506
854
|
ruleId: this.ruleId,
|
|
507
855
|
severity: "error",
|
|
@@ -530,12 +878,39 @@ class S006SymbolBasedAnalyzer {
|
|
|
530
878
|
const name = iface.getName();
|
|
531
879
|
const normalizedName = name.toLowerCase();
|
|
532
880
|
|
|
533
|
-
//
|
|
881
|
+
// Skip request/input payloads - these are data sent TO the server (not exposed BY server)
|
|
882
|
+
if (
|
|
883
|
+
normalizedName.includes("request") ||
|
|
884
|
+
normalizedName.includes("input") ||
|
|
885
|
+
normalizedName.includes("update") ||
|
|
886
|
+
normalizedName.includes("create") ||
|
|
887
|
+
normalizedName.includes("submit") ||
|
|
888
|
+
normalizedName.includes("login") ||
|
|
889
|
+
normalizedName.includes("signup") ||
|
|
890
|
+
normalizedName.includes("register") ||
|
|
891
|
+
normalizedName.includes("change") ||
|
|
892
|
+
normalizedName.includes("reset") ||
|
|
893
|
+
normalizedName.includes("forgot") ||
|
|
894
|
+
normalizedName.includes("verify") ||
|
|
895
|
+
(normalizedName.includes("payload") &&
|
|
896
|
+
(normalizedName.includes("update") ||
|
|
897
|
+
normalizedName.includes("create") ||
|
|
898
|
+
normalizedName.includes("input") ||
|
|
899
|
+
normalizedName.includes("request")))
|
|
900
|
+
) {
|
|
901
|
+
continue; // Input payloads are safe - user sends data to server
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
// Check if it's a response/DTO type (output from server)
|
|
534
905
|
if (
|
|
535
906
|
normalizedName.includes("response") ||
|
|
536
907
|
normalizedName.includes("result") ||
|
|
537
|
-
normalizedName.includes("
|
|
538
|
-
normalizedName.includes("
|
|
908
|
+
normalizedName.includes("output") ||
|
|
909
|
+
(normalizedName.includes("dto") && !normalizedName.includes("input")) ||
|
|
910
|
+
(normalizedName.includes("payload") &&
|
|
911
|
+
(normalizedName.includes("response") ||
|
|
912
|
+
normalizedName.includes("result") ||
|
|
913
|
+
normalizedName.includes("output")))
|
|
539
914
|
) {
|
|
540
915
|
const properties = iface.getProperties();
|
|
541
916
|
|
|
@@ -656,12 +1031,31 @@ class S006SymbolBasedAnalyzer {
|
|
|
656
1031
|
return false; // Will be caught by containsSensitiveCode if it's actual password value
|
|
657
1032
|
}
|
|
658
1033
|
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
1034
|
+
// Exact match first
|
|
1035
|
+
if (this.sensitiveIdentifiers.has(normalizedName)) {
|
|
1036
|
+
return true;
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
// For partial match, be more strict to avoid false positives
|
|
1040
|
+
// Only match if sensitive word is at start or end, or clearly separated
|
|
1041
|
+
return Array.from(this.sensitiveIdentifiers).some((sensitive) => {
|
|
1042
|
+
// Skip very short words (2-3 chars) to avoid matching inside other words
|
|
1043
|
+
// e.g., "pin" should not match "dedupingInterval", "shopping", etc.
|
|
1044
|
+
if (sensitive.length <= 3) {
|
|
1045
|
+
// For short words, only match if:
|
|
1046
|
+
// 1. Exact match (already checked above)
|
|
1047
|
+
// 2. At start: "pin" matches "pincode", "pintester"
|
|
1048
|
+
// 3. At end: "pin" matches "loginpin", "resetpin"
|
|
1049
|
+
// 4. Standalone with separators (camelCase boundaries)
|
|
1050
|
+
return (
|
|
1051
|
+
normalizedName.startsWith(sensitive) ||
|
|
1052
|
+
normalizedName.endsWith(sensitive)
|
|
1053
|
+
);
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// For longer sensitive words (4+ chars), use includes as before
|
|
1057
|
+
return normalizedName.includes(sensitive);
|
|
1058
|
+
});
|
|
665
1059
|
}
|
|
666
1060
|
|
|
667
1061
|
/**
|
|
@@ -724,6 +1118,54 @@ class S006SymbolBasedAnalyzer {
|
|
|
724
1118
|
return false;
|
|
725
1119
|
}
|
|
726
1120
|
|
|
1121
|
+
/**
|
|
1122
|
+
* Helper: Find parent variable name for object literals
|
|
1123
|
+
*/
|
|
1124
|
+
findParentVariableName(node) {
|
|
1125
|
+
let current = node.getParent();
|
|
1126
|
+
let depth = 0;
|
|
1127
|
+
|
|
1128
|
+
while (current && depth < 5) {
|
|
1129
|
+
if (current.getKind() === SyntaxKind.VariableDeclaration) {
|
|
1130
|
+
return current.getName?.() || "";
|
|
1131
|
+
}
|
|
1132
|
+
current = current.getParent();
|
|
1133
|
+
depth++;
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
return null;
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
/**
|
|
1140
|
+
* Helper: Find parent function name for config builder detection
|
|
1141
|
+
*/
|
|
1142
|
+
findParentFunctionName(node) {
|
|
1143
|
+
let current = node.getParent();
|
|
1144
|
+
let depth = 0;
|
|
1145
|
+
|
|
1146
|
+
while (current && depth < 10) {
|
|
1147
|
+
if (
|
|
1148
|
+
current.getKind() === SyntaxKind.FunctionDeclaration ||
|
|
1149
|
+
current.getKind() === SyntaxKind.FunctionExpression ||
|
|
1150
|
+
current.getKind() === SyntaxKind.ArrowFunction
|
|
1151
|
+
) {
|
|
1152
|
+
// For named functions
|
|
1153
|
+
const funcName = current.getName?.();
|
|
1154
|
+
if (funcName) return funcName;
|
|
1155
|
+
|
|
1156
|
+
// For arrow functions assigned to const/let/var
|
|
1157
|
+
const parent = current.getParent();
|
|
1158
|
+
if (parent && parent.getKind() === SyntaxKind.VariableDeclaration) {
|
|
1159
|
+
return parent.getName?.() || "";
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
current = current.getParent();
|
|
1163
|
+
depth++;
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
return null;
|
|
1167
|
+
}
|
|
1168
|
+
|
|
727
1169
|
/**
|
|
728
1170
|
* Helper: Find parent context (call expression, return, etc.)
|
|
729
1171
|
*/
|
|
@@ -764,7 +1206,7 @@ class S006SymbolBasedAnalyzer {
|
|
|
764
1206
|
const text = node.getText().toLowerCase();
|
|
765
1207
|
const normalized = this.normalizeIdentifier(text);
|
|
766
1208
|
|
|
767
|
-
// Check for sensitive identifiers
|
|
1209
|
+
// Check for sensitive identifiers (variable/property access)
|
|
768
1210
|
if (this.isSensitiveIdentifier(normalized)) {
|
|
769
1211
|
return true;
|
|
770
1212
|
}
|