rack-ai 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.
@@ -0,0 +1,145 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'digest'
4
+
5
+ module Rack
6
+ module AI
7
+ module Features
8
+ class RateLimiter
9
+ attr_reader :name, :config
10
+
11
+ def initialize(config)
12
+ @name = :rate_limiter
13
+ @config = config
14
+ @cache = {}
15
+ @cleanup_interval = 300 # 5 minutes
16
+ @last_cleanup = Time.now
17
+ end
18
+
19
+ def enabled?
20
+ @config.feature_enabled?(:rate_limiter)
21
+ end
22
+
23
+ def process_response?
24
+ false
25
+ end
26
+
27
+ def process_request(env)
28
+ return { processed: false, reason: "disabled" } unless enabled?
29
+
30
+ client_id = extract_client_id(env)
31
+ current_time = Time.now
32
+
33
+ cleanup_expired_entries if should_cleanup?
34
+
35
+ # Get or initialize client data
36
+ client_data = @cache[client_id] ||= {
37
+ requests: [],
38
+ blocked_until: nil
39
+ }
40
+
41
+ # Check if client is currently blocked
42
+ if client_data[:blocked_until] && current_time < client_data[:blocked_until]
43
+ return {
44
+ processed: true,
45
+ action: :block,
46
+ reason: "rate_limited",
47
+ blocked_until: client_data[:blocked_until],
48
+ feature: @name,
49
+ timestamp: current_time.iso8601
50
+ }
51
+ end
52
+
53
+ # Clean old requests
54
+ window_start = current_time - rate_limit_window
55
+ client_data[:requests].reject! { |req_time| req_time < window_start }
56
+
57
+ # Check rate limit
58
+ if client_data[:requests].size >= max_requests_per_window
59
+ # Block client for penalty duration
60
+ client_data[:blocked_until] = current_time + penalty_duration
61
+
62
+ return {
63
+ processed: true,
64
+ action: :block,
65
+ reason: "rate_limit_exceeded",
66
+ requests_count: client_data[:requests].size,
67
+ blocked_until: client_data[:blocked_until],
68
+ feature: @name,
69
+ timestamp: current_time.iso8601
70
+ }
71
+ end
72
+
73
+ # Record this request
74
+ client_data[:requests] << current_time
75
+
76
+ {
77
+ processed: true,
78
+ action: :allow,
79
+ requests_count: client_data[:requests].size,
80
+ remaining_requests: max_requests_per_window - client_data[:requests].size,
81
+ feature: @name,
82
+ timestamp: current_time.iso8601
83
+ }
84
+ end
85
+
86
+ private
87
+
88
+ def extract_client_id(env)
89
+ # Try multiple identification methods
90
+ client_ip = env['HTTP_X_FORWARDED_FOR']&.split(',')&.first&.strip ||
91
+ env['HTTP_X_REAL_IP'] ||
92
+ env['REMOTE_ADDR'] ||
93
+ 'unknown'
94
+
95
+ user_agent = env['HTTP_USER_AGENT'] || 'unknown'
96
+
97
+ # Create a hash of IP + User Agent for better identification
98
+ Digest::SHA256.hexdigest("#{client_ip}:#{user_agent}")[0..16]
99
+ end
100
+
101
+ def rate_limit_window
102
+ get_config_value(:rate_limit_window, 60) # 1 minute default
103
+ end
104
+
105
+ def max_requests_per_window
106
+ get_config_value(:max_requests_per_window, 100) # 100 requests per minute default
107
+ end
108
+
109
+ def penalty_duration
110
+ get_config_value(:penalty_duration, 300) # 5 minutes default
111
+ end
112
+
113
+ def get_config_value(key, default)
114
+ return default unless @config.respond_to?(:rate_limiter)
115
+ return default unless @config.rate_limiter.respond_to?(key)
116
+ @config.rate_limiter.public_send(key)
117
+ end
118
+
119
+ def should_cleanup?
120
+ Time.now - @last_cleanup > @cleanup_interval
121
+ end
122
+
123
+ def cleanup_expired_entries
124
+ current_time = Time.now
125
+ window_start = current_time - rate_limit_window
126
+
127
+ @cache.each do |client_id, data|
128
+ # Remove expired requests
129
+ data[:requests].reject! { |req_time| req_time < window_start }
130
+
131
+ # Remove expired blocks
132
+ data[:blocked_until] = nil if data[:blocked_until] && current_time >= data[:blocked_until]
133
+
134
+ # Remove empty entries
135
+ if data[:requests].empty? && data[:blocked_until].nil?
136
+ @cache.delete(client_id)
137
+ end
138
+ end
139
+
140
+ @last_cleanup = current_time
141
+ end
142
+ end
143
+ end
144
+ end
145
+ end
@@ -0,0 +1,326 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ module AI
5
+ module Features
6
+ class SecurityScanner
7
+ attr_reader :name, :config
8
+
9
+ def initialize(config)
10
+ @name = :security_scanner
11
+ @config = config
12
+ end
13
+
14
+ def enabled?
15
+ @config.feature_enabled?(:security_scanner)
16
+ end
17
+
18
+ def process_response?
19
+ false
20
+ end
21
+
22
+ def process_request(env)
23
+ return { processed: false, reason: "disabled" } unless enabled?
24
+
25
+ threats = []
26
+ risk_score = 0.0
27
+
28
+ # Check for various security threats
29
+ threats.concat(check_sql_injection(env))
30
+ threats.concat(check_xss_attempts(env))
31
+ threats.concat(check_path_traversal(env))
32
+ threats.concat(check_command_injection(env))
33
+ threats.concat(check_suspicious_headers(env))
34
+ threats.concat(check_malicious_user_agents(env))
35
+
36
+ # Calculate overall risk score
37
+ risk_score = calculate_risk_score(threats)
38
+ threat_level = determine_threat_level(risk_score)
39
+
40
+ {
41
+ processed: true,
42
+ action: determine_action(threat_level),
43
+ threats: threats,
44
+ risk_score: risk_score,
45
+ threat_level: threat_level,
46
+ feature: @name,
47
+ timestamp: Time.now.iso8601
48
+ }
49
+ end
50
+
51
+ private
52
+
53
+ def check_sql_injection(env)
54
+ threats = []
55
+ patterns = [
56
+ /union\s+select/i,
57
+ /drop\s+table/i,
58
+ /insert\s+into/i,
59
+ /delete\s+from/i,
60
+ /update\s+.*set/i,
61
+ /exec\s*\(/i,
62
+ /script\s*>/i,
63
+ /'.*or.*'.*='/i,
64
+ /1=1/i,
65
+ /admin'--/i
66
+ ]
67
+
68
+ check_patterns_in_request(env, patterns, 'sql_injection').each do |threat|
69
+ threats << threat
70
+ end
71
+
72
+ threats
73
+ end
74
+
75
+ def check_xss_attempts(env)
76
+ threats = []
77
+ patterns = [
78
+ /<script[^>]*>.*?<\/script>/i,
79
+ /javascript:/i,
80
+ /on\w+\s*=/i,
81
+ /<iframe[^>]*>/i,
82
+ /<object[^>]*>/i,
83
+ /<embed[^>]*>/i,
84
+ /eval\s*\(/i,
85
+ /alert\s*\(/i,
86
+ /document\.cookie/i
87
+ ]
88
+
89
+ check_patterns_in_request(env, patterns, 'xss_attempt').each do |threat|
90
+ threats << threat
91
+ end
92
+
93
+ threats
94
+ end
95
+
96
+ def check_path_traversal(env)
97
+ threats = []
98
+ patterns = [
99
+ /\.\.\//,
100
+ /\.\.\\/,
101
+ /%2e%2e%2f/i,
102
+ /%2e%2e%5c/i,
103
+ /etc\/passwd/i,
104
+ /windows\/system32/i
105
+ ]
106
+
107
+ check_patterns_in_request(env, patterns, 'path_traversal').each do |threat|
108
+ threats << threat
109
+ end
110
+
111
+ threats
112
+ end
113
+
114
+ def check_command_injection(env)
115
+ threats = []
116
+ patterns = [
117
+ /;\s*cat\s+/i,
118
+ /;\s*ls\s+/i,
119
+ /;\s*rm\s+/i,
120
+ /;\s*wget\s+/i,
121
+ /;\s*curl\s+/i,
122
+ /\|\s*nc\s+/i,
123
+ /&&\s*whoami/i,
124
+ /`.*`/,
125
+ /\$\(.*\)/
126
+ ]
127
+
128
+ check_patterns_in_request(env, patterns, 'command_injection').each do |threat|
129
+ threats << threat
130
+ end
131
+
132
+ threats
133
+ end
134
+
135
+ def check_suspicious_headers(env)
136
+ threats = []
137
+
138
+ # Check for suspicious User-Agent patterns
139
+ user_agent = env['HTTP_USER_AGENT']
140
+ if user_agent
141
+ suspicious_agents = [
142
+ /sqlmap/i,
143
+ /nikto/i,
144
+ /nessus/i,
145
+ /burp/i,
146
+ /w3af/i,
147
+ /acunetix/i,
148
+ /netsparker/i
149
+ ]
150
+
151
+ suspicious_agents.each do |pattern|
152
+ if user_agent.match?(pattern)
153
+ threats << {
154
+ type: 'suspicious_user_agent',
155
+ pattern: pattern.source,
156
+ value: user_agent,
157
+ severity: 'high'
158
+ }
159
+ end
160
+ end
161
+ end
162
+
163
+ # Check for suspicious headers
164
+ suspicious_headers = %w[
165
+ HTTP_X_FORWARDED_HOST
166
+ HTTP_X_ORIGINAL_URL
167
+ HTTP_X_REWRITE_URL
168
+ ]
169
+
170
+ suspicious_headers.each do |header|
171
+ if env[header] && env[header].include?('..')
172
+ threats << {
173
+ type: 'suspicious_header',
174
+ header: header,
175
+ value: env[header],
176
+ severity: 'medium'
177
+ }
178
+ end
179
+ end
180
+
181
+ threats
182
+ end
183
+
184
+ def check_malicious_user_agents(env)
185
+ threats = []
186
+ user_agent = env['HTTP_USER_AGENT']
187
+ return threats unless user_agent
188
+
189
+ # Check for bot patterns that might be malicious
190
+ malicious_patterns = [
191
+ /python-requests/i,
192
+ /curl/i,
193
+ /wget/i,
194
+ /libwww/i,
195
+ /lwp-trivial/i,
196
+ /^$/ # Empty user agent
197
+ ]
198
+
199
+ malicious_patterns.each do |pattern|
200
+ if user_agent.match?(pattern)
201
+ threats << {
202
+ type: 'potentially_malicious_bot',
203
+ pattern: pattern.source,
204
+ value: user_agent,
205
+ severity: 'low'
206
+ }
207
+ end
208
+ end
209
+
210
+ threats
211
+ end
212
+
213
+ def check_patterns_in_request(env, patterns, threat_type)
214
+ threats = []
215
+
216
+ # Check query string
217
+ if env['QUERY_STRING']
218
+ patterns.each do |pattern|
219
+ if env['QUERY_STRING'].match?(pattern)
220
+ threats << {
221
+ type: threat_type,
222
+ location: 'query_string',
223
+ pattern: pattern.source,
224
+ value: env['QUERY_STRING'],
225
+ severity: 'high'
226
+ }
227
+ end
228
+ end
229
+ end
230
+
231
+ # Check POST data
232
+ if %w[POST PUT PATCH].include?(env['REQUEST_METHOD']) && env['rack.input']
233
+ begin
234
+ body = env['rack.input'].read
235
+ env['rack.input'].rewind if env['rack.input'].respond_to?(:rewind)
236
+
237
+ patterns.each do |pattern|
238
+ if body.match?(pattern)
239
+ threats << {
240
+ type: threat_type,
241
+ location: 'request_body',
242
+ pattern: pattern.source,
243
+ value: body[0..200], # Truncate for logging
244
+ severity: 'high'
245
+ }
246
+ end
247
+ end
248
+ rescue => e
249
+ # Log error but don't fail
250
+ Utils::Logger.warn("Error reading request body for security scan", { error: e.message })
251
+ end
252
+ end
253
+
254
+ # Check headers
255
+ env.each do |key, value|
256
+ next unless key.start_with?('HTTP_')
257
+ next unless value.is_a?(String)
258
+
259
+ patterns.each do |pattern|
260
+ if value.match?(pattern)
261
+ threats << {
262
+ type: threat_type,
263
+ location: 'headers',
264
+ header: key,
265
+ pattern: pattern.source,
266
+ value: value,
267
+ severity: 'medium'
268
+ }
269
+ end
270
+ end
271
+ end
272
+
273
+ threats
274
+ end
275
+
276
+ def calculate_risk_score(threats)
277
+ return 0.0 if threats.empty?
278
+
279
+ score = 0.0
280
+ threats.each do |threat|
281
+ case threat[:severity]
282
+ when 'high'
283
+ score += 0.4
284
+ when 'medium'
285
+ score += 0.2
286
+ when 'low'
287
+ score += 0.1
288
+ end
289
+ end
290
+
291
+ [score, 1.0].min # Cap at 1.0
292
+ end
293
+
294
+ def determine_threat_level(risk_score)
295
+ case risk_score
296
+ when 0.0...0.2
297
+ :low
298
+ when 0.2...0.5
299
+ :medium
300
+ when 0.5...0.8
301
+ :high
302
+ else
303
+ :critical
304
+ end
305
+ end
306
+
307
+ def determine_action(threat_level)
308
+ case threat_level
309
+ when :critical, :high
310
+ :block
311
+ when :medium
312
+ get_config_value(:medium_threat_action, :flag)
313
+ else
314
+ :allow
315
+ end
316
+ end
317
+
318
+ def get_config_value(key, default)
319
+ return default unless @config.respond_to?(:security_scanner)
320
+ return default unless @config.security_scanner.respond_to?(key)
321
+ @config.security_scanner.public_send(key)
322
+ end
323
+ end
324
+ end
325
+ end
326
+ end
@@ -95,7 +95,7 @@ module Rack
95
95
  feature_class.new(@provider, @config)
96
96
  end
97
97
 
98
- Utils::Logger.debug("Built features: #{features.map(&:name)}")
98
+ Utils::Logger.debug("Built features: #{features.map { |f| f.class.name.split('::').last }}")
99
99
  features
100
100
  end
101
101
 
@@ -6,7 +6,7 @@ require 'json'
6
6
  module Rack
7
7
  module AI
8
8
  module Utils
9
- class EnhancedLogger
9
+ class AdvancedLogger
10
10
  LOG_LEVELS = {
11
11
  debug: ::Logger::DEBUG,
12
12
  info: ::Logger::INFO,
@@ -123,8 +123,8 @@ module Rack
123
123
  end
124
124
  end
125
125
 
126
- # Backward compatibility
127
- Logger = EnhancedLogger
126
+ # Alias for backward compatibility
127
+ EnhancedLogger = AdvancedLogger
128
128
  end
129
129
  end
130
130
  end
@@ -7,28 +7,13 @@ module Rack
7
7
  module AI
8
8
  module Utils
9
9
  class Logger
10
- def self.debug(message, context = {})
11
- puts "[DEBUG] #{message} #{context}" if ENV['RACK_AI_DEBUG']
12
- end
13
-
14
- def self.info(message, context = {})
15
- puts "[INFO] #{message} #{context}"
16
- end
17
-
18
- def self.warn(message, context = {})
19
- puts "[WARN] #{message} #{context}"
20
- end
21
-
22
- def self.error(message, context = {})
23
- puts "[ERROR] #{message} #{context}"
24
- end
25
-
26
10
  class << self
27
11
  def logger
28
12
  @logger ||= build_logger
29
13
  end
30
14
 
31
15
  def debug(message, metadata = {})
16
+ return unless ENV['RACK_AI_DEBUG']
32
17
  log(:debug, message, metadata)
33
18
  end
34
19
 
@@ -122,6 +107,7 @@ module Rack
122
107
  end
123
108
  end
124
109
  end
110
+
125
111
  end
126
112
  end
127
113
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  module AI
5
- VERSION = "0.2.0"
5
+ VERSION = "0.4.0"
6
6
  end
7
7
  end
data/lib/rack/ai.rb CHANGED
@@ -16,6 +16,8 @@ require_relative "ai/features/logging"
16
16
  require_relative "ai/features/enhancement"
17
17
  require_relative "ai/features/rate_limiting"
18
18
  require_relative "ai/features/anomaly_detection"
19
+ require_relative "ai/features/rate_limiter"
20
+ require_relative "ai/features/security_scanner"
19
21
  require_relative "ai/utils/logger"
20
22
  require_relative "ai/utils/metrics"
21
23
  require_relative "ai/utils/sanitizer"
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: rack-ai
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmet KAHRAMAN
@@ -243,7 +243,9 @@ files:
243
243
  - benchmarks/performance_benchmark.rb
244
244
  - examples/comprehensive_example.rb
245
245
  - examples/rails_integration.rb
246
+ - examples/rails_integration_advanced.rb
246
247
  - examples/sinatra_integration.rb
248
+ - examples/sinatra_microservice.rb
247
249
  - lib/rack/ai.rb
248
250
  - lib/rack/ai/configuration.rb
249
251
  - lib/rack/ai/features/anomaly_detection.rb
@@ -252,9 +254,11 @@ files:
252
254
  - lib/rack/ai/features/enhancement.rb
253
255
  - lib/rack/ai/features/logging.rb
254
256
  - lib/rack/ai/features/moderation.rb
257
+ - lib/rack/ai/features/rate_limiter.rb
255
258
  - lib/rack/ai/features/rate_limiting.rb
256
259
  - lib/rack/ai/features/routing.rb
257
260
  - lib/rack/ai/features/security.rb
261
+ - lib/rack/ai/features/security_scanner.rb
258
262
  - lib/rack/ai/middleware.rb
259
263
  - lib/rack/ai/providers/base.rb
260
264
  - lib/rack/ai/providers/huggingface.rb