ai_root_shield 0.2.0 → 0.4.0

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.
data/exe/ai_root_shield CHANGED
@@ -1,17 +1,30 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative "../lib/ai_root_shield"
5
4
  require "optparse"
6
5
  require "json"
6
+ require_relative "../lib/ai_root_shield"
7
7
 
8
- # CLI interface for AI Root Shield
8
+ # Command line interface for AI Root Shield
9
9
  class AiRootShieldCLI
10
10
  def initialize
11
11
  @options = {
12
- config: {},
13
- output_format: "json",
14
- verbose: false
12
+ format: "json",
13
+ verbose: false,
14
+ threshold: 50,
15
+ enable_root_detection: true,
16
+ enable_emulator_detection: true,
17
+ enable_hooking_detection: true,
18
+ enable_integrity_checks: true,
19
+ enable_network_analysis: true,
20
+ enable_ai_behavioral_analysis: true,
21
+ enable_rasp_protection: false,
22
+ rasp_monitoring_time: 5,
23
+ policy_file: nil,
24
+ enable_certificate_pinning: false,
25
+ enable_proxy_detection: false,
26
+ target_ip: nil,
27
+ target_url: nil
15
28
  }
16
29
  end
17
30
 
@@ -32,8 +45,58 @@ class AiRootShieldCLI
32
45
  end
33
46
 
34
47
  begin
35
- result = AiRootShield.scan_device_with_config(device_logs_path, @options[:config])
48
+ # Configure enterprise policy if provided
49
+ if @options[:policy_file]
50
+ puts "Loading enterprise policy from #{@options[:policy_file]}..." if @options[:verbose]
51
+ AiRootShield.configure_policy(@options[:policy_file])
52
+ end
53
+
54
+ # Configure certificate pinning if enabled
55
+ if @options[:enable_certificate_pinning]
56
+ puts "Configuring certificate pinning..." if @options[:verbose]
57
+ AiRootShield.configure_certificate_pinning
58
+ end
59
+
60
+ # Configure proxy detection if enabled
61
+ if @options[:enable_proxy_detection]
62
+ puts "Configuring proxy detection..." if @options[:verbose]
63
+ AiRootShield.configure_proxy_detection
64
+ end
65
+
66
+ # Start RASP protection if enabled
67
+ if @options[:enable_rasp_protection]
68
+ puts "Starting RASP protection..." if @options[:verbose]
69
+ rasp = AiRootShield.start_rasp_protection(
70
+ enable_real_time_alerts: @options[:verbose],
71
+ protection_interval: 0.5
72
+ )
73
+
74
+ # Set up RASP event logging if verbose
75
+ if @options[:verbose]
76
+ rasp.on_rasp_event do |event|
77
+ puts "[RASP] #{event[:type]}: #{event[:message]}"
78
+ end
79
+ end
80
+
81
+ # Monitor for specified time
82
+ puts "Monitoring with RASP protection for #{@options[:rasp_monitoring_time]} seconds..." if @options[:verbose]
83
+ sleep(@options[:rasp_monitoring_time])
84
+ end
85
+
86
+ result = AiRootShield.scan_device_with_config(device_logs_path, @options)
87
+
88
+ # Add RASP status to result if enabled
89
+ if @options[:enable_rasp_protection] && AiRootShield.rasp_active?
90
+ result[:rasp_status] = AiRootShield.rasp_protection.protection_status
91
+ end
92
+
93
+ # Add security status if verbose
94
+ if @options[:verbose]
95
+ result[:security_status] = AiRootShield.security_status
96
+ end
97
+
36
98
  output_result(result)
99
+
37
100
  rescue AiRootShield::Error => e
38
101
  puts "Error: #{e.message}"
39
102
  exit 1
@@ -41,6 +104,9 @@ class AiRootShieldCLI
41
104
  puts "Unexpected error: #{e.message}"
42
105
  puts e.backtrace if @options[:verbose]
43
106
  exit 1
107
+ ensure
108
+ # Stop RASP protection
109
+ AiRootShield.stop_rasp_protection if @options[:enable_rasp_protection]
44
110
  end
45
111
  end
46
112
 
@@ -54,7 +120,7 @@ class AiRootShieldCLI
54
120
 
