Files
breakpilot-compliance/backend-compliance/compliance/services/scanner_mcp_client.py
T
Benjamin Admin e140477c0b
CI / nodejs-build (push) Successful in 3m12s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 39s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
CI / detect-changes (push) Successful in 15s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Successful in 12s
CI / validate-canonical-controls (push) Successful in 12s
CI / loc-budget (push) Successful in 25s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
feat(cra): Pull-Flow — Findings vom Scanner-MCP ziehen + assessen
(2) Wir als MCP-Client zum compliance-scanner-agent:
- scanner_mcp_client.fetch_findings(): streamablehttp_client + ClientSession →
  list_findings, parst JSON-Text zu Finding-Dicts. Config via SCANNER_MCP_URL/
  SCANNER_MCP_TOKEN (unset = leer → UI behält Demo). Transport lazy-importiert.
- POST /v1/cra/assess-from-scanner: rohe Scanner-Dicts → toleranter Mapper
  (behält scan_type/cvss_score/file_path) → assess + Breadth.
- Tests: parse_findings_text + no-config-Pfad.

Live-Verdrahtung der UI folgt, sobald ihr Endpoint+Token stehen (dann nur Env
setzen + useCRA auf /assess-from-scanner zeigen).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-15 19:05:44 +02:00

60 lines
2.2 KiB
Python

"""MCP client to the external compliance-scanner-agent (pull-flow).
We connect to THEIR MCP server (Streamable HTTP + Bearer), pull the findings they
already produced (list_findings), and feed them into our deterministic CRA
assessment. Their tool returns a JSON array of Finding docs as text content; the
field shape is bridged by ScannerFinding.from_dict (scan_type/cvss_score/...).
Config via env (SCANNER_MCP_URL, SCANNER_MCP_TOKEN) or per-call override. When no
URL is configured, fetch_findings returns [] — callers fall back to their demo.
"""
import json
import os
from typing import Optional
SCANNER_MCP_URL = os.getenv("SCANNER_MCP_URL", "")
SCANNER_MCP_TOKEN = os.getenv("SCANNER_MCP_TOKEN", "")
def parse_findings_text(text: str) -> list:
"""Parse the list_findings tool result (a JSON array, or {findings|results:[...]})."""
try:
data = json.loads(text)
except (json.JSONDecodeError, TypeError):
return []
if isinstance(data, dict):
data = data.get("findings") or data.get("results") or []
return data if isinstance(data, list) else []
async def fetch_findings(
repo_id: Optional[str] = None,
severity: Optional[str] = None,
limit: int = 200,
base_url: Optional[str] = None,
token: Optional[str] = None,
) -> list:
"""Pull findings from the scanner's MCP server. Returns [] if unconfigured or on error."""
url = (base_url or SCANNER_MCP_URL).rstrip("/")
tok = token or SCANNER_MCP_TOKEN
if not url:
return []
from mcp.client.session import ClientSession
from mcp.client.streamable_http import streamablehttp_client
headers = {"Authorization": f"Bearer {tok}"} if tok else None
params: dict = {"limit": limit}
if repo_id:
params["repo_id"] = repo_id
if severity:
params["severity"] = severity
async with streamablehttp_client(url, headers=headers) as (read, write, _):
async with ClientSession(read, write) as session:
await session.initialize()
result = await session.call_tool("list_findings", params)
texts = [c.text for c in (result.content or []) if getattr(c, "type", "") == "text"]
return parse_findings_text(texts[0]) if texts else []