<?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: TN</title>
    <description>The latest articles on DEV Community by TN (@tnuhoglu).</description>
    <link>https://dev.to/tnuhoglu</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%2F4092531%2Fc4d727f7-74b0-4eec-b163-86666a58f305.png</url>
      <title>DEV Community: TN</title>
      <link>https://dev.to/tnuhoglu</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/tnuhoglu"/>
    <language>en</language>
    <item>
      <title>Tblue: 582 passive blue-team scanners for websites you own</title>
      <dc:creator>TN</dc:creator>
      <pubDate>Mon, 24 Aug 2026 20:54:36 +0000</pubDate>
      <link>https://dev.to/tnuhoglu/i-built-a-free-tool-that-runs-614-passive-security-checks-on-any-website-30hj</link>
      <guid>https://dev.to/tnuhoglu/i-built-a-free-tool-that-runs-614-passive-security-checks-on-any-website-30hj</guid>
      <description>

&lt;p&gt;Tblue is an open-source web security scanner for people who own websites. You point it at your site, it tells you what looks wrong, and it runs entirely on your machine. No account, no API key, no findings uploaded anywhere.&lt;/p&gt;

&lt;p&gt;It is also a tool I got wrong in public, had corrected by a stranger who read the source properly, and then rebuilt. That second part is the more useful thing to write about, so I will start there.&lt;/p&gt;

&lt;h2&gt;
  
  
  The word "passive" was doing work it hadn't earned
&lt;/h2&gt;

&lt;p&gt;Tblue is meant to be blue-team only: read HTTP responses, headers, cookies, JavaScript, page content, and report. Nothing modified, nothing brute-forced.&lt;/p&gt;

&lt;p&gt;A security reviewer went through the source and pointed out that the default scan was submitting logins, sending password-reset requests, firing XXE payloads referencing &lt;code&gt;/etc/passwd&lt;/code&gt;, running port scans, and issuing Redis and Memcached commands. Two modules were even named &lt;code&gt;xxe_passive&lt;/code&gt; and &lt;code&gt;log_injection_passive&lt;/code&gt; while sending payloads.&lt;/p&gt;

&lt;p&gt;They were right. I had classified modules by what I assumed they did.&lt;/p&gt;

&lt;p&gt;So I measured instead. I ran every scanner against an instrumented HTTP server that recorded each request, and moved anything that issued a &lt;code&gt;POST&lt;/code&gt;/&lt;code&gt;PUT&lt;/code&gt;/&lt;code&gt;PATCH&lt;/code&gt;/&lt;code&gt;DELETE&lt;/code&gt;, or a &lt;code&gt;GET&lt;/code&gt; carrying a traversal, XXE, CRLF or injection payload.&lt;/p&gt;

&lt;p&gt;A depth-1 scan, before and after:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;&lt;/th&gt;
&lt;th&gt;requests&lt;/th&gt;
&lt;th&gt;POST/PATCH&lt;/th&gt;
&lt;th&gt;attack payloads&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;before&lt;/td&gt;
&lt;td&gt;1,664&lt;/td&gt;
&lt;td&gt;205&lt;/td&gt;
&lt;td&gt;104&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;after&lt;/td&gt;
&lt;td&gt;1,219&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;td&gt;0&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That produced three tiers:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Tier&lt;/th&gt;
&lt;th&gt;Flag&lt;/th&gt;
&lt;th&gt;Modules&lt;/th&gt;
&lt;th&gt;What it sends&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;Passive&lt;/td&gt;
&lt;td&gt;&lt;em&gt;(default)&lt;/em&gt;&lt;/td&gt;
&lt;td&gt;582&lt;/td&gt;
&lt;td&gt;GET/HEAD only&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Probe&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--probe&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;12&lt;/td&gt;
&lt;td&gt;Crafted but side-effect-free&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Intrusive&lt;/td&gt;
&lt;td&gt;&lt;code&gt;--active&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;20&lt;/td&gt;
&lt;td&gt;Submissions, payloads, port scans&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h2&gt;
  
  
  The part that matters: the tiering is enforced
&lt;/h2&gt;

