diff --git a/guarddog_nexus/core/nexus.py b/guarddog_nexus/core/nexus.py index 0934ebd..5e48447 100644 --- a/guarddog_nexus/core/nexus.py +++ b/guarddog_nexus/core/nexus.py @@ -63,16 +63,27 @@ def extract_go_info(asset_path: str) -> tuple[str, str] | None: def extract_npm_info(asset_path: str) -> tuple[str, str] | None: """Extract package name and version from an npm proxy asset path. - Path format: packages/react/-/react-18.2.0.tgz + Path format: + packages/react/-/react-18.2.0.tgz + packages/@angular/core/-/core-18.0.0.tgz (scoped) """ parts = asset_path.strip("/").split("/") if len(parts) < 4 or parts[0] != PKG_PATH_PREFIX: return None - name = parts[1] - # Last segment: -.tgz + + # Scoped package: @scope/name + if parts[1].startswith("@"): + if len(parts) < 5: + return None + name = f"{parts[1]}/{parts[2]}" + short_name = parts[2] + else: + name = parts[1] + short_name = name + last = parts[-1] - if last.startswith(name + "-"): - raw = last[len(name) + 1 :] + if last.startswith(short_name + "-"): + raw = last[len(short_name) + 1 :] for ext in (".tgz", ".tar.gz"): if raw.endswith(ext): return name, raw[: -len(ext)] diff --git a/tests/e2e/test_webhook_flow.py b/tests/e2e/test_webhook_flow.py index 4f6a5f5..135849d 100644 --- a/tests/e2e/test_webhook_flow.py +++ b/tests/e2e/test_webhook_flow.py @@ -103,6 +103,44 @@ class TestWebhookToScanFlow: data = resp.json() assert data["status"] == "accepted" + @pytest.mark.asyncio + async def test_e2e_webhook_accepts_scoped_npm_asset(self, e2e_client, e2e_db_session): + """Verify that scoped npm (@scope/name) assets are accepted.""" + payload = { + "action": "UPDATED", + "repositoryName": "npm-proxy", + "initiator": "e2e-test", + "asset": { + "format": "npm", + "name": "/packages/@angular/core/-/core-18.0.0.tgz", + "downloadUrl": "http://nexus:8081/repository/npm-proxy/@angular/core/-/core-18.0.0.tgz", + }, + } + + async def mock_harvest(*args, **kwargs): + from guarddog_nexus.db.models import Scan, ScanStatus + + scan = Scan( + package_name="@angular/core", + package_version="18.0.0", + ecosystem="npm", + repository="npm-proxy", + nexus_asset_url=args[0], + status=ScanStatus.COMPLETED.value, + total_findings=0, + flagged=False, + ) + e2e_db_session.add(scan) + await e2e_db_session.commit() + await e2e_db_session.refresh(scan) + return scan + + with patch("guarddog_nexus.routes.webhooks._scan_in_background", mock_harvest): + resp = await e2e_client.post("/webhooks/nexus", json=payload) + + assert resp.status_code == 200 + assert resp.json()["status"] == "accepted" + class TestWebhookSignatureValidation: """E2E tests for webhook signature validation.""" diff --git a/tests/test_nexus.py b/tests/test_nexus.py index 47bff2a..ae18d5a 100644 --- a/tests/test_nexus.py +++ b/tests/test_nexus.py @@ -56,8 +56,16 @@ class TestNpmExtractor: ) def test_scoped_package(self): - # Note: scoped packages have a different path in Nexus - assert extract_npm_info("packages/@scope/name/-/name-1.0.0.tgz") is None + assert extract_npm_info("packages/@scope/name/-/name-1.0.0.tgz") == ( + "@scope/name", + "1.0.0", + ) + + def test_scoped_angular_core(self): + assert extract_npm_info("packages/@angular/core/-/core-18.0.0.tgz") == ( + "@angular/core", + "18.0.0", + ) def test_not_packages(self): assert extract_npm_info("/other/lodash/-/lodash-4.17.21.tgz") is None