@sun-asterisk/sunlint 1.3.26 → 1.3.28
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 +101 -17
- package/config/rules/rules-registry-generated.json +22 -22
- package/origin-rules/security-en.md +351 -338
- package/package.json +1 -1
- package/rules/common/C003_no_vague_abbreviations/analyzer.js +73 -21
- package/rules/common/C017_constructor_logic/symbol-based-analyzer.js +206 -2
- package/rules/common/C024_no_scatter_hardcoded_constants/symbol-based-analyzer.js +553 -58
- package/rules/common/C029_catch_block_logging/analyzer.js +47 -12
- package/rules/common/C033_separate_service_repository/symbol-based-analyzer.js +35 -15
- package/rules/common/C041_no_sensitive_hardcode/symbol-based-analyzer.js +9 -5
- package/rules/security/S003_open_redirect_protection/README.md +371 -0
- package/rules/security/S003_open_redirect_protection/analyzer.js +135 -0
- package/rules/security/S003_open_redirect_protection/config.json +58 -0
- package/rules/security/S003_open_redirect_protection/symbol-based-analyzer.js +884 -0
- package/rules/security/S004_sensitive_data_logging/analyzer.js +135 -0
- package/rules/security/S004_sensitive_data_logging/config.json +62 -0
- package/rules/security/S004_sensitive_data_logging/symbol-based-analyzer.js +592 -0
- package/rules/security/S005_no_origin_auth/analyzer.js +97 -148
- package/rules/security/S005_no_origin_auth/config.json +28 -67
- package/rules/security/S005_no_origin_auth/symbol-based-analyzer.js +708 -0
- package/rules/security/S006_no_plaintext_recovery_codes/symbol-based-analyzer.js +170 -31
- package/rules/security/S010_no_insecure_encryption/analyzer.js +8 -2
- package/rules/security/S012_hardcoded_secrets/analyzer.js +149 -0
- package/rules/security/S012_hardcoded_secrets/config.json +75 -0
- package/rules/security/S012_hardcoded_secrets/symbol-based-analyzer.js +1204 -0
- package/rules/security/S013_tls_enforcement/symbol-based-analyzer.js +87 -0
- package/rules/security/S017_use_parameterized_queries/analyzer.js +11 -78
- package/rules/security/S017_use_parameterized_queries/symbol-based-analyzer.js +1146 -1
- package/rules/security/S019_smtp_injection_protection/analyzer.js +120 -0
- package/rules/security/S019_smtp_injection_protection/config.json +35 -0
- package/rules/security/S019_smtp_injection_protection/symbol-based-analyzer.js +687 -0
- package/rules/security/S020_no_eval_dynamic_code/analyzer.js +55 -130
- package/rules/security/S020_no_eval_dynamic_code/symbol-based-analyzer.js +4 -19
- package/rules/security/S022_escape_output_context/README.md +254 -0
- package/rules/security/S022_escape_output_context/analyzer.js +510 -0
- package/rules/security/S022_escape_output_context/config.json +229 -0
- package/rules/security/S023_no_json_injection/analyzer.js +15 -0
- package/rules/security/S023_no_json_injection/ast-analyzer.js +18 -3
- package/rules/security/S023_no_json_injection/config.json +133 -0
- package/rules/security/S024_xpath_xxe_protection/regex-based-analyzer.js +41 -0
- package/rules/security/S027_no_hardcoded_secrets/analyzer.js +67 -8
- package/rules/security/S027_no_hardcoded_secrets/categorized-analyzer.js +29 -6
- package/rules/security/S029_csrf_protection/config.json +127 -0
- package/rules/security/S030_directory_browsing_protection/regex-based-analyzer.js +160 -28
- package/rules/security/S030_directory_browsing_protection/symbol-based-analyzer.js +81 -19
- package/rules/security/S031_secure_session_cookies/analyzer.js +20 -2
- package/rules/security/S031_secure_session_cookies/regex-based-analyzer.js +100 -0
- package/rules/security/S031_secure_session_cookies/symbol-based-analyzer.js +8 -1
- package/rules/security/S032_httponly_session_cookies/analyzer.js +2 -2
- package/rules/security/S032_httponly_session_cookies/regex-based-analyzer.js +115 -0
- package/rules/security/S032_httponly_session_cookies/symbol-based-analyzer.js +39 -10
- package/rules/security/S036_lfi_rfi_protection/analyzer.js +224 -0
- package/rules/security/S036_lfi_rfi_protection/config.json +20 -0
- package/rules/security/S040_session_fixation_protection/analyzer.js +153 -0
- package/rules/security/S040_session_fixation_protection/config.json +20 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/README.md +83 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/analyzer.js +153 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/config.json +41 -0
- package/rules/security/S042_require_re_authentication_for_long_lived/symbol-based-analyzer.js +1139 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/README.md +107 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/analyzer.js +153 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/config.json +41 -0
- package/rules/security/S043_password_changes_invalidate_all_sessions/symbol-based-analyzer.js +541 -0
- package/docs/COMMAND-EXAMPLES.md +0 -390
- package/docs/FILE_LIMITS_COMPLETION_REPORT.md +0 -151
- package/docs/FOLDER_STRUCTURE.md +0 -59
- package/docs/SIMPLIFIED_USAGE_GUIDE.md +0 -208
- package/rules/security/S017_use_parameterized_queries/regex-based-analyzer.js +0 -541
- package/rules/security/S020_no_eval_dynamic_code/regex-based-analyzer.js +0 -307
|
@@ -39,6 +39,10 @@ class S006SymbolBasedAnalyzer {
|
|
|
39
39
|
"responsecode",
|
|
40
40
|
"agencycode",
|
|
41
41
|
"companycode",
|
|
42
|
+
"groupcode", // Group identifier (business code)
|
|
43
|
+
"teamcode", // Team identifier
|
|
44
|
+
"organizationcode", // Organization identifier
|
|
45
|
+
"secretariat", // Secretariat role/position name (not a secret code)
|
|
42
46
|
"eventcode",
|
|
43
47
|
"productcode",
|
|
44
48
|
"itemcode",
|
|
@@ -331,6 +335,45 @@ class S006SymbolBasedAnalyzer {
|
|
|
331
335
|
}
|
|
332
336
|
}
|
|
333
337
|
|
|
338
|
+
// Skip validation schemas returned from React hooks or validation builders
|
|
339
|
+
// Check if this is inside a React hook (useMemo, useCallback, etc.) or validation function
|
|
340
|
+
let currentParent = returnStmt.getParent();
|
|
341
|
+
let isValidationContext = false;
|
|
342
|
+
let depth = 0;
|
|
343
|
+
|
|
344
|
+
while (currentParent && depth < 10) {
|
|
345
|
+
const parentText = currentParent.getText().toLowerCase();
|
|
346
|
+
|
|
347
|
+
// Check for React hooks that typically return configuration/schema objects
|
|
348
|
+
if (
|
|
349
|
+
parentText.startsWith("usememo(") ||
|
|
350
|
+
parentText.startsWith("usecallback(") ||
|
|
351
|
+
parentText.startsWith("useeffect(")
|
|
352
|
+
) {
|
|
353
|
+
// Check if the returned object contains validation properties
|
|
354
|
+
const returnedText = expression.getText().toLowerCase();
|
|
355
|
+
if (
|
|
356
|
+
returnedText.includes("validate:") ||
|
|
357
|
+
returnedText.includes("validator:") ||
|
|
358
|
+
returnedText.includes("rules:") ||
|
|
359
|
+
returnedText.includes("onblur:") ||
|
|
360
|
+
returnedText.includes("onchange:") ||
|
|
361
|
+
returnedText.includes("checkRequired") ||
|
|
362
|
+
returnedText.includes("checkMinLength")
|
|
363
|
+
) {
|
|
364
|
+
isValidationContext = true;
|
|
365
|
+
break;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
currentParent = currentParent.getParent();
|
|
370
|
+
depth++;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (isValidationContext) {
|
|
374
|
+
continue; // Validation schema from React hook - safe
|
|
375
|
+
}
|
|
376
|
+
|
|
334
377
|
// Skip if return object contains only safe messages (success, message, etc.)
|
|
335
378
|
const hasOnlySafeProperties = properties.every((prop) => {
|
|
336
379
|
const name = prop.getName?.() || "";
|
|
@@ -404,27 +447,63 @@ class S006SymbolBasedAnalyzer {
|
|
|
404
447
|
if (this.isSensitiveIdentifier(normalizedName)) {
|
|
405
448
|
// Skip error constants, configuration objects, and form states
|
|
406
449
|
const parentVarName = this.findParentVariableName(objLiteral);
|
|
450
|
+
const parentVarNameUpper = parentVarName?.toUpperCase() || "";
|
|
407
451
|
if (
|
|
408
452
|
parentVarName &&
|
|
409
|
-
(
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
453
|
+
(parentVarNameUpper.includes("ERROR") ||
|
|
454
|
+
parentVarNameUpper.includes("ERRORS") ||
|
|
455
|
+
parentVarNameUpper.includes("MESSAGE") ||
|
|
456
|
+
parentVarNameUpper.includes("MESSAGES") ||
|
|
457
|
+
parentVarNameUpper.includes("MAPPING") ||
|
|
458
|
+
parentVarNameUpper.includes("FIELDS") ||
|
|
459
|
+
parentVarNameUpper.includes("CONFIG") ||
|
|
460
|
+
parentVarNameUpper.includes("CONSTANT") ||
|
|
461
|
+
parentVarNameUpper.includes("ENDPOINT") ||
|
|
462
|
+
parentVarNameUpper.includes("API") ||
|
|
463
|
+
parentVarNameUpper.includes("URL") ||
|
|
464
|
+
parentVarNameUpper.includes("ROUTE") ||
|
|
465
|
+
parentVarNameUpper.includes("ROUTES") ||
|
|
466
|
+
parentVarNameUpper.includes("INITIAL") ||
|
|
467
|
+
parentVarNameUpper.includes("DEFAULT") ||
|
|
468
|
+
parentVarNameUpper.includes("STATE") ||
|
|
469
|
+
parentVarNameUpper.includes("FORM") ||
|
|
470
|
+
parentVarNameUpper.includes("VALIDATION") ||
|
|
471
|
+
parentVarNameUpper.includes("SCHEMA") ||
|
|
472
|
+
parentVarNameUpper.includes("RULES") ||
|
|
473
|
+
parentVarNameUpper.includes("PATTERN") ||
|
|
474
|
+
parentVarNameUpper.includes("REGEX") ||
|
|
475
|
+
parentVarNameUpper.includes("REGEXP"))
|
|
426
476
|
) {
|
|
427
|
-
continue; // Error constants, configs, routes,
|
|
477
|
+
continue; // Error constants, configs, routes, form states, validation schemas, and regex patterns are safe
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Skip masking/sanitization utility configurations
|
|
481
|
+
// These are security utilities that PROTECT sensitive data, not expose them
|
|
482
|
+
if (
|
|
483
|
+
parentVarName &&
|
|
484
|
+
(parentVarNameUpper.includes("MASK") ||
|
|
485
|
+
parentVarNameUpper.includes("SANITIZE") ||
|
|
486
|
+
parentVarNameUpper.includes("REDACT") ||
|
|
487
|
+
parentVarNameUpper.includes("HIDE") ||
|
|
488
|
+
parentVarNameUpper.includes("OBFUSCATE"))
|
|
489
|
+
) {
|
|
490
|
+
continue; // Masking/sanitization utilities are safe
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// Check if inside a masking/sanitization function
|
|
494
|
+
const parentFuncName = this.findParentFunctionName(objLiteral);
|
|
495
|
+
const parentFuncNameUpper = parentFuncName?.toUpperCase() || "";
|
|
496
|
+
if (
|
|
497
|
+
parentFuncName &&
|
|
498
|
+
(parentFuncNameUpper.includes("MASK") ||
|
|
499
|
+
parentFuncNameUpper.includes("SANITIZE") ||
|
|
500
|
+
parentFuncNameUpper.includes("REDACT") ||
|
|
501
|
+
parentFuncNameUpper.includes("HIDE") ||
|
|
502
|
+
parentFuncNameUpper.includes("OBFUSCATE") ||
|
|
503
|
+
parentFuncNameUpper.includes("STRIP") ||
|
|
504
|
+
parentFuncNameUpper.includes("REMOVE"))
|
|
505
|
+
) {
|
|
506
|
+
continue; // Inside masking/sanitization function - safe
|
|
428
507
|
}
|
|
429
508
|
|
|
430
509
|
// Skip URL/endpoint configurations (e.g., set_password_url, generate_otp_api)
|
|
@@ -437,6 +516,32 @@ class S006SymbolBasedAnalyzer {
|
|
|
437
516
|
continue; // URL/API endpoint configurations are not sensitive data
|
|
438
517
|
}
|
|
439
518
|
|
|
519
|
+
// Skip validation schema configurations
|
|
520
|
+
// Check if this property's value contains validation-related properties
|
|
521
|
+
const propValue = prop.getInitializer();
|
|
522
|
+
if (propValue && propValue.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
523
|
+
const valueText = propValue.getText().toLowerCase();
|
|
524
|
+
// If the property value contains validation keywords, it's a validation schema
|
|
525
|
+
if (
|
|
526
|
+
valueText.includes("validate:") ||
|
|
527
|
+
valueText.includes("validator:") ||
|
|
528
|
+
valueText.includes("rules:") ||
|
|
529
|
+
valueText.includes("onblur:") ||
|
|
530
|
+
valueText.includes("onchange:") ||
|
|
531
|
+
valueText.includes("checkRequired") ||
|
|
532
|
+
valueText.includes("checkMinLength") ||
|
|
533
|
+
valueText.includes("checkMaxLength")
|
|
534
|
+
) {
|
|
535
|
+
continue; // Validation schema configuration - safe
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
// Skip regex pattern definitions
|
|
540
|
+
// If the property value is a RegExp literal (e.g., PASSWORD: /^[a-z]+$/), it's a pattern definition
|
|
541
|
+
if (propValue && propValue.getKind() === SyntaxKind.RegularExpressionLiteral) {
|
|
542
|
+
continue; // Regex pattern definition - safe
|
|
543
|
+
}
|
|
544
|
+
|
|
440
545
|
// Check if in exposure context (e.g., res.json, send, etc.)
|
|
441
546
|
const parent = this.findParentContext(objLiteral);
|
|
442
547
|
if (parent && this.isExposureContext(parent)) {
|
|
@@ -1026,10 +1131,9 @@ class S006SymbolBasedAnalyzer {
|
|
|
1026
1131
|
return false; // Generic "code" alone is not sensitive
|
|
1027
1132
|
}
|
|
1028
1133
|
|
|
1029
|
-
//
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
}
|
|
1134
|
+
// Note: "password" as a property name in data objects IS sensitive
|
|
1135
|
+
// Only skip "password" in very specific safe contexts (handled by safePasswordPatterns)
|
|
1136
|
+
// This is checked below via sensitiveIdentifiers.has()
|
|
1033
1137
|
|
|
1034
1138
|
// Exact match first
|
|
1035
1139
|
if (this.sensitiveIdentifiers.has(normalizedName)) {
|
|
@@ -1203,24 +1307,59 @@ class S006SymbolBasedAnalyzer {
|
|
|
1203
1307
|
* Helper: Check if argument contains sensitive code
|
|
1204
1308
|
*/
|
|
1205
1309
|
containsSensitiveCode(node) {
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
// Check for sensitive identifiers (variable/property access)
|
|
1210
|
-
if (this.isSensitiveIdentifier(normalized)) {
|
|
1211
|
-
return true;
|
|
1212
|
-
}
|
|
1213
|
-
|
|
1214
|
-
// Check child nodes (for object literals, etc.)
|
|
1310
|
+
// PRIORITY 1: For object literals, check property names AND values
|
|
1311
|
+
// This prevents false positives from matching keywords in surrounding context
|
|
1215
1312
|
if (node.getKind() === SyntaxKind.ObjectLiteralExpression) {
|
|
1216
1313
|
const properties = node.getProperties();
|
|
1217
1314
|
for (const prop of properties) {
|
|
1315
|
+
// Check property name
|
|
1218
1316
|
const propName = prop.getName?.() || "";
|
|
1219
1317
|
const normalizedPropName = this.normalizeIdentifier(propName);
|
|
1220
1318
|
if (this.isSensitiveIdentifier(normalizedPropName)) {
|
|
1221
1319
|
return true;
|
|
1222
1320
|
}
|
|
1321
|
+
|
|
1322
|
+
// ALSO check property value (important for nested structures)
|
|
1323
|
+
// e.g., { htmlBody: mapDataToTemplate({ password }) }
|
|
1324
|
+
if (prop.getKind() === SyntaxKind.PropertyAssignment) {
|
|
1325
|
+
const initializer = prop.getInitializer();
|
|
1326
|
+
if (initializer && this.containsSensitiveCode(initializer)) {
|
|
1327
|
+
return true;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1223
1330
|
}
|
|
1331
|
+
return false; // Object literal checked, no sensitive props or values
|
|
1332
|
+
}
|
|
1333
|
+
|
|
1334
|
+
// PRIORITY 2: For call expressions, recursively check arguments
|
|
1335
|
+
// This handles cases like mapDataToTemplate({ password })
|
|
1336
|
+
if (node.getKind() === SyntaxKind.CallExpression) {
|
|
1337
|
+
const args = node.getArguments();
|
|
1338
|
+
for (const arg of args) {
|
|
1339
|
+
if (this.containsSensitiveCode(arg)) {
|
|
1340
|
+
return true;
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
return false; // Call expression checked, no sensitive args
|
|
1344
|
+
}
|
|
1345
|
+
|
|
1346
|
+
// PRIORITY 3: For simple identifiers, check the identifier name only
|
|
1347
|
+
if (node.getKind() === SyntaxKind.Identifier) {
|
|
1348
|
+
const text = node.getText().toLowerCase();
|
|
1349
|
+
const normalized = this.normalizeIdentifier(text);
|
|
1350
|
+
return this.isSensitiveIdentifier(normalized);
|
|
1351
|
+
}
|
|
1352
|
+
|
|
1353
|
+
// PRIORITY 4: For other node types, only check if text is short (not complex expression)
|
|
1354
|
+
const text = node.getText();
|
|
1355
|
+
if (text.length > 100) {
|
|
1356
|
+
// Long text likely contains surrounding context - skip to avoid false positives
|
|
1357
|
+
return false;
|
|
1358
|
+
}
|
|
1359
|
+
|
|
1360
|
+
const normalized = this.normalizeIdentifier(text.toLowerCase());
|
|
1361
|
+
if (this.isSensitiveIdentifier(normalized)) {
|
|
1362
|
+
return true;
|
|
1224
1363
|
}
|
|
1225
1364
|
|
|
1226
1365
|
// Check template spans
|
|
@@ -519,8 +519,14 @@ class S010Analyzer {
|
|
|
519
519
|
return true;
|
|
520
520
|
}
|
|
521
521
|
|
|
522
|
-
// Check for performance.now() -
|
|
523
|
-
|
|
522
|
+
// Check for performance.now() - ONLY flag if used for random generation, NOT for timing
|
|
523
|
+
// Safe: const start = performance.now(); const duration = performance.now() - start;
|
|
524
|
+
// Unsafe: const token = performance.now().toString();
|
|
525
|
+
if (/performance\.now\s*\(\).*\.(?:toString|toFixed|toPrecision|slice|substr|substring)/.test(text)) {
|
|
526
|
+
return true;
|
|
527
|
+
}
|
|
528
|
+
// Also flag if used with encoding: btoa(performance.now())
|
|
529
|
+
if (/btoa\s*\(.*performance\.now/.test(text)) {
|
|
524
530
|
return true;
|
|
525
531
|
}
|
|
526
532
|
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Rule S012: Hardcoded Secrets Detection - Analyzer Wrapper
|
|
3
|
+
*
|
|
4
|
+
* This analyzer wraps the symbol-based analyzer to provide a consistent
|
|
5
|
+
* interface for the SunLint engine.
|
|
6
|
+
*/
|
|
7
|
+
// Command: node cli.js --rule=S012 --input=examples/rule-test-fixtures/rules/S012_hardcoded_secrets --engine=heuristic
|
|
8
|
+
|
|
9
|
+
const fs = require("fs");
|
|
10
|
+
const path = require("path");
|
|
11
|
+
const { Project } = require("ts-morph");
|
|
12
|
+
const S012SymbolBasedAnalyzer = require("./symbol-based-analyzer");
|
|
13
|
+
|
|
14
|
+
class S012Analyzer {
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.ruleId = "S012";
|
|
17
|
+
this.semanticEngine = options.semanticEngine || null;
|
|
18
|
+
this.symbolAnalyzer = new S012SymbolBasedAnalyzer(this.semanticEngine);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Analyze multiple files
|
|
23
|
+
*/
|
|
24
|
+
async analyze(files, language, options = {}) {
|
|
25
|
+
const violations = [];
|
|
26
|
+
|
|
27
|
+
for (const filePath of files) {
|
|
28
|
+
try {
|
|
29
|
+
const fileViolations = await this.analyzeFile(filePath, options);
|
|
30
|
+
violations.push(...fileViolations);
|
|
31
|
+
} catch (error) {
|
|
32
|
+
console.error(
|
|
33
|
+
`[S012] Error analyzing file ${filePath}:`,
|
|
34
|
+
error.message
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return violations;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Analyze a single file
|
|
44
|
+
*/
|
|
45
|
+
async analyzeFile(filePath, options = {}) {
|
|
46
|
+
// Skip files that shouldn't be analyzed
|
|
47
|
+
if (this.shouldSkipFile(filePath)) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
try {
|
|
52
|
+
// Check if file exists
|
|
53
|
+
if (!fs.existsSync(filePath)) {
|
|
54
|
+
return [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const content = fs.readFileSync(filePath, "utf-8");
|
|
58
|
+
|
|
59
|
+
// Try to get source file from semantic engine
|
|
60
|
+
let sourceFile = this.semanticEngine?.project?.getSourceFile(filePath);
|
|
61
|
+
|
|
62
|
+
// If not available, create a temporary ts-morph source file
|
|
63
|
+
if (!sourceFile) {
|
|
64
|
+
const tempProject = new Project({
|
|
65
|
+
useInMemoryFileSystem: true,
|
|
66
|
+
compilerOptions: {
|
|
67
|
+
allowJs: true,
|
|
68
|
+
noLib: true,
|
|
69
|
+
target: 99, // ESNext
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
sourceFile = tempProject.createSourceFile(
|
|
74
|
+
path.basename(filePath),
|
|
75
|
+
content
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Run the symbol-based analyzer
|
|
80
|
+
const symbolViolations = await this.symbolAnalyzer.analyze(
|
|
81
|
+
sourceFile,
|
|
82
|
+
filePath
|
|
83
|
+
);
|
|
84
|
+
|
|
85
|
+
// Add file path to each violation
|
|
86
|
+
return symbolViolations.map((violation) => ({
|
|
87
|
+
...violation,
|
|
88
|
+
filePath,
|
|
89
|
+
file: filePath,
|
|
90
|
+
}));
|
|
91
|
+
} catch (error) {
|
|
92
|
+
console.error(
|
|
93
|
+
`[S012] Error in analyzeFile for ${filePath}:`,
|
|
94
|
+
error.message
|
|
95
|
+
);
|
|
96
|
+
return [];
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Check if file should be skipped
|
|
102
|
+
*/
|
|
103
|
+
shouldSkipFile(filePath) {
|
|
104
|
+
const skipPatterns = [
|
|
105
|
+
/test\//i,
|
|
106
|
+
/tests\//i,
|
|
107
|
+
/__tests__\//i,
|
|
108
|
+
/\.test\./i,
|
|
109
|
+
/\.spec\./i,
|
|
110
|
+
/node_modules\//i,
|
|
111
|
+
/dist\//i,
|
|
112
|
+
/build\//i,
|
|
113
|
+
/\.next\//i,
|
|
114
|
+
/\.nuxt\//i,
|
|
115
|
+
/coverage\//i,
|
|
116
|
+
/\.d\.ts$/i,
|
|
117
|
+
/\.min\.js$/i,
|
|
118
|
+
/\.bundle\.js$/i,
|
|
119
|
+
/\.example\./i,
|
|
120
|
+
/\.sample\./i,
|
|
121
|
+
/\.template\./i,
|
|
122
|
+
/vendor\//i,
|
|
123
|
+
/third[_-]party\//i,
|
|
124
|
+
/\.git\//i,
|
|
125
|
+
];
|
|
126
|
+
|
|
127
|
+
return skipPatterns.some((pattern) => pattern.test(filePath));
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get rule metadata
|
|
132
|
+
*/
|
|
133
|
+
getRuleMetadata() {
|
|
134
|
+
const configPath = path.join(__dirname, "config.json");
|
|
135
|
+
if (fs.existsSync(configPath)) {
|
|
136
|
+
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
137
|
+
return config;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
ruleId: this.ruleId,
|
|
141
|
+
name: "Hardcoded Secrets Protection",
|
|
142
|
+
description: "Detects hardcoded secrets in source code",
|
|
143
|
+
category: "security",
|
|
144
|
+
severity: "error",
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
module.exports = S012Analyzer;
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
{
|
|
2
|
+
"ruleId": "S012",
|
|
3
|
+
"name": "Hardcoded Secrets Protection",
|
|
4
|
+
"description": "Detects hardcoded secrets, API keys, passwords, tokens, and credentials in source code to prevent accidental exposure through version control",
|
|
5
|
+
"category": "security",
|
|
6
|
+
"severity": "error",
|
|
7
|
+
"languages": ["All languages"],
|
|
8
|
+
"tags": [
|
|
9
|
+
"security",
|
|
10
|
+
"owasp",
|
|
11
|
+
"secrets",
|
|
12
|
+
"credentials",
|
|
13
|
+
"cryptographic-failures",
|
|
14
|
+
"hardcoded-secrets",
|
|
15
|
+
"api-keys",
|
|
16
|
+
"passwords",
|
|
17
|
+
"tokens"
|
|
18
|
+
],
|
|
19
|
+
"enabled": true,
|
|
20
|
+
"fixable": false,
|
|
21
|
+
"engine": "heuristic",
|
|
22
|
+
"metadata": {
|
|
23
|
+
"owaspCategory": "A02:2021 - Cryptographic Failures",
|
|
24
|
+
"cweId": "CWE-798",
|
|
25
|
+
"cweDescription": "Use of Hard-coded Credentials",
|
|
26
|
+
"description": "Hardcoding secrets in source code is a critical security vulnerability. Secrets can be exposed through version control history, code sharing, or unauthorized access. This rule detects various patterns of hardcoded credentials including API keys, passwords, tokens, private keys, and connection strings.",
|
|
27
|
+
"impact": "Critical - Credential exposure, unauthorized access, data breaches, compliance violations (SOC2, ISO27001, PCI-DSS)",
|
|
28
|
+
"likelihood": "High",
|
|
29
|
+
"remediation": "Use environment variables (process.env), secret management systems (AWS Secrets Manager, HashiCorp Vault, Azure Key Vault, GCP Secret Manager), or configuration management tools. Never commit secrets to version control. Use .gitignore for .env files. Consider using git-secrets or similar tools to prevent accidental commits.",
|
|
30
|
+
"examples": {
|
|
31
|
+
"bad": [
|
|
32
|
+
"const API_KEY = 'AIzaSyD-1234567890abcdef';",
|
|
33
|
+
"const password = 'MySecretPassword123!';",
|
|
34
|
+
"const connectionString = 'mongodb://admin:password@localhost:27017';",
|
|
35
|
+
"const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...';",
|
|
36
|
+
"const awsKey = 'AKIAIOSFODNN7EXAMPLE';"
|
|
37
|
+
],
|
|
38
|
+
"good": [
|
|
39
|
+
"const API_KEY = process.env.API_KEY;",
|
|
40
|
+
"const password = config.get('database.password');",
|
|
41
|
+
"const connectionString = process.env.MONGODB_URI;",
|
|
42
|
+
"const token = await secretsManager.getSecret('jwt-secret');",
|
|
43
|
+
"const awsKey = process.env.AWS_ACCESS_KEY_ID;"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"references": [
|
|
47
|
+
"https://owasp.org/Top10/A02_2021-Cryptographic_Failures/",
|
|
48
|
+
"https://cwe.mitre.org/data/definitions/798.html",
|
|
49
|
+
"https://cheatsheetseries.owasp.org/cheatsheets/Secrets_Management_Cheat_Sheet.html"
|
|
50
|
+
],
|
|
51
|
+
"frameworks": [
|
|
52
|
+
"Node.js",
|
|
53
|
+
"Express",
|
|
54
|
+
"NestJS",
|
|
55
|
+
"Next.js",
|
|
56
|
+
"React",
|
|
57
|
+
"Vue",
|
|
58
|
+
"Angular"
|
|
59
|
+
],
|
|
60
|
+
"secretTypes": [
|
|
61
|
+
"API Keys",
|
|
62
|
+
"Passwords",
|
|
63
|
+
"Access Tokens",
|
|
64
|
+
"Private Keys",
|
|
65
|
+
"JWT Secrets",
|
|
66
|
+
"Database Credentials",
|
|
67
|
+
"OAuth Secrets",
|
|
68
|
+
"AWS Keys",
|
|
69
|
+
"GitHub Tokens",
|
|
70
|
+
"Slack Tokens"
|
|
71
|
+
],
|
|
72
|
+
"detectionPatterns": 50,
|
|
73
|
+
"testCases": 30
|
|
74
|
+
}
|
|
75
|
+
}
|