fix: scanner now handles real guarddog v2 JSON format

This commit is contained in:
Marker689
2026-05-09 04:55:58 +03:00
parent 4ce99d3c85
commit 4bfead8d6e
9 changed files with 201 additions and 116 deletions

View File

@@ -23,7 +23,9 @@ from guarddog_nexus.main import app # noqa: E402
@pytest_asyncio.fixture
async def db_engine():
engine = create_async_engine("sqlite+aiosqlite:///file:guarddog_test?mode=memory&cache=shared&uri=true")
engine = create_async_engine(
"sqlite+aiosqlite:///file:guarddog_test?mode=memory&cache=shared&uri=true"
)
async with engine.begin() as conn:
await conn.run_sync(Base.metadata.create_all)
yield engine
@@ -76,29 +78,44 @@ def sample_nexus_webhook():
@pytest.fixture
def guarddog_output_clean():
return {
"results": [],
"errors": [],
"package": "safe-pkg",
"issues": 0,
"errors": {},
"results": {
"obfuscation": {},
"exec-base64": {},
"shady-links": {},
"typosquatting": None,
"empty_information": None,
},
}
@pytest.fixture
def guarddog_output_flagged():
return {
"results": [
{
"rule": "shady-links",
"severity": "WARNING",
"message": "Package contains URL to suspicious domain",
"location": "setup.py:15",
},
{
"rule": "exec-base64",
"severity": "ERROR",
"message": "Base64-encoded code execution detected",
"location": "core.py:42",
},
],
"errors": [],
"package": "bad-pkg",
"issues": 3,
"errors": {},
"results": {
"shady-links": [
{
"message": "Package contains URL to suspicious domain",
"location": "setup.py:15",
"code": "url = 'http://evil.com'",
}
],
"exec-base64": [
{
"message": "Base64-encoded code execution detected",
"location": "core.py:42",
"code": "exec(base64.b64decode(...))",
}
],
"empty_information": "Package description is empty",
"obfuscation": {},
"typosquatting": None,
},
}
@@ -109,15 +126,21 @@ def guarddog_normalized_flagged():
{
"rule": "shady-links",
"severity": "WARNING",
"message": "Suspicious URL",
"message": "Package contains URL to suspicious domain",
"location": "setup.py:15",
},
{
"rule": "exec-base64",
"severity": "ERROR",
"message": "Base64 exec",
"severity": "WARNING",
"message": "Base64-encoded code execution detected",
"location": "core.py:42",
},
{
"rule": "empty_information",
"severity": "WARNING",
"message": "Package description is empty",
"location": "",
},
],
"errors": [],
}

View File

@@ -34,7 +34,7 @@ async def test_harvest_new_package(db_session, guarddog_normalized_flagged):
assert scan.ecosystem == "pypi"
assert scan.status == "completed"
assert scan.flagged is True
assert scan.total_findings == 2
assert scan.total_findings == 3
assert scan.sha256 == "abc123"
findings = (
@@ -42,7 +42,7 @@ async def test_harvest_new_package(db_session, guarddog_normalized_flagged):
.scalars()
.all()
)
assert len(findings) == 2
assert len(findings) == 3
@pytest.mark.asyncio
@@ -58,11 +58,17 @@ async def test_harvest_skips_duplicate(db_session, guarddog_normalized_flagged):
first = await harvest(
"http://nexus:8081/repo/pypi-proxy/packages/x/1.0/x-1.0.tar.gz",
"pypi-proxy", "pypi", "packages/x/1.0/x-1.0.tar.gz", db_session,
"pypi-proxy",
"pypi",
"packages/x/1.0/x-1.0.tar.gz",
db_session,
)
second = await harvest(
"http://nexus:8081/repo/pypi-proxy/packages/x/1.0/x-1.0.tar.gz",
"pypi-proxy", "pypi", "packages/x/1.0/x-1.0.tar.gz", db_session,
"pypi-proxy",
"pypi",
"packages/x/1.0/x-1.0.tar.gz",
db_session,
)
assert first is not None
@@ -82,7 +88,10 @@ async def test_harvest_clean_package(db_session, guarddog_normalized_clean):
scan = await harvest(
"http://nexus:8081/repo/pypi-proxy/packages/django/4.2/django-4.2.tar.gz",
"pypi-proxy", "pypi", "packages/django/4.2/django-4.2.tar.gz", db_session,
"pypi-proxy",
"pypi",
"packages/django/4.2/django-4.2.tar.gz",
db_session,
)
assert scan is not None
@@ -97,7 +106,10 @@ async def test_harvest_download_failure(db_session):
scan = await harvest(
"http://nexus:8081/repo/pypi-proxy/packages/fail/1.0/fail-1.0.tar.gz",
"pypi-proxy", "pypi", "packages/fail/1.0/fail-1.0.tar.gz", db_session,
"pypi-proxy",
"pypi",
"packages/fail/1.0/fail-1.0.tar.gz",
db_session,
)
assert scan is not None
@@ -109,6 +121,9 @@ async def test_harvest_download_failure(db_session):
async def test_harvest_skips_non_package_asset(db_session):
scan = await harvest(
"http://nexus:8081/repo/pypi-proxy/simple/index.html",
"pypi-proxy", "pypi", "simple/index.html", db_session,
"pypi-proxy",
"pypi",
"simple/index.html",
db_session,
)
assert scan is None

View File

@@ -11,18 +11,41 @@ def test_normalize_clean_output(guarddog_output_clean):
def test_normalize_flagged_output(guarddog_output_flagged):
result = _normalize_output(guarddog_output_flagged)
assert len(result["findings"]) == 2
assert result["findings"][0]["rule"] == "shady-links"
assert result["findings"][0]["severity"] == "WARNING"
assert result["findings"][1]["rule"] == "exec-base64"
assert result["findings"][1]["severity"] == "ERROR"
assert len(result["findings"]) == 3
rules = {f["rule"] for f in result["findings"]}
assert "shady-links" in rules
assert "exec-base64" in rules
assert "empty_information" in rules
def test_normalize_issues_format():
def test_normalize_skips_null_and_empty_dicts():
data = {
"issues": [{"id": "test-rule", "severity": "ERROR", "description": "Bad"}],
"errors": [],
"issues": 0,
"errors": {},
"results": {
"foo": None,
"bar": {},
"baz": "metadata finding",
},
}
result = _normalize_output(data)
assert len(result["findings"]) == 1
assert result["findings"][0]["rule"] == "test-rule"
assert result["findings"][0]["rule"] == "baz"
assert result["findings"][0]["message"] == "metadata finding"
def test_normalize_semgrep_list():
data = {
"issues": 2,
"errors": {},
"results": {
"code-execution": [
{"message": "Found exec()", "location": "setup.py:10", "severity": "ERROR"},
{"message": "Found eval()", "location": "core.py:5", "severity": "ERROR"},
],
},
}
result = _normalize_output(data)
assert len(result["findings"]) == 2
assert result["findings"][0]["location"] == "setup.py:10"
assert result["findings"][0]["severity"] == "ERROR"