Class: ShieldPressTracker::SecurityCollector

Inherits:
Object
  • Object
show all
Defined in:
lib/shieldpress_tracker/collectors.rb

Constant Summary collapse

RULES =
[
  [:sql_injection, :critical, /\b(union|select|drop|delete)\b.*\b(from|table|where)\b|--|\/\*/i],
  [:xss_attempt, :high, /<script|javascript:|onerror\s*=/i],
  [:command_injection, :critical, /[;&|`]\s*(rm|curl|wget|bash|sh|python)\b|\$\(/i],
  [:suspicious_path, :high, /\.\.\/|%2e%2e|\/\.env|\/\.git/i],
  [:bot_detected, :medium, /sqlmap|nikto|nmap|burp|nuclei|wpscan/i]
].freeze

Instance Method Summary collapse

Constructor Details

#initialize(maximum) ⇒ SecurityCollector

Returns a new instance of SecurityCollector.



67
# File 'lib/shieldpress_tracker/collectors.rb', line 67

def initialize(maximum); @maximum, @events, @ips, @mutex = maximum, [], {}, Mutex.new end

Instance Method Details

#analyze(url:, method: "GET", headers: {}, body: nil, ip: nil, query: nil) ⇒ Object



75
76
77
78
79
80
81
82
83
84
85
# File 'lib/shieldpress_tracker/collectors.rb', line 75

def analyze(url:, method: "GET", headers: {}, body: nil, ip: nil, query: nil)
  headers = headers.transform_keys { |k| k.to_s.downcase }; text = "#{url} #{body} #{query.to_json}"; ua = headers["user-agent"].to_s
  RULES.each do |type, severity, pattern|
    target = type == :bot_detected ? ua : text
    record(type: type, severity: severity, message: "#{type.to_s.tr('_', ' ')} detected in request", ip: ip, url: url,
           method: method, userAgent: ua, payload: %i[sql_injection xss_attempt command_injection].include?(type) ? text[0, 500] : nil) if pattern.match?(target)
  end
  record(type: :oversized_payload, severity: :medium, message: "Request payload exceeds 10MB", ip: ip, url: url, method: method) if headers["content-length"].to_i > 10 * 1024 * 1024
rescue StandardError
  nil
end

#flushObject



86
87
88
89
90
91
92
93
94
95
# File 'lib/shieldpress_tracker/collectors.rb', line 86

def flush
  events = @mutex.synchronize { rows = @events; @events = []; rows }
  return nil if events.empty? && @ips.empty?
  severity = %i[critical high medium low info].to_h { |x| [x, 0] }; types = Hash.new(0)
  events.each { |e| severity[e[:severity]] += 1; types[e[:type]] += 1 }
  ips = @ips.map { |ip, row| { ip: ip, count: row[:count], types: row[:types] } }.sort_by { |x| -x[:count] }.first(10)
  { totalEvents: events.length, bySeverity: severity, byType: types, topOffendingIps: ips, events: events.last(50) }
rescue StandardError
  nil
end

#record(type:, severity:, message:, **data) ⇒ Object



68
69
70
71
72
73
74
# File 'lib/shieldpress_tracker/collectors.rb', line 68

def record(type:, severity:, message:, **data)
  event = { type: type, severity: severity, message: message.to_s[0, 4096], timestamp: Time.now.utc.iso8601 }.merge(data.compact)
  @mutex.synchronize do
    @events << event; @events = @events.last(@maximum)
    if data[:ip]; row = (@ips[data[:ip]] ||= { count: 0, types: [] }); row[:count] += 1; row[:types] |= [type]; end
  end
end