Cybersecurity-Projects/PROJECTS/beginner/deserialization-gadget-lab/scripts/package-gate.sh

245 lines
8.9 KiB
Bash
Executable File

#!/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