@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,182 @@
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
+ /// S005: Enforce authorization at trusted service layer
10
+ /// Detect client-side authorization that can be bypassed
11
+ class S005TrustedServiceAuthorizationAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S005';
14
+
15
+ // Client-side authorization patterns (dangerous)
16
+ static const _clientSideAuthPatterns = [
17
+ 'localstorage.getitem',
18
+ 'sessionstorage.getitem',
19
+ 'sharedpreferences',
20
+ 'window.location',
21
+ 'document.cookie',
22
+ ];
23
+
24
+ // Authorization keywords
25
+ static const _authKeywords = [
26
+ 'isadmin',
27
+ 'is_admin',
28
+ 'hasrole',
29
+ 'has_role',
30
+ 'haspermission',
31
+ 'has_permission',
32
+ 'canaccess',
33
+ 'can_access',
34
+ 'isauthorized',
35
+ 'is_authorized',
36
+ 'checkpermission',
37
+ 'check_permission',
38
+ 'checkrole',
39
+ 'check_role',
40
+ ];
41
+
42
+ // Server-side patterns (safe)
43
+ static const _serverSidePatterns = [
44
+ 'middleware',
45
+ 'interceptor',
46
+ 'guard',
47
+ '@authorized',
48
+ '@authenticated',
49
+ 'authservice',
50
+ 'auth_service',
51
+ 'api.',
52
+ 'backend',
53
+ 'server',
54
+ ];
55
+
56
+ @override
57
+ List<Violation> analyze({
58
+ required CompilationUnit unit,
59
+ required String filePath,
60
+ required Rule rule,
61
+ required LineInfo lineInfo,
62
+ }) {
63
+ final violations = <Violation>[];
64
+ final visitor = _S005Visitor(
65
+ filePath: filePath,
66
+ lineInfo: lineInfo,
67
+ violations: violations,
68
+ analyzer: this,
69
+ );
70
+ unit.accept(visitor);
71
+ return violations;
72
+ }
73
+ }
74
+
75
+ class _S005Visitor extends RecursiveAstVisitor<void> {
76
+ final String filePath;
77
+ final LineInfo lineInfo;
78
+ final List<Violation> violations;
79
+ final S005TrustedServiceAuthorizationAnalyzer analyzer;
80
+
81
+ _S005Visitor({
82
+ required this.filePath,
83
+ required this.lineInfo,
84
+ required this.violations,
85
+ required this.analyzer,
86
+ });
87
+
88
+ @override
89
+ void visitIfStatement(IfStatement node) {
90
+ final condition = node.expression.toSource().toLowerCase();
91
+
92
+ // Check if this is an authorization check
93
+ bool isAuthCheck = S005TrustedServiceAuthorizationAnalyzer._authKeywords
94
+ .any((k) => condition.contains(k));
95
+
96
+ if (isAuthCheck) {
97
+ // Check if relying on client-side data
98
+ bool usesClientSideData =
99
+ S005TrustedServiceAuthorizationAnalyzer._clientSideAuthPatterns
100
+ .any((p) => condition.contains(p));
101
+
102
+ // Check if it's server-side context
103
+ bool isServerSide =
104
+ S005TrustedServiceAuthorizationAnalyzer._serverSidePatterns
105
+ .any((p) => condition.contains(p));
106
+
107
+ if (usesClientSideData && !isServerSide) {
108
+ violations.add(analyzer.createViolation(
109
+ filePath: filePath,
110
+ line: analyzer.getLine(lineInfo, node.offset),
111
+ column: analyzer.getColumn(lineInfo, node.offset),
112
+ message:
113
+ 'Authorization check uses client-side data - enforce authorization at trusted service layer',
114
+ ));
115
+ }
116
+ }
117
+
118
+ super.visitIfStatement(node);
119
+ }
120
+
121
+ @override
122
+ void visitMethodInvocation(MethodInvocation node) {
123
+ final source = node.toSource().toLowerCase();
124
+ final methodName = node.methodName.name.toLowerCase();
125
+
126
+ // Check for authorization methods using client-side data
127
+ bool isAuthMethod = S005TrustedServiceAuthorizationAnalyzer._authKeywords
128
+ .any((k) => methodName.contains(k));
129
+
130
+ if (isAuthMethod) {
131
+ // Check arguments for client-side data sources
132
+ for (final arg in node.argumentList.arguments) {
133
+ final argSource = arg.toSource().toLowerCase();
134
+ bool usesClientData =
135
+ S005TrustedServiceAuthorizationAnalyzer._clientSideAuthPatterns
136
+ .any((p) => argSource.contains(p));
137
+
138
+ if (usesClientData) {
139
+ violations.add(analyzer.createViolation(
140
+ filePath: filePath,
141
+ line: analyzer.getLine(lineInfo, node.offset),
142
+ column: analyzer.getColumn(lineInfo, node.offset),
143
+ message:
144
+ 'Authorization relies on client-controlled data - validate permissions server-side',
145
+ ));
146
+ break;
147
+ }
148
+ }
149
+ }
150
+
151
+ super.visitMethodInvocation(node);
152
+ }
153
+
154
+ @override
155
+ void visitVariableDeclaration(VariableDeclaration node) {
156
+ final varName = node.name.lexeme.toLowerCase();
157
+ final initializer = node.initializer;
158
+
159
+ // Check for authorization variables from client-side storage
160
+ bool isAuthVar = S005TrustedServiceAuthorizationAnalyzer._authKeywords
161
+ .any((k) => varName.contains(k));
162
+
163
+ if (isAuthVar && initializer != null) {
164
+ final initSource = initializer.toSource().toLowerCase();
165
+ bool fromClientStorage =
166
+ S005TrustedServiceAuthorizationAnalyzer._clientSideAuthPatterns
167
+ .any((p) => initSource.contains(p));
168
+
169
+ if (fromClientStorage) {
170
+ violations.add(analyzer.createViolation(
171
+ filePath: filePath,
172
+ line: analyzer.getLine(lineInfo, node.offset),
173
+ column: analyzer.getColumn(lineInfo, node.offset),
174
+ message:
175
+ 'Authorization state from client storage can be manipulated - use server-side validation',
176
+ ));
177
+ }
178
+ }
179
+
180
+ super.visitVariableDeclaration(node);
181
+ }
182
+ }
@@ -0,0 +1,208 @@
1
+ import 'package:analyzer/dart/ast/ast.dart';
2
+ import 'package:analyzer/dart/ast/visitor.dart';
3
+ import 'package:analyzer/source/line_info.dart';
4
+
5
+ import '../../models/rule.dart';
6
+ import '../../models/violation.dart';
7
+ import '../base_analyzer.dart';
8
+
9
+ /// S006: Do not use default credentials for service authentication
10
+ /// Detect common default or well-known credentials
11
+ class S006NoDefaultCredentialsAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S006';
14
+
15
+ // Common default credential combinations
16
+ static const _defaultCredentials = [
17
+ 'admin:admin',
18
+ 'root:root',
19
+ 'test:test',
20
+ 'user:user',
21
+ 'guest:guest',
22
+ 'admin:password',
23
+ 'admin:123456',
24
+ 'root:password',
25
+ 'postgres:postgres',
26
+ 'mysql:mysql',
27
+ 'sa:sa',
28
+ 'administrator:administrator',
29
+ ];
30
+
31
+ // Default password patterns
32
+ static const _defaultPasswords = [
33
+ 'password',
34
+ 'password123',
35
+ '123456',
36
+ 'admin',
37
+ 'root',
38
+ 'test',
39
+ 'default',
40
+ 'changeme',
41
+ 'letmein',
42
+ 'qwerty',
43
+ ];
44
+
45
+ // Credential context patterns
46
+ static const _credentialContexts = [
47
+ 'password',
48
+ 'passwd',
49
+ 'credential',
50
+ 'auth',
51
+ 'login',
52
+ 'connect',
53
+ 'database',
54
+ 'db_',
55
+ 'redis',
56
+ 'mongo',
57
+ 'mysql',
58
+ 'postgres',
59
+ ];
60
+
61
+ @override
62
+ List<Violation> analyze({
63
+ required CompilationUnit unit,
64
+ required String filePath,
65
+ required Rule rule,
66
+ required LineInfo lineInfo,
67
+ }) {
68
+ final violations = <Violation>[];
69
+ final visitor = _S006Visitor(
70
+ filePath: filePath,
71
+ lineInfo: lineInfo,
72
+ violations: violations,
73
+ analyzer: this,
74
+ );
75
+ unit.accept(visitor);
76
+ return violations;
77
+ }
78
+ }
79
+
80
+ class _S006Visitor extends RecursiveAstVisitor<void> {
81
+ final String filePath;
82
+ final LineInfo lineInfo;
83
+ final List<Violation> violations;
84
+ final S006NoDefaultCredentialsAnalyzer analyzer;
85
+
86
+ _S006Visitor({
87
+ required this.filePath,
88
+ required this.lineInfo,
89
+ required this.violations,
90
+ required this.analyzer,
91
+ });
92
+
93
+ @override
94
+ void visitVariableDeclaration(VariableDeclaration node) {
95
+ final varName = node.name.lexeme.toLowerCase();
96
+ final initializer = node.initializer;
97
+
98
+ if (initializer is StringLiteral) {
99
+ final value = _getStringValue(initializer).toLowerCase();
100
+
101
+ // Check for default credential combinations
102
+ bool hasDefaultCredential =
103
+ S006NoDefaultCredentialsAnalyzer._defaultCredentials
104
+ .any((c) => value.contains(c));
105
+
106
+ if (hasDefaultCredential) {
107
+ violations.add(analyzer.createViolation(
108
+ filePath: filePath,
109
+ line: analyzer.getLine(lineInfo, node.offset),
110
+ column: analyzer.getColumn(lineInfo, node.offset),
111
+ message:
112
+ 'Default credentials detected - use unique, strong credentials for each service',
113
+ ));
114
+ }
115
+
116
+ // Check for default passwords in credential contexts
117
+ bool isCredentialContext =
118
+ S006NoDefaultCredentialsAnalyzer._credentialContexts
119
+ .any((c) => varName.contains(c));
120
+
121
+ if (isCredentialContext) {
122
+ bool hasDefaultPassword =
123
+ S006NoDefaultCredentialsAnalyzer._defaultPasswords
124
+ .any((p) => value == p || value.contains(p));
125
+
126
+ if (hasDefaultPassword) {
127
+ violations.add(analyzer.createViolation(
128
+ filePath: filePath,
129
+ line: analyzer.getLine(lineInfo, node.offset),
130
+ column: analyzer.getColumn(lineInfo, node.offset),
131
+ message:
132
+ 'Default/weak password detected - generate unique strong credentials',
133
+ ));
134
+ }
135
+ }
136
+ }
137
+
138
+ super.visitVariableDeclaration(node);
139
+ }
140
+
141
+ @override
142
+ void visitMapLiteralEntry(MapLiteralEntry node) {
143
+ final key = node.key.toSource().toLowerCase();
144
+ final value = node.value;
145
+
146
+ // Check for credential keys with default values
147
+ bool isCredentialKey = S006NoDefaultCredentialsAnalyzer._credentialContexts
148
+ .any((c) => key.contains(c));
149
+
150
+ if (isCredentialKey && value is StringLiteral) {
151
+ final valueStr = _getStringValue(value).toLowerCase();
152
+
153
+ bool hasDefaultPassword = S006NoDefaultCredentialsAnalyzer._defaultPasswords
154
+ .any((p) => valueStr == p || valueStr.contains(p));
155
+
156
+ bool hasDefaultCredential =
157
+ S006NoDefaultCredentialsAnalyzer._defaultCredentials
158
+ .any((c) => valueStr.contains(c));
159
+
160
+ if (hasDefaultPassword || hasDefaultCredential) {
161
+ violations.add(analyzer.createViolation(
162
+ filePath: filePath,
163
+ line: analyzer.getLine(lineInfo, node.offset),
164
+ column: analyzer.getColumn(lineInfo, node.offset),
165
+ message:
166
+ 'Default credentials in configuration - use secrets manager or environment variables',
167
+ ));
168
+ }
169
+ }
170
+
171
+ super.visitMapLiteralEntry(node);
172
+ }
173
+
174
+ @override
175
+ void visitMethodInvocation(MethodInvocation node) {
176
+ final source = node.toSource().toLowerCase();
177
+
178
+ // Check for connection strings with default credentials
179
+ if (source.contains('connect') ||
180
+ source.contains('createclient') ||
181
+ source.contains('database')) {
182
+ bool hasDefaultCredential =
183
+ S006NoDefaultCredentialsAnalyzer._defaultCredentials
184
+ .any((c) => source.contains(c));
185
+
186
+ if (hasDefaultCredential) {
187
+ violations.add(analyzer.createViolation(
188
+ filePath: filePath,
189
+ line: analyzer.getLine(lineInfo, node.offset),
190
+ column: analyzer.getColumn(lineInfo, node.offset),
191
+ message:
192
+ 'Default credentials in connection - use environment variables or secrets manager',
193
+ ));
194
+ }
195
+ }
196
+
197
+ super.visitMethodInvocation(node);
198
+ }
199
+
200
+ String _getStringValue(StringLiteral literal) {
201
+ if (literal is SimpleStringLiteral) {
202
+ return literal.value;
203
+ } else if (literal is AdjacentStrings) {
204
+ return literal.strings.map((s) => _getStringValue(s)).join();
205
+ }
206
+ return literal.toSource();
207
+ }
208
+ }
@@ -0,0 +1,224 @@
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
+ /// S007: Perform output encoding before interpreter use
10
+ /// Detect missing output encoding for different contexts
11
+ class S007OutputEncodingAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S007';
14
+
15
+ // Output context patterns
16
+ static const _htmlOutputPatterns = [
17
+ 'innerhtml',
18
+ 'innerHtml',
19
+ 'setinnerhtml',
20
+ 'outerhtml',
21
+ 'outerHtml',
22
+ 'dangerouslysetinnerhtml',
23
+ 'unsafehtml',
24
+ ];
25
+
26
+ // JavaScript context patterns
27
+ static const _jsOutputPatterns = [
28
+ 'eval(',
29
+ 'new function(',
30
+ 'settimeout(',
31
+ 'setinterval(',
32
+ 'script>',
33
+ ];
34
+
35
+ // URL context patterns
36
+ static const _urlOutputPatterns = [
37
+ 'href=',
38
+ 'src=',
39
+ 'action=',
40
+ 'window.location',
41
+ 'document.location',
42
+ ];
43
+
44
+ // Safe encoding patterns
45
+ static const _encodingPatterns = [
46
+ 'htmlescape',
47
+ 'html.escape',
48
+ 'htmlentities',
49
+ 'encodeuri',
50
+ 'encodeuricomponent',
51
+ 'urlencode',
52
+ 'sanitize',
53
+ 'encode',
54
+ 'escape',
55
+ 'dompurify',
56
+ ];
57
+
58
+ @override
59
+ List<Violation> analyze({
60
+ required CompilationUnit unit,
61
+ required String filePath,
62
+ required Rule rule,
63
+ required LineInfo lineInfo,
64
+ }) {
65
+ final violations = <Violation>[];
66
+ final visitor = _S007Visitor(
67
+ filePath: filePath,
68
+ lineInfo: lineInfo,
69
+ violations: violations,
70
+ analyzer: this,
71
+ );
72
+ unit.accept(visitor);
73
+ return violations;
74
+ }
75
+ }
76
+
77
+ class _S007Visitor extends RecursiveAstVisitor<void> {
78
+ final String filePath;
79
+ final LineInfo lineInfo;
80
+ final List<Violation> violations;
81
+ final S007OutputEncodingAnalyzer analyzer;
82
+
83
+ _S007Visitor({
84
+ required this.filePath,
85
+ required this.lineInfo,
86
+ required this.violations,
87
+ required this.analyzer,
88
+ });
89
+
90
+ @override
91
+ void visitAssignmentExpression(AssignmentExpression node) {
92
+ final left = node.leftHandSide.toSource().toLowerCase();
93
+ final right = node.rightHandSide.toSource().toLowerCase();
94
+
95
+ // Check for HTML context without encoding
96
+ bool isHtmlContext = S007OutputEncodingAnalyzer._htmlOutputPatterns
97
+ .any((p) => left.contains(p));
98
+
99
+ if (isHtmlContext) {
100
+ bool hasEncoding = S007OutputEncodingAnalyzer._encodingPatterns
101
+ .any((p) => right.contains(p));
102
+
103
+ // Check if using user input without encoding
104
+ bool hasUserInput = _containsUserInput(right);
105
+
106
+ if (hasUserInput && !hasEncoding) {
107
+ violations.add(analyzer.createViolation(
108
+ filePath: filePath,
109
+ line: analyzer.getLine(lineInfo, node.offset),
110
+ column: analyzer.getColumn(lineInfo, node.offset),
111
+ message:
112
+ 'HTML output without encoding - apply HTML encoding before inserting user data',
113
+ ));
114
+ }
115
+ }
116
+
117
+ super.visitAssignmentExpression(node);
118
+ }
119
+
120
+ @override
121
+ void visitMethodInvocation(MethodInvocation node) {
122
+ final source = node.toSource().toLowerCase();
123
+ final methodName = node.methodName.name.toLowerCase();
124
+
125
+ // Check for dangerous output methods
126
+ bool isHtmlOutput = S007OutputEncodingAnalyzer._htmlOutputPatterns
127
+ .any((p) => methodName.contains(p) || source.contains(p));
128
+
129
+ if (isHtmlOutput) {
130
+ bool hasEncoding = S007OutputEncodingAnalyzer._encodingPatterns
131
+ .any((p) => source.contains(p));
132
+
133
+ bool hasUserInput = _containsUserInput(source);
134
+
135
+ if (hasUserInput && !hasEncoding) {
136
+ violations.add(analyzer.createViolation(
137
+ filePath: filePath,
138
+ line: analyzer.getLine(lineInfo, node.offset),
139
+ column: analyzer.getColumn(lineInfo, node.offset),
140
+ message:
141
+ 'Output to HTML without encoding - use context-appropriate encoding',
142
+ ));
143
+ }
144
+ }
145
+
146
+ // Check for JavaScript context
147
+ bool isJsOutput = S007OutputEncodingAnalyzer._jsOutputPatterns
148
+ .any((p) => source.contains(p));
149
+
150
+ if (isJsOutput) {
151
+ bool hasEncoding = S007OutputEncodingAnalyzer._encodingPatterns
152
+ .any((p) => source.contains(p));
153
+
154
+ bool hasUserInput = _containsUserInput(source);
155
+
156
+ if (hasUserInput && !hasEncoding) {
157
+ violations.add(analyzer.createViolation(
158
+ filePath: filePath,
159
+ line: analyzer.getLine(lineInfo, node.offset),
160
+ column: analyzer.getColumn(lineInfo, node.offset),
161
+ message:
162
+ 'User data in JavaScript context without encoding - escape properly before use',
163
+ ));
164
+ }
165
+ }
166
+
167
+ super.visitMethodInvocation(node);
168
+ }
169
+
170
+ @override
171
+ void visitStringInterpolation(StringInterpolation node) {
172
+ final source = node.toSource().toLowerCase();
173
+
174
+ // Check if interpolation is in URL context
175
+ bool isUrlContext = S007OutputEncodingAnalyzer._urlOutputPatterns
176
+ .any((p) => source.contains(p));
177
+
178
+ if (isUrlContext) {
179
+ // Check parent context for encoding
180
+ bool hasEncoding = _parentHasEncoding(node);
181
+
182
+ if (!hasEncoding && _containsUserInput(source)) {
183
+ violations.add(analyzer.createViolation(
184
+ filePath: filePath,
185
+ line: analyzer.getLine(lineInfo, node.offset),
186
+ column: analyzer.getColumn(lineInfo, node.offset),
187
+ message:
188
+ 'URL construction with user data - use encodeURIComponent() for query parameters',
189
+ ));
190
+ }
191
+ }
192
+
193
+ super.visitStringInterpolation(node);
194
+ }
195
+
196
+ bool _containsUserInput(String source) {
197
+ final userInputPatterns = [
198
+ 'request.',
199
+ 'params.',
200
+ 'query.',
201
+ 'body.',
202
+ 'input',
203
+ 'userdata',
204
+ 'user.',
205
+ 'form.',
206
+ ];
207
+ return userInputPatterns.any((p) => source.contains(p));
208
+ }
209
+
210
+ bool _parentHasEncoding(AstNode node) {
211
+ AstNode? current = node.parent;
212
+ int depth = 0;
213
+ while (current != null && depth < 5) {
214
+ final source = current.toSource().toLowerCase();
215
+ if (S007OutputEncodingAnalyzer._encodingPatterns
216
+ .any((p) => source.contains(p))) {
217
+ return true;
218
+ }
219
+ current = current.parent;
220
+ depth++;
221
+ }
222
+ return false;
223
+ }
224
+ }