<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Art</title>
    <description>The latest articles on DEV Community by Art (@sleepti3ht).</description>
    <link>https://dev.to/sleepti3ht</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4165243%2Fbadb28fc-c58a-48da-b170-505c8d913806.png</url>
      <title>DEV Community: Art</title>
      <link>https://dev.to/sleepti3ht</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/sleepti3ht"/>
    <language>en</language>
    <item>
      <title>Building Panopticon: A Local-First CVE Intelligence Map with Tauri 2 + Rust + Python</title>
      <dc:creator>Art</dc:creator>
      <pubDate>Thu, 08 Oct 2026 05:52:14 +0000</pubDate>
      <link>https://dev.to/sleepti3ht/building-panopticon-a-local-first-cve-intelligence-map-with-tauri-2-rust-python-4p6p</link>
      <guid>https://dev.to/sleepti3ht/building-panopticon-a-local-first-cve-intelligence-map-with-tauri-2-rust-python-4p6p</guid>
      <description>&lt;p&gt;Vulnerability triage today is browser tab soup. NVD in one tab. Vendor advisory in another. CISA KEV in a third. ChatGPT in a fourth. Yesterday's runbook in a fifth.&lt;/p&gt;

&lt;p&gt;The analyst context-switches every thirty seconds. The mental model — this CVE, this vendor, these products, these mitigations — never lives in one place. Close the browser, and it's gone.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Panopticon&lt;/strong&gt; collapses that soup into a single native window. It draws a force-directed threat graph of vendor↔CVE relationships. It wires that graph to a local AI agent that drafts mitigation plans, keeps a versioned chat history, and resumes conversations across sessions. CVEs actively exploited in the wild get flagged straight from the CISA KEV catalog.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6yeirbm7re9u961k9v9.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fs6yeirbm7re9u961k9v9.png" alt="Threat graph centered on Log4Shell: camera focus, CVSS-colored nodes, details panel" width="799" height="535"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;The graph recentered on CVE-2021-44228. Node color encodes CVSS; the right panel is the analysis workspace.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Local-first, with honest boundaries. The CVE database, chat history, reports and API keys never leave your disk. What does go out is exactly what you configure: LLM prompts to OpenRouter (your model, your key), plus lookups to the public NVD and CISA feeds. No telemetry. No accounts. No cloud lock-in.&lt;/p&gt;
&lt;h2&gt;
  
  
  Why not Electron
&lt;/h2&gt;

&lt;p&gt;Electron ships a full Chromium runtime with every app. That means hundreds of megabytes of install size and a heavy idle footprint — for what is usually a single-page UI. Tauri 2 reuses the OS-native WebView instead: WebView2 on Windows, WebKit elsewhere. In practice the binary lands an order of magnitude smaller, and the idle memory profile is dramatically lighter. For a tool analysts keep open all day next to a SIEM, that matters.&lt;/p&gt;

&lt;p&gt;The trade-off is real. WebView2 has quirks: programmatic blob downloads silently no-op, some CSS lags Chromium. Each quirk had a workaround. None justified shipping our own browser.&lt;/p&gt;
&lt;h3&gt;
  
  
  The deliberately thin Rust layer
&lt;/h3&gt;


&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Vanilla JS (Vite + vis-network)
        │  invoke("chat_with_agent", { cveId, messages, model })
        ▼
Tauri 2 · Rust command handlers      ← owns window, routes IPC, spawns processes
        │  spawn python.exe · history via stdin
        ▼
Python asyncio layer
   ai_agent.py ──► MCP client ──► mcp_server.py (CVE context, KEV, CWE stats)
        │                              ├─► SQLite (local CVE DB)
        │                              └─► CISA KEV catalog + NVD live fallback
        └──► OpenRouter API + SQLite (chat_reports)
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;


&lt;p&gt;Rust owns the window and the IPC. All heavy logic lives in Python: LLM orchestration, graph building, search, persistence. Iterating on prompts and parsers is simply faster there.&lt;/p&gt;

&lt;p&gt;The Python layer reaches the LLM through the Model Context Protocol (MCP) — a small standard that lets models call tools. The same CVE-context tools serve the built-in chat and any external MCP-compatible client.&lt;/p&gt;
&lt;h2&gt;
  
  
  Four decisions worth stealing
&lt;/h2&gt;
&lt;h3&gt;
  
  
  1. Conversation history goes through stdin, not CLI args
&lt;/h3&gt;

