Dark Web Threat Intel Dashboard

Build a live dark web threat intelligence dashboard for medical devices.

Introduction

30 Second Summary

Medical devices are increasingly targeted by ransomware groups and nation-state actors. When an attack hits a device manufacturer, security teams scramble to understand the threat landscape across dozens of scattered dark web sources.

In this project, you will build a threat intelligence pipeline that queries live dark web data relevant to Stryker medical devices and presents it through a role-segregated dashboard tailored to different security stakeholders.

What You'll Build

You will demo a locally-running dashboard where you pull live threat data from 16 dark web sources via a single MCP server, then switch between three persona-specific views that each tell a different story about the same underlying intelligence.

By the end of this project, you'll have:

  • A working darknet-mcp-server connected to Claude Desktop, letting you run natural-language threat investigations across ransomware trackers, breach databases, and exploit repositories with 66 tools at your fingertips.
  • A Streamlit dashboard that ingests structured threat data and renders three persona-specific views (Product Team, Leadership, and Pentesting Team), each surfacing only the data that audience needs for decision-making.
  • A repeatable workflow for collecting dark web intelligence on medical device manufacturers, normalizing heterogeneous JSON responses into a unified data model, and presenting actionable insights to different stakeholders.
  • Secret Mission: Add a real-time alert feed that highlights new threats detected since the last dashboard refresh.

Are there any prerequisites?

No prior setup is needed. This guide walks you through installing Node.js, Python, and all required tools from scratch.

Before We Start

Before we dive in, let's lock in what you're about to build and why it matters.

You're building a threat intelligence pipeline that pulls live data from dark web sources and presents it to three different stakeholders: a Product Team, Leadership, and a Pentesting Team. Each audience needs a different slice of the same raw intelligence to make decisions in their own context.

Install Node.js, Python, and the Darknet MCP Server

For the dashboard to pull live threat intelligence from the dark web, it needs two runtimes working together. Node.js runs the darknet-mcp-server that queries 16 data sources. Python powers the collection script and the Streamlit dashboard you will build later.

In this step, you will install both runtimes, create a project directory, and confirm the MCP server connects to all 16 dark web intelligence sources without needing a single API key.

In this step, get ready to:
  • Install and verify Node.js LTS (v24).
  • Set up Python 3 with a virtual environment.
  • Verify the darknet-mcp-server prints all 66 tools.
Install Node.js LTS

The darknet-mcp-server requires Node.js 18 or newer. You will install v24 LTS (codename Krypton), which is the current long-term support release.

  • Open your terminal. Press Cmd+Space (macOS) or the Windows key (Windows) to open your search bar.
  • Type Terminal (macOS) or PowerShell (Windows) and press Enter to open it.
  • Check whether Node.js is already installed by running this command:
node --version

✔️ I see version 24.x or higher

Node.js is ready. You already have the correct LTS version installed.

ⓧ I see an older version

You have an outdated version of Node.js. You need v24 or higher for the MCP server's dependencies.

  • Go to https://nodejs.org in your browser.
  • Click the LTS download button (look for v24 Krypton).
  • Run the installer and accept all defaults.
  • Close and reopen your terminal, then run node --version again to confirm it shows v24.x.

ⓧ Command not found

Node.js is not installed on your machine.

  • Go to https://nodejs.org in your browser.
  • Click the LTS download button (look for v24 Krypton).
  • Run the downloaded installer (.pkg on macOS, .msi on Windows) and accept all defaults.
  • Close and reopen your terminal, then run node --version again to confirm it shows v24.x.

What does the Node.js installer include?

The installer also sets up npm (the package manager) and npx (which runs packages without global installation). You will use npx to execute the darknet-mcp-server throughout this project.

Install Python and create your project environment

Python runs the data collection script and the Streamlit dashboard. You also need a virtual environment to keep this project's packages isolated from other Python projects on your machine.

  • Check whether Python 3 is already installed by running this command:
python3 --version

✔️ I see a version number

Python 3 is ready. Any Python 3.x version works for this project.

ⓧ Command not found

Python 3 is not installed.

  • Go to https://python.org/downloads in your browser.
  • Download the latest Python 3 installer for your operating system.
  • Run the installer. On Windows, check the box that says Add Python to PATH before clicking Install.
  • Close and reopen your terminal, then run python3 --version again to confirm it prints a version number.

Now create your project directory and virtual environment.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir threat-intel-dashboard
cd threat-intel-dashboard
  • Create a Python virtual environment by running this command:
python3 -m venv venv
  • Activate the virtual environment by running the command for your operating system:

macOS

source venv/bin/activate

Windows

venv\Scripts\activate
  • Confirm the virtual environment is active and your directory exists by running:
ls

You should see the venv folder listed in the output. Your terminal prompt should also show (venv) at the beginning, confirming the virtual environment is active.

Not seeing (venv) in your prompt?

Make sure you ran the activate command for your operating system. On macOS/Linux it is source venv/bin/activate and on Windows it is venv\Scripts\activate.

If you get a permissions error on Windows, you may need to allow script execution first. Run Set-ExecutionPolicy -Scope CurrentUser RemoteSigned in PowerShell, then try the activate command again.

Help me troubleshoot my virtual environment activation

Verify the darknet-mcp-server

The darknet-mcp-server is an MCP server that wraps 66 threat intelligence tools across 16 data sources into a single interface. You do not need to install it globally. The npx command downloads and runs it on the fly.

What is an MCP server?

The Model Context Protocol (MCP) is a standard that lets AI assistants call external tools. An MCP server exposes a set of tools that any MCP client (like Claude Desktop) can invoke.

In this project, you will also use the server's CLI mode to call those same tools from Python scripts.

  • Before you run this next command, what do you think will happen? The server has never been downloaded to your machine before.
  • List all available tools by running this command:
npx darknet-mcp-server --list

