@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,208 @@
|
|
|
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
|
+
/// S003: Open Redirect Protection
|
|
10
|
+
/// Prevents open redirect vulnerabilities by detecting:
|
|
11
|
+
/// - URL redirects using user input without validation
|
|
12
|
+
/// - External URL launching with user input
|
|
13
|
+
/// Note: Internal Flutter app navigation is NOT flagged (not a security issue)
|
|
14
|
+
class S003OpenRedirectProtectionAnalyzer extends BaseAnalyzer {
|
|
15
|
+
@override
|
|
16
|
+
String get ruleId => 'S003';
|
|
17
|
+
|
|
18
|
+
/// External redirect methods that could be vulnerable
|
|
19
|
+
/// Note: Internal Flutter navigation (push, go, navigate, replace) are NOT included
|
|
20
|
+
/// because they use typed routes, not external URLs
|
|
21
|
+
static final Set<String> _externalRedirectMethods = {
|
|
22
|
+
// Web/HTTP redirects (server-side)
|
|
23
|
+
'redirect',
|
|
24
|
+
'redirectTo',
|
|
25
|
+
'sendRedirect',
|
|
26
|
+
'temporaryRedirect',
|
|
27
|
+
'permanentRedirect',
|
|
28
|
+
'movedPermanently',
|
|
29
|
+
'movedTemporarily',
|
|
30
|
+
// URL launchers (opens external browser/apps)
|
|
31
|
+
'launchUrl',
|
|
32
|
+
'launch',
|
|
33
|
+
'launchUrlString',
|
|
34
|
+
'openUrl',
|
|
35
|
+
'canLaunchUrl',
|
|
36
|
+
// Web location changes (NOT Flutter router.replace which uses typed routes)
|
|
37
|
+
'assign',
|
|
38
|
+
// 'replace' - REMOVED: conflicts with Flutter router.replace()
|
|
39
|
+
// window.location.replace is detected by checking target is 'location'
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/// User input sources that could contain malicious URLs
|
|
43
|
+
static final Set<String> _externalUserInputPatterns = {
|
|
44
|
+
'request',
|
|
45
|
+
'queryParameters',
|
|
46
|
+
'queryParams',
|
|
47
|
+
'formData',
|
|
48
|
+
'body',
|
|
49
|
+
'returnUrl',
|
|
50
|
+
'returnTo',
|
|
51
|
+
'redirectUrl',
|
|
52
|
+
'nextUrl',
|
|
53
|
+
'callbackUrl',
|
|
54
|
+
'targetUrl',
|
|
55
|
+
'destination',
|
|
56
|
+
'goto',
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
/// Validation/allowlist methods
|
|
60
|
+
static final Set<String> _validationMethods = {
|
|
61
|
+
'isAllowed',
|
|
62
|
+
'isValid',
|
|
63
|
+
'isValidUrl',
|
|
64
|
+
'isSafeUrl',
|
|
65
|
+
'isTrusted',
|
|
66
|
+
'isWhitelisted',
|
|
67
|
+
'isAllowlisted',
|
|
68
|
+
'validateUrl',
|
|
69
|
+
'checkUrl',
|
|
70
|
+
'verifyUrl',
|
|
71
|
+
'sanitizeUrl',
|
|
72
|
+
'startsWith',
|
|
73
|
+
'contains',
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/// Files to skip (Flutter UI, generated code)
|
|
77
|
+
static const _skipFilePatterns = [
|
|
78
|
+
'_screen', 'screen.dart', '_page', 'page.dart',
|
|
79
|
+
'_widget', 'widget.dart', '_view', 'view.dart',
|
|
80
|
+
'.gr.dart', '.g.dart', 'router', 'route',
|
|
81
|
+
];
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
List<Violation> analyze({
|
|
85
|
+
required CompilationUnit unit,
|
|
86
|
+
required String filePath,
|
|
87
|
+
required Rule rule,
|
|
88
|
+
required LineInfo lineInfo,
|
|
89
|
+
}) {
|
|
90
|
+
// Skip Flutter UI files - internal navigation is not a security issue
|
|
91
|
+
final fileName = filePath.toLowerCase();
|
|
92
|
+
if (_skipFilePatterns.any((p) => fileName.contains(p))) {
|
|
93
|
+
return [];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
final violations = <Violation>[];
|
|
97
|
+
|
|
98
|
+
final visitor = _OpenRedirectVisitor(
|
|
99
|
+
filePath: filePath,
|
|
100
|
+
lineInfo: lineInfo,
|
|
101
|
+
violations: violations,
|
|
102
|
+
analyzer: this,
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
unit.accept(visitor);
|
|
106
|
+
|
|
107
|
+
return violations;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/// Check if method launches external URLs (not internal navigation)
|
|
111
|
+
static bool isExternalRedirectMethod(String name) {
|
|
112
|
+
return _externalRedirectMethods.contains(name) ||
|
|
113
|
+
name.toLowerCase() == 'launchurl' ||
|
|
114
|
+
name.toLowerCase() == 'openurl';
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/// Check if expression contains external user input
|
|
118
|
+
static bool containsExternalUserInput(String source) {
|
|
119
|
+
final lowerSource = source.toLowerCase();
|
|
120
|
+
return _externalUserInputPatterns.any((pattern) => lowerSource.contains(pattern));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Check if validation is present
|
|
124
|
+
static bool hasValidation(String source) {
|
|
125
|
+
final lowerSource = source.toLowerCase();
|
|
126
|
+
return _validationMethods.any((method) => lowerSource.contains(method));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
class _OpenRedirectVisitor extends RecursiveAstVisitor<void> {
|
|
131
|
+
final String filePath;
|
|
132
|
+
final LineInfo lineInfo;
|
|
133
|
+
final List<Violation> violations;
|
|
134
|
+
final S003OpenRedirectProtectionAnalyzer analyzer;
|
|
135
|
+
|
|
136
|
+
_OpenRedirectVisitor({
|
|
137
|
+
required this.filePath,
|
|
138
|
+
required this.lineInfo,
|
|
139
|
+
required this.violations,
|
|
140
|
+
required this.analyzer,
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
@override
|
|
144
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
145
|
+
final methodName = node.methodName.name;
|
|
146
|
+
|
|
147
|
+
// Only check external redirect methods (launchUrl, etc.)
|
|
148
|
+
if (S003OpenRedirectProtectionAnalyzer.isExternalRedirectMethod(methodName)) {
|
|
149
|
+
_checkExternalRedirect(node);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
super.visitMethodInvocation(node);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
void _checkExternalRedirect(MethodInvocation node) {
|
|
156
|
+
for (final arg in node.argumentList.arguments) {
|
|
157
|
+
final argSource = arg.toSource();
|
|
158
|
+
|
|
159
|
+
// Skip string literals - hardcoded URLs are safe
|
|
160
|
+
if (arg is SimpleStringLiteral || arg is AdjacentStrings) {
|
|
161
|
+
continue;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Skip const values
|
|
165
|
+
if (argSource.contains('Constants.') || argSource.contains('const ')) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Check if argument contains external user input
|
|
170
|
+
if (S003OpenRedirectProtectionAnalyzer.containsExternalUserInput(argSource)) {
|
|
171
|
+
// Check if validation is present
|
|
172
|
+
if (!S003OpenRedirectProtectionAnalyzer.hasValidation(argSource)) {
|
|
173
|
+
violations.add(analyzer.createViolation(
|
|
174
|
+
filePath: filePath,
|
|
175
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
176
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
177
|
+
message: 'Open redirect risk - validate URL before launching: ${_truncate(argSource, 40)}',
|
|
178
|
+
severity: 'error',
|
|
179
|
+
));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// Check for dynamic URL with interpolation going to external launch
|
|
184
|
+
if (arg is StringInterpolation) {
|
|
185
|
+
// Check if it's launching an external URL with user input
|
|
186
|
+
final interpolatedVars = arg.elements
|
|
187
|
+
.whereType<InterpolationExpression>()
|
|
188
|
+
.map((e) => e.toSource())
|
|
189
|
+
.join(' ');
|
|
190
|
+
|
|
191
|
+
if (S003OpenRedirectProtectionAnalyzer.containsExternalUserInput(interpolatedVars)) {
|
|
192
|
+
violations.add(analyzer.createViolation(
|
|
193
|
+
filePath: filePath,
|
|
194
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
195
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
196
|
+
message: 'Dynamic URL construction with user input - validate against allowlist',
|
|
197
|
+
severity: 'warning',
|
|
198
|
+
));
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
String _truncate(String s, int maxLen) {
|
|
205
|
+
if (s.length <= maxLen) return s;
|
|
206
|
+
return '${s.substring(0, maxLen)}...';
|
|
207
|
+
}
|
|
208
|
+
}
|
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
/// S004: Sensitive Data Logging Protection
|
|
10
|
+
/// Prevents logging of sensitive information:
|
|
11
|
+
/// - Passwords, tokens, API keys
|
|
12
|
+
/// - Credit card numbers, SSN
|
|
13
|
+
/// - Personal identifiable information (PII)
|
|
14
|
+
/// - Authentication credentials
|
|
15
|
+
class S004SensitiveDataLoggingAnalyzer extends BaseAnalyzer {
|
|
16
|
+
@override
|
|
17
|
+
String get ruleId => 'S004';
|
|
18
|
+
|
|
19
|
+
/// Patterns for sensitive variable/field names
|
|
20
|
+
/// NOTE: Use word boundaries (\b) to avoid matching substrings like "passwordLength"
|
|
21
|
+
static final List<RegExp> sensitivePatterns = [
|
|
22
|
+
// Credentials - match actual password variables, not validation labels
|
|
23
|
+
RegExp(r'^_?password$', caseSensitive: false), // _password, password
|
|
24
|
+
RegExp(r'^_?newPassword$', caseSensitive: false), // _newPassword
|
|
25
|
+
RegExp(r'^_?currentPassword$', caseSensitive: false),
|
|
26
|
+
RegExp(r'^_?oldPassword$', caseSensitive: false),
|
|
27
|
+
RegExp(r'passwd', caseSensitive: false),
|
|
28
|
+
RegExp(r'^pwd$', caseSensitive: false),
|
|
29
|
+
RegExp(r'^_?secret$', caseSensitive: false),
|
|
30
|
+
RegExp(r'^_?credential', caseSensitive: false),
|
|
31
|
+
// Tokens - actual token values
|
|
32
|
+
RegExp(r'^_?userToken$', caseSensitive: false),
|
|
33
|
+
RegExp(r'^_?accessToken$', caseSensitive: false),
|
|
34
|
+
RegExp(r'^_?refreshToken$', caseSensitive: false),
|
|
35
|
+
RegExp(r'^_?authToken$', caseSensitive: false),
|
|
36
|
+
RegExp(r'^_?idToken$', caseSensitive: false),
|
|
37
|
+
RegExp(r'^_?saveToken$', caseSensitive: false),
|
|
38
|
+
RegExp(r'api[_-]?key', caseSensitive: false),
|
|
39
|
+
RegExp(r'apikey', caseSensitive: false),
|
|
40
|
+
RegExp(r'auth[_-]?key', caseSensitive: false),
|
|
41
|
+
RegExp(r'^bearer', caseSensitive: false),
|
|
42
|
+
RegExp(r'^jwt$', caseSensitive: false),
|
|
43
|
+
// Keys
|
|
44
|
+
RegExp(r'private[_-]?key', caseSensitive: false),
|
|
45
|
+
RegExp(r'encryption[_-]?key', caseSensitive: false),
|
|
46
|
+
RegExp(r'ssh[_-]?key', caseSensitive: false),
|
|
47
|
+
RegExp(r'signing[_-]?key', caseSensitive: false),
|
|
48
|
+
// Financial
|
|
49
|
+
RegExp(r'credit[_-]?card', caseSensitive: false),
|
|
50
|
+
RegExp(r'card[_-]?number', caseSensitive: false),
|
|
51
|
+
RegExp(r'^cvv$', caseSensitive: false),
|
|
52
|
+
RegExp(r'^cvc$', caseSensitive: false),
|
|
53
|
+
RegExp(r'bank[_-]?account', caseSensitive: false),
|
|
54
|
+
// PII
|
|
55
|
+
RegExp(r'^ssn$', caseSensitive: false),
|
|
56
|
+
RegExp(r'social[_-]?security', caseSensitive: false),
|
|
57
|
+
RegExp(r'tax[_-]?id', caseSensitive: false),
|
|
58
|
+
RegExp(r'national[_-]?id', caseSensitive: false),
|
|
59
|
+
// Auth - actual OTP/PIN values
|
|
60
|
+
RegExp(r'^_?otp$', caseSensitive: false),
|
|
61
|
+
RegExp(r'^_?otpCode$', caseSensitive: false),
|
|
62
|
+
RegExp(r'pin[_-]?code', caseSensitive: false),
|
|
63
|
+
RegExp(r'^mfa$', caseSensitive: false),
|
|
64
|
+
RegExp(r'^2fa$', caseSensitive: false),
|
|
65
|
+
RegExp(r'^totp$', caseSensitive: false),
|
|
66
|
+
// Headers
|
|
67
|
+
RegExp(r'^authorization$', caseSensitive: false),
|
|
68
|
+
RegExp(r'x-api-key', caseSensitive: false),
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
/// Patterns that look like sensitive names but are actually safe
|
|
72
|
+
/// These are localization keys, validation labels, error messages, etc.
|
|
73
|
+
static final List<RegExp> safePatterns = [
|
|
74
|
+
// Localization/i18n keys (l10n.passwordXXX)
|
|
75
|
+
RegExp(r'l10n\.password', caseSensitive: false),
|
|
76
|
+
RegExp(r'l10n\.token', caseSensitive: false),
|
|
77
|
+
// Validation/requirement labels
|
|
78
|
+
RegExp(r'password.*Length', caseSensitive: false),
|
|
79
|
+
RegExp(r'password.*Requirement', caseSensitive: false),
|
|
80
|
+
RegExp(r'password.*Invalid', caseSensitive: false),
|
|
81
|
+
RegExp(r'password.*Error', caseSensitive: false),
|
|
82
|
+
RegExp(r'password.*Require', caseSensitive: false),
|
|
83
|
+
RegExp(r'password.*Width', caseSensitive: false),
|
|
84
|
+
RegExp(r'password.*Character', caseSensitive: false),
|
|
85
|
+
RegExp(r'password.*Message', caseSensitive: false),
|
|
86
|
+
RegExp(r'password.*Label', caseSensitive: false),
|
|
87
|
+
RegExp(r'password.*Hint', caseSensitive: false),
|
|
88
|
+
RegExp(r'password.*Placeholder', caseSensitive: false),
|
|
89
|
+
RegExp(r'password.*Title', caseSensitive: false),
|
|
90
|
+
RegExp(r'password.*Description', caseSensitive: false),
|
|
91
|
+
RegExp(r'token.*Expired', caseSensitive: false),
|
|
92
|
+
RegExp(r'token.*Error', caseSensitive: false),
|
|
93
|
+
// Route/navigation parameters
|
|
94
|
+
RegExp(r'LoginRoute', caseSensitive: false),
|
|
95
|
+
RegExp(r'RegisterRoute', caseSensitive: false),
|
|
96
|
+
// Enum values for analytics events
|
|
97
|
+
RegExp(r'UserEvent\.\w*password', caseSensitive: false),
|
|
98
|
+
RegExp(r'AnalyticsEvent\.\w*password', caseSensitive: false),
|
|
99
|
+
// Constants for validation
|
|
100
|
+
RegExp(r'min.*Password', caseSensitive: false),
|
|
101
|
+
RegExp(r'max.*Password', caseSensitive: false),
|
|
102
|
+
];
|
|
103
|
+
|
|
104
|
+
/// Logging functions to check
|
|
105
|
+
static final Set<String> loggingFunctions = {
|
|
106
|
+
// Dart core
|
|
107
|
+
'print',
|
|
108
|
+
'debugPrint',
|
|
109
|
+
// Logger packages
|
|
110
|
+
'log',
|
|
111
|
+
'logInfo',
|
|
112
|
+
'logDebug',
|
|
113
|
+
'logWarning',
|
|
114
|
+
'logError',
|
|
115
|
+
'info',
|
|
116
|
+
'debug',
|
|
117
|
+
'warning',
|
|
118
|
+
'error',
|
|
119
|
+
'severe',
|
|
120
|
+
'fine',
|
|
121
|
+
'finer',
|
|
122
|
+
'finest',
|
|
123
|
+
'trace',
|
|
124
|
+
'verbose',
|
|
125
|
+
// stdout/stderr
|
|
126
|
+
'write',
|
|
127
|
+
'writeln',
|
|
128
|
+
'writeAll',
|
|
129
|
+
// Console
|
|
130
|
+
'console.log',
|
|
131
|
+
'console.info',
|
|
132
|
+
'console.warn',
|
|
133
|
+
'console.error',
|
|
134
|
+
'console.debug',
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
/// Analytics/event tracking functions - these log event names, not sensitive data
|
|
138
|
+
static final Set<String> analyticsEventFunctions = {
|
|
139
|
+
'sendlogevent', 'sendeventlog', 'sendevent',
|
|
140
|
+
'logevent', 'trackevent', 'recordevent',
|
|
141
|
+
'firebaseevent', 'analyticsevent',
|
|
142
|
+
'sendlogcustomevent',
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
/// Event name patterns that are safe to log (not actual sensitive data)
|
|
146
|
+
static final List<RegExp> eventNamePatterns = [
|
|
147
|
+
RegExp(r'^tap', caseSensitive: false), // tapForgotPassword, tapBtnRegist
|
|
148
|
+
RegExp(r'^click', caseSensitive: false), // clickLogin
|
|
149
|
+
RegExp(r'^on[A-Z]', caseSensitive: false), // onLogin, onSubmit
|
|
150
|
+
RegExp(r'event$', caseSensitive: false), // loginEvent
|
|
151
|
+
RegExp(r'button$', caseSensitive: false), // forgotPasswordButton
|
|
152
|
+
RegExp(r'^screen', caseSensitive: false), // screenView
|
|
153
|
+
RegExp(r'^view', caseSensitive: false), // viewPage
|
|
154
|
+
RegExp(r'^open', caseSensitive: false), // openModal
|
|
155
|
+
RegExp(r'^close', caseSensitive: false), // closeDialog
|
|
156
|
+
RegExp(r'^show', caseSensitive: false), // showPopup
|
|
157
|
+
RegExp(r'^hide', caseSensitive: false), // hideModal
|
|
158
|
+
RegExp(r'action$', caseSensitive: false), // loginAction
|
|
159
|
+
];
|
|
160
|
+
|
|
161
|
+
/// Redaction/masking methods
|
|
162
|
+
static final Set<String> redactionMethods = {
|
|
163
|
+
'mask',
|
|
164
|
+
'redact',
|
|
165
|
+
'sanitize',
|
|
166
|
+
'obscure',
|
|
167
|
+
'hide',
|
|
168
|
+
'censor',
|
|
169
|
+
'omit',
|
|
170
|
+
'exclude',
|
|
171
|
+
'filter',
|
|
172
|
+
'replace',
|
|
173
|
+
'substring',
|
|
174
|
+
'replaceAll',
|
|
175
|
+
'replaceRange',
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
@override
|
|
179
|
+
List<Violation> analyze({
|
|
180
|
+
required CompilationUnit unit,
|
|
181
|
+
required String filePath,
|
|
182
|
+
required Rule rule,
|
|
183
|
+
required LineInfo lineInfo,
|
|
184
|
+
}) {
|
|
185
|
+
final violations = <Violation>[];
|
|
186
|
+
|
|
187
|
+
final visitor = _SensitiveLoggingVisitor(
|
|
188
|
+
filePath: filePath,
|
|
189
|
+
lineInfo: lineInfo,
|
|
190
|
+
violations: violations,
|
|
191
|
+
analyzer: this,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
unit.accept(visitor);
|
|
195
|
+
|
|
196
|
+
return violations;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/// Check if a name matches sensitive patterns
|
|
200
|
+
static bool isSensitiveName(String name) {
|
|
201
|
+
return sensitivePatterns.any((pattern) => pattern.hasMatch(name));
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/// Check if function is a logging function
|
|
205
|
+
static bool isLoggingFunction(String name) {
|
|
206
|
+
final lowerName = name.toLowerCase();
|
|
207
|
+
return loggingFunctions.any((fn) =>
|
|
208
|
+
lowerName == fn.toLowerCase() ||
|
|
209
|
+
lowerName.endsWith('.${fn.toLowerCase()}') ||
|
|
210
|
+
lowerName.contains(fn.toLowerCase()));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/// Check if expression has redaction
|
|
214
|
+
static bool hasRedaction(String source) {
|
|
215
|
+
final lowerSource = source.toLowerCase();
|
|
216
|
+
return redactionMethods.any((method) => lowerSource.contains(method));
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
class _SensitiveLoggingVisitor extends RecursiveAstVisitor<void> {
|
|
221
|
+
final String filePath;
|
|
222
|
+
final LineInfo lineInfo;
|
|
223
|
+
final List<Violation> violations;
|
|
224
|
+
final S004SensitiveDataLoggingAnalyzer analyzer;
|
|
225
|
+
|
|
226
|
+
_SensitiveLoggingVisitor({
|
|
227
|
+
required this.filePath,
|
|
228
|
+
required this.lineInfo,
|
|
229
|
+
required this.violations,
|
|
230
|
+
required this.analyzer,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
@override
|
|
234
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
235
|
+
final methodName = node.methodName.name;
|
|
236
|
+
final target = node.target?.toSource() ?? '';
|
|
237
|
+
final fullName = target.isEmpty ? methodName : '$target.$methodName';
|
|
238
|
+
final lowerMethodName = methodName.toLowerCase();
|
|
239
|
+
|
|
240
|
+
// Skip analytics/event tracking functions - these log event names, not data
|
|
241
|
+
if (S004SensitiveDataLoggingAnalyzer.analyticsEventFunctions
|
|
242
|
+
.any((fn) => lowerMethodName.contains(fn))) {
|
|
243
|
+
super.visitMethodInvocation(node);
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Check for logging sensitive data
|
|
248
|
+
if (S004SensitiveDataLoggingAnalyzer.isLoggingFunction(fullName) ||
|
|
249
|
+
S004SensitiveDataLoggingAnalyzer.isLoggingFunction(methodName)) {
|
|
250
|
+
_checkArgumentsForSensitiveData(node);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
super.visitMethodInvocation(node);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
@override
|
|
257
|
+
void visitFunctionExpressionInvocation(FunctionExpressionInvocation node) {
|
|
258
|
+
final functionName = node.function.toSource();
|
|
259
|
+
|
|
260
|
+
if (S004SensitiveDataLoggingAnalyzer.isLoggingFunction(functionName)) {
|
|
261
|
+
for (final arg in node.argumentList.arguments) {
|
|
262
|
+
_checkExpressionForSensitiveData(arg, node);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
super.visitFunctionExpressionInvocation(node);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
void _checkArgumentsForSensitiveData(MethodInvocation logCall) {
|
|
270
|
+
for (final arg in logCall.argumentList.arguments) {
|
|
271
|
+
_checkExpressionForSensitiveData(arg, logCall);
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
void _checkExpressionForSensitiveData(Expression expr, AstNode logCall) {
|
|
276
|
+
final exprSource = expr.toSource();
|
|
277
|
+
|
|
278
|
+
// Skip if redaction is present
|
|
279
|
+
if (S004SensitiveDataLoggingAnalyzer.hasRedaction(exprSource)) {
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Check string interpolation
|
|
284
|
+
if (expr is StringInterpolation) {
|
|
285
|
+
for (final element in expr.elements) {
|
|
286
|
+
if (element is InterpolationExpression) {
|
|
287
|
+
_checkExpressionForSensitiveData(element.expression, logCall);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Check identifiers
|
|
294
|
+
if (expr is SimpleIdentifier) {
|
|
295
|
+
_checkIdentifier(expr.name, expr.offset, logCall);
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
// Check property access
|
|
300
|
+
if (expr is PrefixedIdentifier) {
|
|
301
|
+
_checkIdentifier(expr.identifier.name, expr.offset, logCall);
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// Check property access chain
|
|
306
|
+
if (expr is PropertyAccess) {
|
|
307
|
+
_checkIdentifier(expr.propertyName.name, expr.offset, logCall);
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// Check binary expressions (concatenation)
|
|
312
|
+
if (expr is BinaryExpression) {
|
|
313
|
+
_checkExpressionForSensitiveData(expr.leftOperand, logCall);
|
|
314
|
+
_checkExpressionForSensitiveData(expr.rightOperand, logCall);
|
|
315
|
+
return;
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Check parenthesized expressions
|
|
319
|
+
if (expr is ParenthesizedExpression) {
|
|
320
|
+
_checkExpressionForSensitiveData(expr.expression, logCall);
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// Check conditional expressions
|
|
325
|
+
if (expr is ConditionalExpression) {
|
|
326
|
+
_checkExpressionForSensitiveData(expr.thenExpression, logCall);
|
|
327
|
+
_checkExpressionForSensitiveData(expr.elseExpression, logCall);
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Check map/list literals for sensitive keys
|
|
332
|
+
if (expr is SetOrMapLiteral) {
|
|
333
|
+
for (final element in expr.elements) {
|
|
334
|
+
if (element is MapLiteralEntry) {
|
|
335
|
+
final key = element.key;
|
|
336
|
+
if (key is SimpleStringLiteral) {
|
|
337
|
+
if (S004SensitiveDataLoggingAnalyzer.isSensitiveName(key.value)) {
|
|
338
|
+
violations.add(analyzer.createViolation(
|
|
339
|
+
filePath: filePath,
|
|
340
|
+
line: analyzer.getLine(lineInfo, element.offset),
|
|
341
|
+
column: analyzer.getColumn(lineInfo, element.offset),
|
|
342
|
+
message:
|
|
343
|
+
'Sensitive data "${key.value}" logged in map - mask or redact before logging',
|
|
344
|
+
severity: 'error',
|
|
345
|
+
metadata: {
|
|
346
|
+
'key': key.value,
|
|
347
|
+
'issue': 'sensitive_data_in_map',
|
|
348
|
+
},
|
|
349
|
+
));
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
return;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
void _checkIdentifier(String name, int offset, AstNode logCall) {
|
|
359
|
+
// Skip if this looks like an event name (analytics tracking)
|
|
360
|
+
bool isEventName = S004SensitiveDataLoggingAnalyzer.eventNamePatterns
|
|
361
|
+
.any((pattern) => pattern.hasMatch(name));
|
|
362
|
+
|
|
363
|
+
if (isEventName) {
|
|
364
|
+
return;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Skip safe patterns (localization keys, validation labels, etc.)
|
|
368
|
+
bool isSafePattern = S004SensitiveDataLoggingAnalyzer.safePatterns
|
|
369
|
+
.any((pattern) => pattern.hasMatch(name));
|
|
370
|
+
|
|
371
|
+
if (isSafePattern) {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (S004SensitiveDataLoggingAnalyzer.isSensitiveName(name)) {
|
|
376
|
+
violations.add(analyzer.createViolation(
|
|
377
|
+
filePath: filePath,
|
|
378
|
+
line: analyzer.getLine(lineInfo, offset),
|
|
379
|
+
column: analyzer.getColumn(lineInfo, offset),
|
|
380
|
+
message:
|
|
381
|
+
'Sensitive data "$name" logged without redaction - mask or exclude before logging',
|
|
382
|
+
severity: 'error',
|
|
383
|
+
metadata: {
|
|
384
|
+
'variableName': name,
|
|
385
|
+
'issue': 'sensitive_data_logging',
|
|
386
|
+
'recommendation': 'Use masking like: "${name.length > 4 ? name.substring(0, 4) : name}****" or omit entirely',
|
|
387
|
+
},
|
|
388
|
+
));
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|