Cybersecurity-Projects/PROJECTS/beginner/deserialization-gadget-lab/test/corpus_test.rb

87 lines
3.3 KiB
Ruby

# ©AngelaMos | 2026
# corpus_test.rb
require_relative "test_helper"
require_relative "support/adversarial_corpus"
module Rube
class CorpusTest < Minitest::Test
def detector(allowed = [])
Marshal::BoundaryDetector.new(allowed_class_names: allowed)
end
def test_corpus_covers_both_verdicts
verdicts = AdversarialCorpus::CASES.map { |c| c[:verdict] }.uniq
assert_includes verdicts, AdversarialCorpus::VERDICT_ACCEPT
assert_includes verdicts, AdversarialCorpus::VERDICT_REJECT
end
def test_corpus_case_names_are_unique
names = AdversarialCorpus::CASES.map { |c| c[:name] }
assert_equal names.length, names.uniq.length
end
def test_every_corpus_case_matches_its_verdict
disagreements = AdversarialCorpus::CASES.filter_map do |kase|
decision = detector(kase[:allowed]).inspect_stream(kase[:bytes])
expected = kase[:verdict] == AdversarialCorpus::VERDICT_ACCEPT
next if decision.accepted? == expected
"#{kase[:name]}: expected #{kase[:verdict]}, got #{decision.accepted? ? 'accept' : 'reject'} (#{decision.reason})"
end
assert_empty disagreements, "corpus disagreements:\n #{disagreements.join("\n ")}"
end
def test_parser_never_raises_outside_the_stream_error_hierarchy
leaks = AdversarialCorpus::CASES.filter_map do |kase|
detector(kase[:allowed]).inspect_stream(kase[:bytes])
nil
rescue StandardError => e
"#{kase[:name]}: #{e.class}"
end
assert_empty leaks, "unhandled exceptions escaped the detector:\n #{leaks.join("\n ")}"
end
def test_allowlisted_cases_would_still_be_rejected_without_their_allowlist
allowlisted = AdversarialCorpus::CASES.reject { |kase| kase[:allowed].empty? }
refute_empty allowlisted
leaks = allowlisted.reject { |kase| detector.inspect_stream(kase[:bytes]).rejected? }
assert_empty leaks.map { |kase| kase[:name] },
"an allowlist must widen what is accepted, never what is rejected"
end
def test_every_class_name_slot_carries_byte_identical_gadget
blind = AdversarialCorpus::CLASS_NAME_SLOTS.reject do |_slot, bytes|
bytes.include?(AdversarialCorpus::CLASS_NAME_GADGET)
end
assert_empty blind.keys,
"slot verdicts only compare if the embedded gadget bytes are identical"
end
def test_class_name_slots_whose_host_is_not_itself_a_sink_are_all_in_the_corpus
names = AdversarialCorpus::CASES.map { |kase| kase[:name] }
missing = AdversarialCorpus::CLASS_NAME_SLOTS_WITH_NON_SINK_HOST.reject do |slot|
names.include?(:"class_name_slot_#{slot}")
end
assert_empty missing, "these slots can flip a detector verdict and must be corpus cases"
end
def test_sink_hosted_class_name_slots_are_excluded_from_the_corpus
hosts = AdversarialCorpus::CLASS_NAME_SLOTS.keys -
AdversarialCorpus::CLASS_NAME_SLOTS_WITH_NON_SINK_HOST
names = AdversarialCorpus::CASES.map { |kase| kase[:name] }
hosts.each do |slot|
refute_includes names, :"class_name_slot_#{slot}",
"#{slot} rejects on its own tag either way, so a corpus case cannot fail"
assert detector.inspect_stream(AdversarialCorpus::CLASS_NAME_SLOTS[slot]).rejected?
end
end
end
end