@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.
- package/dart_analyzer/README.md +226 -0
- package/dart_analyzer/analysis_options.yaml +66 -0
- package/dart_analyzer/bin/sunlint-dart-macos +0 -0
- package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
- package/dart_analyzer/lib/analyzer_service.dart +625 -0
- package/dart_analyzer/lib/json_rpc_server.dart +275 -0
- package/dart_analyzer/lib/models/rule.dart +67 -0
- package/dart_analyzer/lib/models/symbol_table.dart +607 -0
- package/dart_analyzer/lib/models/violation.dart +69 -0
- package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
- package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
- package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
- package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
- package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
- package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
- package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
- package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
- package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
- package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
- package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
- package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
- package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
- package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
- package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
- package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
- package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
- package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
- package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
- package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
- package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
- package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
- package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
- package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
- package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
- package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
- package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
- package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
- package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
- package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
- package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
- package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
- package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
- package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
- package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
- package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
- package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
- package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
- package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
- package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
- package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
- package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
- package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
- package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
- package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
- package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
- package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
- package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
- package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
- package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
- package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
- package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
- package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
- package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
- package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
- package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
- package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
- package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
- package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
- package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
- package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
- package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
- package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
- package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
- package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
- package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
- package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
- package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
- package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
- package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
- package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
- package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
- package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
- package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
- package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
- package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
- package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
- package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
- package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
- package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
- package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
- package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
- package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
- package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
- package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
- package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
- package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
- package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
- package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
- package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
- package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
- package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
- package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
- package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
- package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
- package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
- package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
- package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
- package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
- package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
- package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
- package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
- package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
- package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
- package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
- package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
- package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
- package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
- package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
- package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
- package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
- package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
- package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
- package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
- package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
- package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
- package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
- package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
- package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
- package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
- package/dart_analyzer/lib/utils/common_utils.dart +26 -0
- package/dart_analyzer/pubspec.lock +557 -0
- package/dart_analyzer/pubspec.yaml +39 -0
- package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
- package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
- 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
|
+
}
|