What does this command do?

The npx command downloads the darknet-mcp-server package (v0.1.1) from npm the first time you run it. The --list flag tells it to print every tool it exposes instead of starting as a server.

You should see all 66 tools printed to your terminal, organized across categories like Ransomware Intelligence, Breach Data (HIBP), Vulners, Tor Network, and more. No API keys are needed for any of these tools.

Why no API keys?

The darknet-mcp-server aggregates publicly accessible data sources. Tools like ransomwareBySector, breachList, and vulners_search query open APIs that do not require authentication. This means zero configuration to start pulling live threat intelligence.

Seeing a download prompt or error?

If npx asks you to confirm the download ("Need to install the following packages"), type y and press Enter.

If you see a "command not found" error for npx, go back to the Node.js installation step. The npx command is bundled with Node.js and should be available after a successful install and terminal restart.

Help me fix the darknet-mcp-server list command

Windows users: platform support note

The darknet-mcp-server is tested on macOS and Linux. If you are on Windows and encounter issues, consider running the project inside Windows Subsystem for Linux (WSL), which provides a Linux environment directly on Windows.

Your environment is ready. You have Node.js, Python, a virtual environment, and a verified connection to 16 dark web intelligence sources. Next up, you will run your first threat queries against healthcare targets and see real data come back.

Run Your First Stryker-Focused Threat Queries

Your environment is verified and ready to pull live threat data. Now it's time to see what that data actually looks like.

Before building a dashboard, you need to understand the raw material. Each of the 66 tools in the darknet-mcp-server returns data in its own JSON format. Running a few focused queries gives you a concrete feel for the response shapes you will normalize and visualize in later steps.

In this step, get ready to:
  • Query healthcare-sector ransomware activity and observe the response structure.
  • Search for Stryker-related breach data and CVE/exploit exposure.
  • Search dark web mentions and observe the normalization problem across different tools.
Query healthcare-sector ransomware activity

The ransomwareBySector tool filters ransomware victim data by industry. Querying the healthcare sector shows which groups are actively claiming victims in the space where Stryker operates.

Before you run this, what do you expect to see back from a live ransomware intelligence query? Will it be a single record or many? Think about what healthcare-sector targeting looks like in 2026.

  • Run your first threat intelligence query by executing this command:
npx darknet-mcp-server --tool ransomwareBySector '{"sector":"healthcare"}'

You should see a JSON response listing healthcare-sector ransomware victims. Each entry includes fields like victim name, ransomware group, date, and country.

What does this response tell you?

Each JSON object in the response represents one claimed victim. The key fields to notice are:

  • group identifies which ransomware gang claimed the attack.
  • name is the victim organization.
  • date and country provide temporal and geographic context for your dashboard later.

Seeing an empty response or an error?

  • Make sure you are connected to the internet. The tool queries live external APIs.
  • Verify the JSON parameter uses double quotes inside single quotes exactly as shown. Mismatched quotes cause parsing failures.
  • If you see a timeout, wait a moment and try again. The upstream API may be slow to respond.

Why is my ransomwareBySector query not returning results?

Search for Stryker breach and exploit data

Ransomware is only one threat category. Breach exposure tells you what data has already leaked, and CVE/exploit databases reveal known vulnerabilities that attackers can weaponize against Stryker devices.

  • Search for known data breaches associated with Stryker by running this command:
npx darknet-mcp-server --tool breachList '{"domain":"stryker.com"}'

You should see breach records from HIBP (Have I Been Pwned) with fields like breach name, data classes, and dates. Notice that the structure is completely different from the ransomware response.

  • Now search for vulnerabilities and exploits related to Stryker medical devices by running:
npx darknet-mcp-server --tool vulners_search '{"query":"stryker medical device"}'

This returns CVE and exploit records from the Vulners database. The response shape is different again. Fields like title, id, and severity indicators appear here instead of victim names and groups.

Why do these responses look so different?

Each tool wraps a different upstream data source. Ransomware data comes from ransomware.live, breach data from HIBP, and exploit data from Vulners. Each source has its own schema and field names.

This is the core challenge your dashboard will solve. You cannot compare ransomware victims to CVE records without first normalizing them into a common format.

Getting no results for the breach query?

  • An empty result from breachList may mean no public breach records exist for that exact domain. The collection script in the next step handles this gracefully.
  • For vulners_search, if you see no results, confirm your query string is wrapped in single quotes around the JSON object.

Help me understand why my breach or exploit queries are returning empty results.

Search dark web mentions via Ahmia

The final data source is the dark web itself. The tor_search_onion tool searches for .onion site mentions through Ahmia.fi without requiring a local Tor daemon.

  • Search for Stryker-related dark web mentions by running:
npx darknet-mcp-server --tool tor_search_onion '{"query":"stryker medical"}'

You should see search results from .onion sites with fields like title and url. Yet another completely different response shape from the same CLI tool.

What is the normalization problem?

You have now queried four different tools and received four different JSON structures. Ransomware victims have groups and dates. Breaches have data classes. Exploits have CVE IDs and severity scores. Dark web results have URLs and titles.

No human can reason about threats across these sources by reading raw JSON output. This is exactly the problem your collection layer and dashboard will solve in the next steps.

Seeing a Tor SOCKS5 proxy error?

  • The tor_search_onion tool uses Ahmia.fi (a clearnet search engine for .onion sites) and does NOT require a local Tor daemon. If you see a SOCKS5 error, your system may be interfering with the HTTP request. Check that no VPN or proxy is blocking outbound connections.
  • An empty result set is not an error. It just means Ahmia did not index any .onion pages matching that query.

Why am I getting a proxy error from tor_search_onion?

You have seen live threat intelligence flowing from four different sources through the same CLI. The data is real, but it is scattered across incompatible formats. Next up, you will build a Python collection layer that normalizes all of this into a single structured file your dashboard can consume.

