@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,159 @@
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
+ /// S053: Return generic error messages, hide internal details
10
+ /// Detect exposure of internal details in error responses
11
+ class S053GenericErrorMessagesAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S053';
14
+
15
+ @override
16
+ List<Violation> analyze({
17
+ required CompilationUnit unit,
18
+ required String filePath,
19
+ required Rule rule,
20
+ required LineInfo lineInfo,
21
+ }) {
22
+ final violations = <Violation>[];
23
+ final visitor = _S053Visitor(
24
+ filePath: filePath,
25
+ lineInfo: lineInfo,
26
+ violations: violations,
27
+ analyzer: this,
28
+ );
29
+ unit.accept(visitor);
30
+ return violations;
31
+ }
32
+ }
33
+
34
+ class _S053Visitor extends RecursiveAstVisitor<void> {
35
+ final String filePath;
36
+ final LineInfo lineInfo;
37
+ final List<Violation> violations;
38
+ final S053GenericErrorMessagesAnalyzer analyzer;
39
+
40
+ // Track if we're inside a catch block
41
+ bool _inCatchBlock = false;
42
+
43
+ _S053Visitor({
44
+ required this.filePath,
45
+ required this.lineInfo,
46
+ required this.violations,
47
+ required this.analyzer,
48
+ });
49
+
50
+ @override
51
+ void visitCatchClause(CatchClause node) {
52
+ _inCatchBlock = true;
53
+ super.visitCatchClause(node);
54
+ _inCatchBlock = false;
55
+ }
56
+
57
+ @override
58
+ void visitMethodInvocation(MethodInvocation node) {
59
+ final methodName = node.methodName.name.toLowerCase();
60
+ final source = node.toSource().toLowerCase();
61
+
62
+ // Check for HTTP response methods that expose error details
63
+ if (_inCatchBlock) {
64
+ // Check for Response or json methods with error details
65
+ if (methodName == 'json' ||
66
+ methodName == 'send' ||
67
+ methodName == 'write') {
68
+ // Check if error stack trace is exposed
69
+ if (source.contains('.stacktrace') || source.contains('.stack')) {
70
+ violations.add(analyzer.createViolation(
71
+ filePath: filePath,
72
+ line: analyzer.getLine(lineInfo, node.offset),
73
+ column: analyzer.getColumn(lineInfo, node.offset),
74
+ message:
75
+ 'Stack trace exposed in response - return generic error message',
76
+ ));
77
+ }
78
+
79
+ // Check if error message is directly exposed
80
+ if (source.contains('error.message') ||
81
+ source.contains('exception.message') ||
82
+ source.contains('e.message')) {
83
+ violations.add(analyzer.createViolation(
84
+ filePath: filePath,
85
+ line: analyzer.getLine(lineInfo, node.offset),
86
+ column: analyzer.getColumn(lineInfo, node.offset),
87
+ message:
88
+ 'Internal error message exposed - return generic error message',
89
+ ));
90
+ }
91
+
92
+ // Check for SQL/database details
93
+ if (source.contains('sql') ||
94
+ source.contains('query') ||
95
+ source.contains('database')) {
96
+ violations.add(analyzer.createViolation(
97
+ filePath: filePath,
98
+ line: analyzer.getLine(lineInfo, node.offset),
99
+ column: analyzer.getColumn(lineInfo, node.offset),
100
+ message: 'SQL/database details may be exposed in response',
101
+ ));
102
+ }
103
+ }
104
+ }
105
+
106
+ // Check for toString() on exceptions in response context
107
+ if (methodName == 'tostring' && _inCatchBlock) {
108
+ final target = node.target?.toSource().toLowerCase() ?? '';
109
+ if (target.contains('error') ||
110
+ target.contains('exception') ||
111
+ target == 'e') {
112
+ // Check if this is being sent to client
113
+ AstNode? current = node.parent;
114
+ int depth = 0;
115
+ while (current != null && depth < 5) {
116
+ final parentSource = current.toSource().toLowerCase();
117
+ if (parentSource.contains('response') ||
118
+ parentSource.contains('json') ||
119
+ parentSource.contains('send')) {
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
+ 'Exception toString() exposed to client - use generic message',
126
+ ));
127
+ break;
128
+ }
129
+ current = current.parent;
130
+ depth++;
131
+ }
132
+ }
133
+ }
134
+
135
+ super.visitMethodInvocation(node);
136
+ }
137
+
138
+ @override
139
+ void visitThrowExpression(ThrowExpression node) {
140
+ final source = node.toSource().toLowerCase();
141
+
142
+ // Check for throwing exceptions with sensitive details
143
+ if (source.contains('file path') ||
144
+ source.contains('directory') ||
145
+ source.contains('connection string') ||
146
+ source.contains('password') ||
147
+ source.contains('secret')) {
148
+ violations.add(analyzer.createViolation(
149
+ filePath: filePath,
150
+ line: analyzer.getLine(lineInfo, node.offset),
151
+ column: analyzer.getColumn(lineInfo, node.offset),
152
+ message:
153
+ 'Exception may contain sensitive details - use generic message',
154
+ ));
155
+ }
156
+
157
+ super.visitThrowExpression(node);
158
+ }
159
+ }
@@ -0,0 +1,141 @@
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
+ /// S054: No Default Accounts
10
+ /// Avoid hardcoded default accounts/credentials
11
+ class S054NoDefaultAccountsAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S054';
14
+
15
+ // Common default account patterns
16
+ static const _defaultAccountPatterns = [
17
+ 'admin', 'administrator', 'root', 'superuser', 'super_user',
18
+ 'guest', 'test', 'demo', 'default', 'system',
19
+ ];
20
+
21
+ // Default password patterns
22
+ static const _defaultPasswordPatterns = [
23
+ 'admin', 'password', '123456', 'admin123', 'password123',
24
+ 'root', 'guest', 'default', 'changeme', 'test',
25
+ ];
26
+
27
+ @override
28
+ List<Violation> analyze({
29
+ required CompilationUnit unit,
30
+ required String filePath,
31
+ required Rule rule,
32
+ required LineInfo lineInfo,
33
+ }) {
34
+ final violations = <Violation>[];
35
+ final visitor = _S054Visitor(
36
+ filePath: filePath,
37
+ lineInfo: lineInfo,
38
+ violations: violations,
39
+ analyzer: this,
40
+ );
41
+ unit.accept(visitor);
42
+ return violations;
43
+ }
44
+ }
45
+
46
+ class _S054Visitor extends RecursiveAstVisitor<void> {
47
+ final String filePath;
48
+ final LineInfo lineInfo;
49
+ final List<Violation> violations;
50
+ final S054NoDefaultAccountsAnalyzer analyzer;
51
+
52
+ _S054Visitor({
53
+ required this.filePath,
54
+ required this.lineInfo,
55
+ required this.violations,
56
+ required this.analyzer,
57
+ });
58
+
59
+ @override
60
+ void visitVariableDeclaration(VariableDeclaration node) {
61
+ final varName = node.name.lexeme.toLowerCase();
62
+ final initializer = node.initializer;
63
+
64
+ // Check for default user/account names
65
+ if ((varName.contains('user') || varName.contains('account') || varName.contains('login')) &&
66
+ varName.contains('default')) {
67
+ if (initializer is SimpleStringLiteral) {
68
+ final value = initializer.value.toLowerCase();
69
+ bool isDefaultAccount = S054NoDefaultAccountsAnalyzer._defaultAccountPatterns
70
+ .any((p) => value == p);
71
+
72
+ if (isDefaultAccount) {
73
+ violations.add(analyzer.createViolation(
74
+ filePath: filePath,
75
+ line: analyzer.getLine(lineInfo, node.offset),
76
+ column: analyzer.getColumn(lineInfo, node.offset),
77
+ message: 'Avoid hardcoded default account "$value"',
78
+ ));
79
+ }
80
+ }
81
+ }
82
+
83
+ // Check for default passwords
84
+ if ((varName.contains('password') || varName.contains('pwd')) &&
85
+ varName.contains('default')) {
86
+ if (initializer is SimpleStringLiteral) {
87
+ violations.add(analyzer.createViolation(
88
+ filePath: filePath,
89
+ line: analyzer.getLine(lineInfo, node.offset),
90
+ column: analyzer.getColumn(lineInfo, node.offset),
91
+ message: 'Hardcoded default password detected',
92
+ ));
93
+ }
94
+ }
95
+
96
+ super.visitVariableDeclaration(node);
97
+ }
98
+
99
+ @override
100
+ void visitMapLiteralEntry(MapLiteralEntry node) {
101
+ final key = node.key.toSource().toLowerCase().replaceAll("'", '').replaceAll('"', '');
102
+ final value = node.value;
103
+
104
+ // Check for default credentials in config maps
105
+ if (key.contains('user') || key.contains('account')) {
106
+ if (value is SimpleStringLiteral) {
107
+ final valStr = value.value.toLowerCase();
108
+ bool isDefaultAccount = S054NoDefaultAccountsAnalyzer._defaultAccountPatterns
109
+ .any((p) => valStr == p);
110
+
111
+ if (isDefaultAccount) {
112
+ violations.add(analyzer.createViolation(
113
+ filePath: filePath,
114
+ line: analyzer.getLine(lineInfo, node.offset),
115
+ column: analyzer.getColumn(lineInfo, node.offset),
116
+ message: 'Default account in configuration',
117
+ ));
118
+ }
119
+ }
120
+ }
121
+
122
+ if (key.contains('password') || key.contains('pwd')) {
123
+ if (value is SimpleStringLiteral) {
124
+ final valStr = value.value.toLowerCase();
125
+ bool isDefaultPassword = S054NoDefaultAccountsAnalyzer._defaultPasswordPatterns
126
+ .any((p) => valStr == p);
127
+
128
+ if (isDefaultPassword) {
129
+ violations.add(analyzer.createViolation(
130
+ filePath: filePath,
131
+ line: analyzer.getLine(lineInfo, node.offset),
132
+ column: analyzer.getColumn(lineInfo, node.offset),
133
+ message: 'Default/weak password in configuration',
134
+ ));
135
+ }
136
+ }
137
+ }
138
+
139
+ super.visitMapLiteralEntry(node);
140
+ }
141
+ }
@@ -0,0 +1,324 @@
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
+ /// S055: Content-Type Validation
10
+ /// Validate Content-Type header for uploaded files
11
+ /// Note: This rule should only flag upload implementations,
12
+ /// not callers of upload functions that already validate Content-Type
13
+ class S055ContentTypeValidationAnalyzer extends BaseAnalyzer {
14
+ @override
15
+ String get ruleId => 'S055';
16
+
17
+ // File upload method patterns - HTTP uploads only
18
+ // Note: Local file operations (save to gallery, write to disk) are NOT uploads
19
+ static const _uploadPatterns = [
20
+ 'uploadfile', 'upload_file', 'processupload', 'process_upload',
21
+ 'handlemultipart', 'handle_multipart', 'saveformdata', 'save_form_data',
22
+ ];
23
+
24
+ // Patterns that are NOT HTTP uploads (local file operations)
25
+ // These should be excluded from Content-Type validation requirement
26
+ static const _localFileOperationPatterns = [
27
+ 'imagegallerysaver', // Flutter plugin to save to device gallery
28
+ 'gallerysaver', // Save to gallery
29
+ 'saveimagetogallery', // Save image to gallery
30
+ 'savevideoTogallery', // Save video to gallery
31
+ 'filesaver', // File saver plugins
32
+ 'savetodisk', // Local disk save
33
+ 'writetofile', // Local file write
34
+ 'localfilesystem', // Local filesystem operations
35
+ 'pathprovider', // Flutter path provider (local storage)
36
+ 'gettemporarydirectory', // Temp directory
37
+ 'getapplicationdocumentsdirectory', // App documents
38
+ ];
39
+
40
+ // Patterns that indicate Content-Type is being validated/set
41
+ static const _contentTypeValidationPatterns = [
42
+ 'lookupmimetype', 'lookup_mime_type', // mime package
43
+ 'getmimetype', 'get_mime_type',
44
+ 'mimetype', 'mime_type',
45
+ 'contenttype', 'content_type',
46
+ 'mediatype', 'media_type',
47
+ ];
48
+
49
+ // Patterns indicating upload is delegated to another function
50
+ static const _uploadDelegationPatterns = [
51
+ 'uploadimage', 'upload_image', // Calling uploadImage function
52
+ 'uploadfile', 'upload_file',
53
+ 'repository', 'usecase', 'use_case',
54
+ 'mediarepo', 'media_repo',
55
+ ];
56
+
57
+ // Method name patterns that are NOT actual file uploads
58
+ // These methods create/get URLs but don't upload files themselves
59
+ static const _nonUploadMethodPatterns = [
60
+ 'createuploadurl', 'create_upload_url',
61
+ 'getuploadurl', 'get_upload_url',
62
+ 'generateuploadurl', 'generate_upload_url',
63
+ 'presignedurl', 'presigned_url',
64
+ 'uploadurl', 'upload_url', // Methods that return URLs, not upload
65
+ ];
66
+
67
+ // Client-side upload patterns that automatically handle Content-Type
68
+ // Dio's MultipartFile.fromFile() automatically infers Content-Type from file extension
69
+ // These don't need explicit Content-Type validation in client code
70
+ static const _clientSideUploadPatterns = [
71
+ 'multipartfile.fromfile', // Dio: auto-detects Content-Type from extension
72
+ 'multipartfile.frombytes', // Dio: requires explicit contentType but often inferred
73
+ 'multipartfile.fromstream', // Dio: similar auto-detection
74
+ 'formdata.frommap', // Dio FormData constructor
75
+ ];
76
+
77
+ @override
78
+ List<Violation> analyze({
79
+ required CompilationUnit unit,
80
+ required String filePath,
81
+ required Rule rule,
82
+ required LineInfo lineInfo,
83
+ }) {
84
+ final violations = <Violation>[];
85
+ final visitor = _S055Visitor(
86
+ filePath: filePath,
87
+ lineInfo: lineInfo,
88
+ violations: violations,
89
+ analyzer: this,
90
+ );
91
+ unit.accept(visitor);
92
+ return violations;
93
+ }
94
+ }
95
+
96
+ class _S055Visitor extends RecursiveAstVisitor<void> {
97
+ final String filePath;
98
+ final LineInfo lineInfo;
99
+ final List<Violation> violations;
100
+ final S055ContentTypeValidationAnalyzer analyzer;
101
+
102
+ _S055Visitor({
103
+ required this.filePath,
104
+ required this.lineInfo,
105
+ required this.violations,
106
+ required this.analyzer,
107
+ });
108
+
109
+ // Patterns to detect content type checks - with word boundaries
110
+ static final _contentTypePatterns = [
111
+ RegExp(r'\bcontent[-_]?type\b', caseSensitive: false),
112
+ RegExp(r'\bcontenttype\b', caseSensitive: false),
113
+ RegExp(r'\bmime[-_]?type\b', caseSensitive: false),
114
+ RegExp(r'\bmimetype\b', caseSensitive: false),
115
+ RegExp(r'\bmedia[-_]?type\b', caseSensitive: false),
116
+ RegExp(r'\bmediatype\b', caseSensitive: false),
117
+ RegExp(r'\blookupmimetype\b', caseSensitive: false),
118
+ ];
119
+
120
+ bool _hasContentTypeCheck(AstNode node) {
121
+ AstNode? current = node.parent;
122
+ int depth = 0;
123
+
124
+ // Only check immediate context (within 5 levels), not class-level
125
+ while (current != null && depth < 5) {
126
+ // Skip checking class declarations - too broad
127
+ if (current is ClassDeclaration) {
128
+ break;
129
+ }
130
+
131
+ final parentSource = current.toSource().toLowerCase();
132
+ for (final pattern in _contentTypePatterns) {
133
+ if (pattern.hasMatch(parentSource)) {
134
+ return true;
135
+ }
136
+ }
137
+ current = current.parent;
138
+ depth++;
139
+ }
140
+ return false;
141
+ }
142
+
143
+ // Check if the method body delegates to a function that handles content-type
144
+ bool _delegatesToValidatedUpload(AstNode node) {
145
+ // Get the containing method body
146
+ AstNode? current = node;
147
+ while (current != null && current is! MethodDeclaration && current is! FunctionDeclaration) {
148
+ current = current.parent;
149
+ }
150
+
151
+ if (current == null) return false;
152
+
153
+ String bodySource;
154
+ if (current is MethodDeclaration) {
155
+ bodySource = current.body?.toSource().toLowerCase().replaceAll('_', '') ?? '';
156
+ } else if (current is FunctionDeclaration) {
157
+ bodySource = current.functionExpression.body?.toSource().toLowerCase().replaceAll('_', '') ?? '';
158
+ } else {
159
+ return false;
160
+ }
161
+
162
+ // Check if calling a function that validates content-type
163
+ return S055ContentTypeValidationAnalyzer._uploadDelegationPatterns
164
+ .any((p) => bodySource.contains(p.replaceAll('_', '')));
165
+ }
166
+
167
+ // Check if the containing method is a URL creation method (not actual upload)
168
+ bool _isUrlCreationMethod(AstNode node) {
169
+ // Get the containing method
170
+ AstNode? current = node;
171
+ while (current != null && current is! MethodDeclaration && current is! FunctionDeclaration) {
172
+ current = current.parent;
173
+ }
174
+
175
+ if (current == null) return false;
176
+
177
+ String methodName;
178
+ if (current is MethodDeclaration) {
179
+ methodName = current.name.lexeme.toLowerCase().replaceAll('_', '');
180
+ } else if (current is FunctionDeclaration) {
181
+ methodName = current.name.lexeme.toLowerCase().replaceAll('_', '');
182
+ } else {
183
+ return false;
184
+ }
185
+
186
+ // Check if method name indicates URL creation (not actual upload)
187
+ return S055ContentTypeValidationAnalyzer._nonUploadMethodPatterns
188
+ .any((p) => methodName.contains(p.replaceAll('_', '')));
189
+ }
190
+
191
+ // Check if the operation is a local file operation (not HTTP upload)
192
+ bool _isLocalFileOperation(MethodInvocation node) {
193
+ final source = node.toSource().toLowerCase();
194
+ final target = node.target?.toSource().toLowerCase() ?? '';
195
+
196
+ // Check if method call target or source matches local file operation patterns
197
+ return S055ContentTypeValidationAnalyzer._localFileOperationPatterns
198
+ .any((p) => source.contains(p) || target.contains(p));
199
+ }
200
+
201
+ // Check if using client-side upload that auto-handles Content-Type
202
+ // Dio's MultipartFile.fromFile() automatically infers Content-Type from file extension
203
+ bool _isClientSideAutoUpload(String source) {
204
+ final sourceLower = source.toLowerCase().replaceAll(' ', '');
205
+ return S055ContentTypeValidationAnalyzer._clientSideUploadPatterns
206
+ .any((p) => sourceLower.contains(p.replaceAll(' ', '')));
207
+ }
208
+
209
+ @override
210
+ void visitMethodInvocation(MethodInvocation node) {
211
+ final methodName = node.methodName.name.toLowerCase();
212
+ final source = node.toSource().toLowerCase();
213
+
214
+ // Skip local file operations (save to gallery, disk, etc.) - NOT HTTP uploads
215
+ if (_isLocalFileOperation(node)) {
216
+ super.visitMethodInvocation(node);
217
+ return;
218
+ }
219
+
220
+ // Check for file upload constructor calls (parsed as method invocations in Dart)
221
+ // FormData(...) or MultipartFile(...) without 'new' keyword
222
+ if (methodName == 'formdata' || methodName == 'multipartfile') {
223
+ // Skip if using Dio's auto Content-Type detection (fromFile, fromMap, etc.)
224
+ if (_isClientSideAutoUpload(source)) {
225
+ super.visitMethodInvocation(node);
226
+ return;
227
+ }
228
+
229
+ if (!_hasContentTypeCheck(node) && !_delegatesToValidatedUpload(node)) {
230
+ violations.add(analyzer.createViolation(
231
+ filePath: filePath,
232
+ line: analyzer.getLine(lineInfo, node.offset),
233
+ column: analyzer.getColumn(lineInfo, node.offset),
234
+ message: 'File upload without Content-Type validation',
235
+ ));
236
+ }
237
+ }
238
+
239
+ // Only flag actual upload IMPLEMENTATIONS, not callers of upload functions
240
+ // A caller like `uploadVideoToChat(...)` should not be flagged
241
+ // Only flag if this method directly creates FormData/MultipartFile or uses HTTP upload APIs
242
+
243
+ // Check if this is a CALLER of an upload function (not implementation)
244
+ // Callers just invoke methods like `notifier.uploadVideo()` - these are OK
245
+ bool isCallingUploadFunction = methodName.contains('upload') &&
246
+ node.target != null && // Has a target (e.g., notifier.uploadX)
247
+ !source.contains('formdata') &&
248
+ !source.contains('multipartfile') &&
249
+ !source.contains('.post(') &&
250
+ !source.contains('.put(');
251
+
252
+ if (isCallingUploadFunction) {
253
+ // This is just calling an upload function, not implementing upload logic
254
+ super.visitMethodInvocation(node);
255
+ return;
256
+ }
257
+
258
+ // Check for actual HTTP upload operations (dio.post with FormData, etc.)
259
+ // Only match FormData() constructor call or FormData.fromMap() - NOT variable names like formData
260
+ // Use case-sensitive match to distinguish:
261
+ // - FormData() or FormData.fromMap() → Dio HTTP upload class (MUST check)
262
+ // - formData → local variable name (skip)
263
+ final formDataClassPattern = RegExp(r'FormData\s*[.(]'); // FormData() or FormData.fromMap(
264
+ final multipartFilePattern = RegExp(r'MultipartFile\s*[.(]'); // MultipartFile() or MultipartFile.fromFile(
265
+
266
+ bool hasFormData = formDataClassPattern.hasMatch(node.toSource());
267
+ bool hasMultipartFile = multipartFilePattern.hasMatch(node.toSource());
268
+
269
+ bool isActualUploadImplementation =
270
+ (hasFormData || hasMultipartFile) ||
271
+ ((methodName == 'post' || methodName == 'put') &&
272
+ (hasFormData || hasMultipartFile));
273
+
274
+ if (isActualUploadImplementation) {
275
+ // Skip if using Dio's auto Content-Type detection (fromFile, fromMap, etc.)
276
+ if (_isClientSideAutoUpload(source)) {
277
+ super.visitMethodInvocation(node);
278
+ return;
279
+ }
280
+
281
+ // Skip if this is a call to a function that handles content-type validation
282
+ if (_delegatesToValidatedUpload(node)) {
283
+ super.visitMethodInvocation(node);
284
+ return;
285
+ }
286
+
287
+ // Skip if this is inside a URL creation method (not actual file upload)
288
+ if (_isUrlCreationMethod(node)) {
289
+ super.visitMethodInvocation(node);
290
+ return;
291
+ }
292
+
293
+ if (!_hasContentTypeCheck(node)) {
294
+ violations.add(analyzer.createViolation(
295
+ filePath: filePath,
296
+ line: analyzer.getLine(lineInfo, node.offset),
297
+ column: analyzer.getColumn(lineInfo, node.offset),
298
+ message: 'File upload without Content-Type validation',
299
+ ));
300
+ }
301
+ }
302
+
303
+ super.visitMethodInvocation(node);
304
+ }
305
+
306
+ @override
307
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
308
+ final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
309
+
310
+ // Check for FormData or MultipartFile creation (when 'new' keyword is used)
311
+ if (typeName == 'formdata' || typeName == 'multipartfile') {
312
+ if (!_hasContentTypeCheck(node) && !_delegatesToValidatedUpload(node)) {
313
+ violations.add(analyzer.createViolation(
314
+ filePath: filePath,
315
+ line: analyzer.getLine(lineInfo, node.offset),
316
+ column: analyzer.getColumn(lineInfo, node.offset),
317
+ message: 'File upload creation without Content-Type validation',
318
+ ));
319
+ }
320
+ }
321
+
322
+ super.visitInstanceCreationExpression(node);
323
+ }
324
+ }