<?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: Signal Over Noise</title>
    <description>The latest articles on DEV Community by Signal Over Noise (@signalovernoizx).</description>
    <link>https://dev.to/signalovernoizx</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.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3792464%2F7c42bfa4-fda3-41ea-9c71-10112f459de8.png</url>
      <title>DEV Community: Signal Over Noise</title>
      <link>https://dev.to/signalovernoizx</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/signalovernoizx"/>
    <language>en</language>
    <item>
      <title># How I Built a Live Cybersecurity Intelligence Dashboard on a Raspberry Pi 5</title>
      <dc:creator>Signal Over Noise</dc:creator>
      <pubDate>Wed, 25 Feb 2026 19:07:01 +0000</pubDate>
      <link>https://dev.to/signalovernoizx/how-i-built-a-live-cybersecurity-intelligence-dashboard-on-a-raspberry-pi-5-d8k</link>
      <guid>https://dev.to/signalovernoizx/how-i-built-a-live-cybersecurity-intelligence-dashboard-on-a-raspberry-pi-5-d8k</guid>
      <description>&lt;p&gt;A few weeks ago I got tired of manually trawling through 15+ security blogs, RSS feeds, and Twitter/X accounts every morning. So I built an automated cybersecurity intelligence dashboard that runs 24/7 on a Raspberry Pi 5 sitting on my desk.&lt;/p&gt;

&lt;p&gt;Here's what it does, how I built it, and every mistake I made along the way.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it does
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;&lt;a href="https://signal-noise.tech" rel="noopener noreferrer"&gt;signal-noise.tech&lt;/a&gt;&lt;/strong&gt; is a live threat intelligence feed that:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Aggregates 18 cybersecurity RSS sources (The Hacker News, BleepingComputer, CISA advisories, Krebs, Unit 42, Cisco Talos, and more)&lt;/li&gt;
&lt;li&gt;Refreshes every 30 minutes&lt;/li&gt;
&lt;li&gt;Classifies stories by severity (Critical/Gov/Vendor/Media)&lt;/li&gt;
&lt;li&gt;Detects 30+ known threat actors in headlines (Lazarus, ALPHV, Volt Typhoon, etc.)&lt;/li&gt;
&lt;li&gt;Counts CVEs tracked, critical headlines, CISA advisories — live KPIs on the homepage&lt;/li&gt;
&lt;li&gt;Auto-posts the top stories to &lt;a href="https://x.com/SignalOverNoizX" rel="noopener noreferrer"&gt;@SignalOverNoizX&lt;/a&gt; on X&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Everything runs on a Pi 5 behind a domestic broadband connection via Cloudflare tunnelling (no port forwarding required).&lt;/p&gt;




&lt;h2&gt;
  
  
  The stack
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hardware&lt;/strong&gt;: Raspberry Pi 5, 8GB RAM, running Raspberry Pi OS Bookworm 64-bit&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Web server&lt;/strong&gt;: Apache2 with SSL (Let's Encrypt), reverse proxy headers, security hardening&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backend&lt;/strong&gt;: Python 3 scripts, scheduled via &lt;code&gt;cron.d&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;AI&lt;/strong&gt;: GPT-5-mini for tweet commentary generation&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Frontend&lt;/strong&gt;: Vanilla HTML/CSS/JS (no frameworks — keeps it fast on a Pi)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Feed data&lt;/strong&gt;: &lt;code&gt;news.json&lt;/code&gt; served as a static file, rebuilt every 30 minutes&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  The feed pipeline
&lt;/h2&gt;

&lt;p&gt;The core of the system is &lt;code&gt;update_news.py&lt;/code&gt;. Every 30 minutes it:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Fetches all 18 RSS feeds concurrently with a thread pool&lt;/li&gt;
&lt;li&gt;Deduplicates by URL&lt;/li&gt;
&lt;li&gt;Attempts to pull &lt;code&gt;og:image&lt;/code&gt; for each story (for the card thumbnails)&lt;/li&gt;
&lt;li&gt;Falls back to Bing image search if no OG image is found&lt;/li&gt;
&lt;li&gt;Generates a branded dark-themed placeholder card if all else fails&lt;/li&gt;
&lt;li&gt;Scores stories by recency and source tier&lt;/li&gt;
&lt;li&gt;Writes &lt;code&gt;news.json&lt;/code&gt; to the web root
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="k"&gt;with&lt;/span&gt; &lt;span class="nc"&gt;ThreadPoolExecutor&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;max_workers&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="mi"&gt;10&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;as&lt;/span&gt; &lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;futures&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="n"&gt;ex&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;submit&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;fetch_feed&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;src&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;SOURCES&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;f&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;as_completed&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;futures&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;items&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;extend&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;f&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;result&lt;/span&gt;&lt;span class="p"&gt;())&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON structure is dead simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"generated_at"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T14:30:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"items"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"title"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Critical RCE in Ivanti products under active exploitation"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"url"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"source"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"BleepingComputer"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"published"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"2026-02-25T12:00:00Z"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"image"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"https://..."&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The frontend is a single JS file that fetches &lt;code&gt;news.json&lt;/code&gt; and renders everything client-side. No database, no API calls at page load — just a static JSON file. The Pi barely breaks a sweat.&lt;/p&gt;




