rack-ai 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 70c4c59ceaa3a8fd55136f31d5935e0f701f83086a48c09f3ff7b8f012961d4b
4
- data.tar.gz: a74d4964324bdec31e68fe9a9d73ce343091159817f8707a4cf99a871fe5ce03
3
+ metadata.gz: a73e35063e7e68e0af7c74603df68d971a2da7837b06d66c1721abc183929604
4
+ data.tar.gz: 9cca59b691a9a532944175d18c0b2b61e2c7d2e32c9a199b032180d7fbaea836
5
5
  SHA512:
6
- metadata.gz: 5272d46f9facd1245c5e953033bc5e1c2994ab4b528efc1ae69b6fbed599c836159d34970110bac8c0044e0a6bd576c9976da9dbe2d81df3ab20d73ca9d4ff33
7
- data.tar.gz: e67d23491fc329a4eb251f33f22c7c541cbde6af5298c29289914801f4793372f6384dc0912df026b605f2fb0f837659bc45a8c67e411046809418421be10be5
6
+ metadata.gz: ce2759602f6428f04102889998d096aa776a403f62c3751bc26c8a7ae47040e0f658e55823108135f930c34261339d9a40c9bd3d9341e6a7114ea2e7e2042d63
7
+ data.tar.gz: 9f77a8725714b6107fdf069f3b1fc1d4a1124cb37264ed45932929e4bcca221fee49dccbb053245571e664e60dee76761983fe763ded8606c3b6ec83f78dd50b
data/CHANGELOG.md CHANGED
@@ -7,25 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.4.0] - 2025-01-11
11
+
10
12
  ### Added
11
- - Rate limiting feature with configurable windows and thresholds
12
- - Anomaly detection with baseline learning and risk scoring
13
- - Enhanced logging system with JSON/structured output formats
14
- - Comprehensive example application demonstrating all features
15
- - Advanced error handling and edge case coverage
16
- - Performance optimizations and memory management
13
+ - **New Security Scanner Feature**: Advanced security threat detection with pattern matching for SQL injection, XSS, path traversal, and command injection
14
+ - **Rate Limiter Feature**: Built-in rate limiting with client identification, configurable windows, and penalty periods
15
+ - **Enhanced Logger**: Improved logging system with proper method delegation and structured output
16
+ - **Comprehensive Test Coverage**: Added complete test suites for new features with 96 passing tests
17
17
 
18
18
  ### Changed
19
- - Removed OStruct dependency to eliminate Ruby 3.5+ warnings
20
- - Improved configuration system with better validation
21
- - Enhanced middleware initialization and provider setup
22
- - Better test coverage and mocking strategies
19
+ - **Improved Error Handling**: Enhanced configuration validation with safe method access patterns
20
+ - **Better Code Quality**: Fixed potential null pointer exceptions in classification and moderation features
21
+ - **Enhanced Security**: Added defensive programming patterns throughout the codebase
23
22
 
24
23
  ### Fixed
25
- - Configuration validation issues
26
- - Provider initialization edge cases
27
- - Test suite stability and reliability
28
- - Memory leaks in long-running applications
24
+ - Logger method delegation issues - now properly uses structured logging
25
+ - Configuration access safety in classification feature routing checks
26
+ - Moderation feature configuration validation for toxicity thresholds
27
+ - Syntax errors in security scanner regex patterns
28
+
29
+ ### Security
30
+ - Advanced threat detection patterns for multiple attack vectors
31
+ - Improved rate limiting with client fingerprinting
32
+ - Enhanced input validation and sanitization
29
33
 
30
34
  ## [0.3.0] - 2024-01-16
31
35
 
@@ -48,9 +48,9 @@ module Rack
48
48
  when :spam
49
49
  :block
50
50
  when :suspicious
51
- @config.routing.smart_routing_enabled ? :route : :allow
51
+ smart_routing_enabled? ? :route : :allow
52
52
  when :bot
53
- @config.routing.smart_routing_enabled ? :route : :allow
53
+ smart_routing_enabled? ? :route : :allow
54
54
  when :human
55
55
  :allow
56
56
  else
@@ -58,6 +58,12 @@ module Rack
58
58
  end