Build the Threat Data Collection Layer

In Step 2, you saw raw JSON come back from four different dark web intelligence tools. The problem is clear: each tool returns a different data shape, making it impossible to compare or visualize the results side-by-side.

In this step, you will build a Python collection script that calls each tool programmatically, normalizes every response into a common schema, and writes the combined output to a single threat_data.json file your dashboard can consume.

In this step, get ready to:
  • Install Streamlit and requests as Python dependencies.
  • Build helper functions that shell out to the darknet-mcp-server CLI and normalize responses.
  • Create four collector functions covering ransomware, breaches, exploits, and dark web mentions.
Install dependencies and create the core helpers

Your collection script needs two packages: Streamlit for the dashboard you will build in the next step, and requests as a common HTTP utility.

  • Install both packages into your virtual environment by running:
pip install streamlit==1.61.1 requests
  • Confirm the install succeeded by checking for a version number:
python3 -c "import streamlit; print(streamlit.__version__)"

You should see 1.61.1 printed to your terminal.

Seeing a ModuleNotFoundError?

Make sure your virtual environment is active. You should see (venv) at the start of your terminal prompt. If not, activate it again with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows), then re-run the install command.

help me fix my virtual environment so pip installs go to the right place

Now you will create the collection script with the two helper functions that every collector will use.

  • Create a file called collect_intel.py in your project directory.
  • Add the following imports and helper functions:
import subprocess
import json
import os
from datetime import datetime


def run_mcp_tool(tool_name, params):
    """Run a darknet-mcp-server tool and return parsed JSON."""
    cmd = [
        "npx", "darknet-mcp-server",
        "--tool", tool_name,
        json.dumps(params)
    ]
    try:
        result = subprocess.run(
            cmd, capture_output=True, text=True, timeout=30
        )
        if result.returncode == 0 and result.stdout.strip():
            return json.loads(result.stdout)
    except (subprocess.TimeoutExpired, json.JSONDecodeError):
        pass
    return None


def normalize_entry(source, category, title, severity, raw_data):
    """Create a normalized threat intel entry."""
    return {
        "timestamp": datetime.utcnow().isoformat(),
        "source": source,
        "category": category,
        "title": title,
        "severity": severity,
        "raw_data": raw_data
    }

What does this code do?

  • run_mcp_tool builds the exact CLI command you used in Step 2 (npx darknet-mcp-server --tool <name> '<params>') and runs it via Python's subprocess module. It captures stdout, parses the JSON, and returns it as a Python dict.
  • normalize_entry stamps every piece of intelligence with a consistent schema: timestamp, source, category, title, severity, and the raw data. This common shape is what makes heterogeneous results comparable.
  • Save collect_intel.py.
  • Verify the helpers load without errors by running:
python3 -c "import collect_intel; print('Helpers loaded OK')"

You should see Helpers loaded OK with no tracebacks.

Seeing a SyntaxError?

Check that you copied the f-string quotes and brackets exactly. Python requires matching triple-quotes for docstrings and balanced braces in the return dict.

help me fix a SyntaxError in my collect_intel.py helper functions

Add the collector functions

Each collector function targets one category of dark web intelligence. They all follow the same pattern: call run_mcp_tool, iterate the results, and normalize each entry.

  • In collect_intel.py, add the ransomware collector below your normalize_entry function:
def collect_ransomware_intel():
    """Collect healthcare-sector ransomware intelligence."""
    entries = []
    data = run_mcp_tool("ransomwareBySector", {"sector": "healthcare"})
    if data:
        victims = data if isinstance(data, list) else data.get("data", [])
        for victim in victims[:20]:
            entries.append(normalize_entry(
                source="ransomware.live",
                category="ransomware_intel",
                title=f"{victim.get('group', 'Unknown')} claimed {victim.get('name', 'Unknown')}",
                severity="critical",
                raw_data=victim
            ))

    stryker_data = run_mcp_tool("ransomwareSearch", {"query": "stryker"})
    if stryker_data:
        victims = stryker_data if isinstance(stryker_data, list) else stryker_data.get("data", [])
        for victim in victims[:10]:
            entries.append(normalize_entry(
                source="ransomware.live",
                category="ransomware_intel",
                title=f"Stryker-related: {victim.get('group', 'Unknown')}",
                severity="critical",
                raw_data=victim
            ))
    return entries

What does this collector do?

This function makes two queries. First, it pulls all recent healthcare-sector ransomware victims (up to 20). Then it searches specifically for Stryker-related entries (up to 10). Both use the ransomware.live data source and tag every entry as critical severity.

  • Below the ransomware collector, add the breach and exploit collectors:
def collect_breach_intel():
    """Collect breach intelligence for stryker.com."""
    entries = []
    data = run_mcp_tool("breachList", {"domain": "stryker.com"})
    if data:
        breaches = data if isinstance(data, list) else data.get("data", [])
        for breach in breaches:
            entries.append(normalize_entry(
                source="HIBP",
                category="breach_intel",
                title=breach.get("Name", breach.get("name", "Unknown breach")),
                severity="high",
                raw_data=breach
            ))
    return entries


def collect_exploit_intel():
    """Collect exploit and vulnerability intelligence."""
    entries = []
    data = run_mcp_tool("vulners_search", {"query": "stryker medical device"})
    if data:
        results = data if isinstance(data, list) else data.get("data", [])
        for item in results[:15]:
            entries.append(normalize_entry(
                source="Vulners",
                category="exploit_intel",
                title=item.get("title", item.get("id", "Unknown exploit")),
                severity="high" if "exploit" in str(item).lower() else "medium",
                raw_data=item
            ))
    return entries