55
121
  opts.on("-f", "--format FORMAT", ["json", "text", "summary"],
56
122
  "Output format (json, text, summary)") do |format|
57
- @options[:output_format] = format
123
+ @options[:format] = format
58
124
  end
59
125
 
60
126
  opts.on("-v", "--verbose", "Enable verbose output") do
@@ -63,27 +129,59 @@ class AiRootShieldCLI
63
129
 
64
130
  opts.on("-t", "--threshold SCORE", Integer,
65
131
  "Risk threshold (0-100, default: 50)") do |threshold|
66
- @options[:config][:risk_threshold] = threshold
132
+ @options[:threshold] = threshold
67
133
  end
68
134
 
69
135
  opts.on("--no-root", "Disable root detection") do
70
- @options[:config][:enable_root_detection] = false
136
+ @options[:enable_root_detection] = false
71
137
  end
72
138
 
73
139
  opts.on("--no-emulator", "Disable emulator detection") do
74
- @options[:config][:enable_emulator_detection] = false
140
+ @options[:enable_emulator_detection] = false
75
141
  end
76
142
 
77
143
  opts.on("--no-hooking", "Disable hooking detection") do
78
- @options[:config][:enable_hooking_detection] = false
144
+ @options[:enable_hooking_detection] = false
79
145
  end
80
146
 
81
147
  opts.on("--no-integrity", "Disable integrity checks") do
82
- @options[:config][:enable_integrity_checks] = false
148
+ @options[:enable_integrity_checks] = false
83
149
  end
84
150
 
85
151
  opts.on("--no-network", "Disable network analysis") do
86
- @options[:config][:enable_network_analysis] = false
152
+ @options[:enable_network_analysis] = false
153
+ end
154
+
155
+ opts.on("--no-ai", "Disable AI behavioral analysis") do
156
+ @options[:enable_ai_behavioral_analysis] = false
157
+ end
158
+
159
+ opts.on("--enable-rasp", "Enable RASP protection during scan") do
160
+ @options[:enable_rasp_protection] = true
161
+ end
162
+
163
+ opts.on("--rasp-time SECONDS", Integer, "RASP monitoring time in seconds (default: 5)") do |time|
164
+ @options[:rasp_monitoring_time] = time
165
+ end
166
+
167
+ opts.on("--policy FILE", "Enterprise policy file path") do |file|
168
+ @options[:policy_file] = file
169
+ end
170
+
171
+ opts.on("--enable-cert-pinning", "Enable certificate pinning validation") do
172
+ @options[:enable_certificate_pinning] = true
173
+ end
174
+
175
+ opts.on("--enable-proxy-detection", "Enable advanced proxy detection") do
176
+ @options[:enable_proxy_detection] = true
177
+ end
178
+
179
+ opts.on("--target-ip IP", "Target IP address for network analysis") do |ip|
180
+ @options[:target_ip] = ip
181
+ end
182
+
183
+ opts.on("--target-url URL", "Target URL for certificate pinning validation") do |url|
184
+ @options[:target_url] = url
87
185
  end
88
186
 
89
187
  opts.on("-h", "--help", "Show this help message") do
@@ -99,7 +197,7 @@ class AiRootShieldCLI
99
197
  end
100
198
 
101
199
  def output_result(result)
102
- case @options[:output_format]
200
+ case @options[:format]
103
201
  when "json"
104
202
  puts JSON.pretty_generate(result)
105
203
  when "text"
@@ -117,6 +215,42 @@ class AiRootShieldCLI
117
215
  puts "Version: #{result[:version]}"
118
216
  puts ""
119
217
 
