@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,211 @@
|
|
|
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
|
+
/// S008: Validate and sanitize SVG content
|
|
10
|
+
/// Detect unsafe SVG handling that could allow script injection
|
|
11
|
+
/// NOTE: Skip local assets (SvgPicture.asset) as they are bundled with the app
|
|
12
|
+
class S008SvgContentSanitizationAnalyzer extends BaseAnalyzer {
|
|
13
|
+
@override
|
|
14
|
+
String get ruleId => 'S008';
|
|
15
|
+
|
|
16
|
+
// Dangerous SVG elements that should be sanitized
|
|
17
|
+
static const _dangerousSvgElements = [
|
|
18
|
+
'<script',
|
|
19
|
+
'foreignobject',
|
|
20
|
+
'onclick',
|
|
21
|
+
'onload',
|
|
22
|
+
'onerror',
|
|
23
|
+
'onmouseover',
|
|
24
|
+
'xlink:href',
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// SVG handling patterns
|
|
28
|
+
static const _svgHandlingPatterns = [
|
|
29
|
+
'svg',
|
|
30
|
+
'svgpicture',
|
|
31
|
+
'svgimage',
|
|
32
|
+
'flutter_svg',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Safe sanitization patterns
|
|
36
|
+
static const _sanitizationPatterns = [
|
|
37
|
+
'sanitize',
|
|
38
|
+
'dompurify',
|
|
39
|
+
'allowedtags',
|
|
40
|
+
'allowedelements',
|
|
41
|
+
'removeattributes',
|
|
42
|
+
'stripscripts',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Safe asset patterns - local bundled resources, not user input
|
|
46
|
+
static const _safeAssetPatterns = [
|
|
47
|
+
'.asset(',
|
|
48
|
+
'assets.', // Generated assets: Assets.svgs.icon.svg()
|
|
49
|
+
'assets/',
|
|
50
|
+
'images.', // Generated images
|
|
51
|
+
'images/',
|
|
52
|
+
'icons.', // Generated icons
|
|
53
|
+
'icons/',
|
|
54
|
+
'svgs.', // Generated SVGs: Assets.svgs.something.svg()
|
|
55
|
+
'.svg(', // SvgGenImage extension method: icon.svg()
|
|
56
|
+
'res/',
|
|
57
|
+
'resources/',
|
|
58
|
+
'assetname',
|
|
59
|
+
'assetpath',
|
|
60
|
+
'package:',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
List<Violation> analyze({
|
|
65
|
+
required CompilationUnit unit,
|
|
66
|
+
required String filePath,
|
|
67
|
+
required Rule rule,
|
|
68
|
+
required LineInfo lineInfo,
|
|
69
|
+
}) {
|
|
70
|
+
final violations = <Violation>[];
|
|
71
|
+
final visitor = _S008Visitor(
|
|
72
|
+
filePath: filePath,
|
|
73
|
+
lineInfo: lineInfo,
|
|
74
|
+
violations: violations,
|
|
75
|
+
analyzer: this,
|
|
76
|
+
);
|
|
77
|
+
unit.accept(visitor);
|
|
78
|
+
return violations;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
class _S008Visitor extends RecursiveAstVisitor<void> {
|
|
83
|
+
final String filePath;
|
|
84
|
+
final LineInfo lineInfo;
|
|
85
|
+
final List<Violation> violations;
|
|
86
|
+
final S008SvgContentSanitizationAnalyzer analyzer;
|
|
87
|
+
|
|
88
|
+
_S008Visitor({
|
|
89
|
+
required this.filePath,
|
|
90
|
+
required this.lineInfo,
|
|
91
|
+
required this.violations,
|
|
92
|
+
required this.analyzer,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
@override
|
|
96
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
97
|
+
final source = node.toSource().toLowerCase();
|
|
98
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
99
|
+
|
|
100
|
+
// Check for SVG handling from user input
|
|
101
|
+
bool isSvgHandling = S008SvgContentSanitizationAnalyzer._svgHandlingPatterns
|
|
102
|
+
.any((p) => source.contains(p) || methodName.contains(p));
|
|
103
|
+
|
|
104
|
+
if (isSvgHandling) {
|
|
105
|
+
// Skip if it's a safe asset source (local bundled resources)
|
|
106
|
+
bool isSafeAsset = _isSafeAssetSource(source);
|
|
107
|
+
if (isSafeAsset) {
|
|
108
|
+
super.visitMethodInvocation(node);
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Check if SVG content comes from user/network
|
|
113
|
+
bool isUserInput = _isFromUserInput(node);
|
|
114
|
+
|
|
115
|
+
// Check if sanitization is applied
|
|
116
|
+
bool hasSanitization =
|
|
117
|
+
S008SvgContentSanitizationAnalyzer._sanitizationPatterns
|
|
118
|
+
.any((p) => source.contains(p));
|
|
119
|
+
|
|
120
|
+
if (isUserInput && !hasSanitization) {
|
|
121
|
+
violations.add(analyzer.createViolation(
|
|
122
|
+
filePath: filePath,
|
|
123
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
124
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
125
|
+
message:
|
|
126
|
+
'SVG content from untrusted source should be sanitized to prevent script injection',
|
|
127
|
+
));
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check for SVG string containing dangerous elements
|
|
132
|
+
for (final arg in node.argumentList.arguments) {
|
|
133
|
+
if (arg is StringLiteral) {
|
|
134
|
+
final content = arg.toSource().toLowerCase();
|
|
135
|
+
if (content.contains('svg')) {
|
|
136
|
+
bool hasDangerousElement =
|
|
137
|
+
S008SvgContentSanitizationAnalyzer._dangerousSvgElements
|
|
138
|
+
.any((e) => content.contains(e));
|
|
139
|
+
|
|
140
|
+
if (hasDangerousElement) {
|
|
141
|
+
violations.add(analyzer.createViolation(
|
|
142
|
+
filePath: filePath,
|
|
143
|
+
line: analyzer.getLine(lineInfo, arg.offset),
|
|
144
|
+
column: analyzer.getColumn(lineInfo, arg.offset),
|
|
145
|
+
message:
|
|
146
|
+
'SVG contains potentially dangerous elements (script, onclick, foreignObject) - sanitize before use',
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
super.visitMethodInvocation(node);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
158
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
159
|
+
final source = node.toSource().toLowerCase();
|
|
160
|
+
|
|
161
|
+
// Check for SvgPicture constructors
|
|
162
|
+
if (typeName.contains('svg')) {
|
|
163
|
+
// Skip safe asset sources (SvgPicture.asset, local paths)
|
|
164
|
+
bool isSafeAsset = _isSafeAssetSource(source);
|
|
165
|
+
if (isSafeAsset) {
|
|
166
|
+
super.visitInstanceCreationExpression(node);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
bool hasSanitization =
|
|
171
|
+
S008SvgContentSanitizationAnalyzer._sanitizationPatterns
|
|
172
|
+
.any((p) => source.contains(p));
|
|
173
|
+
|
|
174
|
+
// Only flag network/string constructors (untrusted sources)
|
|
175
|
+
if ((source.contains('.network') || source.contains('.string')) &&
|
|
176
|
+
!hasSanitization) {
|
|
177
|
+
violations.add(analyzer.createViolation(
|
|
178
|
+
filePath: filePath,
|
|
179
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
180
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
181
|
+
message:
|
|
182
|
+
'SVG from network/string should be sanitized before rendering',
|
|
183
|
+
));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
super.visitInstanceCreationExpression(node);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/// Check if source is from safe local assets
|
|
191
|
+
bool _isSafeAssetSource(String source) {
|
|
192
|
+
return S008SvgContentSanitizationAnalyzer._safeAssetPatterns
|
|
193
|
+
.any((p) => source.contains(p));
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
bool _isFromUserInput(MethodInvocation node) {
|
|
197
|
+
final source = node.toSource().toLowerCase();
|
|
198
|
+
|
|
199
|
+
// First check if it's a safe asset - if so, not user input
|
|
200
|
+
if (_isSafeAssetSource(source)) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return source.contains('request') ||
|
|
205
|
+
source.contains('upload') ||
|
|
206
|
+
source.contains('network') ||
|
|
207
|
+
source.contains('http') ||
|
|
208
|
+
source.contains('url') ||
|
|
209
|
+
source.contains('input');
|
|
210
|
+
}
|
|
211
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
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
|
+
/// S009: No Insecure Encryption Modes, Padding, or Cryptographic Algorithms
|
|
10
|
+
/// Do not use insecure encryption modes, padding, or cryptographic algorithms
|
|
11
|
+
class S009NoInsecureEncryptionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S009';
|
|
14
|
+
|
|
15
|
+
// Insecure algorithms and modes - use word boundaries to avoid false positives
|
|
16
|
+
static final _insecurePatterns = {
|
|
17
|
+
RegExp(r'\bdes\b', caseSensitive: false): 'DES is insecure - use AES-256',
|
|
18
|
+
RegExp(r'\b3des\b', caseSensitive: false): '3DES is deprecated - use AES-256',
|
|
19
|
+
RegExp(r'\btripledes\b', caseSensitive: false): 'Triple DES is deprecated - use AES-256',
|
|
20
|
+
RegExp(r'\brc4\b', caseSensitive: false): 'RC4 is broken - use AES-256-GCM',
|
|
21
|
+
RegExp(r'\brc2\b', caseSensitive: false): 'RC2 is insecure - use AES-256',
|
|
22
|
+
RegExp(r'\bblowfish\b', caseSensitive: false): 'Blowfish is outdated - use AES-256',
|
|
23
|
+
RegExp(r'\becb\b', caseSensitive: false): 'ECB mode is insecure - use GCM or CBC with IV',
|
|
24
|
+
RegExp(r'\bmd5\b', caseSensitive: false): 'MD5 is broken for security purposes - use SHA-256+',
|
|
25
|
+
RegExp(r'\bsha1\b', caseSensitive: false): 'SHA1 is weak - use SHA-256 or stronger',
|
|
26
|
+
RegExp(r'\bpkcs1\b', caseSensitive: false): 'PKCS#1 v1.5 is vulnerable - use OAEP padding',
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
@override
|
|
30
|
+
List<Violation> analyze({
|
|
31
|
+
required CompilationUnit unit,
|
|
32
|
+
required String filePath,
|
|
33
|
+
required Rule rule,
|
|
34
|
+
required LineInfo lineInfo,
|
|
35
|
+
}) {
|
|
36
|
+
final violations = <Violation>[];
|
|
37
|
+
final visitor = _S009Visitor(
|
|
38
|
+
filePath: filePath,
|
|
39
|
+
lineInfo: lineInfo,
|
|
40
|
+
violations: violations,
|
|
41
|
+
analyzer: this,
|
|
42
|
+
);
|
|
43
|
+
unit.accept(visitor);
|
|
44
|
+
return violations;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
class _S009Visitor extends RecursiveAstVisitor<void> {
|
|
49
|
+
final String filePath;
|
|
50
|
+
final LineInfo lineInfo;
|
|
51
|
+
final List<Violation> violations;
|
|
52
|
+
final S009NoInsecureEncryptionAnalyzer analyzer;
|
|
53
|
+
|
|
54
|
+
_S009Visitor({
|
|
55
|
+
required this.filePath,
|
|
56
|
+
required this.lineInfo,
|
|
57
|
+
required this.violations,
|
|
58
|
+
required this.analyzer,
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
@override
|
|
62
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
63
|
+
final source = node.toSource().toLowerCase();
|
|
64
|
+
|
|
65
|
+
_checkInsecurePatterns(source, node.offset);
|
|
66
|
+
super.visitMethodInvocation(node);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
@override
|
|
70
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
71
|
+
final source = node.toSource().toLowerCase();
|
|
72
|
+
|
|
73
|
+
_checkInsecurePatterns(source, node.offset);
|
|
74
|
+
super.visitInstanceCreationExpression(node);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
void visitSimpleStringLiteral(SimpleStringLiteral node) {
|
|
79
|
+
// Skip import/export directives - they are not crypto code
|
|
80
|
+
AstNode? parent = node.parent;
|
|
81
|
+
while (parent != null) {
|
|
82
|
+
if (parent is ImportDirective || parent is ExportDirective || parent is PartDirective) {
|
|
83
|
+
super.visitSimpleStringLiteral(node);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
parent = parent.parent;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
final value = node.value.toLowerCase();
|
|
90
|
+
|
|
91
|
+
_checkInsecurePatterns(value, node.offset);
|
|
92
|
+
super.visitSimpleStringLiteral(node);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
void _checkInsecurePatterns(String source, int offset) {
|
|
96
|
+
for (final entry in S009NoInsecureEncryptionAnalyzer._insecurePatterns.entries) {
|
|
97
|
+
if (entry.key.hasMatch(source)) {
|
|
98
|
+
// Avoid false positives for variable names containing the pattern
|
|
99
|
+
if (_isInCryptoContext(source)) {
|
|
100
|
+
violations.add(analyzer.createViolation(
|
|
101
|
+
filePath: filePath,
|
|
102
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
103
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
104
|
+
message: entry.value,
|
|
105
|
+
));
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
bool _isInCryptoContext(String source) {
|
|
112
|
+
// First check for false positive patterns (UI-related, non-crypto)
|
|
113
|
+
final falsePositivePatterns = [
|
|
114
|
+
'designsize', 'design_size', 'screensize', 'screen_size',
|
|
115
|
+
'fontsize', 'font_size', 'textsize', 'text_size',
|
|
116
|
+
'imagesize', 'image_size', 'iconsize', 'icon_size',
|
|
117
|
+
'mediaquery', 'media_query', 'screenutil', 'screen_util',
|
|
118
|
+
'description', 'descriptor', 'destiny', 'desktop', 'destroy',
|
|
119
|
+
'deserialize', 'deserialization',
|
|
120
|
+
];
|
|
121
|
+
if (falsePositivePatterns.any((p) => source.contains(p))) {
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Non-security use cases for MD5/SHA1 (acceptable uses)
|
|
126
|
+
// MD5 is fine for: identifiers, cache keys, checksums (non-security)
|
|
127
|
+
// Only flag when used for security purposes (passwords, tokens, signatures)
|
|
128
|
+
final nonSecurityContexts = [
|
|
129
|
+
// Identifier/checksum generation (not security-sensitive)
|
|
130
|
+
'cid', 'uid', 'id', 'identifier', 'checksum',
|
|
131
|
+
'cachekey', 'cache_key', 'etag', 'fingerprint',
|
|
132
|
+
// Deep link / URL parameters
|
|
133
|
+
'deeplink', 'deep_link', 'urlparam', 'url_param',
|
|
134
|
+
// File operations (integrity, not security)
|
|
135
|
+
'filename', 'file_name', 'filepath', 'file_path',
|
|
136
|
+
];
|
|
137
|
+
if (nonSecurityContexts.any((p) => source.contains(p))) {
|
|
138
|
+
return false;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Security-critical contexts (must flag)
|
|
142
|
+
final securityCriticalContexts = [
|
|
143
|
+
'password', 'passwd', 'pwd', 'secret', 'token',
|
|
144
|
+
'auth', 'credential', 'session', 'signature',
|
|
145
|
+
'privatekey', 'private_key', 'apikey', 'api_key',
|
|
146
|
+
];
|
|
147
|
+
if (securityCriticalContexts.any((p) => source.contains(p))) {
|
|
148
|
+
return true;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// General crypto context - only flag if clearly doing crypto operations
|
|
152
|
+
final cryptoKeywords = [
|
|
153
|
+
'cipher', 'encrypt', 'decrypt',
|
|
154
|
+
'hmac', 'sign', 'verify',
|
|
155
|
+
'aes', 'rsa', 'createcipher', 'createhash',
|
|
156
|
+
'pbkdf', 'bcrypt', 'scrypt', 'argon',
|
|
157
|
+
];
|
|
158
|
+
return cryptoKeywords.any((k) => source.contains(k));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
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
|
+
/// S010: Must use cryptographically secure random number generators (CSPRNG)
|
|
10
|
+
/// Detect usage of insecure random number generators for security purposes
|
|
11
|
+
class S010UseCsprngAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S010';
|
|
14
|
+
|
|
15
|
+
// Insecure random patterns
|
|
16
|
+
static const _insecureRandomPatterns = [
|
|
17
|
+
'Random()', // Dart's default Random is not cryptographically secure
|
|
18
|
+
'Random.nextInt',
|
|
19
|
+
'Random.nextDouble',
|
|
20
|
+
'Random.nextBool',
|
|
21
|
+
'DateTime.now().millisecondsSinceEpoch',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Security-sensitive contexts - ONLY actual security-related IDs
|
|
25
|
+
// Note: 'id' alone is too generic - causes false positives for alarm IDs, notification IDs, etc.
|
|
26
|
+
static const _securityContexts = [
|
|
27
|
+
'token', 'session', 'auth', 'key', 'secret', 'password',
|
|
28
|
+
'nonce', 'salt', 'iv', 'otp', 'code', 'uuid',
|
|
29
|
+
'apikey', 'api_key', 'accesstoken', 'access_token',
|
|
30
|
+
'sessionid', 'session_id', 'userid', 'user_id', // Only compound IDs
|
|
31
|
+
'securityid', 'security_id', 'authid', 'auth_id',
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
// Non-security contexts - these use IDs but are NOT security sensitive
|
|
35
|
+
static const _nonSecurityContexts = [
|
|
36
|
+
'alarm', 'notification', 'widget', 'component', 'element',
|
|
37
|
+
'timer', 'animation', 'screen', 'page', 'view', 'controller',
|
|
38
|
+
'index', 'counter', 'position', 'offset', 'size', 'duration',
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
@override
|
|
42
|
+
List<Violation> analyze({
|
|
43
|
+
required CompilationUnit unit,
|
|
44
|
+
required String filePath,
|
|
45
|
+
required Rule rule,
|
|
46
|
+
required LineInfo lineInfo,
|
|
47
|
+
}) {
|
|
48
|
+
final violations = <Violation>[];
|
|
49
|
+
final visitor = _S010Visitor(
|
|
50
|
+
filePath: filePath,
|
|
51
|
+
lineInfo: lineInfo,
|
|
52
|
+
violations: violations,
|
|
53
|
+
analyzer: this,
|
|
54
|
+
);
|
|
55
|
+
unit.accept(visitor);
|
|
56
|
+
return violations;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
class _S010Visitor extends RecursiveAstVisitor<void> {
|
|
61
|
+
final String filePath;
|
|
62
|
+
final LineInfo lineInfo;
|
|
63
|
+
final List<Violation> violations;
|
|
64
|
+
final S010UseCsprngAnalyzer analyzer;
|
|
65
|
+
|
|
66
|
+
_S010Visitor({
|
|
67
|
+
required this.filePath,
|
|
68
|
+
required this.lineInfo,
|
|
69
|
+
required this.violations,
|
|
70
|
+
required this.analyzer,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
75
|
+
final varName = node.name.lexeme.toLowerCase();
|
|
76
|
+
final initializer = node.initializer;
|
|
77
|
+
|
|
78
|
+
if (initializer != null) {
|
|
79
|
+
final initSource = initializer.toSource().toLowerCase();
|
|
80
|
+
|
|
81
|
+
// Check if this is a non-security context (alarm, notification, etc.)
|
|
82
|
+
// These use IDs but are NOT security sensitive
|
|
83
|
+
bool isNonSecurityContext = S010UseCsprngAnalyzer._nonSecurityContexts
|
|
84
|
+
.any((ctx) => varName.contains(ctx));
|
|
85
|
+
|
|
86
|
+
// Also check parent/enclosing context for non-security patterns
|
|
87
|
+
AstNode? current = node.parent;
|
|
88
|
+
int depth = 0;
|
|
89
|
+
while (current != null && depth < 5 && !isNonSecurityContext) {
|
|
90
|
+
final parentSource = current.toSource().toLowerCase();
|
|
91
|
+
isNonSecurityContext = S010UseCsprngAnalyzer._nonSecurityContexts
|
|
92
|
+
.any((ctx) => parentSource.contains(ctx));
|
|
93
|
+
current = current.parent;
|
|
94
|
+
depth++;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (isNonSecurityContext) {
|
|
98
|
+
super.visitVariableDeclaration(node);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check if variable name suggests security context
|
|
103
|
+
bool isSecurityContext = S010UseCsprngAnalyzer._securityContexts
|
|
104
|
+
.any((ctx) => varName.contains(ctx));
|
|
105
|
+
|
|
106
|
+
// Check if using insecure random
|
|
107
|
+
bool usesInsecureRandom = initSource.contains('random(') ||
|
|
108
|
+
initSource.contains('random.next') ||
|
|
109
|
+
initSource.contains('datetime.now');
|
|
110
|
+
|
|
111
|
+
// Check if using secure random
|
|
112
|
+
bool usesSecureRandom = initSource.contains('random.secure') ||
|
|
113
|
+
initSource.contains('securerandom');
|
|
114
|
+
|
|
115
|
+
if (isSecurityContext && usesInsecureRandom && !usesSecureRandom) {
|
|
116
|
+
violations.add(analyzer.createViolation(
|
|
117
|
+
filePath: filePath,
|
|
118
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
119
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
120
|
+
message: 'Use Random.secure() instead of Random() for security-sensitive data',
|
|
121
|
+
));
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
super.visitVariableDeclaration(node);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@override
|
|
129
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
130
|
+
final source = node.toSource().toLowerCase();
|
|
131
|
+
|
|
132
|
+
// Check for Random() usage in security contexts
|
|
133
|
+
if (source.contains('random(') && !source.contains('random.secure')) {
|
|
134
|
+
// Check surrounding context
|
|
135
|
+
final parent = node.parent;
|
|
136
|
+
if (parent != null) {
|
|
137
|
+
final parentSource = parent.toSource().toLowerCase();
|
|
138
|
+
bool isSecurityContext = S010UseCsprngAnalyzer._securityContexts
|
|
139
|
+
.any((ctx) => parentSource.contains(ctx));
|
|
140
|
+
|
|
141
|
+
if (isSecurityContext) {
|
|
142
|
+
violations.add(analyzer.createViolation(
|
|
143
|
+
filePath: filePath,
|
|
144
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
145
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
146
|
+
message: 'Use Random.secure() for cryptographically secure random numbers',
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
super.visitMethodInvocation(node);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@override
|
|
156
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
157
|
+
final typeName = node.constructorName.type.name2.lexeme;
|
|
158
|
+
|
|
159
|
+
if (typeName == 'Random') {
|
|
160
|
+
// Check if it's Random.secure()
|
|
161
|
+
final constructorName = node.constructorName.name?.name;
|
|
162
|
+
if (constructorName != 'secure') {
|
|
163
|
+
// Check context
|
|
164
|
+
final parent = node.parent;
|
|
165
|
+
if (parent != null) {
|
|
166
|
+
final parentSource = parent.toSource().toLowerCase();
|
|
167
|
+
bool isSecurityContext = S010UseCsprngAnalyzer._securityContexts
|
|
168
|
+
.any((ctx) => parentSource.contains(ctx));
|
|
169
|
+
|
|
170
|
+
if (isSecurityContext) {
|
|
171
|
+
violations.add(analyzer.createViolation(
|
|
172
|
+
filePath: filePath,
|
|
173
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
174
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
175
|
+
message: 'Use Random.secure() instead of Random() for security purposes',
|
|
176
|
+
));
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
super.visitInstanceCreationExpression(node);
|
|
183
|
+
}
|
|
184
|
+
}
|