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

199 lines
5.6 KiB
Ruby

# ©AngelaMos | 2026
# scanner_test.rb
require_relative "test_helper"
module Rube
class ScannerTest < Minitest::Test
def scan(**options)
Scanner.new(**options).scan
end
def local_scan
scan(namespace: "Rube::ScannerTest")
end
def candidates_for(class_name)
local_scan.candidates.select { |c| c.class_name == class_name }
end
def test_finds_gated_sink_defined_on_a_class
found = candidates_for("Rube::ScannerTest::GatedFixture")
assert_equal ["marshal_load"], found.map(&:method_name)
assert_equal :gated, found.first.gate
end
def test_finds_singleton_load_as_gated_sink
found = candidates_for("Rube::ScannerTest::UserDefFixture")
assert_includes found.map(&:method_name), "_load"
end
def test_finds_ungated_dispatch_method
found = candidates_for("Rube::ScannerTest::UngatedFixture")
assert_equal ["hash"], found.map(&:method_name)
assert_equal :ungated, found.first.gate
end
def test_finds_method_missing_as_ungated
found = candidates_for("Rube::ScannerTest::ProxyFixture")
assert_includes found.map(&:method_name), "method_missing"
end
def test_negative_control_class_with_no_auto_invoked_methods_is_not_reported
assert_empty candidates_for("Rube::ScannerTest::InertFixture")
end
def test_precision_control_inherited_methods_are_not_reported
assert_empty candidates_for("Rube::ScannerTest::InheritsOnlyFixture")
end
def test_candidates_carry_source_location
candidate = candidates_for("Rube::ScannerTest::GatedFixture").first
refute_nil candidate.source_location
assert_includes candidate.source_location, "scanner_test.rb"
end
def test_candidates_report_arity
candidate = candidates_for("Rube::ScannerTest::GatedFixture").first
assert_equal 1, candidate.arity
end
def test_ungated_methods_report_zero_arity
candidate = candidates_for("Rube::ScannerTest::UngatedFixture").first
assert_predicate candidate, :zero_arity?
end
def test_reachability_via_instance_variable_read
candidate = candidates_for("Rube::ScannerTest::StatefulFixture").first
assert_predicate candidate, :touches_state?
assert_predicate candidate, :reachable?
end
def test_reachability_via_implicit_self_call
candidate = candidates_for("Rube::ScannerTest::AccessorFixture").first
assert_predicate candidate, :touches_state?, "attr_reader access must count as touching state"
assert_predicate candidate, :reachable?
end
def test_negative_control_stateless_method_is_not_reachable
candidate = candidates_for("Rube::ScannerTest::StatelessFixture").first
refute_predicate candidate, :touches_state?
refute_predicate candidate, :reachable?
end
def test_gated_sinks_are_reachable_regardless_of_state
candidate = candidates_for("Rube::ScannerTest::GatedFixture").first
assert_predicate candidate, :reachable?
end
def test_reachable_is_a_strict_subset_of_candidates
report = scan(namespace: "Gem")
refute_empty report.reachable
assert_operator report.reachable.length, :<, report.candidates.length,
"reachability filter kept everything, so it is not filtering"
end
def test_rediscovers_accessor_backed_stdlib_sink
report = scan(namespace: "Gem")
names = report.reachable.map(&:to_s)
assert_includes names, "Gem::Requirement#hash"
end
def test_rediscovers_a_real_stdlib_sink_without_hardcoding
report = scan(namespace: "Gem")
names = report.gated.map { |c| "#{c.class_name}##{c.method_name}" }
assert_includes names, "Gem::Requirement#marshal_load"
refute_includes Scanner::GATED_METHODS + Scanner::UNGATED_METHODS, "Gem::Requirement"
end
def test_namespace_filter_excludes_everything_else
report = scan(namespace: "Rube::ScannerTest")
assert(report.candidates.all? { |c| c.class_name.start_with?("Rube::ScannerTest") })
end
def test_report_partitions_gated_and_ungated
report = local_scan
assert_equal report.candidates.length, report.gated.length + report.ungated.length
assert_empty(report.gated & report.ungated)
end
def test_scan_is_deterministic
first = local_scan.candidates.map(&:to_s).sort
second = local_scan.candidates.map(&:to_s).sort
assert_equal first, second
end
def test_anonymous_classes_are_skipped
Class.new { def marshal_load(data); end }
refute(local_scan.candidates.any? { |c| c.class_name.nil? || c.class_name.empty? })
end
def test_scanning_does_not_instantiate_anything
refute GatedFixture.instantiated
local_scan
refute GatedFixture.instantiated, "scanner constructed a candidate class"
end
class GatedFixture
@instantiated = false
class << self
attr_accessor :instantiated
end
def marshal_load(data); end
end
class UserDefFixture
def self._load(data)
allocate
end
end
class UngatedFixture
def hash
super
end
end
class ProxyFixture
def method_missing(name, *args)
super
end
def respond_to_missing?(name, include_private = false)
super
end
end
class StatefulFixture
def hash
@seed.to_i
end
end
class AccessorFixture
attr_reader :seed
def hash
seed.to_i
end
end
class StatelessFixture
def hash
42
end
end
class InertFixture
def ordinary_method; end
def another_one(argument); end
end
class InheritsOnlyFixture
end
end
end