zupt/tests/test_source_only.sh
2026-08-31 20:27:14 -03:00

428 lines
16 KiB
Shell
Executable file

#!/usr/bin/env bash
# SPDX-License-Identifier: AGPL-3.0-or-later
set -Eeuo pipefail
ROOT=$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd -P)
SCANNER=$ROOT/scripts/check-source-only.sh
TEST_TMP=$(mktemp -d "${TMPDIR:-/tmp}/zupt-source-only-tests.XXXXXXXX")
PASSED=0
cleanup() {
local status=$?
trap - EXIT HUP INT TERM
rm -rf -- "$TEST_TMP"
exit "$status"
}
trap cleanup EXIT HUP INT TERM
pass() {
PASSED=$((PASSED + 1))
printf 'ok %d - %s\n' "$PASSED" "$1"
}
skip() {
PASSED=$((PASSED + 1))
printf 'ok %d - %s # SKIP\n' "$PASSED" "$1"
}
expect_pass_tree() {
local name=$1 tree=$2 output=$TEST_TMP/output
if "$SCANNER" --tree "$tree" >"$output" 2>&1 && grep -q '^PASS source-only:' "$output"; then
pass "$name"
else
printf 'not ok - %s\n' "$name"
sed -n '1,120p' "$output"
exit 1
fi
}
expect_fail_tree() {
local name=$1 tree=$2 expected=${3:-} output=$TEST_TMP/output
if "$SCANNER" --tree "$tree" >"$output" 2>&1; then
printf 'not ok - %s (scanner unexpectedly passed)\n' "$name"
exit 1
fi
grep -q '^FAIL ' "$output" || {
printf 'not ok - %s (missing FAIL finding)\n' "$name"
exit 1
}
grep -q '^FAIL source-only:' "$output" || {
printf 'not ok - %s (missing FAIL summary)\n' "$name"
exit 1
}
if [[ -n $expected ]] && ! grep -Fq -- "$expected" "$output"; then
printf 'not ok - %s (missing expected path)\n' "$name"
exit 1
fi
pass "$name"
}
expect_fail_archive_with_limits() {
local name=$1 archive=$2 expected=$3
shift 3
local output=$TEST_TMP/output
if env "$@" "$SCANNER" --archive "$archive" >"$output" 2>&1; then
printf 'not ok - %s (scanner unexpectedly passed)\n' "$name"
exit 1
fi
if ! grep -Fq -- "$expected" "$output"; then
printf 'not ok - %s (missing expected bounded-archive finding)\n' "$name"
sed -n '1,120p' "$output"
exit 1
fi
pass "$name"
}
fresh_tree() {
local name=$1
mkdir -p "$TEST_TMP/$name"
printf '%s' "$TEST_TMP/$name"
}
safe=$(fresh_tree safe)
mkdir -p "$safe/src" "$safe/assets"
printf '#include <stdio.h>\nint main(void) { return 0; }\n' >"$safe/src/main.c"
printf '.text\n.globl portable_symbol\nportable_symbol:\n ret\n' >"$safe/src/portable.S"
printf '\211PNG\r\n\032\n' >"$safe/assets/icon.png"
printf '\000\000\001\000' >"$safe/assets/icon.ico"
if ln -s src/main.c "$safe/main-link.c" 2>/dev/null &&
[[ -L $safe/main-link.c ]]; then
SYMLINKS_SUPPORTED=1
safe_label='text source, assembly, PNG, ICO, and internal symlink pass'
else
SYMLINKS_SUPPORTED=0
safe_label='text source, assembly, PNG, and ICO pass (symlink unavailable)'
fi
expect_pass_tree "$safe_label" "$safe"
tree=$(fresh_tree undeclared-bin)
printf '\001\002\003fixture data\n' >"$tree/vector.bin"
expect_fail_tree 'undeclared .bin data is rejected' "$tree" vector.bin
tree=$(fresh_tree declared-bin)
mkdir -p "$tree/tests/data"
printf '\001\002\003fixture data\n' >"$tree/tests/data/vector.bin"
manifest=$TEST_TMP/source-data.tsv
printf 'tests/data/vector.bin\ttest vector\tgenerated by test_source_only.sh\tAGPL-3.0-or-later\n' >"$manifest"
if "$SCANNER" --data-manifest "$manifest" --tree "$tree" >"$TEST_TMP/output" 2>&1 &&
grep -q '^PASS source-only:' "$TEST_TMP/output"; then
pass 'declared non-executable .bin fixture passes with complete metadata'
else
printf 'not ok - declared non-executable .bin fixture passes\n'
sed -n '1,120p' "$TEST_TMP/output"
exit 1
fi
printf '\177ELF\002\001\001\000compiled' >"$tree/tests/data/vector.bin"
if "$SCANNER" --data-manifest "$manifest" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - manifest cannot allow executable magic\n'
exit 1
elif grep -Fq 'tests/data/vector.bin' "$TEST_TMP/output"; then
pass 'data manifest cannot exempt executable magic'
else
printf 'not ok - executable magic path missing from manifest test\n'
exit 1
fi
tree=$(fresh_tree elf)
printf '\177ELF\002\001\001\000compiled' >"$tree/renamed.txt"
expect_fail_tree 'ELF renamed as text is rejected' "$tree" renamed.txt
case "$(uname -s)" in
MINGW*|MSYS*|CYGWIN*)
skip 'control-byte filenames are forbidden by the Windows filesystem'
skip 'raw C1 filenames are forbidden by the Windows filesystem'
skip 'UTF-8 C1 filenames are forbidden by the Windows filesystem'
skip 'bidirectional-control filenames are forbidden by the Windows filesystem'
skip 'printable UTF-8 filename preservation is not exercised on Windows'
;;
*)
tree=$(fresh_tree control-path)
control_name=$'escape\033[31m.txt'
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - control-byte path was not rejected\n'
exit 1
elif grep -q $'\033' "$TEST_TMP/output" ||
! grep -Fq 'escape\x1b[31m.txt' "$TEST_TMP/output"; then
printf 'not ok - control-byte path was not rendered safely\n'
exit 1
else
pass 'scanner escapes terminal control bytes in reported paths'
fi
tree=$(fresh_tree raw-c1-path)
control_name=$'raw-\200.txt'
if { printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"; } 2>/dev/null; then
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - raw C1 path was not rejected\n'
exit 1
elif ! grep -Fq 'raw-\x80.txt' "$TEST_TMP/output" ||
LC_ALL=C grep -q $'\200' "$TEST_TMP/output"; then
printf 'not ok - raw C1 path was not rendered safely\n'
exit 1
else
pass 'scanner escapes invalid raw C1 bytes in reported paths'
fi
else
skip 'raw C1 filenames are forbidden by this filesystem'
fi
tree=$(fresh_tree utf8-c1-path)
control_name=$'utf8-\302\233.txt'
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - UTF-8 C1 path was not rejected\n'
exit 1
elif ! grep -Fq 'utf8-\u009b.txt' "$TEST_TMP/output" ||
LC_ALL=C grep -q $'\302\233' "$TEST_TMP/output"; then
printf 'not ok - UTF-8 C1 path was not rendered safely\n'
exit 1
else
pass 'scanner escapes UTF-8-encoded C1 controls in reported paths'
fi
tree=$(fresh_tree bidi-path)
control_name=$'report-\342\200\256txt.exe'
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - bidirectional-control path was not rejected\n'
exit 1
elif ! grep -Fq 'report-\u202etxt.exe' "$TEST_TMP/output" ||
LC_ALL=C grep -q $'\342\200\256' "$TEST_TMP/output"; then
printf 'not ok - bidirectional-control path was not rendered safely\n'
exit 1
else
pass 'scanner escapes UTF-8 bidirectional controls in reported paths'
fi
tree=$(fresh_tree printable-utf8-path)
control_name=$'caf\303\251.txt'
printf '\177ELF\002\001\001\000compiled' >"$tree/$control_name"
if "$SCANNER" --tree "$tree" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - printable UTF-8 path was not rejected\n'
exit 1
elif ! LC_ALL=C grep -Fq -- "$control_name" "$TEST_TMP/output"; then
printf 'not ok - printable UTF-8 path was not preserved\n'
exit 1
else
pass 'scanner preserves printable UTF-8 in reported paths'
fi
;;
esac
tree=$(fresh_tree ar)
printf '!<arch>\n' >"$tree/renamed.data"
expect_fail_tree 'ar library renamed as data is rejected' "$tree" renamed.data
tree=$(fresh_tree thin-ar)
printf '!<thin>\n' >"$tree/renamed.data"
expect_fail_tree 'GNU thin archive renamed as data is rejected' "$tree" renamed.data
tree=$(fresh_tree mz)
printf 'MZnot-source' >"$tree/renamed.data"
expect_fail_tree 'PE/MZ renamed as data is rejected' "$tree" renamed.data
tree=$(fresh_tree macho)
printf '\376\355\372\317compiled' >"$tree/renamed.data"
expect_fail_tree 'Mach-O renamed as data is rejected' "$tree" renamed.data
tree=$(fresh_tree coff)
printf '\144\206\001\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000\000' >"$tree/renamed.data"
expect_fail_tree 'COFF object renamed as data is rejected' "$tree" renamed.data
tree=$(fresh_tree lfs)
printf 'version https://git-lfs.github.com/spec/v1\noid sha256:0000\nsize 4\n' >"$tree/pointer.c"
expect_fail_tree 'unresolved Git LFS pointer is rejected' "$tree" pointer.c
tree=$(fresh_tree symlink)
if ((SYMLINKS_SUPPORTED)) && ln -s ../../outside "$tree/escape" 2>/dev/null &&
[[ -L $tree/escape ]]; then
expect_fail_tree 'escaping symlink is rejected' "$tree" escape
else
skip 'escaping symlink test is unsupported by this runner'
fi
tree=$(fresh_tree so-version)
printf 'not actually compiled\n' >"$tree/libexample.so.1"
expect_fail_tree 'versioned shared-library extension is rejected' "$tree" libexample.so.1
tree=$(fresh_tree rpm)
printf '\355\253\356\333package' >"$tree/renamed.data"
expect_fail_tree 'RPM magic is rejected without relying on extension' "$tree" renamed.data
tree=$(fresh_tree deb)
printf '!<arch>\ndebian-binary 000000000000000000000000000000000000000000000000000000\n' >"$tree/renamed.data"
expect_fail_tree 'DEB magic is rejected without relying on extension' "$tree" renamed.data
tree=$(fresh_tree appimage)
printf '\177ELF\002\001\001\000AI\002payload' >"$tree/renamed.data"
expect_fail_tree 'AppImage magic is rejected without relying on extension' "$tree" renamed.data
tree=$(fresh_tree wasm)
printf '\000asm\001\000\000\000' >"$tree/module.data"
expect_fail_tree 'WebAssembly magic is rejected' "$tree" module.data
tree=$(fresh_tree class)
printf '\312\376\272\276\000\000\000\075' >"$tree/class.data"
expect_fail_tree 'Java class magic is rejected' "$tree" class.data
tree=$(fresh_tree pyc)
printf '\247\015\015\012\000\000\000\000\000\000\000\000\000\000\000\000' >"$tree/python.data"
expect_fail_tree 'Python bytecode magic is rejected' "$tree" python.data
tree=$(fresh_tree nested)
mkdir -p "$tree/input"
printf '\177ELF\002\001\001\000nested' >"$tree/input/payload.txt"
tar -C "$tree/input" -cf "$tree/outer.tar" payload.txt
rm -rf -- "$tree/input"
expect_fail_tree 'compiled content inside an archive is rejected' "$tree" 'outer.tar!payload.txt'
tree=$(fresh_tree renamed-7z)
printf '\067\172\274\257\047\034malformed' >"$tree/renamed.data"
expect_fail_tree '7z magic is recognized and cannot bypass archive inspection' \
"$tree" renamed.data
tree=$(fresh_tree renamed-rar)
printf 'Rar!\032\007\001\000malformed' >"$tree/renamed.data"
expect_fail_tree 'RAR magic is recognized and cannot bypass archive inspection' \
"$tree" renamed.data
tree=$(fresh_tree empty-archive)
tar -cf "$tree/empty.tar" --files-from /dev/null
expect_fail_tree 'empty archives are rejected as having no inspectable source' \
"$tree" empty.tar
tree=$(fresh_tree member-limit)
mkdir -p "$tree/input"
for member_number in 1 2 3 4; do
printf 'source %s\n' "$member_number" >"$tree/input/$member_number.c"
done
tar -C "$tree/input" -cf "$tree/members.tar" .
expect_fail_archive_with_limits \
'archive member count is bounded during preflight listing' \
"$tree/members.tar" 'archive member limit exceeded' \
SOURCE_AUDIT_MAX_MEMBERS=3
expect_fail_archive_with_limits \
'archive member-name output is byte-bounded during preflight listing' \
"$tree/members.tar" 'archive member-name budget exceeded' \
SOURCE_AUDIT_MAX_LIST_KIB=0
tree=$(fresh_tree expanded-limit)
mkdir -p "$tree/input"
dd if=/dev/zero of="$tree/input/zeros.c" bs=1024 count=2048 2>/dev/null
tar -C "$tree/input" -czf "$tree/compressed-size-bomb.tar.gz" zeros.c
expect_fail_archive_with_limits \
'compressed archive declared size is rejected before extraction' \
"$tree/compressed-size-bomb.tar.gz" \
'archive declared-size limit exceeded before extraction' \
SOURCE_AUDIT_MAX_KIB=1024
tree=$(fresh_tree global-expanded-limit)
mkdir -p "$tree/one" "$tree/two"
dd if=/dev/zero of="$tree/one/one.c" bs=700 count=1 2>/dev/null
dd if=/dev/zero of="$tree/two/two.c" bs=700 count=1 2>/dev/null
tar -C "$tree/one" -cf "$tree/one.tar" one.c
tar -C "$tree/two" -cf "$tree/two.tar" two.c
if env SOURCE_AUDIT_MAX_KIB=2 SOURCE_AUDIT_MAX_TOTAL_KIB=1 \
"$SCANNER" --archive "$tree/one.tar" --archive "$tree/two.tar" \
>"$TEST_TMP/output" 2>&1; then
printf 'not ok - global archive size budget unexpectedly passed\n'
exit 1
elif grep -Fq 'global archive declared-size budget exceeded' "$TEST_TMP/output"; then
pass 'global declared-size budget covers multiple archives'
else
printf 'not ok - global archive size budget finding missing\n'
sed -n '1,120p' "$TEST_TMP/output"
exit 1
fi
tree=$(fresh_tree archive-symlink)
mkdir -p "$tree/input"
if ((SYMLINKS_SUPPORTED)) &&
ln -s ../../outside "$tree/input/escape" 2>/dev/null &&
[[ -L $tree/input/escape ]]; then
tar -C "$tree/input" -cf "$tree/escape.tar" escape
rm -rf -- "$tree/input"
expect_fail_tree 'escaping symlink inside an archive is rejected before extraction' "$tree" 'escape.tar!escape'
else
skip 'archive symlink test is unsupported by this runner'
fi
tree=$(fresh_tree bad-ref)
printf 'SDK_LIB = vendor/vuptsdk/libvuptsdk.so.2\n' >"$tree/Makefile"
expect_fail_tree 'removed vendored library references are rejected' "$tree" Makefile
archive_src=$(fresh_tree standalone-archive)
printf 'source text\n' >"$archive_src/source.c"
tar -C "$archive_src" -cf "$TEST_TMP/source.tar" source.c
if "$SCANNER" --archive "$TEST_TMP/source.tar" >"$TEST_TMP/output" 2>&1 &&
grep -q '^PASS source-only:' "$TEST_TMP/output"; then
pass 'standalone source archive passes'
else
printf 'not ok - standalone source archive passes\n'
sed -n '1,120p' "$TEST_TMP/output"
exit 1
fi
if SOURCE_AUDIT_FORCE_WATCHDOG=1 \
"$SCANNER" --archive "$TEST_TMP/source.tar" >"$TEST_TMP/output" 2>&1 &&
grep -q '^PASS source-only:' "$TEST_TMP/output"; then
pass 'portable archive watchdog fallback completes a normal scan'
else
printf 'not ok - portable archive watchdog fallback\n'
sed -n '1,120p' "$TEST_TMP/output"
exit 1
fi
repo=$TEST_TMP/repository
mkdir -p "$repo"
git -C "$repo" init -q
git -C "$repo" config user.name 'Source Audit Test'
git -C "$repo" config user.email 'source-audit@example.invalid'
printf 'safe source\n' >"$repo/source.c"
printf '*.o\n' >"$repo/.gitignore"
mkdir -p "$repo/tests" "$repo/scripts" "$repo/packaging/opensuse"
printf 'fixture mentions vendor/vuptsdk/libvuptsdk.so.2\n' >"$repo/tests/test_source_only.sh"
printf '# scanner implementation fixture\n' >"$repo/scripts/check-source-only.sh"
printf '# scanner wrapper fixture\n' >"$repo/packaging/opensuse/source-audit.sh"
git -C "$repo" add source.c .gitignore tests scripts packaging
git -C "$repo" commit -qm 'safe source'
git -C "$repo" tag v1.0.0
if "$SCANNER" --root "$repo" --tag v1.0.0 >"$TEST_TMP/output" 2>&1 &&
grep -q '^PASS source-only:' "$TEST_TMP/output"; then
pass 'tracked, working-tree, HEAD archive, and tag archive pass'
else
printf 'not ok - repository and tag audit pass\n'
sed -n '1,120p' "$TEST_TMP/output"
exit 1
fi
printf '\177ELF\002\001\001\000ignored' >"$repo/ignored.o"
if "$SCANNER" --root "$repo" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - ignored working-tree object is rejected\n'
exit 1
elif grep -Fq ignored.o "$TEST_TMP/output"; then
pass 'ignored working-tree object is rejected'
else
printf 'not ok - ignored object path missing\n'
exit 1
fi
rm -f -- "$repo/ignored.o"
printf '\177ELF\002\001\001\000indexed' >"$repo/indexed.txt"
git -C "$repo" add indexed.txt
printf 'safe worktree replacement\n' >"$repo/indexed.txt"
if "$SCANNER" --root "$repo" >"$TEST_TMP/output" 2>&1; then
printf 'not ok - compiled indexed blob is rejected\n'
exit 1
elif grep -Fq indexed.txt "$TEST_TMP/output"; then
pass 'Git index content is audited independently of the worktree'
else
printf 'not ok - indexed path missing\n'
exit 1
fi
printf '1..%d\n' "$PASSED"