@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,195 @@
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
+ /// S027: Validate mTLS client certificates before authentication
10
+ /// Ensure mTLS client certificates are properly validated before trusting certificate identity
11
+ class S027MtlsCertificateValidationAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S027';
14
+
15
+ // Patterns for excluded URLs/paths
16
+ static final _excludedUrlPatterns = [
17
+ RegExp(r'localhost', caseSensitive: false),
18
+ RegExp(r'127\.0\.0\.1'),
19
+ RegExp(r'\[::1\]'),
20
+ RegExp(r'0\.0\.0\.0'),
21
+ ];
22
+
23
+ @override
24
+ List<Violation> analyze({
25
+ required CompilationUnit unit,
26
+ required String filePath,
27
+ required Rule rule,
28
+ required LineInfo lineInfo,
29
+ }) {
30
+ final violations = <Violation>[];
31
+ final visitor = _S027Visitor(
32
+ filePath: filePath,
33
+ lineInfo: lineInfo,
34
+ violations: violations,
35
+ analyzer: this,
36
+ );
37
+ unit.accept(visitor);
38
+ return violations;
39
+ }
40
+ }
41
+
42
+ class _S027Visitor extends RecursiveAstVisitor<void> {
43
+ final String filePath;
44
+ final LineInfo lineInfo;
45
+ final List<Violation> violations;
46
+ final S027MtlsCertificateValidationAnalyzer analyzer;
47
+
48
+ _S027Visitor({
49
+ required this.filePath,
50
+ required this.lineInfo,
51
+ required this.violations,
52
+ required this.analyzer,
53
+ });
54
+
55
+ @override
56
+ void visitAssignmentExpression(AssignmentExpression node) {
57
+ final source = node.toSource().toLowerCase();
58
+
59
+ // Check for badCertificateCallback = (args) => true (disables cert validation)
60
+ if (source.contains('badcertificatecallback') && source.contains('=> true')) {
61
+ violations.add(analyzer.createViolation(
62
+ filePath: filePath,
63
+ line: analyzer.getLine(lineInfo, node.offset),
64
+ column: analyzer.getColumn(lineInfo, node.offset),
65
+ message: 'mTLS certificate validation disabled. badCertificateCallback should validate certificates properly',
66
+ ));
67
+ }
68
+
69
+ super.visitAssignmentExpression(node);
70
+ }
71
+
72
+ @override
73
+ void visitNamedExpression(NamedExpression node) {
74
+ final name = node.name.label.name.toLowerCase();
75
+ final expression = node.expression;
76
+
77
+ // Check for badCertificateCallback returning true (disabling validation)
78
+ if (name == 'badcertificatecallback' && expression is FunctionExpression) {
79
+ final body = expression.body;
80
+ if (body is ExpressionFunctionBody) {
81
+ final bodySource = body.expression.toSource().toLowerCase();
82
+ if (bodySource == 'true') {
83
+ violations.add(analyzer.createViolation(
84
+ filePath: filePath,
85
+ line: analyzer.getLine(lineInfo, node.offset),
86
+ column: analyzer.getColumn(lineInfo, node.offset),
87
+ message: 'mTLS certificate validation disabled. Implement proper certificate validation',
88
+ ));
89
+ }
90
+ }
91
+ }
92
+
93
+ // Check for onBadCertificate: (cert) => true
94
+ if (name == 'onbadcertificate' && expression is FunctionExpression) {
95
+ final body = expression.body;
96
+ if (body is ExpressionFunctionBody) {
97
+ final bodySource = body.expression.toSource().toLowerCase();
98
+ if (bodySource == 'true') {
99
+ violations.add(analyzer.createViolation(
100
+ filePath: filePath,
101
+ line: analyzer.getLine(lineInfo, node.offset),
102
+ column: analyzer.getColumn(lineInfo, node.offset),
103
+ message: 'Certificate validation bypassed in onBadCertificate. Validate certificates properly',
104
+ ));
105
+ }
106
+ }
107
+ }
108
+
109
+ // Check for context.setTrustedCertificatesBytes with allowInvalidCertificates: true
110
+ if (name == 'allowinvalidcertificates' &&
111
+ expression is BooleanLiteral &&
112
+ expression.value) {
113
+ violations.add(analyzer.createViolation(
114
+ filePath: filePath,
115
+ line: analyzer.getLine(lineInfo, node.offset),
116
+ column: analyzer.getColumn(lineInfo, node.offset),
117
+ message: 'allowInvalidCertificates enabled. This bypasses certificate validation',
118
+ ));
119
+ }
120
+
121
+ // Check for insecure SecurityContext options
122
+ if (name == 'onbadcertificate' || name == 'badcertificatecallback') {
123
+ // Check if the callback always returns true (inline)
124
+ if (expression is SimpleIdentifier) {
125
+ // Could be a function reference - need deeper analysis
126
+ }
127
+ }
128
+
129
+ super.visitNamedExpression(node);
130
+ }
131
+
132
+ @override
133
+ void visitMethodInvocation(MethodInvocation node) {
134
+ final methodName = node.methodName.name.toLowerCase();
135
+
136
+ // Check for SecurityContext methods that bypass validation
137
+ if (methodName == 'settrustanchorsfrombytes' ||
138
+ methodName == 'settrustedcertificatesbytes') {
139
+ // Check if used with allowInvalidCertificates: true in arguments
140
+ for (final arg in node.argumentList.arguments) {
141
+ if (arg is NamedExpression) {
142
+ final argName = arg.name.label.name.toLowerCase();
143
+ if (argName == 'allowinvalidcertificates') {
144
+ final expr = arg.expression;
145
+ if (expr is BooleanLiteral && expr.value) {
146
+ violations.add(analyzer.createViolation(
147
+ filePath: filePath,
148
+ line: analyzer.getLine(lineInfo, node.offset),
149
+ column: analyzer.getColumn(lineInfo, node.offset),
150
+ message: 'Certificate validation bypassed with allowInvalidCertificates: true',
151
+ ));
152
+ }
153
+ }
154
+ }
155
+ }
156
+ }
157
+
158
+ super.visitMethodInvocation(node);
159
+ }
160
+
161
+ @override
162
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
163
+ final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
164
+
165
+ // Check for HttpClient or SecurityContext creation with insecure options
166
+ if (typeName == 'httpclient' || typeName == 'securitycontext') {
167
+ for (final arg in node.argumentList.arguments) {
168
+ if (arg is NamedExpression) {
169
+ final argName = arg.name.label.name.toLowerCase();
170
+
171
+ // Check for badCertificateCallback: (cert, host, port) => true
172
+ if (argName == 'badcertificatecallback' || argName == 'onbadcertificate') {
173
+ final expr = arg.expression;
174
+ if (expr is FunctionExpression) {
175
+ final body = expr.body;
176
+ if (body is ExpressionFunctionBody) {
177
+ final bodySource = body.expression.toSource().toLowerCase();
178
+ if (bodySource == 'true') {
179
+ violations.add(analyzer.createViolation(
180
+ filePath: filePath,
181
+ line: analyzer.getLine(lineInfo, node.offset),
182
+ column: analyzer.getColumn(lineInfo, node.offset),
183
+ message: 'Insecure $typeName created with certificate validation disabled',
184
+ ));
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ }
191
+ }
192
+
193
+ super.visitInstanceCreationExpression(node);
194
+ }
195
+ }
@@ -0,0 +1,186 @@
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
+ /// S028: File Upload Size Limits
10
+ /// Ensure file upload size limits are enforced
11
+ class S028FileUploadSizeLimitsAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S028';
14
+
15
+ // File upload methods - ONLY actual implementations that create FormData/MultipartFile
16
+ // Note: We now focus on FormData/MultipartFile creation, not method names
17
+ // because many methods call upload functions without implementing the actual upload
18
+ // This list is kept for special cases where the upload is done differently
19
+ static const _uploadMethods = <String>[]; // Empty - rely on FormData/MultipartFile detection
20
+
21
+ // Patterns that are NOT HTTP uploads (local file operations)
22
+ // These should be excluded from file size limit requirement
23
+ static const _localFileOperationPatterns = [
24
+ 'imagegallerysaver', // Flutter plugin to save to device gallery
25
+ 'gallerysaver', // Save to gallery
26
+ 'saveimagetogallery', // Save image to gallery
27
+ 'savevideotogallery', // Save video to gallery
28
+ 'filesaver', // File saver plugins
29
+ 'savetodisk', // Local disk save
30
+ 'writetofile', // Local file write
31
+ 'localfilesystem', // Local filesystem operations
32
+ 'pathprovider', // Flutter path provider (local storage)
33
+ 'gettemporarydirectory', // Temp directory
34
+ 'getapplicationdocumentsdirectory', // App documents
35
+ 'addpositionlistener', // Audio/video player listeners
36
+ 'addcompletelistener', // Completion listeners
37
+ 'startvideorecording', // Camera recording (local)
38
+ 'stopvideorecording', // Camera recording (local)
39
+ ];
40
+
41
+ @override
42
+ List<Violation> analyze({
43
+ required CompilationUnit unit,
44
+ required String filePath,
45
+ required Rule rule,
46
+ required LineInfo lineInfo,
47
+ }) {
48
+ final violations = <Violation>[];
49
+ final visitor = _S028Visitor(
50
+ filePath: filePath,
51
+ lineInfo: lineInfo,
52
+ violations: violations,
53
+ analyzer: this,
54
+ );
55
+ unit.accept(visitor);
56
+ return violations;
57
+ }
58
+ }
59
+
60
+ class _S028Visitor extends RecursiveAstVisitor<void> {
61
+ final String filePath;
62
+ final LineInfo lineInfo;
63
+ final List<Violation> violations;
64
+ final S028FileUploadSizeLimitsAnalyzer analyzer;
65
+
66
+ _S028Visitor({
67
+ required this.filePath,
68
+ required this.lineInfo,
69
+ required this.violations,
70
+ required this.analyzer,
71
+ });
72
+
73
+ // Patterns to detect size limit checks - with word boundaries
74
+ static final _sizeLimitPatterns = [
75
+ RegExp(r'\bsize\b', caseSensitive: false),
76
+ RegExp(r'\blength\b', caseSensitive: false),
77
+ RegExp(r'\blimit\b', caseSensitive: false),
78
+ RegExp(r'\bmaxsize\b', caseSensitive: false),
79
+ RegExp(r'\bmax_size\b', caseSensitive: false),
80
+ RegExp(r'\bbytes\b', caseSensitive: false),
81
+ RegExp(r'\bsizelimit\b', caseSensitive: false),
82
+ RegExp(r'\bfilesize\b', caseSensitive: false),
83
+ RegExp(r'\bcontentlength\b', caseSensitive: false),
84
+ ];
85
+
86
+ bool _hasSizeCheck(AstNode node) {
87
+ AstNode? current = node.parent;
88
+ int depth = 0;
89
+
90
+ // Only check immediate context (within 5 levels), not class-level
91
+ while (current != null && depth < 5) {
92
+ // Skip checking class declarations - too broad
93
+ if (current is ClassDeclaration) {
94
+ break;
95
+ }
96
+
97
+ final parentSource = current.toSource().toLowerCase();
98
+ for (final pattern in _sizeLimitPatterns) {
99
+ if (pattern.hasMatch(parentSource)) {
100
+ return true;
101
+ }
102
+ }
103
+ current = current.parent;
104
+ depth++;
105
+ }
106
+ return false;
107
+ }
108
+
109
+ // Check if the operation is a local file operation (not HTTP upload)
110
+ bool _isLocalFileOperation(MethodInvocation node) {
111
+ final source = node.toSource().toLowerCase();
112
+ final target = node.target?.toSource().toLowerCase() ?? '';
113
+
114
+ // Check if method call target or source matches local file operation patterns
115
+ return S028FileUploadSizeLimitsAnalyzer._localFileOperationPatterns
116
+ .any((p) => source.contains(p) || target.contains(p));
117
+ }
118
+
119
+ @override
120
+ void visitMethodInvocation(MethodInvocation node) {
121
+ final methodName = node.methodName.name.toLowerCase();
122
+ final source = node.toSource().toLowerCase();
123
+
124
+ // Skip local file operations (save to gallery, disk, etc.) - NOT HTTP uploads
125
+ if (_isLocalFileOperation(node)) {
126
+ super.visitMethodInvocation(node);
127
+ return;
128
+ }
129
+
130
+ // Check for file upload constructor calls (parsed as method invocations in Dart)
131
+ // MultipartFile(...) or FormData(...) without 'new' keyword
132
+ if (methodName == 'multipartfile' || methodName == 'formdata') {
133
+ if (!_hasSizeCheck(node)) {
134
+ violations.add(analyzer.createViolation(
135
+ filePath: filePath,
136
+ line: analyzer.getLine(lineInfo, node.offset),
137
+ column: analyzer.getColumn(lineInfo, node.offset),
138
+ message: 'File upload without size limit - enforce maximum file size',
139
+ ));
140
+ }
141
+ }
142
+
143
+ // Skip debug/logging statements - they just mention "upload" in strings
144
+ if (methodName == 'debugprint' || methodName == 'print' ||
145
+ methodName == 'log' || methodName.startsWith('log')) {
146
+ super.visitMethodInvocation(node);
147
+ return;
148
+ }
149
+
150
+ // Check for file upload operations by method name only (not string content)
151
+ bool isUploadOp = S028FileUploadSizeLimitsAnalyzer._uploadMethods
152
+ .any((m) => methodName.contains(m));
153
+
154
+ if (isUploadOp) {
155
+ if (!_hasSizeCheck(node)) {
156
+ violations.add(analyzer.createViolation(
157
+ filePath: filePath,
158
+ line: analyzer.getLine(lineInfo, node.offset),
159
+ column: analyzer.getColumn(lineInfo, node.offset),
160
+ message: 'File upload without size limit check - enforce maximum file size',
161
+ ));
162
+ }
163
+ }
164
+
165
+ super.visitMethodInvocation(node);
166
+ }
167
+
168
+ @override
169
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
170
+ final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
171
+
172
+ // Check for MultipartFile or FormData creation (when 'new' keyword is used)
173
+ if (typeName == 'multipartfile' || typeName == 'formdata') {
174
+ if (!_hasSizeCheck(node)) {
175
+ violations.add(analyzer.createViolation(
176
+ filePath: filePath,
177
+ line: analyzer.getLine(lineInfo, node.offset),
178
+ column: analyzer.getColumn(lineInfo, node.offset),
179
+ message: 'File upload creation without size limit',
180
+ ));
181
+ }
182
+ }
183
+
184
+ super.visitInstanceCreationExpression(node);
185
+ }
186
+ }
@@ -0,0 +1,171 @@
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
+ /// S029: CSRF Protection
10
+ /// Ensure CSRF protection is implemented for state-changing HTTP operations
11
+ class S029CsrfProtectionAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S029';
14
+
15
+ // HTTP client methods that indicate actual network requests
16
+ static const _httpClientMethods = [
17
+ 'http.post', 'http.put', 'http.patch', 'http.delete',
18
+ 'dio.post', 'dio.put', 'dio.patch', 'dio.delete',
19
+ 'client.post', 'client.put', 'client.patch', 'client.delete',
20
+ ];
21
+
22
+ // HTTP method names when called on HTTP clients
23
+ static const _httpMethods = ['post', 'put', 'patch', 'delete'];
24
+
25
+ // HTTP client class names
26
+ static const _httpClients = [
27
+ 'http', 'dio', 'client', 'httpclient', 'http_client',
28
+ 'apiservice', 'api_service', 'apiclient', 'api_client',
29
+ ];
30
+
31
+ // CSRF protection indicators
32
+ static const _csrfIndicators = [
33
+ 'csrf', 'xsrf', '_token', 'csrftoken', 'csrf_token',
34
+ 'x-csrf-token', 'x-xsrf-token', 'antiforgery',
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
+ // Skip Flutter UI files - they don't make direct HTTP calls
45
+ final fileName = filePath.toLowerCase();
46
+ if (fileName.contains('screen') ||
47
+ fileName.contains('widget') ||
48
+ fileName.contains('view') ||
49
+ fileName.contains('page') ||
50
+ fileName.contains('component') ||
51
+ fileName.contains('_item') ||
52
+ fileName.contains('effect_item') ||
53
+ fileName.contains('stepper') ||
54
+ fileName.contains('step_')) {
55
+ return [];
56
+ }
57
+
58
+ final violations = <Violation>[];
59
+ final visitor = _S029Visitor(
60
+ filePath: filePath,
61
+ lineInfo: lineInfo,
62
+ violations: violations,
63
+ analyzer: this,
64
+ );
65
+ unit.accept(visitor);
66
+ return violations;
67
+ }
68
+ }
69
+
70
+ class _S029Visitor extends RecursiveAstVisitor<void> {
71
+ final String filePath;
72
+ final LineInfo lineInfo;
73
+ final List<Violation> violations;
74
+ final S029CsrfProtectionAnalyzer analyzer;
75
+
76
+ _S029Visitor({
77
+ required this.filePath,
78
+ required this.lineInfo,
79
+ required this.violations,
80
+ required this.analyzer,
81
+ });
82
+
83
+ @override
84
+ void visitMethodInvocation(MethodInvocation node) {
85
+ final methodName = node.methodName.name.toLowerCase();
86
+
87
+ // Only check if it's an HTTP method
88
+ if (!S029CsrfProtectionAnalyzer._httpMethods.contains(methodName)) {
89
+ super.visitMethodInvocation(node);
90
+ return;
91
+ }
92
+
93
+ // Check if the method is called on an HTTP client
94
+ final target = node.target;
95
+ if (target == null) {
96
+ super.visitMethodInvocation(node);
97
+ return;
98
+ }
99
+
100
+ final targetSource = target.toSource().toLowerCase();
101
+ bool isHttpClient = S029CsrfProtectionAnalyzer._httpClients
102
+ .any((c) => targetSource.contains(c));
103
+
104
+ if (!isHttpClient) {
105
+ super.visitMethodInvocation(node);
106
+ return;
107
+ }
108
+
109
+ // This is a state-changing HTTP call - check for CSRF protection
110
+ final source = node.toSource().toLowerCase();
111
+
112
+ bool hasCsrfProtection = S029CsrfProtectionAnalyzer._csrfIndicators
113
+ .any((i) => source.contains(i));
114
+
115
+ if (!hasCsrfProtection) {
116
+ // Check in surrounding context (headers, options, etc.)
117
+ AstNode? current = node.parent;
118
+ int depth = 0;
119
+
120
+ while (current != null && depth < 5) {
121
+ final parentSource = current.toSource().toLowerCase();
122
+ if (S029CsrfProtectionAnalyzer._csrfIndicators.any((i) => parentSource.contains(i))) {
123
+ hasCsrfProtection = true;
124
+ break;
125
+ }
126
+ current = current.parent;
127
+ depth++;
128
+ }
129
+
130
+ if (!hasCsrfProtection) {
131
+ violations.add(analyzer.createViolation(
132
+ filePath: filePath,
133
+ line: analyzer.getLine(lineInfo, node.offset),
134
+ column: analyzer.getColumn(lineInfo, node.offset),
135
+ message: 'CSRF protection may be missing for HTTP ${methodName.toUpperCase()} request',
136
+ ));
137
+ }
138
+ }
139
+
140
+ super.visitMethodInvocation(node);
141
+ }
142
+
143
+ @override
144
+ void visitAnnotation(Annotation node) {
145
+ final name = node.name.toSource().toLowerCase();
146
+
147
+ // Check for route annotations with state-changing methods
148
+ if (name.contains('route') || name.contains('mapping')) {
149
+ final source = node.toSource().toLowerCase();
150
+
151
+ bool isStateChanging = S029CsrfProtectionAnalyzer._httpMethods
152
+ .any((m) => source.contains("'$m'") || source.contains('"$m"'));
153
+
154
+ if (isStateChanging) {
155
+ bool hasCsrfProtection = S029CsrfProtectionAnalyzer._csrfIndicators
156
+ .any((i) => source.contains(i));
157
+
158
+ if (!hasCsrfProtection) {
159
+ violations.add(analyzer.createViolation(
160
+ filePath: filePath,
161
+ line: analyzer.getLine(lineInfo, node.offset),
162
+ column: analyzer.getColumn(lineInfo, node.offset),
163
+ message: 'Route with state-changing method should have CSRF protection',
164
+ ));
165
+ }
166
+ }
167
+ }
168
+
169
+ super.visitAnnotation(node);
170
+ }
171
+ }
@@ -0,0 +1,144 @@
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
+ /// S030: Directory Browsing Protection
10
+ /// Prevent directory listing/browsing attacks on web servers
11
+ class S030DirectoryBrowsingProtectionAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S030';
14
+
15
+ // Actual static file serving methods in web frameworks
16
+ static const _webServerMethods = [
17
+ 'createstatichandler', 'staticfilehandler', 'statichandler',
18
+ 'servedir', 'serve_dir', 'servestaticfiles',
19
+ 'staticfilemiddleware', 'staticfiles',
20
+ ];
21
+
22
+ // Web server frameworks/packages
23
+ static const _webServerPackages = [
24
+ 'shelf_static', 'shelf', 'dart_frog', 'angel',
25
+ 'conduit', 'aqueduct', 'express',
26
+ ];
27
+
28
+ @override
29
+ List<Violation> analyze({
30
+ required CompilationUnit unit,
31
+ required String filePath,
32
+ required Rule rule,
33
+ required LineInfo lineInfo,
34
+ }) {
35
+ // Only analyze files that are likely web server related
36
+ final fileName = filePath.toLowerCase();
37
+ final isWebServerFile = fileName.contains('server') ||
38
+ fileName.contains('handler') ||
39
+ fileName.contains('middleware') ||
40
+ fileName.contains('route') ||
41
+ fileName.contains('api');
42
+
43
+ // Skip mobile/Flutter UI files
44
+ if (fileName.contains('screen') ||
45
+ fileName.contains('widget') ||
46
+ fileName.contains('view') ||
47
+ fileName.contains('page') ||
48
+ fileName.contains('notifier') ||
49
+ fileName.contains('provider') ||
50
+ fileName.contains('state')) {
51
+ return [];
52
+ }
53
+
54
+ final violations = <Violation>[];
55
+ final visitor = _S030Visitor(
56
+ filePath: filePath,
57
+ lineInfo: lineInfo,
58
+ violations: violations,
59
+ analyzer: this,
60
+ );
61
+ unit.accept(visitor);
62
+ return violations;
63
+ }
64
+ }
65
+
66
+ class _S030Visitor extends RecursiveAstVisitor<void> {
67
+ final String filePath;
68
+ final LineInfo lineInfo;
69
+ final List<Violation> violations;
70
+ final S030DirectoryBrowsingProtectionAnalyzer analyzer;
71
+
72
+ _S030Visitor({
73
+ required this.filePath,
74
+ required this.lineInfo,
75
+ required this.violations,
76
+ required this.analyzer,
77
+ });
78
+
79
+ @override
80
+ void visitMethodInvocation(MethodInvocation node) {
81
+ final methodName = node.methodName.name.toLowerCase();
82
+ final source = node.toSource().toLowerCase();
83
+
84
+ // Only check actual web server static file serving methods
85
+ bool isWebServerStaticServe = S030DirectoryBrowsingProtectionAnalyzer._webServerMethods
86
+ .any((m) => methodName.contains(m) || source.contains(m));
87
+
88
+ // Also check if using web server packages
89
+ bool usesWebServerPackage = S030DirectoryBrowsingProtectionAnalyzer._webServerPackages
90
+ .any((p) => source.contains(p));
91
+
92
+ if (isWebServerStaticServe || usesWebServerPackage) {
93
+ // Check for listing/browsing configuration
94
+ bool hasListingConfig = source.contains('listdirectory') ||
95
+ source.contains('list_directory') ||
96
+ source.contains('directorylisting') ||
97
+ source.contains('directory_listing') ||
98
+ source.contains('allowlisting') ||
99
+ source.contains('allow_listing');
100
+
101
+ // Check if listing is explicitly enabled
102
+ if (hasListingConfig && source.contains('true')) {
103
+ violations.add(analyzer.createViolation(
104
+ filePath: filePath,
105
+ line: analyzer.getLine(lineInfo, node.offset),
106
+ column: analyzer.getColumn(lineInfo, node.offset),
107
+ message: 'Directory listing enabled on static file server - security risk',
108
+ ));
109
+ } else if (!hasListingConfig && isWebServerStaticServe) {
110
+ // Only warn if it's definitely a static file handler without config
111
+ violations.add(analyzer.createViolation(
112
+ filePath: filePath,
113
+ line: analyzer.getLine(lineInfo, node.offset),
114
+ column: analyzer.getColumn(lineInfo, node.offset),
115
+ message: 'Static file handler - ensure directory listing is disabled',
116
+ ));
117
+ }
118
+ }
119
+
120
+ super.visitMethodInvocation(node);
121
+ }
122
+
123
+ @override
124
+ void visitNamedExpression(NamedExpression node) {
125
+ final paramName = node.name.label.name.toLowerCase();
126
+
127
+ // Check for directory listing parameter in web server context
128
+ if (paramName.contains('listing') || paramName.contains('browse')) {
129
+ if (node.expression is BooleanLiteral) {
130
+ final value = (node.expression as BooleanLiteral).value;
131
+ if (value) {
132
+ violations.add(analyzer.createViolation(
133
+ filePath: filePath,
134
+ line: analyzer.getLine(lineInfo, node.offset),
135
+ column: analyzer.getColumn(lineInfo, node.offset),
136
+ message: 'Directory listing enabled - security risk',
137
+ ));
138
+ }
139
+ }
140
+ }
141
+
142
+ super.visitNamedExpression(node);
143
+ }
144
+ }