95 lines
2.9 KiB
Ruby
95 lines
2.9 KiB
Ruby
# ©AngelaMos | 2026
|
|
# render_matrix.rb
|
|
|
|
require "json"
|
|
require "rube"
|
|
|
|
TRACKED_CLASSES = %w[Gem::SpecFetcher Gem::Source::Git Gem::URI Net::WriteAdapter].freeze
|
|
MARK_YES = "yes"
|
|
MARK_NO = "no"
|
|
MARK_UNKNOWN = "?"
|
|
RULE_WIDTH = 78
|
|
|
|
CHAIN = Rube::Chains::ErbDefMethod
|
|
|
|
def cve_patched?(version)
|
|
!CHAIN.affects?(version)
|
|
end
|
|
|
|
rows = File.readlines(ARGV.fetch(0)).reject { |line| line.strip.empty? }.map { |line| JSON.parse(line) }
|
|
abort "no probe results" if rows.empty?
|
|
|
|
def mark(value)
|
|
case value
|
|
when true then MARK_YES
|
|
when false then MARK_NO
|
|
else MARK_UNKNOWN
|
|
end
|
|
end
|
|
|
|
def short(image)
|
|
image.sub("ruby:", "")
|
|
end
|
|
|
|
def section(title)
|
|
puts title
|
|
puts " #{'-' * RULE_WIDTH}"
|
|
yield
|
|
puts
|
|
end
|
|
|
|
section("RUNTIME") do
|
|
puts format(" %-14s %-8s %-9s %-8s %-9s %-7s", "image", "ruby", "rubygems", "psych", "erb", "marshal")
|
|
rows.each do |r|
|
|
puts format(" %-14s %-8s %-9s %-8s %-9s %-7s",
|
|
short(r["image"]), r["ruby"], r["rubygems"], r["psych"], r["erb"], r["marshal_format"])
|
|
end
|
|
end
|
|
|
|
section("GADGET SURFACE") do
|
|
puts format(" %-14s %-12s %-14s %s",
|
|
"image", "git gadget", "safe_marshal", TRACKED_CLASSES.map { |c| format("%-13s", c.split("::").last) }.join)
|
|
rows.each do |r|
|
|
present = TRACKED_CLASSES.map { |c| format("%-13s", mark(r["classes_baseline"][c])) }.join
|
|
puts format(" %-14s %-12s %-14s %s", short(r["image"]), r["git_gadget"], r["safe_marshal"], present)
|
|
end
|
|
end
|
|
|
|
section("ERB @_init GUARD (CVE-2026-41316), anchor = def_method") do
|
|
puts format(" %-14s %-9s %-9s %-24s %s", "image", "erb", "guarded", "delegating", "cve says")
|
|
rows.each do |r|
|
|
guard = r["erb_guard"]
|
|
expected = cve_patched?(r["erb"]) ? "patched" : "affected"
|
|
puts format(" %-14s %-9s %-9s %-24s %s",
|
|
short(r["image"]), r["erb"], mark(guard["guarded"]),
|
|
guard["delegating"].join(","), expected)
|
|
end
|
|
end
|
|
|
|
git_states = rows.map { |r| r["git_gadget"] }.uniq
|
|
guard_states = rows.map { |r| r["erb_guard"]["guarded"] }.uniq
|
|
|
|
agreements = rows.map do |r|
|
|
[short(r["image"]), r["erb_guard"]["guarded"] == cve_patched?(r["erb"])]
|
|
end
|
|
disagreements = agreements.reject { |_, ok| ok }.map(&:first)
|
|
|
|
section("CONTROLS") do
|
|
puts format(" git gadget states observed : %s", git_states.join(", "))
|
|
puts format(" erb guard states observed : %s", guard_states.map { |v| mark(v) }.join(", "))
|
|
puts format(" guard vs published CVE : %d/%d agree", agreements.count { |_, ok| ok }, agreements.length)
|
|
end
|
|
|
|
failures = []
|
|
failures << "git gadget reported '#{git_states.first}' on every image" if git_states.length < 2
|
|
failures << "erb guard reported the same value on every image" if guard_states.length < 2
|
|
failures << "guard disagrees with the CVE ranges on: #{disagreements.join(', ')}" unless disagreements.empty?
|
|
|
|
if failures.empty?
|
|
puts "PASS - the matrix discriminates on both axes and matches the published CVE ranges"
|
|
exit 0
|
|
end
|
|
|
|
failures.each { |f| puts "FAIL - #{f}" }
|
|
exit 1
|
|
|