&lt;p&gt;Windows caps a command line at ~32,767 characters. A ten-message history with mitigation payloads and code blocks hits 30-50 KB of JSON. Long sessions died silently.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;child&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nn"&gt;Command&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;new&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;python_path&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"--chat"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;&lt;span class="nf"&gt;.arg&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;&amp;amp;&lt;/span&gt;&lt;span class="n"&gt;cve_id&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="nf"&gt;.stdin&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nn"&gt;Stdio&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="nf"&gt;piped&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="nf"&gt;.spawn&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="n"&gt;child&lt;/span&gt;&lt;span class="py"&gt;.stdin&lt;/span&gt;&lt;span class="nf"&gt;.as_mut&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.unwrap&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;&lt;span class="nf"&gt;.write_all&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;messages&lt;/span&gt;&lt;span class="nf"&gt;.as_bytes&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;&lt;span class="o"&gt;?&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="c1"&gt;// stdin dropped here → child sees EOF&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;No deadlock: the child writes stdout only after draining stdin, so the streams never block each other.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. CISA KEV as a first-class signal
&lt;/h3&gt;

&lt;p&gt;CVSS alone misprioritizes. A 7.5 that is exploited beats a 9.8 that is theoretical. Panopticon lazy-loads the public KEV catalog (~1.7k entries) once per process. Listed CVEs get a thick red ring on the graph and an &lt;code&gt;ACTIVELY EXPLOITED&lt;/code&gt; row with the patch due date in the details panel.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3c3kw746e7t03qgu84il.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2F3c3kw746e7t03qgu84il.png" alt="KEV-flagged CVE: red ring on the node and ACTIVELY EXPLOITED row with due date" width="800" height="537"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;CISA KEV is public government data. The flag cuts through CVSS noise during triage.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;One&amp;nbsp;bug&amp;nbsp;worth&amp;nbsp;mentioning here was self-inflicted. The KEV fields were computed correctly, then dropped by a serialization whitelist that copied &lt;code&gt;details&lt;/code&gt; into the graph JSON key by key. The UI never saw the flag. Whitelists are fine — until you add a field and forget the projection.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Defensive LLM parsing, twice
&lt;/h3&gt;

&lt;p&gt;Free-tier providers sometimes return HTTP 200 with &lt;code&gt;{"error": {...}}&lt;/code&gt; instead of &lt;code&gt;{"choices": [...]}}&lt;/code&gt;. Naive parsing crashes with &lt;code&gt;KeyError&lt;/code&gt;. So every response is categorized: rate limit, credits, overload, network. The user sees an actionable message, not a stack trace.&lt;/p&gt;

&lt;p&gt;The second failure mode is degenerate repetition: the model collapses into loops like &lt;code&gt;health health health....).&lt;/code&gt; mid-answer. Prevention is sampling penalties. Detection is a punctuation-insensitive n-gram run check on the response tail. Sanitation truncates at the first degenerate segment — before the response is cached or saved, so poisoned text never persists.&lt;/p&gt;

&lt;h3&gt;
  
  
  4. Vanilla JS over a framework
&lt;/h3&gt;

&lt;p&gt;&lt;code&gt;main.js&lt;/code&gt; is about nine hundred lines. &lt;code&gt;vis-network&lt;/code&gt; needs raw DOM access. The state is a handful of explicit globals with race guards. For a single-user desktop tool, a component tree would add ceremony, not clarity. XSS-safety is handled by escaping HTML before the markdown transform.&lt;/p&gt;

&lt;h2&gt;
  
  
  Walkthrough
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Search is navigation.&lt;/strong&gt; Typing &lt;code&gt;log4j&lt;/code&gt; resolves CVEs, vendors and CWEs across tables. Clicking a hit rebuilds the graph around that CVE's vendor scope and flies the camera to the node — even when the CVE sits outside the currently loaded slice.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2vuuhycxz3u00xjvgks.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fi2vuuhycxz3u00xjvgks.png" alt="Global search for log4j with typed results" width="800" height="532"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;One query across cve_id, description, vendor and CWE tables.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The chat is the workspace.&lt;/strong&gt; The session below runs on CVE-2021-45046:&lt;/p&gt;

&lt;blockquote&gt;
&lt;ol&gt;
&lt;li&gt;What's the mitigation plan for this CVE?&lt;/li&gt;
&lt;li&gt;How does this compare to Log4Shell (CVE-2021-44228)?&lt;/li&gt;
&lt;li&gt;Show me remediation commands for Ubuntu and Docker.&lt;/li&gt;
&lt;/ol&gt;
&lt;/blockquote&gt;

&lt;p&gt;Question 2 is the point. The agent holds the CVE context from MCP and the conversation at the same time. Regenerate keeps every draft — the &lt;code&gt;2/3&lt;/code&gt; indicator walks the versions.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc769poi2sw5cb1voz1vr.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fc769poi2sw5cb1voz1vr.png" alt="Versioned mitigation chat in the expanded details panel" width="800" height="532"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Every assistant turn is auto-saved. The panel expands for long reads.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Work resumes.&lt;/strong&gt; Conversations persist as reports. Pin the ones you return to, tag by campaign or asset, bulk-delete the rest, export any session to Markdown.&lt;/p&gt;

