@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.
Files changed (137) hide show
  1. package/dart_analyzer/README.md +226 -0
  2. package/dart_analyzer/analysis_options.yaml +66 -0
  3. package/dart_analyzer/bin/sunlint-dart-macos +0 -0
  4. package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
  5. package/dart_analyzer/lib/analyzer_service.dart +625 -0
  6. package/dart_analyzer/lib/json_rpc_server.dart +275 -0
  7. package/dart_analyzer/lib/models/rule.dart +67 -0
  8. package/dart_analyzer/lib/models/symbol_table.dart +607 -0
  9. package/dart_analyzer/lib/models/violation.dart +69 -0
  10. package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
  11. package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
  12. package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
  13. package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
  14. package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
  15. package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
  16. package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
  17. package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
  18. package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
  19. package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
  20. package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
  21. package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
  22. package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
  23. package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
  24. package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
  25. package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
  26. package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
  27. package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
  28. package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
  29. package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
  30. package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
  31. package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
  32. package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
  33. package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
  34. package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
  35. package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
  36. package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
  37. package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
  38. package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
  39. package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
  40. package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
  41. package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
  42. package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
  43. package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
  44. package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
  45. package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
  46. package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
  47. package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
  48. package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
  49. package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
  50. package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
  51. package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
  52. package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
  53. package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
  54. package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
  55. package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
  56. package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
  57. package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
  58. package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
  59. package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
  60. package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
  61. package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
  62. package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
  63. package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
  64. package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
  65. package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
  66. package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
  67. package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
  68. package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
  69. package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
  70. package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
  71. package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
  72. package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
  73. package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
  74. package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
  75. package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
  76. package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
  77. package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
  78. package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
  79. package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
  80. package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
  81. package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
  82. package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
  83. package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
  84. package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
  85. package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
  86. package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
  87. package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
  88. package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
  89. package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
  90. package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
  91. package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
  92. package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
  93. package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
  94. package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
  95. package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
  96. package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
  97. package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
  98. package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
  99. package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
  100. package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
  101. package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
  102. package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
  103. package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
  104. package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
  105. package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
  106. package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
  107. package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
  108. package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
  109. package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
  110. package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
  111. package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
  112. package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
  113. package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
  114. package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
  115. package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
  116. package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
  117. package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
  118. package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
  119. package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
  120. package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
  121. package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
  122. package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
  123. package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
  124. package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
  125. package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
  126. package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
  127. package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
  128. package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
  129. package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
  130. package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
  131. package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
  132. package/dart_analyzer/lib/utils/common_utils.dart +26 -0
  133. package/dart_analyzer/pubspec.lock +557 -0
  134. package/dart_analyzer/pubspec.yaml +39 -0
  135. package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
  136. package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
  137. 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
+ }