@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,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
|
+
}
|