59
59
  end
60
60
 
61
+ def smart_routing_enabled?
62
+ @config.respond_to?(:routing) &&
63
+ @config.routing.respond_to?(:smart_routing_enabled) &&
64
+ @config.routing.smart_routing_enabled
65
+ end
66
+
61
67
  def generate_request_id(env)
62
68
  "#{Time.now.to_i}-#{env.object_id}"
63
69
  end
@@ -17,7 +17,9 @@ module Rack
17
17
  end
18
18
 
19
19
  def process_response?
20
- @config.moderation.check_response
20
+ @config.respond_to?(:moderation) &&
21
+ @config.moderation.respond_to?(:check_response) &&
22
+ @config.moderation.check_response
21
23
  end
22
24
 
23
25
  def process_request(env)
@@ -27,11 +29,11 @@ module Rack
27
29
  result = @provider.moderate_content(content.join(" "))
28
30
 
29
31
  # Apply toxicity threshold
30
- threshold = @config.moderation.toxicity_threshold
32
+ threshold = get_toxicity_threshold
31
33
  max_score = result[:category_scores]&.values&.max || 0.0
32
34
 
33
35
  result[:action] = if result[:flagged] && max_score > threshold
34
- @config.moderation.block_on_violation ? :block : :flag
36
+ should_block_on_violation? ? :block : :flag
35
37
  else
36
38
  :allow
37
39
  end
@@ -87,6 +89,18 @@ module Rack
87
89
  content.compact.reject(&:empty?)
88
90
  end
89
91
 
92
+ def get_toxicity_threshold
93
+ return 0.7 unless @config.respond_to?(:moderation)
94
+ return 0.7 unless @config.moderation.respond_to?(:toxicity_threshold)
95
+ @config.moderation.toxicity_threshold
96
+ end
97
+
98
+ def should_block_on_violation?
99
+ return true unless @config.respond_to?(:moderation)
100
+ return true unless @config.moderation.respond_to?(:block_on_violation)
101
+ @config.moderation.block_on_violation
102
+ end
103
+
90
104
  def extract_content_from_response(body)
91
105
  return "" unless body.respond_to?(:each)
92
106
 
@@ -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
@@ -13,23 +13,24 @@ module Rack
13
13
  end
14
14
 
15
15
  def debug(message, metadata = {})
16
- puts "[DEBUG] #{message} #{metadata}" if ENV['RACK_AI_DEBUG']
16
+ return unless ENV['RACK_AI_DEBUG']
17
+ log(:debug, message, metadata)
17
18
  end
18
19
 
19
20
  def info(message, metadata = {})
20
- puts "[INFO] #{message} #{metadata}"
21
+ log(:info, message, metadata)
21
22
  end
22
23
 
23
24
  def warn(message, metadata = {})
24
- puts "[WARN] #{message} #{metadata}"
25
+ log(:warn, message, metadata)
25
26
  end
26
27
 
27
28
  def error(message, metadata = {})
28
- puts "[ERROR] #{message} #{metadata}"
29
+ log(:error, message, metadata)
29
30
  end
30
31
 
31
32
  def fatal(message, metadata = {})
32
- puts "[FATAL] #{message} #{metadata}"
33
+ log(:fatal, message, metadata)
33
34
  end
34
35
 
35
36
  private
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  module AI
5
- VERSION = "0.3.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.3.0
4
+ version: 0.4.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ahmet KAHRAMAN
@@ -254,9 +254,11 @@ files:
254
254
  - lib/rack/ai/features/enhancement.rb
255
255
  - lib/rack/ai/features/logging.rb
256
256
  - lib/rack/ai/features/moderation.rb
257
+ - lib/rack/ai/features/rate_limiter.rb
257
258
  - lib/rack/ai/features/rate_limiting.rb
258
259
  - lib/rack/ai/features/routing.rb
259
260
  - lib/rack/ai/features/security.rb
261
+ - lib/rack/ai/features/security_scanner.rb
260
262
  - lib/rack/ai/middleware.rb
261
263
  - lib/rack/ai/providers/base.rb
262
264
  - lib/rack/ai/providers/huggingface.rb