| 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) |
|
|