@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,315 @@
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
+ /// S015: Insecure TLS Certificate
10
+ /// Only accept trusted TLS certificates and reject weak ciphers
11
+ /// - Do not accept untrusted/self-signed certificates automatically
12
+ /// - Disable weak cipher suites (RC4, 3DES, NULL, MD5, SHA1)
13
+ /// - Use modern ciphers (AES-GCM, ChaCha20-Poly1305, TLS_ECDHE)
14
+ class S015InsecureTlsCertificateAnalyzer extends BaseAnalyzer {
15
+ @override
16
+ String get ruleId => 'S015';
17
+
18
+ // Patterns indicating certificate verification bypass
19
+ static const _insecureCertPatterns = [
20
+ 'badcertificatecallback',
21
+ 'allowbadcertificates',
22
+ 'trustbadcertificate',
23
+ 'onbadcertificate',
24
+ 'allowselfsigned',
25
+ 'allow_self_signed',
26
+ 'skipverify',
27
+ 'skip_verify',
28
+ 'verifycertificate: false',
29
+ 'verify_certificate: false',
30
+ 'ignorecertificate',
31
+ 'ignore_certificate',
32
+ 'trustallcertificates',
33
+ 'trust_all_certificates',
34
+ ];
35
+
36
+ // Weak cipher patterns
37
+ static const _weakCipherPatterns = [
38
+ 'rc4',
39
+ 'des',
40
+ '3des',
41
+ 'tripledes',
42
+ 'null_cipher',
43
+ 'nullcipher',
44
+ 'export_cipher',
45
+ 'exportcipher',
46
+ 'md5',
47
+ 'sha1',
48
+ ];
49
+
50
+ // Insecure TLS versions
51
+ static const _insecureTlsVersions = [
52
+ 'sslv2',
53
+ 'sslv3',
54
+ 'ssl_v2',
55
+ 'ssl_v3',
56
+ 'tlsv1.0',
57
+ 'tlsv1_0',
58
+ 'tls_v1_0',
59
+ 'tlsv1.1',
60
+ 'tlsv1_1',
61
+ 'tls_v1_1',
62
+ 'tls1.0',
63
+ 'tls1.1',
64
+ ];
65
+
66
+ @override
67
+ List<Violation> analyze({
68
+ required CompilationUnit unit,
69
+ required String filePath,
70
+ required Rule rule,
71
+ required LineInfo lineInfo,
72
+ }) {
73
+ final violations = <Violation>[];
74
+ final visitor = _S015Visitor(
75
+ filePath: filePath,
76
+ lineInfo: lineInfo,
77
+ violations: violations,
78
+ analyzer: this,
79
+ );
80
+ unit.accept(visitor);
81
+ return violations;
82
+ }
83
+ }
84
+
85
+ class _S015Visitor extends RecursiveAstVisitor<void> {
86
+ final String filePath;
87
+ final LineInfo lineInfo;
88
+ final List<Violation> violations;
89
+ final S015InsecureTlsCertificateAnalyzer analyzer;
90
+
91
+ _S015Visitor({
92
+ required this.filePath,
93
+ required this.lineInfo,
94
+ required this.violations,
95
+ required this.analyzer,
96
+ });
97
+
98
+ @override
99
+ void visitMethodInvocation(MethodInvocation node) {
100
+ final source = node.toSource().toLowerCase();
101
+ final methodName = node.methodName.name.toLowerCase();
102
+
103
+ // Check for certificate verification bypass
104
+ _checkInsecureCertPatterns(source, node.offset);
105
+
106
+ // Check for weak ciphers in method calls
107
+ _checkWeakCiphers(source, node.offset);
108
+
109
+ // Check for insecure TLS versions
110
+ _checkInsecureTlsVersions(source, node.offset);
111
+
112
+ // Check for badCertificateCallback that always returns true
113
+ if (methodName == 'badcertificatecallback' ||
114
+ source.contains('onbadcertificate')) {
115
+ // Check if callback returns true (accepts all certs)
116
+ if (source.contains('=> true') ||
117
+ source.contains('return true') ||
118
+ source.contains('(_) => true') ||
119
+ source.contains('(_, __, ___) => true')) {
120
+ violations.add(analyzer.createViolation(
121
+ filePath: filePath,
122
+ line: analyzer.getLine(lineInfo, node.offset),
123
+ column: analyzer.getColumn(lineInfo, node.offset),
124
+ message:
125
+ 'Certificate callback accepts all certificates - only trust valid certificates from trusted CAs',
126
+ ));
127
+ }
128
+ }
129
+
130
+ super.visitMethodInvocation(node);
131
+ }
132
+
133
+ @override
134
+ void visitNamedExpression(NamedExpression node) {
135
+ final paramName = node.name.label.name.toLowerCase();
136
+ final expression = node.expression;
137
+ final source = node.toSource().toLowerCase();
138
+
139
+ // Check for certificate verification parameters set to false
140
+ if (_isCertVerificationParam(paramName)) {
141
+ if (expression is BooleanLiteral && !expression.value) {
142
+ violations.add(analyzer.createViolation(
143
+ filePath: filePath,
144
+ line: analyzer.getLine(lineInfo, node.offset),
145
+ column: analyzer.getColumn(lineInfo, node.offset),
146
+ message:
147
+ 'Certificate verification disabled - this allows man-in-the-middle attacks',
148
+ ));
149
+ }
150
+ }
151
+
152
+ // Check for badCertificateCallback parameter
153
+ if (paramName.contains('badcertificate') ||
154
+ paramName.contains('onbadcertificate')) {
155
+ if (source.contains('=> true') ||
156
+ source.contains('(_) => true') ||
157
+ source.contains('return true')) {
158
+ violations.add(analyzer.createViolation(
159
+ filePath: filePath,
160
+ line: analyzer.getLine(lineInfo, node.offset),
161
+ column: analyzer.getColumn(lineInfo, node.offset),
162
+ message:
163
+ 'Bad certificate callback accepts all certificates - only trust valid certificates',
164
+ ));
165
+ }
166
+ }
167
+
168
+ // Check for weak ciphers in parameters
169
+ _checkWeakCiphers(source, node.offset);
170
+
171
+ // Check for insecure TLS versions in parameters
172
+ _checkInsecureTlsVersions(source, node.offset);
173
+
174
+ super.visitNamedExpression(node);
175
+ }
176
+
177
+ @override
178
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
179
+ final source = node.toSource().toLowerCase();
180
+ final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
181
+
182
+ // Check SecurityContext or HttpClient configuration
183
+ if (typeName == 'securitycontext' ||
184
+ typeName == 'httpclient' ||
185
+ typeName == 'httpsclient') {
186
+ _checkInsecureCertPatterns(source, node.offset);
187
+ _checkWeakCiphers(source, node.offset);
188
+ _checkInsecureTlsVersions(source, node.offset);
189
+ }
190
+
191
+ super.visitInstanceCreationExpression(node);
192
+ }
193
+
194
+ @override
195
+ void visitSimpleStringLiteral(SimpleStringLiteral node) {
196
+ final value = node.value.toLowerCase();
197
+
198
+ // Check for weak cipher strings
199
+ _checkWeakCiphers(value, node.offset);
200
+
201
+ // Check for insecure TLS version strings
202
+ _checkInsecureTlsVersions(value, node.offset);
203
+
204
+ super.visitSimpleStringLiteral(node);
205
+ }
206
+
207
+ @override
208
+ void visitFunctionExpression(FunctionExpression node) {
209
+ // Check if this is a callback that always returns true (bad certificate handler)
210
+ final parent = node.parent;
211
+ if (parent is NamedExpression) {
212
+ final paramName = parent.name.label.name.toLowerCase();
213
+ if (paramName.contains('badcertificate') ||
214
+ paramName.contains('certificate')) {
215
+ final body = node.body;
216
+ if (body != null) {
217
+ final bodySource = body.toSource().toLowerCase();
218
+ if (bodySource.contains('return true') ||
219
+ bodySource == '=> true' ||
220
+ bodySource.contains('=> true')) {
221
+ violations.add(analyzer.createViolation(
222
+ filePath: filePath,
223
+ line: analyzer.getLine(lineInfo, node.offset),
224
+ column: analyzer.getColumn(lineInfo, node.offset),
225
+ message:
226
+ 'Certificate callback blindly trusts all certificates - validate certificates properly',
227
+ ));
228
+ }
229
+ }
230
+ }
231
+ }
232
+
233
+ super.visitFunctionExpression(node);
234
+ }
235
+
236
+ bool _isCertVerificationParam(String paramName) {
237
+ return paramName.contains('verify') ||
238
+ paramName.contains('validate') ||
239
+ paramName.contains('check') && paramName.contains('cert') ||
240
+ paramName.contains('ssl') ||
241
+ paramName.contains('tls') ||
242
+ paramName == 'secure';
243
+ }
244
+
245
+ void _checkInsecureCertPatterns(String source, int offset) {
246
+ for (final pattern
247
+ in S015InsecureTlsCertificateAnalyzer._insecureCertPatterns) {
248
+ if (source.contains(pattern.replaceAll('_', ''))) {
249
+ violations.add(analyzer.createViolation(
250
+ filePath: filePath,
251
+ line: analyzer.getLine(lineInfo, offset),
252
+ column: analyzer.getColumn(lineInfo, offset),
253
+ message:
254
+ 'Insecure certificate handling detected - only trust valid certificates from trusted CAs',
255
+ ));
256
+ return; // Avoid duplicate violations
257
+ }
258
+ }
259
+ }
260
+
261
+ void _checkWeakCiphers(String source, int offset) {
262
+ // Only flag if in crypto/cipher context
263
+ if (!_isInCipherContext(source)) return;
264
+
265
+ for (final cipher
266
+ in S015InsecureTlsCertificateAnalyzer._weakCipherPatterns) {
267
+ if (source.contains(cipher)) {
268
+ violations.add(analyzer.createViolation(
269
+ filePath: filePath,
270
+ line: analyzer.getLine(lineInfo, offset),
271
+ column: analyzer.getColumn(lineInfo, offset),
272
+ message:
273
+ 'Weak cipher "$cipher" detected - use AES-GCM, ChaCha20-Poly1305, or other modern ciphers',
274
+ ));
275
+ return;
276
+ }
277
+ }
278
+ }
279
+
280
+ void _checkInsecureTlsVersions(String source, int offset) {
281
+ // Only flag if in TLS/SSL context
282
+ if (!_isInTlsContext(source)) return;
283
+
284
+ for (final version
285
+ in S015InsecureTlsCertificateAnalyzer._insecureTlsVersions) {
286
+ if (source.contains(version)) {
287
+ violations.add(analyzer.createViolation(
288
+ filePath: filePath,
289
+ line: analyzer.getLine(lineInfo, offset),
290
+ column: analyzer.getColumn(lineInfo, offset),
291
+ message:
292
+ 'Insecure TLS version detected - use TLS 1.2 or TLS 1.3 only',
293
+ ));
294
+ return;
295
+ }
296
+ }
297
+ }
298
+
299
+ bool _isInCipherContext(String source) {
300
+ return source.contains('cipher') ||
301
+ source.contains('encrypt') ||
302
+ source.contains('ssl') ||
303
+ source.contains('tls') ||
304
+ source.contains('security');
305
+ }
306
+
307
+ bool _isInTlsContext(String source) {
308
+ return source.contains('tls') ||
309
+ source.contains('ssl') ||
310
+ source.contains('https') ||
311
+ source.contains('security') ||
312
+ source.contains('protocol') ||
313
+ source.contains('version');
314
+ }
315
+ }
@@ -0,0 +1,244 @@
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
+ /// S016: No Sensitive Data in Query Strings
10
+ /// Sensitive data should not be passed in URL query strings
11
+ class S016NoSensitiveQuerystringAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S016';
14
+
15
+ // Sensitive parameter names (truly sensitive data only)
16
+ static const _sensitiveParams = [
17
+ 'password', 'passwd', 'pwd', 'secret', 'apikey', 'api_key',
18
+ 'accesskey', 'access_key', 'sessionid', 'session_id',
19
+ 'credit_card', 'creditcard', 'cardnumber', 'card_number', 'cvv', 'ssn',
20
+ 'otp', 'pin', 'credential', 'bearer', 'private_key',
21
+ ];
22
+
23
+ // Non-sensitive keys that may contain "key" or "token" but are not secrets
24
+ // These are business domain identifiers, not security tokens
25
+ static const _nonSensitiveKeys = [
26
+ 'objective_key', 'objectivekey', // Business objective identifier
27
+ 'device_token', 'devicetoken', // Push notification token (not auth)
28
+ 'fcm_token', 'fcmtoken', // Firebase Cloud Messaging token
29
+ 'apns_token', 'apnstoken', // Apple Push Notification token
30
+ 'primary_key', 'primarykey', // Database key
31
+ 'foreign_key', 'foreignkey', // Database key
32
+ 'sort_key', 'sortkey', // Sorting key
33
+ 'cache_key', 'cachekey', // Cache identifier
34
+ 'lookup_key', 'lookupkey', // Lookup identifier
35
+ ];
36
+
37
+ @override
38
+ List<Violation> analyze({
39
+ required CompilationUnit unit,
40
+ required String filePath,
41
+ required Rule rule,
42
+ required LineInfo lineInfo,
43
+ }) {
44
+ final violations = <Violation>[];
45
+ final visitor = _S016Visitor(
46
+ filePath: filePath,
47
+ lineInfo: lineInfo,
48
+ violations: violations,
49
+ analyzer: this,
50
+ );
51
+ unit.accept(visitor);
52
+ return violations;
53
+ }
54
+ }
55
+
56
+ class _S016Visitor extends RecursiveAstVisitor<void> {
57
+ final String filePath;
58
+ final LineInfo lineInfo;
59
+ final List<Violation> violations;
60
+ final S016NoSensitiveQuerystringAnalyzer analyzer;
61
+
62
+ _S016Visitor({
63
+ required this.filePath,
64
+ required this.lineInfo,
65
+ required this.violations,
66
+ required this.analyzer,
67
+ });
68
+
69
+ @override
70
+ void visitSimpleStringLiteral(SimpleStringLiteral node) {
71
+ final value = node.value.toLowerCase();
72
+
73
+ // Check for URLs with sensitive query params
74
+ if (value.contains('?') || value.contains('&')) {
75
+ for (final param in S016NoSensitiveQuerystringAnalyzer._sensitiveParams) {
76
+ // Match patterns like ?password= or &token=
77
+ if (RegExp('[?&]$param=').hasMatch(value)) {
78
+ violations.add(analyzer.createViolation(
79
+ filePath: filePath,
80
+ line: analyzer.getLine(lineInfo, node.offset),
81
+ column: analyzer.getColumn(lineInfo, node.offset),
82
+ message: 'Sensitive data "$param" should not be in query string - use POST body or headers',
83
+ ));
84
+ break;
85
+ }
86
+ }
87
+ }
88
+
89
+ super.visitSimpleStringLiteral(node);
90
+ }
91
+
92
+ @override
93
+ void visitStringInterpolation(StringInterpolation node) {
94
+ final source = node.toSource().toLowerCase();
95
+
96
+ // Check for query string patterns with sensitive data
97
+ if (source.contains('?') || source.contains('&')) {
98
+ for (final param in S016NoSensitiveQuerystringAnalyzer._sensitiveParams) {
99
+ if (source.contains('$param=') || source.contains('${param}=')) {
100
+ violations.add(analyzer.createViolation(
101
+ filePath: filePath,
102
+ line: analyzer.getLine(lineInfo, node.offset),
103
+ column: analyzer.getColumn(lineInfo, node.offset),
104
+ message: 'Sensitive data should not be in query string - use POST body or headers',
105
+ ));
106
+ break;
107
+ }
108
+ }
109
+ }
110
+
111
+ super.visitStringInterpolation(node);
112
+ }
113
+
114
+ @override
115
+ void visitMethodInvocation(MethodInvocation node) {
116
+ final methodName = node.methodName.name.toLowerCase();
117
+
118
+ // Check for Uri construction with sensitive query parameters
119
+ if (methodName == 'queryparameters' || methodName == 'replace') {
120
+ final source = node.toSource().toLowerCase();
121
+ for (final param in S016NoSensitiveQuerystringAnalyzer._sensitiveParams) {
122
+ if (source.contains("'$param'") || source.contains('"$param"')) {
123
+ violations.add(analyzer.createViolation(
124
+ filePath: filePath,
125
+ line: analyzer.getLine(lineInfo, node.offset),
126
+ column: analyzer.getColumn(lineInfo, node.offset),
127
+ message: 'Sensitive parameter "$param" should not be in query parameters',
128
+ ));
129
+ break;
130
+ }
131
+ }
132
+ }
133
+
134
+ super.visitMethodInvocation(node);
135
+ }
136
+
137
+ @override
138
+ void visitMapLiteralEntry(MapLiteralEntry node) {
139
+ final key = node.key.toSource().toLowerCase().replaceAll("'", '').replaceAll('"', '');
140
+
141
+ // Skip non-sensitive keys (business identifiers, not secrets)
142
+ bool isNonSensitive = S016NoSensitiveQuerystringAnalyzer._nonSensitiveKeys
143
+ .any((p) => key.contains(p.replaceAll('_', '')));
144
+
145
+ if (isNonSensitive) {
146
+ super.visitMapLiteralEntry(node);
147
+ return;
148
+ }
149
+
150
+ // Check if this is a query parameter map entry
151
+ bool isSensitive = S016NoSensitiveQuerystringAnalyzer._sensitiveParams
152
+ .any((p) => key.contains(p));
153
+
154
+ if (isSensitive) {
155
+ // Check parent context to see if it's for query parameters (NOT headers or POST body)
156
+ AstNode? current = node.parent;
157
+ bool isQueryParam = false;
158
+ bool isHeader = false;
159
+ bool isPostBody = false;
160
+ int depth = 0;
161
+ const maxDepth = 15;
162
+
163
+ while (current != null && depth < maxDepth) {
164
+ final source = current.toSource().toLowerCase();
165
+
166
+ // Check if this is a header context - skip these
167
+ if (source.contains('headers') ||
168
+ source.contains('header') ||
169
+ source.contains('authorization')) {
170
+ isHeader = true;
171
+ break;
172
+ }
173
+
174
+ // Check if this is a POST/PUT request body (data: {}) - skip these
175
+ // POST body is secure when using HTTPS
176
+ if (current is NamedExpression && current.name.label.name == 'data') {
177
+ final grandParent = current.parent?.parent;
178
+ if (grandParent != null) {
179
+ final gpSource = grandParent.toSource().toLowerCase();
180
+ if (gpSource.contains('.post(') || gpSource.contains('.put(')) {
181
+ isPostBody = true;
182
+ break;
183
+ }
184
+ }
185
+ }
186
+
187
+ // Check for variable assignment that is used in POST/PUT request body
188
+ // Pattern: final data = {...}; then _api.post(path, data: data)
189
+ if (current is VariableDeclaration) {
190
+ final varName = current.name.lexeme;
191
+ // Check if parent function/block contains POST/PUT with this variable
192
+ final enclosingScope = _findEnclosingScope(current);
193
+ if (enclosingScope != null) {
194
+ final scopeSource = enclosingScope.toSource().toLowerCase();
195
+ // Check if there's a .post() or .put() call with data: varName
196
+ if ((scopeSource.contains('.post(') || scopeSource.contains('.put(')) &&
197
+ scopeSource.contains('data: ${varName.toLowerCase()}')) {
198
+ isPostBody = true;
199
+ break;
200
+ }
201
+ }
202
+ }
203
+
204
+ // Check if this is a query parameter context
205
+ if (source.contains('queryparameters') ||
206
+ source.contains('query_parameters') ||
207
+ (source.contains('uri(') && !source.contains('headers'))) {
208
+ isQueryParam = true;
209
+ break;
210
+ }
211
+
212
+ current = current.parent;
213
+ depth++;
214
+ }
215
+
216
+ // Only report if it's query parameters and NOT headers/POST body
217
+ if (isQueryParam && !isHeader && !isPostBody) {
218
+ violations.add(analyzer.createViolation(
219
+ filePath: filePath,
220
+ line: analyzer.getLine(lineInfo, node.offset),
221
+ column: analyzer.getColumn(lineInfo, node.offset),
222
+ message: 'Sensitive data "$key" should not be in query parameters',
223
+ ));
224
+ }
225
+ }
226
+
227
+ super.visitMapLiteralEntry(node);
228
+ }
229
+
230
+ /// Find the enclosing function or block scope
231
+ AstNode? _findEnclosingScope(AstNode node) {
232
+ AstNode? current = node.parent;
233
+ while (current != null) {
234
+ if (current is FunctionBody ||
235
+ current is MethodDeclaration ||
236
+ current is FunctionDeclaration ||
237
+ current is Block) {
238
+ return current;
239
+ }
240
+ current = current.parent;
241
+ }
242
+ return null;
243
+ }
244
+ }