@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,191 @@
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
+ /// S017: Use Parameterized Queries
10
+ /// Detect SQL injection vulnerabilities - use parameterized queries
11
+ class S017UseParameterizedQueriesAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S017';
14
+
15
+ // SQL statement patterns that strongly indicate a query (not just keywords)
16
+ static const _sqlStatementPatterns = [
17
+ 'select * from', 'select ', 'insert into', 'update ',
18
+ 'delete from', 'drop table', 'create table', 'alter table',
19
+ 'truncate table', 'exec ', 'execute ',
20
+ ];
21
+
22
+ // Raw query method names in common Dart/Flutter DB packages
23
+ static const _rawQueryMethods = [
24
+ 'rawquery', 'rawinsert', 'rawupdate', 'rawdelete',
25
+ 'execute', 'query', 'executesql',
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
+ // Skip UI files - they don't have SQL
36
+ final fileName = filePath.toLowerCase();
37
+ if (fileName.contains('screen') ||
38
+ fileName.contains('widget') ||
39
+ fileName.contains('view') ||
40
+ fileName.contains('page') ||
41
+ fileName.contains('component')) {
42
+ return [];
43
+ }
44
+
45
+ final violations = <Violation>[];
46
+ final visitor = _S017Visitor(
47
+ filePath: filePath,
48
+ lineInfo: lineInfo,
49
+ violations: violations,
50
+ analyzer: this,
51
+ );
52
+ unit.accept(visitor);
53
+ return violations;
54
+ }
55
+
56
+ /// Check if source looks like actual SQL
57
+ static bool looksLikeSql(String source) {
58
+ final lowerSource = source.toLowerCase();
59
+ return _sqlStatementPatterns.any((p) => lowerSource.contains(p));
60
+ }
61
+ }
62
+
63
+ class _S017Visitor extends RecursiveAstVisitor<void> {
64
+ final String filePath;
65
+ final LineInfo lineInfo;
66
+ final List<Violation> violations;
67
+ final S017UseParameterizedQueriesAnalyzer analyzer;
68
+
69
+ _S017Visitor({
70
+ required this.filePath,
71
+ required this.lineInfo,
72
+ required this.violations,
73
+ required this.analyzer,
74
+ });
75
+
76
+ @override
77
+ void visitStringInterpolation(StringInterpolation node) {
78
+ // Skip if this is inside a logging call - not SQL context
79
+ if (_isInLoggingContext(node)) {
80
+ super.visitStringInterpolation(node);
81
+ return;
82
+ }
83
+
84
+ final source = node.toSource();
85
+
86
+ // Only check if it looks like actual SQL (not just contains common words)
87
+ if (S017UseParameterizedQueriesAnalyzer.looksLikeSql(source)) {
88
+ // Check if there are interpolation elements
89
+ bool hasInterpolation = node.elements.any((e) => e is InterpolationExpression);
90
+
91
+ if (hasInterpolation) {
92
+ violations.add(analyzer.createViolation(
93
+ filePath: filePath,
94
+ line: analyzer.getLine(lineInfo, node.offset),
95
+ column: analyzer.getColumn(lineInfo, node.offset),
96
+ message: 'SQL injection risk - use parameterized queries instead of string interpolation',
97
+ ));
98
+ }
99
+ }
100
+
101
+ super.visitStringInterpolation(node);
102
+ }
103
+
104
+ /// Check if the node is inside a logging/debug context
105
+ bool _isInLoggingContext(AstNode node) {
106
+ AstNode? current = node.parent;
107
+ int depth = 0;
108
+
109
+ while (current != null && depth < 5) {
110
+ if (current is MethodInvocation) {
111
+ final methodName = current.methodName.name.toLowerCase();
112
+ // Logging method patterns
113
+ if (methodName.contains('log') ||
114
+ methodName == 'print' ||
115
+ methodName == 'debugprint' ||
116
+ methodName == 'info' ||
117
+ methodName == 'debug' ||
118
+ methodName == 'warn' ||
119
+ methodName == 'error' ||
120
+ methodName == 'trace') {
121
+ return true;
122
+ }
123
+ // Check if target is a logging utility
124
+ final target = current.target?.toSource().toLowerCase() ?? '';
125
+ if (target.contains('log') || target.contains('logger')) {
126
+ return true;
127
+ }
128
+ }
129
+ current = current.parent;
130
+ depth++;
131
+ }
132
+ return false;
133
+ }
134
+
135
+ @override
136
+ void visitMethodInvocation(MethodInvocation node) {
137
+ final methodName = node.methodName.name.toLowerCase();
138
+
139
+ // Check for raw query methods
140
+ bool isRawQueryMethod = S017UseParameterizedQueriesAnalyzer._rawQueryMethods
141
+ .any((m) => methodName.contains(m));
142
+
143
+ if (isRawQueryMethod && node.argumentList.arguments.isNotEmpty) {
144
+ final firstArg = node.argumentList.arguments.first;
145
+
146
+ if (firstArg is StringInterpolation) {
147
+ violations.add(analyzer.createViolation(
148
+ filePath: filePath,
149
+ line: analyzer.getLine(lineInfo, node.offset),
150
+ column: analyzer.getColumn(lineInfo, node.offset),
151
+ message: 'SQL injection risk in raw query - use parameterized version',
152
+ ));
153
+ } else if (firstArg is BinaryExpression &&
154
+ firstArg.operator.type.lexeme == '+') {
155
+ violations.add(analyzer.createViolation(
156
+ filePath: filePath,
157
+ line: analyzer.getLine(lineInfo, node.offset),
158
+ column: analyzer.getColumn(lineInfo, node.offset),
159
+ message: 'SQL injection risk - avoid string concatenation in raw queries',
160
+ ));
161
+ }
162
+ }
163
+
164
+ super.visitMethodInvocation(node);
165
+ }
166
+
167
+ @override
168
+ void visitBinaryExpression(BinaryExpression node) {
169
+ // Only check string concatenation in database-related contexts
170
+ if (node.operator.type.lexeme == '+') {
171
+ final combined = node.toSource();
172
+
173
+ // Only flag if it clearly looks like SQL
174
+ if (S017UseParameterizedQueriesAnalyzer.looksLikeSql(combined)) {
175
+ bool concatenatingVariable = node.rightOperand is SimpleIdentifier ||
176
+ node.rightOperand is PrefixedIdentifier;
177
+
178
+ if (concatenatingVariable) {
179
+ violations.add(analyzer.createViolation(
180
+ filePath: filePath,
181
+ line: analyzer.getLine(lineInfo, node.offset),
182
+ column: analyzer.getColumn(lineInfo, node.offset),
183
+ message: 'SQL injection risk - use parameterized queries instead of string concatenation',
184
+ ));
185
+ }
186
+ }
187
+ }
188
+
189
+ super.visitBinaryExpression(node);
190
+ }
191
+ }
@@ -0,0 +1,175 @@
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
+ /// S018: Do not store sensitive data in browser storage
10
+ /// Detect sensitive data being stored in localStorage, sessionStorage, IndexedDB
11
+ class S018NoSensitiveBrowserStorageAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S018';
14
+
15
+ // Browser storage patterns
16
+ static const _browserStoragePatterns = [
17
+ 'localstorage',
18
+ 'sessionstorage',
19
+ 'indexeddb',
20
+ 'sharedpreferences', // Flutter equivalent
21
+ 'securestorage', // Should use this instead
22
+ 'setstring',
23
+ 'setitem',
24
+ 'put(',
25
+ ];
26
+
27
+ // Sensitive data patterns that should NOT be stored
28
+ static const _sensitiveDataPatterns = [
29
+ 'password',
30
+ 'passwd',
31
+ 'secret',
32
+ 'apikey',
33
+ 'api_key',
34
+ 'accesstoken',
35
+ 'access_token',
36
+ 'refreshtoken',
37
+ 'refresh_token',
38
+ 'privatekey',
39
+ 'private_key',
40
+ 'creditcard',
41
+ 'credit_card',
42
+ 'cardnumber',
43
+ 'card_number',
44
+ 'cvv',
45
+ 'ssn',
46
+ 'socialsecurity',
47
+ 'bankaccount',
48
+ 'bank_account',
49
+ 'encryptionkey',
50
+ 'encryption_key',
51
+ ];
52
+
53
+ // Allowed patterns (session token is acceptable with proper handling)
54
+ static const _allowedPatterns = [
55
+ 'encrypted',
56
+ 'hashed',
57
+ 'securestorage',
58
+ 'flutter_secure_storage',
59
+ ];
60
+
61
+ @override
62
+ List<Violation> analyze({
63
+ required CompilationUnit unit,
64
+ required String filePath,
65
+ required Rule rule,
66
+ required LineInfo lineInfo,
67
+ }) {
68
+ final violations = <Violation>[];
69
+ final visitor = _S018Visitor(
70
+ filePath: filePath,
71
+ lineInfo: lineInfo,
72
+ violations: violations,
73
+ analyzer: this,
74
+ );
75
+ unit.accept(visitor);
76
+ return violations;
77
+ }
78
+ }
79
+
80
+ class _S018Visitor extends RecursiveAstVisitor<void> {
81
+ final String filePath;
82
+ final LineInfo lineInfo;
83
+ final List<Violation> violations;
84
+ final S018NoSensitiveBrowserStorageAnalyzer analyzer;
85
+
86
+ _S018Visitor({
87
+ required this.filePath,
88
+ required this.lineInfo,
89
+ required this.violations,
90
+ required this.analyzer,
91
+ });
92
+
93
+ @override
94
+ void visitMethodInvocation(MethodInvocation node) {
95
+ final source = node.toSource().toLowerCase();
96
+ final methodName = node.methodName.name.toLowerCase();
97
+
98
+ // Check if this is a storage operation
99
+ bool isStorageOp =
100
+ S018NoSensitiveBrowserStorageAnalyzer._browserStoragePatterns
101
+ .any((p) => source.contains(p) || methodName.contains(p));
102
+
103
+ if (isStorageOp) {
104
+ // Check if storing sensitive data
105
+ bool hasSensitiveData =
106
+ S018NoSensitiveBrowserStorageAnalyzer._sensitiveDataPatterns
107
+ .any((p) => source.contains(p));
108
+
109
+ // Check if using secure storage or encryption
110
+ bool isSecureUsage =
111
+ S018NoSensitiveBrowserStorageAnalyzer._allowedPatterns
112
+ .any((p) => source.contains(p));
113
+
114
+ if (hasSensitiveData && !isSecureUsage) {
115
+ violations.add(analyzer.createViolation(
116
+ filePath: filePath,
117
+ line: analyzer.getLine(lineInfo, node.offset),
118
+ column: analyzer.getColumn(lineInfo, node.offset),
119
+ message:
120
+ 'Sensitive data should not be stored in browser storage - use flutter_secure_storage or server-side sessions',
121
+ ));
122
+ }
123
+ }
124
+
125
+ super.visitMethodInvocation(node);
126
+ }
127
+
128
+ @override
129
+ void visitMapLiteralEntry(MapLiteralEntry node) {
130
+ final key = node.key.toSource().toLowerCase();
131
+ final value = node.value.toSource().toLowerCase();
132
+ final fullSource = node.toSource().toLowerCase();
133
+
134
+ // Check if key indicates sensitive data
135
+ bool hasSensitiveKey =
136
+ S018NoSensitiveBrowserStorageAnalyzer._sensitiveDataPatterns
137
+ .any((p) => key.contains(p));
138
+
139
+ if (hasSensitiveKey) {
140
+ // Check context - is this being stored in browser storage?
141
+ bool isStorageContext = _isInStorageContext(node);
142
+
143
+ // Check if encrypted
144
+ bool isSecure = S018NoSensitiveBrowserStorageAnalyzer._allowedPatterns
145
+ .any((p) => fullSource.contains(p));
146
+
147
+ if (isStorageContext && !isSecure) {
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
+ 'Sensitive data in storage context - encrypt before storing or use secure storage',
154
+ ));
155
+ }
156
+ }
157
+
158
+ super.visitMapLiteralEntry(node);
159
+ }
160
+
161
+ bool _isInStorageContext(AstNode node) {
162
+ AstNode? current = node.parent;
163
+ int depth = 0;
164
+ while (current != null && depth < 10) {
165
+ final source = current.toSource().toLowerCase();
166
+ if (S018NoSensitiveBrowserStorageAnalyzer._browserStoragePatterns
167
+ .any((p) => source.contains(p))) {
168
+ return true;
169
+ }
170
+ current = current.parent;
171
+ depth++;
172
+ }
173
+ return false;
174
+ }
175
+ }
@@ -0,0 +1,166 @@
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
+ /// S019: SMTP Injection Protection
10
+ /// Prevent SMTP header injection attacks in email functionality
11
+ class S019SmtpInjectionProtectionAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S019';
14
+
15
+ // Email header fields that can be injected
16
+ // Note: 'x-' removed because it causes false positives with format strings like "index 1-5"
17
+ // X-headers are less critical anyway and often legitimately contain dynamic content
18
+ static const _headerFields = [
19
+ 'to:', 'from:', 'cc:', 'bcc:', 'subject:', 'reply-to:',
20
+ 'content-type:', 'mime-version:',
21
+ ];
22
+
23
+ // Dangerous characters for SMTP injection
24
+ static const _dangerousChars = ['\r', '\n', '\r\n', '%0d', '%0a'];
25
+
26
+ @override
27
+ List<Violation> analyze({
28
+ required CompilationUnit unit,
29
+ required String filePath,
30
+ required Rule rule,
31
+ required LineInfo lineInfo,
32
+ }) {
33
+ final violations = <Violation>[];
34
+ final visitor = _S019Visitor(
35
+ filePath: filePath,
36
+ lineInfo: lineInfo,
37
+ violations: violations,
38
+ analyzer: this,
39
+ );
40
+ unit.accept(visitor);
41
+ return violations;
42
+ }
43
+ }
44
+
45
+ class _S019Visitor extends RecursiveAstVisitor<void> {
46
+ final String filePath;
47
+ final LineInfo lineInfo;
48
+ final List<Violation> violations;
49
+ final S019SmtpInjectionProtectionAnalyzer analyzer;
50
+
51
+ _S019Visitor({
52
+ required this.filePath,
53
+ required this.lineInfo,
54
+ required this.violations,
55
+ required this.analyzer,
56
+ });
57
+
58
+ @override
59
+ void visitMethodInvocation(MethodInvocation node) {
60
+ final methodName = node.methodName.name.toLowerCase();
61
+ final source = node.toSource().toLowerCase();
62
+
63
+ // Check for email sending methods
64
+ if (methodName.contains('send') &&
65
+ (source.contains('mail') || source.contains('email') || source.contains('smtp'))) {
66
+ // Check if user input is used in email headers without sanitization
67
+ for (final arg in node.argumentList.arguments) {
68
+ if (arg is NamedExpression) {
69
+ final paramName = arg.name.label.name.toLowerCase();
70
+
71
+ // Check if it's a header field
72
+ bool isHeaderField = ['to', 'from', 'cc', 'bcc', 'subject', 'replyto', 'reply_to']
73
+ .any((h) => paramName.contains(h));
74
+
75
+ if (isHeaderField) {
76
+ final value = arg.expression;
77
+ // Check if value comes from user input (simple heuristic)
78
+ if (value is SimpleIdentifier || value is StringInterpolation) {
79
+ violations.add(analyzer.createViolation(
80
+ filePath: filePath,
81
+ line: analyzer.getLine(lineInfo, node.offset),
82
+ column: analyzer.getColumn(lineInfo, node.offset),
83
+ message: 'SMTP injection risk - sanitize email headers to remove CRLF characters',
84
+ ));
85
+ break;
86
+ }
87
+ }
88
+ }
89
+ }
90
+ }
91
+
92
+ super.visitMethodInvocation(node);
93
+ }
94
+
95
+ @override
96
+ void visitStringInterpolation(StringInterpolation node) {
97
+ // Skip debug/logging statements - these are not email contexts
98
+ final parent = node.parent;
99
+ if (parent is ArgumentList) {
100
+ final grandParent = parent.parent;
101
+ if (grandParent is MethodInvocation) {
102
+ final methodName = grandParent.methodName.name.toLowerCase();
103
+ if (methodName == 'debugprint' || methodName == 'print' ||
104
+ methodName == 'log' || methodName.startsWith('log')) {
105
+ super.visitStringInterpolation(node);
106
+ return;
107
+ }
108
+ }
109
+ }
110
+
111
+ final source = node.toSource().toLowerCase();
112
+
113
+ // Check if building email headers with user input
114
+ // Must be in email-related context, not just any string with header-like patterns
115
+ bool isEmailHeader = S019SmtpInjectionProtectionAnalyzer._headerFields
116
+ .any((h) => source.contains(h));
117
+
118
+ // Additional check: should also have email-related context
119
+ bool isEmailContext = source.contains('email') || source.contains('mail') ||
120
+ source.contains('smtp') || source.contains('message');
121
+
122
+ if (isEmailHeader && isEmailContext) {
123
+ // Check for interpolation (user input)
124
+ bool hasInterpolation = node.elements.any((e) => e is InterpolationExpression);
125
+
126
+ if (hasInterpolation) {
127
+ violations.add(analyzer.createViolation(
128
+ filePath: filePath,
129
+ line: analyzer.getLine(lineInfo, node.offset),
130
+ column: analyzer.getColumn(lineInfo, node.offset),
131
+ message: 'SMTP header injection risk - sanitize user input in email headers',
132
+ ));
133
+ }
134
+ }
135
+
136
+ super.visitStringInterpolation(node);
137
+ }
138
+
139
+ @override
140
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
141
+ final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
142
+
143
+ // Check for Message/Email class construction
144
+ if (typeName.contains('message') || typeName.contains('email') || typeName.contains('mail')) {
145
+ for (final arg in node.argumentList.arguments) {
146
+ if (arg is NamedExpression) {
147
+ final paramName = arg.name.label.name.toLowerCase();
148
+ final isHeaderField = ['to', 'from', 'cc', 'bcc', 'subject', 'replyto']
149
+ .any((h) => paramName == h);
150
+
151
+ if (isHeaderField && arg.expression is StringInterpolation) {
152
+ violations.add(analyzer.createViolation(
153
+ filePath: filePath,
154
+ line: analyzer.getLine(lineInfo, node.offset),
155
+ column: analyzer.getColumn(lineInfo, node.offset),
156
+ message: 'SMTP injection risk in email construction - sanitize header values',
157
+ ));
158
+ break;
159
+ }
160
+ }
161
+ }
162
+ }
163
+
164
+ super.visitInstanceCreationExpression(node);
165
+ }
166
+ }
@@ -0,0 +1,149 @@
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
+ /// S020: No Eval or Dynamic Code Execution
10
+ /// Prevent code injection through eval() and similar dynamic code execution
11
+ class S020NoEvalDynamicCodeAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S020';
14
+
15
+ // Dangerous dynamic code execution methods - use exact match to avoid false positives
16
+ static const _dangerousMethods = [
17
+ 'eval', 'exec', 'execute', 'runcode', 'run_code',
18
+ 'dynamiccode', 'dynamic_code', 'compile',
19
+ ];
20
+
21
+ // Check if method name exactly matches or ends with a dangerous pattern
22
+ static bool _isDangerousMethod(String methodName) {
23
+ final lowerName = methodName.toLowerCase().replaceAll('_', '');
24
+ // Exact match
25
+ for (final m in _dangerousMethods) {
26
+ final pattern = m.replaceAll('_', '');
27
+ if (lowerName == pattern) return true;
28
+ }
29
+ return false;
30
+ }
31
+
32
+ // Dart-specific dynamic execution patterns
33
+ static const _dartDangerousPatterns = [
34
+ 'mirror', 'reflect', 'invoke', 'dynamicinvoke',
35
+ 'noSuchMethod', // Can be abused for dynamic invocation
36
+ ];
37
+
38
+ @override
39
+ List<Violation> analyze({
40
+ required CompilationUnit unit,
41
+ required String filePath,
42
+ required Rule rule,
43
+ required LineInfo lineInfo,
44
+ }) {
45
+ final violations = <Violation>[];
46
+ final visitor = _S020Visitor(
47
+ filePath: filePath,
48
+ lineInfo: lineInfo,
49
+ violations: violations,
50
+ analyzer: this,
51
+ );
52
+ unit.accept(visitor);
53
+ return violations;
54
+ }
55
+ }
56
+
57
+ class _S020Visitor extends RecursiveAstVisitor<void> {
58
+ final String filePath;
59
+ final LineInfo lineInfo;
60
+ final List<Violation> violations;
61
+ final S020NoEvalDynamicCodeAnalyzer analyzer;
62
+
63
+ _S020Visitor({
64
+ required this.filePath,
65
+ required this.lineInfo,
66
+ required this.violations,
67
+ required this.analyzer,
68
+ });
69
+
70
+ @override
71
+ void visitMethodInvocation(MethodInvocation node) {
72
+ final methodName = node.methodName.name;
73
+
74
+ // Check for dangerous method names - use exact match to avoid false positives
75
+ bool isDangerous = S020NoEvalDynamicCodeAnalyzer._isDangerousMethod(methodName);
76
+
77
+ if (isDangerous) {
78
+ // Check if argument is user input (string interpolation or variable)
79
+ for (final arg in node.argumentList.arguments) {
80
+ if (arg is StringInterpolation || arg is SimpleIdentifier) {
81
+ violations.add(analyzer.createViolation(
82
+ filePath: filePath,
83
+ line: analyzer.getLine(lineInfo, node.offset),
84
+ column: analyzer.getColumn(lineInfo, node.offset),
85
+ message: 'Dynamic code execution is dangerous - avoid eval/exec with user input',
86
+ ));
87
+ break;
88
+ }
89
+ }
90
+ }
91
+
92
+ // Check for reflection with user input
93
+ final lowerMethodName = methodName.toLowerCase();
94
+ if (lowerMethodName == 'invoke' || lowerMethodName == 'invokemember') {
95
+ final source = node.toSource().toLowerCase();
96
+ if (source.contains('mirror') || source.contains('reflect')) {
97
+ for (final arg in node.argumentList.arguments) {
98
+ if (arg is StringInterpolation || arg is SimpleIdentifier) {
99
+ violations.add(analyzer.createViolation(
100
+ filePath: filePath,
101
+ line: analyzer.getLine(lineInfo, node.offset),
102
+ column: analyzer.getColumn(lineInfo, node.offset),
103
+ message: 'Reflection with dynamic input is dangerous - validate input strictly',
104
+ ));
105
+ break;
106
+ }
107
+ }
108
+ }
109
+ }
110
+
111
+ super.visitMethodInvocation(node);
112
+ }
113
+
114
+ @override
115
+ void visitImportDirective(ImportDirective node) {
116
+ final uri = node.uri.stringValue?.toLowerCase() ?? '';
117
+
118
+ // Check for mirrors import
119
+ if (uri.contains('mirrors')) {
120
+ violations.add(analyzer.createViolation(
121
+ filePath: filePath,
122
+ line: analyzer.getLine(lineInfo, node.offset),
123
+ column: analyzer.getColumn(lineInfo, node.offset),
124
+ message: 'dart:mirrors enables reflection - use with caution, can enable code injection',
125
+ ));
126
+ }
127
+
128
+ super.visitImportDirective(node);
129
+ }
130
+
131
+ @override
132
+ void visitFunctionExpression(FunctionExpression node) {
133
+ // Check for Function.apply with dynamic arguments
134
+ final parent = node.parent;
135
+ if (parent is MethodInvocation) {
136
+ final methodName = parent.methodName.name.toLowerCase();
137
+ if (methodName == 'apply') {
138
+ violations.add(analyzer.createViolation(
139
+ filePath: filePath,
140
+ line: analyzer.getLine(lineInfo, node.offset),
141
+ column: analyzer.getColumn(lineInfo, node.offset),
142
+ message: 'Function.apply with dynamic arguments can be dangerous',
143
+ ));
144
+ }
145
+ }
146
+
147
+ super.visitFunctionExpression(node);
148
+ }
149
+ }