rack-ai 0.1.0 → 0.2.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 +4 -4
- data/CHANGELOG.md +59 -35
- data/examples/comprehensive_example.rb +203 -0
- data/lib/rack/ai/configuration.rb +40 -20
- data/lib/rack/ai/features/anomaly_detection.rb +236 -0
- data/lib/rack/ai/features/rate_limiting.rb +114 -0
- data/lib/rack/ai/middleware.rb +3 -1
- data/lib/rack/ai/utils/enhanced_logger.rb +130 -0
- data/lib/rack/ai/utils/logger.rb +16 -0
- data/lib/rack/ai/version.rb +1 -1
- data/lib/rack/ai.rb +4 -1
- metadata +5 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 1d95d49cfff0cb1d7f62fb2f31680b6b2ce139ef8c2bf87c4bb86e5a5123c876
|
4
|
+
data.tar.gz: 05ed2583e65ef1fa68dbb502bafcc4046ed67495b049ed1ffc41e4ed7240f283
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3fc2d6b2dcd73a946e20de45c66cf92bcf65c342f434adf939d79b1b732a4b16d42d4faa9f04b28c4dcc116baf3168c1cc014d29d7a7035b55bea915be441cda
|
7
|
+
data.tar.gz: 8fbe16fb2da9b93e3ff3896e923c3e3832f69319f4bff630fb68fcab16530e70e0fb95b293a00559bb9ec95cb87a707b8cd69b5fa45398817ecbdacbe1beac2a
|
data/CHANGELOG.md
CHANGED
@@ -7,43 +7,67 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
7
7
|
|
8
8
|
## [Unreleased]
|
9
9
|
|
10
|
-
|
10
|
+
### 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
|
17
|
+
|
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
|
23
|
+
|
24
|
+
### Fixed
|
25
|
+
- Configuration validation issues
|
26
|
+
- Provider initialization edge cases
|
27
|
+
- Test suite stability and reliability
|
28
|
+
- Memory leaks in long-running applications
|
29
|
+
|
30
|
+
## [0.1.0] - 2025-01-11
|
11
31
|
|
12
32
|
### Added
|
13
|
-
- Initial release
|
14
|
-
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
33
|
+
- Initial release with core AI middleware functionality
|
34
|
+
- OpenAI, HuggingFace, and Local provider support
|
35
|
+
- AI-powered request classification (human, bot, spam, suspicious)
|
36
|
+
- Security analysis with injection detection and threat assessment
|
37
|
+
- Content moderation using AI models
|
38
|
+
- Smart caching with predictive algorithms
|
39
|
+
- Flexible configuration system using Dry::Configurable
|
40
|
+
- Fail-safe operation with graceful degradation
|
41
|
+
- Async processing for non-blocking AI analysis
|
42
|
+
- Comprehensive logging and monitoring
|
43
|
+
- Health check endpoints
|
44
|
+
- Complete test suite with WebMock integration
|
45
|
+
- Documentation and usage examples
|
46
|
+
|
47
|
+
### Features
|
48
|
+
- **Request Classification**: Automatically classify incoming requests using AI
|
49
|
+
- **Security Analysis**: Detect SQL injection, XSS, CSRF, and other threats
|
50
|
+
- **Content Moderation**: Filter inappropriate content with configurable thresholds
|
51
|
+
- **Rate Limiting**: Protect against abuse with intelligent rate limiting
|
52
|
+
- **Anomaly Detection**: Identify unusual patterns and potential attacks
|
53
|
+
- **Smart Caching**: Predictive caching based on AI analysis patterns
|
54
|
+
- **Multi-Provider Support**: OpenAI, HuggingFace, and local model integration
|
55
|
+
- **Async Processing**: Non-blocking AI analysis for optimal performance
|
56
|
+
- **Fail-Safe Mode**: Graceful handling of AI service outages
|
57
|
+
- **Comprehensive Logging**: Structured logging with multiple output formats
|
58
|
+
|
59
|
+
### Providers
|
60
|
+
- **OpenAI**: GPT-3.5/4 integration for classification and moderation
|
61
|
+
- **HuggingFace**: Transformer models for various AI tasks
|
62
|
+
- **Local**: Support for self-hosted AI models and services
|
63
|
+
|
64
|
+
### Configuration
|
65
|
+
- Flexible feature pipeline configuration
|
66
|
+
- Provider-specific settings and API keys
|
67
|
+
- Timeout and retry mechanisms
|
68
|
+
- Security and sanitization options
|
69
|
+
- Performance tuning parameters
|
70
|
+
- Logging and monitoring controls
|
47
71
|
- Detailed documentation
|
48
72
|
- Integration examples
|
49
73
|
|
@@ -0,0 +1,203 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'rack'
|
5
|
+
require 'rack/ai'
|
6
|
+
|
7
|
+
# Comprehensive example showing all Rack::AI features
|
8
|
+
class ComprehensiveApp
|
9
|
+
def self.app
|
10
|
+
Rack::Builder.new do
|
11
|
+
# Configure Rack::AI with all features enabled
|
12
|
+
use Rack::AI::Middleware,
|
13
|
+
provider: :openai,
|
14
|
+
features: [:classification, :security, :rate_limiting, :anomaly_detection, :moderation],
|
15
|
+
fail_safe: true,
|
16
|
+
async_processing: false, # Synchronous for demo
|
17
|
+
explain_decisions: true,
|
18
|
+
|
19
|
+
# OpenAI Configuration
|
20
|
+
openai: {
|
21
|
+
api_key: ENV['OPENAI_API_KEY'] || 'your-api-key-here',
|
22
|
+
timeout: 30,
|
23
|
+
retries: 3
|
24
|
+
},
|
25
|
+
|
26
|
+
# Classification settings
|
27
|
+
classification: {
|
28
|
+
confidence_threshold: 0.8,
|
29
|
+
categories: [:human, :bot, :spam, :suspicious]
|
30
|
+
},
|
31
|
+
|
32
|
+
# Security settings
|
33
|
+
security: {
|
34
|
+
injection_detection: true,
|
35
|
+
anomaly_threshold: 0.7,
|
36
|
+
suspicious_patterns: [
|
37
|
+
/\b(union|select|insert|delete|drop)\b/i,
|
38
|
+
/<script\b[^<]*(?:(?!<\/script>)<[^<]*)*<\/script>/mi,
|
39
|
+
/javascript:/i
|
40
|
+
]
|
41
|
+
},
|
42
|
+
|
43
|
+
# Rate limiting settings
|
44
|
+
rate_limiting: {
|
45
|
+
window_size: 3600, # 1 hour
|
46
|
+
max_requests: 1000,
|
47
|
+
block_duration: 3600
|
48
|
+
},
|
49
|
+
|
50
|
+
# Anomaly detection settings
|
51
|
+
anomaly_detection: {
|
52
|
+
sensitivity: 0.8,
|
53
|
+
risk_threshold_block: 80
|
54
|
+
},
|
55
|
+
|
56
|
+
# Moderation settings
|
57
|
+
moderation: {
|
58
|
+
toxicity_threshold: 0.8,
|
59
|
+
check_response: false,
|
60
|
+
block_on_violation: true
|
61
|
+
}
|
62
|
+
|
63
|
+
# Main application
|
64
|
+
run lambda { |env|
|
65
|
+
request = Rack::Request.new(env)
|
66
|
+
|
67
|
+
# Get AI analysis results
|
68
|
+
ai_results = env['rack.ai'][:results]
|
69
|
+
|
70
|
+
# Build response based on AI analysis
|
71
|
+
response_data = {
|
72
|
+
message: "Request processed successfully",
|
73
|
+
path: request.path,
|
74
|
+
method: request.request_method,
|
75
|
+
ai_analysis: {
|
76
|
+
classification: ai_results[:classification],
|
77
|
+
security: ai_results[:security],
|
78
|
+
rate_limiting: ai_results[:rate_limiting],
|
79
|
+
anomaly_detection: ai_results[:anomaly_detection],
|
80
|
+
moderation: ai_results[:moderation]
|
81
|
+
},
|
82
|
+
processing_time: env['rack.ai'][:processing_time],
|
83
|
+
timestamp: Time.now.iso8601
|
84
|
+
}
|
85
|
+
|
86
|
+
# Return JSON response
|
87
|
+
[
|
88
|
+
200,
|
89
|
+
{
|
90
|
+
'Content-Type' => 'application/json',
|
91
|
+
'X-Powered-By' => 'Rack::AI'
|
92
|
+
},
|
93
|
+
[response_data.to_json]
|
94
|
+
]
|
95
|
+
}
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
# Demo routes for testing different scenarios
|
101
|
+
class DemoRoutes
|
102
|
+
def self.app
|
103
|
+
Rack::Builder.new do
|
104
|
+
use Rack::AI::Middleware,
|
105
|
+
provider: :openai,
|
106
|
+
features: [:classification, :security, :rate_limiting],
|
107
|
+
openai: { api_key: ENV['OPENAI_API_KEY'] }
|
108
|
+
|
109
|
+
map '/api/safe' do
|
110
|
+
run lambda { |env|
|
111
|
+
[200, { 'Content-Type' => 'application/json' },
|
112
|
+
[{ message: 'Safe endpoint', status: 'ok' }.to_json]]
|
113
|
+
}
|
114
|
+
end
|
115
|
+
|
116
|
+
map '/api/suspicious' do
|
117
|
+
run lambda { |env|
|
118
|
+
# This might trigger security features
|
119
|
+
query = Rack::Request.new(env).params['q']
|
120
|
+
if query&.include?('DROP TABLE')
|
121
|
+
[400, {}, ['Bad request']]
|
122
|
+
else
|
123
|
+
[200, { 'Content-Type' => 'application/json' },
|
124
|
+
[{ message: 'Processed query', query: query }.to_json]]
|
125
|
+
end
|
126
|
+
}
|
127
|
+
end
|
128
|
+
|
129
|
+
map '/api/bulk' do
|
130
|
+
run lambda { |env|
|
131
|
+
# This might trigger rate limiting
|
132
|
+
[200, { 'Content-Type' => 'application/json' },
|
133
|
+
[{ message: 'Bulk operation completed' }.to_json]]
|
134
|
+
}
|
135
|
+
end
|
136
|
+
|
137
|
+
map '/health' do
|
138
|
+
run lambda { |env|
|
139
|
+
# Health check endpoint (might be excluded from AI processing)
|
140
|
+
[200, { 'Content-Type' => 'application/json' },
|
141
|
+
[{ status: 'healthy', timestamp: Time.now.iso8601 }.to_json]]
|
142
|
+
}
|
143
|
+
end
|
144
|
+
|
145
|
+
# Default route
|
146
|
+
run lambda { |env|
|
147
|
+
ai_results = env['rack.ai'][:results]
|
148
|
+
|
149
|
+
[200, { 'Content-Type' => 'text/html' }, [<<~HTML
|
150
|
+
<!DOCTYPE html>
|
151
|
+
<html>
|
152
|
+
<head>
|
153
|
+
<title>Rack::AI Demo</title>
|
154
|
+
<style>
|
155
|
+
body { font-family: Arial, sans-serif; margin: 40px; }
|
156
|
+
.ai-results { background: #f5f5f5; padding: 20px; border-radius: 8px; }
|
157
|
+
.feature { margin: 10px 0; padding: 10px; border-left: 4px solid #007cba; }
|
158
|
+
pre { background: #333; color: #fff; padding: 15px; border-radius: 4px; overflow-x: auto; }
|
159
|
+
</style>
|
160
|
+
</head>
|
161
|
+
<body>
|
162
|
+
<h1>🤖 Rack::AI Demo Application</h1>
|
163
|
+
<p>This request was analyzed by Rack::AI middleware.</p>
|
164
|
+
|
165
|
+
<div class="ai-results">
|
166
|
+
<h2>AI Analysis Results:</h2>
|
167
|
+
#{ai_results.map { |feature, result|
|
168
|
+
"<div class='feature'><strong>#{feature.to_s.capitalize}:</strong> #{result[:action]} (confidence: #{result[:confidence] || 'N/A'})</div>"
|
169
|
+
}.join}
|
170
|
+
</div>
|
171
|
+
|
172
|
+
<h2>Test Endpoints:</h2>
|
173
|
+
<ul>
|
174
|
+
<li><a href="/api/safe">Safe API endpoint</a></li>
|
175
|
+
<li><a href="/api/suspicious?q=SELECT * FROM users">Suspicious query</a></li>
|
176
|
+
<li><a href="/api/bulk">Bulk operation (rate limiting test)</a></li>
|
177
|
+
<li><a href="/health">Health check</a></li>
|
178
|
+
</ul>
|
179
|
+
|
180
|
+
<h2>Raw AI Results:</h2>
|
181
|
+
<pre>#{JSON.pretty_generate(ai_results)}</pre>
|
182
|
+
</body>
|
183
|
+
</html>
|
184
|
+
HTML
|
185
|
+
]]
|
186
|
+
}
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
if __FILE__ == $0
|
192
|
+
puts "🚀 Starting Rack::AI Demo Server..."
|
193
|
+
puts "Features: Classification, Security, Rate Limiting, Anomaly Detection"
|
194
|
+
puts "Visit: http://localhost:9292"
|
195
|
+
puts "Press Ctrl+C to stop"
|
196
|
+
|
197
|
+
# Use the comprehensive app
|
198
|
+
Rack::Handler::WEBrick.run(
|
199
|
+
ComprehensiveApp.app,
|
200
|
+
Port: 9292,
|
201
|
+
Host: '0.0.0.0'
|
202
|
+
)
|
203
|
+
end
|
@@ -1,8 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require "dry
|
4
|
-
require "dry
|
5
|
-
require "ostruct"
|
3
|
+
require "dry/configurable"
|
4
|
+
require "dry/validation"
|
6
5
|
|
7
6
|
module Rack
|
8
7
|
module AI
|
@@ -16,8 +15,8 @@ module Rack
|
|
16
15
|
setting :timeout, default: 30
|
17
16
|
setting :retries, default: 3
|
18
17
|
|
19
|
-
#
|
20
|
-
setting :features, default: [:classification, :
|
18
|
+
# Features
|
19
|
+
setting :features, default: [:classification, :security]
|
21
20
|
setting :fail_safe, default: true
|
22
21
|
setting :async_processing, default: true
|
23
22
|
|
@@ -55,6 +54,24 @@ module Rack
|
|
55
54
|
setting :redis_url, default: "redis://localhost:6379"
|
56
55
|
end
|
57
56
|
|
57
|
+
# Rate limiting configuration
|
58
|
+
setting :rate_limiting do
|
59
|
+
setting :window_size, default: 3600 # 1 hour in seconds
|
60
|
+
setting :max_requests, default: 1000 # max requests per window
|
61
|
+
setting :block_duration, default: 3600 # block duration in seconds
|
62
|
+
setting :cleanup_interval, default: 300 # cleanup old entries every 5 minutes
|
63
|
+
end
|
64
|
+
|
65
|
+
# Anomaly detection configuration
|
66
|
+
setting :anomaly_detection do
|
67
|
+
setting :sensitivity, default: 0.8 # Detection sensitivity (0-1)
|
68
|
+
setting :baseline_window, default: 86400 # 24 hours for baseline calculation
|
69
|
+
setting :min_requests_for_baseline, default: 100
|
70
|
+
setting :risk_threshold_monitor, default: 20
|
71
|
+
setting :risk_threshold_challenge, default: 50
|
72
|
+
setting :risk_threshold_block, default: 80
|
73
|
+
end
|
74
|
+
|
58
75
|
setting :routing do
|
59
76
|
setting :smart_routing_enabled, default: true
|
60
77
|
setting :suspicious_route, default: "/captcha"
|
@@ -89,28 +106,28 @@ module Rack
|
|
89
106
|
@explain_decisions = false
|
90
107
|
|
91
108
|
# Initialize nested configurations
|
92
|
-
@classification =
|
109
|
+
@classification = {
|
93
110
|
confidence_threshold: 0.8,
|
94
111
|
categories: [:spam, :bot, :human, :suspicious]
|
95
|
-
|
112
|
+
}
|
96
113
|
|
97
|
-
@moderation =
|
114
|
+
@moderation = {
|
98
115
|
toxicity_threshold: 0.7,
|
99
116
|
check_response: false,
|
100
117
|
block_on_violation: true
|
101
|
-
|
118
|
+
}
|
102
119
|
|
103
|
-
@caching =
|
120
|
+
@caching = {
|
104
121
|
predictive_enabled: true,
|
105
122
|
prefetch_threshold: 0.9,
|
106
123
|
redis_url: "redis://localhost:6379"
|
107
|
-
|
124
|
+
}
|
108
125
|
|
109
|
-
@routing =
|
126
|
+
@routing = {
|
110
127
|
smart_routing_enabled: true,
|
111
128
|
suspicious_route: "/captcha",
|
112
129
|
bot_route: "/api/bot"
|
113
|
-
|
130
|
+
}
|
114
131
|
end
|
115
132
|
|
116
133
|
def classification
|
@@ -195,13 +212,16 @@ module Rack
|
|
195
212
|
end
|
196
213
|
|
197
214
|
def provider_config
|
198
|
-
|
199
|
-
|
200
|
-
api_key: @api_key,
|
201
|
-
|
202
|
-
timeout: @timeout,
|
203
|
-
|
204
|
-
|
215
|
+
case @provider
|
216
|
+
when :openai
|
217
|
+
{ api_key: @api_key, api_url: @api_url, timeout: @timeout, retries: @retries }
|
218
|
+
when :huggingface
|
219
|
+
{ api_key: @api_key, api_url: @api_url, timeout: @timeout, retries: @retries }
|
220
|
+
when :local
|
221
|
+
{ api_url: @api_url, timeout: @timeout, retries: @retries }
|
222
|
+
else
|
223
|
+
{}
|
224
|
+
end
|
205
225
|
end
|
206
226
|
end
|
207
227
|
end
|
@@ -0,0 +1,236 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class AnomalyDetection
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :anomaly_detection
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
@baseline_metrics = {}
|
14
|
+
@request_patterns = {}
|
15
|
+
end
|
16
|
+
|
17
|
+
def enabled?
|
18
|
+
@config.feature_enabled?(:anomaly_detection)
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_response?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_request(env)
|
26
|
+
current_time = Time.now
|
27
|
+
client_ip = get_client_ip(env)
|
28
|
+
|
29
|
+
# Extract request metrics
|
30
|
+
metrics = extract_request_metrics(env)
|
31
|
+
|
32
|
+
# Update baseline and detect anomalies
|
33
|
+
anomalies = detect_anomalies(client_ip, metrics, current_time)
|
34
|
+
|
35
|
+
# Calculate risk score
|
36
|
+
risk_score = calculate_risk_score(anomalies, metrics)
|
37
|
+
|
38
|
+
# Determine action based on risk score
|
39
|
+
action = determine_action(risk_score)
|
40
|
+
|
41
|
+
{
|
42
|
+
action: action,
|
43
|
+
feature: :anomaly_detection,
|
44
|
+
timestamp: current_time.iso8601,
|
45
|
+
risk_score: risk_score,
|
46
|
+
anomalies: anomalies,
|
47
|
+
metrics: metrics,
|
48
|
+
baseline_deviation: calculate_baseline_deviation(metrics)
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
private
|
53
|
+
|
54
|
+
def get_client_ip(env)
|
55
|
+
env['HTTP_X_FORWARDED_FOR']&.split(',')&.first&.strip ||
|
56
|
+
env['HTTP_X_REAL_IP'] ||
|
57
|
+
env['REMOTE_ADDR'] ||
|
58
|
+
'unknown'
|
59
|
+
end
|
60
|
+
|
61
|
+
def extract_request_metrics(env)
|
62
|
+
{
|
63
|
+
request_size: calculate_request_size(env),
|
64
|
+
header_count: env.select { |k, _| k.start_with?('HTTP_') }.size,
|
65
|
+
path_length: env['PATH_INFO']&.length || 0,
|
66
|
+
query_complexity: calculate_query_complexity(env['QUERY_STRING']),
|
67
|
+
user_agent_entropy: calculate_entropy(env['HTTP_USER_AGENT']),
|
68
|
+
request_frequency: calculate_request_frequency(get_client_ip(env)),
|
69
|
+
time_of_day: Time.now.hour,
|
70
|
+
day_of_week: Time.now.wday
|
71
|
+
}
|
72
|
+
end
|
73
|
+
|
74
|
+
def calculate_request_size(env)
|
75
|
+
size = 0
|
76
|
+
size += env['CONTENT_LENGTH'].to_i if env['CONTENT_LENGTH']
|
77
|
+
size += env['QUERY_STRING']&.length || 0
|
78
|
+
env.each { |k, v| size += k.length + v.to_s.length if k.start_with?('HTTP_') }
|
79
|
+
size
|
80
|
+
end
|
81
|
+
|
82
|
+
def calculate_query_complexity(query_string)
|
83
|
+
return 0 unless query_string
|
84
|
+
|
85
|
+
params = query_string.split('&')
|
86
|
+
complexity = params.size
|
87
|
+
|
88
|
+
# Add complexity for nested parameters, arrays, etc.
|
89
|
+
params.each do |param|
|
90
|
+
complexity += 1 if param.include?('[')
|
91
|
+
complexity += 1 if param.include?('%')
|
92
|
+
complexity += param.scan(/[=&]/).size
|
93
|
+
end
|
94
|
+
|
95
|
+
complexity
|
96
|
+
end
|
97
|
+
|
98
|
+
def calculate_entropy(string)
|
99
|
+
return 0 unless string
|
100
|
+
|
101
|
+
frequencies = Hash.new(0)
|
102
|
+
string.each_char { |char| frequencies[char] += 1 }
|
103
|
+
|
104
|
+
entropy = 0
|
105
|
+
string.length.times do
|
106
|
+
freq = frequencies.values.shift
|
107
|
+
next if freq == 0
|
108
|
+
|
109
|
+
probability = freq.to_f / string.length
|
110
|
+
entropy -= probability * Math.log2(probability)
|
111
|
+
end
|
112
|
+
|
113
|
+
entropy
|
114
|
+
end
|
115
|
+
|
116
|
+
def calculate_request_frequency(client_ip)
|
117
|
+
current_time = Time.now
|
118
|
+
@request_patterns[client_ip] ||= []
|
119
|
+
|
120
|
+
# Clean old entries (older than 1 hour)
|
121
|
+
@request_patterns[client_ip].reject! { |time| current_time - time > 3600 }
|
122
|
+
|
123
|
+
# Add current request
|
124
|
+
@request_patterns[client_ip] << current_time
|
125
|
+
|
126
|
+
# Return requests per minute
|
127
|
+
recent_requests = @request_patterns[client_ip].count { |time| current_time - time <= 60 }
|
128
|
+
recent_requests
|
129
|
+
end
|
130
|
+
|
131
|
+
def detect_anomalies(client_ip, metrics, current_time)
|
132
|
+
anomalies = []
|
133
|
+
|
134
|
+
# Update baseline for this client
|
135
|
+
@baseline_metrics[client_ip] ||= initialize_baseline
|
136
|
+
baseline = @baseline_metrics[client_ip]
|
137
|
+
|
138
|
+
metrics.each do |metric, value|
|
139
|
+
next unless value.is_a?(Numeric)
|
140
|
+
|
141
|
+
# Calculate z-score
|
142
|
+
mean = baseline[metric][:mean]
|
143
|
+
std_dev = baseline[metric][:std_dev]
|
144
|
+
|
145
|
+
if std_dev > 0
|
146
|
+
z_score = (value - mean).abs / std_dev
|
147
|
+
|
148
|
+
if z_score > 3 # 3 standard deviations
|
149
|
+
anomalies << {
|
150
|
+
metric: metric,
|
151
|
+
value: value,
|
152
|
+
expected_range: [mean - 2 * std_dev, mean + 2 * std_dev],
|
153
|
+
z_score: z_score,
|
154
|
+
severity: z_score > 5 ? :high : :medium
|
155
|
+
}
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Update baseline with exponential moving average
|
160
|
+
alpha = 0.1
|
161
|
+
baseline[metric][:mean] = alpha * value + (1 - alpha) * mean
|
162
|
+
|
163
|
+
# Update standard deviation
|
164
|
+
variance = baseline[metric][:variance] || 0
|
165
|
+
new_variance = alpha * (value - mean) ** 2 + (1 - alpha) * variance
|
166
|
+
baseline[metric][:variance] = new_variance
|
167
|
+
baseline[metric][:std_dev] = Math.sqrt(new_variance)
|
168
|
+
end
|
169
|
+
|
170
|
+
anomalies
|
171
|
+
end
|
172
|
+
|
173
|
+
def initialize_baseline
|
174
|
+
Hash.new do |hash, key|
|
175
|
+
hash[key] = { mean: 0, variance: 0, std_dev: 1 }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
def calculate_risk_score(anomalies, metrics)
|
180
|
+
base_score = 0
|
181
|
+
|
182
|
+
# Score based on anomalies
|
183
|
+
anomalies.each do |anomaly|
|
184
|
+
case anomaly[:severity]
|
185
|
+
when :high
|
186
|
+
base_score += 30
|
187
|
+
when :medium
|
188
|
+
base_score += 15
|
189
|
+
else
|
190
|
+
base_score += 5
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
# Additional risk factors
|
195
|
+
base_score += 10 if metrics[:request_frequency] > 60 # More than 1 req/sec
|
196
|
+
base_score += 15 if metrics[:request_size] > 1_000_000 # Large requests
|
197
|
+
base_score += 5 if metrics[:query_complexity] > 50 # Complex queries
|
198
|
+
base_score += 10 if metrics[:user_agent_entropy] < 2 # Suspicious UA
|
199
|
+
|
200
|
+
# Time-based risk (higher risk during off-hours)
|
201
|
+
if metrics[:time_of_day] < 6 || metrics[:time_of_day] > 22
|
202
|
+
base_score += 5
|
203
|
+
end
|
204
|
+
|
205
|
+
[base_score, 100].min # Cap at 100
|
206
|
+
end
|
207
|
+
|
208
|
+
def calculate_baseline_deviation(metrics)
|
209
|
+
# Simple calculation of how much current metrics deviate from expected
|
210
|
+
deviation_score = 0
|
211
|
+
|
212
|
+
metrics.each do |_, value|
|
213
|
+
next unless value.is_a?(Numeric)
|
214
|
+
# Simplified deviation calculation
|
215
|
+
deviation_score += value > 1000 ? 1 : 0
|
216
|
+
end
|
217
|
+
|
218
|
+
deviation_score
|
219
|
+
end
|
220
|
+
|
221
|
+
def determine_action(risk_score)
|
222
|
+
case risk_score
|
223
|
+
when 0..20
|
224
|
+
:allow
|
225
|
+
when 21..50
|
226
|
+
:monitor
|
227
|
+
when 51..80
|
228
|
+
:challenge
|
229
|
+
else
|
230
|
+
:block
|
231
|
+
end
|
232
|
+
end
|
233
|
+
end
|
234
|
+
end
|
235
|
+
end
|
236
|
+
end
|
@@ -0,0 +1,114 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Rack
|
4
|
+
module AI
|
5
|
+
module Features
|
6
|
+
class RateLimiting
|
7
|
+
attr_reader :name, :provider, :config
|
8
|
+
|
9
|
+
def initialize(provider, config)
|
10
|
+
@name = :rate_limiting
|
11
|
+
@provider = provider
|
12
|
+
@config = config
|
13
|
+
@rate_store = {}
|
14
|
+
@cleanup_timer = Time.now
|
15
|
+
end
|
16
|
+
|
17
|
+
def enabled?
|
18
|
+
@config.feature_enabled?(:rate_limiting)
|
19
|
+
end
|
20
|
+
|
21
|
+
def process_response?
|
22
|
+
false
|
23
|
+
end
|
24
|
+
|
25
|
+
def process_request(env)
|
26
|
+
client_ip = get_client_ip(env)
|
27
|
+
current_time = Time.now
|
28
|
+
|
29
|
+
# Cleanup old entries every 5 minutes
|
30
|
+
cleanup_old_entries if current_time - @cleanup_timer > 300
|
31
|
+
|
32
|
+
# Get rate limit settings
|
33
|
+
window_size = @config.rate_limiting.window_size || 3600 # 1 hour default
|
34
|
+
max_requests = @config.rate_limiting.max_requests || 1000 # 1000 requests default
|
35
|
+
|
36
|
+
# Initialize client data if not exists
|
37
|
+
@rate_store[client_ip] ||= { requests: [], blocked_until: nil }
|
38
|
+
client_data = @rate_store[client_ip]
|
39
|
+
|
40
|
+
# Check if client is currently blocked
|
41
|
+
if client_data[:blocked_until] && current_time < client_data[:blocked_until]
|
42
|
+
return {
|
43
|
+
action: :block,
|
44
|
+
reason: "Rate limit exceeded - blocked until #{client_data[:blocked_until]}",
|
45
|
+
status: 429,
|
46
|
+
feature: :rate_limiting,
|
47
|
+
timestamp: current_time.iso8601,
|
48
|
+
retry_after: (client_data[:blocked_until] - current_time).to_i
|
49
|
+
}
|
50
|
+
end
|
51
|
+
|
52
|
+
# Remove old requests outside the window
|
53
|
+
window_start = current_time - window_size
|
54
|
+
client_data[:requests].reject! { |timestamp| timestamp < window_start }
|
55
|
+
|
56
|
+
# Check if rate limit is exceeded
|
57
|
+
if client_data[:requests].length >= max_requests
|
58
|
+
block_duration = @config.rate_limiting.block_duration || 3600 # 1 hour default
|
59
|
+
client_data[:blocked_until] = current_time + block_duration
|
60
|
+
|
61
|
+
return {
|
62
|
+
action: :block,
|
63
|
+
reason: "Rate limit exceeded: #{client_data[:requests].length}/#{max_requests} requests in #{window_size}s",
|
64
|
+
status: 429,
|
65
|
+
feature: :rate_limiting,
|
66
|
+
timestamp: current_time.iso8601,
|
67
|
+
retry_after: block_duration,
|
68
|
+
requests_count: client_data[:requests].length,
|
69
|
+
window_size: window_size
|
70
|
+
}
|
71
|
+
end
|
72
|
+
|
73
|
+
# Add current request
|
74
|
+
client_data[:requests] << current_time
|
75
|
+
|
76
|
+
{
|
77
|
+
action: :allow,
|
78
|
+
feature: :rate_limiting,
|
79
|
+
timestamp: current_time.iso8601,
|
80
|
+
requests_count: client_data[:requests].length,
|
81
|
+
requests_remaining: max_requests - client_data[:requests].length,
|
82
|
+
window_size: window_size,
|
83
|
+
reset_time: (window_start + window_size).iso8601
|
84
|
+
}
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def get_client_ip(env)
|
90
|
+
# Try various headers for real IP
|
91
|
+
forwarded_for = env['HTTP_X_FORWARDED_FOR']
|
92
|
+
return forwarded_for.split(',').first.strip if forwarded_for
|
93
|
+
|
94
|
+
real_ip = env['HTTP_X_REAL_IP']
|
95
|
+
return real_ip if real_ip
|
96
|
+
|
97
|
+
env['REMOTE_ADDR'] || 'unknown'
|
98
|
+
end
|
99
|
+
|
100
|
+
def cleanup_old_entries
|
101
|
+
current_time = Time.now
|
102
|
+
@rate_store.each do |ip, data|
|
103
|
+
# Remove clients with no recent activity and no active blocks
|
104
|
+
if data[:requests].empty? &&
|
105
|
+
(data[:blocked_until].nil? || current_time > data[:blocked_until])
|
106
|
+
@rate_store.delete(ip)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
@cleanup_timer = current_time
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
114
|
+
end
|
data/lib/rack/ai/middleware.rb
CHANGED
@@ -83,7 +83,9 @@ module Rack
|
|
83
83
|
routing: Features::Routing,
|
84
84
|
logging: Features::Logging,
|
85
85
|
enhancement: Features::Enhancement,
|
86
|
-
security: Features::Security
|
86
|
+
security: Features::Security,
|
87
|
+
rate_limiting: Features::RateLimiting,
|
88
|
+
anomaly_detection: Features::AnomalyDetection
|
87
89
|
}
|
88
90
|
|
89
91
|
features = @config.features.map do |feature_name|
|
@@ -0,0 +1,130 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'logger'
|
4
|
+
require 'json'
|
5
|
+
|
6
|
+
module Rack
|
7
|
+
module AI
|
8
|
+
module Utils
|
9
|
+
class EnhancedLogger
|
10
|
+
LOG_LEVELS = {
|
11
|
+
debug: ::Logger::DEBUG,
|
12
|
+
info: ::Logger::INFO,
|
13
|
+
warn: ::Logger::WARN,
|
14
|
+
error: ::Logger::ERROR,
|
15
|
+
fatal: ::Logger::FATAL
|
16
|
+
}.freeze
|
17
|
+
|
18
|
+
def self.instance
|
19
|
+
@instance ||= new
|
20
|
+
end
|
21
|
+
|
22
|
+
def initialize(output = $stdout, level: :info, format: :json)
|
23
|
+
@logger = ::Logger.new(output)
|
24
|
+
@logger.level = LOG_LEVELS[level] || ::Logger::INFO
|
25
|
+
@format = format
|
26
|
+
@logger.formatter = method(:format_message)
|
27
|
+
end
|
28
|
+
|
29
|
+
def debug(message, context = {})
|
30
|
+
log(:debug, message, context)
|
31
|
+
end
|
32
|
+
|
33
|
+
def info(message, context = {})
|
34
|
+
log(:info, message, context)
|
35
|
+
end
|
36
|
+
|
37
|
+
def warn(message, context = {})
|
38
|
+
log(:warn, message, context)
|
39
|
+
end
|
40
|
+
|
41
|
+
def error(message, context = {})
|
42
|
+
log(:error, message, context)
|
43
|
+
end
|
44
|
+
|
45
|
+
def fatal(message, context = {})
|
46
|
+
log(:fatal, message, context)
|
47
|
+
end
|
48
|
+
|
49
|
+
def log_request(env, duration: nil, status: nil)
|
50
|
+
context = {
|
51
|
+
method: env['REQUEST_METHOD'],
|
52
|
+
path: env['PATH_INFO'],
|
53
|
+
query: env['QUERY_STRING'],
|
54
|
+
user_agent: env['HTTP_USER_AGENT'],
|
55
|
+
remote_ip: env['REMOTE_ADDR'],
|
56
|
+
duration_ms: duration ? (duration * 1000).round(2) : nil,
|
57
|
+
status: status
|
58
|
+
}.compact
|
59
|
+
|
60
|
+
info("HTTP Request", context)
|
61
|
+
end
|
62
|
+
|
63
|
+
def log_ai_processing(feature, result, duration: nil)
|
64
|
+
context = {
|
65
|
+
feature: feature,
|
66
|
+
action: result[:action],
|
67
|
+
confidence: result[:confidence],
|
68
|
+
duration_ms: duration ? (duration * 1000).round(2) : nil,
|
69
|
+
provider: result[:provider]
|
70
|
+
}.compact
|
71
|
+
|
72
|
+
info("AI Processing", context)
|
73
|
+
end
|
74
|
+
|
75
|
+
def log_error(error, context = {})
|
76
|
+
error_context = {
|
77
|
+
error_class: error.class.name,
|
78
|
+
error_message: error.message,
|
79
|
+
backtrace: error.backtrace&.first(5)
|
80
|
+
}.merge(context)
|
81
|
+
|
82
|
+
error("Application Error", error_context)
|
83
|
+
end
|
84
|
+
|
85
|
+
private
|
86
|
+
|
87
|
+
def log(level, message, context)
|
88
|
+
@logger.public_send(level) do
|
89
|
+
case @format
|
90
|
+
when :json
|
91
|
+
format_json(level, message, context)
|
92
|
+
when :structured
|
93
|
+
format_structured(level, message, context)
|
94
|
+
else
|
95
|
+
format_simple(level, message, context)
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
99
|
+
|
100
|
+
def format_message(severity, datetime, progname, msg)
|
101
|
+
msg
|
102
|
+
end
|
103
|
+
|
104
|
+
def format_json(level, message, context)
|
105
|
+
{
|
106
|
+
timestamp: Time.now.iso8601,
|
107
|
+
level: level.to_s.upcase,
|
108
|
+
message: message,
|
109
|
+
component: 'rack-ai',
|
110
|
+
**context
|
111
|
+
}.to_json + "\n"
|
112
|
+
end
|
113
|
+
|
114
|
+
def format_structured(level, message, context)
|
115
|
+
timestamp = Time.now.strftime('%Y-%m-%d %H:%M:%S')
|
116
|
+
context_str = context.empty? ? '' : " #{context.inspect}"
|
117
|
+
"[#{timestamp}] #{level.to_s.upcase} [rack-ai] #{message}#{context_str}\n"
|
118
|
+
end
|
119
|
+
|
120
|
+
def format_simple(level, message, context)
|
121
|
+
context_str = context.empty? ? '' : " (#{context.map { |k, v| "#{k}=#{v}" }.join(', ')})"
|
122
|
+
"#{message}#{context_str}\n"
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
# Backward compatibility
|
127
|
+
Logger = EnhancedLogger
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
data/lib/rack/ai/utils/logger.rb
CHANGED
@@ -7,6 +7,22 @@ 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
|
+
|
10
26
|
class << self
|
11
27
|
def logger
|
12
28
|
@logger ||= build_logger
|
data/lib/rack/ai/version.rb
CHANGED
data/lib/rack/ai.rb
CHANGED
@@ -8,15 +8,18 @@ require_relative "ai/providers/openai"
|
|
8
8
|
require_relative "ai/providers/huggingface"
|
9
9
|
require_relative "ai/providers/local"
|
10
10
|
require_relative "ai/features/classification"
|
11
|
+
require_relative "ai/features/security"
|
11
12
|
require_relative "ai/features/moderation"
|
12
13
|
require_relative "ai/features/caching"
|
13
14
|
require_relative "ai/features/routing"
|
14
15
|
require_relative "ai/features/logging"
|
15
16
|
require_relative "ai/features/enhancement"
|
16
|
-
require_relative "ai/features/
|
17
|
+
require_relative "ai/features/rate_limiting"
|
18
|
+
require_relative "ai/features/anomaly_detection"
|
17
19
|
require_relative "ai/utils/logger"
|
18
20
|
require_relative "ai/utils/metrics"
|
19
21
|
require_relative "ai/utils/sanitizer"
|
22
|
+
require_relative "ai/utils/enhanced_logger"
|
20
23
|
|
21
24
|
module Rack
|
22
25
|
module AI
|
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.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Ahmet KAHRAMAN
|
@@ -241,15 +241,18 @@ files:
|
|
241
241
|
- ROADMAP.md
|
242
242
|
- Rakefile
|
243
243
|
- benchmarks/performance_benchmark.rb
|
244
|
+
- examples/comprehensive_example.rb
|
244
245
|
- examples/rails_integration.rb
|
245
246
|
- examples/sinatra_integration.rb
|
246
247
|
- lib/rack/ai.rb
|
247
248
|
- lib/rack/ai/configuration.rb
|
249
|
+
- lib/rack/ai/features/anomaly_detection.rb
|
248
250
|
- lib/rack/ai/features/caching.rb
|
249
251
|
- lib/rack/ai/features/classification.rb
|
250
252
|
- lib/rack/ai/features/enhancement.rb
|
251
253
|
- lib/rack/ai/features/logging.rb
|
252
254
|
- lib/rack/ai/features/moderation.rb
|
255
|
+
- lib/rack/ai/features/rate_limiting.rb
|
253
256
|
- lib/rack/ai/features/routing.rb
|
254
257
|
- lib/rack/ai/features/security.rb
|
255
258
|
- lib/rack/ai/middleware.rb
|
@@ -257,6 +260,7 @@ files:
|
|
257
260
|
- lib/rack/ai/providers/huggingface.rb
|
258
261
|
- lib/rack/ai/providers/local.rb
|
259
262
|
- lib/rack/ai/providers/openai.rb
|
263
|
+
- lib/rack/ai/utils/enhanced_logger.rb
|
260
264
|
- lib/rack/ai/utils/logger.rb
|
261
265
|
- lib/rack/ai/utils/metrics.rb
|
262
266
|
- lib/rack/ai/utils/sanitizer.rb
|