What do these collectors do?

  • collect_breach_intel queries HIBP for any known breaches associated with stryker.com. Each breach is tagged as high severity.
  • collect_exploit_intel searches the Vulners database for CVEs and exploits mentioning Stryker medical devices. Entries that contain the word "exploit" get high severity. Others get medium.
  • Save collect_intel.py and confirm all three collectors load cleanly:
python3 -c "import collect_intel; print('3 collectors loaded OK')"

You should see 3 collectors loaded OK with no errors.

  • Add the final collector below collect_exploit_intel:
def collect_darkweb_mentions():
    """Collect dark web mentions via Tor search."""
    entries = []
    data = run_mcp_tool("tor_search_onion", {"query": "stryker medical"})
    if data:
        results = data if isinstance(data, list) else data.get("data", [])
        for item in results[:10]:
            entries.append(normalize_entry(
                source="Ahmia.fi",
                category="darkweb_mentions",
                title=item.get("title", item.get("url", "Unknown .onion result")),
                severity="medium",
                raw_data=item
            ))
    return entries

What does this collector do?

This function searches Ahmia.fi (a clearnet search engine for .onion sites) for mentions of Stryker medical devices. Results get medium severity since dark web mentions indicate exposure but not necessarily active exploitation.

Assemble and run the collection pipeline

The four collectors exist, but nothing calls them yet. The main() function will orchestrate all four, combine the results into one dict, and write it to threat_data.json.

  • Add the main function and entry point at the bottom of collect_intel.py:
def main():
    print("Collecting threat intelligence...")

    all_intel = {
        "collection_timestamp": datetime.utcnow().isoformat(),
        "target": "stryker.com / healthcare medical devices",
        "ransomware_intel": collect_ransomware_intel(),
        "breach_intel": collect_breach_intel(),
        "exploit_intel": collect_exploit_intel(),
        "darkweb_mentions": collect_darkweb_mentions()
    }

    total = sum(
        len(all_intel[k]) for k in
        ["ransomware_intel", "breach_intel", "exploit_intel", "darkweb_mentions"]
    )

    output_file = "threat_data.json"
    with open(output_file, "w") as f:
        json.dump(all_intel, f, indent=2)

    print(f"Collection complete: {total} entries saved to {output_file}")
    print(f"  Ransomware: {len(all_intel['ransomware_intel'])}")
    print(f"  Breaches:   {len(all_intel['breach_intel'])}")
    print(f"  Exploits:   {len(all_intel['exploit_intel'])}")
    print(f"  Dark Web:   {len(all_intel['darkweb_mentions'])}")


if __name__ == "__main__":
    main()

What does main() do?

  • Calls all four collectors and combines results into a single dictionary keyed by category.
  • Adds a collection_timestamp so you always know when the data was pulled.
  • Writes the entire structure to threat_data.json with readable indentation.
  • Prints a summary showing how many entries were collected per category.
  • Save collect_intel.py.

Before you run the script, what do you expect to see? The script will shell out to the darknet-mcp-server four times. Each call takes up to 30 seconds. Think about whether every query will return data.

  • Run the full collection pipeline:
python3 collect_intel.py

After 30-60 seconds, you should see output like this:

Collecting threat intelligence...
Collection complete: 24 entries saved to threat_data.json
  Ransomware: 15
  Breaches:   2
  Exploits:   5
  Dark Web:   2

What should I see?

Your exact numbers will differ depending on live data available at the time of your query. Some categories may show 0 entries if a source is temporarily unavailable. As long as the script completes without errors and creates threat_data.json, you are on track.

  • Confirm the output file was created:
ls threat_data.json

You should see threat_data.json listed.

  • Inspect the structure to confirm it has categorized sections:
python3 -c "import json; d=json.load(open('threat_data.json')); print(list(d.keys()))"

You should see ['collection_timestamp', 'target', 'ransomware_intel', 'breach_intel', 'exploit_intel', 'darkweb_mentions'] printed. That is the unified data model your dashboard will consume.

Script hangs or times out?

Each tool call has a 30-second timeout. If your network is slow, the script may take up to 2 minutes. If it hangs longer, press Ctrl+C to stop it and re-run. The timeout=30 in run_mcp_tool ensures no single call blocks forever.

If you see 0 entries saved, check that npx darknet-mcp-server --tool ransomwareBySector '{"sector":"healthcare"}' still works on its own from Step 2.

help me debug why my collect_intel.py is returning 0 entries or timing out

✔️ Awesome, I've got everything!

Great. Make sure collect_intel.py is saved and threat_data.json exists in your project directory.

ⓧ I'd like to double check the full code

Your collect_intel.py should contain these sections in order. Scroll up and confirm each is present:

  • Imports (subprocess, json, os, datetime).
  • run_mcp_tool(tool_name, params) helper that runs npx darknet-mcp-server --tool via subprocess.
  • normalize_entry(source, category, title, severity, raw_data) helper returning a dict with timestamp, source, category, title, severity, and raw_data.
  • collect_ransomware_intel() calling ransomwareBySector and ransomwareSearch.
  • collect_breach_intel() calling breachList.
  • collect_exploit_intel() calling vulners_search.
  • collect_darkweb_mentions() calling tor_search_onion.
  • main() assembling all results and writing to threat_data.json.
  • if __name__ == "__main__": main() at the very end.

Your collection layer is working. Raw, scattered JSON from 16 dark web sources now flows into a single structured file with consistent fields. Next up, you will build the Streamlit dashboard that reads this file and presents three persona-specific views of the intelligence.

Build the Persona-Segregated Dashboard

You have structured threat data sitting in threat_data.json. Right now, if a CISO opens that file, they see a wall of JSON. If a pentester opens it, they have to manually grep for IOCs. The data is the same, but each audience needs a completely different lens.

In this step, you will build a Streamlit dashboard that loads the same threat_data.json and renders three persona-specific views. Leadership sees risk metrics and posture. The Product Team sees CVE exposure and device lifecycle context. The Pentesting Team sees raw IOCs, MITRE ATT&CK mappings, and exploit details.

