feat: multi-ecosystem support (12 GHSA ecosystems) + src/ refactor #46
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Test Package Checker | |
| on: | |
| push: | |
| branches: [main, develop] | |
| pull_request: | |
| branches: [main, develop] | |
| workflow_dispatch: | |
| jobs: | |
| test-fixtures: | |
| name: Run Tests on Fixtures | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Make script executable | |
| run: chmod +x script.sh | |
| - name: Run all tests | |
| run: | | |
| set +e # Don't exit on first error | |
| FAILED_TESTS=0 | |
| PASSED_TESTS=0 | |
| echo "==========================================" | |
| echo "Running Package Checker Tests" | |
| echo "==========================================" | |
| echo "" | |
| # Test npm project | |
| echo "📦 Testing npm project..." | |
| cd test-fixtures/npm-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ npm project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ npm project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test yarn project | |
| echo "📦 Testing yarn project..." | |
| cd test-fixtures/yarn-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ yarn project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ yarn project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test yarn berry project | |
| echo "📦 Testing yarn berry project..." | |
| cd test-fixtures/yarn-berry-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ yarn berry project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ yarn berry project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test pnpm project | |
| echo "📦 Testing pnpm project..." | |
| cd test-fixtures/pnpm-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ pnpm project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ pnpm project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test bun project | |
| echo "📦 Testing bun project..." | |
| cd test-fixtures/bun-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ bun project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ bun project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test deno project | |
| echo "📦 Testing deno project..." | |
| cd test-fixtures/deno-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ deno project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ deno project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test npm-shrinkwrap project | |
| echo "📦 Testing npm-shrinkwrap project..." | |
| cd test-fixtures/npm-shrinkwrap-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ npm-shrinkwrap project: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ npm-shrinkwrap project: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test rust project (Cargo.lock) against the shared multi-ecosystem | |
| # test feed: exactly one vulnerable package (time@0.1.45), serde | |
| # must NOT be flagged (its test entry is a version that doesn't match). | |
| echo "📦 Testing rust project..." | |
| cd test-fixtures/rust-project | |
| RUST_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl) | |
| RUST_EXIT=$? | |
| if [ "$RUST_EXIT" -eq 1 ] && echo "$RUST_OUTPUT" | grep -q "time@0.1.45" && ! echo "$RUST_OUTPUT" | grep -q "serde"; then | |
| echo "✅ rust project: Found time@0.1.45 as expected, serde not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ rust project: Expected time@0.1.45 vulnerable and serde unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test rust project against the real GHSA cargo feed, if committed | |
| # (data/ghsa-cargo.purl is generated by the feed-update workflow). | |
| if [ -f data/ghsa-cargo.purl ]; then | |
| echo "📦 Testing rust project against real GHSA cargo feed..." | |
| cd test-fixtures/rust-project | |
| ../../script.sh --source ../../data/ghsa-cargo.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ rust project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ rust project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test go project (go.sum/go.mod) against the shared multi-ecosystem | |
| # test feed: exactly one vulnerable package (golang.org/x/text@0.3.5), | |
| # BurntSushi/toml must NOT be flagged (its test range doesn't match). | |
| echo "📦 Testing go project..." | |
| cd test-fixtures/go-project | |
| GO_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl) | |
| GO_EXIT=$? | |
| if [ "$GO_EXIT" -eq 1 ] && echo "$GO_OUTPUT" | grep -q "golang.org/x/text@0.3.5" && ! echo "$GO_OUTPUT" | grep -q "toml"; then | |
| echo "✅ go project: Found golang.org/x/text@0.3.5 as expected, toml not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ go project: Expected golang.org/x/text@0.3.5 vulnerable and toml unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test go project against the real GHSA golang feed, if committed | |
| # (data/ghsa-golang.purl is generated by the feed-update workflow). | |
| if [ -f data/ghsa-golang.purl ]; then | |
| echo "📦 Testing go project against real GHSA golang feed..." | |
| cd test-fixtures/go-project | |
| ../../script.sh --source ../../data/ghsa-golang.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ go project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ go project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test python project (requirements.txt) against the shared | |
| # multi-ecosystem test feed: exactly one vulnerable package | |
| # (django@3.2, matched only after PEP 503 lower-casing of "Django"); | |
| # requests/certifi/uvicorn must NOT be flagged (safe versions, extras | |
| # stripped), and the -r include and >= line must be skipped. | |
| echo "📦 Testing python project (requirements.txt)..." | |
| cd test-fixtures/python-project | |
| PY_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl) | |
| PY_EXIT=$? | |
| if [ "$PY_EXIT" -eq 1 ] && echo "$PY_OUTPUT" | grep -q "django@3.2" && ! echo "$PY_OUTPUT" | grep -qE "requests|certifi|uvicorn|boto3"; then | |
| echo "✅ python project: Found django@3.2 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ python project: Expected django@3.2 vulnerable and requests/certifi/uvicorn unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test poetry project (poetry.lock via the shared TOML parser) against | |
| # the shared multi-ecosystem test feed: exactly one vulnerable package | |
| # (pillow@8.0.0, matched after lower-casing "Pillow"); requests/certifi | |
| # must NOT be flagged; the [package.dependencies] subtable keyed | |
| # name/version must NOT leak a bogus package (subtable hardening). | |
| echo "📦 Testing poetry project (poetry.lock)..." | |
| cd test-fixtures/poetry-project | |
| PO_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl) | |
| PO_EXIT=$? | |
| if [ "$PO_EXIT" -eq 1 ] && echo "$PO_OUTPUT" | grep -q "pillow@8.0.0" && ! echo "$PO_OUTPUT" | grep -qE "requests|certifi|@>=1.0|@>=2.0"; then | |
| echo "✅ poetry project: Found pillow@8.0.0 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ poetry project: Expected pillow@8.0.0 vulnerable and no bogus/safe package flagged" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test pipenv project (Pipfile.lock JSON) against the shared feed: | |
| # exactly one vulnerable package (django-rest-framework@3.0.0, matched | |
| # only after collapsing the underscores in "django_rest_framework"); | |
| # certifi/requests must NOT be flagged and the VCS entry without a | |
| # version must be skipped. | |
| echo "📦 Testing pipenv project (Pipfile.lock)..." | |
| cd test-fixtures/pipenv-project | |
| PL_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl) | |
| PL_EXIT=$? | |
| if [ "$PL_EXIT" -eq 1 ] && echo "$PL_OUTPUT" | grep -q "django-rest-framework@3.0.0" && ! echo "$PL_OUTPUT" | grep -qE "certifi|internal-tool|@2.32.3"; then | |
| echo "✅ pipenv project: Found django-rest-framework@3.0.0 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ pipenv project: Expected django-rest-framework@3.0.0 vulnerable and certifi/requests/VCS unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test python + poetry projects against the real GHSA pypi feed, if | |
| # committed (data/ghsa-pypi.purl is generated by the feed-update | |
| # workflow). django@3.2 and pillow@8.0.0 are covered by real advisories. | |
| if [ -f data/ghsa-pypi.purl ]; then | |
| echo "📦 Testing python project against real GHSA pypi feed..." | |
| cd test-fixtures/python-project | |
| ../../script.sh --source ../../data/ghsa-pypi.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ python project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ python project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| echo "📦 Testing poetry project against real GHSA pypi feed..." | |
| cd test-fixtures/poetry-project | |
| ../../script.sh --source ../../data/ghsa-pypi.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ poetry project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ poetry project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test ruby project (Gemfile.lock) against the shared multi-ecosystem | |
| # test feed: exactly one vulnerable package (rack@2.2.3); rake, json | |
| # and the platform-suffixed nokogiri must NOT be flagged (safe | |
| # versions), and the GIT/PATH-sourced specs (rspec-support, | |
| # local_tool) must be skipped entirely (no rubygems version to check). | |
| echo "📦 Testing ruby project (Gemfile.lock)..." | |
| cd test-fixtures/ruby-project | |
| RB_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types ruby) | |
| RB_EXIT=$? | |
| if [ "$RB_EXIT" -eq 1 ] && echo "$RB_OUTPUT" | grep -q "rack@2.2.3" && ! echo "$RB_OUTPUT" | grep -qE "rake@|json@|nokogiri@|rspec-support|local_tool"; then | |
| echo "✅ ruby project: Found rack@2.2.3 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ ruby project: Expected rack@2.2.3 vulnerable and rake/json/nokogiri/GIT+PATH specs unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test ruby project against the real GHSA gem feed, if committed | |
| # (data/ghsa-gem.purl is generated by the feed-update workflow). | |
| if [ -f data/ghsa-gem.purl ]; then | |
| echo "📦 Testing ruby project against real GHSA gem feed..." | |
| cd test-fixtures/ruby-project | |
| ../../script.sh --source ../../data/ghsa-gem.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ ruby project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ ruby project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test php project (composer.lock: "packages" + "packages-dev") | |
| # against the shared multi-ecosystem test feed: exactly one | |
| # vulnerable package (guzzlehttp/guzzle@7.4.0); monolog/monolog | |
| # (safe version), psr/log (v-prefixed "v3.0.0" in the lockfile, | |
| # proving the leading-v strip; not in any feed) and the | |
| # packages-dev entry phpunit/phpunit (proving the dev section is | |
| # parsed) must NOT be flagged. | |
| echo "📦 Testing php project (composer.lock)..." | |
| cd test-fixtures/php-project | |
| PHP_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types php) | |
| PHP_EXIT=$? | |
| if [ "$PHP_EXIT" -eq 1 ] && echo "$PHP_OUTPUT" | grep -q "guzzlehttp/guzzle@7.4.0" && ! echo "$PHP_OUTPUT" | grep -qE "monolog/monolog|psr/log|phpunit/phpunit"; then | |
| echo "✅ php project: Found guzzlehttp/guzzle@7.4.0 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ php project: Expected guzzlehttp/guzzle@7.4.0 vulnerable and monolog/psr/phpunit unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test php project against the real GHSA composer feed, if | |
| # committed (data/ghsa-composer.purl is generated by the | |
| # feed-update workflow). | |
| if [ -f data/ghsa-composer.purl ]; then | |
| echo "📦 Testing php project against real GHSA composer feed..." | |
| cd test-fixtures/php-project | |
| ../../script.sh --source ../../data/ghsa-composer.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ php project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ php project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test maven project (gradle.lockfile + pom.xml coexisting in ONE | |
| # directory — proving multi-file dispatch) against the shared | |
| # multi-ecosystem test feed: exactly one vulnerable package, | |
| # org.apache.logging.log4j:log4j-core@2.14.1 (log4shell, matched by the | |
| # >=2.0 <2.15.0 range; note the groupId:artifactId colon in the name). | |
| # guava (33.2.0-jre, not < 20.0) and slf4j-api from the gradle.lockfile, | |
| # plus commons-lang3 (3.14.0) from the pom, are safe; jackson-databind's | |
| # ${property} version in the pom is skipped (no manifest-grade property | |
| # resolution). | |
| echo "📦 Testing maven project (gradle.lockfile + pom.xml)..." | |
| cd test-fixtures/maven-project | |
| MVN_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types maven) | |
| MVN_EXIT=$? | |
| if [ "$MVN_EXIT" -eq 1 ] && echo "$MVN_OUTPUT" | grep -q "org.apache.logging.log4j:log4j-core@2.14.1" && ! echo "$MVN_OUTPUT" | grep -qE "guava|slf4j-api|commons-lang3|jackson-databind"; then | |
| echo "✅ maven project: Found log4j-core@2.14.1 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ maven project: Expected log4j-core@2.14.1 vulnerable and guava/slf4j/commons-lang3/jackson unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test maven project against the real GHSA maven feed, if committed | |
| # (data/ghsa-maven.purl is generated by the feed-update workflow). | |
| # log4shell (GHSA-jfh8-c2jp-5v3q, log4j-core >=2.13.0 <2.15.0) is | |
| # definitely present, so 2.14.1 must be flagged. | |
| if [ -f data/ghsa-maven.purl ]; then | |
| echo "📦 Testing maven project against real GHSA maven feed..." | |
| cd test-fixtures/maven-project | |
| ../../script.sh --source ../../data/ghsa-maven.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ maven project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ maven project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test nuget project (packages.lock.json: two frameworks, net8.0 + | |
| # net9.0, proving cross-framework dedupe) against the shared | |
| # multi-ecosystem test feed: exactly one vulnerable package, | |
| # newtonsoft.json@12.0.2 (name lowercased per NuGet canon, and | |
| # reported only ONCE despite appearing in both framework blocks). | |
| # serilog@3.1.1 is safe (not < 2.0.0), the Transitive entry | |
| # serilog.sinks.console is unaffected, and the Project-type entry | |
| # contoso.shared carries no resolved version and must be skipped | |
| # entirely (no manifest-grade csproj resolution - tier 2, skipped). | |
| echo "📦 Testing nuget project (packages.lock.json)..." | |
| cd test-fixtures/nuget-project | |
| NUGET_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types nuget) | |
| NUGET_EXIT=$? | |
| if [ "$NUGET_EXIT" -eq 1 ] && echo "$NUGET_OUTPUT" | grep -q "newtonsoft.json@12.0.2" && echo "$NUGET_OUTPUT" | grep -q "Found 1 vulnerable package(s) in 1 location(s)" && ! echo "$NUGET_OUTPUT" | grep -qE "serilog@3.1.1|serilog.sinks.console|contoso.shared"; then | |
| echo "✅ nuget project: Found newtonsoft.json@12.0.2 exactly once as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ nuget project: Expected newtonsoft.json@12.0.2 vulnerable exactly once and serilog/transitive/project entries unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test nuget project against the real GHSA nuget feed, if committed | |
| # (data/ghsa-nuget.purl is generated by the feed-update workflow). | |
| # GHSA-5crp-9r3c-p9vr / GHSA-8rfx-6mr3-5jh3 (newtonsoft.json | |
| # <13.0.1) are definitely present, so 12.0.2 must be flagged. | |
| if [ -f data/ghsa-nuget.purl ]; then | |
| echo "📦 Testing nuget project against real GHSA nuget feed..." | |
| cd test-fixtures/nuget-project | |
| ../../script.sh --source ../../data/ghsa-nuget.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ nuget project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ nuget project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test dart project (pubspec.lock) against the shared multi-ecosystem | |
| # test feed: exactly one vulnerable package, dio@4.0.6 (matches the | |
| # <5.0.0 range). http@1.2.0 is safe (not < 0.13.3), the git-sourced | |
| # internal_widgets entry is skipped (no pub.dev release to check), | |
| # and the sdk-sourced flutter pseudo-package carries no hosted | |
| # version at all (source: sdk, not source: hosted, skipped entirely). | |
| echo "📦 Testing dart project (pubspec.lock)..." | |
| cd test-fixtures/dart-project | |
| DART_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types dart) | |
| DART_EXIT=$? | |
| if [ "$DART_EXIT" -eq 1 ] && echo "$DART_OUTPUT" | grep -q "dio@4.0.6" && ! echo "$DART_OUTPUT" | grep -qE "http@1.2.0|internal_widgets|flutter@0.0.0"; then | |
| echo "✅ dart project: Found dio@4.0.6 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ dart project: Expected dio@4.0.6 vulnerable and http/git/sdk entries unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test dart project against the real GHSA pub feed, if committed | |
| # (data/ghsa-pub.purl is generated by the feed-update workflow). | |
| # GHSA-jwpw-q68h-r678 / GHSA-9324-jv53-9cc8 (dio <5.0.0) are | |
| # definitely present, so 4.0.6 must be flagged. | |
| if [ -f data/ghsa-pub.purl ]; then | |
| echo "📦 Testing dart project against real GHSA pub feed..." | |
| cd test-fixtures/dart-project | |
| ../../script.sh --source ../../data/ghsa-pub.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ dart project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ dart project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test elixir project (mix.lock) against the shared multi-ecosystem | |
| # test feed: exactly one vulnerable package, sweet_xml@0.6.6 | |
| # (matches the <0.7.0 range). paginator@1.2.0 is safe (not < 1.0.0), | |
| # the git-sourced internal_auth entry is skipped (no hex.pm release | |
| # to check), and jason/telemetry are unlisted (safe by absence). | |
| echo "📦 Testing elixir project (mix.lock)..." | |
| cd test-fixtures/elixir-project | |
| HEX_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types hex) | |
| HEX_EXIT=$? | |
| if [ "$HEX_EXIT" -eq 1 ] && echo "$HEX_OUTPUT" | grep -q "sweet_xml@0.6.6" && ! echo "$HEX_OUTPUT" | grep -qE "paginator@1.2.0|internal_auth|jason@1.4.1|telemetry@1.2.1"; then | |
| echo "✅ elixir project: Found sweet_xml@0.6.6 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ elixir project: Expected sweet_xml@0.6.6 vulnerable and paginator/git/unlisted entries unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test elixir project against the real GHSA hex feed, if committed | |
| # (data/ghsa-hex.purl is generated by the feed-update workflow). | |
| # GHSA-qpmc-wprv-x746 (sweet_xml <0.7.0) is definitely present, so | |
| # 0.6.6 must be flagged. | |
| if [ -f data/ghsa-hex.purl ]; then | |
| echo "📦 Testing elixir project against real GHSA hex feed..." | |
| cd test-fixtures/elixir-project | |
| ../../script.sh --source ../../data/ghsa-hex.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ elixir project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ elixir project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test swift project (Package.resolved) against the shared | |
| # multi-ecosystem test feed: exactly one vulnerable package, | |
| # github.com/apple/swift-nio@2.10.0 (matches the <2.30.0 range). | |
| # The pin is deliberately mixed-case | |
| # ("https://GitHub.com/Apple/Swift-NIO.git") to prove URL | |
| # canonicalization (scheme-strip + .git-strip + lowercase); it | |
| # must still be reported under the lowercased name. | |
| # github.com/apple/swift-log@1.14.0 is safe (not < 1.0.0), and the | |
| # branch-pinned swift-atomics entry is skipped (no released | |
| # version to check). | |
| echo "📦 Testing swift project (Package.resolved)..." | |
| cd test-fixtures/swift-project | |
| SWIFT_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types swift) | |
| SWIFT_EXIT=$? | |
| if [ "$SWIFT_EXIT" -eq 1 ] && echo "$SWIFT_OUTPUT" | grep -q "github.com/apple/swift-nio@2.10.0" && ! echo "$SWIFT_OUTPUT" | grep -qE "swift-log@1.14.0|swift-atomics"; then | |
| echo "✅ swift project: Found github.com/apple/swift-nio@2.10.0 as expected, others not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ swift project: Expected github.com/apple/swift-nio@2.10.0 vulnerable (lowercased) and swift-log/swift-atomics unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test swift project against the real GHSA swift feed, if | |
| # committed (data/ghsa-swift.purl is generated by the | |
| # feed-update workflow). GHSA-mgc4-wqv7-4pxm | |
| # (github.com/apple/swift-nio >=2.0.0 <2.13.1) is definitely | |
| # present, so 2.10.0 must be flagged. Guard on `[ -s ... ]` | |
| # instead of `[ -f ... ]` if this feed can ever be committed | |
| # empty (0 lines) - an empty file would otherwise pass `-f` and | |
| # the smoke test would then legitimately fail to find anything. | |
| if [ -f data/ghsa-swift.purl ]; then | |
| echo "📦 Testing swift project against real GHSA swift feed..." | |
| cd test-fixtures/swift-project | |
| ../../script.sh --source ../../data/ghsa-swift.purl | |
| if [ $? -eq 1 ]; then | |
| echo "✅ swift project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ swift project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test GitHub Actions project (.github/workflows/*.yml) — the ONLY | |
| # ecosystem discovered by PATH, not a lockfile basename. Scanned | |
| # against the shared multi-ecosystem test feed. Exactly one vulnerable | |
| # action: tj-actions/changed-files@v35 (leading `v` stripped to 35, | |
| # matches the <46.0.1 range, GHSA-mrrh-fwg8-r2c3). It must be reported | |
| # with the LOWERCASED owner/repo name under the [githubactions] | |
| # prefix. actions/checkout@v4 is a safe decoy (feed range <1.0.0, and | |
| # 4 is not < 1); the quoted setup-python, the SHA-pinned cache | |
| # (comment ignored), the owner/repo/subpath reusable-workflow call, | |
| # the ./local step and the docker:// step are all safe or skipped. | |
| # --lockfile-types actions isolates workflow scanning, and the fixture | |
| # `cd` keeps the repo's own .github/workflows out of scope. | |
| echo "📦 Testing GitHub Actions project (workflow uses:)..." | |
| cd test-fixtures/actions-project | |
| ACTIONS_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --lockfile-types actions) | |
| ACTIONS_EXIT=$? | |
| if [ "$ACTIONS_EXIT" -eq 1 ] && echo "$ACTIONS_OUTPUT" | grep -q "\[githubactions\] tj-actions/changed-files@35" && ! echo "$ACTIONS_OUTPUT" | grep -qE "checkout|setup-node|setup-python|upload-artifact|cache|shared-workflows|alpine|notify"; then | |
| echo "✅ actions project: Found tj-actions/changed-files@35 as expected ([githubactions], lowercased); safe/skipped steps not flagged" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ actions project: Expected tj-actions/changed-files@35 vulnerable and checkout/setup/subpath/local/docker unaffected" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test actions project against the real GHSA githubactions feed, if | |
| # committed (data/ghsa-githubactions.purl is generated by the | |
| # feed-update workflow). tj-actions/changed-files GHSA-mrrh-fwg8-r2c3 | |
| # (<46.0.1) and GHSA-mcph-m25j-8j63 (<41) both cover v35, so it must | |
| # be flagged. Guard on `[ -s ... ]` so a committed-empty feed (0 | |
| # lines) does not pass and then fail to find anything. | |
| if [ -s data/ghsa-githubactions.purl ]; then | |
| echo "📦 Testing actions project against real GHSA githubactions feed..." | |
| cd test-fixtures/actions-project | |
| ../../script.sh --source ../../data/ghsa-githubactions.purl --lockfile-types actions | |
| if [ $? -eq 1 ]; then | |
| echo "✅ actions project (real feed): Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ actions project (real feed): Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| fi | |
| # Test monorepo | |
| echo "📦 Testing monorepo..." | |
| cd test-fixtures/monorepo | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ monorepo: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ monorepo: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test safe project (should NOT find vulnerabilities) | |
| echo "📦 Testing safe project (should pass)..." | |
| cd test-fixtures/safe-project | |
| ../../script.sh --source ../test-vulnerabilities.json | |
| if [ $? -eq 0 ]; then | |
| echo "✅ safe project: No vulnerabilities found as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ safe project: Should NOT have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Polyglot project: ONE directory holding an npm lockfile, a Python | |
| # requirements.txt and a Go go.sum. A single scan against the shared | |
| # multi-ecosystem feed must flag all three (lodash@4.17.20, | |
| # django@3.2, golang.org/x/text@0.3.5) each with its ecosystem prefix, | |
| # and the JSON export must carry all three distinct "ecosystem" values. | |
| echo "📦 Testing polyglot project (npm + pypi + golang in one scan)..." | |
| cd test-fixtures/polyglot-project | |
| POLY_OUTPUT=$(../../script.sh --source ../test-vulnerabilities-multi.purl --export-json poly.json) | |
| POLY_EXIT=$? | |
| POLY_JSON=$(cat poly.json 2>/dev/null) | |
| rm -f poly.json | |
| if [ "$POLY_EXIT" -eq 1 ] \ | |
| && echo "$POLY_OUTPUT" | grep -q "lodash@4.17.20" \ | |
| && echo "$POLY_OUTPUT" | grep -q "django@3.2" \ | |
| && echo "$POLY_OUTPUT" | grep -q "golang.org/x/text@0.3.5" \ | |
| && echo "$POLY_JSON" | grep -q '"ecosystem": "npm"' \ | |
| && echo "$POLY_JSON" | grep -q '"ecosystem": "pypi"' \ | |
| && echo "$POLY_JSON" | grep -q '"ecosystem": "golang"'; then | |
| echo "✅ polyglot project: npm+pypi+golang all detected and exported" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ polyglot project: expected npm+pypi+golang all detected and in JSON export" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test with CSV source | |
| echo "📦 Testing with CSV source..." | |
| cd test-fixtures/npm-project | |
| ../../script.sh --source ../test-vulnerabilities.csv | |
| if [ $? -eq 1 ]; then | |
| echo "✅ CSV source: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ CSV source: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd ../.. | |
| echo "" | |
| # Test with config file | |
| echo "📦 Testing with config file..." | |
| cd test-fixtures | |
| ../script.sh --config .package-checker.config.json | |
| if [ $? -eq 1 ]; then | |
| echo "✅ Config file: Found vulnerabilities as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ Config file: Should have found vulnerabilities" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| cd .. | |
| echo "" | |
| # Test metadata collision (correct advisory displayed when multiple advisories affect same package) | |
| echo "📦 Testing metadata collision..." | |
| test-fixtures/metadata-collision-project/test-metadata-collision.sh | |
| if [ $? -eq 0 ]; then | |
| echo "✅ metadata collision: Correct advisory metadata as expected" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ metadata collision: Wrong advisory metadata displayed" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| echo "" | |
| # Comprehensive bug fix tests (multi-vuln, build metadata, empty range, pre-release, workspace) | |
| echo "📦 Testing all bug fixes..." | |
| test-fixtures/test-all-bugs.sh | |
| if [ $? -eq 0 ]; then | |
| echo "✅ bug fixes: All bug fix tests passed" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ bug fixes: Some bug fix tests failed" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| echo "" | |
| # Version comparator harness (semver-2 + Go pseudo-versions + npm dispatch fallback) | |
| echo "📦 Testing version comparators..." | |
| bash test-fixtures/test-version-compare.sh | |
| if [ $? -eq 0 ]; then | |
| echo "✅ version comparators: All comparator rows passed" | |
| PASSED_TESTS=$((PASSED_TESTS + 1)) | |
| else | |
| echo "❌ version comparators: Some comparator rows failed" | |
| FAILED_TESTS=$((FAILED_TESTS + 1)) | |
| fi | |
| echo "" | |
| # Summary | |
| echo "==========================================" | |
| echo "Test Summary" | |
| echo "==========================================" | |
| echo "✅ Passed: $PASSED_TESTS" | |
| echo "❌ Failed: $FAILED_TESTS" | |
| echo "==========================================" | |
| if [ $FAILED_TESTS -gt 0 ]; then | |
| echo "Some tests failed!" | |
| exit 1 | |
| else | |
| echo "All tests passed!" | |
| exit 0 | |
| fi | |
| verify-build: | |
| name: Verify script.sh matches src/ | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@v4 | |
| - name: Rebuild from src | |
| run: bash build.sh | |
| - name: Check for drift | |
| run: | | |
| if ! git diff --exit-code script.sh; then | |
| echo "" | |
| echo "ERROR: script.sh does not match src/." | |
| echo "Someone edited script.sh directly, or forgot to run ./build.sh after editing src/." | |
| echo "Fix: run ./build.sh and commit the result together with your src/ changes." | |
| exit 1 | |
| fi |