from __future__ import annotations import html import json import tempfile import gradio as gr from scanner import ScanError, ScanResult, result_json, scan_repository def _summary_markdown(result: ScanResult) -> str: counts = result.counts() badges = " · ".join( f"**{name.title()}: {counts.get(name, 0)}**" for name in ("critical", "high", "medium", "low", "info") ) artifacts = result.artifacts priority = "No unresolved non-informational finding was detected." for finding in result.finding_groups(): if finding["status"] != "unresolved" or finding["severity"] == "info": continue priority = f"**{finding['severity'].upper()} · {finding['rule_id']}** — {html.escape(finding['title'])}" break return f"""### Scan complete: `{html.escape(result.target)}` {badges} - **Type / revision:** {result.repo_type.title()} · `{result.revision[:12]}` - **Coverage / OSV:** `{html.escape(str(artifacts.get('static_coverage', 'unknown')).upper())}` {artifacts.get('inspected_text_files', len(result.inspected_files))}/{artifacts.get('eligible_text_files', len(result.inspected_files))} · `{html.escape(str(result.osv_summary.get('status', 'not checked')).upper())}` {result.osv_summary.get('queried_packages', 0)}/{result.osv_summary.get('exact_packages', 0)} - **Top priority:** {priority} """ def _safe(value: object, fallback: str = "not stated") -> str: text = str(value) if value not in (None, "") else fallback return html.escape(text) def _overview_markdown(result: ScanResult) -> str: metadata = result.metadata_summary artifacts = result.artifacts dependencies = ", ".join(f"`{_safe(item)}`" for item in result.dependencies) or "None declared in Hub metadata" notes = "\n".join(f"- {_safe(note)}" for note in result.notes) or "- No additional note was produced." exact = sum(1 for item in result.package_inventory if item.get("source_type") == "exact") unresolved = sum(1 for item in result.package_inventory if item.get("source_type") != "exact") return f"""### Repository details - **License / task / library:** `{_safe(metadata.get('license'), 'undeclared')}` / `{_safe(metadata.get('pipeline_tag'), 'undeclared')}` / `{_safe(metadata.get('library'), 'undeclared')}` - **Architecture:** `{_safe(metadata.get('architecture'), 'not identified')}` - **Text/config files inspected:** {len(result.inspected_files)} - **Candidate files skipped:** {result.skipped_files} - **Published files:** {artifacts.get('repository_file_count', 0)} totaling {artifacts.get('repository_size', '0 B')} - **Weight artifacts:** {artifacts.get('weight_file_count', 0)} totaling {artifacts.get('weight_size', '0 B')} ({_safe(artifacts.get('weight_formats') or 'none')}) - **Safetensors index:** {artifacts.get('index_referenced_shards', 0)} referenced shards; {artifacts.get('index_declared_tensor_size', '0 B')} declared tensor data - **Declared dependencies:** {dependencies} - **Package inventory:** {len(result.package_inventory)} components ({exact} exact; {unresolved} unresolved/direct) ### Important limitations {notes} """ def _coverage_items(result: ScanResult) -> list[dict[str, str]]: items = [] for item in result.checks: status = item.status.upper().replace("_", " ") items.append({ "label": f"{status} · {item.category}", "detail": f"### {_safe(item.category)}\n\n**Status:** `{_safe(status)}`\n\n{_safe(item.detail)}", }) return items or [{"label": "No coverage records", "detail": "No coverage record was produced."}] def _dependency_items(result: ScanResult) -> list[dict[str, str]]: items = [] for item in result.dependency_details: relations = ", ".join(item.get("relation") or []) items.append({ "label": f"{relations or 'declared'} · {item.get('repository', 'unknown')}", "detail": f"### {_safe(item.get('repository'))}\n\n" f"- **Relationship:** {_safe(relations)}\n" f"- **Depth:** {_safe(item.get('depth'))}\n" f"- **License:** {_safe(item.get('license'), 'undeclared')}\n" f"- **Status:** {_safe(item.get('status'), 'unknown')}\n" f"- **Revision:** `{_safe(item.get('revision'))}`\n" f"- **Gated:** {_safe(item.get('gated', False))}", }) if not items: items = [{"label": f"declared · {item}", "detail": f"### {_safe(item)}\n\nDeclared dependency; details were not checked."} for item in result.dependencies] return items or [{"label": "No upstream dependencies", "detail": "No upstream repository dependency was declared."}] def _finding_items(result: ScanResult) -> list[dict[str, str]]: items = [] for finding in result.finding_groups(): occurrences = finding["occurrences"] locations = [] evidence = "" for occurrence in occurrences[:3]: location = occurrence.get("path") or "Repository metadata" if occurrence.get("line"): location += f":{occurrence['line']}" locations.append(location) if not evidence and occurrence.get("evidence"): evidence = occurrence["evidence"] location_text = ", ".join(locations) + (" …" if len(occurrences) > 3 else "") evidence_line = f"\n- **Evidence:** `{_safe(evidence)}`" if evidence else "" label = f"{finding['severity'].upper()} · {finding['rule_id']} · {finding['title']}" items.append({ "label": label, "detail": f"### {_safe(label)}\n\n" f"- **Status:** {_safe(finding['status'].upper())}\n" f"- **Occurrences:** {len(occurrences)}\n" f"- **Locations:** {_safe(location_text)}" f"{evidence_line}\n" f"- **Confidence:** {_safe(finding['confidence'])}\n\n" f"**Why it matters**\n\n{_safe(finding['detail'])}\n\n" f"**Remediation**\n\n{_safe(finding['remediation'])}", }) return items or [{"label": "No findings", "detail": "No evidence-linked finding was produced."}] def _package_items(result: ScanResult) -> list[dict[str, str]]: items = [] for item in result.package_inventory: manifests = ", ".join(sorted({occurrence.get("path", "") for occurrence in item.get("occurrences", [])})) version = item.get("version") or "unresolved" label = f"{item.get('name', 'unknown')} · {version}" items.append({ "label": label, "detail": f"### {_safe(label)}\n\n" f"- **Source type:** {_safe(item.get('source_type'))}\n" f"- **Constraint:** `{_safe(item.get('constraint'))}`\n" f"- **PURL:** `{_safe(item.get('purl'))}`\n" f"- **Manifest:** {_safe(manifests)}", }) return items or [{"label": "No packages", "detail": "No supported package declaration was found."}] def _vulnerability_items(result: ScanResult) -> list[dict[str, str]]: items = [] for item in result.vulnerabilities: label = f"{item.get('severity', '').upper()} · {item.get('id', 'unknown')} · {item.get('package', 'unknown')}" items.append({ "label": label, "detail": f"### {_safe(label)}\n\n" f"{_safe(item.get('summary'))}\n\n" f"- **Package:** {_safe(item.get('package'))} `{_safe(item.get('version'))}`\n" f"- **Aliases:** {_safe(', '.join(item.get('aliases') or []))}\n" f"- **Fixed versions:** {_safe(', '.join(item.get('fixed_versions') or []))}\n" f"- **Advisory:** {_safe(item.get('url'))}", }) return items or [{"label": "No known matches", "detail": "No known OSV match was returned for the exactly pinned packages checked."}] def _selector_update(items: list[dict[str, str]], label: str): choices = [(item["label"], str(index)) for index, item in enumerate(items)] return gr.Dropdown(choices=choices, value="0", label=f"{label} ({len(items)})") def _select_detail(selection: str | None, state: dict[str, list[str]] | None, section: str) -> str: details = (state or {}).get(section) or [] try: index = int(selection or 0) except (TypeError, ValueError): index = 0 if not details: return "Run a scan to populate this section." return details[index] if 0 <= index < len(details) else details[0] def _write_sbom(result: ScanResult) -> str: with tempfile.NamedTemporaryFile( mode="w", encoding="utf-8", prefix="modelsentry-", suffix=".cdx.json", delete=False, ) as handle: json.dump(result.sbom, handle, indent=2, ensure_ascii=False) handle.write("\n") return handle.name def run_scan(target: str): try: result = scan_repository(target) except ScanError as exc: raise gr.Error(str(exc)) from exc except Exception as exc: raise gr.Error("The public repository could not be scanned because of an unexpected service error. Please try again.") from exc sections = { "overview": [{"label": "Repository profile", "detail": _overview_markdown(result)}] + _coverage_items(result), "dependencies": _dependency_items(result), "packages": _package_items(result), "vulnerabilities": _vulnerability_items(result), "findings": _finding_items(result), } view_state = {name: [item["detail"] for item in items] for name, items in sections.items()} return ( _summary_markdown(result), _selector_update(sections["overview"], "Overview topic"), sections["overview"][0]["detail"], _selector_update(sections["dependencies"], "Upstream repository"), sections["dependencies"][0]["detail"], _selector_update(sections["packages"], "Package"), sections["packages"][0]["detail"], _selector_update(sections["vulnerabilities"], "Known vulnerability"), sections["vulnerabilities"][0]["detail"], _selector_update(sections["findings"], "Finding"), sections["findings"][0]["detail"], result_json(result), _write_sbom(result), view_state, ) DESCRIPTION = """Inspect a public Hugging Face Model or Space at an immutable revision. ModelSentry never executes repository code or downloads model weights; its findings are bounded static evidence, not a safety certification.""" APP_CSS = """ html, body { height: 100%; overflow: hidden !important; margin: 0; } .audit-cards, .audit-cards p, .audit-cards li, .audit-cards code { overflow-wrap: anywhere; word-break: break-word; white-space: normal; } .scan-summary { border: 1px solid var(--border-color-primary); border-left: 0.35rem solid var(--primary-500); border-radius: var(--radius-lg); padding: 0.25rem 1rem; background: var(--background-fill-secondary); min-height: 12rem; } .detail-card { border: 1px solid var(--border-color-primary); border-radius: var(--radius-lg); padding: 0.75rem 1rem; min-height: 22rem; background: var(--background-fill-primary); } .compact-note { font-size: 0.85rem; color: var(--body-text-color-subdued); } .gradio-container { width: 100%; max-width: none !important; height: 100vh !important; overflow-y: scroll !important; overflow-x: hidden !important; scrollbar-gutter: stable; scrollbar-width: auto; scrollbar-color: rgba(100, 116, 139, 0.9) rgba(148, 163, 184, 0.22); box-sizing: border-box; padding: 1rem max(1rem, calc((100vw - 1200px) / 2)) 2rem !important; } .gradio-container::-webkit-scrollbar { width: 14px; } .gradio-container::-webkit-scrollbar-track { background: rgba(148, 163, 184, 0.22); } .gradio-container::-webkit-scrollbar-thumb { background: rgba(100, 116, 139, 0.9); border: 3px solid transparent; border-radius: 999px; background-clip: padding-box; } .gradio-container::-webkit-scrollbar-thumb:hover { background: var(--primary-500, #6366f1); background-clip: padding-box; } """ with gr.Blocks(title="ModelSentry", delete_cache=(600, 1800)) as demo: gr.Markdown("# 🛡️ ModelSentry · Hugging Face repository auditor") gr.Markdown(DESCRIPTION) with gr.Row(): target = gr.Textbox( label="Public Model or Space", placeholder="https://huggingface.co/spaces/owner/name or model:owner/name", scale=5, ) scan = gr.Button("Scan immutable revision", variant="primary", scale=1) summary = gr.Markdown( "### Ready to scan\n\nEnter `model:owner/name`, `space:owner/name`, or a public Hugging Face URL.\n\nThe result summary will stay in this fixed panel.", elem_classes=["scan-summary", "audit-cards"], ) view_state = gr.State({}) with gr.Tabs(): with gr.Tab("Overview"): with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=280): coverage_selector = gr.Dropdown(label="Overview topic", choices=[], interactive=True) with gr.Column(scale=2, min_width=360): coverage_detail = gr.Markdown( "### Overview inspector\n\nRun a scan, then choose the repository profile or a coverage area.", elem_classes=["detail-card", "audit-cards"], ) with gr.Tab("Findings"): with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=280): finding_selector = gr.Dropdown(label="Finding", choices=[], interactive=True) with gr.Column(scale=2, min_width=360): finding_detail = gr.Markdown("Select a finding.", elem_classes=["detail-card", "audit-cards"]) with gr.Tab("Provenance"): with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=280): dependency_selector = gr.Dropdown(label="Upstream repository", choices=[], interactive=True) with gr.Column(scale=2, min_width=360): dependency_detail = gr.Markdown("Select an upstream repository.", elem_classes=["detail-card", "audit-cards"]) with gr.Tab("Packages"): with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=280): package_selector = gr.Dropdown(label="Package", choices=[], interactive=True) with gr.Column(scale=2, min_width=360): package_detail = gr.Markdown("Select a package.", elem_classes=["detail-card", "audit-cards"]) with gr.Tab("Vulnerabilities"): with gr.Row(equal_height=False): with gr.Column(scale=1, min_width=280): vulnerability_selector = gr.Dropdown(label="Known vulnerability", choices=[], interactive=True) with gr.Column(scale=2, min_width=360): vulnerability_detail = gr.Markdown("Select a vulnerability.", elem_classes=["detail-card", "audit-cards"]) with gr.Tab("JSON / SBOM"): report = gr.Code(language="json", label="modelsentry.scan.v4 JSON", lines=18) sbom = gr.File(label="CycloneDX 1.6 SBOM", interactive=False) gr.Markdown( "Static evidence requires human review. SBOM download files are eligible for automatic cleanup within 30 minutes.", elem_classes=["compact-note"], ) outputs = [ summary, coverage_selector, coverage_detail, dependency_selector, dependency_detail, package_selector, package_detail, vulnerability_selector, vulnerability_detail, finding_selector, finding_detail, report, sbom, view_state, ] scan.click(run_scan, inputs=target, outputs=outputs) target.submit(run_scan, inputs=target, outputs=outputs) coverage_selector.change( lambda selection, state: _select_detail(selection, state, "overview"), inputs=[coverage_selector, view_state], outputs=coverage_detail, api_name=False, ) dependency_selector.change( lambda selection, state: _select_detail(selection, state, "dependencies"), inputs=[dependency_selector, view_state], outputs=dependency_detail, api_name=False, ) package_selector.change( lambda selection, state: _select_detail(selection, state, "packages"), inputs=[package_selector, view_state], outputs=package_detail, api_name=False, ) vulnerability_selector.change( lambda selection, state: _select_detail(selection, state, "vulnerabilities"), inputs=[vulnerability_selector, view_state], outputs=vulnerability_detail, api_name=False, ) finding_selector.change( lambda selection, state: _select_detail(selection, state, "findings"), inputs=[finding_selector, view_state], outputs=finding_detail, api_name=False, ) if __name__ == "__main__": demo.launch(theme=gr.themes.Soft(), css=APP_CSS)