@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,145 @@
|
|
|
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
|
+
/// S049: Short Validity Tokens
|
|
10
|
+
/// Security tokens should have short validity periods
|
|
11
|
+
class S049ShortValidityTokensAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S049';
|
|
14
|
+
|
|
15
|
+
// Token creation indicators - must be actual creation, not reading
|
|
16
|
+
static const _tokenCreationPatterns = [
|
|
17
|
+
'createtoken', 'create_token', 'generatetoken', 'generate_token',
|
|
18
|
+
'signtoken', 'sign_token', 'issuetoken', 'issue_token',
|
|
19
|
+
'newtoken', 'new_token', 'buildtoken', 'build_token',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Reading/getting/saving patterns - these are NOT token creation
|
|
23
|
+
// These operations don't create tokens, they just store, retrieve, or validate existing ones
|
|
24
|
+
static const _tokenReadPatterns = [
|
|
25
|
+
// Get/read operations
|
|
26
|
+
'gettoken', 'get_token', 'getaccesstoken', 'get_access_token',
|
|
27
|
+
'getrefreshtoken', 'get_refresh_token', 'readtoken', 'read_token',
|
|
28
|
+
'fetchtoken', 'fetch_token', 'loadtoken', 'load_token',
|
|
29
|
+
'retrievetoken', 'retrieve_token', 'parsetoken', 'parse_token',
|
|
30
|
+
'decodetoken', 'decode_token', 'verifytoken', 'verify_token',
|
|
31
|
+
'validatetoken', 'validate_token',
|
|
32
|
+
// Save/store operations (NOT token creation)
|
|
33
|
+
'savetoken', 'save_token', 'storetoken', 'store_token',
|
|
34
|
+
'settoken', 'set_token', 'setstring', 'set_string',
|
|
35
|
+
'cachetoken', 'cache_token', 'persisttoken', 'persist_token',
|
|
36
|
+
'usertoken', 'user_token', // Common variable names
|
|
37
|
+
'preferencekey', 'preference_key', // Shared preferences
|
|
38
|
+
'securestorage', 'secure_storage', // Secure storage
|
|
39
|
+
// Firebase token operations (not creating JWT, just getting Firebase's token)
|
|
40
|
+
'getidtoken', 'get_id_token', 'currentuser',
|
|
41
|
+
// Update/refresh patterns (not creating new tokens, just refreshing existing)
|
|
42
|
+
'updatetoken', 'update_token', 'updatefcmtoken', 'update_fcm_token',
|
|
43
|
+
'refreshtoken', 'refresh_token', 'renewtoken', 'renew_token',
|
|
44
|
+
// Save access token patterns
|
|
45
|
+
'saveaccesstoken', 'save_access_token',
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
// Long expiry values (in seconds) - 30 days = 2592000
|
|
49
|
+
static const int _maxRecommendedExpiry = 86400; // 24 hours
|
|
50
|
+
|
|
51
|
+
@override
|
|
52
|
+
List<Violation> analyze({
|
|
53
|
+
required CompilationUnit unit,
|
|
54
|
+
required String filePath,
|
|
55
|
+
required Rule rule,
|
|
56
|
+
required LineInfo lineInfo,
|
|
57
|
+
}) {
|
|
58
|
+
final violations = <Violation>[];
|
|
59
|
+
final visitor = _S049Visitor(
|
|
60
|
+
filePath: filePath,
|
|
61
|
+
lineInfo: lineInfo,
|
|
62
|
+
violations: violations,
|
|
63
|
+
analyzer: this,
|
|
64
|
+
);
|
|
65
|
+
unit.accept(visitor);
|
|
66
|
+
return violations;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
class _S049Visitor extends RecursiveAstVisitor<void> {
|
|
71
|
+
final String filePath;
|
|
72
|
+
final LineInfo lineInfo;
|
|
73
|
+
final List<Violation> violations;
|
|
74
|
+
final S049ShortValidityTokensAnalyzer analyzer;
|
|
75
|
+
|
|
76
|
+
_S049Visitor({
|
|
77
|
+
required this.filePath,
|
|
78
|
+
required this.lineInfo,
|
|
79
|
+
required this.violations,
|
|
80
|
+
required this.analyzer,
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
@override
|
|
84
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
85
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
86
|
+
final source = node.toSource().toLowerCase().replaceAll('_', '');
|
|
87
|
+
|
|
88
|
+
// Skip if this is reading/getting a token, not creating one
|
|
89
|
+
bool isTokenRead = S049ShortValidityTokensAnalyzer._tokenReadPatterns
|
|
90
|
+
.any((p) => source.contains(p.replaceAll('_', '')));
|
|
91
|
+
|
|
92
|
+
if (isTokenRead) {
|
|
93
|
+
super.visitMethodInvocation(node);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
bool isTokenCreation = S049ShortValidityTokensAnalyzer._tokenCreationPatterns
|
|
98
|
+
.any((p) => source.contains(p.replaceAll('_', '')));
|
|
99
|
+
|
|
100
|
+
if (isTokenCreation) {
|
|
101
|
+
// Check for expiry/ttl settings
|
|
102
|
+
bool hasExpiryCheck = source.contains('expir') ||
|
|
103
|
+
source.contains('ttl') ||
|
|
104
|
+
source.contains('validity') ||
|
|
105
|
+
source.contains('lifetime');
|
|
106
|
+
|
|
107
|
+
if (!hasExpiryCheck) {
|
|
108
|
+
violations.add(analyzer.createViolation(
|
|
109
|
+
filePath: filePath,
|
|
110
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
111
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
112
|
+
message: 'Token creation should have explicit expiry time',
|
|
113
|
+
));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
super.visitMethodInvocation(node);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@override
|
|
121
|
+
void visitNamedExpression(NamedExpression node) {
|
|
122
|
+
final paramName = node.name.label.name.toLowerCase();
|
|
123
|
+
|
|
124
|
+
// Check for expiry-related parameters
|
|
125
|
+
if (paramName.contains('expir') || paramName.contains('ttl') || paramName.contains('lifetime')) {
|
|
126
|
+
final expression = node.expression;
|
|
127
|
+
|
|
128
|
+
// Check if value is too large
|
|
129
|
+
if (expression is IntegerLiteral) {
|
|
130
|
+
final value = expression.value ?? 0;
|
|
131
|
+
// Assuming value is in seconds
|
|
132
|
+
if (value > S049ShortValidityTokensAnalyzer._maxRecommendedExpiry) {
|
|
133
|
+
violations.add(analyzer.createViolation(
|
|
134
|
+
filePath: filePath,
|
|
135
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
136
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
137
|
+
message: 'Token expiry time is too long - consider shorter validity periods',
|
|
138
|
+
));
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
super.visitNamedExpression(node);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
@@ -0,0 +1,234 @@
|
|
|
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
|
+
/// S050: Reference tokens must be unique with 128-bit entropy using CSPRNG
|
|
10
|
+
/// Detect weak token generation patterns
|
|
11
|
+
class S050ReferenceTokensEntropyAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S050';
|
|
14
|
+
|
|
15
|
+
@override
|
|
16
|
+
List<Violation> analyze({
|
|
17
|
+
required CompilationUnit unit,
|
|
18
|
+
required String filePath,
|
|
19
|
+
required Rule rule,
|
|
20
|
+
required LineInfo lineInfo,
|
|
21
|
+
}) {
|
|
22
|
+
final violations = <Violation>[];
|
|
23
|
+
final visitor = _S050Visitor(
|
|
24
|
+
filePath: filePath,
|
|
25
|
+
lineInfo: lineInfo,
|
|
26
|
+
violations: violations,
|
|
27
|
+
analyzer: this,
|
|
28
|
+
);
|
|
29
|
+
unit.accept(visitor);
|
|
30
|
+
return violations;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
class _S050Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S050ReferenceTokensEntropyAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
// Patterns indicating token/session/ID context
|
|
41
|
+
static const _tokenContextPatterns = [
|
|
42
|
+
'token',
|
|
43
|
+
'session',
|
|
44
|
+
'sessionid',
|
|
45
|
+
'accesstoken',
|
|
46
|
+
'refreshtoken',
|
|
47
|
+
'apikey',
|
|
48
|
+
'authcode',
|
|
49
|
+
];
|
|
50
|
+
|
|
51
|
+
// Safe patterns - DateTime used for timing/logging, not token generation
|
|
52
|
+
static const _safeTimestampPatterns = [
|
|
53
|
+
'starttime', // Request timing
|
|
54
|
+
'endtime', // Request timing
|
|
55
|
+
'duration', // Duration calculation
|
|
56
|
+
'elapsed', // Elapsed time
|
|
57
|
+
'timestamp', // Logging timestamp (if not in token context)
|
|
58
|
+
'logdate', // Logging date
|
|
59
|
+
'logtime', // Logging time
|
|
60
|
+
'createdat', // Record creation timestamp
|
|
61
|
+
'updatedat', // Record update timestamp
|
|
62
|
+
'lastseen', // Last seen tracking
|
|
63
|
+
'expiresat', // Expiration tracking
|
|
64
|
+
'formatter', // Date formatting
|
|
65
|
+
'dateformat', // Date formatting
|
|
66
|
+
'extra[', // Dio interceptor extra data
|
|
67
|
+
];
|
|
68
|
+
|
|
69
|
+
_S050Visitor({
|
|
70
|
+
required this.filePath,
|
|
71
|
+
required this.lineInfo,
|
|
72
|
+
required this.violations,
|
|
73
|
+
required this.analyzer,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
@override
|
|
77
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
78
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
79
|
+
final source = node.toSource().toLowerCase();
|
|
80
|
+
|
|
81
|
+
// Check for Random() (not secure) in token context
|
|
82
|
+
if (methodName == 'nextint' || methodName == 'nextdouble') {
|
|
83
|
+
// Skip if using Random.secure() - check target variable name
|
|
84
|
+
final target = node.target?.toSource().toLowerCase() ?? '';
|
|
85
|
+
if (target.contains('secure') || source.contains('secure')) {
|
|
86
|
+
// Using Random.secure() - this is safe
|
|
87
|
+
super.visitMethodInvocation(node);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (_isInTokenContext(node) && !_isSecureRandomContext(node)) {
|
|
92
|
+
violations.add(analyzer.createViolation(
|
|
93
|
+
filePath: filePath,
|
|
94
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
95
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
96
|
+
message:
|
|
97
|
+
'Random() is not cryptographically secure - use Random.secure() for tokens',
|
|
98
|
+
));
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Check for DateTime.now() used as token
|
|
103
|
+
if (methodName == 'now' && source.contains('datetime')) {
|
|
104
|
+
// Skip if used for timing/logging purposes
|
|
105
|
+
if (!_isSafeTimestampUsage(node) && _isInTokenContext(node)) {
|
|
106
|
+
violations.add(analyzer.createViolation(
|
|
107
|
+
filePath: filePath,
|
|
108
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
109
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
110
|
+
message:
|
|
111
|
+
'Timestamp-based tokens are predictable - use CSPRNG with 128-bit entropy',
|
|
112
|
+
));
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Check for uuid without secure random (some uuid packages use weak random)
|
|
117
|
+
if (methodName == 'v1' && source.contains('uuid')) {
|
|
118
|
+
if (_isInTokenContext(node)) {
|
|
119
|
+
violations.add(analyzer.createViolation(
|
|
120
|
+
filePath: filePath,
|
|
121
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
122
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
123
|
+
message:
|
|
124
|
+
'UUID v1 is time-based and predictable - use UUID v4 with secure random',
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
super.visitMethodInvocation(node);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
@override
|
|
133
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
134
|
+
final typeName = node.constructorName.type.name2.lexeme.toLowerCase();
|
|
135
|
+
|
|
136
|
+
// Check for Random() (not Random.secure()) in token context
|
|
137
|
+
if (typeName == 'random') {
|
|
138
|
+
final constructorName =
|
|
139
|
+
node.constructorName.name?.name.toLowerCase() ?? '';
|
|
140
|
+
if (constructorName != 'secure') {
|
|
141
|
+
if (_isInTokenContext(node)) {
|
|
142
|
+
violations.add(analyzer.createViolation(
|
|
143
|
+
filePath: filePath,
|
|
144
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
145
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
146
|
+
message:
|
|
147
|
+
'Random() is not cryptographically secure - use Random.secure() for tokens',
|
|
148
|
+
));
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
super.visitInstanceCreationExpression(node);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
void visitAssignmentExpression(AssignmentExpression node) {
|
|
158
|
+
final left = node.leftHandSide.toSource().toLowerCase();
|
|
159
|
+
final right = node.rightHandSide.toSource().toLowerCase();
|
|
160
|
+
|
|
161
|
+
// Check for sequential token generation (++counter, counter++)
|
|
162
|
+
if (_tokenContextPatterns.any((p) => left.contains(p))) {
|
|
163
|
+
if (right.contains('++') || right.contains('--') || right == left) {
|
|
164
|
+
violations.add(analyzer.createViolation(
|
|
165
|
+
filePath: filePath,
|
|
166
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
167
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
168
|
+
message:
|
|
169
|
+
'Sequential tokens are predictable - use CSPRNG with 128-bit entropy',
|
|
170
|
+
));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
super.visitAssignmentExpression(node);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
bool _isInTokenContext(AstNode node) {
|
|
178
|
+
AstNode? current = node.parent;
|
|
179
|
+
int depth = 0;
|
|
180
|
+
while (current != null && depth < 10) {
|
|
181
|
+
final source = current.toSource().toLowerCase();
|
|
182
|
+
if (_tokenContextPatterns.any((p) => source.contains(p))) {
|
|
183
|
+
return true;
|
|
184
|
+
}
|
|
185
|
+
current = current.parent;
|
|
186
|
+
depth++;
|
|
187
|
+
}
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
bool _isSecureRandomContext(AstNode node) {
|
|
192
|
+
// Check if this node is using Random.secure()
|
|
193
|
+
AstNode? current = node.parent;
|
|
194
|
+
int depth = 0;
|
|
195
|
+
while (current != null && depth < 5) {
|
|
196
|
+
final source = current.toSource().toLowerCase();
|
|
197
|
+
// Check for secure random patterns
|
|
198
|
+
if (source.contains('random.secure') ||
|
|
199
|
+
source.contains('securerandom') ||
|
|
200
|
+
source.contains('_securerandom') ||
|
|
201
|
+
source.contains('_secure')) {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
current = current.parent;
|
|
205
|
+
depth++;
|
|
206
|
+
}
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/// Check if DateTime.now() is used for safe purposes (timing, logging)
|
|
211
|
+
bool _isSafeTimestampUsage(AstNode node) {
|
|
212
|
+
// Check immediate parent and surrounding context
|
|
213
|
+
AstNode? current = node.parent;
|
|
214
|
+
int depth = 0;
|
|
215
|
+
while (current != null && depth < 5) {
|
|
216
|
+
final source = current.toSource().toLowerCase();
|
|
217
|
+
// Check for safe timestamp patterns
|
|
218
|
+
if (_safeTimestampPatterns.any((p) => source.contains(p))) {
|
|
219
|
+
return true;
|
|
220
|
+
}
|
|
221
|
+
// Check for timing operations
|
|
222
|
+
if (source.contains('.difference(') ||
|
|
223
|
+
source.contains('.inmilliseconds') ||
|
|
224
|
+
source.contains('.inseconds') ||
|
|
225
|
+
source.contains('.inminutes') ||
|
|
226
|
+
source.contains('format(')) {
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
current = current.parent;
|
|
230
|
+
depth++;
|
|
231
|
+
}
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
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
|
+
/// S051: Password Length Policy
|
|
10
|
+
/// Enforce minimum password length requirements
|
|
11
|
+
class S051PasswordLengthPolicyAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S051';
|
|
14
|
+
|
|
15
|
+
// Minimum recommended password length
|
|
16
|
+
static const int _minRecommendedLength = 8;
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
List<Violation> analyze({
|
|
20
|
+
required CompilationUnit unit,
|
|
21
|
+
required String filePath,
|
|
22
|
+
required Rule rule,
|
|
23
|
+
required LineInfo lineInfo,
|
|
24
|
+
}) {
|
|
25
|
+
final violations = <Violation>[];
|
|
26
|
+
final visitor = _S051Visitor(
|
|
27
|
+
filePath: filePath,
|
|
28
|
+
lineInfo: lineInfo,
|
|
29
|
+
violations: violations,
|
|
30
|
+
analyzer: this,
|
|
31
|
+
);
|
|
32
|
+
unit.accept(visitor);
|
|
33
|
+
return violations;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class _S051Visitor extends RecursiveAstVisitor<void> {
|
|
38
|
+
final String filePath;
|
|
39
|
+
final LineInfo lineInfo;
|
|
40
|
+
final List<Violation> violations;
|
|
41
|
+
final S051PasswordLengthPolicyAnalyzer analyzer;
|
|
42
|
+
|
|
43
|
+
_S051Visitor({
|
|
44
|
+
required this.filePath,
|
|
45
|
+
required this.lineInfo,
|
|
46
|
+
required this.violations,
|
|
47
|
+
required this.analyzer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
52
|
+
final varName = node.name.lexeme.toLowerCase();
|
|
53
|
+
final initializer = node.initializer;
|
|
54
|
+
|
|
55
|
+
// Check for password min length configuration
|
|
56
|
+
if ((varName.contains('password') || varName.contains('pwd')) &&
|
|
57
|
+
(varName.contains('min') || varName.contains('length'))) {
|
|
58
|
+
if (initializer is IntegerLiteral) {
|
|
59
|
+
final value = initializer.value ?? 0;
|
|
60
|
+
if (value < S051PasswordLengthPolicyAnalyzer._minRecommendedLength) {
|
|
61
|
+
violations.add(analyzer.createViolation(
|
|
62
|
+
filePath: filePath,
|
|
63
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
64
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
65
|
+
message: 'Minimum password length should be at least 8 characters',
|
|
66
|
+
));
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
super.visitVariableDeclaration(node);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
void visitBinaryExpression(BinaryExpression node) {
|
|
76
|
+
final source = node.toSource().toLowerCase();
|
|
77
|
+
|
|
78
|
+
// Check for password length comparisons
|
|
79
|
+
if ((source.contains('password') || source.contains('pwd')) &&
|
|
80
|
+
source.contains('length')) {
|
|
81
|
+
// Check for weak minimum length
|
|
82
|
+
if (node.operator.type.lexeme == '<' || node.operator.type.lexeme == '>=') {
|
|
83
|
+
final right = node.rightOperand;
|
|
84
|
+
if (right is IntegerLiteral) {
|
|
85
|
+
final value = right.value ?? 0;
|
|
86
|
+
if (value < S051PasswordLengthPolicyAnalyzer._minRecommendedLength) {
|
|
87
|
+
violations.add(analyzer.createViolation(
|
|
88
|
+
filePath: filePath,
|
|
89
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
90
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
91
|
+
message: 'Password length validation is too weak - require at least 8 characters',
|
|
92
|
+
));
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
super.visitBinaryExpression(node);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
@override
|
|
102
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
103
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
104
|
+
|
|
105
|
+
// Skip if this is just a call to a validation function (not the implementation)
|
|
106
|
+
// e.g., loginNotifier.validatePassword(value) - this is a CALL, not implementation
|
|
107
|
+
// We should only flag the actual implementation of validatePassword
|
|
108
|
+
if (methodName == 'validatepassword' || methodName == 'validate_password') {
|
|
109
|
+
// Check if this is inside a method BODY (implementation) vs a method CALL
|
|
110
|
+
// Method calls have targets like: notifier.validatePassword(value)
|
|
111
|
+
if (node.target != null) {
|
|
112
|
+
// This is a call like: something.validatePassword(value) - skip
|
|
113
|
+
super.visitMethodInvocation(node);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
super.visitMethodInvocation(node);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
@override
|
|
122
|
+
void visitMethodDeclaration(MethodDeclaration node) {
|
|
123
|
+
final methodName = node.name.lexeme.toLowerCase();
|
|
124
|
+
|
|
125
|
+
// Check password validation method implementations
|
|
126
|
+
if (methodName.contains('validatepassword') || methodName.contains('validate_password')) {
|
|
127
|
+
final body = node.body;
|
|
128
|
+
if (body != null) {
|
|
129
|
+
final bodySource = body.toSource().toLowerCase();
|
|
130
|
+
|
|
131
|
+
// Skip if this is just a setter/state-change method (not actual validation)
|
|
132
|
+
// These methods just store the password value and trigger UI updates
|
|
133
|
+
// Pattern: { _password = value; someMethod(); notifyListeners(); }
|
|
134
|
+
bool isSetterPattern = (bodySource.contains('_password =') ||
|
|
135
|
+
bodySource.contains('password =')) &&
|
|
136
|
+
(bodySource.contains('notifylisteners') ||
|
|
137
|
+
bodySource.contains('setstate') ||
|
|
138
|
+
bodySource.contains('notify()')) &&
|
|
139
|
+
!bodySource.contains('if ') && // No conditional logic = not validation
|
|
140
|
+
!bodySource.contains('throw') &&
|
|
141
|
+
!bodySource.contains('error');
|
|
142
|
+
|
|
143
|
+
if (isSetterPattern) {
|
|
144
|
+
// This is just a state setter, not actual validation - skip
|
|
145
|
+
super.visitMethodDeclaration(node);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Check if the implementation includes length validation
|
|
150
|
+
bool hasLengthCheck = bodySource.contains('length') ||
|
|
151
|
+
bodySource.contains('minlength') ||
|
|
152
|
+
bodySource.contains('min_length') ||
|
|
153
|
+
bodySource.contains('>= 8') ||
|
|
154
|
+
bodySource.contains('>= 12') ||
|
|
155
|
+
bodySource.contains('< 8') ||
|
|
156
|
+
bodySource.contains('< 12');
|
|
157
|
+
|
|
158
|
+
if (!hasLengthCheck) {
|
|
159
|
+
violations.add(analyzer.createViolation(
|
|
160
|
+
filePath: filePath,
|
|
161
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
162
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
163
|
+
message: 'Password validation should include length check',
|
|
164
|
+
));
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
super.visitMethodDeclaration(node);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
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
|
+
/// S052: Weak OTP Entropy
|
|
10
|
+
/// OTP codes should have sufficient entropy (length and randomness)
|
|
11
|
+
class S052WeakOtpEntropyAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S052';
|
|
14
|
+
|
|
15
|
+
// Minimum recommended OTP length
|
|
16
|
+
static const int _minOtpLength = 6;
|
|
17
|
+
|
|
18
|
+
@override
|
|
19
|
+
List<Violation> analyze({
|
|
20
|
+
required CompilationUnit unit,
|
|
21
|
+
required String filePath,
|
|
22
|
+
required Rule rule,
|
|
23
|
+
required LineInfo lineInfo,
|
|
24
|
+
}) {
|
|
25
|
+
final violations = <Violation>[];
|
|
26
|
+
final visitor = _S052Visitor(
|
|
27
|
+
filePath: filePath,
|
|
28
|
+
lineInfo: lineInfo,
|
|
29
|
+
violations: violations,
|
|
30
|
+
analyzer: this,
|
|
31
|
+
);
|
|
32
|
+
unit.accept(visitor);
|
|
33
|
+
return violations;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
class _S052Visitor extends RecursiveAstVisitor<void> {
|
|
38
|
+
final String filePath;
|
|
39
|
+
final LineInfo lineInfo;
|
|
40
|
+
final List<Violation> violations;
|
|
41
|
+
final S052WeakOtpEntropyAnalyzer analyzer;
|
|
42
|
+
|
|
43
|
+
_S052Visitor({
|
|
44
|
+
required this.filePath,
|
|
45
|
+
required this.lineInfo,
|
|
46
|
+
required this.violations,
|
|
47
|
+
required this.analyzer,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
@override
|
|
51
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
52
|
+
final varName = node.name.lexeme.toLowerCase();
|
|
53
|
+
final initializer = node.initializer;
|
|
54
|
+
|
|
55
|
+
// Check for OTP length configuration
|
|
56
|
+
if (varName.contains('otp') && varName.contains('length')) {
|
|
57
|
+
if (initializer is IntegerLiteral) {
|
|
58
|
+
final value = initializer.value ?? 0;
|
|
59
|
+
if (value < S052WeakOtpEntropyAnalyzer._minOtpLength) {
|
|
60
|
+
violations.add(analyzer.createViolation(
|
|
61
|
+
filePath: filePath,
|
|
62
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
63
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
64
|
+
message: 'OTP length should be at least 6 digits for adequate security',
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
super.visitVariableDeclaration(node);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
@override
|
|
74
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
75
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
76
|
+
final source = node.toSource().toLowerCase();
|
|
77
|
+
|
|
78
|
+
// Check for OTP generation
|
|
79
|
+
if (methodName.contains('otp') || source.contains('generateotp') || source.contains('generate_otp')) {
|
|
80
|
+
// Check if using secure random
|
|
81
|
+
bool usesSecureRandom = source.contains('random.secure') ||
|
|
82
|
+
source.contains('securerandom') ||
|
|
83
|
+
source.contains('crypto');
|
|
84
|
+
|
|
85
|
+
if (!usesSecureRandom && source.contains('random')) {
|
|
86
|
+
violations.add(analyzer.createViolation(
|
|
87
|
+
filePath: filePath,
|
|
88
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
89
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
90
|
+
message: 'OTP generation should use cryptographically secure random',
|
|
91
|
+
));
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Check for weak patterns like sequential or predictable
|
|
95
|
+
if (source.contains('datetime.now') || source.contains('timestamp')) {
|
|
96
|
+
violations.add(analyzer.createViolation(
|
|
97
|
+
filePath: filePath,
|
|
98
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
99
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
100
|
+
message: 'OTP should not be based on timestamp - use secure random',
|
|
101
|
+
));
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
super.visitMethodInvocation(node);
|
|
106
|
+
}
|
|
107
|
+
}
|