Class: QueryGuard::Core::Finding

Inherits:
Object
  • Object
show all
Defined in:
lib/query_guard/core/finding.rb

Overview

Immutable result of a rule analysis. Represents a single violation or finding detected by an analyzer.

A Finding encapsulates all information about a detected issue:

  • What rule triggered (analyzer_name, rule_name)
  • How severe it is (severity)
  • Where it occurred (file_path, line_number, sql)
  • Why it matters (title, description, recommendations)
  • Context data (metadata)

Constant Summary collapse

SEVERITIES =
i[info warn error].freeze

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(analyzer_name:, rule_name:, severity: :warn, title: nil, description: nil, message: nil, file_path: nil, line_number: nil, sql: nil, metadata: {}, query: nil, recommendations: []) ⇒ Finding

Returns a new instance of Finding.



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
# File 'lib/query_guard/core/finding.rb', line 32

def initialize(
  analyzer_name:,
  rule_name:,
  severity: :warn,
  title: nil,
  description: nil,
  message: nil,
  file_path: nil,
  line_number: nil,
  sql: nil,
  metadata: {},
  query: nil,
  recommendations: []
)
  @analyzer_name = analyzer_name.to_sym
  @rule_name = rule_name.to_sym
  @severity = validate_severity(severity)

  # Message handling: use title/description or fall back to message

  @title = title.to_s.freeze if title
  @description = description.to_s.freeze if description
  @message = (message || @title || "").to_s.freeze

  # Location info

  @file_path = file_path.to_s.freeze if file_path
  @line_number = line_number.to_i if line_number
  @sql = sql.to_s.freeze if sql

  # Recommendations

  @recommendations = Array(recommendations).map { |r| r.to_s.freeze }.freeze

  # Metadata

   = .freeze
  @query = query
  @created_at = Time.now.freeze

  # Generate deterministic ID based on analyzer, rule, and sql for deduplication

  @id = generate_id
end

Instance Attribute Details

#analyzer_name ⇒ Object (readonly)

Core identification



18
19
20
# File 'lib/query_guard/core/finding.rb', line 18

def analyzer_name
  @analyzer_name
end

#created_at ⇒ Object (readonly)

Additional context



27
28
29
# File 'lib/query_guard/core/finding.rb', line 27

def created_at
  @created_at
end

#description ⇒ Object (readonly)

Message and description (user-facing)



21
22
23
# File 'lib/query_guard/core/finding.rb', line 21

def description
  @description
end

#file_path ⇒ Object (readonly)

Location information



24
25
26
# File 'lib/query_guard/core/finding.rb', line 24

def file_path
  @file_path
end

#id ⇒ Object (readonly)

Internal ID for tracking/deduplication



30
31
32
# File 'lib/query_guard/core/finding.rb', line 30

def id
  @id
end

#line_number ⇒ Object (readonly)

Location information



24
25
26
# File 'lib/query_guard/core/finding.rb', line 24

def line_number
  @line_number
end

#message ⇒ Object (readonly)

Message and description (user-facing)



21
22
23
# File 'lib/query_guard/core/finding.rb', line 21

def message
  @message
end

#metadata ⇒ Object (readonly)

Additional context



27
28
29
# File 'lib/query_guard/core/finding.rb', line 27

def 
  
end

#query ⇒ Object (readonly)

Additional context



27
28
29
# File 'lib/query_guard/core/finding.rb', line 27

def query
  @query
end

#recommendations ⇒ Object (readonly)

Message and description (user-facing)



21
22
23
# File 'lib/query_guard/core/finding.rb', line 21

def recommendations
  @recommendations
end

#rule_name ⇒ Object (readonly)

Core identification



18
19
20
# File 'lib/query_guard/core/finding.rb', line 18

def rule_name
  @rule_name
end

#severity ⇒ Object (readonly)

Core identification



18
19
20
# File 'lib/query_guard/core/finding.rb', line 18

def severity
  @severity
end

#sql ⇒ Object (readonly)

Location information



24
25
26
# File 'lib/query_guard/core/finding.rb', line 24

def sql
  @sql
end

#title ⇒ Object (readonly)

Message and description (user-facing)



21
22
23
# File 'lib/query_guard/core/finding.rb', line 21

def title
  @title
end

Instance Method Details

#==(other) ⇒ Object Also known as: eql?

Compare findings by key attributes



73
74
75
76
77
78
79
# File 'lib/query_guard/core/finding.rb', line 73

def ==(other)
  other.is_a?(Finding) &&
    analyzer_name == other.analyzer_name &&
    rule_name == other.rule_name &&
    severity == other.severity &&
    message == other.message
end

#has_location? ⇒ Boolean

Check if finding has location information

Returns:

  • (Boolean)


136
137
138
# File 'lib/query_guard/core/finding.rb', line 136

def has_location?
  !file_path.nil?
end

#hash ⇒ Object

Hash based on ID for Set operations



82
83
84
# File 'lib/query_guard/core/finding.rb', line 82

def hash
  id.hash
end

#inspect ⇒ Object

Inspection string



131
132
133
# File 'lib/query_guard/core/finding.rb', line 131

def inspect
  "#<Finding id=#{id[0, 8]} #{analyzer_name}:#{rule_name} severity=#{severity}>"
end

#to_h ⇒ Object

Serialize to hash for reporting/API Useful for JSON output, CI integration, and telemetry



90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# File 'lib/query_guard/core/finding.rb', line 90

def to_h
  {
    id: id,
    analyzer: analyzer_name,
    rule: rule_name,
    severity: severity,
    title: title,
    description: description,
    message: message,
    file_path: file_path,
    line_number: line_number,
    sql: sql,
    metadata: ,
    recommendations: recommendations,
    created_at: created_at&.iso8601,
    query: query&.to_h
  }.compact
end

#to_json_h ⇒ Object

Serialize to JSON-friendly hash (excludes large/binary data)



110
111
112
113
114
115
# File 'lib/query_guard/core/finding.rb', line 110

def to_json_h
  h = to_h
  # Optionally truncate SQL for JSON payloads

  h[:sql] = truncate_sql(h[:sql], 500) if h[:sql]
  h.except(:query) # Exclude full query object from JSON

end

#to_log_s ⇒ Object

Detailed log format



123
124
125
126
127
128
# File 'lib/query_guard/core/finding.rb', line 123

def to_log_s
  parts = [to_s]
  parts << "File: #{file_path}:#{line_number}" if file_path
  parts << "SQL: #{truncate_sql(sql, 100)}" if sql
  parts.join(" | ")
end

#to_s ⇒ Object

Human-readable string for logs



118
119
120
# File 'lib/query_guard/core/finding.rb', line 118

def to_s
  "[#{severity.upcase}] #{analyzer_name}:#{rule_name} - #{message}"
end