diff --git a/image/tests/test_shellsafe.py b/image/tests/test_shellsafe.py new file mode 100644 index 0000000..03fe326 --- /dev/null +++ b/image/tests/test_shellsafe.py @@ -0,0 +1,98 @@ +"""shellsafe validators: the API-boundary whitelist behind the v0.19.0 SSH +command-injection hardening. The quoting *sink* is covered in +test_launch_command.py; this locks in the *boundary* — that hostile input is +rejected early, and that a valid value passes through unchanged so callers can +use `validate_x(v)` inline. +""" +import pytest + +from app.shellsafe import validate_container, validate_image, validate_repo + +# Shell metacharacters that must never survive any validator — these are the +# actual injection vectors. (Path traversal like "../" is NOT in scope here: +# validate_image legitimately permits "/" and "." for real image refs such as +# nvcr.io/nim/...; the defense for images is "no shell metacharacters" + the +# quote_arg sink, not path-shape. Slash-rejection is tested directly for repo +# and container, where "/" is disallowed.) +HOSTILE = [ + "; rm -rf /", + " a b", + "$(touch pwned)", + "`id`", + "x|cat", + "x&y", + "x>out", + "x\nrm", +] + + +# ---- validate_repo: HF 'org/name', exactly one slash ---- + +@pytest.mark.parametrize("repo", [ + "RedHatAI/Qwen3.6-35B-A3B-NVFP4", # the live production model + "org/name", + "a.b_c-d/x.y_z-1", +]) +def test_repo_valid_passes_through_unchanged(repo): + assert validate_repo(repo) == repo + + +@pytest.mark.parametrize("repo", [ + "", + "noslash", + "a/b/c", # two slashes + "/name", # empty org + "org/", # empty name +] + [f"org/name{h}" for h in HOSTILE]) +def test_repo_rejects_malformed_and_hostile(repo): + with pytest.raises(ValueError): + validate_repo(repo) + + +# ---- validate_image: registry/path:tag@digest ---- + +@pytest.mark.parametrize("image", [ + "nvcr.io/nim/nvidia/parakeet-1_1b-ctc-en-us:latest", + "ubuntu", + "img@sha256:deadbeefcafe", + "a.b/c:1.2_3-4", +]) +def test_image_valid_passes_through_unchanged(image): + assert validate_image(image) == image + + +@pytest.mark.parametrize("image", [ + "", + "-leading", # must start alphanumeric + ".leading", + "/leading", + ":leading", + "a" * 513, # over the 512 cap +] + [f"img{h}" for h in HOSTILE]) +def test_image_rejects_malformed_and_hostile(image): + with pytest.raises(ValueError): + validate_image(image) + + +# ---- validate_container: Docker name rule, no slash ---- + +@pytest.mark.parametrize("name", [ + "parakeet-asr", + "a", + "vol_1.2-3", +]) +def test_container_valid_passes_through_unchanged(name): + assert validate_container(name) == name + + +@pytest.mark.parametrize("name", [ + "", + "_leading", # underscore is not a valid first char + "-leading", + ".leading", + "has/slash", # slash not allowed in a container name + "a" * 129, # over the 128 cap +] + [f"name{h}" for h in HOSTILE]) +def test_container_rejects_malformed_and_hostile(name): + with pytest.raises(ValueError): + validate_container(name)