218
+ # Display compliance status if available
219
+ if result[:compliance]
220
+ puts "Policy Compliance:"
221
+ puts " Status: #{result[:compliance][:compliant] ? 'COMPLIANT' : 'NON-COMPLIANT'}"
222
+ puts " Policy Version: #{result[:compliance][:policy_version]}"
223
+
224
+ if result[:compliance][:violations].any?
225
+ puts " Violations:"
226
+ result[:compliance][:violations].each do |violation|
227
+ puts " • #{violation[:message]} (#{violation[:severity]})"
228
+ end
229
+ end
230
+ puts ""
231
+ end
232
+
233
+ # Display network analysis if available
234
+ if result[:network_analysis]
235
+ puts "Network Security Analysis:"
236
+
237
+ if result[:network_analysis][:proxy_detection]
238
+ proxy = result[:network_analysis][:proxy_detection]
239
+ puts " Proxy Detection: #{proxy[:proxy_detected] ? 'DETECTED' : 'Clean'}"
240
+ if proxy[:proxy_detected]
241
+ puts " Types: #{proxy[:proxy_types].join(', ')}"
242
+ puts " Confidence: #{(proxy[:confidence_score] * 100).round}%"
243
+ end
244
+ end
245
+
246
+ if result[:network_analysis][:certificate_pinning]
247
+ pinning = result[:network_analysis][:certificate_pinning]
248
+ puts " Certificate Pinning: #{pinning[:valid] ? 'VALID' : 'FAILED'}"
249
+ puts " Reason: #{pinning[:reason]}" unless pinning[:valid]
250
+ end
251
+ puts ""
252
+ end
253
+
120
254
  if result[:factors].any?
121
255
  puts "Detected Security Factors:"
122
256
  result[:factors].each do |factor|
@@ -134,6 +268,15 @@ class AiRootShieldCLI
134
268
  else
135
269
  puts "No security threats detected."
136
270
  end
271
+
272
+ # Display RASP status if available
273
+ if result[:rasp_status]
274
+ puts ""
275
+ puts "RASP Protection Status:"
276
+ puts " Active: #{result[:rasp_status][:active] ? 'YES' : 'NO'}"
277
+ puts " Events Detected: #{result[:rasp_status][:events_detected] || 0}"
278
+ puts " Protection Level: #{result[:rasp_status][:protection_level] || 'Standard'}"
279
+ end
137
280
  end
138
281
 
139
282
  def output_summary_format(result)
