Module: QueryGuard::Subscriber

Defined in:
lib/query_guard/subscriber.rb

Constant Summary collapse

SQL_EVENT =
"sql.active_record"

Class Method Summary collapse

Class Method Details

.install!(config) ⇒ Object



9
10
11
12
13
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
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
# File 'lib/query_guard/subscriber.rb', line 9

def self.install!(config)
  return if @installed

  @subscriber = ActiveSupport::Notifications.subscribe(SQL_EVENT) do |_, started, finished, _, payload|
    context = Thread.current[:query_guard_context]
    next unless context # only track inside our middleware window

    stats = Thread.current[:query_guard_stats]
    next unless stats

    name = payload[:name].to_s
    next if name == "SCHEMA"

    sql = payload[:sql].to_s
    next if config.ignored_sql.any? { |r| r === sql }

    duration_ms = (finished - started) * 1000.0
    
    # Collect query into context (new behavior)
    context.add_query(
      sql: sql,
      duration_ms: duration_ms,
      name: name,
      started_at: Time.at(started),
      finished_at: Time.at(finished)
    )

    # Legacy: Also update Thread.current stats for backward compatibility
    stats = Thread.current[:query_guard_stats] ||= { count: 0, total_duration_ms: 0.0, violations: [] }
    stats[:count] += 1
    stats[:total_duration_ms] += duration_ms

    fp = QueryGuard::Security.fingerprint(sql)
    stats[:fingerprints][fp] += 1

    if config.max_duration_ms_per_query && duration_ms > config.max_duration_ms_per_query
      stats[:violations] << { type: :slow_query, duration_ms: duration_ms.round(2), sql: sql }
    end

    if config.block_select_star && sql =~ /\bSELECT\s+\*/i
      stats[:violations] << { type: :select_star, sql: sql }
    end

    # --- SQL Injection detection ---
    if config.enable_security && config.detect_sql_injection
      if QueryGuard::Security.suspicious_sql_injection?(sql, config.sql_injection_patterns)
        stats[:violations] << { type: :sql_injection_suspected, sql: sql }
      end
    end

    # --- Data exfiltration query-shape heuristic ---
    if config.enable_security && config.detect_data_exfiltration
      if QueryGuard::Security.possible_exfiltration_query?(sql)
        stats[:violations] << { type: :possible_data_exfiltration_query, sql: sql }
      end
    end

    max = config.max_query_events_per_req || 200
    if stats[:queries].length < max
      stats[:queries] << {
        sql: sql,
        duration_ms: duration_ms.round(2),
        occurred_at: Time.now.utc.iso8601
      }
    end
  end

  @installed = true
end