From bfb0ab36e8076a65c4d28133f7da4c1627204469 Mon Sep 17 00:00:00 2001 From: CarterPerez-dev Date: Wed, 29 Jul 2026 14:58:40 -0400 Subject: [PATCH] feat(rube): M7 - a version floor is a measurement, not a preference required_ruby_version claimed ">= 3.3" while every gate stage ran on Ruby 4.0 images only. The claim was false. Marshal.load did not validate the bignum sign byte until 3.4, so on 3.3 real Ruby accepts "!", "\x00", "\xFF" and "0" in the sign position and reads them all as positive, where 3.4 and 4.0 raise ArgumentError. The parser accepts "+" and "-" only, so it models 3.4+, and on 3.3 parser_test.rb goes red at its own liveness guard: the differential oracle finds nothing rejected and says so instead of passing vacuously. 3.4.10 runs all five suites green at the same counts as 4.0 and prints ALL CONTROLS PASSED. That makes 3.4 the oldest release actually proven, so the floor is ">= 3.4". TargetRubyVersion moves with it, since those two must stay equal. Teaching the parser two Marshal models to keep 3.3 was rejected. It buys a branch in security maintenance only, and it pays with a second sign-validation path in the one component whose whole job is modelling Marshal.load correctly. The untracked rube-0.1.0.gem sitting in the repo root turned out to be built from pre-B17 source: 12 lib files instead of 13, no float_body.rb, read_float still using Float() with a bare rescue, no frozen_string_literal lines, declaring ">= 3.3". It installed and required without error, so nothing caught it. Two artifacts with the same name and version and no way to tell them apart. Deleted. package-gate.sh therefore asserts every shipped lib file is byte-identical to the worktree rather than merely present, builds from the declared manifest alone so an omitted file cannot produce a gem that builds anyway, installs the artifact on the floor and current images and exercises it from the installed copy, and re-proves the floor in both directions each run. Three negative controls: a gem shipping the vulnerable target must be rejected, a gem with a drifted lib file must be rejected, and RubyGems must refuse to install below the declared floor. Aimed at the stale artifact it fails 6 of 23; on a fresh build it passes 23 of 23. Both executed. just build now writes to tmp/build as the invoking user instead of leaving a root-owned gem in the tree, and just package audits an artifact you already have. Full gate: 56 PASS, 0 FAIL across six stages. 194 tests. Lint 0 across 30 files. --- .../deserialization-gadget-lab/.rubocop.yml | 3 +- .../deserialization-gadget-lab/CHANGELOG.md | 19 ++ .../deserialization-gadget-lab/README.md | 42 ++- .../deserialization-gadget-lab/justfile | 12 +- .../deserialization-gadget-lab/rube.gemspec | 2 +- .../scripts/audit_gem.rb | 79 ++++++ .../scripts/package-gate.sh | 244 ++++++++++++++++++ 7 files changed, 396 insertions(+), 5 deletions(-) create mode 100644 PROJECTS/beginner/deserialization-gadget-lab/scripts/audit_gem.rb create mode 100755 PROJECTS/beginner/deserialization-gadget-lab/scripts/package-gate.sh diff --git a/PROJECTS/beginner/deserialization-gadget-lab/.rubocop.yml b/PROJECTS/beginner/deserialization-gadget-lab/.rubocop.yml index ca78cb86..207a40f8 100644 --- a/PROJECTS/beginner/deserialization-gadget-lab/.rubocop.yml +++ b/PROJECTS/beginner/deserialization-gadget-lab/.rubocop.yml @@ -8,10 +8,11 @@ plugins: AllCops: NewCops: enable - TargetRubyVersion: 3.3 + TargetRubyVersion: 3.4 Exclude: - "vendor/**/*" - "docs/**/*" + - "tmp/**/*" Style/Documentation: Enabled: false diff --git a/PROJECTS/beginner/deserialization-gadget-lab/CHANGELOG.md b/PROJECTS/beginner/deserialization-gadget-lab/CHANGELOG.md index f10b70f8..07e39f6e 100644 --- a/PROJECTS/beginner/deserialization-gadget-lab/CHANGELOG.md +++ b/PROJECTS/beginner/deserialization-gadget-lab/CHANGELOG.md @@ -38,6 +38,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Scanner error accounting: every swallowed rescue is recorded with its site, subject and error class, and `Report` exposes `suppressed_count`, `suppressions_by_site`, `complete?` and `candidates_lost?` +- Packaging gate that builds the gem from its declared manifest alone, audits + what shipped, installs the artifact on the floor and current images, and + exercises it from the installed copy rather than the worktree. It asserts every + shipped `lib` file is byte-identical to source, which catches an artifact built + from stale code even though such a gem installs and requires without error. It + can be pointed at a `.gem` you already have, and it carries three negative + controls: a gem that ships the vulnerable target must be rejected, a gem with a + drifted `lib` file must be rejected, and RubyGems must refuse to install below + the declared floor - Four-state reachability analysis. A method is analysed (touches state or does not), `unreadable_source?` (a path was given and could not be parsed, so it fails open and stays reachable), or `unanalysable?` (no Ruby source exists at @@ -81,6 +90,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 of a stream you already trust - `Limits.permissive` is a class method; it was an instance method that ignored its receiver and allocated twice +- `required_ruby_version` raised from `>= 3.3` to `>= 3.4`. The old floor was + never tested: every gate stage ran on Ruby 4.0 images only. Ruby 3.3 turns out + to fail the suite, because `Marshal.load` did not validate the bignum sign byte + until 3.4 and the parser is written against the version that does. 3.4 is the + oldest release on which the whole suite is green, so it is the floor. + `TargetRubyVersion` moves with it, as those two must stay equal +- `just build` writes to `tmp/build` as the invoking user instead of dropping a + root-owned `.gem` in the repository root, and stages only the files the gemspec + declares, so a manifest that omits a file can no longer produce a gem that + builds anyway ### Fixed diff --git a/PROJECTS/beginner/deserialization-gadget-lab/README.md b/PROJECTS/beginner/deserialization-gadget-lab/README.md index e44310fd..14089e84 100644 --- a/PROJECTS/beginner/deserialization-gadget-lab/README.md +++ b/PROJECTS/beginner/deserialization-gadget-lab/README.md @@ -46,6 +46,38 @@ All six pieces are built and tested. - **Boundary detector** — the defensive layer, with an explicit written statement of what it cannot do. +## Requirements + +Ruby **3.4 or newer**. That floor is measured, not picked for tidiness. + +Ruby changed `Marshal.load` between 3.3 and 3.4. Through 3.3, any byte in a bignum's sign +position is accepted and anything that is not `-` is read as positive. From 3.4 onward the +same stream raises `ArgumentError: invalid Bignum sign`: + +| sign byte | 3.2.11 | 3.3.12 | 3.4.10 | 4.0.6 | +|---|---|---|---|---| +| `+` and `-` | accept | accept | accept | accept | +| `!`, `\x00`, `\xFF`, `0` | accept | accept | **reject** | **reject** | + +rube's parser accepts `+` and `-` only, so it models 3.4 and newer. Run it on 3.3 and it +disagrees with the interpreter it exists to model on four of those six bytes. A stream +inspector that disagrees with the loader it guards is not worth shipping, so the floor sits +where the agreement starts. `just package` re-proves this in both directions on every run: +on the floor image Ruby and the parser agree, one version below it they diverge. + +## Installation + +rube is not published to rubygems.org. Build it from this checkout and install the artifact: + +``` +just build +gem install --local tmp/build/rube-0.1.0.gem +``` + +The gem carries `lib/`, the README, the changelog, and the license. Nothing else. The +vulnerable target, the adversarial corpus, the gate scripts, and the research notes stay in +the repository, and `just package` fails if any of them turn up inside a built artifact. + ## Usage ```ruby @@ -116,11 +148,19 @@ just matrix probe six pinned Ruby images and render the compatibility matrix just exploit prove the chain fires on a vulnerable image and is blocked on a patched one just target stand up the vulnerable app and attack it over HTTP just detector prove the defensive layer rejects the payload the target executes +just package build the gem, audit what shipped, install it, prove the version floor just gate everything above, in order -just build build the gem with --strict +just build build the gem with --strict into tmp/build just manifest list exactly what would ship in the .gem ``` +`just package` also audits an artifact you already have, which is how you check that a gem +on disk still matches the source it claims to be built from: + +``` +just package tmp/build/rube-0.1.0.gem +``` + ## A note on the object-link index Ruby's Marshal format documentation states that object links are one-indexed. **They are diff --git a/PROJECTS/beginner/deserialization-gadget-lab/justfile b/PROJECTS/beginner/deserialization-gadget-lab/justfile index 6abf887a..0986925c 100644 --- a/PROJECTS/beginner/deserialization-gadget-lab/justfile +++ b/PROJECTS/beginner/deserialization-gadget-lab/justfile @@ -8,6 +8,8 @@ vuln_image := "ruby:4.0.2-slim" run := "docker run --rm --network none -v $PWD:/app -w /app " + image run_ro := "docker run --rm --network none -v $PWD:/app:ro -w /app " + image lint_run := "docker run --rm -v $PWD:/app -w /app " + image +owner := `id -u` + ":" + `id -g` +build_run := "docker run --rm --network none --user " + owner + " -e HOME=/tmp" default: @just --list @@ -48,10 +50,15 @@ target: detector: @bash scripts/detector-gate.sh -gate: check matrix exploit detector target +package *gem: + @bash scripts/package-gate.sh {{gem}} + +gate: check matrix exploit detector target package build: - {{run}} sh -c "gem build --strict rube.gemspec" + @mkdir -p tmp/build + {{build_run}} -v $PWD:/src:ro -v $PWD/tmp/build:/out -w /out {{image}} sh -c "set -e; cd /src && ruby -e 'puts Gem::Specification.load(%q{rube.gemspec}).files' >/out/declared.txt; cd /out && tar -C /src -T declared.txt -cf - | tar -xf -; cp /src/rube.gemspec /out/; gem build --strict rube.gemspec" + @ls -l tmp/build/*.gem manifest: {{run_ro}} ruby -e 'spec = Gem::Specification.load("rube.gemspec"); puts spec.files.sort; puts; puts "#{spec.files.length} files"' @@ -65,3 +72,4 @@ pull: clean: rm -f *.gem + rm -rf tmp/build tmp/package diff --git a/PROJECTS/beginner/deserialization-gadget-lab/rube.gemspec b/PROJECTS/beginner/deserialization-gadget-lab/rube.gemspec index 47ada9b3..cbd33765 100644 --- a/PROJECTS/beginner/deserialization-gadget-lab/rube.gemspec +++ b/PROJECTS/beginner/deserialization-gadget-lab/rube.gemspec @@ -17,7 +17,7 @@ Gem::Specification.new do |spec| spec.homepage = "https://github.com/CarterPerez-dev/Cybersecurity-Projects" spec.license = "AGPL-3.0-or-later" - spec.required_ruby_version = ">= 3.3" + spec.required_ruby_version = ">= 3.4" spec.metadata = { "source_code_uri" => "#{spec.homepage}/tree/main/PROJECTS/beginner/deserialization-gadget-lab", diff --git a/PROJECTS/beginner/deserialization-gadget-lab/scripts/audit_gem.rb b/PROJECTS/beginner/deserialization-gadget-lab/scripts/audit_gem.rb new file mode 100644 index 00000000..ad3b1153 --- /dev/null +++ b/PROJECTS/beginner/deserialization-gadget-lab/scripts/audit_gem.rb @@ -0,0 +1,79 @@ +# ©AngelaMos | 2026 +# audit_gem.rb +# frozen_string_literal: true + +require "rubygems/package" +require "digest" +require "fileutils" + +GEM_PATH = ARGV.fetch(0) +TREE = ARGV.fetch(1) +EXPECTED_FLOOR = ARGV.fetch(2) +EXTRACT_ROOT = ARGV.fetch(3) + +DOC_FILES = %w[README.md CHANGELOG.md LICENSE].freeze +LIB_PREFIX = "lib/" + +FORBIDDEN = { + "target_absent" => %r{\Atarget/}, + "tests_absent" => %r{\Atest/}, + "scripts_absent" => %r{\Ascripts/}, + "dev_docs_absent" => %r{\Adocs/|\AAGENTS\.md\z}, + "build_tooling_absent" => /\A(justfile|Gemfile|Gemfile\.lock|Rakefile|\.rubocop\.yml|\.gitignore)\z/, + "container_files_absent" => /Dockerfile|compose|config\.ru/i, + "lab_artifacts_absent" => %r{\.gem\z|\.marshal\z|\.bin\z|\Apayloads/|\Aloot/|canary}i +}.freeze + +def tree_lib_files + Dir.chdir(TREE) { Dir.glob("#{LIB_PREFIX}**/*.rb").sort } +end + +def emit(key, value) + puts "#{key}=#{value}" +end + +def info(label, value) + puts "INFO #{label}: #{value}" +end + +package = Gem::Package.new(GEM_PATH) +spec = package.spec +contents = package.contents.sort +expected = (DOC_FILES + tree_lib_files).sort +shipped_lib = contents.select { |path| path.start_with?(LIB_PREFIX) } + +FileUtils.rm_rf(EXTRACT_ROOT) +FileUtils.mkdir_p(EXTRACT_ROOT) +package.extract_files(EXTRACT_ROOT) + +missing = expected - contents +unexpected = contents - expected + +drifted = shipped_lib.reject do |path| + source = File.join(TREE, path) + next false unless File.file?(source) + + Digest::SHA256.file(File.join(EXTRACT_ROOT, path)).hexdigest == + Digest::SHA256.file(source).hexdigest +end + +orphaned = shipped_lib.reject { |path| File.file?(File.join(TREE, path)) } + +info "gem", File.basename(GEM_PATH) +info "declares", "#{spec.name} #{spec.version} ruby #{spec.required_ruby_version}" +info "shipped", "#{contents.length} files, #{shipped_lib.length} under lib/" +info "worktree", "#{tree_lib_files.length} files under lib/" +info "missing", missing.empty? ? "none" : missing.join(", ") +info "unexpected", unexpected.empty? ? "none" : unexpected.join(", ") +info "drifted", drifted.empty? ? "none" : drifted.join(", ") +info "orphaned", orphaned.empty? ? "none" : orphaned.join(", ") + +emit "every_declared_file_shipped", missing.empty? +emit "nothing_undeclared_shipped", unexpected.empty? +emit "lib_is_non_empty", shipped_lib.length.positive? +emit "lib_matches_worktree", drifted.empty? && orphaned.empty? +emit "floor_is_declared", spec.required_ruby_version.to_s == EXPECTED_FLOOR + +FORBIDDEN.each do |key, pattern| + emit(key, contents.none? { |path| pattern.match?(path) }) +end diff --git a/PROJECTS/beginner/deserialization-gadget-lab/scripts/package-gate.sh b/PROJECTS/beginner/deserialization-gadget-lab/scripts/package-gate.sh new file mode 100755 index 00000000..7d0b6737 --- /dev/null +++ b/PROJECTS/beginner/deserialization-gadget-lab/scripts/package-gate.sh @@ -0,0 +1,244 @@ +#!/usr/bin/env bash +# ©AngelaMos | 2026 +# package-gate.sh + +set -uo pipefail + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" + +BUILD_IMAGE="ruby:4.0-slim" +FLOOR_IMAGE="ruby:3.4-slim" +BELOW_FLOOR_IMAGE="ruby:3.3-slim" +DECLARED_FLOOR=">= 3.4" + +WORK="${HERE}/tmp/package" +OBSERVED="${WORK}/observed" +OWNER="$(id -u):$(id -g)" +GIVEN="${1:-}" + +INVALID_SIGN_STREAM='"\x04\x08l!\x06\x01\x00".b' + +run() { + docker run --rm --network none --user "${OWNER}" -e HOME=/tmp "$@" +} + +record() { + tee -a "${OBSERVED}" | sed 's/^/ /' +} + +echo "packaging gate" +echo + +rm -rf "${WORK}" +mkdir -p "${WORK}/build" "${WORK}/ships-target" "${WORK}/drifted" +: >"${OBSERVED}" + +echo "=== 1 build ===" +if [[ -n "${GIVEN}" ]]; then + GEM_DIR="$(cd "$(dirname "${GIVEN}")" && pwd)" + GEM_FILE="$(basename "${GIVEN}")" + echo " auditing a pre-built artifact, no build performed" + echo " ${GEM_DIR}/${GEM_FILE}" + echo "gem_built=true" | record +else + GEM_DIR="${WORK}/build" + GEM_FILE="rube-$(run -v "${HERE}:/src:ro" -w /src "${BUILD_IMAGE}" \ + ruby -e 'require "./lib/rube/version"; print Rube::VERSION').gem" + + run -v "${HERE}:/src:ro" -v "${WORK}/build:/out" -w /out "${BUILD_IMAGE}" sh -c ' + set -e + cd /src && ruby -e "puts Gem::Specification.load(%q{rube.gemspec}).files" >/out/declared.txt + cd /out && tar -C /src -T declared.txt -cf - | tar -xf - + cp /src/rube.gemspec /out/ + gem build --strict rube.gemspec + ' 2>&1 | sed 's/^/ /' + + if [[ -f "${GEM_DIR}/${GEM_FILE}" ]]; then + echo "gem_built=true" | record + else + echo "gem_built=false" | record + fi +fi +echo + +echo "=== 2 manifest audit ===" +run -v "${HERE}:/src:ro" -v "${GEM_DIR}:/gem:ro" "${BUILD_IMAGE}" \ + ruby /src/scripts/audit_gem.rb "/gem/${GEM_FILE}" /src "${DECLARED_FLOOR}" /tmp/extract 2>&1 | record +echo + +echo "=== 3 install path ===" +install_and_use() { + local image="$1" + local label="$2" + + docker run --rm --network none -v "${GEM_DIR}:/gem:ro" -w / "${image}" sh -c " + set -e + gem install --local --no-document /gem/${GEM_FILE} >/dev/null + ruby -e ' + require \"rube\" + raise \"loaded from the worktree\" unless Gem.loaded_specs[\"rube\"] + blob = Marshal.dump(Gem::Requirement.new(\">= 0\")) + result = Rube::Marshal::Parser.new(blob).parse + sinks = result.sinks.map { |s| \"#{s.class_name}##{s.sink_method}\" } + decision = Rube::Marshal::BoundaryDetector.new.inspect_stream(blob) + ok = result.class_names.include?(\"Gem::Requirement\") && + sinks.include?(\"Gem::Requirement#marshal_load\") && + decision.blocked? && + !defined?(Rube::Marshal::FloatBody).nil? + puts \"installed_gem_works_on_${label}=#{ok}\" + ' + " 2>&1 | tail -1 +} + +install_and_use "${FLOOR_IMAGE}" floor | record +install_and_use "${BUILD_IMAGE}" current | record +echo + +echo "=== 4 the floor is measured, not asserted ===" +suite_status=0 +for suite in marshal/parser_test scanner_test chains_test marshal/boundary_detector_test corpus_test; do + if ! docker run --rm --network none -v "${HERE}:/app:ro" -w /app "${FLOOR_IMAGE}" \ + ruby -Ilib -Itest "test/${suite}.rb" >/dev/null 2>&1; then + echo " ${suite} is RED on the floor image" + suite_status=1 + fi +done +if [[ ${suite_status} -eq 0 ]]; then + echo "suite_green_on_floor=true" | record +else + echo "suite_green_on_floor=false" | record +fi + +differential() { + local image="$1" + local label="$2" + + docker run --rm --network none -v "${HERE}:/app:ro" -w /app "${image}" ruby -Ilib -e " + require \"rube\" + bytes = ${INVALID_SIGN_STREAM} + ruby_accepts = begin + Marshal.load(bytes) + true + rescue StandardError + false + end + parser_accepts = begin + Rube::Marshal::Parser.new(bytes).parse + true + rescue Rube::Marshal::StreamError + false + end + puts \"${label}_ruby_accepts_invalid_sign=#{ruby_accepts}\" + puts \"${label}_parser_accepts_invalid_sign=#{parser_accepts}\" + " 2>&1 | tail -2 +} + +differential "${FLOOR_IMAGE}" floor | record +differential "${BELOW_FLOOR_IMAGE}" below_floor | record +echo + +echo "=== 5 negative controls ===" +cat >"${WORK}/ships-target/rube.gemspec" <<'SPEC' +require_relative "lib/rube/version" + +Gem::Specification.new do |spec| + spec.name = "rube" + spec.version = Rube::VERSION + spec.authors = ["Carter Perez"] + spec.email = ["carterperez2222@gmail.com"] + spec.summary = "control fixture that deliberately ships the vulnerable target" + spec.description = "control fixture for package-gate.sh, never published" + spec.homepage = "https://github.com/CarterPerez-dev/Cybersecurity-Projects" + spec.license = "AGPL-3.0-or-later" + spec.required_ruby_version = ">= 3.4" + spec.files = Dir["lib/**/*.rb", "target/**/*", "README.md", "CHANGELOG.md", "LICENSE"] + spec.require_paths = ["lib"] +end +SPEC + +run -v "${HERE}:/src:ro" -v "${WORK}/ships-target:/out" -w /out "${BUILD_IMAGE}" sh -c ' + set -e + tar -C /src -cf - lib target README.md CHANGELOG.md LICENSE | tar -xf - + gem build rube.gemspec +' >/dev/null 2>&1 + +control_target="$(run -v "${HERE}:/src:ro" -v "${WORK}/ships-target:/gem:ro" "${BUILD_IMAGE}" \ + ruby /src/scripts/audit_gem.rb "/gem/${GEM_FILE}" /src "${DECLARED_FLOOR}" /tmp/extract 2>/dev/null | + grep -c '^target_absent=false$')" +echo "control_auditor_rejects_a_gem_shipping_the_target=$([[ ${control_target} == 1 ]] && echo true || echo false)" | record + +run -v "${HERE}:/src:ro" -v "${WORK}/drifted:/out" -w /out "${BUILD_IMAGE}" sh -c ' + set -e + cd /src && ruby -e "puts Gem::Specification.load(%q{rube.gemspec}).files" >/out/declared.txt + cd /out && tar -C /src -T declared.txt -cf - | tar -xf - + cp /src/rube.gemspec /out/ + ruby -e "File.write(%q{lib/rube/version.rb}, File.read(%q{lib/rube/version.rb}) + %q{ +})" + gem build rube.gemspec +' >/dev/null 2>&1 + +control_drift="$(run -v "${HERE}:/src:ro" -v "${WORK}/drifted:/gem:ro" "${BUILD_IMAGE}" \ + ruby /src/scripts/audit_gem.rb "/gem/${GEM_FILE}" /src "${DECLARED_FLOOR}" /tmp/extract 2>/dev/null | + grep -c '^lib_matches_worktree=false$')" +echo "control_auditor_rejects_a_drifted_lib_file=$([[ ${control_drift} == 1 ]] && echo true || echo false)" | record + +if docker run --rm --network none -v "${GEM_DIR}:/gem:ro" -w / "${BELOW_FLOOR_IMAGE}" \ + gem install --local --no-document "/gem/${GEM_FILE}" >/dev/null 2>&1; then + echo "control_floor_blocks_install_below_it=false" | record +else + echo "control_floor_blocks_install_below_it=true" | record +fi +echo + +failures=0 +expect() { + if grep -qx "$1=true" "${OBSERVED}"; then + echo " PASS $2" + else + echo " FAIL $2" + failures=$((failures + 1)) + fi +} + +reject() { + if grep -qx "$1=false" "${OBSERVED}"; then + echo " PASS $2" + else + echo " FAIL $2" + failures=$((failures + 1)) + fi +} + +echo "=== verdict ===" +expect gem_built "the gem builds with --strict from its declared manifest alone" +expect every_declared_file_shipped "every file the gemspec declares is in the artifact" +expect nothing_undeclared_shipped "the artifact carries nothing the gemspec did not declare" +expect lib_is_non_empty "the artifact ships a non-empty lib, so the audit is not vacuous" +expect lib_matches_worktree "every shipped lib file is byte-identical to the worktree" +expect floor_is_declared "the artifact declares the floor this gate proves" +expect target_absent "the vulnerable target is absent" +expect tests_absent "the test suite, corpus and fixtures are absent" +expect scripts_absent "the gate scripts are absent" +expect dev_docs_absent "research, plans and agent briefing are absent" +expect build_tooling_absent "justfile, Gemfile, Rakefile and lint config are absent" +expect container_files_absent "Dockerfile and rack config are absent" +expect lab_artifacts_absent "no canary, payload or nested gem artifact shipped" +expect installed_gem_works_on_floor "the installed gem parses, classifies and blocks on the floor" +expect installed_gem_works_on_current "the installed gem parses, classifies and blocks on current" +expect suite_green_on_floor "every suite is green on the floor image" +reject floor_ruby_accepts_invalid_sign "on the floor, real Marshal rejects an invalid bignum sign" +reject floor_parser_accepts_invalid_sign "on the floor, the parser rejects it too, so they agree" +expect below_floor_ruby_accepts_invalid_sign "one version below the floor, real Marshal accepts it" +reject below_floor_parser_accepts_invalid_sign "the parser still rejects it, so below the floor they disagree" +expect control_auditor_rejects_a_gem_shipping_the_target "the auditor rejects a gem that ships the target" +expect control_auditor_rejects_a_drifted_lib_file "the auditor rejects a gem whose lib drifted from source" +expect control_floor_blocks_install_below_it "RubyGems refuses to install below the declared floor" + +echo +if [[ ${failures} -eq 0 ]]; then + echo "GATE PASSED" + exit 0 +fi + +echo "GATE FAILED (${failures})" +exit 1