@sun-asterisk/sunlint 1.3.43 → 1.3.44

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.
Files changed (137) hide show
  1. package/dart_analyzer/README.md +226 -0
  2. package/dart_analyzer/analysis_options.yaml +66 -0
  3. package/dart_analyzer/bin/sunlint-dart-macos +0 -0
  4. package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
  5. package/dart_analyzer/lib/analyzer_service.dart +625 -0
  6. package/dart_analyzer/lib/json_rpc_server.dart +275 -0
  7. package/dart_analyzer/lib/models/rule.dart +67 -0
  8. package/dart_analyzer/lib/models/symbol_table.dart +607 -0
  9. package/dart_analyzer/lib/models/violation.dart +69 -0
  10. package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
  11. package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
  12. package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
  13. package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
  14. package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
  15. package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
  16. package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
  17. package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
  18. package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
  19. package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
  20. package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
  21. package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
  22. package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
  23. package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
  24. package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
  25. package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
  26. package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
  27. package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
  28. package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
  29. package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
  30. package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
  31. package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
  32. package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
  33. package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
  34. package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
  35. package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
  36. package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
  37. package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
  38. package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
  39. package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
  40. package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
  41. package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
  42. package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
  43. package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
  44. package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
  45. package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
  46. package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
  47. package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
  48. package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
  49. package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
  50. package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
  51. package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
  52. package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
  53. package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
  54. package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
  55. package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
  56. package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
  57. package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
  58. package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
  59. package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
  60. package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
  61. package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
  62. package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
  63. package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
  64. package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
  65. package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
  66. package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
  67. package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
  68. package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
  69. package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
  70. package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
  71. package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
  72. package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
  73. package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
  74. package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
  75. package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
  76. package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
  77. package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
  78. package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
  79. package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
  80. package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
  81. package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
  82. package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
  83. package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
  84. package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
  85. package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
  86. package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
  87. package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
  88. package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
  89. package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
  90. package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
  91. package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
  92. package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
  93. package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
  94. package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
  95. package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
  96. package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
  97. package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
  98. package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
  99. package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
  100. package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
  101. package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
  102. package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
  103. package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
  104. package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
  105. package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
  106. package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
  107. package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
  108. package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
  109. package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
  110. package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
  111. package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
  112. package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
  113. package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
  114. package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
  115. package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
  116. package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
  117. package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
  118. package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
  119. package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
  120. package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
  121. package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
  122. package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
  123. package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
  124. package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
  125. package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
  126. package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
  127. package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
  128. package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
  129. package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
  130. package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
  131. package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
  132. package/dart_analyzer/lib/utils/common_utils.dart +26 -0
  133. package/dart_analyzer/pubspec.lock +557 -0
  134. package/dart_analyzer/pubspec.yaml +39 -0
  135. package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
  136. package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
  137. package/package.json +3 -2