&lt;p&gt;&lt;a href="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnyuq4zl2uvyunuunasg.png" class="article-body-image-wrapper"&gt;&lt;img src="https://media2.dev.to/dynamic/image/width=800%2Cheight=%2Cfit=scale-down%2Cgravity=auto%2Cformat=auto/https%3A%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Farticles%2Fjnyuq4zl2uvyunuunasg.png" alt="Reports panel with pins, tags and bulk controls" width="800" height="533"&gt;&lt;/a&gt;&lt;br&gt;
&lt;em&gt;Reports carry the vendor scope, so reopening rebuilds the exact graph you left.&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Exported Markdown is explicitly labeled as an AI-generated draft that requires human verification. Hiding that would not protect the analyst who forwards it. It would remove the only signal that says "check these commands first".&lt;/p&gt;

&lt;h2&gt;
  
  
  Lessons learned
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Absolute paths from day one.&lt;/strong&gt; A relative &lt;code&gt;DB_PATH&lt;/code&gt; created two databases in two directories, depending on the working directory. One &lt;code&gt;Path(__file__).resolve().parent&lt;/code&gt; fixed the whole genre of bugs.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;PowerShell 5.1 drops empty native-exe arguments.&lt;/strong&gt; My &lt;code&gt;script.py "" "" arg&lt;/code&gt; test silently became &lt;code&gt;script.py arg&lt;/code&gt;, which produced a very confusing "empty graph" diagnosis. Test CLI contracts from Python, not from PS 5.1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Real data beats mock data in public.&lt;/strong&gt; NVD and CISA KEV are public feeds. Screenshots with &lt;code&gt;banana software&lt;/code&gt; read as a toy. Screenshots with Log4Shell read as a tool.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Roadmap
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Streaming responses via Tauri Events (kill the micro-freeze on long reports)&lt;/li&gt;
&lt;li&gt;In-UI sync button with a live ingest log stream&lt;/li&gt;
&lt;li&gt;KEV catalog persisted to a local table (offline flags)&lt;/li&gt;
&lt;li&gt;CWE layer in the graph: vendor weakness patterns&lt;/li&gt;
&lt;li&gt;CI release builds per OS&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/sleepti3ht/Panopticon.git
&lt;span class="nb"&gt;cd &lt;/span&gt;Panopticon                      &lt;span class="c"&gt;# repo root IS the desktop project&lt;/span&gt;

&lt;span class="c"&gt;# Python backend&lt;/span&gt;
&lt;span class="nb"&gt;cd &lt;/span&gt;panopticon-python
python &lt;span class="nt"&gt;-m&lt;/span&gt; venv venv
venv&lt;span class="se"&gt;\S&lt;/span&gt;cripts&lt;span class="se"&gt;\a&lt;/span&gt;ctivate              &lt;span class="c"&gt;# Windows; on PS policy errors: .\venv\Scripts\Activate.ps1&lt;/span&gt;
&lt;span class="c"&gt;# source venv/bin/activate         # macOS / Linux&lt;/span&gt;
pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-r&lt;/span&gt; requirements.txt
Copy-Item .env.example .env        &lt;span class="c"&gt;# PowerShell (cmd/Unix: cp .env.example .env)&lt;/span&gt;
&lt;span class="c"&gt;# add your OPENROUTER_API_KEY to .env&lt;/span&gt;

python seed_mock.py                &lt;span class="c"&gt;# instant demo data, no keys needed&lt;/span&gt;

&lt;span class="c"&gt;# Frontend + Tauri&lt;/span&gt;
&lt;span class="nb"&gt;cd&lt;/span&gt; ..
npm &lt;span class="nb"&gt;install
&lt;/span&gt;npm run tauri dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ready for the real feed? &lt;code&gt;python ingestor.py 4000 90&lt;/code&gt; pulls a rolling 90-day window from NVD, auto-chunked against the 120-day query limit.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Repo:&lt;/strong&gt; &lt;a href="https://github.com/sleepti3ht/Panopticon" rel="noopener noreferrer"&gt;github.com/sleepti3ht/Panopticon&lt;/a&gt; — MIT, issues and PRs open.&lt;/p&gt;

&lt;p&gt;Panopticon does not replace your scanner or your SIEM. It replaces the six tabs you keep open during triage — and remembers what you figured out yesterday.&lt;/p&gt;




</description>
      <category>rust</category>
      <category>tauri</category>
      <category>cybersecurity</category>
      <category>ai</category>
    </item>
  </channel>
</rss>