In this step, get ready to:
  • Create a Streamlit dashboard file that loads threat data and displays a metadata sidebar.
  • Build a Leadership view with risk summary metrics, a ransomware group chart, and posture assessment.
  • Build Product Team and Pentesting Team views with CVE exposure, MITRE ATT&CK context, and raw IOC tables.
Create the dashboard skeleton

Every Streamlit app starts with page configuration and data loading. You will create dashboard.py with the imports, a cached data loader, and a sidebar that shows collection metadata at a glance.

  • Create a new file called dashboard.py in your project folder and add the following imports, page configuration, and data loading function:
import streamlit as st
import json
import os
from datetime import datetime

st.set_page_config(
    page_title="Stryker Threat Intelligence Dashboard",
    page_icon="\ud83d\udd12",
    layout="wide"
)

DATA_FILE = "threat_data.json"


@st.cache_data(ttl=60)
def load_threat_data():
    """Load threat intelligence data from JSON file."""
    if not os.path.exists(DATA_FILE):
        return None
    with open(DATA_FILE, "r") as f:
        return json.load(f)

What does this code do?

  • st.set_page_config sets the browser tab title, favicon, and uses a wide layout so the dashboard has room for metrics columns.
  • @st.cache_data(ttl=60) caches the loaded JSON for 60 seconds, preventing the file from being re-read on every Streamlit interaction.
  • load_threat_data() returns None if the data file is missing, which the dashboard will handle with an error message.
  • Below the code you just added, add the sidebar rendering function:
def render_sidebar(data):
    """Render the sidebar with collection metadata."""
    st.sidebar.title("Threat Intel Status")
    if data:
        timestamp = data.get("collection_timestamp", "Unknown")
        st.sidebar.metric("Last Collection", timestamp[:16])
        total = sum(
            len(data.get(k, [])) for k in
            ["ransomware_intel", "breach_intel", "exploit_intel", "darkweb_mentions"]
        )
        st.sidebar.metric("Total Threats", total)
        st.sidebar.metric("Target", data.get("target", "Unknown"))
        st.sidebar.divider()
        st.sidebar.metric("Ransomware", len(data.get("ransomware_intel", [])))
        st.sidebar.metric("Breaches", len(data.get("breach_intel", [])))
        st.sidebar.metric("Exploits", len(data.get("exploit_intel", [])))
        st.sidebar.metric("Dark Web", len(data.get("darkweb_mentions", [])))
    else:
        st.sidebar.warning("No data loaded. Run collect_intel.py first.")

What does the sidebar show?

The sidebar acts as a persistent status panel. It displays when the data was last collected, the total threat count, the target being monitored, and a per-category breakdown. This gives any viewer instant context regardless of which tab they are looking at.

  • Save dashboard.py and launch the Streamlit server by running:
streamlit run dashboard.py

A new browser tab opens automatically. You should see a blank page with "Stryker Threat Intelligence Dashboard" shown in the browser tab title. The page content is empty because no main() function calls your rendering logic yet.

  • Stop the Streamlit server by pressing Ctrl+C in your terminal.

Don't see the browser tab open?

  • Make sure your virtual environment is activated. If streamlit is not found, activate it with source venv/bin/activate (macOS/Linux) or venv\Scripts\activate (Windows).
  • Check for syntax errors in the terminal output. A typo in dashboard.py will prevent the server from starting.
  • help me debug my Streamlit dashboard not launching
Build the Leadership view

Leadership needs a single-glance risk picture. They do not want to parse JSON or click through individual threats. This view surfaces the numbers that drive board-level decisions: total exposure, severity distribution, and a posture assessment.

  • In dashboard.py, below the render_sidebar function, add the first part of the Leadership view with metric cards and a bar chart:
def render_leadership_view(data):
    """Leadership view: risk summary, trends, high-level posture."""
    st.header("Executive Threat Summary")

    col1, col2, col3, col4 = st.columns(4)
    ransomware_count = len(data.get("ransomware_intel", []))
    breach_count = len(data.get("breach_intel", []))
    exploit_count = len(data.get("exploit_intel", []))
    critical_count = sum(
        1 for entry in data.get("ransomware_intel", [])
        if entry.get("severity") == "critical"
    )

    col1.metric("Healthcare Ransomware Victims", ransomware_count)
    col2.metric("Breach Exposures", breach_count)
    col3.metric("Known Exploits", exploit_count)
    col4.metric("Critical Severity", critical_count)

    st.subheader("Healthcare Sector Ransomware Activity")
    ransomware_entries = data.get("ransomware_intel", [])
    if ransomware_entries:
        groups = {}
        for entry in ransomware_entries:
            raw = entry.get("raw_data", {})
            group = raw.get("group", "Unknown")
            groups[group] = groups.get(group, 0) + 1
        st.bar_chart(groups)

What do the metric cards show?

st.columns(4) creates a four-column layout. Each column displays one st.metric card: total ransomware victims in healthcare, breach count, exploit count, and critical-severity entries.

The bar chart tallies ransomware groups by name, giving leadership a visual of which groups are most active against healthcare organizations.

  • Still inside the render_leadership_view function, add the risk posture assessment and key findings directly below the bar chart code:
    st.subheader("Risk Posture Assessment")
    if critical_count > 10:
        st.error("HIGH RISK: Significant ransomware activity targeting healthcare sector")
    elif critical_count > 5:
        st.warning("ELEVATED: Active ransomware campaigns against healthcare organizations")
    else:
        st.info("MODERATE: Ongoing monitoring recommended")

    st.subheader("Key Findings for Board Communication")
    st.markdown(
        "- Healthcare sector remains a top-3 target for ransomware groups\n"
        "- The March 2026 Stryker/Handala incident demonstrates nation-state interest in medical device supply chains\n"
        "- Identity management and MDM platforms are emerging attack vectors requiring multi-admin approval controls"
    )

