Class: RailVerdict::Baseline

Inherits:
Object
  • Object
show all
Defined in:
lib/rail_verdict/baseline.rb

Defined Under Namespace

Classes: IncompatibleError

Constant Summary collapse

SCHEMA_VERSION =
"1.0"
DEFAULT_FILENAME =
".railverdict-baseline.json"

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(hash) ⇒ Baseline

Returns a new instance of Baseline.



20
21
22
23
24
25
26
27
# File 'lib/rail_verdict/baseline.rb', line 20

def initialize(hash)
  @hash = hash.freeze
  @entries = hash.fetch("entries").freeze
  @created_at = hash.fetch("created_at").freeze
  @configuration_digest = hash.fetch("configuration_digest").freeze
  @analyzer_versions = hash.fetch("analyzer_versions").freeze
  freeze
end

Instance Attribute Details

#analyzer_versionsObject (readonly)

Returns the value of attribute analyzer_versions.



18
19
20
# File 'lib/rail_verdict/baseline.rb', line 18

def analyzer_versions
  @analyzer_versions
end

#configuration_digestObject (readonly)

Returns the value of attribute configuration_digest.



18
19
20
# File 'lib/rail_verdict/baseline.rb', line 18

def configuration_digest
  @configuration_digest
end

#created_atObject (readonly)

Returns the value of attribute created_at.



18
19
20
# File 'lib/rail_verdict/baseline.rb', line 18

def created_at
  @created_at
end

#entriesObject (readonly)

Returns the value of attribute entries.



18
19
20
# File 'lib/rail_verdict/baseline.rb', line 18

def entries
  @entries
end

#hashObject (readonly)

Returns the value of attribute hash.



18
19
20
# File 'lib/rail_verdict/baseline.rb', line 18

def hash
  @hash
end

Class Method Details

.create(findings:, configuration:, analyzer_versions:, clock: Time.now.utc) ⇒ Object

Raises:



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
# File 'lib/rail_verdict/baseline.rb', line 41

def self.create(findings:, configuration:, analyzer_versions:, clock: Time.now.utc)
  created_at = clock.utc.iso8601
  entries = findings.sort_by(&:sort_key).map do |finding|
    {
      "fingerprint" => finding.fingerprint,
      "analyzer" => finding.analyzer,
      "rule_id" => finding.rule_id,
      "path" => finding.location.fetch("path"),
      "message" => finding.message,
      "first_seen" => created_at
    }
  end
  entries = entries.uniq { |entry| entry.fetch("fingerprint") }.sort_by { |entry| entry.fetch("fingerprint") }
  hash = {
    "schema_version" => SCHEMA_VERSION,
    "fingerprint_version" => Fingerprint::VERSION,
    "algorithm" => Fingerprint::ALGORITHM,
    "payload_schema" => Fingerprint::PAYLOAD_SCHEMA,
    "created_at" => created_at,
    "created_by" => "railverdict #{RailVerdict::VERSION}",
    "configuration_digest" => configuration.digest,
    "analyzer_versions" => analyzer_versions.transform_keys(&:to_s).sort.to_h,
    "entries" => entries
  }
  errors = SchemaValidator.validate_baseline(hash)
  raise IncompatibleError, "baseline validation failed: #{errors.join('; ')}" unless errors.empty?

  new(hash)
end

.default_path(repository_root) ⇒ Object



71
72
73
# File 'lib/rail_verdict/baseline.rb', line 71

def self.default_path(repository_root)
  File.join(File.realpath(repository_root), DEFAULT_FILENAME)
end

.read(path) ⇒ Object



117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
# File 'lib/rail_verdict/baseline.rb', line 117

def self.read(path)
  unless File.file?(path)
    raise IncompatibleError, "baseline not found at #{path}"
  end
  raw = File.binread(path)
  begin
    data = JSON.parse(raw)
  rescue JSON::ParserError => error
    raise IncompatibleError, "baseline at #{path} is corrupted: #{error.message}; re-create with `railverdict baseline create`"
  end
  errors = SchemaValidator.validate_baseline(data)
  unless errors.empty?
    raise IncompatibleError, "baseline at #{path} is incompatible: #{errors.join('; ')}; re-create with `railverdict baseline create`"
  end
  fingerprints = data.fetch("entries").map { |entry| entry.fetch("fingerprint") }
  if fingerprints.uniq.length != fingerprints.length
    raise IncompatibleError, "baseline at #{path} contains duplicate fingerprints; re-create with `railverdict baseline create`"
  end
  new(data)
end

.read_optional(path) ⇒ Object



138
139
140
141
142
# File 'lib/rail_verdict/baseline.rb', line 138

def self.read_optional(path)
  return nil unless File.file?(path)

  read(path)
end

.resolve_path(repository_root:, configuration:, output_override: nil) ⇒ Object



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

def self.resolve_path(repository_root:, configuration:, output_override: nil)
  root = File.realpath(repository_root)
  if output_override && !output_override.to_s.strip.empty?
    path = output_override.to_s
    return Pathname.new(path).absolute? ? path : File.expand_path(path, root)
  end
  if configuration.respond_to?(:baseline_path) && configuration.baseline_path
    configured = configuration.baseline_path.to_s
    return Pathname.new(configured).absolute? ? configured : File.expand_path(configured, root)
  end
  File.join(root, DEFAULT_FILENAME)
end

.validate_hash(hash) ⇒ Object

Raises:



144
145
146
147
148
149
# File 'lib/rail_verdict/baseline.rb', line 144

def self.validate_hash(hash)
  errors = SchemaValidator.validate_baseline(hash)
  raise IncompatibleError, errors.join("; ") unless errors.empty?

  hash
end

.write(path:, baseline:, force: false) ⇒ Object



88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
# File 'lib/rail_verdict/baseline.rb', line 88

def self.write(path:, baseline:, force: false)
  if File.exist?(path) && !force
    raise UsageError, "baseline already exists at #{path}; use --force to overwrite"
  end
  dir = File.dirname(File.expand_path(path))
  FileUtils.mkdir_p(dir)
  tmp = File.join(dir, ".#{File.basename(path)}.tmp.#{Process.pid}.#{SecureRandom.hex(8)}")
  begin
    File.open(tmp, "wb", 0o600) do |file|
      file.write(JSON.pretty_generate(baseline.to_h) + "\n")
      file.flush
      file.fsync
    end
    errors = SchemaValidator.validate_baseline(baseline.to_h)
    raise IncompatibleError, "baseline validation failed: #{errors.join('; ')}" unless errors.empty?

    File.rename(tmp, path)
    begin
      dir_fd = File.open(dir, "r")
      dir_fd.fsync
      dir_fd.close
    rescue StandardError
      nil
    end
  ensure
    File.unlink(tmp) if File.exist?(tmp)
  end
end

Instance Method Details

#entry_by_fingerprintObject



37
38
39
# File 'lib/rail_verdict/baseline.rb', line 37

def entry_by_fingerprint
  @entry_by_fingerprint ||= @entries.to_h { |entry| [entry.fetch("fingerprint"), entry] }.freeze
end

#fingerprint_setObject



33
34
35
# File 'lib/rail_verdict/baseline.rb', line 33

def fingerprint_set
  @fingerprint_set ||= Set.new(@entries.map { |entry| entry.fetch("fingerprint") }).freeze
end

#to_hObject



29
30
31
# File 'lib/rail_verdict/baseline.rb', line 29

def to_h
  @hash
end