@sun-asterisk/sunlint 1.3.43 → 1.3.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart_analyzer/README.md +226 -0
- package/dart_analyzer/analysis_options.yaml +66 -0
- package/dart_analyzer/bin/sunlint-dart-macos +0 -0
- package/dart_analyzer/bin/sunlint_dart_analyzer.dart +124 -0
- package/dart_analyzer/lib/analyzer_service.dart +625 -0
- package/dart_analyzer/lib/json_rpc_server.dart +275 -0
- package/dart_analyzer/lib/models/rule.dart +67 -0
- package/dart_analyzer/lib/models/symbol_table.dart +607 -0
- package/dart_analyzer/lib/models/violation.dart +69 -0
- package/dart_analyzer/lib/rules/base_analyzer.dart +52 -0
- package/dart_analyzer/lib/rules/common/C002_no_duplicate_code.dart +344 -0
- package/dart_analyzer/lib/rules/common/C003_no_vague_abbreviations.dart +318 -0
- package/dart_analyzer/lib/rules/common/C006_function_naming.dart +219 -0
- package/dart_analyzer/lib/rules/common/C008_variable_declaration_locality.dart +205 -0
- package/dart_analyzer/lib/rules/common/C010_limit_block_nesting.dart +162 -0
- package/dart_analyzer/lib/rules/common/C012_command_query_separation.dart +214 -0
- package/dart_analyzer/lib/rules/common/C013_no_dead_code.dart +225 -0
- package/dart_analyzer/lib/rules/common/C014_dependency_injection.dart +249 -0
- package/dart_analyzer/lib/rules/common/C017_constructor_logic.dart +158 -0
- package/dart_analyzer/lib/rules/common/C018_no_throw_generic_error.dart +141 -0
- package/dart_analyzer/lib/rules/common/C019_log_level_usage.dart +165 -0
- package/dart_analyzer/lib/rules/common/C020_unused_imports.dart +128 -0
- package/dart_analyzer/lib/rules/common/C021_import_organization.dart +86 -0
- package/dart_analyzer/lib/rules/common/C023_no_duplicate_variable.dart +112 -0
- package/dart_analyzer/lib/rules/common/C024_no_scatter_hardcoded_constants.dart +79 -0
- package/dart_analyzer/lib/rules/common/C029_catch_block_logging.dart +81 -0
- package/dart_analyzer/lib/rules/common/C030_use_custom_error_classes.dart +77 -0
- package/dart_analyzer/lib/rules/common/C031_validation_separation.dart +90 -0
- package/dart_analyzer/lib/rules/common/C033_separate_service_repository.dart +80 -0
- package/dart_analyzer/lib/rules/common/C035_error_logging_context.dart +148 -0
- package/dart_analyzer/lib/rules/common/C040_centralized_validation.dart +84 -0
- package/dart_analyzer/lib/rules/common/C041_no_sensitive_hardcode.dart +103 -0
- package/dart_analyzer/lib/rules/common/C042_boolean_name_prefix.dart +105 -0
- package/dart_analyzer/lib/rules/common/C043_no_console_or_print.dart +101 -0
- package/dart_analyzer/lib/rules/common/C047_no_duplicate_retry_logic.dart +94 -0
- package/dart_analyzer/lib/rules/common/C048_no_bypass_architectural_layers.dart +132 -0
- package/dart_analyzer/lib/rules/common/C052_parsing_or_data_transformation.dart +95 -0
- package/dart_analyzer/lib/rules/common/C060_no_override_superclass.dart +81 -0
- package/dart_analyzer/lib/rules/common/C065_one_behavior_per_test.dart +83 -0
- package/dart_analyzer/lib/rules/common/C067_no_hardcoded_config.dart +89 -0
- package/dart_analyzer/lib/rules/common/C070_no_real_time_tests.dart +99 -0
- package/dart_analyzer/lib/rules/common/C072_single_test_behavior.dart +78 -0
- package/dart_analyzer/lib/rules/common/C073_validate_required_config_on_startup.dart +82 -0
- package/dart_analyzer/lib/rules/common/C075_explicit_return_types.dart +85 -0
- package/dart_analyzer/lib/rules/common/C076_explicit_function_types.dart +104 -0
- package/dart_analyzer/lib/rules/dart/D001_recommended_lint_rules.dart +309 -0
- package/dart_analyzer/lib/rules/dart/D002_dispose_resources.dart +338 -0
- package/dart_analyzer/lib/rules/dart/D003_prefer_widgets_over_methods.dart +273 -0
- package/dart_analyzer/lib/rules/dart/D004_avoid_shrinkwrap_listview.dart +154 -0
- package/dart_analyzer/lib/rules/dart/D005_limit_widget_nesting.dart +265 -0
- package/dart_analyzer/lib/rules/dart/D006_prefer_extracting_large_callbacks.dart +135 -0
- package/dart_analyzer/lib/rules/dart/D007_prefer_init_first_dispose_last.dart +150 -0
- package/dart_analyzer/lib/rules/dart/D008_avoid_long_functions.dart +394 -0
- package/dart_analyzer/lib/rules/dart/D009_limit_function_parameters.dart +179 -0
- package/dart_analyzer/lib/rules/dart/D010_limit_cyclomatic_complexity.dart +257 -0
- package/dart_analyzer/lib/rules/dart/D011_prefer_named_parameters.dart +152 -0
- package/dart_analyzer/lib/rules/dart/D012_prefer_named_boolean_parameters.dart +156 -0
- package/dart_analyzer/lib/rules/dart/D013_single_public_class.dart +246 -0
- package/dart_analyzer/lib/rules/dart/D014_unsafe_collection_access.dart +202 -0
- package/dart_analyzer/lib/rules/dart/D015_copywith_all_parameters.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D016_project_should_have_tests.dart +134 -0
- package/dart_analyzer/lib/rules/dart/D017_pubspec_dependencies_review.dart +187 -0
- package/dart_analyzer/lib/rules/dart/D018_remove_commented_code.dart +196 -0
- package/dart_analyzer/lib/rules/dart/D019_avoid_single_child_multi_child_widget.dart +161 -0
- package/dart_analyzer/lib/rules/dart/D020_limit_if_else_branches.dart +125 -0
- package/dart_analyzer/lib/rules/dart/D021_avoid_negated_boolean_checks.dart +227 -0
- package/dart_analyzer/lib/rules/dart/D022_use_setstate_correctly.dart +269 -0
- package/dart_analyzer/lib/rules/dart/D023_avoid_unnecessary_method_overrides.dart +191 -0
- package/dart_analyzer/lib/rules/dart/D024_avoid_unnecessary_stateful_widget.dart +194 -0
- package/dart_analyzer/lib/rules/dart/D025_avoid_nested_conditional_expressions.dart +90 -0
- package/dart_analyzer/lib/rules/security/S001_backend_auth_communications.dart +155 -0
- package/dart_analyzer/lib/rules/security/S002_os_command_injection.dart +159 -0
- package/dart_analyzer/lib/rules/security/S003_open_redirect_protection.dart +208 -0
- package/dart_analyzer/lib/rules/security/S004_sensitive_data_logging.dart +391 -0
- package/dart_analyzer/lib/rules/security/S005_trusted_service_authorization.dart +182 -0
- package/dart_analyzer/lib/rules/security/S006_no_default_credentials.dart +208 -0
- package/dart_analyzer/lib/rules/security/S007_output_encoding.dart +224 -0
- package/dart_analyzer/lib/rules/security/S008_svg_content_sanitization.dart +211 -0
- package/dart_analyzer/lib/rules/security/S009_no_insecure_encryption.dart +160 -0
- package/dart_analyzer/lib/rules/security/S010_use_csprng.dart +184 -0
- package/dart_analyzer/lib/rules/security/S011_ech_tls_config.dart +175 -0
- package/dart_analyzer/lib/rules/security/S012_hardcoded_secrets.dart +255 -0
- package/dart_analyzer/lib/rules/security/S013_tls_enforcement.dart +148 -0
- package/dart_analyzer/lib/rules/security/S014_tls_version_enforcement.dart +117 -0
- package/dart_analyzer/lib/rules/security/S015_insecure_tls_certificate.dart +315 -0
- package/dart_analyzer/lib/rules/security/S016_no_sensitive_querystring.dart +244 -0
- package/dart_analyzer/lib/rules/security/S017_use_parameterized_queries.dart +191 -0
- package/dart_analyzer/lib/rules/security/S018_no_sensitive_browser_storage.dart +175 -0
- package/dart_analyzer/lib/rules/security/S019_smtp_injection_protection.dart +166 -0
- package/dart_analyzer/lib/rules/security/S020_no_eval_dynamic_code.dart +149 -0
- package/dart_analyzer/lib/rules/security/S021_referrer_policy.dart +146 -0
- package/dart_analyzer/lib/rules/security/S022_escape_output_context.dart +111 -0
- package/dart_analyzer/lib/rules/security/S023_no_json_injection.dart +550 -0
- package/dart_analyzer/lib/rules/security/S024_xpath_xxe_protection.dart +299 -0
- package/dart_analyzer/lib/rules/security/S025_server_side_validation.dart +140 -0
- package/dart_analyzer/lib/rules/security/S026_tls_all_connections.dart +196 -0
- package/dart_analyzer/lib/rules/security/S027_mtls_certificate_validation.dart +195 -0
- package/dart_analyzer/lib/rules/security/S028_file_upload_size_limits.dart +186 -0
- package/dart_analyzer/lib/rules/security/S029_csrf_protection.dart +171 -0
- package/dart_analyzer/lib/rules/security/S030_directory_browsing_protection.dart +144 -0
- package/dart_analyzer/lib/rules/security/S031_secure_session_cookies.dart +118 -0
- package/dart_analyzer/lib/rules/security/S032_httponly_session_cookies.dart +114 -0
- package/dart_analyzer/lib/rules/security/S033_samesite_session_cookies.dart +120 -0
- package/dart_analyzer/lib/rules/security/S034_host_prefix_session_cookies.dart +160 -0
- package/dart_analyzer/lib/rules/security/S035_separate_app_hostnames.dart +117 -0
- package/dart_analyzer/lib/rules/security/S036_lfi_rfi_protection.dart +188 -0
- package/dart_analyzer/lib/rules/security/S037_cache_headers.dart +113 -0
- package/dart_analyzer/lib/rules/security/S038_no_version_headers.dart +114 -0
- package/dart_analyzer/lib/rules/security/S039_tls_certificate_validation.dart +131 -0
- package/dart_analyzer/lib/rules/security/S040_session_fixation_protection.dart +155 -0
- package/dart_analyzer/lib/rules/security/S041_session_token_invalidation.dart +201 -0
- package/dart_analyzer/lib/rules/security/S042_require_re_authentication_for_long_lived.dart +158 -0
- package/dart_analyzer/lib/rules/security/S043_password_changes_invalidate_all_sessions.dart +88 -0
- package/dart_analyzer/lib/rules/security/S044_re_authentication_required.dart +119 -0
- package/dart_analyzer/lib/rules/security/S045_brute_force_protection.dart +253 -0
- package/dart_analyzer/lib/rules/security/S046_jwt_algorithm_allowlist.dart +113 -0
- package/dart_analyzer/lib/rules/security/S047_oauth_pkce_protection.dart +124 -0
- package/dart_analyzer/lib/rules/security/S048_oauth_redirect_uri_validation.dart +134 -0
- package/dart_analyzer/lib/rules/security/S049_short_validity_tokens.dart +145 -0
- package/dart_analyzer/lib/rules/security/S050_reference_tokens_entropy.dart +234 -0
- package/dart_analyzer/lib/rules/security/S051_password_length_policy.dart +171 -0
- package/dart_analyzer/lib/rules/security/S052_weak_otp_entropy.dart +107 -0
- package/dart_analyzer/lib/rules/security/S053_generic_error_messages.dart +159 -0
- package/dart_analyzer/lib/rules/security/S054_no_default_accounts.dart +141 -0
- package/dart_analyzer/lib/rules/security/S055_content_type_validation.dart +324 -0
- package/dart_analyzer/lib/rules/security/S056_log_injection_protection.dart +119 -0
- package/dart_analyzer/lib/rules/security/S057_utc_logging.dart +114 -0
- package/dart_analyzer/lib/rules/security/S058_no_ssrf.dart +175 -0
- package/dart_analyzer/lib/rules/security/S059_disable_debug_mode.dart +172 -0
- package/dart_analyzer/lib/rules/security/S060_password_minimum_length.dart +170 -0
- package/dart_analyzer/lib/symbol_table_extractor.dart +510 -0
- package/dart_analyzer/lib/utils/common_utils.dart +26 -0
- package/dart_analyzer/pubspec.lock +557 -0
- package/dart_analyzer/pubspec.yaml +39 -0
- package/dart_analyzer/test/fixtures/complex_code.dart +95 -0
- package/docs/GENERATED_FILE_HANDLING_SUMMARY.md +2 -2
- package/package.json +3 -2
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import 'package:analyzer/dart/ast/ast.dart';
|
|
2
|
+
import 'package:analyzer/dart/ast/visitor.dart';
|
|
3
|
+
import 'package:analyzer/source/line_info.dart';
|
|
4
|
+
|
|
5
|
+
import '../../models/rule.dart';
|
|
6
|
+
import '../../models/violation.dart';
|
|
7
|
+
import '../base_analyzer.dart';
|
|
8
|
+
|
|
9
|
+
/// S056: Log Injection Protection
|
|
10
|
+
/// Sanitize user input before logging to prevent log injection
|
|
11
|
+
class S056LogInjectionProtectionAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S056';
|
|
14
|
+
|
|
15
|
+
// Logging methods
|
|
16
|
+
static const _logMethods = [
|
|
17
|
+
'log', 'info', 'debug', 'warn', 'warning', 'error', 'trace',
|
|
18
|
+
'verbose', 'print', 'writeln',
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// User input indicators - be specific to avoid false positives
|
|
22
|
+
static const _userInputPatterns = [
|
|
23
|
+
'request.body', 'request.query', 'request.params', 'request.input',
|
|
24
|
+
'req.body', 'req.query', 'req.params', 'req.input',
|
|
25
|
+
'userinput', 'user_input', 'userdata', 'user_data',
|
|
26
|
+
'forminput', 'form_input', 'formdata', 'form_data',
|
|
27
|
+
'rawbody', 'raw_body', 'rawinput', 'raw_input',
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// Safe patterns that should be skipped
|
|
31
|
+
static const _safePatterns = [
|
|
32
|
+
'requestoptions', 'request_options', 'response',
|
|
33
|
+
'statuscode', 'status_code', 'path', 'url', 'method',
|
|
34
|
+
'contenttype', 'content_type', 'headers',
|
|
35
|
+
// Controller values - sanitized by framework
|
|
36
|
+
'controller', '.text', '.value',
|
|
37
|
+
// Class/widget names containing "forminput" but not actual user input
|
|
38
|
+
'forminputbirthday', 'forminputname', 'forminputemail', 'forminputphone',
|
|
39
|
+
'forminputaddress', 'forminputcard', 'forminputpassword',
|
|
40
|
+
];
|
|
41
|
+
|
|
42
|
+
@override
|
|
43
|
+
List<Violation> analyze({
|
|
44
|
+
required CompilationUnit unit,
|
|
45
|
+
required String filePath,
|
|
46
|
+
required Rule rule,
|
|
47
|
+
required LineInfo lineInfo,
|
|
48
|
+
}) {
|
|
49
|
+
final violations = <Violation>[];
|
|
50
|
+
final visitor = _S056Visitor(
|
|
51
|
+
filePath: filePath,
|
|
52
|
+
lineInfo: lineInfo,
|
|
53
|
+
violations: violations,
|
|
54
|
+
analyzer: this,
|
|
55
|
+
);
|
|
56
|
+
unit.accept(visitor);
|
|
57
|
+
return violations;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
class _S056Visitor extends RecursiveAstVisitor<void> {
|
|
62
|
+
final String filePath;
|
|
63
|
+
final LineInfo lineInfo;
|
|
64
|
+
final List<Violation> violations;
|
|
65
|
+
final S056LogInjectionProtectionAnalyzer analyzer;
|
|
66
|
+
|
|
67
|
+
_S056Visitor({
|
|
68
|
+
required this.filePath,
|
|
69
|
+
required this.lineInfo,
|
|
70
|
+
required this.violations,
|
|
71
|
+
required this.analyzer,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
@override
|
|
75
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
76
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
77
|
+
|
|
78
|
+
bool isLogMethod = S056LogInjectionProtectionAnalyzer._logMethods
|
|
79
|
+
.any((m) => methodName == m || methodName.endsWith(m));
|
|
80
|
+
|
|
81
|
+
if (isLogMethod) {
|
|
82
|
+
for (final arg in node.argumentList.arguments) {
|
|
83
|
+
final argSource = arg.toSource().toLowerCase().replaceAll('_', '');
|
|
84
|
+
|
|
85
|
+
// Skip safe patterns (response metadata, etc.)
|
|
86
|
+
bool isSafe = S056LogInjectionProtectionAnalyzer._safePatterns
|
|
87
|
+
.any((p) => argSource.contains(p.replaceAll('_', '')));
|
|
88
|
+
|
|
89
|
+
if (isSafe) {
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Check if logging user input
|
|
94
|
+
bool hasUserInput = S056LogInjectionProtectionAnalyzer._userInputPatterns
|
|
95
|
+
.any((p) => argSource.contains(p.replaceAll('_', '')));
|
|
96
|
+
|
|
97
|
+
if (hasUserInput) {
|
|
98
|
+
// Check for sanitization
|
|
99
|
+
bool isSanitized = argSource.contains('sanitize') ||
|
|
100
|
+
argSource.contains('escape') ||
|
|
101
|
+
argSource.contains('encode') ||
|
|
102
|
+
argSource.contains('replace');
|
|
103
|
+
|
|
104
|
+
if (!isSanitized && (arg is StringInterpolation || arg is SimpleIdentifier)) {
|
|
105
|
+
violations.add(analyzer.createViolation(
|
|
106
|
+
filePath: filePath,
|
|
107
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
108
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
109
|
+
message: 'Log injection risk - sanitize user input before logging',
|
|
110
|
+
));
|
|
111
|
+
break;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
super.visitMethodInvocation(node);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
@@ -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
|
+
/// S057: UTC Logging
|
|
10
|
+
/// Use UTC timestamps in logs for consistency across time zones
|
|
11
|
+
class S057UtcLoggingAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S057';
|
|
14
|
+
|
|
15
|
+
// Logging methods
|
|
16
|
+
static const _logMethods = [
|
|
17
|
+
'log', 'info', 'debug', 'warn', 'warning', 'error', 'trace',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
@override
|
|
21
|
+
List<Violation> analyze({
|
|
22
|
+
required CompilationUnit unit,
|
|
23
|
+
required String filePath,
|
|
24
|
+
required Rule rule,
|
|
25
|
+
required LineInfo lineInfo,
|
|
26
|
+
}) {
|
|
27
|
+
final violations = <Violation>[];
|
|
28
|
+
final visitor = _S057Visitor(
|
|
29
|
+
filePath: filePath,
|
|
30
|
+
lineInfo: lineInfo,
|
|
31
|
+
violations: violations,
|
|
32
|
+
analyzer: this,
|
|
33
|
+
);
|
|
34
|
+
unit.accept(visitor);
|
|
35
|
+
return violations;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
class _S057Visitor extends RecursiveAstVisitor<void> {
|
|
40
|
+
final String filePath;
|
|
41
|
+
final LineInfo lineInfo;
|
|
42
|
+
final List<Violation> violations;
|
|
43
|
+
final S057UtcLoggingAnalyzer analyzer;
|
|
44
|
+
|
|
45
|
+
_S057Visitor({
|
|
46
|
+
required this.filePath,
|
|
47
|
+
required this.lineInfo,
|
|
48
|
+
required this.violations,
|
|
49
|
+
required this.analyzer,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
@override
|
|
53
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
54
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
55
|
+
|
|
56
|
+
bool isLogMethod = S057UtcLoggingAnalyzer._logMethods
|
|
57
|
+
.any((m) => methodName == m);
|
|
58
|
+
|
|
59
|
+
if (isLogMethod) {
|
|
60
|
+
final source = node.toSource().toLowerCase();
|
|
61
|
+
|
|
62
|
+
// Check for DateTime.now() without .toUtc()
|
|
63
|
+
if (source.contains('datetime.now()') && !source.contains('.toutc()')) {
|
|
64
|
+
violations.add(analyzer.createViolation(
|
|
65
|
+
filePath: filePath,
|
|
66
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
67
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
68
|
+
message: 'Use UTC timestamps in logs - call .toUtc() on DateTime',
|
|
69
|
+
));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Check for local time methods
|
|
73
|
+
if (source.contains('tolocal()') || source.contains('localtime')) {
|
|
74
|
+
violations.add(analyzer.createViolation(
|
|
75
|
+
filePath: filePath,
|
|
76
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
77
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
78
|
+
message: 'Avoid local time in logs - use UTC for consistency',
|
|
79
|
+
));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
super.visitMethodInvocation(node);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
@override
|
|
87
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
88
|
+
final varName = node.name.lexeme.toLowerCase();
|
|
89
|
+
final initializer = node.initializer;
|
|
90
|
+
|
|
91
|
+
// Only check for LOG timestamp variables, not general timestamps
|
|
92
|
+
// uploadTimestamp, activityTime, etc. are NOT logging timestamps
|
|
93
|
+
// Must have 'log' in name to be considered a logging timestamp
|
|
94
|
+
bool isLogTimestamp = (varName.contains('log') && varName.contains('time')) ||
|
|
95
|
+
(varName.contains('log') && varName.contains('timestamp')) ||
|
|
96
|
+
varName == 'logtime' || varName == 'log_time' ||
|
|
97
|
+
varName == 'logtimestamp' || varName == 'log_timestamp';
|
|
98
|
+
|
|
99
|
+
if (isLogTimestamp && initializer != null) {
|
|
100
|
+
final initSource = initializer.toSource().toLowerCase();
|
|
101
|
+
|
|
102
|
+
if (initSource.contains('datetime.now()') && !initSource.contains('.toutc()')) {
|
|
103
|
+
violations.add(analyzer.createViolation(
|
|
104
|
+
filePath: filePath,
|
|
105
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
106
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
107
|
+
message: 'Log timestamp should use UTC',
|
|
108
|
+
));
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
super.visitVariableDeclaration(node);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
@@ -0,0 +1,175 @@
|
|
|
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
|
+
/// S058: No SSRF (Server-Side Request Forgery)
|
|
10
|
+
/// Prevent SSRF attacks by validating URLs from user input
|
|
11
|
+
class S058NoSsrfAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S058';
|
|
14
|
+
|
|
15
|
+
// HTTP request methods - must be actual HTTP client methods
|
|
16
|
+
static const _httpMethods = [
|
|
17
|
+
'fetch', 'httprequest', 'http_request',
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
// HTTP client names - check target/receiver
|
|
21
|
+
static const _httpClients = [
|
|
22
|
+
'http', 'dio', 'client', 'httpclient', 'http_client',
|
|
23
|
+
'request', 'axios', 'fetch',
|
|
24
|
+
];
|
|
25
|
+
|
|
26
|
+
// User input indicators - be more specific to avoid false positives
|
|
27
|
+
static const _userInputPatterns = [
|
|
28
|
+
'request.body', 'request.query', 'request.params', 'request.url',
|
|
29
|
+
'req.body', 'req.query', 'req.params', 'req.url',
|
|
30
|
+
'userinput', 'user_input', 'userurl', 'user_url',
|
|
31
|
+
'externalurl', 'external_url', 'remoteurl', 'remote_url',
|
|
32
|
+
'targeturl', 'target_url',
|
|
33
|
+
];
|
|
34
|
+
|
|
35
|
+
// Variable names that suggest user input
|
|
36
|
+
static final _userInputVarPatterns = [
|
|
37
|
+
RegExp(r'\buserurl\b', caseSensitive: false),
|
|
38
|
+
RegExp(r'\buser_url\b', caseSensitive: false),
|
|
39
|
+
RegExp(r'\btargeturl\b', caseSensitive: false),
|
|
40
|
+
RegExp(r'\btarget_url\b', caseSensitive: false),
|
|
41
|
+
RegExp(r'\bexternalurl\b', caseSensitive: false),
|
|
42
|
+
RegExp(r'\bexternal_url\b', caseSensitive: false),
|
|
43
|
+
];
|
|
44
|
+
|
|
45
|
+
@override
|
|
46
|
+
List<Violation> analyze({
|
|
47
|
+
required CompilationUnit unit,
|
|
48
|
+
required String filePath,
|
|
49
|
+
required Rule rule,
|
|
50
|
+
required LineInfo lineInfo,
|
|
51
|
+
}) {
|
|
52
|
+
final violations = <Violation>[];
|
|
53
|
+
final visitor = _S058Visitor(
|
|
54
|
+
filePath: filePath,
|
|
55
|
+
lineInfo: lineInfo,
|
|
56
|
+
violations: violations,
|
|
57
|
+
analyzer: this,
|
|
58
|
+
);
|
|
59
|
+
unit.accept(visitor);
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
class _S058Visitor extends RecursiveAstVisitor<void> {
|
|
65
|
+
final String filePath;
|
|
66
|
+
final LineInfo lineInfo;
|
|
67
|
+
final List<Violation> violations;
|
|
68
|
+
final S058NoSsrfAnalyzer analyzer;
|
|
69
|
+
|
|
70
|
+
_S058Visitor({
|
|
71
|
+
required this.filePath,
|
|
72
|
+
required this.lineInfo,
|
|
73
|
+
required this.violations,
|
|
74
|
+
required this.analyzer,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
@override
|
|
78
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
79
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
80
|
+
final source = node.toSource().toLowerCase();
|
|
81
|
+
|
|
82
|
+
// Check if this is an HTTP client method call
|
|
83
|
+
bool isHttpRequest = S058NoSsrfAnalyzer._httpMethods
|
|
84
|
+
.any((m) => methodName == m);
|
|
85
|
+
|
|
86
|
+
// Check target/receiver for HTTP client usage
|
|
87
|
+
final target = node.target?.toSource().toLowerCase() ?? '';
|
|
88
|
+
bool isHttpClient = S058NoSsrfAnalyzer._httpClients
|
|
89
|
+
.any((c) => target.contains(c));
|
|
90
|
+
|
|
91
|
+
// Check for common HTTP patterns: dio.get(), http.post(), client.request()
|
|
92
|
+
bool isHttpOp = (methodName == 'get' || methodName == 'post' ||
|
|
93
|
+
methodName == 'put' || methodName == 'patch' ||
|
|
94
|
+
methodName == 'delete' || methodName == 'head') && isHttpClient;
|
|
95
|
+
|
|
96
|
+
if (isHttpRequest || isHttpOp) {
|
|
97
|
+
// Check if URL comes from user input
|
|
98
|
+
for (final arg in node.argumentList.arguments) {
|
|
99
|
+
final argSource = arg.toSource().toLowerCase();
|
|
100
|
+
|
|
101
|
+
// Check against string patterns
|
|
102
|
+
bool isUserInput = S058NoSsrfAnalyzer._userInputPatterns
|
|
103
|
+
.any((p) => argSource.contains(p));
|
|
104
|
+
|
|
105
|
+
// Check against variable name patterns (e.g., userUrl parameter)
|
|
106
|
+
if (!isUserInput) {
|
|
107
|
+
isUserInput = S058NoSsrfAnalyzer._userInputVarPatterns
|
|
108
|
+
.any((p) => p.hasMatch(argSource));
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (isUserInput) {
|
|
112
|
+
// Check for URL validation
|
|
113
|
+
bool hasValidation = argSource.contains('validate') ||
|
|
114
|
+
argSource.contains('whitelist') ||
|
|
115
|
+
argSource.contains('allowlist') ||
|
|
116
|
+
argSource.contains('isallowed') ||
|
|
117
|
+
argSource.contains('is_allowed');
|
|
118
|
+
|
|
119
|
+
if (!hasValidation) {
|
|
120
|
+
violations.add(analyzer.createViolation(
|
|
121
|
+
filePath: filePath,
|
|
122
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
123
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
124
|
+
message: 'SSRF risk - validate/whitelist URLs from user input',
|
|
125
|
+
));
|
|
126
|
+
}
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
super.visitMethodInvocation(node);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
@override
|
|
136
|
+
void visitInstanceCreationExpression(InstanceCreationExpression node) {
|
|
137
|
+
final typeName = node.constructorName.type.name2.lexeme;
|
|
138
|
+
|
|
139
|
+
// Check for Uri construction with user input
|
|
140
|
+
if (typeName == 'Uri') {
|
|
141
|
+
for (final arg in node.argumentList.arguments) {
|
|
142
|
+
final argSource = arg.toSource().toLowerCase();
|
|
143
|
+
|
|
144
|
+
// Check against string patterns
|
|
145
|
+
bool isUserInput = S058NoSsrfAnalyzer._userInputPatterns
|
|
146
|
+
.any((p) => argSource.contains(p));
|
|
147
|
+
|
|
148
|
+
// Check against variable name patterns
|
|
149
|
+
if (!isUserInput) {
|
|
150
|
+
isUserInput = S058NoSsrfAnalyzer._userInputVarPatterns
|
|
151
|
+
.any((p) => p.hasMatch(argSource));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (isUserInput) {
|
|
155
|
+
// Check for validation
|
|
156
|
+
bool hasValidation = argSource.contains('validate') ||
|
|
157
|
+
argSource.contains('whitelist') ||
|
|
158
|
+
argSource.contains('allowlist');
|
|
159
|
+
|
|
160
|
+
if (!hasValidation) {
|
|
161
|
+
violations.add(analyzer.createViolation(
|
|
162
|
+
filePath: filePath,
|
|
163
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
164
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
165
|
+
message: 'SSRF risk - Uri created from user input without validation',
|
|
166
|
+
));
|
|
167
|
+
}
|
|
168
|
+
break;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
super.visitInstanceCreationExpression(node);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
@@ -0,0 +1,172 @@
|
|
|
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
|
+
/// S059: Disable debug modes in production environments
|
|
10
|
+
/// Detect hardcoded debug modes and development features
|
|
11
|
+
class S059DisableDebugModeAnalyzer extends BaseAnalyzer {
|
|
12
|
+
@override
|
|
13
|
+
String get ruleId => 'S059';
|
|
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 = _S059Visitor(
|
|
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 _S059Visitor extends RecursiveAstVisitor<void> {
|
|
35
|
+
final String filePath;
|
|
36
|
+
final LineInfo lineInfo;
|
|
37
|
+
final List<Violation> violations;
|
|
38
|
+
final S059DisableDebugModeAnalyzer analyzer;
|
|
39
|
+
|
|
40
|
+
_S059Visitor({
|
|
41
|
+
required this.filePath,
|
|
42
|
+
required this.lineInfo,
|
|
43
|
+
required this.violations,
|
|
44
|
+
required this.analyzer,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
@override
|
|
48
|
+
void visitVariableDeclaration(VariableDeclaration node) {
|
|
49
|
+
final name = node.name.lexeme.toLowerCase();
|
|
50
|
+
final initializer = node.initializer;
|
|
51
|
+
|
|
52
|
+
// Check for debug flags hardcoded to true
|
|
53
|
+
if ((name == 'debug' ||
|
|
54
|
+
name == 'isdebug' ||
|
|
55
|
+
name == 'debugmode' ||
|
|
56
|
+
name == 'is_debug') &&
|
|
57
|
+
initializer != null) {
|
|
58
|
+
final value = initializer.toSource().toLowerCase();
|
|
59
|
+
if (value == 'true') {
|
|
60
|
+
violations.add(analyzer.createViolation(
|
|
61
|
+
filePath: filePath,
|
|
62
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
63
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
64
|
+
message: 'DEBUG hardcoded to true - use environment variable',
|
|
65
|
+
));
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// Check for verbose/logging flags
|
|
70
|
+
if ((name == 'verbose' || name == 'enablelogging') &&
|
|
71
|
+
initializer != null) {
|
|
72
|
+
final value = initializer.toSource().toLowerCase();
|
|
73
|
+
if (value == 'true') {
|
|
74
|
+
violations.add(analyzer.createViolation(
|
|
75
|
+
filePath: filePath,
|
|
76
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
77
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
78
|
+
message:
|
|
79
|
+
'Verbose logging hardcoded to true - disable in production',
|
|
80
|
+
));
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
super.visitVariableDeclaration(node);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
@override
|
|
88
|
+
void visitNamedExpression(NamedExpression node) {
|
|
89
|
+
final name = node.name.label.name.toLowerCase();
|
|
90
|
+
final value = node.expression.toSource().toLowerCase();
|
|
91
|
+
|
|
92
|
+
// Check for debugShowCheckedModeBanner (Flutter)
|
|
93
|
+
if (name == 'debugshowcheckedmodebanner' && value == 'true') {
|
|
94
|
+
violations.add(analyzer.createViolation(
|
|
95
|
+
filePath: filePath,
|
|
96
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
97
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
98
|
+
message:
|
|
99
|
+
'debugShowCheckedModeBanner: true - set to false in production',
|
|
100
|
+
));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Check for showPerformanceOverlay
|
|
104
|
+
if (name == 'showperformanceoverlay' && value == 'true') {
|
|
105
|
+
violations.add(analyzer.createViolation(
|
|
106
|
+
filePath: filePath,
|
|
107
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
108
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
109
|
+
message: 'showPerformanceOverlay: true - disable in production',
|
|
110
|
+
));
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// Check for debugPaintSizeEnabled
|
|
114
|
+
if (name == 'debugpaintsizeenabled' && value == 'true') {
|
|
115
|
+
violations.add(analyzer.createViolation(
|
|
116
|
+
filePath: filePath,
|
|
117
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
118
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
119
|
+
message: 'debugPaintSizeEnabled: true - disable in production',
|
|
120
|
+
));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Check for checkerboardRasterCacheImages
|
|
124
|
+
if (name == 'checkerboardrastercacheimages' && value == 'true') {
|
|
125
|
+
violations.add(analyzer.createViolation(
|
|
126
|
+
filePath: filePath,
|
|
127
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
128
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
129
|
+
message:
|
|
130
|
+
'checkerboardRasterCacheImages: true - disable in production',
|
|
131
|
+
));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
super.visitNamedExpression(node);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
@override
|
|
138
|
+
void visitAssignmentExpression(AssignmentExpression node) {
|
|
139
|
+
final left = node.leftHandSide.toSource().toLowerCase();
|
|
140
|
+
final right = node.rightHandSide.toSource().toLowerCase();
|
|
141
|
+
|
|
142
|
+
// Check for debug assignments
|
|
143
|
+
if ((left.contains('debug') || left.contains('isdebug')) &&
|
|
144
|
+
right == 'true') {
|
|
145
|
+
violations.add(analyzer.createViolation(
|
|
146
|
+
filePath: filePath,
|
|
147
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
148
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
149
|
+
message: 'Debug mode enabled - use environment check for production',
|
|
150
|
+
));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
super.visitAssignmentExpression(node);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
@override
|
|
157
|
+
void visitMethodInvocation(MethodInvocation node) {
|
|
158
|
+
final methodName = node.methodName.name.toLowerCase();
|
|
159
|
+
|
|
160
|
+
// Check for debugPrint in production code
|
|
161
|
+
if (methodName == 'debugprint') {
|
|
162
|
+
violations.add(analyzer.createViolation(
|
|
163
|
+
filePath: filePath,
|
|
164
|
+
line: analyzer.getLine(lineInfo, node.offset),
|
|
165
|
+
column: analyzer.getColumn(lineInfo, node.offset),
|
|
166
|
+
message: 'debugPrint() found - use proper logging or remove in production',
|
|
167
|
+
));
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
super.visitMethodInvocation(node);
|
|
171
|
+
}
|
|
172
|
+
}
|