fix: try/except in _scan_component, serialize_finding to prevent data injection, DRY LLM template, SUPPORTED_ECOSYSTEMS constant
This commit is contained in:
@@ -32,6 +32,7 @@ METADATA_PATTERNS = (
|
|||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
DEFAULT_ECOSYSTEM = "pypi"
|
DEFAULT_ECOSYSTEM = "pypi"
|
||||||
|
SUPPORTED_ECOSYSTEMS = frozenset({"pypi", "go", "npm"})
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Severity
|
# Severity
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ from ..constants import (
|
|||||||
)
|
)
|
||||||
from ..db.engine import get_session
|
from ..db.engine import get_session
|
||||||
from ..db.models import Finding
|
from ..db.models import Finding
|
||||||
from ..schemas import FindingsListResponse
|
from ..schemas import FindingsListResponse, serialize_finding
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1/findings", tags=["findings"])
|
router = APIRouter(prefix="/api/v1/findings", tags=["findings"])
|
||||||
|
|
||||||
@@ -42,14 +42,5 @@ async def list_findings(
|
|||||||
"total": total,
|
"total": total,
|
||||||
"limit": limit,
|
"limit": limit,
|
||||||
"offset": offset,
|
"offset": offset,
|
||||||
"findings": [
|
"findings": [serialize_finding(f) for f in findings],
|
||||||
{
|
|
||||||
"id": f.id,
|
|
||||||
"scan_id": f.scan_id,
|
|
||||||
**f.data,
|
|
||||||
"report": f.report,
|
|
||||||
"created_at": f.created_at.isoformat() if f.created_at else None,
|
|
||||||
}
|
|
||||||
for f in findings
|
|
||||||
],
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ from ..core.nexus import parse_package_path
|
|||||||
from ..db.engine import get_session
|
from ..db.engine import get_session
|
||||||
from ..db.models import Scan
|
from ..db.models import Scan
|
||||||
from ..db.queries import build_package_list_query
|
from ..db.queries import build_package_list_query
|
||||||
from ..schemas import PackageDetailOut, PackageListResponse
|
from ..schemas import PackageDetailOut, PackageListResponse, serialize_finding
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1/packages", tags=["packages"])
|
router = APIRouter(prefix="/api/v1/packages", tags=["packages"])
|
||||||
|
|
||||||
@@ -145,7 +145,7 @@ async def get_package(
|
|||||||
all_findings: list[dict] = []
|
all_findings: list[dict] = []
|
||||||
for s in scans:
|
for s in scans:
|
||||||
for f in s.findings:
|
for f in s.findings:
|
||||||
all_findings.append({"id": f.id, **f.data, "report": f.report})
|
all_findings.append(serialize_finding(f))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"name": scans[0].package_name,
|
"name": scans[0].package_name,
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ from ..constants import (
|
|||||||
from ..db.engine import get_session
|
from ..db.engine import get_session
|
||||||
from ..db.models import Scan
|
from ..db.models import Scan
|
||||||
from ..db.queries import build_scan_list_query, get_dashboard_stats
|
from ..db.queries import build_scan_list_query, get_dashboard_stats
|
||||||
from ..schemas import ScanDetailOut, ScanListResponse, StatsResponse
|
from ..schemas import ScanDetailOut, ScanListResponse, StatsResponse, serialize_finding
|
||||||
|
|
||||||
router = APIRouter(prefix="/api/v1/scans", tags=["scans"])
|
router = APIRouter(prefix="/api/v1/scans", tags=["scans"])
|
||||||
|
|
||||||
@@ -171,5 +171,5 @@ async def get_scan(scan_id: int, session: AsyncSession = Depends(get_session)) -
|
|||||||
"error_message": scan.error_message,
|
"error_message": scan.error_message,
|
||||||
"initiator": scan.initiator,
|
"initiator": scan.initiator,
|
||||||
"source_ip": scan.source_ip,
|
"source_ip": scan.source_ip,
|
||||||
"findings": [{"id": f.id, **f.data, "report": f.report} for f in scan.findings],
|
"findings": [serialize_finding(f) for f in scan.findings],
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -159,40 +159,45 @@ async def nexus_webhook(
|
|||||||
|
|
||||||
|
|
||||||
async def _scan_component(repository: str, name: str, version: str, ecosystem: str):
|
async def _scan_component(repository: str, name: str, version: str, ecosystem: str):
|
||||||
from ..core.nexus import nexus_get
|
|
||||||
|
|
||||||
params = urlencode(
|
|
||||||
{
|
|
||||||
"repository": repository,
|
|
||||||
"name": name,
|
|
||||||
"version": version,
|
|
||||||
"format": ecosystem,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
api_path = f"/service/rest/v1/search?{params}"
|
|
||||||
try:
|
try:
|
||||||
resp = await nexus_get(api_path)
|
from ..core.nexus import nexus_get
|
||||||
resp.raise_for_status()
|
|
||||||
data = resp.json()
|
params = urlencode(
|
||||||
|
{
|
||||||
|
"repository": repository,
|
||||||
|
"name": name,
|
||||||
|
"version": version,
|
||||||
|
"format": ecosystem,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
api_path = f"/service/rest/v1/search?{params}"
|
||||||
|
try:
|
||||||
|
resp = await nexus_get(api_path)
|
||||||
|
resp.raise_for_status()
|
||||||
|
data = resp.json()
|
||||||
|
except Exception as e:
|
||||||
|
log.warning("Component lookup error for %s==%s: %s", name, version, e)
|
||||||
|
return
|
||||||
|
|
||||||
|
items = data.get("items", [])
|
||||||
|
if not items:
|
||||||
|
log.warning("No items found in search for %s==%s", name, version)
|
||||||
|
return
|
||||||
|
|
||||||
|
for item in items:
|
||||||
|
for asset in item.get("assets", []):
|
||||||
|
asset_path = _extract_asset_path(asset)
|
||||||
|
if not asset_path or not _is_package_asset(asset_path):
|
||||||
|
continue
|
||||||
|
download_url = asset.get("downloadUrl") or _build_download_url(
|
||||||
|
repository, asset_path
|
||||||
|
)
|
||||||
|
log.info("Scanning component asset: %s", asset_path)
|
||||||
|
async for session in get_session():
|
||||||
|
await harvest(download_url, repository, ecosystem, asset_path, session)
|
||||||
|
break
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
log.warning("Component lookup error for %s==%s: %s", name, version, e)
|
log.error("Component scan failed for %s==%s: %s", name, version, e)
|
||||||
return
|
|
||||||
|
|
||||||
items = data.get("items", [])
|
|
||||||
if not items:
|
|
||||||
log.warning("No items found in search for %s==%s", name, version)
|
|
||||||
return
|
|
||||||
|
|
||||||
for item in items:
|
|
||||||
for asset in item.get("assets", []):
|
|
||||||
asset_path = _extract_asset_path(asset)
|
|
||||||
if not asset_path or not _is_package_asset(asset_path):
|
|
||||||
continue
|
|
||||||
download_url = asset.get("downloadUrl") or _build_download_url(repository, asset_path)
|
|
||||||
log.info("Scanning component asset: %s", asset_path)
|
|
||||||
async for session in get_session():
|
|
||||||
await harvest(download_url, repository, ecosystem, asset_path, session)
|
|
||||||
break
|
|
||||||
|
|
||||||
|
|
||||||
async def _scan_in_background(
|
async def _scan_in_background(
|
||||||
|
|||||||
@@ -100,3 +100,20 @@ class StatsResponse(BaseModel):
|
|||||||
total_findings: int
|
total_findings: int
|
||||||
top_rules: list[dict]
|
top_rules: list[dict]
|
||||||
latest_scan_at: datetime | None = None
|
latest_scan_at: datetime | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# Finding data known fields (prevents **f.data from overwriting id/scan_id)
|
||||||
|
_FINDING_DATA_FIELDS = ("rule", "severity", "message", "location", "code")
|
||||||
|
|
||||||
|
|
||||||
|
def serialize_finding(finding) -> dict:
|
||||||
|
"""Extract known fields from a Finding, preventing data field injection."""
|
||||||
|
result = {
|
||||||
|
"id": finding.id,
|
||||||
|
"scan_id": finding.scan_id,
|
||||||
|
"report": finding.report,
|
||||||
|
"created_at": finding.created_at.isoformat() if finding.created_at else None,
|
||||||
|
}
|
||||||
|
for field in _FINDING_DATA_FIELDS:
|
||||||
|
result[field] = finding.data.get(field, "")
|
||||||
|
return result
|
||||||
|
|||||||
@@ -57,24 +57,9 @@
|
|||||||
{% if f.report and f.report.status == "analyzing" %}
|
{% if f.report and f.report.status == "analyzing" %}
|
||||||
{% include "_llm_spinner.html" %}
|
{% include "_llm_spinner.html" %}
|
||||||
{% elif f.report and f.report.verdict %}
|
{% elif f.report and f.report.verdict %}
|
||||||
<div class="llm-report llm-{{ f.report.verdict }}">
|
{% with report=f.report, finding_id=f.id %}
|
||||||
<div class="llm-header">
|
{% include "_llm_report_fragment.html" %}
|
||||||
<span class="llm-badge llm-badge-{{ f.report.verdict }}">{{ f.report.verdict }}</span>
|
{% endwith %}
|
||||||
{% if f.report.severity_rating %}
|
|
||||||
<span class="llm-severity">{{ f.report.severity_rating }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.llm_enabled and not config.llm_auto_analyze %}
|
|
||||||
<button class="llm-retry"
|
|
||||||
hx-post="/api/v1/findings/{{ f.id }}/analyze?retry=1"
|
|
||||||
hx-target="closest .llm-report"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<p class="llm-summary">{{ f.report.summary }}</p>
|
|
||||||
<p class="llm-analysis">{{ f.report.analysis }}</p>
|
|
||||||
<p class="llm-disclaimer">{{ t('llm_disclaimer', request.state.lang) }}</p>
|
|
||||||
</div>
|
|
||||||
{% elif config.llm_enabled and not config.llm_auto_analyze %}
|
{% elif config.llm_enabled and not config.llm_auto_analyze %}
|
||||||
<div class="llm-actions" id="llm-{{ f.id }}">
|
<div class="llm-actions" id="llm-{{ f.id }}">
|
||||||
<button class="outline"
|
<button class="outline"
|
||||||
|
|||||||
@@ -53,24 +53,9 @@
|
|||||||
{% if f.report and f.report.status == "analyzing" %}
|
{% if f.report and f.report.status == "analyzing" %}
|
||||||
{% include "_llm_spinner.html" %}
|
{% include "_llm_spinner.html" %}
|
||||||
{% elif f.report and f.report.verdict %}
|
{% elif f.report and f.report.verdict %}
|
||||||
<div class="llm-report llm-{{ f.report.verdict }}">
|
{% with report=f.report, finding_id=f.id %}
|
||||||
<div class="llm-header">
|
{% include "_llm_report_fragment.html" %}
|
||||||
<span class="llm-badge llm-badge-{{ f.report.verdict }}">{{ f.report.verdict }}</span>
|
{% endwith %}
|
||||||
{% if f.report.severity_rating %}
|
|
||||||
<span class="llm-severity">{{ f.report.severity_rating }}</span>
|
|
||||||
{% endif %}
|
|
||||||
{% if config.llm_enabled and not config.llm_auto_analyze %}
|
|
||||||
<button class="llm-retry"
|
|
||||||
hx-post="/api/v1/findings/{{ f.id }}/analyze?retry=1"
|
|
||||||
hx-target="closest .llm-report"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
hx-indicator="closest .llm-report">{{ t('llm_retry', request.state.lang) }}</button>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
<p class="llm-summary">{{ f.report.summary }}</p>
|
|
||||||
<p class="llm-analysis">{{ f.report.analysis }}</p>
|
|
||||||
<p class="llm-disclaimer">{{ t('llm_disclaimer', request.state.lang) }}</p>
|
|
||||||
</div>
|
|
||||||
{% elif config.llm_enabled and not config.llm_auto_analyze %}
|
{% elif config.llm_enabled and not config.llm_auto_analyze %}
|
||||||
<div class="llm-actions" id="llm-{{ f.id }}">
|
<div class="llm-actions" id="llm-{{ f.id }}">
|
||||||
<button class="outline"
|
<button class="outline"
|
||||||
|
|||||||
Reference in New Issue
Block a user