@@ -0,0 +1,208 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/dart/ast/visitor.dart';
3
+ import 'package:analyzer/source/line_info.dart';
4
+
5
+ import '../../models/rule.dart';
6
+ import '../../models/violation.dart';
7
+ import '../base_analyzer.dart';
8
+
9
+ /// S003: Open Redirect Protection
10
+ /// Prevents open redirect vulnerabilities by detecting:
11
+ /// - URL redirects using user input without validation
12
+ /// - External URL launching with user input
13
+ /// Note: Internal Flutter app navigation is NOT flagged (not a security issue)
14
+ class S003OpenRedirectProtectionAnalyzer extends BaseAnalyzer {
15
+ @override
16
+ String get ruleId => 'S003';
17
+
18
+ /// External redirect methods that could be vulnerable
19
+ /// Note: Internal Flutter navigation (push, go, navigate, replace) are NOT included
20
+ /// because they use typed routes, not external URLs
21
+ static final Set<String> _externalRedirectMethods = {
22
+ // Web/HTTP redirects (server-side)
23
+ 'redirect',
24
+ 'redirectTo',
25
+ 'sendRedirect',
26
+ 'temporaryRedirect',
27
+ 'permanentRedirect',
28
+ 'movedPermanently',
29
+ 'movedTemporarily',
30
+ // URL launchers (opens external browser/apps)
31
+ 'launchUrl',
32
+ 'launch',
33
+ 'launchUrlString',
34
+ 'openUrl',
35
+ 'canLaunchUrl',
36
+ // Web location changes (NOT Flutter router.replace which uses typed routes)
37
+ 'assign',
38
+ // 'replace' - REMOVED: conflicts with Flutter router.replace()
39
+ // window.location.replace is detected by checking target is 'location'
40
+ };
41
+
42
+ /// User input sources that could contain malicious URLs
43
+ static final Set<String> _externalUserInputPatterns = {
44
+ 'request',
45
+ 'queryParameters',
46
+ 'queryParams',
47
+ 'formData',
48
+ 'body',
49
+ 'returnUrl',
50
+ 'returnTo',
51
+ 'redirectUrl',
52
+ 'nextUrl',
53
+ 'callbackUrl',
54
+ 'targetUrl',
55
+ 'destination',
56
+ 'goto',
57
+ };
58
+
59
+ /// Validation/allowlist methods
60
+ static final Set<String> _validationMethods = {
61
+ 'isAllowed',
62
+ 'isValid',
63
+ 'isValidUrl',
64
+ 'isSafeUrl',
65
+ 'isTrusted',
66
+ 'isWhitelisted',
67
+ 'isAllowlisted',
68
+ 'validateUrl',
69
+ 'checkUrl',
70
+ 'verifyUrl',
71
+ 'sanitizeUrl',
72
+ 'startsWith',
73
+ 'contains',
74
+ };
75
+
76
+ /// Files to skip (Flutter UI, generated code)
77
+ static const _skipFilePatterns = [
78
+ '_screen', 'screen.dart', '_page', 'page.dart',
79
+ '_widget', 'widget.dart', '_view', 'view.dart',
80
+ '.gr.dart', '.g.dart', 'router', 'route',
81
+ ];
82
+
83
+ @override
84
+ List<Violation> analyze({
85
+ required CompilationUnit unit,
86
+ required String filePath,
87
+ required Rule rule,
88
+ required LineInfo lineInfo,
89
+ }) {
90
+ // Skip Flutter UI files - internal navigation is not a security issue
91
+ final fileName = filePath.toLowerCase();
92
+ if (_skipFilePatterns.any((p) => fileName.contains(p))) {
93
+ return [];
94
+ }
95
+
96
+ final violations = <Violation>[];
97
+
98
+ final visitor = _OpenRedirectVisitor(
99
+ filePath: filePath,
100
+ lineInfo: lineInfo,
101
+ violations: violations,
102
+ analyzer: this,
103
+ );
104
+
105
+ unit.accept(visitor);
106
+
107
+ return violations;
108
+ }
109
+
110
+ /// Check if method launches external URLs (not internal navigation)
111
+ static bool isExternalRedirectMethod(String name) {
112
+ return _externalRedirectMethods.contains(name) ||
113
+ name.toLowerCase() == 'launchurl' ||
114
+ name.toLowerCase() == 'openurl';
115
+ }
116
+
117
+ /// Check if expression contains external user input
118
+ static bool containsExternalUserInput(String source) {
119
+ final lowerSource = source.toLowerCase();
120
+ return _externalUserInputPatterns.any((pattern) => lowerSource.contains(pattern));
121
+ }
122
+
123
+ /// Check if validation is present
124
+ static bool hasValidation(String source) {
125
+ final lowerSource = source.toLowerCase();
126
+ return _validationMethods.any((method) => lowerSource.contains(method));
127
+ }
128
+ }
129
+
130
+ class _OpenRedirectVisitor extends RecursiveAstVisitor<void> {
131
+ final String filePath;
132
+ final LineInfo lineInfo;
133
+ final List<Violation> violations;
134
+ final S003OpenRedirectProtectionAnalyzer analyzer;
135
+
136
+ _OpenRedirectVisitor({
137
+ required this.filePath,
138
+ required this.lineInfo,
139
+ required this.violations,
140
+ required this.analyzer,
141
+ });
142
+
143
+ @override
144
+ void visitMethodInvocation(MethodInvocation node) {
145
+ final methodName = node.methodName.name;
146
+
147
+ // Only check external redirect methods (launchUrl, etc.)
148
+ if (S003OpenRedirectProtectionAnalyzer.isExternalRedirectMethod(methodName)) {
149
+ _checkExternalRedirect(node);
150
+ }
151
+
152
+ super.visitMethodInvocation(node);
153
+ }
154
+
155
+ void _checkExternalRedirect(MethodInvocation node) {
156
+ for (final arg in node.argumentList.arguments) {
157
+ final argSource = arg.toSource();
158
+
159
+ // Skip string literals - hardcoded URLs are safe
160
+ if (arg is SimpleStringLiteral || arg is AdjacentStrings) {
161
+ continue;
162
+ }
163
+
164
+ // Skip const values
165
+ if (argSource.contains('Constants.') || argSource.contains('const ')) {
166
+ continue;
167
+ }
168
+
169
+ // Check if argument contains external user input
170
+ if (S003OpenRedirectProtectionAnalyzer.containsExternalUserInput(argSource)) {
171
+ // Check if validation is present
172
+ if (!S003OpenRedirectProtectionAnalyzer.hasValidation(argSource)) {
173
+ violations.add(analyzer.createViolation(
174
+ filePath: filePath,
175
+ line: analyzer.getLine(lineInfo, node.offset),
176
+ column: analyzer.getColumn(lineInfo, node.offset),
177
+ message: 'Open redirect risk - validate URL before launching: ${_truncate(argSource, 40)}',
178
+ severity: 'error',
179
+ ));
180
+ }
181
+ }
182
+
183
+ // Check for dynamic URL with interpolation going to external launch
184
+ if (arg is StringInterpolation) {
185
+ // Check if it's launching an external URL with user input
186
+ final interpolatedVars = arg.elements
187
+ .whereType<InterpolationExpression>()
188
+ .map((e) => e.toSource())
189
+ .join(' ');
190
+
191
+ if (S003OpenRedirectProtectionAnalyzer.containsExternalUserInput(interpolatedVars)) {
192
+ violations.add(analyzer.createViolation(
193
+ filePath: filePath,
194
+ line: analyzer.getLine(lineInfo, node.offset),
195
+ column: analyzer.getColumn(lineInfo, node.offset),
196
+ message: 'Dynamic URL construction with user input - validate against allowlist',
197
+ severity: 'warning',
198
+ ));
199
+ }
200
+ }
201
+ }
202
+ }
203
+
204
+ String _truncate(String s, int maxLen) {
205
+ if (s.length <= maxLen) return s;
206
+ return '${s.substring(0, maxLen)}...';
207
+ }
208
+ }
@@ -0,0 +1,391 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/dart/ast/visitor.dart';
3
+ import 'package:analyzer/source/line_info.dart';
4
+
5
+ import '../../models/rule.dart';
6
+ import '../../models/violation.dart';
7
+ import '../base_analyzer.dart';
8
+
9
+ /// S004: Sensitive Data Logging Protection
10
+ /// Prevents logging of sensitive information:
11
+ /// - Passwords, tokens, API keys
12
+ /// - Credit card numbers, SSN
13
+ /// - Personal identifiable information (PII)
14
+ /// - Authentication credentials
15
+ class S004SensitiveDataLoggingAnalyzer extends BaseAnalyzer {
16
+ @override
17
+ String get ruleId => 'S004';
18
+
19
+ /// Patterns for sensitive variable/field names
20
+ /// NOTE: Use word boundaries (\b) to avoid matching substrings like "passwordLength"
21
+ static final List<RegExp> sensitivePatterns = [
22
+ // Credentials - match actual password variables, not validation labels
23
+ RegExp(r'^_?password$', caseSensitive: false), // _password, password
24
+ RegExp(r'^_?newPassword$', caseSensitive: false), // _newPassword
25
+ RegExp(r'^_?currentPassword$', caseSensitive: false),
26
+ RegExp(r'^_?oldPassword$', caseSensitive: false),
27
+ RegExp(r'passwd', caseSensitive: false),
28
+ RegExp(r'^pwd$', caseSensitive: false),
29
+ RegExp(r'^_?secret$', caseSensitive: false),
30
+ RegExp(r'^_?credential', caseSensitive: false),
31
+ // Tokens - actual token values
32
+ RegExp(r'^_?userToken$', caseSensitive: false),
33
+ RegExp(r'^_?accessToken$', caseSensitive: false),
34
+ RegExp(r'^_?refreshToken$', caseSensitive: false),
35
+ RegExp(r'^_?authToken$', caseSensitive: false),
36
+ RegExp(r'^_?idToken$', caseSensitive: false),
37
+ RegExp(r'^_?saveToken$', caseSensitive: false),
38
+ RegExp(r'api[_-]?key', caseSensitive: false),
39
+ RegExp(r'apikey', caseSensitive: false),
40
+ RegExp(r'auth[_-]?key', caseSensitive: false),
41
+ RegExp(r'^bearer', caseSensitive: false),
42
+ RegExp(r'^jwt$', caseSensitive: false),
43
+ // Keys
44
+ RegExp(r'private[_-]?key', caseSensitive: false),
45
+ RegExp(r'encryption[_-]?key', caseSensitive: false),
46
+ RegExp(r'ssh[_-]?key', caseSensitive: false),
47
+ RegExp(r'signing[_-]?key', caseSensitive: false),
48
+ // Financial
49
+ RegExp(r'credit[_-]?card', caseSensitive: false),
50
+ RegExp(r'card[_-]?number', caseSensitive: false),
51
+ RegExp(r'^cvv$', caseSensitive: false),
52
+ RegExp(r'^cvc$', caseSensitive: false),
53
+ RegExp(r'bank[_-]?account', caseSensitive: false),
54
+ // PII
55
+ RegExp(r'^ssn$', caseSensitive: false),
56
+ RegExp(r'social[_-]?security', caseSensitive: false),
57
+ RegExp(r'tax[_-]?id', caseSensitive: false),
58
+ RegExp(r'national[_-]?id', caseSensitive: false),
59
+ // Auth - actual OTP/PIN values
60
+ RegExp(r'^_?otp$', caseSensitive: false),
61
+ RegExp(r'^_?otpCode$', caseSensitive: false),
62
+ RegExp(r'pin[_-]?code', caseSensitive: false),
63
+ RegExp(r'^mfa$', caseSensitive: false),
64
+ RegExp(r'^2fa$', caseSensitive: false),
65
+ RegExp(r'^totp$', caseSensitive: false),
66
+ // Headers
67
+ RegExp(r'^authorization$', caseSensitive: false),
68
+ RegExp(r'x-api-key', caseSensitive: false),
69
+ ];
70
+
71
+ /// Patterns that look like sensitive names but are actually safe
72
+ /// These are localization keys, validation labels, error messages, etc.
73
+ static final List<RegExp> safePatterns = [
74
+ // Localization/i18n keys (l10n.passwordXXX)
75
+ RegExp(r'l10n\.password', caseSensitive: false),
76
+ RegExp(r'l10n\.token', caseSensitive: false),
77
+ // Validation/requirement labels
78
+ RegExp(r'password.*Length', caseSensitive: false),
79
+ RegExp(r'password.*Requirement', caseSensitive: false),
80
+ RegExp(r'password.*Invalid', caseSensitive: false),
81
+ RegExp(r'password.*Error', caseSensitive: false),
82
+ RegExp(r'password.*Require', caseSensitive: false),
83
+ RegExp(r'password.*Width', caseSensitive: false),
84
+ RegExp(r'password.*Character', caseSensitive: false),
85
+ RegExp(r'password.*Message', caseSensitive: false),
86
+ RegExp(r'password.*Label', caseSensitive: false),
87
+ RegExp(r'password.*Hint', caseSensitive: false),
88
+ RegExp(r'password.*Placeholder', caseSensitive: false),
89
+ RegExp(r'password.*Title', caseSensitive: false),
90
+ RegExp(r'password.*Description', caseSensitive: false),
91
+ RegExp(r'token.*Expired', caseSensitive: false),
92
+ RegExp(r'token.*Error', caseSensitive: false),
93
+ // Route/navigation parameters
94
+ RegExp(r'LoginRoute', caseSensitive: false),
95
+ RegExp(r'RegisterRoute', caseSensitive: false),
96
+ // Enum values for analytics events
97
+ RegExp(r'UserEvent\.\w*password', caseSensitive: false),
98
+ RegExp(r'AnalyticsEvent\.\w*password', caseSensitive: false),
99
+ // Constants for validation
100
+ RegExp(r'min.*Password', caseSensitive: false),
101
+ RegExp(r'max.*Password', caseSensitive: false),
102
+ ];
103
+
104
+ /// Logging functions to check
105
+ static final Set<String> loggingFunctions = {
106
+ // Dart core
107
+ 'print',
108
+ 'debugPrint',
109
+ // Logger packages
110
+ 'log',
111
+ 'logInfo',
112
+ 'logDebug',
113
+ 'logWarning',
114
+ 'logError',
115
+ 'info',
116
+ 'debug',
117
+ 'warning',
118
+ 'error',
119
+ 'severe',
120
+ 'fine',
121
+ 'finer',
122
+ 'finest',
123
+ 'trace',
124
+ 'verbose',
125
+ // stdout/stderr
126
+ 'write',
127
+ 'writeln',
128
+ 'writeAll',
129
+ // Console
130
+ 'console.log',
131
+ 'console.info',
132
+ 'console.warn',
133
+ 'console.error',
134
+ 'console.debug',
135
+ };
136
+
137
+ /// Analytics/event tracking functions - these log event names, not sensitive data
138
+ static final Set<String> analyticsEventFunctions = {
139
+ 'sendlogevent', 'sendeventlog', 'sendevent',
140
+ 'logevent', 'trackevent', 'recordevent',
141
+ 'firebaseevent', 'analyticsevent',
142
+ 'sendlogcustomevent',
143
+ };
144
+
145
+ /// Event name patterns that are safe to log (not actual sensitive data)
146
+ static final List<RegExp> eventNamePatterns = [
147
+ RegExp(r'^tap', caseSensitive: false), // tapForgotPassword, tapBtnRegist
148
+ RegExp(r'^click', caseSensitive: false), // clickLogin
149
+ RegExp(r'^on[A-Z]', caseSensitive: false), // onLogin, onSubmit
150
+ RegExp(r'event$', caseSensitive: false), // loginEvent
151
+ RegExp(r'button$', caseSensitive: false), // forgotPasswordButton
152
+ RegExp(r'^screen', caseSensitive: false), // screenView
153
+ RegExp(r'^view', caseSensitive: false), // viewPage
154
+ RegExp(r'^open', caseSensitive: false), // openModal
155
+ RegExp(r'^close', caseSensitive: false), // closeDialog
156
+ RegExp(r'^show', caseSensitive: false), // showPopup
157
+ RegExp(r'^hide', caseSensitive: false), // hideModal
158
+ RegExp(r'action$', caseSensitive: false), // loginAction
159
+ ];
160
+
161
+ /// Redaction/masking methods
162
+ static final Set<String> redactionMethods = {
163
+ 'mask',
164
+ 'redact',
165
+ 'sanitize',
166
+ 'obscure',
167
+ 'hide',
168
+ 'censor',
169
+ 'omit',
170
+ 'exclude',
171
+ 'filter',
172
+ 'replace',
173
+ 'substring',
174
+ 'replaceAll',
175
+ 'replaceRange',
176
+ };
177
+
178
+ @override
179
+ List<Violation> analyze({
180
+ required CompilationUnit unit,
181
+ required String filePath,
182
+ required Rule rule,
183
+ required LineInfo lineInfo,
184
+ }) {
185
+ final violations = <Violation>[];
186
+
187
+ final visitor = _SensitiveLoggingVisitor(
188
+ filePath: filePath,
189
+ lineInfo: lineInfo,
190
+ violations: violations,
191
+ analyzer: this,
192
+ );
193
+
194
+ unit.accept(visitor);
195
+
196
+ return violations;
197
+ }
198
+
199
+ /// Check if a name matches sensitive patterns
200
+ static bool isSensitiveName(String name) {
201
+ return sensitivePatterns.any((pattern) => pattern.hasMatch(name));
202
+ }
203
+
204
+ /// Check if function is a logging function
205
+ static bool isLoggingFunction(String name) {
206
+ final lowerName = name.toLowerCase();
207
+ return loggingFunctions.any((fn) =>
208
+ lowerName == fn.toLowerCase() ||
209
+ lowerName.endsWith('.${fn.toLowerCase()}') ||
210
+ lowerName.contains(fn.toLowerCase()));
211
+ }
212
+
213
+ /// Check if expression has redaction
214
+ static bool hasRedaction(String source) {
215
+ final lowerSource = source.toLowerCase();
216
+ return redactionMethods.any((method) => lowerSource.contains(method));
217
+ }
218
+ }
219
+
220
+ class _SensitiveLoggingVisitor extends RecursiveAstVisitor<void> {
221
+ final String filePath;
222
+ final LineInfo lineInfo;
223
+ final List<Violation> violations;
224
+ final S004SensitiveDataLoggingAnalyzer analyzer;
225
+
226
+ _SensitiveLoggingVisitor({
227
+ required this.filePath,
228
+ required this.lineInfo,
229
+ required this.violations,
230
+ required this.analyzer,
231
+ });
232
+
233
+ @override
234
+ void visitMethodInvocation(MethodInvocation node) {
235
+ final methodName = node.methodName.name;
236
+ final target = node.target?.toSource() ?? '';
237
+ final fullName = target.isEmpty ? methodName : '$target.$methodName';
238
+ final lowerMethodName = methodName.toLowerCase();
239
+
240
+ // Skip analytics/event tracking functions - these log event names, not data
241
+ if (S004SensitiveDataLoggingAnalyzer.analyticsEventFunctions
242
+ .any((fn) => lowerMethodName.contains(fn))) {
243
+ super.visitMethodInvocation(node);
244
+ return;
245
+ }
246
+
247
+ // Check for logging sensitive data
248
+ if (S004SensitiveDataLoggingAnalyzer.isLoggingFunction(fullName) ||
249
+ S004SensitiveDataLoggingAnalyzer.isLoggingFunction(methodName)) {
250
+ _checkArgumentsForSensitiveData(node);
251
+ }
252
+
253
+ super.visitMethodInvocation(node);
254
+ }
255
+
256
+ @override
257
+ void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
258
+ final functionName = node.function.toSource();
259
+
260
+ if (S004SensitiveDataLoggingAnalyzer.isLoggingFunction(functionName)) {
261
+ for (final arg in node.argumentList.arguments) {
262
+ _checkExpressionForSensitiveData(arg, node);
263
+ }
264
+ }
265
+
266
+ super.visitFunctionExpressionInvocation(node);
267
+ }
268
+
269
+ void _checkArgumentsForSensitiveData(MethodInvocation logCall) {
270
+ for (final arg in logCall.argumentList.arguments) {
271
+ _checkExpressionForSensitiveData(arg, logCall);
272
+ }
273
+ }
274
+
275
+ void _checkExpressionForSensitiveData(Expression expr, AstNode logCall) {
276
+ final exprSource = expr.toSource();
277
+
278
+ // Skip if redaction is present
279
+ if (S004SensitiveDataLoggingAnalyzer.hasRedaction(exprSource)) {
280
+ return;
281
+ }
282
+
283
+ // Check string interpolation
284
+ if (expr is StringInterpolation) {
285
+ for (final element in expr.elements) {
286
+ if (element is InterpolationExpression) {
287
+ _checkExpressionForSensitiveData(element.expression, logCall);
288
+ }
289
+ }
290
+ return;
291
+ }
292
+
293
+ // Check identifiers
294
+ if (expr is SimpleIdentifier) {
295
+ _checkIdentifier(expr.name, expr.offset, logCall);
296
+ return;
297
+ }
298
+
299
+ // Check property access
300
+ if (expr is PrefixedIdentifier) {
301
+ _checkIdentifier(expr.identifier.name, expr.offset, logCall);
302
+ return;
303
+ }
304
+
305
+ // Check property access chain
306
+ if (expr is PropertyAccess) {
307
+ _checkIdentifier(expr.propertyName.name, expr.offset, logCall);
308
+ return;
309
+ }
310
+
311
+ // Check binary expressions (concatenation)
312
+ if (expr is BinaryExpression) {
313
+ _checkExpressionForSensitiveData(expr.leftOperand, logCall);
314
+ _checkExpressionForSensitiveData(expr.rightOperand, logCall);
315
+ return;
316
+ }
317
+
318
+ // Check parenthesized expressions
319
+ if (expr is ParenthesizedExpression) {
320
+ _checkExpressionForSensitiveData(expr.expression, logCall);
321
+ return;
322
+ }
323
+
324
+ // Check conditional expressions
325
+ if (expr is ConditionalExpression) {
326
+ _checkExpressionForSensitiveData(expr.thenExpression, logCall);
327
+ _checkExpressionForSensitiveData(expr.elseExpression, logCall);
328
+ return;
329
+ }
330
+
331
+ // Check map/list literals for sensitive keys
332
+ if (expr is SetOrMapLiteral) {
333
+ for (final element in expr.elements) {
334
+ if (element is MapLiteralEntry) {
335
+ final key = element.key;
336
+ if (key is SimpleStringLiteral) {
337
+ if (S004SensitiveDataLoggingAnalyzer.isSensitiveName(key.value)) {
338
+ violations.add(analyzer.createViolation(
339
+ filePath: filePath,
340
+ line: analyzer.getLine(lineInfo, element.offset),
341
+ column: analyzer.getColumn(lineInfo, element.offset),
342
+ message:
343
+ 'Sensitive data "${key.value}" logged in map - mask or redact before logging',
344
+ severity: 'error',
345
+ metadata: {
346
+ 'key': key.value,
347
+ 'issue': 'sensitive_data_in_map',
348
+ },
349
+ ));
350
+ }
351
+ }
352
+ }
353
+ }
354
+ return;
355
+ }
356
+ }
357
+
358
+ void _checkIdentifier(String name, int offset, AstNode logCall) {
359
+ // Skip if this looks like an event name (analytics tracking)
360
+ bool isEventName = S004SensitiveDataLoggingAnalyzer.eventNamePatterns
361
+ .any((pattern) => pattern.hasMatch(name));
362
+
363
+ if (isEventName) {
364
+ return;
365
+ }
366
+
367
+ // Skip safe patterns (localization keys, validation labels, etc.)
368
+ bool isSafePattern = S004SensitiveDataLoggingAnalyzer.safePatterns
369
+ .any((pattern) => pattern.hasMatch(name));
370
+
371
+ if (isSafePattern) {
372
+ return;
373
+ }
374
+
375
+ if (S004SensitiveDataLoggingAnalyzer.isSensitiveName(name)) {
376
+ violations.add(analyzer.createViolation(
377
+ filePath: filePath,
378
+ line: analyzer.getLine(lineInfo, offset),
379
+ column: analyzer.getColumn(lineInfo, offset),
380
+ message:
381
+ 'Sensitive data "$name" logged without redaction - mask or exclude before logging',
382
+ severity: 'error',
383
+ metadata: {
384
+ 'variableName': name,
385
+ 'issue': 'sensitive_data_logging',
386
+ 'recommendation': 'Use masking like: "${name.length > 4 ? name.substring(0, 4) : name}****" or omit entirely',
387
+ },
388
+ ));
389
+ }
390
+ }
391
+ }