&lt;p&gt;A classification you maintain by hand rots the first time someone adds a module. So it is a test:&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="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;_&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;cli&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;_SCANNER_REGISTRY&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
    &lt;span class="n"&gt;sent&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;clear&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="nf"&gt;klass&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;session&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;allowed_host&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;127.0.0.1&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;scan&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;target&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="n"&gt;bad&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;sent&lt;/span&gt;
           &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;r&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="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;_MUTATING&lt;/span&gt; &lt;span class="ow"&gt;or&lt;/span&gt; &lt;span class="n"&gt;_PAYLOAD&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;search&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt; &lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="n"&gt;r&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;])]&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;bad&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
        &lt;span class="n"&gt;offenders&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="n"&gt;key&lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="n"&gt;bad&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="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;assertEqual&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;offenders&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{})&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Every one of the 582 default scanners runs against a live instrumented server in CI. If a contributor adds a module that sends a POST and leaves it in the default tier, the build fails and names the module. The guarantee is checked on every push rather than promised in a README.&lt;/p&gt;

&lt;h2&gt;
  
  
  Credentials and blast radius
&lt;/h2&gt;

&lt;p&gt;The same review found that authenticated scans leaked. Passing &lt;code&gt;--bearer&lt;/code&gt; or &lt;code&gt;--cookie&lt;/code&gt; attached those values to a shared &lt;code&gt;requests.Session&lt;/code&gt;, and enrichment scanners used that same session to reach crt.sh, OSV and NVD. Those services received the caller's credentials.&lt;/p&gt;

&lt;p&gt;The fix is a scoped session: anything off-target goes through a session with no auth, no cookies, no user-supplied headers.&lt;/p&gt;

&lt;p&gt;While testing that I found a second path the reviewer had not seen. Choosing a session per request covers requests you issue yourself. Redirects are different — &lt;code&gt;requests&lt;/code&gt; follows them inside a single &lt;code&gt;send()&lt;/code&gt;, using whichever session began the call. So a target answering &lt;code&gt;302&lt;/code&gt; with an off-host &lt;code&gt;Location&lt;/code&gt; still handed that host your custom headers and cookie jar.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;--bearer&lt;/code&gt; and &lt;code&gt;--auth&lt;/code&gt; were already safe, because &lt;code&gt;requests&lt;/code&gt;' own &lt;code&gt;rebuild_auth&lt;/code&gt; drops &lt;code&gt;Authorization&lt;/code&gt; when the netloc changes. It leaves arbitrary headers alone, and a cookie set without an explicit domain matches any host:&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="k"&gt;class&lt;/span&gt; &lt;span class="nc"&gt;ScopedSession&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;Session&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
    &lt;span class="k"&gt;def&lt;/span&gt; &lt;span class="nf"&gt;rebuild_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;prepared_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
        &lt;span class="nf"&gt;super&lt;/span&gt;&lt;span class="p"&gt;().&lt;/span&gt;&lt;span class="nf"&gt;rebuild_auth&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prepared_request&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;response&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="nf"&gt;_host_in_scope&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;prepared_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;url&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;allowed_host&lt;/span&gt;&lt;span class="p"&gt;):&lt;/span&gt;
            &lt;span class="k"&gt;return&lt;/span&gt;
        &lt;span class="k"&gt;for&lt;/span&gt; &lt;span class="n"&gt;name&lt;/span&gt; &lt;span class="ow"&gt;in&lt;/span&gt; &lt;span class="n"&gt;self&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;scoped_headers&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;
            &lt;span class="n"&gt;prepared_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;name&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="n"&gt;prepared_request&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="n"&gt;headers&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;pop&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Cookie&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="bp"&gt;None&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you are building anything that carries credentials through &lt;code&gt;requests&lt;/code&gt;, this is worth knowing. &lt;code&gt;rebuild_auth&lt;/code&gt; is the hook, and it only handles &lt;code&gt;Authorization&lt;/code&gt; for you.&lt;/p&gt;

&lt;h2&gt;
  
  
  What it actually checks
&lt;/h2&gt;

