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