| """RelayShield Agentic Attack Surface MCP Server — Hugging Face Space. |
| |
| Exposes 13 of RelayShield's agent-security checks as MCP tools via Gradio's |
| mcp_server=True mode: |
| - check_mcp_server_risk: typosquat/reputation/registration-age risk for |
| an MCP server URL. |
| - check_prompt_injection_breach: credential breach exposure sourced |
| specifically from prompt-injection attacks against AI agents. |
| - check_tech_stack_cve: CISA KEV / high-EPSS CVEs targeting a declared |
| AI agent framework / tech stack, now with a bounded public-PoC/exploit- |
| availability signal on top-ranked non-KEV matches. |
| - check_bulk_identity_risk: hierarchical org + AI-agent-identity risk |
| scoring for a domain and its agent/service-account identities. |
| - check_oauth_watchlist: OAuth-app breach + stolen-token exposure. |
| - check_supply_chain: up to 10 vendor domains checked for breach/ |
| infostealer exposure. |
| - check_session_risk: active/reusable stolen session (cookie/token) |
| exposure. |
| - check_nhi_exposure: non-human-identity credential exposure (API keys, |
| service tokens, PATs). |
| - check_secret_scan: secrets exposed in public GitHub repos. |
| - check_llm_credential_exposure: exposed LLM/AI provider API keys |
| (LLMjacking) — free to try, no api_key required (shared demo quota). |
| - check_agent_risk_summary: composite check — breach + LLM credential |
| exposure + tech-stack CVE in one call instead of four. |
| - get_stix_indicators: RelayShield's IOC corpus as STIX 2.1 objects via |
| the TAXII 2.1 feed (requires a TI subscription key). |
| - check_server_status: introspection — which tools/sources are live, no |
| api_key required. |
| |
| v3 (2026-07-26) — LLMjacking + MCP enhancement sprint: added the last 4 |
| tools above (check_llm_credential_exposure, check_agent_risk_summary, |
| get_stix_indicators, check_server_status), a zero-key free tier on |
| check_llm_credential_exposure specifically, and a bounded PoC/exploit- |
| availability signal on check_tech_stack_cve's top non-KEV matches. |
| |
| v2 (2026-07-19) — response to community feedback: added 5 tools, and every |
| tool now returns a typed structured result instead of a human-formatted |
| string: |
| |
| { |
| "outcome": "finding" | "no_known_finding" | "error", |
| "recommended_action": "allow" | "review" | "deny" | "defer", |
| "reason_codes": [...], |
| "evidence": [...], |
| "coverage": {"complete": bool, "scope": "what was actually checked"}, |
| "freshness": {"observed_at": "...", "expires_at": null}, |
| "error": {"kind": "...", "message": "...", "retryable": bool} | null |
| } |
| |
| "no_known_finding" deliberately does not mean "safe" — it means nothing was |
| found in the sources and scope actually queried. |
| |
| Each caller supplies their own RelayShield API key as a tool argument — |
| this Space is a single shared server handling many remote MCP callers at |
| once, so there is no per-caller "environment" to read a key from. Get a |
| key at https://api.relayshield.net/developers. |
| """ |
|
|
| import json |
| import os |
| import re |
|
|
| import gradio as gr |
| import requests |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| AWS_MARKETPLACE_MODE = os.environ.get("AWS_MARKETPLACE_MODE", "").lower() == "true" |
|
|
| _KEY_URL_RE = re.compile( |
| r"(?:Get (?:a(?:\s+key)?|one|your own(?:\s+key)?)|get your own key)\s+at\s+" |
| r"(?:https?://)?api\.relayshield\.net/developers(?:#[\w-]+)?\.?", |
| ) |
| _AWS_KEY_TEXT = "Included with your AWS Marketplace subscription, issued automatically by email" |
|
|
|
|
| def _scrub(text: str) -> str: |
| """No-op unless AWS_MARKETPLACE_MODE is set — see comment above.""" |
| if not AWS_MARKETPLACE_MODE or not text: |
| return text |
| return _KEY_URL_RE.sub(_AWS_KEY_TEXT, text) |
|
|
|
|
| __doc__ = _scrub(__doc__) |
|
|
| API_BASE_URL = "https://api.relayshield.net" |
|
|
|
|
| def _headers(api_key: str) -> dict[str, str]: |
| |
| |
| |
| |
| |
| |
| return { |
| "Content-Type": "application/json", |
| "X-RS-API-KEY": api_key, |
| "X-RS-Source": "hf-mcp-space", |
| } |
|
|
|
|
| def _outcome_for_severity(highest_severity: str | None, found: bool = True) -> tuple[str, str]: |
| """Maps a RelayShield severity string to (outcome, recommended_action). |
| |
| CRITICAL escalates to a deny recommendation; HIGH/MEDIUM/LOW are surfaced as |
| findings for human/agent review rather than an automatic block, since these |
| are enrichment signals, not a certified detection. |
| """ |
| if not found or highest_severity in (None, "NONE", "CLEAN"): |
| return "no_known_finding", "allow" |
| if highest_severity == "CRITICAL": |
| return "finding", "deny" |
| return "finding", "review" |
|
|
|
|
| def _structured_result( |
| outcome: str, |
| recommended_action: str, |
| *, |
| reason_codes: list[str] | None = None, |
| evidence: list | None = None, |
| scope: str, |
| coverage_complete: bool = True, |
| observed_at: str | None = None, |
| ) -> str: |
| return json.dumps({ |
| "outcome": outcome, |
| "recommended_action": recommended_action, |
| "reason_codes": reason_codes or [], |
| "evidence": evidence or [], |
| "coverage": {"complete": coverage_complete, "scope": scope}, |
| "freshness": {"observed_at": observed_at, "expires_at": None}, |
| "error": None, |
| }, indent=2) |
|
|
|
|
| def _error_result(kind: str, message: str, retryable: bool = False) -> str: |
| return json.dumps({ |
| "outcome": "error", |
| "recommended_action": "defer", |
| "reason_codes": [], |
| "evidence": [], |
| "coverage": {"complete": False, "scope": ""}, |
| "freshness": {"observed_at": None, "expires_at": None}, |
| "error": {"kind": kind, "message": message, "retryable": retryable}, |
| }, indent=2) |
|
|
|
|
| def _call(path: str, payload: dict, api_key: str) -> tuple[dict | None, str | None]: |
| """POSTs to a RelayShield metered endpoint. Returns (data, error_json_string). |
| error_json_string is None on success, or a ready-to-return _error_result() on |
| failure.""" |
| try: |
| resp = requests.post( |
| f"{API_BASE_URL}{path}", |
| json=payload, |
| headers=_headers(api_key), |
| timeout=15, |
| ) |
| except requests.Timeout: |
| return None, _error_result("timeout", f"RelayShield API call to {path} timed out after 15s.", retryable=True) |
| except requests.RequestException as exc: |
| return None, _error_result("upstream", f"RelayShield API call to {path} failed: {exc}", retryable=True) |
|
|
| if resp.status_code in (401, 403): |
| return None, _error_result("auth", f"RelayShield API call to {path} returned {resp.status_code} — check api_key.", retryable=False) |
| if resp.status_code == 429: |
| return None, _error_result("rate_limited", f"RelayShield API call to {path} was rate-limited.", retryable=True) |
| if resp.status_code >= 500: |
| return None, _error_result("upstream", f"RelayShield API call to {path} returned {resp.status_code}.", retryable=True) |
| if resp.status_code >= 400: |
| return None, _error_result("other", f"RelayShield API call to {path} returned {resp.status_code}: {resp.text[:200]}", retryable=False) |
|
|
| try: |
| response_json = resp.json() |
| except ValueError: |
| return None, _error_result("malformed_response", f"RelayShield API call to {path} returned non-JSON content.", retryable=False) |
|
|
| if not isinstance(response_json, dict): |
| return None, _error_result("malformed_response", f"RelayShield API call to {path} returned an unexpected response shape.", retryable=False) |
|
|
| return response_json.get("data", {}), None |
|
|
|
|
| def check_mcp_server_risk(server_url: str, api_key: str) -> str: |
| """Check an MCP server URL for typosquat risk, presence in RelayShield's |
| criminal IOC corpus, and domain-registration age. |
| |
| Args: |
| server_url: Full URL of the MCP server to check, e.g. https://example.com/mcp |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-mcp-registry-risk |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| if not server_url: |
| return _error_result("other", "server_url is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-mcp-registry-risk")) |
|
|
| data, error = _call("/v1/metered/mcp-registry-risk", {"server_url": server_url}, api_key) |
| if error: |
| return error |
|
|
| findings = data.get("findings", []) |
| outcome, action = _outcome_for_severity(data.get("verdict"), found=bool(findings)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=[f.get("type", "unknown") for f in findings], |
| evidence=findings, |
| scope=f"Checked {data.get('queried', server_url)!r} against known-MCP-domain typosquat distance, " |
| "RelayShield's criminal IOC corpus, and RDAP registration age.", |
| ) |
|
|
|
|
| def check_prompt_injection_breach(email: str, api_key: str) -> str: |
| """Check an email address for credential exposure sourced specifically |
| from prompt-injection attacks against AI agents, distinct from ordinary |
| phishing/malware-sourced breaches. |
| |
| Args: |
| email: Email address to check, e.g. agent@example.com |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-prompt-injection-breach |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| if not email or "@" not in email: |
| return _error_result("other", "a valid email address is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-prompt-injection-breach")) |
|
|
| data, error = _call("/v1/metered/prompt-injection-breach", {"email": email}, api_key) |
| if error: |
| return error |
|
|
| sessions = data.get("sessions", []) |
| severities = [s.get("severity") for s in sessions if s.get("severity")] |
| highest = max(severities, key=lambda s: {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1}.get(s, 0), default=None) |
| outcome, action = _outcome_for_severity(highest, found=data.get("found", False)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=["prompt_injection_sourced_breach"] if data.get("found") else [], |
| evidence=sessions, |
| scope=f"Checked {email} against stolen-session records flagged as prompt-injection-sourced " |
| "(heuristic keyword classifier over dump-announcement text, not confirmed attribution).", |
| ) |
|
|
|
|
| def check_tech_stack_cve(tech_stack: str, api_key: str) -> str: |
| """Check a declared AI agent framework / tech stack for CISA KEV or |
| high-EPSS CVEs actively being exploited. Covers AI agent orchestration |
| frameworks (LangChain, CrewAI, AutoGPT, Flowise, n8n self-hosted) and |
| common companion infrastructure (Nacos, MinIO). |
| |
| Args: |
| tech_stack: Comma-separated list of products, e.g. "langchain, nacos, minio" |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-tech-stack-cve |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| stack_list = [t.strip() for t in tech_stack.split(",") if t.strip()] |
| if not stack_list: |
| return _error_result("other", "tech_stack is required (comma-separated list of products).") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-tech-stack-cve")) |
|
|
| data, error = _call("/v1/metered/tech-stack-cve", {"tech_stack": stack_list}, api_key) |
| if error: |
| return error |
|
|
| matched = data.get("all_matches", []) |
| critical = data.get("critical_cves", []) |
| highest = "CRITICAL" if critical else ("HIGH" if matched else None) |
| outcome, action = _outcome_for_severity(highest, found=bool(matched)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=[c.get("cve_id", "unknown") for c in critical] if critical else [], |
| evidence=matched, |
| scope=f"Checked {data.get('tech_stack_queried', stack_list)} against CISA KEV and high-EPSS-score CVEs.", |
| ) |
|
|
|
|
| def check_bulk_identity_risk(domain: str, agent_emails: str, api_key: str) -> str: |
| """Hierarchical org + AI-agent-identity risk scoring for a domain and up |
| to 5 agent/service-account identities. A critically-exposed agent |
| automatically elevates the organizational risk rating. |
| |
| Args: |
| domain: Organization domain to score, e.g. example.com |
| agent_emails: Comma-separated agent/service-account emails, up to 5, e.g. "ai-agent@example.com, svc@example.com" |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-bulk-identity-risk |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| if not domain: |
| return _error_result("other", "domain is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-bulk-identity-risk")) |
|
|
| agents = [e.strip() for e in agent_emails.split(",") if e.strip()][:5] |
|
|
| data, error = _call("/v1/metered/bulk-identity-risk", {"targets": [{"domain": domain, "agents": agents}]}, api_key) |
| if error: |
| return error |
|
|
| results = data.get("results", []) |
| critical = data.get("critical_count", 0) |
| high = data.get("high_count", 0) |
| highest = "CRITICAL" if critical else ("HIGH" if high else None) |
| outcome, action = _outcome_for_severity(highest, found=bool(critical or high)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=[r.get("domain", "unknown") for r in results |
| if r.get("domain_risk") in ("CRITICAL", "HIGH") or r.get("highest_agent_risk") in ("CRITICAL", "HIGH")], |
| evidence=results, |
| scope=f"Scored {data.get('queried', len(results))} domain(s) for combined breach/infostealer/session/CVE risk.", |
| ) |
|
|
|
|
| def check_oauth_watchlist(email: str, api_key: str) -> str: |
| """Check an email for OAuth-connected-app exposure: apps matched against |
| breach history, plus stolen OAuth/session tokens found in criminal |
| stealer logs. |
| |
| Args: |
| email: Email address to check for OAuth app exposure, e.g. agent@example.com |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-oauth-watchlist |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| if not email or "@" not in email: |
| return _error_result("other", "a valid email address is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-oauth-watchlist")) |
|
|
| data, error = _call("/v1/metered/oauth-watchlist", {"email": email}, api_key) |
| if error: |
| return error |
|
|
| matched_apps = data.get("matched_apps", []) |
| stolen_tokens = data.get("stolen_tokens", []) |
| outcome, action = _outcome_for_severity(data.get("highest_severity"), found=bool(matched_apps or stolen_tokens)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=(["oauth_app_breach_match"] if matched_apps else []) + (["stolen_oauth_token"] if stolen_tokens else []), |
| evidence=matched_apps + stolen_tokens, |
| scope=f"Checked {email} against HIBP breach history for OAuth-app matches and RelayShield's " |
| "stealer-log corpus for stolen session/OAuth tokens.", |
| observed_at=data.get("checked_at"), |
| ) |
|
|
|
|
| def check_supply_chain(vendor_domains: str, api_key: str) -> str: |
| """Check up to 10 vendor domains for breach and infostealer exposure — |
| third-party and supply-chain risk inherited through vendor relationships. |
| |
| Args: |
| vendor_domains: Comma-separated vendor domains, up to 10, e.g. "vendor1.com, vendor2.com" |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-supply-chain |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| domains = [d.strip() for d in vendor_domains.split(",") if d.strip()][:10] |
| if not domains: |
| return _error_result("other", "vendor_domains is required (comma-separated list, up to 10).") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-supply-chain")) |
|
|
| data, error = _call("/v1/metered/supply-chain", {"vendor_domains": domains}, api_key) |
| if error: |
| return error |
|
|
| results = data.get("results", []) |
| highest = data.get("highest_risk") |
| outcome, action = _outcome_for_severity(highest if highest != "CLEAN" else None, found=highest not in (None, "CLEAN")) |
| return _structured_result( |
| outcome, action, |
| reason_codes=data.get("critical_vendors", []) + data.get("high_risk_vendors", []), |
| evidence=results, |
| scope=f"Checked {data.get('domains_checked', 0)} vendor domain(s) for breach and infostealer exposure.", |
| observed_at=data.get("checked_at"), |
| ) |
|
|
|
|
| def check_session_risk(email: str, api_key: str) -> str: |
| """Check an email for active or reusable stolen session material |
| (cookies, tokens) found in criminal stealer logs — exposure that can |
| bypass MFA/authentication entirely, not just a password. |
| |
| Args: |
| email: Email address to check for active session/AiTM exposure, e.g. agent@example.com |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-session-risk |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| if not email or "@" not in email: |
| return _error_result("other", "a valid email address is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-session-risk")) |
|
|
| data, error = _call("/v1/metered/session-risk", {"email": email}, api_key) |
| if error: |
| return error |
|
|
| sessions = data.get("sessions", []) |
| outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=[s.get("service_category", "unknown") for s in sessions], |
| evidence=sessions, |
| scope=f"Checked {email} against RelayShield's stolen-session corpus for reusable session/cookie material.", |
| ) |
|
|
|
|
| def check_nhi_exposure(domain: str, vendor_domains: str, api_key: str) -> str: |
| """Check a domain, or up to 10 vendor domains, for exposed non-human- |
| identity (NHI) credentials — API keys, service-account tokens, personal |
| access tokens, and other machine identities found in criminal stealer logs. |
| |
| Args: |
| domain: Your own domain to check. Leave blank if only using vendor_domains. |
| vendor_domains: Comma-separated vendor/supply-chain domains, up to 10. Leave blank if only using domain. |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-nhi-exposure |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| domain = (domain or "").strip() |
| domains = [d.strip() for d in (vendor_domains or "").split(",") if d.strip()][:10] |
| if not domain and not domains: |
| return _error_result("other", "domain or vendor_domains is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-nhi-exposure")) |
|
|
| payload: dict = {} |
| if domain: |
| payload["domain"] = domain |
| if domains: |
| payload["vendor_domains"] = domains |
|
|
| data, error = _call("/v1/metered/nhi-exposure", payload, api_key) |
| if error: |
| return error |
|
|
| findings = data.get("findings", []) |
| outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=[f.get("type", "unknown") for f in findings], |
| evidence=findings, |
| scope=f"Checked {data.get('domains_checked', 0)} domain(s) against RelayShield's stealer-log corpus " |
| "for exposed non-human-identity credentials.", |
| observed_at=data.get("checked_at"), |
| ) |
|
|
|
|
| def check_secret_scan(domain: str, vendor_domains: str, api_key: str) -> str: |
| """Check a domain, or up to 5 vendor domains, for secrets and |
| credentials exposed in public GitHub repositories. |
| |
| Args: |
| domain: Your own domain to check. Leave blank if only using vendor_domains. |
| vendor_domains: Comma-separated vendor domains, up to 5. Leave blank if only using domain. |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-secret-scan |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| domain = (domain or "").strip() |
| domains = [d.strip() for d in (vendor_domains or "").split(",") if d.strip()][:5] |
| if not domain and not domains: |
| return _error_result("other", "domain or vendor_domains is required.") |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-secret-scan")) |
|
|
| payload: dict = {} |
| if domain: |
| payload["domain"] = domain |
| if domains: |
| payload["vendor_domains"] = domains |
|
|
| data, error = _call("/v1/metered/secret-scan", payload, api_key) |
| if error: |
| return error |
|
|
| findings = data.get("findings", []) |
| outcome, action = _outcome_for_severity(data.get("highest_severity"), found=data.get("found", False)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=[f.get("severity", "unknown") for f in findings], |
| evidence=findings, |
| scope=f"Checked {data.get('domains_checked', 0)} domain(s) against public GitHub repositories " |
| "for exposed secrets.", |
| observed_at=data.get("checked_at"), |
| ) |
|
|
|
|
| |
| |
| |
| |
| |
| |
| _DEMO_API_KEY = "rs_live_9797cff2e80b407eae16a12c8faac71e" |
|
|
|
|
| def check_llm_credential_exposure(domain: str, api_key: str = "") -> str: |
| """Check a domain for exposed LLM/AI provider API keys (OpenAI, Anthropic, |
| Google, Groq, xAI, Replicate) in criminal stealer logs -- LLMjacking, a |
| fast-growing threat where a leaked key becomes a live, uncapped billing |
| liability rather than just a data exposure. Real incidents have run from |
| tens of thousands of dollars per day to a $500K single-month bill from |
| one leaked, unthrottled key. |
| |
| Args: |
| domain: Domain to check, e.g. example.com |
| api_key: Your RelayShield API key. Leave blank to try this specific |
| tool for free (shared demo quota, 20 calls/day across all users -- |
| get your own key at https://api.relayshield.net/developers#ep-llm-credential-exposure |
| for unlimited use and access to the other 12 tools on this server). |
| |
| Returns: |
| A structured result: outcome/recommended_action plus evidence and reason codes. |
| """ |
| if not domain: |
| return _error_result("other", "domain is required.") |
|
|
| used_key = (api_key or "").strip() or _DEMO_API_KEY |
| data, error = _call("/v1/metered/llm-credential-exposure", {"domain": domain}, used_key) |
| if error: |
| return error |
|
|
| findings = data.get("findings", []) |
| providers = data.get("providers_affected", []) |
| outcome, action = _outcome_for_severity(data.get("highest_severity"), found=bool(findings)) |
| return _structured_result( |
| outcome, action, |
| reason_codes=providers, |
| evidence=findings, |
| scope=f"Checked {domain} against RelayShield's stealer-log corpus for exposed LLM/AI provider " |
| f"API keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate).{' Used the shared free demo key -- get your own for unlimited use.' if used_key == _DEMO_API_KEY else ''}", |
| observed_at=data.get("checked_at"), |
| ) |
|
|
|
|
| def check_agent_risk_summary(domain: str, email: str, tech_stack: str, api_key: str) -> str: |
| """Composite risk check -- one call instead of four. Runs breach exposure, |
| LLM credential exposure, and tech-stack CVE checks together and returns a |
| single fused risk verdict, so a calling agent doesn't have to make |
| multiple tool calls and reason about how to combine their results itself. |
| |
| Args: |
| domain: Domain to check (used for LLM credential exposure and tech-stack CVE lookup). Leave blank to skip these. |
| email: Email address to check for breach exposure. Leave blank to skip this check. |
| tech_stack: Comma-separated tech stack for CVE matching, e.g. "langchain, nacos". Leave blank to skip. |
| api_key: Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-agent-risk-summary |
| |
| Returns: |
| A structured result combining all sub-checks that were run, with the |
| highest severity found across them driving the overall outcome. |
| """ |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required. Get one at https://api.relayshield.net/developers#ep-agent-risk-summary")) |
| if not domain and not email and not tech_stack: |
| return _error_result("other", "at least one of domain, email, or tech_stack is required.") |
|
|
| severity_rank = {"CRITICAL": 4, "HIGH": 3, "MEDIUM": 2, "LOW": 1} |
| sub_results: dict = {} |
| highest_seen = None |
| all_evidence: list = [] |
| reason_codes: list = [] |
| any_error = None |
|
|
| def _note(check_name: str, data: dict | None, err: str | None, sev_key: str, evidence_key: str): |
| nonlocal highest_seen, any_error |
| if err: |
| sub_results[check_name] = {"error": True} |
| any_error = any_error or err |
| return |
| sub_results[check_name] = data |
| sev = data.get(sev_key) |
| if sev and severity_rank.get(sev, 0) > severity_rank.get(highest_seen, 0): |
| highest_seen = sev |
| ev = data.get(evidence_key) or [] |
| if ev: |
| all_evidence.extend(ev if isinstance(ev, list) else [ev]) |
|
|
| if email and "@" in email: |
| data, err = _call("/v1/metered/breach", {"email": email}, api_key) |
| _note("breach", data, err, "highest_severity", "breaches") |
| if not err and data.get("breach_count"): |
| reason_codes.append("email_breach_exposure") |
|
|
| if domain: |
| data, err = _call("/v1/metered/llm-credential-exposure", {"domain": domain}, api_key) |
| _note("llm_credential_exposure", data, err, "highest_severity", "findings") |
| if not err and data.get("found"): |
| reason_codes.append("llm_credential_exposure") |
|
|
| if tech_stack.strip(): |
| stack_list = [t.strip() for t in tech_stack.split(",") if t.strip()] |
| data, err = _call("/v1/metered/tech-stack-cve", {"tech_stack": stack_list}, api_key) |
| if not err: |
| highest = "CRITICAL" if data.get("critical_count") else ("HIGH" if data.get("total_matches") else None) |
| sub_results["tech_stack_cve"] = data |
| if highest and severity_rank.get(highest, 0) > severity_rank.get(highest_seen, 0): |
| highest_seen = highest |
| if data.get("critical_count"): |
| reason_codes.append("tech_stack_cve_critical") |
| all_evidence.extend(data.get("critical_cves", [])) |
| else: |
| sub_results["tech_stack_cve"] = {"error": True} |
| any_error = any_error or err |
|
|
| outcome, action = _outcome_for_severity(highest_seen, found=bool(reason_codes)) |
| result = _structured_result( |
| outcome, action, |
| reason_codes=reason_codes, |
| evidence=all_evidence[:20], |
| scope=f"Composite check across: {', '.join(k for k in sub_results if sub_results[k] and not sub_results[k].get('error'))}.", |
| ) |
| if any_error and not reason_codes and outcome == "no_known_finding": |
| |
| |
| parsed = json.loads(result) |
| parsed["coverage"]["complete"] = False |
| parsed["coverage"]["scope"] += " One or more sub-checks failed to complete -- see logs." |
| return json.dumps(parsed, indent=2) |
| return result |
|
|
|
|
| def get_stix_indicators(limit: int, api_key: str) -> str: |
| """Fetch RelayShield's IOC corpus as STIX 2.1 Indicator objects via the |
| TAXII 2.1 feed -- for agents/tools that consume threat intel in STIX |
| format directly (SIEM/TIP ingestion, correlation engines) rather than |
| RelayShield's own JSON shape. Requires a TI (Threat Intelligence) |
| subscription API key, not just any metered key -- a standard metered |
| key will get an auth error here, that's expected. |
| |
| Args: |
| limit: Number of STIX objects to return, up to 2000. Defaults to 50 if 0 or not set. |
| api_key: Your RelayShield TI subscription API key. Get one at https://api.relayshield.net/developers#ti-subscription |
| |
| Returns: |
| A structured result with STIX 2.1 Indicator objects as evidence. |
| """ |
| if not api_key: |
| return _error_result("auth", _scrub("api_key is required (must be a TI subscription key). Get one at https://api.relayshield.net/developers#ti-subscription")) |
|
|
| page_size = limit if limit and 0 < limit <= 2000 else 50 |
| try: |
| resp = requests.get( |
| f"{API_BASE_URL}/v1/intel/taxii/collections/iocs/objects/", |
| params={"limit": page_size}, |
| headers=_headers(api_key), |
| timeout=20, |
| ) |
| except requests.RequestException as exc: |
| return _error_result("upstream", f"TAXII request failed: {exc}", retryable=True) |
|
|
| if resp.status_code in (401, 403): |
| return _error_result("auth", f"TAXII endpoint returned {resp.status_code} -- this endpoint requires a TI subscription key, not a standard metered key.", retryable=False) |
| if resp.status_code >= 400: |
| return _error_result("other", f"TAXII endpoint returned {resp.status_code}: {resp.text[:200]}", retryable=resp.status_code >= 500) |
|
|
| try: |
| body = resp.json() |
| except ValueError: |
| return _error_result("malformed_response", "TAXII endpoint returned non-JSON content.", retryable=False) |
|
|
| objects = body.get("objects", []) |
| return _structured_result( |
| "finding" if objects else "no_known_finding", |
| "review" if objects else "allow", |
| reason_codes=["stix_indicator_bundle"] if objects else [], |
| evidence=objects, |
| scope=f"Fetched {len(objects)} STIX 2.1 Indicator object(s) from RelayShield's TAXII 2.1 feed " |
| "(/v1/intel/taxii/collections/iocs/objects/).", |
| ) |
|
|
|
|
| def check_server_status() -> str: |
| """Check which RelayShield tools and data sources are currently live on |
| this MCP server -- a lightweight introspection call for an agent (or a |
| human debugging) to confirm connectivity and see what's available before |
| making a real check. Requires no API key. |
| |
| Returns: |
| A structured result listing available tools and a live upstream connectivity check. |
| """ |
| tool_names = [t["name"] for t in _SERVER_CARD["tools"]] |
| try: |
| resp = requests.get(f"{API_BASE_URL}/developers", timeout=8) |
| upstream_reachable = resp.status_code < 500 |
| except requests.RequestException: |
| upstream_reachable = False |
|
|
| return _structured_result( |
| "no_known_finding", "allow", |
| evidence=[{"tools_available": tool_names, "tool_count": len(tool_names), "upstream_reachable": upstream_reachable}], |
| scope="Introspection check -- lists available tools and confirms the RelayShield API is reachable. " |
| "Not a security finding; 'outcome' is always no_known_finding for this tool.", |
| ) |
|
|
|
|
| mcp_risk_tab = gr.Interface( |
| fn=check_mcp_server_risk, |
| inputs=[ |
| gr.Textbox(label="MCP Server URL", placeholder="https://example.com/mcp"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-mcp-registry-risk")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="MCP Server Risk Check", |
| description="Typosquat, IOC-corpus, and registration-age risk check for an MCP server URL.", |
| ) |
|
|
| breach_tab = gr.Interface( |
| fn=check_prompt_injection_breach, |
| inputs=[ |
| gr.Textbox(label="Email Address", placeholder="agent@example.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-prompt-injection-breach")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Prompt-Injection Breach Check", |
| description="Checks for credential breaches sourced from prompt-injection attacks against AI agents.", |
| ) |
|
|
| tech_stack_tab = gr.Interface( |
| fn=check_tech_stack_cve, |
| inputs=[ |
| gr.Textbox(label="Tech Stack (comma-separated)", placeholder="langchain, nacos, minio"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-tech-stack-cve")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Tech Stack CVE Check", |
| description="CISA KEV / high-EPSS CVEs targeting a declared AI agent framework or tech stack.", |
| ) |
|
|
| bulk_identity_tab = gr.Interface( |
| fn=check_bulk_identity_risk, |
| inputs=[ |
| gr.Textbox(label="Domain", placeholder="example.com"), |
| gr.Textbox(label="Agent Emails (comma-separated, up to 5)", placeholder="ai-agent@example.com, svc@example.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-bulk-identity-risk")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Bulk Identity Risk Check", |
| description="Hierarchical org + AI-agent-identity risk scoring for a domain and its agent/service-account identities.", |
| ) |
|
|
| oauth_watchlist_tab = gr.Interface( |
| fn=check_oauth_watchlist, |
| inputs=[ |
| gr.Textbox(label="Email Address", placeholder="agent@example.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-oauth-watchlist")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="OAuth Watchlist Check", |
| description="OAuth-connected-app breach exposure plus stolen OAuth/session tokens found in criminal stealer logs.", |
| ) |
|
|
| supply_chain_tab = gr.Interface( |
| fn=check_supply_chain, |
| inputs=[ |
| gr.Textbox(label="Vendor Domains (comma-separated, up to 10)", placeholder="vendor1.com, vendor2.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-supply-chain")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Supply Chain Risk Check", |
| description="Breach and infostealer exposure check for up to 10 vendor domains.", |
| ) |
|
|
| session_risk_tab = gr.Interface( |
| fn=check_session_risk, |
| inputs=[ |
| gr.Textbox(label="Email Address", placeholder="agent@example.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-session-risk")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Session Risk Check", |
| description="Active or reusable stolen session (cookie/token) exposure that can bypass MFA entirely.", |
| ) |
|
|
| nhi_exposure_tab = gr.Interface( |
| fn=check_nhi_exposure, |
| inputs=[ |
| gr.Textbox(label="Domain (optional)", placeholder="example.com"), |
| gr.Textbox(label="Vendor Domains (comma-separated, up to 10, optional)", placeholder="vendor1.com, vendor2.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-nhi-exposure")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Non-Human Identity Exposure Check", |
| description="API keys, service-account tokens, PATs, and other machine credentials found in criminal stealer logs.", |
| ) |
|
|
| secret_scan_tab = gr.Interface( |
| fn=check_secret_scan, |
| inputs=[ |
| gr.Textbox(label="Domain (optional)", placeholder="example.com"), |
| gr.Textbox(label="Vendor Domains (comma-separated, up to 5, optional)", placeholder="vendor1.com, vendor2.com"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-secret-scan")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Secret Scan Check", |
| description="Secrets and credentials exposed in public GitHub repositories.", |
| ) |
|
|
| llm_credential_tab = gr.Interface( |
| fn=check_llm_credential_exposure, |
| inputs=[ |
| gr.Textbox(label="Domain", placeholder="example.com"), |
| gr.Textbox(label="RelayShield API Key (optional — try free, 20 calls/day shared demo)", type="password", placeholder=_scrub("Leave blank to try for free, or get your own key at api.relayshield.net/developers#ep-llm-credential-exposure")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="LLM Credential Exposure (LLMjacking)", |
| description="Exposed OpenAI/Anthropic/Google/Groq/xAI/Replicate API keys in criminal stealer logs — a live, uncapped billing liability. Free to try, no key required.", |
| ) |
|
|
| agent_risk_summary_tab = gr.Interface( |
| fn=check_agent_risk_summary, |
| inputs=[ |
| gr.Textbox(label="Domain (optional)", placeholder="example.com"), |
| gr.Textbox(label="Email Address (optional)", placeholder="agent@example.com"), |
| gr.Textbox(label="Tech Stack (comma-separated, optional)", placeholder="langchain, nacos"), |
| gr.Textbox(label="RelayShield API Key", type="password", placeholder=_scrub("Get one at api.relayshield.net/developers#ep-agent-risk-summary")), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="Agent Risk Summary (Composite)", |
| description="One call instead of four — combines breach, LLM credential exposure, and tech-stack CVE checks into a single fused verdict.", |
| ) |
|
|
| stix_tab = gr.Interface( |
| fn=get_stix_indicators, |
| inputs=[ |
| gr.Number(label="Limit (up to 2000, default 50)", value=50), |
| gr.Textbox(label="RelayShield API Key (TI subscription required)", type="password", placeholder="Requires a TI subscription key, not a standard metered key"), |
| ], |
| outputs=gr.Textbox(label="Result"), |
| title="STIX 2.1 Indicators (TAXII)", |
| description="Fetch RelayShield's IOC corpus as STIX 2.1 Indicator objects for SIEM/TIP ingestion. Requires a TI subscription key.", |
| ) |
|
|
| status_tab = gr.Interface( |
| fn=check_server_status, |
| inputs=[], |
| outputs=gr.Textbox(label="Result"), |
| title="Server Status", |
| description="Lists available tools and confirms upstream connectivity. No API key required.", |
| ) |
|
|
| demo = gr.TabbedInterface( |
| [ |
| mcp_risk_tab, breach_tab, tech_stack_tab, bulk_identity_tab, |
| oauth_watchlist_tab, supply_chain_tab, session_risk_tab, nhi_exposure_tab, secret_scan_tab, |
| llm_credential_tab, agent_risk_summary_tab, stix_tab, status_tab, |
| ], |
| [ |
| "MCP Server Risk", "Prompt-Injection Breach", "Tech Stack CVE", "Bulk Identity Risk", |
| "OAuth Watchlist", "Supply Chain", "Session Risk", "NHI Exposure", "Secret Scan", |
| "LLM Credential Exposure", "Agent Risk Summary", "STIX Indicators", "Server Status", |
| ], |
| title="RelayShield Agentic Attack Surface", |
| ) |
|
|
| |
| |
| |
| |
| |
| _SERVER_CARD_RAW = { |
| "serverInfo": { |
| "name": "RelayShield Agentic Attack Surface", |
| "version": "3.0.1", |
| }, |
| "authentication": { |
| "required": False, |
| }, |
| "tools": [ |
| { |
| "name": "check_mcp_server_risk", |
| "description": "Check an MCP server URL for typosquat risk, presence in RelayShield's criminal IOC corpus, and domain-registration age. Returns a structured outcome/recommended_action result with evidence.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "server_url": {"type": "string", "description": "Full URL of the MCP server to check, e.g. https://example.com/mcp"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-mcp-registry-risk"}, |
| }, |
| "required": ["server_url", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_prompt_injection_breach", |
| "description": "Check an email address for credential exposure sourced specifically from prompt-injection attacks against AI agents, distinct from ordinary phishing/malware-sourced breaches.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "email": {"type": "string", "description": "Email address to check, e.g. agent@example.com"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-prompt-injection-breach"}, |
| }, |
| "required": ["email", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_tech_stack_cve", |
| "description": "Check a declared AI agent framework / tech stack for CISA KEV or high-EPSS CVEs actively being exploited. Covers AI agent orchestration frameworks and common companion infrastructure.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "tech_stack": {"type": "string", "description": "Comma-separated list of products, e.g. \"langchain, nacos, minio\""}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-tech-stack-cve"}, |
| }, |
| "required": ["tech_stack", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_bulk_identity_risk", |
| "description": "Hierarchical org + AI-agent-identity risk scoring for a domain and up to 5 agent/service-account identities. A critically-exposed agent automatically elevates the organizational risk rating.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "domain": {"type": "string", "description": "Organization domain to score, e.g. example.com"}, |
| "agent_emails": {"type": "string", "description": "Comma-separated agent/service-account emails, up to 5"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-bulk-identity-risk"}, |
| }, |
| "required": ["domain", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_oauth_watchlist", |
| "description": "Check an email for OAuth-connected-app exposure: apps matched against breach history, plus stolen OAuth/session tokens found in criminal stealer logs. Part of the oauth_watchlist/nhi_exposure/session_risk agent-authority family.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "email": {"type": "string", "description": "Email address to check, e.g. agent@example.com"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-oauth-watchlist"}, |
| }, |
| "required": ["email", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_supply_chain", |
| "description": "Check up to 10 vendor domains for breach and infostealer exposure — third-party and supply-chain risk inherited through vendor relationships.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "vendor_domains": {"type": "string", "description": "Comma-separated vendor domains, up to 10"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-supply-chain"}, |
| }, |
| "required": ["vendor_domains", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_session_risk", |
| "description": "Check an email for active or reusable stolen session material (cookies, tokens) found in criminal stealer logs — exposure that can bypass MFA entirely. Part of the oauth_watchlist/nhi_exposure/session_risk agent-authority family.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "email": {"type": "string", "description": "Email address to check, e.g. agent@example.com"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-session-risk"}, |
| }, |
| "required": ["email", "api_key"], |
| }, |
| }, |
| { |
| "name": "check_nhi_exposure", |
| "description": "Check a domain, or up to 10 vendor domains, for exposed non-human-identity credentials — API keys, service-account tokens, PATs, and other machine identities. Part of the oauth_watchlist/nhi_exposure/session_risk agent-authority family.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "domain": {"type": "string", "description": "Your own domain to check (optional if vendor_domains supplied)"}, |
| "vendor_domains": {"type": "string", "description": "Comma-separated vendor domains, up to 10 (optional if domain supplied)"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-nhi-exposure"}, |
| }, |
| "required": ["api_key"], |
| }, |
| }, |
| { |
| "name": "check_secret_scan", |
| "description": "Check a domain, or up to 5 vendor domains, for secrets and credentials exposed in public GitHub repositories.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "domain": {"type": "string", "description": "Your own domain to check (optional if vendor_domains supplied)"}, |
| "vendor_domains": {"type": "string", "description": "Comma-separated vendor domains, up to 5 (optional if domain supplied)"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-secret-scan"}, |
| }, |
| "required": ["api_key"], |
| }, |
| }, |
| { |
| "name": "check_llm_credential_exposure", |
| "description": "Check a domain for exposed LLM/AI provider API keys (OpenAI, Anthropic, Google, Groq, xAI, Replicate) in criminal stealer logs -- LLMjacking, a live billing liability, not just a data exposure. Free to try (no api_key required, shared demo quota); pass your own key for unlimited use.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "domain": {"type": "string", "description": "Domain to check, e.g. example.com"}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Optional -- leave blank to try for free (shared 20/day demo quota). Get your own at https://api.relayshield.net/developers#ep-llm-credential-exposure"}, |
| }, |
| "required": ["domain"], |
| }, |
| }, |
| { |
| "name": "check_agent_risk_summary", |
| "description": "Composite check -- one call instead of four. Runs breach exposure, LLM credential exposure, and tech-stack CVE checks together and returns a single fused risk verdict.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "domain": {"type": "string", "description": "Domain to check (LLM credential exposure + tech-stack CVE). Optional."}, |
| "email": {"type": "string", "description": "Email to check for breach exposure. Optional."}, |
| "tech_stack": {"type": "string", "description": "Comma-separated tech stack for CVE matching. Optional."}, |
| "api_key": {"type": "string", "description": "Your RelayShield API key. Get one at https://api.relayshield.net/developers#ep-agent-risk-summary"}, |
| }, |
| "required": ["api_key"], |
| }, |
| }, |
| { |
| "name": "get_stix_indicators", |
| "description": "Fetch RelayShield's IOC corpus as STIX 2.1 Indicator objects via the TAXII 2.1 feed, for SIEM/TIP ingestion. Requires a TI subscription API key, not a standard metered key.", |
| "inputSchema": { |
| "type": "object", |
| "properties": { |
| "limit": {"type": "integer", "description": "Number of STIX objects to return, up to 2000. Defaults to 50."}, |
| "api_key": {"type": "string", "description": "Your RelayShield TI subscription API key. Get one at https://api.relayshield.net/developers#ti-subscription"}, |
| }, |
| "required": ["api_key"], |
| }, |
| }, |
| { |
| "name": "check_server_status", |
| "description": "Lists available tools on this MCP server and confirms upstream RelayShield API connectivity. No API key required.", |
| "inputSchema": { |
| "type": "object", |
| "properties": {}, |
| "required": [], |
| }, |
| }, |
| ], |
| "resources": [], |
| "prompts": [], |
| } |
|
|
| |
| |
| |
| _SERVER_CARD = json.loads(_scrub(json.dumps(_SERVER_CARD_RAW))) |
|
|
| |
| |
| |
| for _fn in ( |
| check_mcp_server_risk, check_prompt_injection_breach, check_tech_stack_cve, |
| check_bulk_identity_risk, check_oauth_watchlist, check_supply_chain, |
| check_session_risk, check_nhi_exposure, check_secret_scan, |
| check_llm_credential_exposure, check_agent_risk_summary, |
| get_stix_indicators, check_server_status, |
| ): |
| if _fn.__doc__: |
| _fn.__doc__ = _scrub(_fn.__doc__) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| if __name__ == "__main__": |
| import threading |
|
|
| from fastapi.responses import JSONResponse |
|
|
| demo.launch( |
| server_name="0.0.0.0", server_port=7860, mcp_server=True, |
| ssr_mode=False, prevent_thread_lock=True, |
| ) |
|
|
| @demo.app.get("/.well-known/mcp/server-card.json") |
| async def server_card(): |
| return JSONResponse(_SERVER_CARD) |
|
|
| threading.Event().wait() |
|
|