@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,194 @@
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
+ /// D024: Avoid Unnecessary StatefulWidget
10
+ /// Detects StatefulWidget classes that don't use any state and could be StatelessWidget
11
+ ///
12
+ /// A StatefulWidget is considered unnecessary if its State class:
13
+ /// - Has no mutable fields (all fields are final/const)
14
+ /// - Never calls setState()
15
+ /// - Uses no lifecycle methods (initState, dispose, didUpdateWidget, etc.)
16
+ /// - Uses no mixins (AutomaticKeepAliveClientMixin, TickerProviderStateMixin, etc.)
17
+ class D024AvoidUnnecessaryStatefulWidgetAnalyzer extends BaseAnalyzer {
18
+ @override
19
+ String get ruleId => 'D024';
20
+
21
+ @override
22
+ List<Violation> analyze({
23
+ required CompilationUnit unit,
24
+ required String filePath,
25
+ required Rule rule,
26
+ required LineInfo lineInfo,
27
+ }) {
28
+ final violations = <Violation>[];
29
+
30
+ final visitor = _D024Visitor(
31
+ filePath: filePath,
32
+ lineInfo: lineInfo,
33
+ violations: violations,
34
+ analyzer: this,
35
+ );
36
+
37
+ unit.accept(visitor);
38
+
39
+ return violations;
40
+ }
41
+ }
42
+
43
+ class _D024Visitor extends RecursiveAstVisitor<void> {
44
+ final String filePath;
45
+ final LineInfo lineInfo;
46
+ final List<Violation> violations;
47
+ final D024AvoidUnnecessaryStatefulWidgetAnalyzer analyzer;
48
+
49
+ // Track StatefulWidget classes and their State classes
50
+ final Map<String, ClassDeclaration> _statefulWidgets = {};
51
+ final Map<String, _StateClassInfo> _stateClasses = {};
52
+
53
+ _D024Visitor({
54
+ required this.filePath,
55
+ required this.lineInfo,
56
+ required this.violations,
57
+ required this.analyzer,
58
+ });
59
+
60
+ @override
61
+ void visitCompilationUnit(CompilationUnit node) {
62
+ // First pass: collect all StatefulWidget and State classes
63
+ for (final declaration in node.declarations) {
64
+ if (declaration is ClassDeclaration) {
65
+ _analyzeClassDeclaration(declaration);
66
+ }
67
+ }
68
+
69
+ // Second pass: check for unnecessary StatefulWidgets
70
+ _checkUnnecessaryStatefulWidgets();
71
+
72
+ super.visitCompilationUnit(node);
73
+ }
74
+
75
+ void _analyzeClassDeclaration(ClassDeclaration node) {
76
+ final extendsClause = node.extendsClause;
77
+ if (extendsClause == null) return;
78
+
79
+ final superclass = extendsClause.superclass.name2.lexeme;
80
+ final className = node.name.lexeme;
81
+
82
+ if (superclass == 'StatefulWidget') {
83
+ _statefulWidgets[className] = node;
84
+ } else if (superclass == 'State') {
85
+ // Analyze the State class
86
+ final stateInfo = _StateClassInfo(
87
+ className: className,
88
+ node: node,
89
+ );
90
+
91
+ // Check if State class uses mixins (e.g., AutomaticKeepAliveClientMixin, TickerProviderStateMixin)
92
+ // Mixins often provide state management capabilities and require StatefulWidget
93
+ if (node.withClause != null && node.withClause!.mixinTypes.isNotEmpty) {
94
+ stateInfo.hasMixins = true;
95
+ }
96
+
97
+ // Check for mutable fields
98
+ for (final member in node.members) {
99
+ if (member is FieldDeclaration) {
100
+ // Check if field is final or const (applies to all variables in this declaration)
101
+ if (!member.fields.isFinal && !member.fields.isConst) {
102
+ stateInfo.hasMutableFields = true;
103
+ break;
104
+ }
105
+ }
106
+ }
107
+
108
+ // Check for setState calls
109
+ final visitor = _SetStateDetector();
110
+ node.accept(visitor);
111
+ stateInfo.hasSetStateCalls = visitor.hasSetState;
112
+
113
+ // Check for lifecycle methods that typically use state
114
+ for (final member in node.members) {
115
+ if (member is MethodDeclaration) {
116
+ final methodName = member.name.lexeme;
117
+ if (_isStateLifecycleMethod(methodName) &&
118
+ methodName != 'build' &&
119
+ methodName != 'createState') {
120
+ stateInfo.hasLifecycleMethods = true;
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ _stateClasses[className] = stateInfo;
127
+ }
128
+ }
129
+
130
+ bool _isStateLifecycleMethod(String methodName) {
131
+ return const [
132
+ 'initState',
133
+ 'dispose',
134
+ 'didUpdateWidget',
135
+ 'didChangeDependencies',
136
+ 'deactivate',
137
+ 'reassemble',
138
+ ].contains(methodName);
139
+ }
140
+
141
+ void _checkUnnecessaryStatefulWidgets() {
142
+ for (final entry in _statefulWidgets.entries) {
143
+ final widgetName = entry.key;
144
+ final widgetNode = entry.value;
145
+
146
+ // Find corresponding State class (typically _WidgetNameState)
147
+ final expectedStateName = '_${widgetName}State';
148
+ final stateInfo = _stateClasses[expectedStateName];
149
+
150
+ if (stateInfo != null) {
151
+ // Check if the State class actually uses state
152
+ // State usage includes: mutable fields, setState calls, lifecycle methods, or mixins
153
+ if (!stateInfo.hasMutableFields &&
154
+ !stateInfo.hasSetStateCalls &&
155
+ !stateInfo.hasLifecycleMethods &&
156
+ !stateInfo.hasMixins) {
157
+ violations.add(analyzer.createViolation(
158
+ filePath: filePath,
159
+ line: analyzer.getLine(lineInfo, widgetNode.name.offset),
160
+ column: analyzer.getColumn(lineInfo, widgetNode.name.offset),
161
+ message:
162
+ 'StatefulWidget "$widgetName" does not use any state. Consider converting to StatelessWidget for better performance.',
163
+ ));
164
+ }
165
+ }
166
+ }
167
+ }
168
+ }
169
+
170
+ class _StateClassInfo {
171
+ final String className;
172
+ final ClassDeclaration node;
173
+ bool hasMutableFields = false;
174
+ bool hasSetStateCalls = false;
175
+ bool hasLifecycleMethods = false;
176
+ bool hasMixins = false;
177
+
178
+ _StateClassInfo({
179
+ required this.className,
180
+ required this.node,
181
+ });
182
+ }
183
+
184
+ class _SetStateDetector extends RecursiveAstVisitor<void> {
185
+ bool hasSetState = false;
186
+
187
+ @override
188
+ void visitMethodInvocation(MethodInvocation node) {
189
+ if (node.methodName.name == 'setState') {
190
+ hasSetState = true;
191
+ }
192
+ super.visitMethodInvocation(node);
193
+ }
194
+ }
@@ -0,0 +1,90 @@
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
+ /// D025: Avoid Nested Conditional Expressions
10
+ /// Detects nested conditional expressions (ternary operators) which reduce readability
11
+ class D025AvoidNestedConditionalExpressionsAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'D025';
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
+
24
+ final visitor = _D025Visitor(
25
+ filePath: filePath,
26
+ lineInfo: lineInfo,
27
+ violations: violations,
28
+ analyzer: this,
29
+ );
30
+
31
+ unit.accept(visitor);
32
+
33
+ return violations;
34
+ }
35
+ }
36
+
37
+ class _D025Visitor extends RecursiveAstVisitor<void> {
38
+ final String filePath;
39
+ final LineInfo lineInfo;
40
+ final List<Violation> violations;
41
+ final D025AvoidNestedConditionalExpressionsAnalyzer analyzer;
42
+
43
+ _D025Visitor({
44
+ required this.filePath,
45
+ required this.lineInfo,
46
+ required this.violations,
47
+ required this.analyzer,
48
+ });
49
+
50
+ @override
51
+ void visitConditionalExpression(ConditionalExpression node) {
52
+ // Check if the then or else expression contains another conditional expression
53
+ final hasNestedInThen = _containsConditionalExpression(node.thenExpression);
54
+ final hasNestedInElse = _containsConditionalExpression(node.elseExpression);
55
+
56
+ if (hasNestedInThen || hasNestedInElse) {
57
+ violations.add(analyzer.createViolation(
58
+ filePath: filePath,
59
+ line: analyzer.getLine(lineInfo, node.question.offset),
60
+ column: analyzer.getColumn(lineInfo, node.question.offset),
61
+ message:
62
+ 'Avoid nested conditional expressions. Consider using if-else statements or extracting to a function for better readability.',
63
+ ));
64
+ }
65
+
66
+ super.visitConditionalExpression(node);
67
+ }
68
+
69
+ bool _containsConditionalExpression(Expression expression) {
70
+ // Direct check if the expression itself is a conditional expression
71
+ if (expression is ConditionalExpression) {
72
+ return true;
73
+ }
74
+
75
+ // Check if expression contains conditional expressions
76
+ final detector = _ConditionalExpressionDetector();
77
+ expression.accept(detector);
78
+ return detector.hasConditionalExpression;
79
+ }
80
+ }
81
+
82
+ class _ConditionalExpressionDetector extends RecursiveAstVisitor<void> {
83
+ bool hasConditionalExpression = false;
84
+
85
+ @override
86
+ void visitConditionalExpression(ConditionalExpression node) {
87
+ hasConditionalExpression = true;
88
+ // Don't call super to stop traversal once we find one
89
+ }
90
+ }
@@ -0,0 +1,155 @@
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
+ /// S001: Authenticate backend component communications securely
10
+ /// Detect static credentials, long-lived tokens, or shared accounts in service-to-service calls
11
+ class S001BackendAuthCommunicationsAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S001';
14
+
15
+ // Static/hardcoded credential patterns
16
+ static const _staticCredentialPatterns = [
17
+ 'apikey', 'api_key', 'api-key',
18
+ 'secretkey', 'secret_key', 'secret-key',
19
+ 'password', 'passwd',
20
+ 'authtoken', 'auth_token', 'auth-token',
21
+ 'accesskey', 'access_key', 'access-key',
22
+ ];
23
+
24
+ // Service-to-service communication patterns
25
+ static const _serviceCallPatterns = [
26
+ 'http.', 'dio.', 'client.',
27
+ '.get(', '.post(', '.put(', '.delete(',
28
+ 'grpc', 'rabbitmq', 'kafka', 'redis',
29
+ ];
30
+
31
+ // Secure patterns (short-lived tokens, mTLS)
32
+ static const _securePatterns = [
33
+ 'jwt', 'oauth', 'bearer',
34
+ 'mtls', 'certificate', 'x509',
35
+ 'shortlived', 'short_lived', 'expir',
36
+ 'refresh', 'rotate',
37
+ ];
38
+
39
+ @override
40
+ List<Violation> analyze({
41
+ required CompilationUnit unit,
42
+ required String filePath,
43
+ required Rule rule,
44
+ required LineInfo lineInfo,
45
+ }) {
46
+ final violations = <Violation>[];
47
+ final visitor = _S001Visitor(
48
+ filePath: filePath,
49
+ lineInfo: lineInfo,
50
+ violations: violations,
51
+ analyzer: this,
52
+ );
53
+ unit.accept(visitor);
54
+ return violations;
55
+ }
56
+ }
57
+
58
+ class _S001Visitor extends RecursiveAstVisitor<void> {
59
+ final String filePath;
60
+ final LineInfo lineInfo;
61
+ final List<Violation> violations;
62
+ final S001BackendAuthCommunicationsAnalyzer analyzer;
63
+
64
+ _S001Visitor({
65
+ required this.filePath,
66
+ required this.lineInfo,
67
+ required this.violations,
68
+ required this.analyzer,
69
+ });
70
+
71
+ @override
72
+ void visitVariableDeclaration(VariableDeclaration node) {
73
+ final varName = node.name.lexeme.toLowerCase();
74
+ final initializer = node.initializer;
75
+
76
+ if (initializer != null) {
77
+ final initSource = initializer.toSource();
78
+
79
+ // Check for hardcoded static credentials in service context
80
+ bool isCredentialVar = S001BackendAuthCommunicationsAnalyzer
81
+ ._staticCredentialPatterns
82
+ .any((p) => varName.contains(p));
83
+
84
+ // Check if it's a hardcoded string (not from env or config)
85
+ bool isHardcoded = initializer is StringLiteral &&
86
+ !initSource.toLowerCase().contains('env') &&
87
+ !initSource.toLowerCase().contains('config') &&
88
+ !initSource.toLowerCase().contains('secret');
89
+
90
+ if (isCredentialVar && isHardcoded) {
91
+ // Check if using secure patterns
92
+ bool isSecure = S001BackendAuthCommunicationsAnalyzer._securePatterns
93
+ .any((p) => initSource.toLowerCase().contains(p));
94
+
95
+ if (!isSecure) {
96
+ violations.add(analyzer.createViolation(
97
+ filePath: filePath,
98
+ line: analyzer.getLine(lineInfo, node.offset),
99
+ column: analyzer.getColumn(lineInfo, node.offset),
100
+ message:
101
+ 'Static credentials detected - use short-lived tokens or mTLS for backend communications',
102
+ ));
103
+ }
104
+ }
105
+ }
106
+
107
+ super.visitVariableDeclaration(node);
108
+ }
109
+
110
+ @override
111
+ void visitMethodInvocation(MethodInvocation node) {
112
+ final source = node.toSource().toLowerCase();
113
+
114
+ // Check for service-to-service calls with static credentials
115
+ bool isServiceCall = S001BackendAuthCommunicationsAnalyzer
116
+ ._serviceCallPatterns
117
+ .any((p) => source.contains(p));
118
+
119
+ if (isServiceCall) {
120
+ // Check for hardcoded credentials in headers
121
+ bool hasStaticCredential = S001BackendAuthCommunicationsAnalyzer
122
+ ._staticCredentialPatterns
123
+ .any((p) => source.contains(p));
124
+
125
+ // Check for secure patterns
126
+ bool isSecure = S001BackendAuthCommunicationsAnalyzer._securePatterns
127
+ .any((p) => source.contains(p));
128
+
129
+ // Check for hardcoded string literals in authorization headers
130
+ if (hasStaticCredential && !isSecure) {
131
+ for (final arg in node.argumentList.arguments) {
132
+ final argSource = arg.toSource().toLowerCase();
133
+ if (argSource.contains('authorization') ||
134
+ argSource.contains('x-api-key')) {
135
+ if (arg.toSource().contains('"') || arg.toSource().contains("'")) {
136
+ // Check if the value is hardcoded (contains literal string)
137
+ if (!argSource.contains('env') && !argSource.contains('config')) {
138
+ violations.add(analyzer.createViolation(
139
+ filePath: filePath,
140
+ line: analyzer.getLine(lineInfo, node.offset),
141
+ column: analyzer.getColumn(lineInfo, node.offset),
142
+ message:
143
+ 'Hardcoded credentials in service call - use environment variables or secrets manager',
144
+ ));
145
+ break;
146
+ }
147
+ }
148
+ }
149
+ }
150
+ }
151
+ }
152
+
153
+ super.visitMethodInvocation(node);
154
+ }
155
+ }
@@ -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
+ /// S002: Protect against OS command injection
10
+ /// Detect dangerous shell execution patterns with user input
11
+ class S002OsCommandInjectionAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S002';
14
+
15
+ // Dangerous shell execution methods
16
+ static const _shellExecutionMethods = [
17
+ 'Process.run',
18
+ 'Process.runSync',
19
+ 'Process.start',
20
+ 'shell',
21
+ 'exec',
22
+ 'system',
23
+ ];
24
+
25
+ // Dangerous patterns indicating shell mode
26
+ static const _shellModePatterns = [
27
+ 'runInShell: true',
28
+ 'shell: true',
29
+ 'runInShell:true',
30
+ ];
31
+
32
+ @override
33
+ List<Violation> analyze({
34
+ required CompilationUnit unit,
35
+ required String filePath,
36
+ required Rule rule,
37
+ required LineInfo lineInfo,
38
+ }) {
39
+ final violations = <Violation>[];
40
+ final visitor = _S002Visitor(
41
+ filePath: filePath,
42
+ lineInfo: lineInfo,
43
+ violations: violations,
44
+ analyzer: this,
45
+ );
46
+ unit.accept(visitor);
47
+ return violations;
48
+ }
49
+ }
50
+
51
+ class _S002Visitor extends RecursiveAstVisitor<void> {
52
+ final String filePath;
53
+ final LineInfo lineInfo;
54
+ final List<Violation> violations;
55
+ final S002OsCommandInjectionAnalyzer analyzer;
56
+
57
+ _S002Visitor({
58
+ required this.filePath,
59
+ required this.lineInfo,
60
+ required this.violations,
61
+ required this.analyzer,
62
+ });
63
+
64
+ @override
65
+ void visitMethodInvocation(MethodInvocation node) {
66
+ final source = node.toSource();
67
+ final sourceLower = source.toLowerCase();
68
+ final methodName = node.methodName.name;
69
+
70
+ // Check for Process.run, Process.runSync, Process.start
71
+ bool isProcessCall = methodName == 'run' ||
72
+ methodName == 'runSync' ||
73
+ methodName == 'start';
74
+
75
+ if (isProcessCall) {
76
+ final target = node.target?.toSource() ?? '';
77
+ if (target == 'Process' || target.contains('Process')) {
78
+ // Check for runInShell: true (dangerous)
79
+ bool hasShellMode = S002OsCommandInjectionAnalyzer._shellModePatterns
80
+ .any((p) => source.contains(p));
81
+
82
+ if (hasShellMode) {
83
+ // Check if command contains string interpolation or concatenation
84
+ bool hasUserInput = _containsUserInput(node);
85
+
86
+ if (hasUserInput) {
87
+ violations.add(analyzer.createViolation(
88
+ filePath: filePath,
89
+ line: analyzer.getLine(lineInfo, node.offset),
90
+ column: analyzer.getColumn(lineInfo, node.offset),
91
+ message:
92
+ 'OS command injection risk - avoid runInShell:true with user input. Use argument list instead',
93
+ ));
94
+ }
95
+ }
96
+
97
+ // Check for string concatenation in command
98
+ final firstArg = node.argumentList.arguments.isNotEmpty
99
+ ? node.argumentList.arguments.first
100
+ : null;
101
+
102
+ if (firstArg != null) {
103
+ if (_isStringConcatenation(firstArg) ||
104
+ _containsInterpolation(firstArg)) {
105
+ violations.add(analyzer.createViolation(
106
+ filePath: filePath,
107
+ line: analyzer.getLine(lineInfo, node.offset),
108
+ column: analyzer.getColumn(lineInfo, node.offset),
109
+ message:
110
+ 'OS command injection risk - do not concatenate user input into commands. Use parameterized arguments',
111
+ ));
112
+ }
113
+ }
114
+ }
115
+ }
116
+
117
+ // Check for shell-like execution patterns
118
+ if (sourceLower.contains('exec(') || sourceLower.contains('system(')) {
119
+ if (_containsUserInput(node)) {
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
+ 'Potential OS command injection - sanitize input before shell execution',
126
+ ));
127
+ }
128
+ }
129
+
130
+ super.visitMethodInvocation(node);
131
+ }
132
+
133
+ bool _containsUserInput(MethodInvocation node) {
134
+ final source = node.toSource().toLowerCase();
135
+ // Common patterns indicating user input
136
+ return source.contains('request.') ||
137
+ source.contains('params.') ||
138
+ source.contains('query.') ||
139
+ source.contains('body.') ||
140
+ source.contains('input') ||
141
+ source.contains('userdata') ||
142
+ source.contains('user_input');
143
+ }
144
+
145
+ bool _isStringConcatenation(Expression expr) {
146
+ if (expr is BinaryExpression) {
147
+ return expr.operator.lexeme == '+';
148
+ }
149
+ return false;
150
+ }
151
+
152
+ bool _containsInterpolation(Expression expr) {
153
+ if (expr is StringInterpolation) {
154
+ return true;
155
+ }
156
+ final source = expr.toSource();
157
+ return source.contains(r'$');
158
+ }
159
+ }