&lt;h2&gt;
  
  
  The hardest part: getting decent images
&lt;/h2&gt;

&lt;p&gt;Security articles are plagued with terrible default images — the CISA US flag, padlock stock photos, vendor logos. I built a three-level fallback:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Parse &lt;code&gt;og:image&lt;/code&gt; from the article HTML&lt;/li&gt;
&lt;li&gt;Query Bing Image Search API for the article title&lt;/li&gt;
&lt;li&gt;Generate a branded dark-themed card with PIL (Python Imaging Library)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The generated cards look surprisingly good — dark cyber aesthetic, company name, colour-coded by source type. Here's the basic approach:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="n"&gt;img&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;Image&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="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;RGB&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;color&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#0a0f1c&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="n"&gt;draw&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;ImageDraw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nc"&gt;Draw&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;img&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# dot grid background
&lt;/span&gt;&lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="nf"&gt;range&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;800&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;28&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;ellipse&lt;/span&gt;&lt;span class="p"&gt;([&lt;/span&gt;&lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;-&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;x&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;y&lt;/span&gt;&lt;span class="o"&gt;+&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;#1a2744&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="c1"&gt;# company name centred
&lt;/span&gt;&lt;span class="n"&gt;draw&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="mi"&gt;200&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt; &lt;span class="n"&gt;company_name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;font&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fill&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="n"&gt;accent_colour&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;anchor&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="s"&gt;mm&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  The X automation
&lt;/h2&gt;

&lt;p&gt;Four times a day, GPT-5-mini reads the top story and generates commentary in the voice of a sharp, opinionated security analyst. Not "Here's the latest news from BleepingComputer" — actual takes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;System: You are a sharp, opinionated cybersecurity analyst with dry humour.
        Rules: max 220 chars, no hashtags, no sycophancy, never promotional.
        Sound human. Sound confident. Make people want to follow you.

User: Tweet about: Critical RCE in Palo Alto GlobalProtect...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The model is a reasoning model, which means it burns internal "thinking tokens" before producing output. Important gotchas I learned the hard way:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Use &lt;code&gt;max_completion_tokens&lt;/code&gt;, NOT &lt;code&gt;max_tokens&lt;/code&gt; (different parameter name)&lt;/li&gt;
&lt;li&gt;Set it to at least 4000 — at 1000 it often burns everything on reasoning and returns empty output&lt;/li&gt;
&lt;li&gt;Minimum 60s timeout — reasoning can take 30-45 seconds&lt;/li&gt;
&lt;li&gt;Temperature parameter is NOT supported (reasoning models ignore it)&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  Security hardening the Pi
&lt;/h2&gt;

&lt;p&gt;Exposing a Pi to the internet requires some care. Things I did:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;ServerTokens Prod&lt;/code&gt; + &lt;code&gt;ServerSignature Off&lt;/code&gt; — hides Apache version&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;Options -Indexes&lt;/code&gt; — no directory listings&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;/scripts/&lt;/code&gt; blocked in Apache config (301 to /) + &lt;code&gt;robots.txt&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Full security header suite: &lt;code&gt;X-Frame-Options&lt;/code&gt;, &lt;code&gt;X-Content-Type-Options&lt;/code&gt;, &lt;code&gt;Referrer-Policy&lt;/code&gt;, &lt;code&gt;Content-Security-Policy&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;GoAccess analytics behind basic auth, LAN IP restriction only&lt;/li&gt;
&lt;li&gt;Credentials in &lt;code&gt;/etc/openclaw/&lt;/code&gt; (root-owned, 600 perms), never in code&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The stats dashboard (GoAccess) only serves to LAN IPs — I can check it on my local network but it's invisible to the internet. Given the &lt;code&gt;/stats/&lt;/code&gt; path isn't listed in &lt;code&gt;robots.txt&lt;/code&gt;, it's security through obscurity, but combined with the IP restriction it's fine for this use case.&lt;/p&gt;




&lt;h2&gt;
  
  
  The RSS feed
&lt;/h2&gt;

&lt;p&gt;Generating RSS 2.0 from &lt;code&gt;news.json&lt;/code&gt; is surprisingly straightforward. The feed is at &lt;code&gt;/feed.xml&lt;/code&gt; and updates every 30 minutes alongside the JSON. Already picked up by some RSS readers.&lt;/p&gt;

&lt;p&gt;The key is correct RFC-822 date formatting — RSS validators are picky:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="kn"&gt;from&lt;/span&gt; &lt;span class="n"&gt;email.utils&lt;/span&gt; &lt;span class="kn"&gt;import&lt;/span&gt; &lt;span class="n"&gt;format_datetime&lt;/span&gt;
&lt;span class="n"&gt;pub_date&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;format_datetime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;dt&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;  &lt;span class="c1"&gt;# "Tue, 25 Feb 2026 14:30:00 +0000"
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






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

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Static files beat databases for this use case.&lt;/strong&gt; No query latency, trivially cacheable, survives traffic spikes on a Pi.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Reasoning models need breathing room.&lt;/strong&gt; Set &lt;code&gt;max_completion_tokens&lt;/code&gt; to 4000 even if your output is 50 characters. The model burns tokens thinking before it writes.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Image quality matters more than you'd think.&lt;/strong&gt; The first version showed every article with a padlock clipart or the CISA flag. It looked terrible. The three-level fallback (OG → Bing → generated card) made a massive difference.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Deduplication by URL, not title.&lt;/strong&gt; The same CVE advisory gets published by 8 sources simultaneously. Title dedup gives false positives (slightly different wording); URL dedup is clean.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Apache's &lt;code&gt;mod_headers&lt;/code&gt; is your friend.&lt;/strong&gt; Adding security headers takes 10 minutes and dramatically improves security posture.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;




&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;Email newsletter (subscribers get a weekly "Week in Cyber" digest)&lt;/li&gt;
&lt;li&gt;Reddit integration for &lt;code&gt;/r/netsec&lt;/code&gt; and &lt;code&gt;/r/cybersecurity&lt;/code&gt; monitoring (API access pending)&lt;/li&gt;
&lt;li&gt;Mastodon cross-posting to infosec.exchange (live at @&lt;a href="mailto:SignalOverNoizX@infosec.exchange"&gt;SignalOverNoizX@infosec.exchange&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;Subscriber analytics — who's reading what&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;The site is live at &lt;strong&gt;&lt;a href="https://signal-noise.tech" rel="noopener noreferrer"&gt;signal-noise.tech&lt;/a&gt;&lt;/strong&gt; and the X account is &lt;strong&gt;&lt;a href="https://x.com/SignalOverNoizX" rel="noopener noreferrer"&gt;@SignalOverNoizX&lt;/a&gt;&lt;/strong&gt;. RSS feed at &lt;code&gt;/feed.xml&lt;/code&gt; if that's your thing.&lt;/p&gt;

&lt;p&gt;Happy to answer questions in the comments — especially around the image pipeline and the GPT-5-mini quirks, which took the most debugging.&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>raspberrypi</category>
      <category>python</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
