@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,188 @@
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
+ /// S036: LFI/RFI Protection
10
+ /// Prevent Local/Remote File Inclusion attacks
11
+ class S036LfiRfiProtectionAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S036';
14
+
15
+ // Actual risky file operations that read content from paths
16
+ static const _riskyFileOperations = [
17
+ 'readasstring', 'readasbytes', 'readaslines',
18
+ ];
19
+
20
+ // Safe Flutter/Dart path utilities that should NOT trigger violations
21
+ static const _safePathMethods = [
22
+ // path_provider methods
23
+ 'gettemporarydirectory', 'getapplicationdocumentsdirectory',
24
+ 'getapplicationsupportdirectory', 'getdownloadsdirectory',
25
+ 'getexternalstoragedirectory', 'getlibrarypath',
26
+ // path package methods
27
+ 'join', 'basename', 'dirname', 'extension', 'normalize',
28
+ 'relative', 'absolute', 'separator', 'split', 'context',
29
+ // Flutter asset methods
30
+ 'rootbundle', 'assetbundle', 'loadstring', 'load',
31
+ // Future utilities
32
+ 'microtask', 'delayed', 'wait', 'timeout',
33
+ ];
34
+
35
+ // Safe context identifiers that should NOT trigger violations
36
+ static const _safeContextPatterns = [
37
+ 'element.path', // Widget element path
38
+ 'route.path', // Navigation route
39
+ 'widget.path', // Widget path
40
+ 'context.path', // Build context
41
+ 'navigator', // Navigation
42
+ '.value', // Value notifier
43
+ 'notifier', // State notifier
44
+ ];
45
+
46
+ // Path traversal patterns (actual security concern)
47
+ static const _traversalPatterns = [
48
+ '..', '%2e%2e', '..%2f', '%2e%2e%2f', '..\\',
49
+ ];
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 = _S036Visitor(
60
+ filePath: filePath,
61
+ lineInfo: lineInfo,
62
+ violations: violations,
63
+ analyzer: this,
64
+ );
65
+ unit.accept(visitor);
66
+ return violations;
67
+ }
68
+
69
+ /// Check if method is a safe path utility
70
+ static bool isSafeMethod(String source) {
71
+ final lowerSource = source.toLowerCase();
72
+ return _safePathMethods.any((m) => lowerSource.contains(m)) ||
73
+ _safeContextPatterns.any((p) => lowerSource.contains(p));
74
+ }
75
+ }
76
+
77
+ class _S036Visitor extends RecursiveAstVisitor<void> {
78
+ final String filePath;
79
+ final LineInfo lineInfo;
80
+ final List<Violation> violations;
81
+ final S036LfiRfiProtectionAnalyzer analyzer;
82
+
83
+ _S036Visitor({
84
+ required this.filePath,
85
+ required this.lineInfo,
86
+ required this.violations,
87
+ required this.analyzer,
88
+ });
89
+
90
+ @override
91
+ void visitMethodInvocation(MethodInvocation node) {
92
+ final methodName = node.methodName.name.toLowerCase();
93
+ final source = node.toSource().toLowerCase();
94
+
95
+ // Skip safe path methods (path_provider, path package, etc.)
96
+ if (S036LfiRfiProtectionAnalyzer.isSafeMethod(source)) {
97
+ super.visitMethodInvocation(node);
98
+ return;
99
+ }
100
+
101
+ // Only check actual risky file read operations
102
+ bool isRiskyFileOp = S036LfiRfiProtectionAnalyzer._riskyFileOperations
103
+ .any((op) => methodName.contains(op));
104
+
105
+ if (isRiskyFileOp) {
106
+ // Check for user input in file path (only StringInterpolation with external data)
107
+ for (final arg in node.argumentList.arguments) {
108
+ if (arg is StringInterpolation) {
109
+ // Check if interpolation contains potentially dangerous input
110
+ final argSource = arg.toSource().toLowerCase();
111
+ if (_containsUserInput(argSource)) {
112
+ violations.add(analyzer.createViolation(
113
+ filePath: filePath,
114
+ line: analyzer.getLine(lineInfo, node.offset),
115
+ column: analyzer.getColumn(lineInfo, node.offset),
116
+ message: 'LFI/RFI risk - validate and sanitize file paths from user input',
117
+ ));
118
+ break;
119
+ }
120
+ }
121
+ }
122
+ }
123
+
124
+ super.visitMethodInvocation(node);
125
+ }
126
+
127
+ @override
128
+ void visitInstanceCreationExpression(InstanceCreationExpression node) {
129
+ final typeName = node.constructorName.type.name2.lexeme;
130
+
131
+ // Only check File class with user input paths
132
+ if (typeName == 'File') {
133
+ final args = node.argumentList.arguments;
134
+ if (args.isNotEmpty) {
135
+ final firstArg = args.first;
136
+ final source = node.toSource().toLowerCase();
137
+
138
+ // Skip if using safe path methods
139
+ if (S036LfiRfiProtectionAnalyzer.isSafeMethod(source)) {
140
+ super.visitInstanceCreationExpression(node);
141
+ return;
142
+ }
143
+
144
+ // Only flag if path comes from potentially dangerous user input
145
+ if (firstArg is StringInterpolation) {
146
+ final argSource = firstArg.toSource().toLowerCase();
147
+ if (_containsUserInput(argSource)) {
148
+ violations.add(analyzer.createViolation(
149
+ filePath: filePath,
150
+ line: analyzer.getLine(lineInfo, node.offset),
151
+ column: analyzer.getColumn(lineInfo, node.offset),
152
+ message: 'File path with user input - validate against path traversal attacks',
153
+ ));
154
+ }
155
+ }
156
+ }
157
+ }
158
+
159
+ super.visitInstanceCreationExpression(node);
160
+ }
161
+
162
+ @override
163
+ void visitBinaryExpression(BinaryExpression node) {
164
+ // Skip - too many false positives for path concatenation
165
+ // Real LFI protection is at the file read level
166
+ super.visitBinaryExpression(node);
167
+ }
168
+
169
+ /// Check if source likely contains user input (not just internal state)
170
+ bool _containsUserInput(String source) {
171
+ // Patterns that suggest external/user input
172
+ final userInputPatterns = [
173
+ 'request', 'param', 'query', 'body', 'input', 'args',
174
+ 'argument', 'header', 'url', 'uri', 'form', 'upload',
175
+ ];
176
+
177
+ // Skip if source is from safe internal sources
178
+ final safePatterns = [
179
+ 'document', 'cache', 'temp', 'application', 'bundle',
180
+ 'asset', 'resource', 'config', 'settings',
181
+ ];
182
+
183
+ final hasUserInput = userInputPatterns.any((p) => source.contains(p));
184
+ final isSafeSource = safePatterns.any((p) => source.contains(p));
185
+
186
+ return hasUserInput && !isSafeSource;
187
+ }
188
+ }
@@ -0,0 +1,113 @@
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
+ /// S037: Cache Headers for Sensitive Data
10
+ /// Ensure proper cache-control headers for sensitive responses
11
+ class S037CacheHeadersAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S037';
14
+
15
+ // Sensitive response indicators
16
+ static const _sensitiveIndicators = [
17
+ 'password', 'token', 'secret', 'auth', 'session',
18
+ 'credit', 'ssn', 'personal', 'private', 'user',
19
+ ];
20
+
21
+ @override
22
+ List<Violation> analyze({
23
+ required CompilationUnit unit,
24
+ required String filePath,
25
+ required Rule rule,
26
+ required LineInfo lineInfo,
27
+ }) {
28
+ final violations = <Violation>[];
29
+ final visitor = _S037Visitor(
30
+ filePath: filePath,
31
+ lineInfo: lineInfo,
32
+ violations: violations,
33
+ analyzer: this,
34
+ );
35
+ unit.accept(visitor);
36
+ return violations;
37
+ }
38
+ }
39
+
40
+ class _S037Visitor extends RecursiveAstVisitor<void> {
41
+ final String filePath;
42
+ final LineInfo lineInfo;
43
+ final List<Violation> violations;
44
+ final S037CacheHeadersAnalyzer analyzer;
45
+
46
+ _S037Visitor({
47
+ required this.filePath,
48
+ required this.lineInfo,
49
+ required this.violations,
50
+ required this.analyzer,
51
+ });
52
+
53
+ @override
54
+ void visitMethodInvocation(MethodInvocation node) {
55
+ final methodName = node.methodName.name.toLowerCase();
56
+ final source = node.toSource().toLowerCase();
57
+ final target = node.target?.toSource().toLowerCase() ?? '';
58
+
59
+ // Skip non-HTTP contexts (local storage, file I/O, etc.)
60
+ final nonHttpContexts = [
61
+ 'securestorage', 'secure_storage', 'sharedpreferences', 'shared_preferences',
62
+ 'hive', 'sqlite', 'database', 'file', 'stream', 'socket',
63
+ 'localstore', 'local_store', 'cache', 'prefs', 'preferences',
64
+ ];
65
+ if (nonHttpContexts.any((c) => source.contains(c) || target.contains(c))) {
66
+ super.visitMethodInvocation(node);
67
+ return;
68
+ }
69
+
70
+ // Check for HTTP response methods with sensitive data
71
+ // Must be in HTTP context: response.json(), res.send(), http.write()
72
+ bool isHttpResponse = target.contains('response') ||
73
+ target.contains('res') ||
74
+ target.contains('http') ||
75
+ target.contains('reply');
76
+
77
+ if ((methodName == 'json' || methodName == 'send') && isHttpResponse) {
78
+ bool hasSensitiveData = S037CacheHeadersAnalyzer._sensitiveIndicators
79
+ .any((i) => source.contains(i));
80
+
81
+ if (hasSensitiveData) {
82
+ // Check for cache-control headers in surrounding context
83
+ AstNode? current = node.parent;
84
+ bool hasCacheControl = false;
85
+ int depth = 0;
86
+
87
+ while (current != null && depth < 10) {
88
+ final parentSource = current.toSource().toLowerCase();
89
+ if (parentSource.contains('cache-control') ||
90
+ parentSource.contains('cachecontrol') ||
91
+ parentSource.contains('no-store') ||
92
+ parentSource.contains('no-cache')) {
93
+ hasCacheControl = true;
94
+ break;
95
+ }
96
+ current = current.parent;
97
+ depth++;
98
+ }
99
+
100
+ if (!hasCacheControl) {
101
+ violations.add(analyzer.createViolation(
102
+ filePath: filePath,
103
+ line: analyzer.getLine(lineInfo, node.offset),
104
+ column: analyzer.getColumn(lineInfo, node.offset),
105
+ message: 'Sensitive response should have Cache-Control: no-store header',
106
+ ));
107
+ }
108
+ }
109
+ }
110
+
111
+ super.visitMethodInvocation(node);
112
+ }
113
+ }
@@ -0,0 +1,114 @@
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
+ /// S038: No Version Headers
10
+ /// Do not expose server/framework version in HTTP headers
11
+ class S038NoVersionHeadersAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S038';
14
+
15
+ // Headers that expose version info
16
+ static const _versionHeaders = [
17
+ 'x-powered-by', 'server', 'x-aspnet-version', 'x-aspnetmvc-version',
18
+ 'x-drupal-cache', 'x-generator', 'x-runtime', 'x-version',
19
+ ];
20
+
21
+ @override
22
+ List<Violation> analyze({
23
+ required CompilationUnit unit,
24
+ required String filePath,
25
+ required Rule rule,
26
+ required LineInfo lineInfo,
27
+ }) {
28
+ final violations = <Violation>[];
29
+ final visitor = _S038Visitor(
30
+ filePath: filePath,
31
+ lineInfo: lineInfo,
32
+ violations: violations,
33
+ analyzer: this,
34
+ );
35
+ unit.accept(visitor);
36
+ return violations;
37
+ }
38
+ }
39
+
40
+ class _S038Visitor extends RecursiveAstVisitor<void> {
41
+ final String filePath;
42
+ final LineInfo lineInfo;
43
+ final List<Violation> violations;
44
+ final S038NoVersionHeadersAnalyzer analyzer;
45
+
46
+ _S038Visitor({
47
+ required this.filePath,
48
+ required this.lineInfo,
49
+ required this.violations,
50
+ required this.analyzer,
51
+ });
52
+
53
+ @override
54
+ void visitMethodInvocation(MethodInvocation node) {
55
+ final methodName = node.methodName.name.toLowerCase();
56
+
57
+ // Check for header setting methods
58
+ if (methodName == 'setheader' || methodName == 'set' || methodName == 'add') {
59
+ final args = node.argumentList.arguments;
60
+ if (args.isNotEmpty) {
61
+ final firstArg = args.first;
62
+ if (firstArg is SimpleStringLiteral) {
63
+ final headerName = firstArg.value.toLowerCase();
64
+
65
+ bool isVersionHeader = S038NoVersionHeadersAnalyzer._versionHeaders
66
+ .any((h) => headerName.contains(h));
67
+
68
+ if (isVersionHeader) {
69
+ violations.add(analyzer.createViolation(
70
+ filePath: filePath,
71
+ line: analyzer.getLine(lineInfo, node.offset),
72
+ column: analyzer.getColumn(lineInfo, node.offset),
73
+ message: 'Do not expose server/framework version in "$headerName" header',
74
+ ));
75
+ }
76
+ }
77
+ }
78
+ }
79
+
80
+ super.visitMethodInvocation(node);
81
+ }
82
+
83
+ @override
84
+ void visitMapLiteralEntry(MapLiteralEntry node) {
85
+ final key = node.key;
86
+
87
+ if (key is SimpleStringLiteral) {
88
+ final headerName = key.value.toLowerCase();
89
+
90
+ bool isVersionHeader = S038NoVersionHeadersAnalyzer._versionHeaders
91
+ .any((h) => headerName.contains(h));
92
+
93
+ if (isVersionHeader) {
94
+ // Check if this is in a headers context
95
+ AstNode? current = node.parent;
96
+ while (current != null) {
97
+ final source = current.toSource().toLowerCase();
98
+ if (source.contains('header')) {
99
+ violations.add(analyzer.createViolation(
100
+ filePath: filePath,
101
+ line: analyzer.getLine(lineInfo, node.offset),
102
+ column: analyzer.getColumn(lineInfo, node.offset),
103
+ message: 'Avoid setting version-revealing header "$headerName"',
104
+ ));
105
+ break;
106
+ }
107
+ current = current.parent;
108
+ }
109
+ }
110
+ }
111
+
112
+ super.visitMapLiteralEntry(node);
113
+ }
114
+ }
@@ -0,0 +1,131 @@
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
+ /// S039: TLS clients must validate server certificates
10
+ /// Detect patterns where TLS certificate validation is disabled
11
+ class S039TlsCertificateValidationAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S039';
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 = _S039Visitor(
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 _S039Visitor extends RecursiveAstVisitor<void> {
35
+ final String filePath;
36
+ final LineInfo lineInfo;
37
+ final List<Violation> violations;
38
+ final S039TlsCertificateValidationAnalyzer analyzer;
39
+
40
+ _S039Visitor({
41
+ required this.filePath,
42
+ required this.lineInfo,
43
+ required this.violations,
44
+ required this.analyzer,
45
+ });
46
+
47
+ @override
48
+ void visitMethodInvocation(MethodInvocation node) {
49
+ final source = node.toSource().toLowerCase();
50
+
51
+ // Check for badCertificateCallback that returns true (bypasses validation)
52
+ if (source.contains('badcertificatecallback')) {
53
+ // Check if the callback returns true (bypasses certificate validation)
54
+ final fullSource = node.toSource();
55
+ if (fullSource.contains('=> true') ||
56
+ fullSource.contains('return true')) {
57
+ violations.add(analyzer.createViolation(
58
+ filePath: filePath,
59
+ line: analyzer.getLine(lineInfo, node.offset),
60
+ column: analyzer.getColumn(lineInfo, node.offset),
61
+ message:
62
+ 'TLS certificate validation disabled - badCertificateCallback returns true',
63
+ ));
64
+ }
65
+ }
66
+
67
+ super.visitMethodInvocation(node);
68
+ }
69
+
70
+ @override
71
+ void visitNamedExpression(NamedExpression node) {
72
+ final name = node.name.label.name.toLowerCase();
73
+ final value = node.expression.toSource().toLowerCase();
74
+
75
+ // Check for onBadCertificate: (cert) => true
76
+ if (name == 'onbadcertificate') {
77
+ if (value.contains('=> true') || value.contains('return true')) {
78
+ violations.add(analyzer.createViolation(
79
+ filePath: filePath,
80
+ line: analyzer.getLine(lineInfo, node.offset),
81
+ column: analyzer.getColumn(lineInfo, node.offset),
82
+ message:
83
+ 'TLS certificate validation bypassed - onBadCertificate returns true',
84
+ ));
85
+ }
86
+ }
87
+
88
+ // Check for allowInvalidCertificates: true (common in http packages)
89
+ if (name == 'allowinvalidcertificates' && value == 'true') {
90
+ violations.add(analyzer.createViolation(
91
+ filePath: filePath,
92
+ line: analyzer.getLine(lineInfo, node.offset),
93
+ column: analyzer.getColumn(lineInfo, node.offset),
94
+ message:
95
+ 'allowInvalidCertificates: true disables certificate validation',
96
+ ));
97
+ }
98
+
99
+ // Check for rejectUnauthorized: false
100
+ if (name == 'rejectunauthorized' && value == 'false') {
101
+ violations.add(analyzer.createViolation(
102
+ filePath: filePath,
103
+ line: analyzer.getLine(lineInfo, node.offset),
104
+ column: analyzer.getColumn(lineInfo, node.offset),
105
+ message:
106
+ 'rejectUnauthorized: false disables TLS certificate validation',
107
+ ));
108
+ }
109
+
110
+ super.visitNamedExpression(node);
111
+ }
112
+
113
+ @override
114
+ void visitAssignmentExpression(AssignmentExpression node) {
115
+ final left = node.leftHandSide.toSource().toLowerCase();
116
+ final right = node.rightHandSide.toSource().toLowerCase();
117
+
118
+ // Check for SecurityContext settings that disable validation
119
+ if (left.contains('securitycontext') &&
120
+ (right.contains('false') || right.contains('=> true'))) {
121
+ violations.add(analyzer.createViolation(
122
+ filePath: filePath,
123
+ line: analyzer.getLine(lineInfo, node.offset),
124
+ column: analyzer.getColumn(lineInfo, node.offset),
125
+ message: 'SecurityContext configured to bypass certificate validation',
126
+ ));
127
+ }
128
+
129
+ super.visitAssignmentExpression(node);
130
+ }
131
+ }
@@ -0,0 +1,155 @@
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
+ /// S040: Session Fixation Protection
10
+ /// Regenerate session ID after authentication
11
+ class S040SessionFixationProtectionAnalyzer extends BaseAnalyzer {
12
+ @override
13
+ String get ruleId => 'S040';
14
+
15
+ // Login/auth method indicators
16
+ static const _authMethods = [
17
+ 'login', 'signin', 'sign_in', 'authenticate', 'auth',
18
+ 'logon', 'log_on', 'signon', 'sign_on',
19
+ ];
20
+
21
+ // Session regeneration methods
22
+ static const _regenerateMethods = [
23
+ 'regenerate', 'regenerateid', 'regenerate_id', 'newsession',
24
+ 'new_session', 'createsession', 'create_session', 'invalidate',
25
+ ];
26
+
27
+ @override
28
+ List<Violation> analyze({
29
+ required CompilationUnit unit,
30
+ required String filePath,
31
+ required Rule rule,
32
+ required LineInfo lineInfo,
33
+ }) {
34
+ final violations = <Violation>[];
35
+ final visitor = _S040Visitor(
36
+ filePath: filePath,
37
+ lineInfo: lineInfo,
38
+ violations: violations,
39
+ analyzer: this,
40
+ );
41
+ unit.accept(visitor);
42
+ return violations;
43
+ }
44
+ }
45
+
46
+ class _S040Visitor extends RecursiveAstVisitor<void> {
47
+ final String filePath;
48
+ final LineInfo lineInfo;
49
+ final List<Violation> violations;
50
+ final S040SessionFixationProtectionAnalyzer analyzer;
51
+
52
+ _S040Visitor({
53
+ required this.filePath,
54
+ required this.lineInfo,
55
+ required this.violations,
56
+ required this.analyzer,
57
+ });
58
+
59
+ @override
60
+ void visitMethodDeclaration(MethodDeclaration node) {
61
+ final methodName = node.name.lexeme.toLowerCase();
62
+
63
+ // Check if this is an authentication method
64
+ bool isAuthMethod = S040SessionFixationProtectionAnalyzer._authMethods
65
+ .any((m) => methodName.contains(m));
66
+
67
+ if (isAuthMethod) {
68
+ final body = node.body;
69
+ if (body != null) {
70
+ final bodySource = body.toSource().toLowerCase();
71
+
72
+ // Skip helper/stub functions (very short bodies or just return statements)
73
+ // These are typically: `=> true;`, `=> null;`, `async => ...;`
74
+ if (body is ExpressionFunctionBody) {
75
+ // Arrow function - likely a helper, skip it
76
+ super.visitMethodDeclaration(node);
77
+ return;
78
+ }
79
+
80
+ // Skip if body is too short (less than 50 chars typically means stub)
81
+ if (bodySource.length < 50) {
82
+ super.visitMethodDeclaration(node);
83
+ return;
84
+ }
85
+
86
+ // Skip if method doesn't actually handle session (no session param/usage)
87
+ if (!bodySource.contains('session')) {
88
+ super.visitMethodDeclaration(node);
89
+ return;
90
+ }
91
+
92
+ bool hasRegeneration = S040SessionFixationProtectionAnalyzer._regenerateMethods
93
+ .any((m) => bodySource.contains(m));
94
+
95
+ if (!hasRegeneration) {
96
+ violations.add(analyzer.createViolation(
97
+ filePath: filePath,
98
+ line: analyzer.getLine(lineInfo, node.offset),
99
+ column: analyzer.getColumn(lineInfo, node.offset),
100
+ message: 'Session fixation risk - regenerate session ID after successful authentication',
101
+ ));
102
+ }
103
+ }
104
+ }
105
+
106
+ super.visitMethodDeclaration(node);
107
+ }
108
+
109
+ @override
110
+ void visitFunctionDeclaration(FunctionDeclaration node) {
111
+ final funcName = node.name.lexeme.toLowerCase();
112
+
113
+ bool isAuthFunc = S040SessionFixationProtectionAnalyzer._authMethods
114
+ .any((m) => funcName.contains(m));
115
+
116
+ if (isAuthFunc) {
117
+ final body = node.functionExpression.body;
118
+ if (body != null) {
119
+ final bodySource = body.toSource().toLowerCase();
120
+
121
+ // Skip arrow functions (helper functions)
122
+ if (body is ExpressionFunctionBody) {
123
+ super.visitFunctionDeclaration(node);
124
+ return;
125
+ }
126
+
127
+ // Skip short bodies
128
+ if (bodySource.length < 50) {
129
+ super.visitFunctionDeclaration(node);
130
+ return;
131
+ }
132
+
133
+ // Skip if function doesn't handle session
134
+ if (!bodySource.contains('session')) {
135
+ super.visitFunctionDeclaration(node);
136
+ return;
137
+ }
138
+
139
+ bool hasRegeneration = S040SessionFixationProtectionAnalyzer._regenerateMethods
140
+ .any((m) => bodySource.contains(m));
141
+
142
+ if (!hasRegeneration) {
143
+ violations.add(analyzer.createViolation(
144
+ filePath: filePath,
145
+ line: analyzer.getLine(lineInfo, node.offset),
146
+ column: analyzer.getColumn(lineInfo, node.offset),
147
+ message: 'Session fixation risk - regenerate session after login',
148
+ ));
149
+ }
150
+ }
151
+ }
152
+
153
+ super.visitFunctionDeclaration(node);
154
+ }
155
+ }