How does the posture indicator work?

The risk posture uses simple threshold logic. More than 10 critical entries triggers a red HIGH RISK alert. Between 5 and 10 shows an orange ELEVATED warning. Anything below shows a blue MODERATE indicator. This gives leadership an instant traffic-light signal without parsing numbers.

  • Save dashboard.py.
Build the Product Team and Pentesting Team views

The Product Team cares about which CVEs affect Stryker device components and which data classes are exposed in breaches. The Pentesting Team needs raw IOCs, exploit payloads, and MITRE ATT&CK context to plan offensive operations.

  • Below render_leadership_view, add the first part of the Product Team view covering CVE exposure and breach data classes:
def render_product_team_view(data):
    """Product Team view: CVE exposure, device lifecycle, component risks."""
    st.header("Product Security Intelligence")

    st.subheader("CVE and Exploit Exposure")
    exploit_entries = data.get("exploit_intel", [])
    if exploit_entries:
        for entry in exploit_entries[:10]:
            with st.expander(entry.get("title", "Unknown")):
                st.json(entry.get("raw_data", {}))
    else:
        st.info("No exploit data collected. Run collect_intel.py to populate.")

    st.subheader("Breach Data Classes at Risk")
    breach_entries = data.get("breach_intel", [])
    if breach_entries:
        data_classes = set()
        for entry in breach_entries:
            raw = entry.get("raw_data", {})
            classes = raw.get("DataClasses", raw.get("dataClasses", []))
            if isinstance(classes, list):
                data_classes.update(classes)
        if data_classes:
            st.write("Exposed data types across known breaches:")
            for dc in sorted(data_classes):
                st.markdown(f"- {dc}")

What do the expanders show?

st.expander creates collapsible panels. Each exploit entry gets its own expandable section titled by CVE or vulnerability name. Clicking it reveals the full raw JSON from Vulners, so the Product Team can drill into specifics without cluttering the default view.

The breach data classes section extracts the types of data exposed (email addresses, passwords, IP addresses) from HIBP breach records.

  • Still inside the render_product_team_view function, add the device lifecycle security context below the breach data classes code:
    st.subheader("Device Lifecycle Security Context")
    st.markdown(
        "**Stryker Cyber Devices in Guaranteed Support:**\n"
        "- CORE 2 (v4.1.x): End of Support January 2035\n"
        "- Steri-Shield 8: End of Support March 2035\n"
        "- RPS Primary TKA Software: End of Support January 2040\n"
        "- RPS Console: End of Support January 2040\n\n"
        "**End of Support (no patches):**\n"
        "- SurgiCount Gen 1 tablet with Medicus software\n"
        "- Triton AI Application v1.9.1 and earlier\n"
        "- Triton QBL Application v1.13.1 and earlier"
    )

Why include device lifecycle data?

Devices past End of Support receive no security patches. If a CVE targets one of these devices, the Product Team has no vendor fix available and must implement compensating controls. This section gives them that context alongside the live exploit data above.

  • Below render_product_team_view, add the first part of the Pentesting Team view with the raw IOC dataframe:
def render_pentesting_view(data):
    """Pentesting Team view: raw IOCs, exploits, MITRE ATT&CK context."""
    st.header("Offensive Security Intelligence")

    st.subheader("Raw IOCs and Indicators")
    all_entries = (
        data.get("ransomware_intel", []) +
        data.get("breach_intel", []) +
        data.get("exploit_intel", []) +
        data.get("darkweb_mentions", [])
    )
    if all_entries:
        st.dataframe(
            [
                {
                    "Source": e.get("source"),
                    "Category": e.get("category"),
                    "Title": e.get("title"),
                    "Severity": e.get("severity"),
                    "Timestamp": e.get("timestamp", "")[:16]
                }
                for e in all_entries
            ],
            use_container_width=True
        )

What is the IOC dataframe?

st.dataframe renders a sortable, scrollable table. It combines ALL threat entries from every category into one flat view so pentesters can sort by severity, filter by source, and quickly identify actionable indicators of compromise.

  • Still inside render_pentesting_view, add the dark web mentions, MITRE ATT&CK context, and exploit availability sections below the dataframe code:
    st.subheader("Dark Web Mentions")
    darkweb_entries = data.get("darkweb_mentions", [])
    if darkweb_entries:
        for entry in darkweb_entries:
            with st.expander(entry.get("title", "Unknown .onion result")):
                st.json(entry.get("raw_data", {}))
    else:
        st.info("No dark web mentions found. Tor tools require a running Tor SOCKS5 proxy.")

    st.subheader("MITRE ATT&CK Context: Stryker/Handala Incident")
    st.markdown(
        "**Attack Vector:** Credential theft targeting Microsoft Intune MDM console\n\n"
        "**Techniques Observed:**\n"
        "- T1078: Valid Accounts (stolen MDM admin credentials)\n"
        "- T1072: Software Deployment Tools (Intune wipe command weaponized)\n"
        "- T1485: Data Destruction (factory reset of ~80,000 devices)\n"
        "- T1036: Masquerading (Living-off-the-Cloud, no malware signature)\n\n"
        "**Threat Actor:** Handala Hack (Iran-linked, affiliated with MOIS)\n\n"
        "**Recommended Testing Focus:**\n"
        "- MDM console authentication strength (test for MFA bypass)\n"
        "- Multi-admin approval workflows for destructive operations\n"
        "- Identity management plane isolation\n"
        "- Credential monitoring and alerting for privileged accounts"
    )

    st.subheader("Exploit Availability")
    exploit_entries = data.get("exploit_intel", [])
    if exploit_entries:
        for entry in exploit_entries:
            raw = entry.get("raw_data", {})
            st.code(json.dumps(raw, indent=2), language="json")

