@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,201 @@
|
|
|
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
|
+
/// S041: Session Token Invalidation
|
|
10
|
+
/// Ensure session tokens are properly invalidated on logout
|
|
11
|
+
class S041SessionTokenInvalidationAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S041';
|
|
14
|
+
|
|
15
|
+
// Logout method indicators
|
|
16
|
+
static const _logoutMethods = [
|
|
17
|
+
'logout', 'log_out', 'signout', 'sign_out', 'logoff', 'log_off',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// Session invalidation methods
|
|
21
|
+
static const _invalidateMethods = [
|
|
22
|
+
'destroy', 'invalidate', 'clear', 'delete', 'remove', 'revoke',
|
|
23
|
+
'terminate', 'expire', 'end',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// SDK logout patterns that handle token invalidation internally
|
|
27
|
+
static const _sdkLogoutPatterns = [
|
|
28
|
+
// Auth0
|
|
29
|
+
'auth0', 'webauthentication.logout', 'credentialsmanager.clearcredentials',
|
|
30
|
+
'clearcredentials',
|
|
31
|
+
// Firebase
|
|
32
|
+
'firebaseauth', 'firebase_auth', 'signout',
|
|
33
|
+
// Google Sign-In
|
|
34
|
+
'googlesignin', 'google_sign_in',
|
|
35
|
+
// Apple Sign-In
|
|
36
|
+
'signinwithapple', 'sign_in_with_apple',
|
|
37
|
+
// Generic OAuth/OIDC
|
|
38
|
+
'oauth', 'oidc', 'openidconnect',
|
|
39
|
+
// Flutter Secure Storage
|
|
40
|
+
'fluttersecurestorage', 'flutter_secure_storage', 'securestorage',
|
|
41
|
+
// SharedPreferences clear
|
|
42
|
+
'sharedpreferences',
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
// Patterns that indicate proper delegation to another logout method
|
|
46
|
+
static const _delegationPatterns = [
|
|
47
|
+
'await_logout', 'awaitlogout', // await _logout() or await logout()
|
|
48
|
+
'_logout(', 'logout(', // calling internal logout method
|
|
49
|
+
'_authservice', '_authmanager',
|
|
50
|
+
'authservice.logout', 'authmanager.logout', 'auth.logout',
|
|
51
|
+
'viewmodel.logout', '_auth.logout',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
@override
|
|
55
|
+
List<Violation> analyze({
|
|
56
|
+
required CompilationUnit unit,
|
|
57
|
+
required String filePath,
|
|
58
|
+
required Rule rule,
|
|
59
|
+
required LineInfo lineInfo,
|
|
60
|
+
}) {
|
|
61
|
+
final violations = <Violation>[];
|
|
62
|
+
final visitor = _S041Visitor(
|
|
63
|
+
filePath: filePath,
|
|
64
|
+
lineInfo: lineInfo,
|
|
65
|
+
violations: violations,
|
|
66
|
+
analyzer: this,
|
|
67
|
+
);
|
|
68
|
+
unit.accept(visitor);
|
|
69
|
+
return violations;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
class _S041Visitor extends RecursiveAstVisitor<void> {
|
|
74
|
+
final String filePath;
|
|
75
|
+
final LineInfo lineInfo;
|
|
76
|
+
final List<Violation> violations;
|
|
77
|
+
final S041SessionTokenInvalidationAnalyzer analyzer;
|
|
78
|
+
|
|
79
|
+
_S041Visitor({
|
|
80
|
+
required this.filePath,
|
|
81
|
+
required this.lineInfo,
|
|
82
|
+
required this.violations,
|
|
83
|
+
required this.analyzer,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
@override
|
|
87
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
88
|
+
// Skip abstract methods (interface declarations) - they have no body to check
|
|
89
|
+
if (node.isAbstract || node.body == null || node.body is EmptyFunctionBody) {
|
|
90
|
+
super.visitMethodDeclaration(node);
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
final methodName = node.name.lexeme.toLowerCase();
|
|
95
|
+
|
|
96
|
+
// Skip setter/getter methods like setClickLogout, isLogout, getLogoutState
|
|
97
|
+
// These are NOT actual logout implementations
|
|
98
|
+
if (methodName.startsWith('set') || methodName.startsWith('get') ||
|
|
99
|
+
methodName.startsWith('is') || methodName.startsWith('has') ||
|
|
100
|
+
methodName.startsWith('can') || methodName.startsWith('should') ||
|
|
101
|
+
methodName.contains('click') || methodName.contains('state') ||
|
|
102
|
+
methodName.contains('status') || methodName.contains('flag')) {
|
|
103
|
+
super.visitMethodDeclaration(node);
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
bool isLogoutMethod = S041SessionTokenInvalidationAnalyzer._logoutMethods
|
|
108
|
+
.any((m) => methodName.contains(m));
|
|
109
|
+
|
|
110
|
+
if (isLogoutMethod) {
|
|
111
|
+
final body = node.body;
|
|
112
|
+
if (body != null) {
|
|
113
|
+
final bodySource = body.toSource().toLowerCase().replaceAll('_', '');
|
|
114
|
+
|
|
115
|
+
// Check if using SDK that handles logout internally
|
|
116
|
+
bool usesAuthSdk = S041SessionTokenInvalidationAnalyzer._sdkLogoutPatterns
|
|
117
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '')));
|
|
118
|
+
|
|
119
|
+
if (usesAuthSdk) {
|
|
120
|
+
// SDK handles token invalidation, skip this method
|
|
121
|
+
super.visitMethodDeclaration(node);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Check if delegating to another logout method
|
|
126
|
+
bool delegatesToLogout = S041SessionTokenInvalidationAnalyzer._delegationPatterns
|
|
127
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '').replaceAll(' ', '')));
|
|
128
|
+
|
|
129
|
+
if (delegatesToLogout) {
|
|
130
|
+
// Delegating to another method, skip
|
|
131
|
+
super.visitMethodDeclaration(node);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
bool hasInvalidation = S041SessionTokenInvalidationAnalyzer._invalidateMethods
|
|
136
|
+
.any((m) => bodySource.contains(m));
|
|
137
|
+
|
|
138
|
+
bool hasSessionOp = bodySource.contains('session') || bodySource.contains('token') ||
|
|
139
|
+
bodySource.contains('credential');
|
|
140
|
+
|
|
141
|
+
if (!hasInvalidation || !hasSessionOp) {
|
|
142
|
+
violations.add(analyzer.createViolation(
|
|
143
|
+
filePath: filePath,
|
|
144
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
145
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
146
|
+
message: 'Logout should invalidate/destroy session token',
|
|
147
|
+
));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
super.visitMethodDeclaration(node);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
@override
|
|
156
|
+
void visitFunctionDeclaration(FunctionDeclaration node) {
|
|
157
|
+
final funcName = node.name.lexeme.toLowerCase();
|
|
158
|
+
|
|
159
|
+
bool isLogoutFunc = S041SessionTokenInvalidationAnalyzer._logoutMethods
|
|
160
|
+
.any((m) => funcName.contains(m));
|
|
161
|
+
|
|
162
|
+
if (isLogoutFunc) {
|
|
163
|
+
final body = node.functionExpression.body;
|
|
164
|
+
if (body != null) {
|
|
165
|
+
final bodySource = body.toSource().toLowerCase().replaceAll('_', '');
|
|
166
|
+
|
|
167
|
+
// Check if using SDK that handles logout internally
|
|
168
|
+
bool usesAuthSdk = S041SessionTokenInvalidationAnalyzer._sdkLogoutPatterns
|
|
169
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '')));
|
|
170
|
+
|
|
171
|
+
if (usesAuthSdk) {
|
|
172
|
+
super.visitFunctionDeclaration(node);
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Check if delegating to another logout method
|
|
177
|
+
bool delegatesToLogout = S041SessionTokenInvalidationAnalyzer._delegationPatterns
|
|
178
|
+
.any((p) => bodySource.contains(p.replaceAll('_', '').replaceAll(' ', '')));
|
|
179
|
+
|
|
180
|
+
if (delegatesToLogout) {
|
|
181
|
+
super.visitFunctionDeclaration(node);
|
|
182
|
+
return;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
bool hasInvalidation = S041SessionTokenInvalidationAnalyzer._invalidateMethods
|
|
186
|
+
.any((m) => bodySource.contains(m));
|
|
187
|
+
|
|
188
|
+
if (!hasInvalidation) {
|
|
189
|
+
violations.add(analyzer.createViolation(
|
|
190
|
+
filePath: filePath,
|
|
191
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
192
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
193
|
+
message: 'Logout function should invalidate session',
|
|
194
|
+
));
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
super.visitFunctionDeclaration(node);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
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
|
+
/// S042: Require Re-authentication for Long-Lived Sessions
|
|
10
|
+
/// Sensitive operations should require re-authentication for long-lived sessions
|
|
11
|
+
class S042RequireReAuthenticationForLongLivedAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S042';
|
|
14
|
+
|
|
15
|
+
// Sensitive operations that need re-auth (critical account/security changes only)
|
|
16
|
+
static const _sensitiveOperations = [
|
|
17
|
+
// Password changes
|
|
18
|
+
'changepassword', 'change_password', 'updatepassword', 'update_password',
|
|
19
|
+
'resetpassword', 'reset_password',
|
|
20
|
+
// Email address changes (not notification settings)
|
|
21
|
+
'changeemail', 'change_email', 'changeemailaddress', 'change_email_address',
|
|
22
|
+
// Account deletion
|
|
23
|
+
'deleteaccount', 'delete_account', 'removeaccount', 'remove_account',
|
|
24
|
+
// Payment/financial
|
|
25
|
+
'updatepayment', 'update_payment', 'changepayment', 'change_payment',
|
|
26
|
+
'addpayment', 'add_payment', 'deletepayment', 'delete_payment',
|
|
27
|
+
// Security settings
|
|
28
|
+
'changesecurity', 'change_security', 'enable2fa', 'disable2fa',
|
|
29
|
+
'enablemfa', 'disablemfa', 'setup2fa', 'remove2fa',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
// Patterns to exclude (not sensitive despite similar names)
|
|
33
|
+
static const _excludePatterns = [
|
|
34
|
+
'notification', 'notificationsetting', 'emailnotification',
|
|
35
|
+
'pushnotification', 'settings', 'preference', 'consent', 'terms',
|
|
36
|
+
// Forgot password flow - user is NOT logged in, can't require re-auth
|
|
37
|
+
'forgot', 'forgotpassword', 'forgot_password',
|
|
38
|
+
'canreset', 'can_reset', 'checkreset', 'check_reset',
|
|
39
|
+
// Validation/getter methods (not actual operations)
|
|
40
|
+
'validate', 'check', 'verify', 'isvalid', 'is_valid',
|
|
41
|
+
'cansend', 'can_send', 'isempty', 'is_empty',
|
|
42
|
+
// Getter methods like canChangePassword, canChangeEmail - these are NOT operations
|
|
43
|
+
'canchange', 'can_change',
|
|
44
|
+
// Setter/state methods - these just set UI state, not actual operations
|
|
45
|
+
'setclick', 'set_click', // setClickDeleteAccount, setClickChangePassword
|
|
46
|
+
'isclick', 'is_click', // isClickDeleteAccount
|
|
47
|
+
// Handler methods that run AFTER user confirmation (cleanup, not initiation)
|
|
48
|
+
'handlelogout', 'handle_logout', 'handledelete', 'handle_delete',
|
|
49
|
+
'handlelogoutordelete', 'handle_logout_or_delete',
|
|
50
|
+
'onlogout', 'on_logout', 'ondelete', 'on_delete',
|
|
51
|
+
'dologout', 'do_logout', 'dodelete', 'do_delete',
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
// Files/layers where re-auth is NOT expected (data layer, not UI)
|
|
55
|
+
// Re-auth should be handled in UI/presentation layer, not repository/datasource
|
|
56
|
+
static const _excludedFileSuffixes = [
|
|
57
|
+
'repository_impl.dart', 'repository.dart',
|
|
58
|
+
'data_source.dart', 'datasource.dart',
|
|
59
|
+
'remote_data_source.dart', 'local_data_source.dart',
|
|
60
|
+
'api_service.dart', 'api_client.dart',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
// Re-auth check indicators
|
|
64
|
+
static const _reAuthIndicators = [
|
|
65
|
+
'verifypassword', 'verify_password', 'confirmpassword', 'confirm_password',
|
|
66
|
+
'reauthenticate', 're_authenticate', 'requireauth', 'require_auth',
|
|
67
|
+
'checkpassword', 'check_password', 'validatepassword', 'validate_password',
|
|
68
|
+
'confirmdialog', 'confirm_dialog', 'showconfirm', // UI confirmation
|
|
69
|
+
// Firebase/OAuth re-authentication (user must provide password again)
|
|
70
|
+
'signinwithemailandpassword', 'signin_with_email_and_password',
|
|
71
|
+
'signinwithcredential', 'signin_with_credential',
|
|
72
|
+
'reauthenticatewithcredential', 'reauthenticate_with_credential',
|
|
73
|
+
// Password confirmation - user provides current password to verify identity
|
|
74
|
+
'currentpassword', 'current_password', 'oldpassword', 'old_password',
|
|
75
|
+
];
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
List<Violation> analyze({
|
|
79
|
+
required CompilationUnit unit,
|
|
80
|
+
required String filePath,
|
|
81
|
+
required Rule rule,
|
|
82
|
+
required LineInfo lineInfo,
|
|
83
|
+
}) {
|
|
84
|
+
// Skip data layer files - re-auth is UI/presentation layer concern
|
|
85
|
+
final fileNameLower = filePath.toLowerCase();
|
|
86
|
+
if (_excludedFileSuffixes.any((suffix) => fileNameLower.endsWith(suffix))) {
|
|
87
|
+
return [];
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
final violations = <Violation>[];
|
|
91
|
+
final visitor = _S042Visitor(
|
|
92
|
+
filePath: filePath,
|
|
93
|
+
lineInfo: lineInfo,
|
|
94
|
+
violations: violations,
|
|
95
|
+
analyzer: this,
|
|
96
|
+
);
|
|
97
|
+
unit.accept(visitor);
|
|
98
|
+
return violations;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
class _S042Visitor extends RecursiveAstVisitor<void> {
|
|
103
|
+
final String filePath;
|
|
104
|
+
final LineInfo lineInfo;
|
|
105
|
+
final List<Violation> violations;
|
|
106
|
+
final S042RequireReAuthenticationForLongLivedAnalyzer analyzer;
|
|
107
|
+
|
|
108
|
+
_S042Visitor({
|
|
109
|
+
required this.filePath,
|
|
110
|
+
required this.lineInfo,
|
|
111
|
+
required this.violations,
|
|
112
|
+
required this.analyzer,
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
@override
|
|
116
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
117
|
+
// Skip abstract methods (interface declarations) - they have no body to check
|
|
118
|
+
if (node.isAbstract || node.body == null || node.body is EmptyFunctionBody) {
|
|
119
|
+
super.visitMethodDeclaration(node);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
final methodName = node.name.lexeme.toLowerCase().replaceAll('_', '');
|
|
124
|
+
|
|
125
|
+
// Check if method name matches exclude patterns (not sensitive)
|
|
126
|
+
bool isExcluded = S042RequireReAuthenticationForLongLivedAnalyzer._excludePatterns
|
|
127
|
+
.any((p) => methodName.contains(p.replaceAll('_', '')));
|
|
128
|
+
|
|
129
|
+
if (isExcluded) {
|
|
130
|
+
super.visitMethodDeclaration(node);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
bool isSensitiveOp = S042RequireReAuthenticationForLongLivedAnalyzer._sensitiveOperations
|
|
135
|
+
.any((op) => methodName.contains(op.replaceAll('_', '')));
|
|
136
|
+
|
|
137
|
+
if (isSensitiveOp) {
|
|
138
|
+
final body = node.body;
|
|
139
|
+
if (body != null) {
|
|
140
|
+
final bodySource = body.toSource().toLowerCase().replaceAll('_', '');
|
|
141
|
+
|
|
142
|
+
bool hasReAuth = S042RequireReAuthenticationForLongLivedAnalyzer._reAuthIndicators
|
|
143
|
+
.any((r) => bodySource.contains(r.replaceAll('_', '')));
|
|
144
|
+
|
|
145
|
+
if (!hasReAuth) {
|
|
146
|
+
violations.add(analyzer.createViolation(
|
|
147
|
+
filePath: filePath,
|
|
148
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
149
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
150
|
+
message: 'Sensitive operation should require re-authentication',
|
|
151
|
+
));
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
super.visitMethodDeclaration(node);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
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
|
+
/// S043: Password Changes Should Invalidate All Sessions
|
|
10
|
+
/// When password is changed, all existing sessions should be invalidated
|
|
11
|
+
class S043PasswordChangesInvalidateAllSessionsAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S043';
|
|
14
|
+
|
|
15
|
+
// Password change method indicators
|
|
16
|
+
static const _passwordChangeMethods = [
|
|
17
|
+
'changepassword', 'change_password', 'updatepassword', 'update_password',
|
|
18
|
+
'resetpassword', 'reset_password', 'setpassword', 'set_password',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Session invalidation indicators
|
|
22
|
+
static const _invalidateAllIndicators = [
|
|
23
|
+
'invalidateall', 'invalidate_all', 'revokeall', 'revoke_all',
|
|
24
|
+
'destroyall', 'destroy_all', 'clearallsessions', 'clear_all_sessions',
|
|
25
|
+
'logoutall', 'logout_all', 'terminateall', 'terminate_all',
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
@override
|
|
29
|
+
List<Violation> analyze({
|
|
30
|
+
required CompilationUnit unit,
|
|
31
|
+
required String filePath,
|
|
32
|
+
required Rule rule,
|
|
33
|
+
required LineInfo lineInfo,
|
|
34
|
+
}) {
|
|
35
|
+
final violations = <Violation>[];
|
|
36
|
+
final visitor = _S043Visitor(
|
|
37
|
+
filePath: filePath,
|
|
38
|
+
lineInfo: lineInfo,
|
|
39
|
+
violations: violations,
|
|
40
|
+
analyzer: this,
|
|
41
|
+
);
|
|
42
|
+
unit.accept(visitor);
|
|
43
|
+
return violations;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
class _S043Visitor extends RecursiveAstVisitor<void> {
|
|
48
|
+
final String filePath;
|
|
49
|
+
final LineInfo lineInfo;
|
|
50
|
+
final List<Violation> violations;
|
|
51
|
+
final S043PasswordChangesInvalidateAllSessionsAnalyzer analyzer;
|
|
52
|
+
|
|
53
|
+
_S043Visitor({
|
|
54
|
+
required this.filePath,
|
|
55
|
+
required this.lineInfo,
|
|
56
|
+
required this.violations,
|
|
57
|
+
required this.analyzer,
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
@override
|
|
61
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
62
|
+
final methodName = node.name.lexeme.toLowerCase().replaceAll('_', '');
|
|
63
|
+
|
|
64
|
+
bool isPasswordChange = S043PasswordChangesInvalidateAllSessionsAnalyzer._passwordChangeMethods
|
|
65
|
+
.any((m) => methodName.contains(m.replaceAll('_', '')));
|
|
66
|
+
|
|
67
|
+
if (isPasswordChange) {
|
|
68
|
+
final body = node.body;
|
|
69
|
+
if (body != null) {
|
|
70
|
+
final bodySource = body.toSource().toLowerCase().replaceAll('_', '');
|
|
71
|
+
|
|
72
|
+
bool invalidatesAll = S043PasswordChangesInvalidateAllSessionsAnalyzer._invalidateAllIndicators
|
|
73
|
+
.any((i) => bodySource.contains(i.replaceAll('_', '')));
|
|
74
|
+
|
|
75
|
+
if (!invalidatesAll) {
|
|
76
|
+
violations.add(analyzer.createViolation(
|
|
77
|
+
filePath: filePath,
|
|
78
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
79
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
80
|
+
message: 'Password change should invalidate all existing sessions',
|
|
81
|
+
));
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
super.visitMethodDeclaration(node);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
|
+
/// S044: Re-Authentication Required for Sensitive Actions
|
|
10
|
+
/// Require re-authentication before performing sensitive actions
|
|
11
|
+
/// Note: This rule focuses on critical data/permission changes
|
|
12
|
+
/// S042 covers account-level operations like deleteAccount, changePassword
|
|
13
|
+
class S044ReAuthenticationRequiredAnalyzer extends BaseAnalyzer {
|
|
14
|
+
@override
|
|
15
|
+
String get ruleId => 'S044';
|
|
16
|
+
|
|
17
|
+
// Sensitive action patterns - focus on data/permission changes
|
|
18
|
+
// Note: deleteaccount, changepassword are handled by S042
|
|
19
|
+
static const _sensitiveActions = [
|
|
20
|
+
'revoke', 'grant', 'transfer', 'withdraw',
|
|
21
|
+
'admin', 'privilege', 'permission', 'role',
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
// Skip patterns already covered by S042
|
|
25
|
+
static const _s042Patterns = [
|
|
26
|
+
'deleteaccount', 'delete_account', 'removeaccount', 'remove_account',
|
|
27
|
+
'changepassword', 'change_password', 'updatepassword', 'update_password',
|
|
28
|
+
'changeemail', 'change_email',
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
@override
|
|
32
|
+
List<Violation> analyze({
|
|
33
|
+
required CompilationUnit unit,
|
|
34
|
+
required String filePath,
|
|
35
|
+
required Rule rule,
|
|
36
|
+
required LineInfo lineInfo,
|
|
37
|
+
}) {
|
|
38
|
+
final violations = <Violation>[];
|
|
39
|
+
final visitor = _S044Visitor(
|
|
40
|
+
filePath: filePath,
|
|
41
|
+
lineInfo: lineInfo,
|
|
42
|
+
violations: violations,
|
|
43
|
+
analyzer: this,
|
|
44
|
+
);
|
|
45
|
+
unit.accept(visitor);
|
|
46
|
+
return violations;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
class _S044Visitor extends RecursiveAstVisitor<void> {
|
|
51
|
+
final String filePath;
|
|
52
|
+
final LineInfo lineInfo;
|
|
53
|
+
final List<Violation> violations;
|
|
54
|
+
final S044ReAuthenticationRequiredAnalyzer analyzer;
|
|
55
|
+
|
|
56
|
+
_S044Visitor({
|
|
57
|
+
required this.filePath,
|
|
58
|
+
required this.lineInfo,
|
|
59
|
+
required this.violations,
|
|
60
|
+
required this.analyzer,
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
@override
|
|
64
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
65
|
+
// Skip abstract methods (interface declarations) - they have no body to check
|
|
66
|
+
if (node.isAbstract || node.body == null || node.body is EmptyFunctionBody) {
|
|
67
|
+
super.visitMethodDeclaration(node);
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
final methodName = node.name.lexeme.toLowerCase().replaceAll('_', '');
|
|
72
|
+
|
|
73
|
+
// Skip patterns already covered by S042 to avoid duplicate warnings
|
|
74
|
+
bool isCoveredByS042 = S044ReAuthenticationRequiredAnalyzer._s042Patterns
|
|
75
|
+
.any((p) => methodName.contains(p.replaceAll('_', '')));
|
|
76
|
+
|
|
77
|
+
if (isCoveredByS042) {
|
|
78
|
+
super.visitMethodDeclaration(node);
|
|
79
|
+
return;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Check if this is a sensitive action
|
|
83
|
+
bool isSensitive = S044ReAuthenticationRequiredAnalyzer._sensitiveActions
|
|
84
|
+
.any((a) => methodName.contains(a));
|
|
85
|
+
|
|
86
|
+
// Check if it's related to user/account/data
|
|
87
|
+
bool isUserRelated = methodName.contains('user') ||
|
|
88
|
+
methodName.contains('account') ||
|
|
89
|
+
methodName.contains('profile') ||
|
|
90
|
+
methodName.contains('member');
|
|
91
|
+
|
|
92
|
+
if (isSensitive && isUserRelated) {
|
|
93
|
+
final body = node.body;
|
|
94
|
+
if (body != null) {
|
|
95
|
+
final bodySource = body.toSource().toLowerCase();
|
|
96
|
+
|
|
97
|
+
bool hasReAuth = bodySource.contains('reauth') ||
|
|
98
|
+
bodySource.contains('re_auth') ||
|
|
99
|
+
bodySource.contains('verifypassword') ||
|
|
100
|
+
bodySource.contains('verify_password') ||
|
|
101
|
+
bodySource.contains('confirmidentity') ||
|
|
102
|
+
bodySource.contains('confirm_identity') ||
|
|
103
|
+
bodySource.contains('confirmdialog') ||
|
|
104
|
+
bodySource.contains('confirm_dialog');
|
|
105
|
+
|
|
106
|
+
if (!hasReAuth) {
|
|
107
|
+
violations.add(analyzer.createViolation(
|
|
108
|
+
filePath: filePath,
|
|
109
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
110
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
111
|
+
message: 'Sensitive user action should require re-authentication',
|
|
112
|
+
));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
super.visitMethodDeclaration(node);
|
|
118
|
+
}
|
|
119
|
+
}
|