&lt;p&gt;582 modules run in parallel on every scan, across TLS, headers, cookies, auth, OAuth, CSRF, injection sinks, XSS sinks, SSRF, secrets in JS bundles, API security, supply chain, cloud exposure, DNS and email, browser APIs, privacy, and compliance mappings for PCI-DSS, HIPAA, SOC 2, ISO 27001 and NIST CSF.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight console"&gt;&lt;code&gt;&lt;span class="gp"&gt;$&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;tblue &lt;span class="nt"&gt;-u&lt;/span&gt; https://example.com
&lt;span class="go"&gt;
╭──────────────────────────────────────────────────────────╮
│    B   Security Score  72/100   ██████████████░░░░░░     │
├──────────────────────────────────────────────────────────┤
│   ● 🟠 High        2  −20 pts                            │
│   ● 🟡 Medium      1  −5 pts                             │
│   ● 🔵 Low         3  −3 pts                             │
├──────────────────────────────────────────────────────────┤
│   1. [FAIL] Security headers                             │
│   2. [FAIL] CSP — missing                                │
│   3. [FAIL] Clickjacking — no framing protection         │
╰──────────────────────────────────────────────────────────╯
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Failing a PR on a missing header
&lt;/h2&gt;

&lt;p&gt;Someone asked for this and it turned out to be the most useful thing I added.&lt;/p&gt;

&lt;p&gt;Tblue had &lt;code&gt;--fail-below&lt;/code&gt;, which gates on the aggregate score. That is not enough. A site with Content-Security-Policy entirely missing still scores in the 80s once everything else passes:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;--fail-below 80   -&amp;gt;  exit 0    (score 84, no CSP at all)
--fail-on high    -&amp;gt;  exit 1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A score threshold cannot express "this specific misconfiguration must never merge." So there is now &lt;code&gt;--fail-on critical|high|medium|low&lt;/code&gt;, gating on the findings themselves. The two gates are independent, and either one failing fails the build.&lt;/p&gt;

&lt;p&gt;In GitHub Actions:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;taylannuhogluofficial-png/Tblue@v2&lt;/span&gt;
  &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;url&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;https://yoursite.com&lt;/span&gt;
    &lt;span class="na"&gt;fail-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;high&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Add &lt;code&gt;sarif: true&lt;/code&gt; and pass the file to &lt;code&gt;github/codeql-action/upload-sarif&lt;/code&gt;, and findings appear as annotations on the PR diff.&lt;/p&gt;

&lt;h2&gt;
  
  
  Output formats
&lt;/h2&gt;

&lt;p&gt;Terminal and HTML for humans. JSON for dashboards. SARIF for the GitHub Security tab. For SOC ingestion: Sigma rules, Splunk SPL, Microsoft Sentinel KQL, and CEF/LEEF/Elastic. Findings carry CWE identifiers and MITRE ATT&amp;amp;CK technique mappings.&lt;/p&gt;

&lt;p&gt;There is also an MCP server, so you can hand the scanners to an AI client:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;pip &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="s2"&gt;"tblue[mcp]"&lt;/span&gt;
tblue-mcp
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  What leaves your machine
&lt;/h2&gt;

&lt;p&gt;Findings are never uploaded. Some scanners look your target up in public intelligence sources — certificate transparency via crt.sh, vulnerability data from OSV and NVD — which discloses the domain being checked. Credentials go only to the target host and its subdomains. &lt;code&gt;--skip&lt;/code&gt; those modules for a fully offline scan. AI analysis is opt-in and transmits nothing unless you pass &lt;code&gt;--ai&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;One caveat worth stating: if &lt;code&gt;HTTP_PROXY&lt;/code&gt; is set, requests go through that proxy and it sees them. That is standard &lt;code&gt;requests&lt;/code&gt; behaviour, and it is in the README.&lt;/p&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;pip &lt;span class="nb"&gt;install &lt;/span&gt;tblue
tblue &lt;span class="nt"&gt;-u&lt;/span&gt; https://yoursite.com
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Python 3.10 through 3.13, MIT licensed. 6,741 tests, green in CI across all four versions.&lt;/p&gt;

&lt;p&gt;Only scan sites you own.&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/taylannuhogluofficial-png/Tblue" rel="noopener noreferrer"&gt;https://github.com/taylannuhogluofficial-png/Tblue&lt;/a&gt;&lt;/p&gt;

</description>
      <category>webdev</category>
      <category>opensource</category>
      <category>cybersecurity</category>
    </item>
  </channel>
</rss>