Why include MITRE ATT&CK mappings?

MITRE ATT&CK technique IDs (T1078, T1072, T1485, T1036) give the pentesting team a shared language for describing the attack chain. Each ID maps to a documented technique with known detection strategies, so the team can validate whether their security controls would catch each stage.

The exploit availability section dumps raw JSON for each exploit so testers can extract proof-of-concept code, CVSS scores, and affected product versions directly.

  • Finally, add the main() function at the bottom of dashboard.py to wire everything together:
def main():
    st.title("Stryker Medical Device Threat Intelligence")
    st.caption("Dark web intelligence powered by darknet-mcp-server (66 tools, 16 sources)")

    data = load_threat_data()
    render_sidebar(data)

    if not data:
        st.error("No threat data found. Run `python3 collect_intel.py` first to collect intelligence.")
        return

    tab1, tab2, tab3 = st.tabs(["Leadership", "Product Team", "Pentesting Team"])

    with tab1:
        render_leadership_view(data)
    with tab2:
        render_product_team_view(data)
    with tab3:
        render_pentesting_view(data)


if __name__ == "__main__":
    main()

What does main() do?

main() is the entry point Streamlit executes. It sets the page title, loads threat data, renders the sidebar, and creates three tabs. Each tab delegates to a dedicated view function so the code stays organized by persona.

If threat_data.json is missing, the dashboard shows a clear error message instead of crashing. This handles the case where someone runs the dashboard before collecting data.

  • Save dashboard.py.

Before you run the dashboard, what do you expect to see when you switch between the three tabs? Think about what data each view pulls from threat_data.json.

  • Launch the dashboard by running:
streamlit run dashboard.py

Your browser opens with the full dashboard. You should see "Stryker Medical Device Threat Intelligence" as the page heading, the sidebar showing threat counts on the left, and three tabs labeled Leadership, Product Team, and Pentesting Team. Click each tab to confirm it renders its unique view of the threat data.

Seeing a NameError or blank page?

  • A NameError means a function is referenced before it is defined. Confirm that render_leadership_view, render_product_team_view, and render_pentesting_view all appear ABOVE the main() function in your file.
  • If you see "No threat data found", make sure threat_data.json exists in the same folder as dashboard.py. Re-run python3 collect_intel.py if needed.
  • If the sidebar says 0 for all counts, your threat_data.json may be empty because earlier queries timed out. Re-run collection and check the terminal output for non-zero counts.
  • help me debug my Streamlit threat intelligence dashboard

✔️ Awesome, I've got everything!

Great. Double check you have saved dashboard.py and that all three tabs render data when you click them.

ⓧ I'd like to double check the full code

Your dashboard.py should contain these sections in this exact order. Scroll up through your file and confirm each is present:

  • Imports, st.set_page_config(), DATA_FILE, and load_threat_data() at the top.
  • render_sidebar(data) function with sidebar metrics and divider.
  • render_leadership_view(data) function with columns, bar chart, risk posture, and key findings.
  • render_product_team_view(data) function with CVE expanders, breach data classes, and device lifecycle markdown.
  • render_pentesting_view(data) function with IOC dataframe, dark web expanders, MITRE ATT&CK markdown, and exploit JSON.
  • main() function with title, sidebar call, error check, and three tabs.
  • if __name__ == "__main__": main() at the very bottom.

Your dashboard is live. You now have a single interface where three different security audiences can each find exactly what they need. Next up, you will connect Claude Desktop to the MCP server so you can run natural-language investigations that feed fresh data into this dashboard.

Connect Claude Desktop to the MCP Server

Your CLI pipeline demonstrates that threat data can flow from 16 dark web sources into a structured dashboard. However, the real operational value of the MCP server is interactive investigation.

Connecting Claude Desktop to the darknet-mcp-server lets you ask natural language questions and get correlated intelligence across all sources simultaneously. This is the investigative workflow you would use day-to-day to feed the dashboard with fresh data.

In this step, get ready to:
  • Install Claude Desktop and locate the MCP configuration file.
  • Add the darknet-mcp-server entry to the configuration.
  • Test interactive threat intelligence queries through natural language.
Install Claude Desktop

Claude Desktop is a free app that supports MCP server connections out of the box. You need it installed before you can wire up the darknet-mcp-server.

  • Open your browser and navigate to claude.ai/download.
  • Download and install Claude Desktop for your operating system.
  • Open Claude Desktop once the installation completes.

What is Claude Desktop?

Claude Desktop is the GUI app for Claude that can connect to local MCP servers. When you add an MCP server to its configuration, Claude can invoke that server's tools in response to your natural language prompts.

Add the darknet-mcp-server to the configuration

Claude Desktop reads its MCP server definitions from a JSON file called claude_desktop_config.json. You need to add a single entry that tells Claude how to launch the darknet-mcp-server.

  • In Claude Desktop, click the Claude menu in your system menu bar (not the settings inside the Claude window).
  • Select Settings... from the dropdown menu.
  • In the Settings window, click the Developer tab in the left sidebar.
  • Click the Edit Config button to open claude_desktop_config.json in your text editor.

Where is the config file?

The Edit Config button creates the file if it does not exist yet. The file location depends on your operating system:

  • macOS: ~/Library/Application Support/Claude/claude_desktop_config.json
  • Windows: %APPDATA%\Claude\claude_desktop_config.json
  • Replace the contents of claude_desktop_config.json with the following configuration:
{
  "mcpServers": {
    "darknet": {
      "command": "npx",
      "args": ["-y", "darknet-mcp-server"]
    }
  }
}

