From 99f04d5a147e1a5df1521d02469c58604fa97511 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 10:27:59 +0000 Subject: [PATCH 01/12] Improved testing and fixed poetry failure handling --- .githooks/pre-push | 13 +-- ossprey/sbom_python.py | 42 ++++++++-- ossprey/scan.py | 84 ++++++++++++------- ossprey/test_main.py | 22 +++-- .../docker_js_simple_math/Dockerfile | 0 .../docker_js_simple_math/build.sh | 0 .../javascript/.gitignore | 0 .../javascript/README.md | 0 .../javascript/package-lock.json | 0 .../javascript/package.json | 0 .../javascript/src/index.js | 0 .../javascript/test/simpleMath.test.js | 0 .../javascript/yarn.lock | 0 .../docker_py_simple_math/Dockerfile | 0 .../docker_py_simple_math/build.sh | 0 .../docker_py_simple_math/python/README.md | 0 .../docker_py_simple_math/python/main.py | 0 .../python/requirements.txt | 0 .../npm_simple_math/.gitignore | 0 .../npm_simple_math/README.md | 0 .../npm_simple_math/package-lock.json | 0 .../npm_simple_math/package.json | 0 .../npm_simple_math/src/index.js | 0 .../npm_simple_math/test/simpleMath.test.js | 0 .../poetry_broken_simple_math}/.gitignore | 0 .../poetry_broken_simple_math}/README.md | 0 .../poetry_simple_math/__init__.py | 0 .../poetry_simple_math/operations.py | 0 .../poetry_broken_simple_math/pyproject.toml | 62 ++++++++++++++ .../poetry_broken_simple_math}/setup.py | 0 .../poetry_simple_math/.gitignore | 1 + .../poetry_simple_math/README.md} | 0 .../poetry_simple_math/__init__.py | 0 .../poetry_simple_math}/operations.py | 0 .../poetry_simple_math/pyproject.toml | 0 .../python_simple_math/requirements.txt | 0 .../test_packages/python_simple_math/setup.py | 18 ++++ .../simple_math/__init__.py | 0 .../simple_math/operations.py | 23 +++++ .../yarn_massive_math/.gitignore | 0 .../yarn_massive_math/README.md | 0 .../yarn_massive_math/package.json | 0 .../yarn_massive_math/src/index.js | 0 .../yarn_massive_math/test/simpleMath.test.js | 0 .../yarn_massive_math/yarn.lock | 0 .../yarn_simple_math/.gitignore | 0 .../yarn_simple_math/README.md | 0 .../yarn_simple_math/package.json | 0 .../yarn_simple_math/src/index.js | 0 .../yarn_simple_math/test/simpleMath.test.js | 0 .../yarn_simple_math/yarn.lock | 0 51 files changed, 215 insertions(+), 50 deletions(-) rename test/{ => test_packages}/docker_js_simple_math/Dockerfile (100%) rename test/{ => test_packages}/docker_js_simple_math/build.sh (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/.gitignore (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/README.md (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/package-lock.json (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/package.json (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/src/index.js (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/test/simpleMath.test.js (100%) rename test/{ => test_packages}/docker_js_simple_math/javascript/yarn.lock (100%) rename test/{ => test_packages}/docker_py_simple_math/Dockerfile (100%) rename test/{ => test_packages}/docker_py_simple_math/build.sh (100%) rename test/{ => test_packages}/docker_py_simple_math/python/README.md (100%) rename test/{ => test_packages}/docker_py_simple_math/python/main.py (100%) rename test/{ => test_packages}/docker_py_simple_math/python/requirements.txt (100%) rename test/{ => test_packages}/npm_simple_math/.gitignore (100%) rename test/{ => test_packages}/npm_simple_math/README.md (100%) rename test/{ => test_packages}/npm_simple_math/package-lock.json (100%) rename test/{ => test_packages}/npm_simple_math/package.json (100%) rename test/{ => test_packages}/npm_simple_math/src/index.js (100%) rename test/{ => test_packages}/npm_simple_math/test/simpleMath.test.js (100%) rename test/{poetry_simple_math => test_packages/poetry_broken_simple_math}/.gitignore (100%) rename test/{poetry_simple_math => test_packages/poetry_broken_simple_math}/README.md (100%) rename test/{poetry_simple_math => test_packages/poetry_broken_simple_math}/poetry_simple_math/__init__.py (100%) rename test/{poetry_simple_math => test_packages/poetry_broken_simple_math}/poetry_simple_math/operations.py (100%) create mode 100644 test/test_packages/poetry_broken_simple_math/pyproject.toml rename test/{python_simple_math => test_packages/poetry_broken_simple_math}/setup.py (100%) create mode 100644 test/test_packages/poetry_simple_math/.gitignore rename test/{python_simple_math/simple_math/__init__.py => test_packages/poetry_simple_math/README.md} (100%) create mode 100644 test/test_packages/poetry_simple_math/poetry_simple_math/__init__.py rename test/{python_simple_math/simple_math => test_packages/poetry_simple_math/poetry_simple_math}/operations.py (100%) rename test/{ => test_packages}/poetry_simple_math/pyproject.toml (100%) rename test/{ => test_packages}/python_simple_math/requirements.txt (100%) create mode 100644 test/test_packages/python_simple_math/setup.py create mode 100644 test/test_packages/python_simple_math/simple_math/__init__.py create mode 100644 test/test_packages/python_simple_math/simple_math/operations.py rename test/{ => test_packages}/yarn_massive_math/.gitignore (100%) rename test/{ => test_packages}/yarn_massive_math/README.md (100%) rename test/{ => test_packages}/yarn_massive_math/package.json (100%) rename test/{ => test_packages}/yarn_massive_math/src/index.js (100%) rename test/{ => test_packages}/yarn_massive_math/test/simpleMath.test.js (100%) rename test/{ => test_packages}/yarn_massive_math/yarn.lock (100%) rename test/{ => test_packages}/yarn_simple_math/.gitignore (100%) rename test/{ => test_packages}/yarn_simple_math/README.md (100%) rename test/{ => test_packages}/yarn_simple_math/package.json (100%) rename test/{ => test_packages}/yarn_simple_math/src/index.js (100%) rename test/{ => test_packages}/yarn_simple_math/test/simpleMath.test.js (100%) rename test/{ => test_packages}/yarn_simple_math/yarn.lock (100%) diff --git a/.githooks/pre-push b/.githooks/pre-push index f019f8b..9b83b9b 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -9,13 +9,16 @@ trap 'exit 1' ERR poetry run pytest # Test with test packages locally -poetry run python -m ossprey --dry-run-safe --package test/npm_simple_math/ --mode npm -poetry run python -m ossprey --dry-run-safe --package test/yarn_simple_math/ --mode yarn -poetry run python -m ossprey --dry-run-safe --package test/yarn_massive_math/ --mode yarn -poetry run python -m ossprey --dry-run-safe --package test/python_simple_math/ --mode python-requirements +poetry run python -m ossprey --dry-run-safe --package test/test_packages/npm_simple_math/ --mode npm +poetry run python -m ossprey --dry-run-safe --package test/test_packages/yarn_simple_math/ --mode yarn +poetry run python -m ossprey --dry-run-safe --package test/test_packages/yarn_massive_math/ --mode yarn +poetry run python -m ossprey --dry-run-safe --package test/test_packages/python_simple_math/ --mode python-requirements +poetry run python -m ossprey --dry-run-safe --package test/test_packages/poetry_simple_math/ --mode poetry +poetry run python -m ossprey --dry-run-safe --package test/test_packages/poetry_broken_simple_math/ + # Test malicious detection (should fail with exit code 1) -if poetry run python -m ossprey --dry-run-malicious --package test/npm_simple_math/ --mode npm; then +if poetry run python -m ossprey --dry-run-malicious --package test/test_packages/npm_simple_math/ --mode npm; then echo "ERROR: Malicious package detection failed - should have detected malware!" exit 1 else diff --git a/ossprey/sbom_python.py b/ossprey/sbom_python.py index a5708d9..b40e16e 100644 --- a/ossprey/sbom_python.py +++ b/ossprey/sbom_python.py @@ -13,6 +13,8 @@ from ossbom.model.ossbom import OSSBOM from ossbom.model.component import Component +from ossprey.virtualenv import VirtualEnv + logger = logging.getLogger(__name__) @@ -34,11 +36,11 @@ def create_sbom_from_requirements(requirements_file: str) -> OSSBOM: cmd = get_cyclonedx_binary() # This command generates an SBOM for the active virtual environment in JSON format result = subprocess.run( - [cmd, 'requirements', requirements_file], + [cmd, "requirements", requirements_file], check=True, capture_output=True, text=True, - env=os.environ.copy() + env=os.environ.copy(), ) ret = result.stdout @@ -60,7 +62,7 @@ def create_sbom_from_requirements(requirements_file: str) -> OSSBOM: def update_sbom_from_requirements(ossbom: OSSBOM, requirements_file: str) -> OSSBOM: sbom = create_sbom_from_requirements(requirements_file) ossbom.add_components(sbom.get_components()) - + return ossbom @@ -70,11 +72,11 @@ def create_sbom_from_env() -> OSSBOM: cmd = get_cyclonedx_binary() # This command generates an SBOM for the active virtual environment in JSON format result = subprocess.run( - [cmd, 'environment'], + [cmd, "environment"], check=True, capture_output=True, text=True, - env=os.environ.copy() + env=os.environ.copy(), ) ret = result.stdout @@ -110,16 +112,38 @@ def get_poetry_purls_from_lock(lockfile: str = "poetry.lock") -> list[PackageURL def update_sbom_from_poetry(ossbom: OSSBOM, package_dir: str) -> OSSBOM: if not os.path.exists(os.path.join(package_dir, "poetry.lock")): - # Run poetry install to generate the poetry.lock file + # Run poetry install to generate the poetry.lock file try: - subprocess.run(['poetry', 'install'], cwd=package_dir, check=True) + subprocess.run(["poetry", "install"], cwd=package_dir, check=True) except subprocess.CalledProcessError as e: logger.error(f"Error running poetry install: {e}") raise e - + # Get the packages from the poetry.lock file purls = get_poetry_purls_from_lock(os.path.join(package_dir, "poetry.lock")) - ossbom.add_components([Component.create(name=purl.name, version=purl.version, source="poetry", type="pypi") for purl in purls]) + ossbom.add_components( + [ + Component.create( + name=purl.name, version=purl.version, source="poetry", type="pypi" + ) + for purl in purls + ] + ) + + return ossbom + + +def update_sbom_from_virtualenv(ossbom: OSSBOM, package_name: str) -> OSSBOM: + + venv = VirtualEnv() + venv.enter() + + venv.install_package(package_name) + requirements_file = venv.create_requirements_file_from_env() + + ossbom = update_sbom_from_requirements(ossbom, requirements_file) + + venv.exit() return ossbom diff --git a/ossprey/scan.py b/ossprey/scan.py index 9cdbabd..eb0871a 100644 --- a/ossprey/scan.py +++ b/ossprey/scan.py @@ -2,16 +2,18 @@ import json import logging import os -from packageurl import PackageURL +from subprocess import CalledProcessError from ossprey.environment import get_environment_details -from ossprey.log import init_logging from ossprey.modes import get_modes, get_all_modes -from ossprey.sbom_python import update_sbom_from_requirements, update_sbom_from_poetry +from ossprey.sbom_python import ( + update_sbom_from_requirements, + update_sbom_from_poetry, + update_sbom_from_virtualenv, +) from ossprey.sbom_javascript import update_sbom_from_npm, update_sbom_from_yarn from ossprey.sbom_filesystem import update_sbom_from_filesystem from ossprey.ossprey import Ossprey -from ossprey.virtualenv import VirtualEnv from ossbom.converters.factory import SBOMConverterFactory from ossbom.model.ossbom import OSSBOM @@ -20,6 +22,49 @@ logger = logging.getLogger(__name__) +def scan_python(modes: list[str], sbom: OSSBOM, package_name: str) -> OSSBOM: + + if "python-requirements" in modes: + sbom = update_sbom_from_requirements(sbom, package_name + "/requirements.txt") + + if "poetry" in modes: + try: + sbom = update_sbom_from_poetry(sbom, package_name) + except CalledProcessError as e: + logger.error( + "Error running poetry scan, skipping poetry scan, attempting pipenv scan instead" + ) + logger.debug(e.stderr) + logger.debug("--") + logger.debug(e.stdout) + + # Appending pipenv to modes to attempt to get some python dependencies if poetry fails + modes.append("pipenv") + + if "pipenv" in modes: + sbom = update_sbom_from_virtualenv(sbom, package_name) + + return sbom + + +def scan_javascript(modes: list[str], sbom: OSSBOM, package_name: str) -> OSSBOM: + if "npm" in modes: + sbom = update_sbom_from_npm(sbom, package_name) + + if "yarn" in modes: + sbom = update_sbom_from_yarn(sbom, package_name) + + return sbom + + +def scan_filesystem(modes: list[str], sbom: OSSBOM, package_name: str) -> OSSBOM: + + if "fs" in modes: + sbom = update_sbom_from_filesystem(sbom, package_name) + + return sbom + + def scan( package_name: str, mode: str = "auto", @@ -50,37 +95,16 @@ def scan( if any(mode not in get_all_modes() for mode in modes) or len(modes) == 0: raise Exception("Invalid scanning method: " + str(modes)) - if "pipenv" in modes: - venv = VirtualEnv() - venv.enter() - - venv.install_package(package_name) - requirements_file = venv.create_requirements_file_from_env() - - sbom = update_sbom_from_requirements(sbom, requirements_file) - - venv.exit() - - if "python-requirements" in modes: - sbom = update_sbom_from_requirements(sbom, package_name + "/requirements.txt") - - if "poetry" in modes: - sbom = update_sbom_from_poetry(sbom, package_name) - - if "npm" in modes: - sbom = update_sbom_from_npm(sbom, package_name) - - if "yarn" in modes: - sbom = update_sbom_from_yarn(sbom, package_name) - - if "fs" in modes: - sbom = update_sbom_from_filesystem(sbom, package_name) + # Enrich the sbom based on the scans provided + sbom = scan_python(modes, sbom, package_name) + sbom = scan_javascript(modes, sbom, package_name) + sbom = scan_filesystem(modes, sbom, package_name) # Update sbom to contain the local environment env = get_environment_details(package_name) sbom.update_environment(env) - logger.info(f"Scanning {len(sbom.get_components())}") + logger.info(f"Scanning {len(sbom.get_components())} components") if not local_scan: ossprey = Ossprey(url, api_key) diff --git a/ossprey/test_main.py b/ossprey/test_main.py index da312b4..53d7c55 100644 --- a/ossprey/test_main.py +++ b/ossprey/test_main.py @@ -7,7 +7,7 @@ def test_main_function(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["script.py"]) - monkeypatch.setenv("INPUT_PACKAGE", "test/python_simple_math") + monkeypatch.setenv("INPUT_PACKAGE", "test/test_packages/python_simple_math") monkeypatch.setenv("INPUT_MODE", "python-requirements") monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") @@ -23,7 +23,7 @@ def test_main_function(monkeypatch, capsys): def test_main_function_with_output(monkeypatch, capsys): monkeypatch.setattr("sys.argv", ["script.py"]) - monkeypatch.setenv("INPUT_PACKAGE", "test/python_simple_math") + monkeypatch.setenv("INPUT_PACKAGE", "test/test_packages/python_simple_math") monkeypatch.setenv("INPUT_MODE", "python-requirements") monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") monkeypatch.setenv("INPUT_OUTPUT", "sbom_output.json") @@ -44,12 +44,12 @@ def test_main_function_with_output(monkeypatch, capsys): os.remove("sbom_output.json") -@pytest.mark.parametrize("soft_error, expected_ret", [ - ("True", 0), - ("False", 1)]) +@pytest.mark.parametrize("soft_error, expected_ret", [("True", 0), ("False", 1)]) def test_main_function_soft_error(monkeypatch, soft_error, expected_ret): monkeypatch.setattr("sys.argv", ["script.py"]) - monkeypatch.setenv("INPUT_PACKAGE", "test/python_simple_math_no_exist") + monkeypatch.setenv( + "INPUT_PACKAGE", "test/test_packages/python_simple_math_no_exist" + ) monkeypatch.setenv("INPUT_MODE", "python-requirements") monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") monkeypatch.setenv("INPUT_SOFT_ERROR", soft_error) @@ -59,3 +59,13 @@ def test_main_function_soft_error(monkeypatch, soft_error, expected_ret): assert excinfo.value.code == expected_ret + +def test_main_function_with_broken_poetry(monkeypatch, capsys): + monkeypatch.setattr("sys.argv", ["script.py"]) + monkeypatch.setenv("INPUT_PACKAGE", "test/test_packages/poetry_broken_simple_math") + monkeypatch.setenv("INPUT_DRY_RUN_SAFE", "True") + + with pytest.raises(SystemExit) as excinfo: + main() + + assert excinfo.value.code == 0 diff --git a/test/docker_js_simple_math/Dockerfile b/test/test_packages/docker_js_simple_math/Dockerfile similarity index 100% rename from test/docker_js_simple_math/Dockerfile rename to test/test_packages/docker_js_simple_math/Dockerfile diff --git a/test/docker_js_simple_math/build.sh b/test/test_packages/docker_js_simple_math/build.sh similarity index 100% rename from test/docker_js_simple_math/build.sh rename to test/test_packages/docker_js_simple_math/build.sh diff --git a/test/docker_js_simple_math/javascript/.gitignore b/test/test_packages/docker_js_simple_math/javascript/.gitignore similarity index 100% rename from test/docker_js_simple_math/javascript/.gitignore rename to test/test_packages/docker_js_simple_math/javascript/.gitignore diff --git a/test/docker_js_simple_math/javascript/README.md b/test/test_packages/docker_js_simple_math/javascript/README.md similarity index 100% rename from test/docker_js_simple_math/javascript/README.md rename to test/test_packages/docker_js_simple_math/javascript/README.md diff --git a/test/docker_js_simple_math/javascript/package-lock.json b/test/test_packages/docker_js_simple_math/javascript/package-lock.json similarity index 100% rename from test/docker_js_simple_math/javascript/package-lock.json rename to test/test_packages/docker_js_simple_math/javascript/package-lock.json diff --git a/test/docker_js_simple_math/javascript/package.json b/test/test_packages/docker_js_simple_math/javascript/package.json similarity index 100% rename from test/docker_js_simple_math/javascript/package.json rename to test/test_packages/docker_js_simple_math/javascript/package.json diff --git a/test/docker_js_simple_math/javascript/src/index.js b/test/test_packages/docker_js_simple_math/javascript/src/index.js similarity index 100% rename from test/docker_js_simple_math/javascript/src/index.js rename to test/test_packages/docker_js_simple_math/javascript/src/index.js diff --git a/test/docker_js_simple_math/javascript/test/simpleMath.test.js b/test/test_packages/docker_js_simple_math/javascript/test/simpleMath.test.js similarity index 100% rename from test/docker_js_simple_math/javascript/test/simpleMath.test.js rename to test/test_packages/docker_js_simple_math/javascript/test/simpleMath.test.js diff --git a/test/docker_js_simple_math/javascript/yarn.lock b/test/test_packages/docker_js_simple_math/javascript/yarn.lock similarity index 100% rename from test/docker_js_simple_math/javascript/yarn.lock rename to test/test_packages/docker_js_simple_math/javascript/yarn.lock diff --git a/test/docker_py_simple_math/Dockerfile b/test/test_packages/docker_py_simple_math/Dockerfile similarity index 100% rename from test/docker_py_simple_math/Dockerfile rename to test/test_packages/docker_py_simple_math/Dockerfile diff --git a/test/docker_py_simple_math/build.sh b/test/test_packages/docker_py_simple_math/build.sh similarity index 100% rename from test/docker_py_simple_math/build.sh rename to test/test_packages/docker_py_simple_math/build.sh diff --git a/test/docker_py_simple_math/python/README.md b/test/test_packages/docker_py_simple_math/python/README.md similarity index 100% rename from test/docker_py_simple_math/python/README.md rename to test/test_packages/docker_py_simple_math/python/README.md diff --git a/test/docker_py_simple_math/python/main.py b/test/test_packages/docker_py_simple_math/python/main.py similarity index 100% rename from test/docker_py_simple_math/python/main.py rename to test/test_packages/docker_py_simple_math/python/main.py diff --git a/test/docker_py_simple_math/python/requirements.txt b/test/test_packages/docker_py_simple_math/python/requirements.txt similarity index 100% rename from test/docker_py_simple_math/python/requirements.txt rename to test/test_packages/docker_py_simple_math/python/requirements.txt diff --git a/test/npm_simple_math/.gitignore b/test/test_packages/npm_simple_math/.gitignore similarity index 100% rename from test/npm_simple_math/.gitignore rename to test/test_packages/npm_simple_math/.gitignore diff --git a/test/npm_simple_math/README.md b/test/test_packages/npm_simple_math/README.md similarity index 100% rename from test/npm_simple_math/README.md rename to test/test_packages/npm_simple_math/README.md diff --git a/test/npm_simple_math/package-lock.json b/test/test_packages/npm_simple_math/package-lock.json similarity index 100% rename from test/npm_simple_math/package-lock.json rename to test/test_packages/npm_simple_math/package-lock.json diff --git a/test/npm_simple_math/package.json b/test/test_packages/npm_simple_math/package.json similarity index 100% rename from test/npm_simple_math/package.json rename to test/test_packages/npm_simple_math/package.json diff --git a/test/npm_simple_math/src/index.js b/test/test_packages/npm_simple_math/src/index.js similarity index 100% rename from test/npm_simple_math/src/index.js rename to test/test_packages/npm_simple_math/src/index.js diff --git a/test/npm_simple_math/test/simpleMath.test.js b/test/test_packages/npm_simple_math/test/simpleMath.test.js similarity index 100% rename from test/npm_simple_math/test/simpleMath.test.js rename to test/test_packages/npm_simple_math/test/simpleMath.test.js diff --git a/test/poetry_simple_math/.gitignore b/test/test_packages/poetry_broken_simple_math/.gitignore similarity index 100% rename from test/poetry_simple_math/.gitignore rename to test/test_packages/poetry_broken_simple_math/.gitignore diff --git a/test/poetry_simple_math/README.md b/test/test_packages/poetry_broken_simple_math/README.md similarity index 100% rename from test/poetry_simple_math/README.md rename to test/test_packages/poetry_broken_simple_math/README.md diff --git a/test/poetry_simple_math/poetry_simple_math/__init__.py b/test/test_packages/poetry_broken_simple_math/poetry_simple_math/__init__.py similarity index 100% rename from test/poetry_simple_math/poetry_simple_math/__init__.py rename to test/test_packages/poetry_broken_simple_math/poetry_simple_math/__init__.py diff --git a/test/poetry_simple_math/poetry_simple_math/operations.py b/test/test_packages/poetry_broken_simple_math/poetry_simple_math/operations.py similarity index 100% rename from test/poetry_simple_math/poetry_simple_math/operations.py rename to test/test_packages/poetry_broken_simple_math/poetry_simple_math/operations.py diff --git a/test/test_packages/poetry_broken_simple_math/pyproject.toml b/test/test_packages/poetry_broken_simple_math/pyproject.toml new file mode 100644 index 0000000..bd95d31 --- /dev/null +++ b/test/test_packages/poetry_broken_simple_math/pyproject.toml @@ -0,0 +1,62 @@ +[tool.coverage.run] +source = ["transformers"] +omit = [ + "*/convert_*", + "*/__main__.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "raise", + "except", + "register_parameter" +] + +[tool.ruff] +target-version = "py39" +line-length = 119 + +[tool.ruff.lint] +# Never enforce `E501` (line length violations). +ignore = ["C901", "E501", "E741", "F402", "F823"] +# RUF013: Checks for the use of implicit Optional +# in type annotations when the default parameter value is None. +select = ["C", "E", "F", "I", "W", "RUF013", "UP006", "PERF102", "PLC1802", "PLC0208"] +extend-safe-fixes = ["UP006"] + +# Ignore import violations in all `__init__.py` files. +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401", "F403", "F811"] +"src/transformers/file_utils.py" = ["F401"] +"src/transformers/utils/dummy_*.py" = ["F401"] + +[tool.ruff.lint.isort] +lines-after-imports = 2 +known-first-party = ["transformers"] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.pytest.ini_options] +addopts = "--doctest-glob='**/*.md'" +doctest_optionflags="NUMBER NORMALIZE_WHITESPACE ELLIPSIS" +markers = [ + "flash_attn_3_test: marks tests related to flash attention 3 (deselect with '-m \"not flash_attn_3_test\"')", + "flash_attn_test: marks tests related to flash attention (deselect with '-m \"not flash_attn_test\"')", + "bitsandbytes: select (or deselect with `not`) bitsandbytes integration tests", + "generate: marks tests that use the GenerationTesterMixin" +] +log_cli = 1 +log_cli_level = "WARNING" +asyncio_default_fixture_loop_scope = "function" \ No newline at end of file diff --git a/test/python_simple_math/setup.py b/test/test_packages/poetry_broken_simple_math/setup.py similarity index 100% rename from test/python_simple_math/setup.py rename to test/test_packages/poetry_broken_simple_math/setup.py diff --git a/test/test_packages/poetry_simple_math/.gitignore b/test/test_packages/poetry_simple_math/.gitignore new file mode 100644 index 0000000..c04bc49 --- /dev/null +++ b/test/test_packages/poetry_simple_math/.gitignore @@ -0,0 +1 @@ +poetry.lock diff --git a/test/python_simple_math/simple_math/__init__.py b/test/test_packages/poetry_simple_math/README.md similarity index 100% rename from test/python_simple_math/simple_math/__init__.py rename to test/test_packages/poetry_simple_math/README.md diff --git a/test/test_packages/poetry_simple_math/poetry_simple_math/__init__.py b/test/test_packages/poetry_simple_math/poetry_simple_math/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/python_simple_math/simple_math/operations.py b/test/test_packages/poetry_simple_math/poetry_simple_math/operations.py similarity index 100% rename from test/python_simple_math/simple_math/operations.py rename to test/test_packages/poetry_simple_math/poetry_simple_math/operations.py diff --git a/test/poetry_simple_math/pyproject.toml b/test/test_packages/poetry_simple_math/pyproject.toml similarity index 100% rename from test/poetry_simple_math/pyproject.toml rename to test/test_packages/poetry_simple_math/pyproject.toml diff --git a/test/python_simple_math/requirements.txt b/test/test_packages/python_simple_math/requirements.txt similarity index 100% rename from test/python_simple_math/requirements.txt rename to test/test_packages/python_simple_math/requirements.txt diff --git a/test/test_packages/python_simple_math/setup.py b/test/test_packages/python_simple_math/setup.py new file mode 100644 index 0000000..6a04527 --- /dev/null +++ b/test/test_packages/python_simple_math/setup.py @@ -0,0 +1,18 @@ +# setup.py + +from setuptools import setup, find_packages + +setup( + name="simple_math", + version="0.1.0", + packages=find_packages(), + install_requires=[ + "requests==2.26.0", + "numpy==2.0.0" + ], + entry_points={ + 'console_scripts': [ + 'simple-math=simple_math.operations:add', + ], + }, +) diff --git a/test/test_packages/python_simple_math/simple_math/__init__.py b/test/test_packages/python_simple_math/simple_math/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/test/test_packages/python_simple_math/simple_math/operations.py b/test/test_packages/python_simple_math/simple_math/operations.py new file mode 100644 index 0000000..cd37a75 --- /dev/null +++ b/test/test_packages/python_simple_math/simple_math/operations.py @@ -0,0 +1,23 @@ +from __future__ import annotations +# simple_math/operations.py +from typing import Union + +Number = Union[int, float] + + +def add(a: Number, b: Number) -> Number: + return a + b + + +def subtract(a: Number, b: Number) -> Number: + return a - b + + +def multiply(a: Number, b: Number) -> Number: + return a * b + + +def divide(a: Number, b: Number) -> float: + if b == 0: + raise ValueError("Cannot divide by zero") + return a / b diff --git a/test/yarn_massive_math/.gitignore b/test/test_packages/yarn_massive_math/.gitignore similarity index 100% rename from test/yarn_massive_math/.gitignore rename to test/test_packages/yarn_massive_math/.gitignore diff --git a/test/yarn_massive_math/README.md b/test/test_packages/yarn_massive_math/README.md similarity index 100% rename from test/yarn_massive_math/README.md rename to test/test_packages/yarn_massive_math/README.md diff --git a/test/yarn_massive_math/package.json b/test/test_packages/yarn_massive_math/package.json similarity index 100% rename from test/yarn_massive_math/package.json rename to test/test_packages/yarn_massive_math/package.json diff --git a/test/yarn_massive_math/src/index.js b/test/test_packages/yarn_massive_math/src/index.js similarity index 100% rename from test/yarn_massive_math/src/index.js rename to test/test_packages/yarn_massive_math/src/index.js diff --git a/test/yarn_massive_math/test/simpleMath.test.js b/test/test_packages/yarn_massive_math/test/simpleMath.test.js similarity index 100% rename from test/yarn_massive_math/test/simpleMath.test.js rename to test/test_packages/yarn_massive_math/test/simpleMath.test.js diff --git a/test/yarn_massive_math/yarn.lock b/test/test_packages/yarn_massive_math/yarn.lock similarity index 100% rename from test/yarn_massive_math/yarn.lock rename to test/test_packages/yarn_massive_math/yarn.lock diff --git a/test/yarn_simple_math/.gitignore b/test/test_packages/yarn_simple_math/.gitignore similarity index 100% rename from test/yarn_simple_math/.gitignore rename to test/test_packages/yarn_simple_math/.gitignore diff --git a/test/yarn_simple_math/README.md b/test/test_packages/yarn_simple_math/README.md similarity index 100% rename from test/yarn_simple_math/README.md rename to test/test_packages/yarn_simple_math/README.md diff --git a/test/yarn_simple_math/package.json b/test/test_packages/yarn_simple_math/package.json similarity index 100% rename from test/yarn_simple_math/package.json rename to test/test_packages/yarn_simple_math/package.json diff --git a/test/yarn_simple_math/src/index.js b/test/test_packages/yarn_simple_math/src/index.js similarity index 100% rename from test/yarn_simple_math/src/index.js rename to test/test_packages/yarn_simple_math/src/index.js diff --git a/test/yarn_simple_math/test/simpleMath.test.js b/test/test_packages/yarn_simple_math/test/simpleMath.test.js similarity index 100% rename from test/yarn_simple_math/test/simpleMath.test.js rename to test/test_packages/yarn_simple_math/test/simpleMath.test.js diff --git a/test/yarn_simple_math/yarn.lock b/test/test_packages/yarn_simple_math/yarn.lock similarity index 100% rename from test/yarn_simple_math/yarn.lock rename to test/test_packages/yarn_simple_math/yarn.lock From 4c39dc59f6e548f0c73b0e99cb38396587bba586 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 11:49:04 +0000 Subject: [PATCH 02/12] add smoke tests --- test/smoke/conftest.py | 31 +++ test/smoke/test_smoke.py | 460 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 491 insertions(+) create mode 100644 test/smoke/conftest.py create mode 100644 test/smoke/test_smoke.py diff --git a/test/smoke/conftest.py b/test/smoke/conftest.py new file mode 100644 index 0000000..21c2474 --- /dev/null +++ b/test/smoke/conftest.py @@ -0,0 +1,31 @@ +"""Fixtures and markers for ossprey smoke tests.""" + +from __future__ import annotations + +import os +import pytest +from pathlib import Path + + +def pytest_configure(config): + config.addinivalue_line("markers", "smoke: mark test as a smoke test") + config.addinivalue_line("markers", "slow: mark test as slow (installs packages)") + config.addinivalue_line("markers", "network: mark test as requiring network access") + + +@pytest.fixture +def project_root() -> Path: + """Return the absolute path to the project root.""" + return Path(__file__).resolve().parent.parent.parent + + +@pytest.fixture +def test_packages_dir(project_root) -> Path: + """Return the absolute path to test/test_packages/.""" + return project_root / "test" / "test_packages" + + +@pytest.fixture +def has_api_key() -> bool: + """Check if API_KEY env var is set.""" + return bool(os.environ.get("API_KEY")) diff --git a/test/smoke/test_smoke.py b/test/smoke/test_smoke.py new file mode 100644 index 0000000..8471ea8 --- /dev/null +++ b/test/smoke/test_smoke.py @@ -0,0 +1,460 @@ +""" +Smoke tests for ossprey — end-to-end CLI verification. + +Invokes `python -m ossprey` via subprocess to exercise the full scanning pipeline. + +By default uses --dry-run-safe (no API key needed). +If API_KEY env var is set, also runs a real API scan pass. + +Usage: + poetry run pytest test/smoke/ -v + poetry run pytest test/smoke/ -v -m "smoke and not slow" + poetry run pytest test/smoke/ -v -m "smoke and not network" + API_KEY= poetry run pytest test/smoke/ -v +""" + +from __future__ import annotations + +import json +import os +import shutil +import subprocess +import sys +import textwrap +from pathlib import Path + +import pytest + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +PYTHON = sys.executable + + +def run_ossprey( + package_dir: str | Path, + *, + mode: str = "auto", + dry_run: str = "safe", + output_file: str | Path | None = None, + expect_exit: int = 0, + extra_args: list[str] | None = None, + timeout: int = 300, +) -> subprocess.CompletedProcess: + """Run ``python -m ossprey`` as a subprocess and assert the exit code. + + Parameters + ---------- + package_dir: + Directory to scan. + mode: + Scanning mode (auto, pipenv, python-requirements, poetry, npm, yarn, fs). + dry_run: + "safe", "malicious", or None. When None and API_KEY is available a + real API scan is made, otherwise falls back to "safe". + output_file: + If provided, pass ``-o `` so ossprey writes the SBOM JSON. + expect_exit: + Expected process exit code. + extra_args: + Additional CLI flags. + timeout: + Subprocess timeout in seconds. + """ + cmd = [PYTHON, "-m", "ossprey", "--package", str(package_dir), "--mode", mode, "-v"] + + if dry_run == "safe": + cmd.append("--dry-run-safe") + elif dry_run == "malicious": + cmd.append("--dry-run-malicious") + else: + # Real API scan — requires API_KEY + api_key = os.environ.get("API_KEY") + if api_key: + cmd.extend(["--api-key", api_key]) + else: + # Fall back to dry-run-safe when no key is present + cmd.append("--dry-run-safe") + + if output_file is not None: + cmd.extend(["-o", str(output_file)]) + + if extra_args: + cmd.extend(extra_args) + + result = subprocess.run( + cmd, + capture_output=True, + text=True, + timeout=timeout, + cwd=str(Path(__file__).resolve().parent.parent.parent), # project root + ) + + assert result.returncode == expect_exit, ( + f"ossprey exited {result.returncode}, expected {expect_exit}.\n" + f"CMD: {' '.join(cmd)}\n" + f"STDOUT:\n{result.stdout[-2000:]}\n" + f"STDERR:\n{result.stderr[-2000:]}" + ) + return result + + +def assert_sbom_has_components(sbom_path: Path, min_components: int = 1): + """Load an SBOM JSON file and assert it contains components.""" + assert sbom_path.exists(), f"SBOM output file not found: {sbom_path}" + data = json.loads(sbom_path.read_text()) + components = data.get("components", []) + assert ( + len(components) >= min_components + ), f"Expected >= {min_components} components, got {len(components)} in {sbom_path}" + return data + + +def maybe_run_api_scan(package_dir: str | Path, output_file: Path | None = None): + """If API_KEY is set, also run a real API scan (non-dry-run).""" + if os.environ.get("API_KEY"): + run_ossprey(package_dir, dry_run=None, output_file=output_file) + + +# --------------------------------------------------------------------------- +# 1. Scan all existing test_packages +# --------------------------------------------------------------------------- + +EXISTING_PACKAGES = [ + "python_simple_math", + "poetry_simple_math", + "poetry_broken_simple_math", + "npm_simple_math", + "yarn_simple_math", + "yarn_massive_math", +] + + +@pytest.mark.smoke +@pytest.mark.parametrize("pkg_name", EXISTING_PACKAGES) +def test_existing_test_packages(pkg_name, test_packages_dir, tmp_path): + """Scan each non-Docker test package and verify a clean SBOM is produced.""" + pkg_dir = test_packages_dir / pkg_name + assert pkg_dir.is_dir(), f"Test package not found: {pkg_dir}" + + sbom_file = tmp_path / "sbom.json" + result = run_ossprey(pkg_dir, output_file=sbom_file) + + # Verify stdout reports clean + assert "No malware found" in result.stdout + + # Verify SBOM file has components + assert_sbom_has_components(sbom_file) + + # Optionally exercise real API + maybe_run_api_scan(pkg_dir) + + +# --------------------------------------------------------------------------- +# 2. Dry-run malicious path +# --------------------------------------------------------------------------- + + +@pytest.mark.smoke +def test_dry_run_malicious(test_packages_dir, tmp_path): + """Verify --dry-run-malicious injects a vulnerability and exits 1.""" + pkg_dir = test_packages_dir / "python_simple_math" + sbom_file = tmp_path / "sbom.json" + + result = run_ossprey( + pkg_dir, + dry_run="malicious", + output_file=sbom_file, + expect_exit=1, + ) + + assert "malware" in result.stdout.lower() or "warning" in result.stdout.lower() + + # SBOM should contain a vulnerability + data = json.loads(sbom_file.read_text()) + vulns = data.get("vulnerabilities", []) + assert len(vulns) >= 1, "Expected at least 1 vulnerability in malicious dry-run" + + +# --------------------------------------------------------------------------- +# 3. Installable test packages (pip-installable projects scanned via auto) +# --------------------------------------------------------------------------- + +INSTALLABLE_PACKAGES = [ + "python_simple_math", # has setup.py + requirements.txt + "poetry_simple_math", # has pyproject.toml with build-system +] + + +@pytest.mark.smoke +@pytest.mark.slow +@pytest.mark.parametrize("pkg_name", INSTALLABLE_PACKAGES) +def test_installable_existing_packages(pkg_name, test_packages_dir, tmp_path): + """Scan pip-installable test packages via auto mode.""" + pkg_dir = test_packages_dir / pkg_name + sbom_file = tmp_path / "sbom.json" + + result = run_ossprey(pkg_dir, output_file=sbom_file, timeout=600) + + assert "No malware found" in result.stdout + assert_sbom_has_components(sbom_file, min_components=2) + + +# --------------------------------------------------------------------------- +# 4. Real-world Python packages (requirements.txt scanning) +# --------------------------------------------------------------------------- + +REAL_PYTHON_PACKAGES = [ + ("flask", "flask==3.0.0"), + ("django", "django==5.0"), + ("fastapi", "fastapi==0.109.0"), + ("boto3", "boto3==1.34.0"), + ("requests", "requests==2.31.0"), + ("numpy", "numpy==1.26.3"), +] + + +@pytest.mark.smoke +@pytest.mark.network +@pytest.mark.parametrize( + "name, spec", REAL_PYTHON_PACKAGES, ids=[p[0] for p in REAL_PYTHON_PACKAGES] +) +def test_real_world_python_requirements(name, spec, tmp_path): + """Create a requirements.txt with a real package and scan it.""" + pkg_dir = tmp_path / name + pkg_dir.mkdir() + (pkg_dir / "requirements.txt").write_text(spec + "\n") + + sbom_file = tmp_path / f"{name}_sbom.json" + result = run_ossprey(pkg_dir, output_file=sbom_file) + + assert "No malware found" in result.stdout + data = assert_sbom_has_components(sbom_file) + + # The named package should appear in components + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert name in comp_names, f"Expected '{name}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 5. Real-world Python packages (installable — full install via auto) +# --------------------------------------------------------------------------- + +REAL_INSTALLABLE_PACKAGES = [ + ("flask_pkg", ["flask==3.0.0"], "flask"), + ("django_pkg", ["django==5.0"], "django"), + ("requests_numpy_pkg", ["requests==2.31.0", "numpy==1.26.3"], "requests"), + ("boto3_pkg", ["boto3==1.34.0"], "boto3"), + ("fastapi_pkg", ["fastapi==0.109.0"], "fastapi"), +] + + +def _create_installable_package( + base_dir: Path, name: str, install_requires: list[str] +) -> Path: + """Create a minimal pip-installable package with a requirements.txt for auto-detection.""" + pkg_dir = base_dir / name + pkg_dir.mkdir() + + inner_pkg = pkg_dir / "mypkg" + inner_pkg.mkdir() + (inner_pkg / "__init__.py").write_text("") + + deps_str = ", ".join(f'"{d}"' for d in install_requires) + setup_py = textwrap.dedent( + f"""\ + from setuptools import setup, find_packages + setup( + name="{name}", + version="0.0.1", + packages=find_packages(), + install_requires=[{deps_str}], + ) + """ + ) + (pkg_dir / "setup.py").write_text(setup_py) + + # Also write requirements.txt so auto mode detects python-requirements + (pkg_dir / "requirements.txt").write_text("\n".join(install_requires) + "\n") + + return pkg_dir + + +@pytest.mark.smoke +@pytest.mark.slow +@pytest.mark.network +@pytest.mark.parametrize( + "name, deps, expected_component", + REAL_INSTALLABLE_PACKAGES, + ids=[p[0] for p in REAL_INSTALLABLE_PACKAGES], +) +def test_real_world_installable_packages(name, deps, expected_component, tmp_path): + """Create a minimal installable package with requirements.txt, scan via auto mode.""" + pkg_dir = _create_installable_package(tmp_path, name, deps) + sbom_file = tmp_path / f"{name}_sbom.json" + + result = run_ossprey(pkg_dir, output_file=sbom_file, timeout=600) + + assert "No malware found" in result.stdout + + data = assert_sbom_has_components(sbom_file) + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert ( + expected_component in comp_names + ), f"Expected '{expected_component}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 6. Real-world npm packages +# --------------------------------------------------------------------------- + +REAL_NPM_PACKAGES = [ + ("express", {"express": "4.18.2"}), + ("react", {"react": "18.2.0"}), + ("axios", {"axios": "1.6.5"}), + ("lodash", {"lodash": "4.17.21"}), + ("typescript", {"typescript": "5.3.3"}), +] + + +def _create_npm_project(base_dir: Path, name: str, dependencies: dict) -> Path: + """Create a directory with a package.json and run npm install.""" + pkg_dir = base_dir / name + pkg_dir.mkdir() + + package_json = { + "name": f"smoke-test-{name}", + "version": "1.0.0", + "private": True, + "dependencies": dependencies, + } + (pkg_dir / "package.json").write_text(json.dumps(package_json, indent=2)) + + # npm install to generate package-lock.json and node_modules + subprocess.run( + ["npm", "install", "--ignore-scripts"], + cwd=str(pkg_dir), + check=True, + capture_output=True, + text=True, + timeout=120, + ) + return pkg_dir + + +@pytest.mark.smoke +@pytest.mark.network +@pytest.mark.parametrize( + "name, deps", REAL_NPM_PACKAGES, ids=[p[0] for p in REAL_NPM_PACKAGES] +) +def test_real_world_npm_packages(name, deps, tmp_path): + """Install a real npm package and scan it.""" + pkg_dir = _create_npm_project(tmp_path, name, deps) + sbom_file = tmp_path / f"{name}_sbom.json" + + result = run_ossprey(pkg_dir, output_file=sbom_file) + + assert "No malware found" in result.stdout + data = assert_sbom_has_components(sbom_file) + + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert name in comp_names, f"Expected '{name}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 7. Real-world GitHub repos +# --------------------------------------------------------------------------- + +GITHUB_REPOS = [ + # (repo_url, pre_scan_cmd, expected_component) + ( + "https://github.com/pallets/click", + None, + # click itself won't appear — it's the root package; check a known dep + "colorama", + ), + ( + "https://github.com/expressjs/express", + ["npm", "install", "--ignore-scripts"], + # express itself won't appear — it's the root package; check a known dep + "body-parser", + ), + ( + "https://github.com/psf/requests", + None, + "requests", + ), +] + + +def _clone_repo(url: str, dest: Path, timeout: int = 120) -> Path: + """Shallow-clone a GitHub repo.""" + subprocess.run( + ["git", "clone", "--depth", "1", url, str(dest)], + check=True, + capture_output=True, + text=True, + timeout=timeout, + ) + return dest + + +@pytest.mark.smoke +@pytest.mark.slow +@pytest.mark.network +@pytest.mark.parametrize( + "repo_url, pre_cmd, expected_component", + GITHUB_REPOS, + ids=[r[0].split("/")[-1] for r in GITHUB_REPOS], +) +def test_real_world_github_repos(repo_url, pre_cmd, expected_component, tmp_path): + """Clone a GitHub repo and scan it end-to-end.""" + repo_name = repo_url.rstrip("/").split("/")[-1] + repo_dir = tmp_path / repo_name + + _clone_repo(repo_url, repo_dir) + + # Run any prerequisite command (e.g. npm install) + if pre_cmd: + subprocess.run( + pre_cmd, + cwd=str(repo_dir), + check=True, + capture_output=True, + text=True, + timeout=180, + ) + + sbom_file = tmp_path / f"{repo_name}_sbom.json" + result = run_ossprey(repo_dir, output_file=sbom_file, timeout=600) + + assert "No malware found" in result.stdout + + data = assert_sbom_has_components(sbom_file) + comp_names = [c.get("name", "").lower() for c in data["components"]] + assert ( + expected_component in comp_names + ), f"Expected '{expected_component}' in components: {comp_names}" + + +# --------------------------------------------------------------------------- +# 8. Combined scan — verify API path if key is available +# --------------------------------------------------------------------------- + + +@pytest.mark.smoke +@pytest.mark.network +def test_api_scan_if_key_available(test_packages_dir, tmp_path): + """If API_KEY is set, run a full API scan on python_simple_math.""" + api_key = os.environ.get("API_KEY") + if not api_key: + pytest.skip("API_KEY not set — skipping real API scan test") + + pkg_dir = test_packages_dir / "python_simple_math" + sbom_file = tmp_path / "api_sbom.json" + + result = run_ossprey(pkg_dir, dry_run=None, output_file=sbom_file) + + assert "No malware found" in result.stdout + assert_sbom_has_components(sbom_file) From 7eba469b9d97ad21d80241d8c1d4cc9379b825e0 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 11:50:09 +0000 Subject: [PATCH 03/12] Add smoke tests to cicd --- .github/workflows/test.yml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d03ca0d..df12432 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -35,4 +35,8 @@ jobs: run: | poetry run python -m ossprey --dry-run-safe --package test/npm_simple_math/ --mode npm poetry run python -m ossprey --dry-run-safe --package test/yarn_simple_math/ --mode yarn - poetry run python -m ossprey --dry-run-safe --package test/python_simple_math/ --mode python-requirements \ No newline at end of file + poetry run python -m ossprey --dry-run-safe --package test/python_simple_math/ --mode python-requirements + + - name: Run smoke tests + run: | + poetry run pytest test/smoke/test_smoke.py \ No newline at end of file From 41b152bc8303ada4d744262229a348d457fb53d3 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 11:50:53 +0000 Subject: [PATCH 04/12] Update pre-push tests --- .githooks/pre-push | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 9b83b9b..1076fc7 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -8,14 +8,8 @@ trap 'exit 1' ERR # Run unit tests poetry run pytest -# Test with test packages locally -poetry run python -m ossprey --dry-run-safe --package test/test_packages/npm_simple_math/ --mode npm -poetry run python -m ossprey --dry-run-safe --package test/test_packages/yarn_simple_math/ --mode yarn -poetry run python -m ossprey --dry-run-safe --package test/test_packages/yarn_massive_math/ --mode yarn -poetry run python -m ossprey --dry-run-safe --package test/test_packages/python_simple_math/ --mode python-requirements -poetry run python -m ossprey --dry-run-safe --package test/test_packages/poetry_simple_math/ --mode poetry -poetry run python -m ossprey --dry-run-safe --package test/test_packages/poetry_broken_simple_math/ - +# Run Smoke tests +poetry run pytest test/smoke/test_smoke.py # Test malicious detection (should fail with exit code 1) if poetry run python -m ossprey --dry-run-malicious --package test/test_packages/npm_simple_math/ --mode npm; then From 56fa8166d461a307e43d501b4fcf21edc56e5be1 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 14:35:42 +0000 Subject: [PATCH 05/12] fix tests --- ossprey/test_mode_fs.py | 4 ++-- ossprey/test_sbom_python.py | 22 +++++++++++++--------- ossprey/test_scan.py | 25 +++++++++++++++---------- ossprey/test_virtualenv.py | 18 +++++++++--------- 4 files changed, 39 insertions(+), 30 deletions(-) diff --git a/ossprey/test_mode_fs.py b/ossprey/test_mode_fs.py index 0940962..282606b 100644 --- a/ossprey/test_mode_fs.py +++ b/ossprey/test_mode_fs.py @@ -21,7 +21,7 @@ def cleanup(): "docker_folder, expected_packages, not_expected_packages, no_of_packages", [ ( - "../test/docker_js_simple_math", + "../test/test_packages/docker_js_simple_math", { "npm": ["lodash", "axios", "@types/ms", "ms"], "github": ["ossprey/example_malicious_javascript"], @@ -32,7 +32,7 @@ def cleanup(): 509, ), ( - "../test/docker_py_simple_math", + "../test/test_packages/docker_py_simple_math", { "pypi": ["fastapi", "uvicorn", "solana", "solders", "pydantic"], "github": ["ossprey/example_malicious_python"], diff --git a/ossprey/test_sbom_python.py b/ossprey/test_sbom_python.py index 6d0f5db..af7c66f 100644 --- a/ossprey/test_sbom_python.py +++ b/ossprey/test_sbom_python.py @@ -4,14 +4,18 @@ import pytest from unittest.mock import patch -from ossprey.sbom_python import create_sbom_from_env, create_sbom_from_requirements, get_cyclonedx_binary +from ossprey.sbom_python import ( + create_sbom_from_env, + create_sbom_from_requirements, + get_cyclonedx_binary, +) from ossprey.virtualenv import VirtualEnv def test_get_sbom(): sbom = create_sbom_from_env() - assert sbom.format == 'OSSBOM' + assert sbom.format == "OSSBOM" def test_get_sbom_from_venv(): @@ -20,16 +24,16 @@ def test_get_sbom_from_venv(): venv.enter() # Install a package - venv.install_package('numpy') + venv.install_package("numpy") requirements_file = venv.create_requirements_file_from_env() # Get the SBOM sbom = create_sbom_from_requirements(requirements_file) - assert sbom.format == 'OSSBOM' + assert sbom.format == "OSSBOM" assert len(sbom.components) == 1 - assert any(map(lambda x: x.name == 'numpy', sbom.components.values())) + assert any(map(lambda x: x.name == "numpy", sbom.components.values())) def test_get_sbom_from_venv_local_package(): @@ -38,16 +42,16 @@ def test_get_sbom_from_venv_local_package(): venv.enter() # Install a package - venv.install_package('test/python_simple_math') - + venv.install_package("test/test_packages/python_simple_math") + requirements_file = venv.create_requirements_file_from_env() # Get the SBOM sbom = create_sbom_from_requirements(requirements_file) - assert sbom.format == 'OSSBOM' + assert sbom.format == "OSSBOM" assert len(sbom.components) == 7 - assert any(map(lambda x: x.name == 'simple_math', sbom.components.values())) + assert any(map(lambda x: x.name == "simple_math", sbom.components.values())) @patch("shutil.which") diff --git a/ossprey/test_scan.py b/ossprey/test_scan.py index d07589c..3f45122 100644 --- a/ossprey/test_scan.py +++ b/ossprey/test_scan.py @@ -6,13 +6,13 @@ @pytest.mark.parametrize("mode", ["python-requirements", "auto"]) def test_scan_py_success(mode: str) -> None: - ret = scan("test/python_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/python_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) assert [comp.name for comp in ret.get_components()] == ["numpy", "requests"] def test_scan_py_success_pipenv() -> None: - ret = scan("test/python_simple_math", mode="pipenv", local_scan=True) + ret = scan("test/test_packages/python_simple_math", mode="pipenv", local_scan=True) assert isinstance(ret, OSSBOM) result = [ @@ -29,7 +29,7 @@ def test_scan_py_success_pipenv() -> None: @pytest.mark.parametrize("mode", ["auto", "poetry"]) def test_scan_poetry_success(mode: str) -> None: - ret = scan("test/poetry_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/poetry_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) result = ["certifi", "charset-normalizer", "idna", "numpy", "requests", "urllib3"] @@ -38,7 +38,7 @@ def test_scan_poetry_success(mode: str) -> None: @pytest.mark.parametrize("mode", ["pipenv"]) def test_scan_poetry_success_pipenv(mode: str) -> None: - ret = scan("test/poetry_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/poetry_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) result = [ @@ -55,14 +55,14 @@ def test_scan_poetry_success_pipenv(mode: str) -> None: @pytest.mark.parametrize("mode", ["npm", "auto"]) def test_scan_npm_success(mode: str) -> None: - ret = scan("test/npm_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/npm_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) assert len(ret.get_components()) == 322 @pytest.mark.parametrize(["mode", "num_components"], [("yarn", 323), ("auto", 323)]) def test_scan_yarn_success(mode: str, num_components: int) -> None: - ret = scan("test/yarn_simple_math", mode=mode, local_scan=True) + ret = scan("test/test_packages/yarn_simple_math", mode=mode, local_scan=True) assert isinstance(ret, OSSBOM) assert len(ret.get_components()) == num_components @@ -70,16 +70,21 @@ def test_scan_yarn_success(mode: str, num_components: int) -> None: def test_scan_failure() -> None: with pytest.raises(Exception) as excinfo: scan( - "test/python_simple_math_no_exist", + "test/test_packages/python_simple_math_no_exist", mode="python-requirements", local_scan=True, ) - assert "Package test/python_simple_math_no_exist does not exist" in str( - excinfo.value + assert ( + "Package test/test_packages/python_simple_math_no_exist does not exist" + in str(excinfo.value) ) def test_scan_invalid_mode() -> None: with pytest.raises(Exception) as excinfo: - scan("test/python_simple_math", mode="invalid-mode", local_scan=True) + scan( + "test/test_packages/python_simple_math", + mode="invalid-mode", + local_scan=True, + ) assert "Invalid scanning method" in str(excinfo.value) diff --git a/ossprey/test_virtualenv.py b/ossprey/test_virtualenv.py index 04145ae..571c797 100644 --- a/ossprey/test_virtualenv.py +++ b/ossprey/test_virtualenv.py @@ -10,7 +10,7 @@ def test_virtual_env_creation() -> None: venv = VirtualEnv() # Check if the virtual environment files exist - assert os.path.isdir(os.path.join(venv.get_venv_dir(), 'bin')) + assert os.path.isdir(os.path.join(venv.get_venv_dir(), "bin")) def test_accessing_virtual_env() -> None: @@ -24,7 +24,7 @@ def test_accessing_virtual_env() -> None: # Make sure we are in a new venv assert len(installed_packages) == 1 - assert installed_packages[0]['name'] == 'pip' + assert installed_packages[0]["name"] == "pip" def test_package_installation() -> None: @@ -32,27 +32,27 @@ def test_package_installation() -> None: venv.enter() # Install a package - venv.install_package('numpy') + venv.install_package("numpy") installed_packages = venv.list_installed_packages() # Make sure we are in a new venv assert len(installed_packages) == 2 - assert any(map(lambda x: x['name'] == 'numpy', installed_packages)) + assert any(map(lambda x: x["name"] == "numpy", installed_packages)) def test_local_requirements_file() -> None: venv = VirtualEnv() venv.enter() - venv.install_package('numpy') + venv.install_package("numpy") requirements_file = venv.create_requirements_file_from_env() # Read the file - with open(requirements_file, 'r') as f: + with open(requirements_file, "r") as f: lines = f.readlines() assert len(lines) == 1 - assert lines[0].startswith('numpy') + assert lines[0].startswith("numpy") def test_local_installation() -> None: @@ -60,10 +60,10 @@ def test_local_installation() -> None: venv.enter() # Install a package - venv.install_package('test/python_simple_math') + venv.install_package("test/test_packages/python_simple_math") installed_packages = venv.list_installed_packages() # Make sure we are in a new venv assert len(installed_packages) == 8 - assert any(map(lambda x: x['name'] == 'numpy', installed_packages)) + assert any(map(lambda x: x["name"] == "numpy", installed_packages)) From 8049efa766c4b96bf2825df472308a82016477b4 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 14:51:04 +0000 Subject: [PATCH 06/12] update runtime test paths --- .github/workflows/test.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index df12432..c85a1d5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,9 +33,9 @@ jobs: - name: Run Runtime Tests run: | - poetry run python -m ossprey --dry-run-safe --package test/npm_simple_math/ --mode npm - poetry run python -m ossprey --dry-run-safe --package test/yarn_simple_math/ --mode yarn - poetry run python -m ossprey --dry-run-safe --package test/python_simple_math/ --mode python-requirements + poetry run python -m ossprey --dry-run-safe --package test/test_packages/npm_simple_math/ --mode npm + poetry run python -m ossprey --dry-run-safe --package test/test_packages/yarn_simple_math/ --mode yarn + poetry run python -m ossprey --dry-run-safe --package test/test_packages/python_simple_math/ --mode python-requirements - name: Run smoke tests run: | From 755353f47f75190fbdf660b008fb37289f556fd3 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 14:56:43 +0000 Subject: [PATCH 07/12] fix venv issue --- ossprey/sbom_python.py | 12 ++++-------- ossprey/virtualenv.py | 8 ++++++++ 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/ossprey/sbom_python.py b/ossprey/sbom_python.py index b40e16e..a85965c 100644 --- a/ossprey/sbom_python.py +++ b/ossprey/sbom_python.py @@ -136,14 +136,10 @@ def update_sbom_from_poetry(ossbom: OSSBOM, package_dir: str) -> OSSBOM: def update_sbom_from_virtualenv(ossbom: OSSBOM, package_name: str) -> OSSBOM: - venv = VirtualEnv() - venv.enter() + with VirtualEnv() as venv: + venv.install_package(package_name) + requirements_file = venv.create_requirements_file_from_env() - venv.install_package(package_name) - requirements_file = venv.create_requirements_file_from_env() - - ossbom = update_sbom_from_requirements(ossbom, requirements_file) - - venv.exit() + ossbom = update_sbom_from_requirements(ossbom, requirements_file) return ossbom diff --git a/ossprey/virtualenv.py b/ossprey/virtualenv.py index bfc4512..0741a1f 100644 --- a/ossprey/virtualenv.py +++ b/ossprey/virtualenv.py @@ -125,6 +125,14 @@ def create_requirements_file_from_env(self) -> str: return requirements_file.name + def __enter__(self): + self.enter() + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + self.exit() + return False + def enter(self) -> None: """ Replaces sys.path with paths from the specified virtual environment. From 3b08b90b0f35a923bdfb7f95bbfd5ef07e8e0c7d Mon Sep 17 00:00:00 2001 From: David <31293924+dreadn0ught@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:09:51 +0000 Subject: [PATCH 08/12] Update .github/workflows/test.yml Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c85a1d5..3cbc5ab 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -39,4 +39,4 @@ jobs: - name: Run smoke tests run: | - poetry run pytest test/smoke/test_smoke.py \ No newline at end of file + poetry run pytest test/smoke/test_smoke.py -m "not network and not slow" \ No newline at end of file From 1963c430db9b0a0ad69aed4b4ba2e4c4b9f64eb6 Mon Sep 17 00:00:00 2001 From: David <31293924+dreadn0ught@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:10:39 +0000 Subject: [PATCH 09/12] Update ossprey/sbom_python.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- ossprey/sbom_python.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/ossprey/sbom_python.py b/ossprey/sbom_python.py index a85965c..f5cc1e9 100644 --- a/ossprey/sbom_python.py +++ b/ossprey/sbom_python.py @@ -134,12 +134,11 @@ def update_sbom_from_poetry(ossbom: OSSBOM, package_dir: str) -> OSSBOM: return ossbom -def update_sbom_from_virtualenv(ossbom: OSSBOM, package_name: str) -> OSSBOM: - - with VirtualEnv() as venv: + try: venv.install_package(package_name) requirements_file = venv.create_requirements_file_from_env() ossbom = update_sbom_from_requirements(ossbom, requirements_file) - + finally: + venv.exit() return ossbom From 78239e9251e891f438ae3db65d11a39068571fa9 Mon Sep 17 00:00:00 2001 From: David <31293924+dreadn0ught@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:10:48 +0000 Subject: [PATCH 10/12] Update test/smoke/test_smoke.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- test/smoke/test_smoke.py | 1 - 1 file changed, 1 deletion(-) diff --git a/test/smoke/test_smoke.py b/test/smoke/test_smoke.py index 8471ea8..1ad9903 100644 --- a/test/smoke/test_smoke.py +++ b/test/smoke/test_smoke.py @@ -17,7 +17,6 @@ import json import os -import shutil import subprocess import sys import textwrap From 488beb8f669d6432ac6ed0b4de7f1d1d7f036d9b Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 10 Feb 2026 15:55:59 +0000 Subject: [PATCH 11/12] Reduce pre-push hook to fast, non-network smoke tests (#47) * Initial plan * Update pre-push hook to run only fast, non-network smoke tests Co-authored-by: dreadn0ught <31293924+dreadn0ught@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: dreadn0ught <31293924+dreadn0ught@users.noreply.github.com> --- .githooks/pre-push | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.githooks/pre-push b/.githooks/pre-push index 1076fc7..24b619c 100755 --- a/.githooks/pre-push +++ b/.githooks/pre-push @@ -8,8 +8,10 @@ trap 'exit 1' ERR # Run unit tests poetry run pytest -# Run Smoke tests -poetry run pytest test/smoke/test_smoke.py +# Run Smoke tests (fast, non-network subset) +# For the full smoke suite including network/slow tests, use: +# poetry run pytest test/smoke/test_smoke.py +poetry run pytest test/smoke/test_smoke.py -m "smoke and not network and not slow" # Test malicious detection (should fail with exit code 1) if poetry run python -m ossprey --dry-run-malicious --package test/test_packages/npm_simple_math/ --mode npm; then From ef3e2ed889a887fcff52cbf201d8a0a010dc6754 Mon Sep 17 00:00:00 2001 From: dreadn0ught Date: Tue, 10 Feb 2026 16:38:12 +0000 Subject: [PATCH 12/12] bug fix --- ossprey/sbom_python.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ossprey/sbom_python.py b/ossprey/sbom_python.py index f5cc1e9..de4d734 100644 --- a/ossprey/sbom_python.py +++ b/ossprey/sbom_python.py @@ -134,6 +134,8 @@ def update_sbom_from_poetry(ossbom: OSSBOM, package_dir: str) -> OSSBOM: return ossbom +def update_sbom_from_virtualenv(ossbom: OSSBOM, package_name: str) -> OSSBOM: + venv = VirtualEnv() try: venv.install_package(package_name) requirements_file = venv.create_requirements_file_from_env()