@@ -0,0 +1,406 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "resolv"
4
+ require "net/http"
5
+ require "ipaddr"
6
+
7
+ module AiRootShield
8
+ # Advanced proxy detection for VPN, Tor, custom DNS, and MITM appliances
9
+ class AdvancedProxyDetector
10
+ # Known Tor exit node IP ranges and identifiers
11
+ TOR_INDICATORS = {
12
+ dns_names: [
13
+ "tor-exit", "torservers", "artikel5ev", "privacyfoundation",
14
+ "torproject", "exitnode", "tor-relay"
15
+ ],
16
+ asn_patterns: [
17
+ /tor/i, /privacy/i, /anonymous/i, /vpn/i
18
+ ]
19
+ }.freeze
20
+
21
+ # Common VPN provider indicators
22
+ VPN_INDICATORS = {
23
+ dns_patterns: [
24
+ /vpn/i, /proxy/i, /tunnel/i, /shield/i, /secure/i,
25
+ /private/i, /anonymous/i, /hide/i, /mask/i
26
+ ],
27
+ asn_patterns: [
28
+ /vpn/i, /virtual private/i, /proxy/i, /datacenter/i,
29
+ /hosting/i, /cloud/i, /server/i
30
+ ],
31
+ known_providers: [
32
+ "nordvpn", "expressvpn", "surfshark", "cyberghost", "pia",
33
+ "mullvad", "protonvpn", "windscribe", "tunnelbear", "hotspotshield"
34
+ ]
35
+ }.freeze
36
+
37
+ # MITM appliance indicators
38
+ MITM_INDICATORS = {
39
+ certificate_issuers: [
40
+ /blue coat/i, /websense/i, /forcepoint/i, /zscaler/i,
41
+ /checkpoint/i, /palo alto/i, /fortinet/i, /sophos/i,
42
+ /mcafee/i, /symantec/i, /broadcom/i
43
+ ],
44
+ proxy_headers: [
45
+ "x-bluecoat-via", "x-forwarded-for", "x-real-ip",
46
+ "x-proxy-id", "x-cache", "via", "x-forwarded-proto"
47
+ ]
48
+ }.freeze
49
+
50
+ # Custom DNS server indicators
51
+ CUSTOM_DNS_INDICATORS = {
52
+ public_dns: [
53
+ "8.8.8.8", "8.8.4.4", # Google
54
+ "1.1.1.1", "1.0.0.1", # Cloudflare
55
+ "208.67.222.222", "208.67.220.220", # OpenDNS
56
+ "9.9.9.9", "149.112.112.112" # Quad9
57
+ ],
58
+ privacy_dns: [
59
+ "94.140.14.14", "94.140.15.15", # AdGuard
60
+ "76.76.19.19", "76.223.100.101", # Alternate DNS
61
+ "185.228.168.9", "185.228.169.9" # CleanBrowsing
62
+ ]
63
+ }.freeze
64
+
65
+ def initialize(config = {})
66
+ @config = {
67
+ enable_tor_detection: true,
68
+ enable_vpn_detection: true,
69
+ enable_mitm_detection: true,
70
+ enable_dns_detection: true,
71
+ timeout: 5,
72
+ max_retries: 2,
73
+ use_external_apis: false
74
+ }.merge(config)
75
+
76
+ @detection_cache = {}
77
+ @last_detection_time = {}
78
+ end
79
+
80
+ # Perform comprehensive proxy detection
81
+ # @param target_ip [String] IP address to analyze
82
+ # @param additional_data [Hash] Additional network data
83
+ # @return [Hash] Detection results
84
+ def detect_proxy(target_ip, additional_data = {})
85
+ return cached_result(target_ip) if cached_and_fresh?(target_ip)
86
+
87
+ results = {
88
+ ip_address: target_ip,
89
+ timestamp: Time.now.to_f,
90
+ proxy_detected: false,
91
+ proxy_types: [],
92
+ confidence_score: 0.0,
93
+ indicators: [],
94
+ details: {}
95
+ }
96
+
97
+ # Perform different detection methods
98
+ results = detect_tor_exit_node(target_ip, results) if @config[:enable_tor_detection]
99
+ results = detect_vpn_service(target_ip, results) if @config[:enable_vpn_detection]
100
+ results = detect_mitm_appliance(target_ip, additional_data, results) if @config[:enable_mitm_detection]
101
+ results = detect_custom_dns(additional_data, results) if @config[:enable_dns_detection]
102
+
103
+ # Calculate overall confidence
104
+ results[:confidence_score] = calculate_confidence_score(results)
105
+ results[:proxy_detected] = results[:confidence_score] > 0.5
106
+
107
+ # Cache results
108
+ cache_result(target_ip, results)
109
+
110
+ results
111
+ end
112
+
113
+ # Detect Tor exit nodes
114
+ # @param ip [String] IP address to check
115
+ # @param results [Hash] Current results hash
116
+ # @return [Hash] Updated results
117
+ def detect_tor_exit_node(ip, results)
118
+ tor_indicators = []
119
+
120
+ # Check reverse DNS for Tor indicators
121
+ begin
122
+ hostname = Resolv.getname(ip)
123
+ TOR_INDICATORS[:dns_names].each do |indicator|
124
+ if hostname.downcase.include?(indicator)
125
+ tor_indicators << "tor_dns_pattern: #{indicator}"
126
+ end
127
+ end
128
+ rescue Resolv::ResolvError
129
+ # No reverse DNS available
130
+ end
131
+
132
+ # Check if IP is in known Tor exit node lists (if external APIs enabled)
133
+ if @config[:use_external_apis]
134
+ tor_indicators.concat(check_tor_exit_lists(ip))
135
+ end
136
+
137
+ # Check for Tor-specific network characteristics
138
+ tor_indicators.concat(analyze_tor_network_characteristics(ip))
139
+
140
+ unless tor_indicators.empty?
141
+ results[:proxy_types] << "tor_exit_node"
142
+ results[:indicators].concat(tor_indicators)
143
+ results[:details][:tor] = {
144
+ detected: true,
145
+ indicators: tor_indicators,
146
+ risk_level: "high"
147
+ }
148
+ end
149
+
150
+ results
151
+ end
152
+
153
+ # Detect VPN services
154
+ # @param ip [String] IP address to check
155
+ # @param results [Hash] Current results hash
156
+ # @return [Hash] Updated results
157
+ def detect_vpn_service(ip, results)
158
+ vpn_indicators = []
159
+
160
+ # Check reverse DNS for VPN patterns
161
+ begin
162
+ hostname = Resolv.getname(ip)
163
+ VPN_INDICATORS[:dns_patterns].each do |pattern|
164
+ if hostname.match?(pattern)
165
+ vpn_indicators << "vpn_dns_pattern: #{pattern}"
166
+ end
167
+ end
168
+
169
+ # Check for known VPN providers
170
+ VPN_INDICATORS[:known_providers].each do |provider|
171
+ if hostname.downcase.include?(provider)
172
+ vpn_indicators << "known_vpn_provider: #{provider}"
173
+ end
174
+ end
175
+ rescue Resolv::ResolvError
176
+ # No reverse DNS available
177
+ end
178
+
179
+ # Check ASN information for VPN characteristics
180
+ vpn_indicators.concat(analyze_asn_for_vpn(ip))
181
+
182
+ # Check for datacenter/hosting characteristics
183
+ vpn_indicators.concat(analyze_hosting_characteristics(ip))
184
+
185
+ unless vpn_indicators.empty?
186
+ results[:proxy_types] << "vpn_service"
187
+ results[:indicators].concat(vpn_indicators)
188
+ results[:details][:vpn] = {
189
+ detected: true,
190
+ indicators: vpn_indicators,
191
+ risk_level: "medium"
192
+ }
193
+ end
194
+
195
+ results
196
+ end
197
+
198
+ # Detect MITM appliances
199
+ # @param ip [String] IP address to check
200
+ # @param additional_data [Hash] Additional network data
201
+ # @param results [Hash] Current results hash
202
+ # @return [Hash] Updated results
203
+ def detect_mitm_appliance(ip, additional_data, results)
204
+ mitm_indicators = []
205
+
206
+ # Check for MITM certificate issuers
207
+ if additional_data[:certificates]
208
+ additional_data[:certificates].each do |cert_info|
209
+ issuer = cert_info[:issuer] || ""
210
+ MITM_INDICATORS[:certificate_issuers].each do |pattern|
211
+ if issuer.match?(pattern)
212
+ mitm_indicators << "mitm_certificate_issuer: #{pattern}"
213
+ end
214
+ end
215
+ end
216
+ end
217
+
218
+ # Check for proxy headers
219
+ if additional_data[:http_headers]
220
+ MITM_INDICATORS[:proxy_headers].each do |header|
221
+ if additional_data[:http_headers].key?(header.downcase)
222
+ mitm_indicators << "proxy_header_detected: #{header}"
223
+ end
224
+ end
225
+ end
226
+
227
+ # Check for transparent proxy characteristics
228
+ mitm_indicators.concat(detect_transparent_proxy(ip, additional_data))
229
+
230
+ unless mitm_indicators.empty?
231
+ results[:proxy_types] << "mitm_appliance"
232
+ results[:indicators].concat(mitm_indicators)
233
+ results[:details][:mitm] = {
234
+ detected: true,
235
+ indicators: mitm_indicators,
236
+ risk_level: "high"
237
+ }
238
+ end
239
+
240
+ results
241
+ end
242
+
243
+ # Detect custom DNS configuration
244
+ # @param additional_data [Hash] Additional network data
245
+ # @param results [Hash] Current results hash
246
+ # @return [Hash] Updated results
247
+ def detect_custom_dns(additional_data, results)
248
+ dns_indicators = []
249
+
250
+ # Check configured DNS servers
251
+ if additional_data[:dns_servers]
252
+ dns_servers = Array(additional_data[:dns_servers])
253
+
254
+ dns_servers.each do |dns_server|
255
+ # Check for public DNS services
256
+ if CUSTOM_DNS_INDICATORS[:public_dns].include?(dns_server)
257
+ dns_indicators << "public_dns_detected: #{dns_server}"
258
+ elsif CUSTOM_DNS_INDICATORS[:privacy_dns].include?(dns_server)
259
+ dns_indicators << "privacy_dns_detected: #{dns_server}"
260
+ elsif !is_isp_dns?(dns_server)
261
+ dns_indicators << "custom_dns_detected: #{dns_server}"
262
+ end
263
+ end
264
+ end
265
+
266
+ # Check for DNS over HTTPS/TLS usage
267
+ if additional_data[:doh_enabled] || additional_data[:dot_enabled]
268
+ dns_indicators << "encrypted_dns_detected"
269
+ end
270
+
271
+ unless dns_indicators.empty?
272
+ results[:proxy_types] << "custom_dns"
273
+ results[:indicators].concat(dns_indicators)
274
+ results[:details][:dns] = {
275
+ detected: true,
276
+ indicators: dns_indicators,
277
+ risk_level: "low"
278
+ }
279
+ end
280
+
281
+ results
282
+ end
283
+
284
+ # Get detection statistics
285
+ # @return [Hash] Detection statistics
286
+ def detection_statistics
287
+ {
288
+ total_detections: @detection_cache.size,
289
+ cache_hits: @detection_cache.values.count { |v| v[:cached] },
290
+ detection_types: @detection_cache.values.flat_map { |v| v[:proxy_types] }.tally,
291
+ average_confidence: calculate_average_confidence,
292
+ last_detection: @last_detection_time.values.max
293
+ }
294
+ end
295
+
296
+ # Clear detection cache
297
+ def clear_cache
298
+ @detection_cache.clear
299
+ @last_detection_time.clear
300
+ end
301
+
302
+ private
303
+
304
+ def cached_and_fresh?(ip, ttl = 300)
305
+ return false unless @detection_cache[ip]
306
+ return false unless @last_detection_time[ip]
307
+
308
+ Time.now.to_f - @last_detection_time[ip] < ttl
309
+ end
310
+
311
+ def cached_result(ip)
312
+ result = @detection_cache[ip].dup
313
+ result[:cached] = true
314
+ result
315
+ end
316
+
317
+ def cache_result(ip, results)
318
+ @detection_cache[ip] = results
319
+ @last_detection_time[ip] = Time.now.to_f
320
+ end
321
+
322
+ def calculate_confidence_score(results)
323
+ base_score = 0.0
324
+
325
+ # Weight different proxy types
326
+ type_weights = {
327
+ "tor_exit_node" => 0.9,
328
+ "mitm_appliance" => 0.8,
329
+ "vpn_service" => 0.6,
330
+ "custom_dns" => 0.3
331
+ }
332
+
333
+ results[:proxy_types].each do |type|
334
+ base_score += type_weights[type] || 0.5
335
+ end
336
+
337
+ # Factor in number of indicators
338
+ indicator_bonus = [results[:indicators].size * 0.1, 0.3].min
339
+
340
+ # Cap at 1.0
341
+ [base_score + indicator_bonus, 1.0].min
342
+ end
343
+
344
+ def check_tor_exit_lists(ip)
345
+ # Placeholder for external Tor exit node list checking
346
+ # In production, this would query Tor directory authorities
347
+ []
348
+ end
349
+
350
+ def analyze_tor_network_characteristics(ip)
351
+ indicators = []
352
+
353
+ # Check for common Tor exit node ports
354
+ common_tor_ports = [80, 443, 993, 995]
355
+ # This would require actual port scanning in production
356
+
357
+ indicators
358
+ end
359
+
360
+ def analyze_asn_for_vpn(ip)
361
+ indicators = []
362
+
363
+ # This would require ASN lookup in production
364
+ # For now, return empty array
365
+
366
+ indicators
367
+ end
368
+
369
+ def analyze_hosting_characteristics(ip)
370
+ indicators = []
371
+
372
+ # Check if IP is in known datacenter ranges
373
+ # This would require GeoIP database in production
374
+
375
+ indicators
376
+ end
377
+
378
+ def detect_transparent_proxy(ip, additional_data)
379
+ indicators = []
380
+
381
+ # Check for transparent proxy indicators
382
+ if additional_data[:tcp_timestamp_anomaly]
383
+ indicators << "tcp_timestamp_anomaly"
384
+ end
385
+
386
+ if additional_data[:ttl_anomaly]
387
+ indicators << "ttl_hop_anomaly"
388
+ end
389
+
390
+ indicators
391
+ end
392
+
393
+ def is_isp_dns?(dns_server)
394
+ # Check if DNS server belongs to common ISP ranges
395
+ # This would require ISP database lookup in production
396
+ false
397
+ end
398
+
399
+ def calculate_average_confidence
400
+ return 0.0 if @detection_cache.empty?
401
+
402
+ total_confidence = @detection_cache.values.sum { |v| v[:confidence_score] }
403
+ total_confidence / @detection_cache.size
404
+ end
405
+ end
406
+ end