What does this configuration do?

  • "darknet" is the name Claude uses to identify this server internally.
  • "command": "npx" tells Claude to use Node.js's npx tool to run the server.
  • "-y" automatically confirms the package download without prompting.
  • "darknet-mcp-server" is the npm package name that provides all 66 threat intelligence tools.
  • Save the file.
  • Fully quit Claude Desktop. On macOS, press Cmd+Q. On Windows, right-click the system tray icon and select Exit.
  • Reopen Claude Desktop.

Closing the window is not enough

Claude Desktop only reads its config file at startup. Closing the window leaves the app running in the background. You must fully quit and reopen it for the new MCP server to load.

Server not loading after restart?

  • Make sure you fully quit Claude Desktop (not just closed the window). On macOS, check the Dock icon is gone. On Windows, check the system tray.
  • Validate your JSON syntax. A missing comma or extra trailing comma breaks the entire config. Paste the contents into jsonlint.com to check.
  • Confirm npx is available to Claude Desktop. GUI apps sometimes cannot find npx if Node.js was installed after the app. Run which npx (macOS) or where npx (Windows) and use the full absolute path in the command field if needed.

help me troubleshoot why Claude Desktop is not loading the darknet-mcp-server

Test interactive threat intelligence

With the MCP server connected, Claude can now invoke any of the 66 darknet-mcp-server tools in response to your questions. This is the investigative workflow that feeds your dashboard with fresh data.

  • Open a new conversation in Claude Desktop.
  • Before you send your query, predict: do you think Claude will call a single tool or multiple tools to answer a healthcare ransomware question?
  • Type the following prompt and press Enter:
Investigate what ransomware groups are currently targeting the healthcare sector and check if stryker.com has appeared in any recent breach data.

Claude invokes multiple darknet-mcp-server tools in parallel and returns a correlated intelligence summary. You should see it call tools like ransomwareBySector and breachList and combine the results into a single narrative answer.

Why is this more powerful than the CLI?

With the CLI, you ran each tool individually and got raw JSON back. Claude correlates results across multiple tools automatically, identifies patterns, and presents findings in natural language. This is the difference between running queries and conducting an investigation.

✔️ Awesome, I've got everything!

Great. Double check that your claude_desktop_config.json is saved and Claude Desktop is responding to threat intelligence queries.

ⓧ I'd like to double check the full code

Your claude_desktop_config.json should contain exactly this:

{
  "mcpServers": {
    "darknet": {
      "command": "npx",
      "args": ["-y", "darknet-mcp-server"]
    }
  }
}

You now have a complete end-to-end threat intelligence pipeline. The darknet-mcp-server feeds data into your Python collection script, which normalizes it for the Streamlit dashboard. Claude Desktop gives you the interactive investigative layer on top. This is the workflow a real security team would use: automated collection for the dashboard, conversational investigation for emerging threats.

Secret mission

Add a Real-Time Alert Feed

Your dashboard shows a snapshot in time, but threat landscapes shift constantly. Security teams need to know what changed since the last review. In this mission, you will add diffing logic that compares collection runs and surfaces net-new threats in a color-coded alert banner at the top of the dashboard.

Clean Up Your Resources

Clean Up Your Resources

This project runs entirely locally with no cloud resources or paid API keys. There are no ongoing costs. Decide whether to keep your resources running, pause them to come back later, or delete them entirely.

Resources you used:

  • Project folder on your Desktop containing collect_intel.py, dashboard.py, threat_data.json, previous_threat_data.json, and alerts.json.
  • Python virtual environment (venv) with Streamlit and requests installed.
  • Claude Desktop MCP configuration (darknet server entry in claude_desktop_config.json).

Keep everything running

No action needed. Choose this if you plan to continue running threat queries or extending your dashboard.

  • The dashboard runs locally on demand whenever you run streamlit run dashboard.py.
  • The Claude Desktop MCP integration persists across sessions. You can query the darknet-mcp-server from Claude at any time.
  • No background processes are running. Nothing consumes resources until you explicitly start a script or open Claude Desktop.

Pause - I'll come back to this later

Shut down any running processes to free up memory. Your files and configuration stay in place for when you return.

  • If Streamlit is running in your terminal, press Ctrl+C to stop it.
  • Close Claude Desktop.
  • Deactivate your virtual environment by running:
deactivate

Your project folder and Claude Desktop configuration remain intact for next time.

Delete - I don't want to use this again

Remove all project resources and clean up the MCP configuration. This is a complete removal.

  • If Streamlit is running, press Ctrl+C to stop it.
  • Deactivate your virtual environment by running:
deactivate
  • Delete your project folder from the Desktop by running:

macOS

rm -rf ~/Desktop/[[PROJECT_DIR="your project folder name"]]

Windows

Remove-Item -Recurse -Force ~\Desktop\[[PROJECT_DIR="your project folder name"]]
  • Remove the darknet MCP server entry from Claude Desktop. Open Claude Desktop, click the Claude menu in your system menu bar, then select Settings....
  • Navigate to the Developer tab and click Edit Config.
  • Delete the "darknet" entry from the mcpServers object, or replace the entire file contents with:
{
  "mcpServers": {}
}
  • Save the file and restart Claude Desktop for the change to take effect.

Nice Work!

Nice Work!

You just built a full threat intelligence pipeline that pulls live dark web data and presents it through role-specific views for medical device security teams.

You've learned how to:

  • Query 16 dark web data sources from a single MCP server and normalize heterogeneous JSON responses into a structured collection layer that feeds a unified data model.
  • Build a persona-segregated Streamlit dashboard with three views tailored to different consumers of threat intelligence: Leadership for risk posture, Product Team for CVE exposure, and Pentesting Team for raw IOCs and MITRE ATT&CK context.
  • Connect Claude Desktop to the darknet-mcp-server for interactive, natural-language threat investigations that correlate intelligence across all sources simultaneously.
  • Secret Mission: Added a real-time alert feed that diffs between collection runs and highlights new threats with color-coded severity indicators.

Ready to quiz yourself?