<?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: Arturo Enrique Mata Garcia</title>
    <description>The latest articles on DEV Community by Arturo Enrique Mata Garcia (@matarturo).</description>
    <link>https://dev.to/matarturo</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%2F4116032%2Fdabe4cf7-4c46-44f9-8b14-b69e5a03b155.jpg</url>
      <title>DEV Community: Arturo Enrique Mata Garcia</title>
      <link>https://dev.to/matarturo</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/matarturo"/>
    <language>en</language>
    <item>
      <title>Beyond Header Grabbing: Detecting Apache mod_http2 (CVE-2026-119618) with Custom Nmap NSE</title>
      <dc:creator>Arturo Enrique Mata Garcia</dc:creator>
      <pubDate>Sat, 14 Nov 2026 19:23:34 +0000</pubDate>
      <link>https://dev.to/matarturo/beyond-header-grabbing-detecting-apache-modhttp2-cve-2026-119618-with-custom-nmap-nse-5606</link>
      <guid>https://dev.to/matarturo/beyond-header-grabbing-detecting-apache-modhttp2-cve-2026-119618-with-custom-nmap-nse-5606</guid>
      <description>&lt;p&gt;When the critical Apache mod_http2 vulnerability (CVE-2026-119618) surfaced — a memory corruption in &lt;code&gt;assign_headers()&lt;/code&gt; leading to potential RCE with a CVSS of 9.8 — the first reflex of most teams was to grep banners for "Apache/2.4.x" and call it a day. That approach has two fatal flaws: it fires on every Apache 2.4 build regardless of whether HTTP/2 is even enabled, and it collapses into silence the moment the target sets &lt;code&gt;ServerTokens Prod&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I built &lt;code&gt;apache-mod-http2-scanner.nse&lt;/code&gt; to solve exactly that: a &lt;strong&gt;non-intrusive&lt;/strong&gt; Nmap Scripting Engine module that identifies Apache HTTP Server instances &lt;em&gt;potentially&lt;/em&gt; affected by CVE-2026-119618, by combining version detection with actual HTTP/2 capability probing — h2 over TLS and h2c over plaintext — instead of trusting a header.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;⚠️ &lt;strong&gt;Honesty first:&lt;/strong&gt; the version ranges and detection criteria come from an internal bulletin (published 2026-11-11). The CVE could not be independently verified at the time of writing. Always confirm against Apache's official advisory (&lt;a href="https://httpd.apache.org/security/vulnerabilities_24.html" rel="noopener noreferrer"&gt;https://httpd.apache.org/security/vulnerabilities_24.html&lt;/a&gt;) before acting on results in production. The script itself is safe — it sends no exploit payloads.&lt;/p&gt;
&lt;/blockquote&gt;




&lt;h2&gt;
  
  
  🛑 The 3 Technical Challenges We Had to Solve
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The Dual-Protocol Problem (h2 vs h2c)
&lt;/h3&gt;

&lt;p&gt;The vulnerability only matters if &lt;strong&gt;HTTP/2 is actually active&lt;/strong&gt; on the target. But HTTP/2 can be enabled in two fundamentally different ways, and each requires a different probe:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;h2 over TLS&lt;/strong&gt; — negotiated via ALPN during the TLS handshake. The only correct way to detect it is to send a ClientHello advertising &lt;code&gt;h2&lt;/code&gt; and inspect the ServerHello. A plain HTTP request tells you nothing, because the negotiation already happened at the TLS layer.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;h2c over plaintext&lt;/strong&gt; — negotiated via an HTTP/1.1 &lt;code&gt;Upgrade: h2c&lt;/code&gt; request, which expects a &lt;code&gt;101 Switching Protocols&lt;/code&gt; response.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A scanner that only does one of these gives &lt;strong&gt;false negatives by design&lt;/strong&gt;. The script inspects the port's known state first: if Nmap knows the port is TLS, it uses ALPN; if it's plaintext, it uses h2c; if it doesn't know, it tries h2c and, if inconclusive, falls back to ALPN.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. The "Silent" Server (&lt;code&gt;ServerTokens Prod&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The moment an admin sets &lt;code&gt;ServerTokens Prod&lt;/code&gt;, the &lt;code&gt;Server&lt;/code&gt; header becomes &lt;code&gt;Apache&lt;/code&gt; with no version. Header grabbing returns nothing useful. But here's the nuance that most scanners miss: &lt;strong&gt;HTTP/2 being active is still a meaningful signal&lt;/strong&gt; even when the version is hidden.&lt;/p&gt;

&lt;p&gt;The script handles this by not lying to you. If it detects Apache + HTTP/2 active + hidden version, it returns &lt;code&gt;LIKELY VULNERABLE&lt;/code&gt; — not &lt;code&gt;VULNERABLE&lt;/code&gt; (we don't know the version) and not &lt;code&gt;NOT VULNERABLE&lt;/code&gt; (we know HTTP/2 is on, and most deployed 2.4.x builds fall in the affected window). If you prefer the stricter stance, set &lt;code&gt;hidden_version=unknown&lt;/code&gt; and it will report &lt;code&gt;NO CONCLUYENTE&lt;/code&gt; instead.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. The Binary Verdict Trap
&lt;/h3&gt;

&lt;p&gt;Most NSE vuln scripts output one of two states: &lt;code&gt;VULNERABLE&lt;/code&gt; or nothing. That's dangerous for a CVE whose affected range is a specific version window &lt;em&gt;and&lt;/em&gt; depends on a runtime feature flag. "Not vulnerable" and "couldn't tell" are not the same thing, especially when &lt;code&gt;NOT VULNERABLE&lt;/code&gt; will be silently skipped in a report.&lt;/p&gt;

&lt;p&gt;The script distinguishes four states:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;State&lt;/th&gt;
&lt;th&gt;Meaning&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;VULNERABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version in range &lt;strong&gt;and&lt;/strong&gt; HTTP/2 active&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;LIKELY VULNERABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Apache + HTTP/2 active, version hidden&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NO CONCLUYENTE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Couldn't complete a probe (TLS 1.3-only, timeout, filtered, &lt;code&gt;hidden_version=unknown&lt;/code&gt;)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;NOT VULNERABLE&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;Version out of range, or HTTP/2 not detected conclusively&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;&lt;code&gt;NO CONCLUYENTE&lt;/code&gt; is deliberately shown &lt;strong&gt;apart&lt;/strong&gt; from the &lt;code&gt;VULNERABLE:&lt;/code&gt; block, so it can't be mistaken for a positive. It tells you to retry with &lt;code&gt;-sV&lt;/code&gt;, bump the timeout, or verify locally — instead of silently dropping the host from your report.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔍 How the Detection Engine Works
&lt;/h2&gt;

&lt;p&gt;The script runs a short cascade — no exploits, no fuzzing:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Identify the server&lt;/strong&gt; via &lt;code&gt;nmap -sV&lt;/code&gt; or the &lt;code&gt;Server&lt;/code&gt; header. If it's not Apache httpd (Tomcat, nginx, Traffic Server), it exits without further probes.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Probe HTTP/2&lt;/strong&gt; with the appropriate mechanism for the port (ALPN or h2c Upgrade).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Compare the version&lt;/strong&gt; against the affected range and emit the verdict.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Two practical notes:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;-sV&lt;/code&gt; is strongly recommended.&lt;/strong&gt; Without it, the script relies solely on the &lt;code&gt;Server&lt;/code&gt; header, and non-standard TLS ports (9443, 10443…) may fall back to h2c and miss an h2-only server. You can force the probe with &lt;code&gt;apache-mod-http2-scanner.ssl=true&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The ALPN probe uses TLS 1.2.&lt;/strong&gt; A server that only accepts TLS 1.3 will produce &lt;code&gt;NO CONCLUYENTE&lt;/code&gt; — by design, not as a crash.&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  💻 The Result in Terminal (Two Real Scenarios)
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Scenario 1: Fully Exposed Environment (h2 over TLS)
&lt;/h3&gt;

&lt;p&gt;In this scenario the port is recognized as TLS, ALPN negotiates &lt;code&gt;h2&lt;/code&gt;, and the version is visible via &lt;code&gt;-sV&lt;/code&gt;. The script confirms both the affected version and the active HTTP/2 capability — a true positive.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT     STATE SERVICE  VERSION
8443/tcp open  ssl/http Apache httpd 2.4.70 ((Unix) OpenSSL/3.5.7)
| apache-mod-http2-scanner:
|   VULNERABLE:
|   Apache HTTP Server mod_http2 - corrupcion de memoria / RCE (severidad Critica)
|     State: VULNERABLE
|     IDs:  CVE:CVE-2026-119618
|     Risk factor: High  CVSSv3: 9.8
|     Check results:
|       Banner/Version: Apache/2.4.70 (nmap -sV)
|       HTTP/2 habilitado: SI - h2 (ALPN)
|       Rango afectado: 2.4.55 - 2.4.70 (corregido en 2.4.71)
|_      Script: apache-mod-http2-scanner v1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;code&gt;Risk factor: High&lt;/code&gt; is a limitation of Nmap's &lt;code&gt;vulns&lt;/code&gt; library, which only accepts High/Medium/Low. The real severity (Critical, CVSS 9.8) is shown in the title and the &lt;code&gt;CVSSv3&lt;/code&gt; field.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h3&gt;
  
  
  Scenario 2: Hidden Version (&lt;code&gt;ServerTokens Prod&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;Here the &lt;code&gt;Server&lt;/code&gt; header leaks only &lt;code&gt;Apache&lt;/code&gt;. The script still detects Apache and still confirms HTTP/2 is active, but because the version is unknown, it reports &lt;code&gt;LIKELY VULNERABLE&lt;/code&gt; — and tells you exactly what to do next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| apache-mod-http2-scanner:
|   VULNERABLE:
|   Apache HTTP Server mod_http2 - corrupcion de memoria / RCE (severidad Critica)
|     State: LIKELY VULNERABLE
|     IDs:  CVE:CVE-2026-119618
|     Risk factor: High  CVSSv3: 9.8
|     Check results:
|       Banner/Version: Apache (nmap -sV)
|       HTTP/2 habilitado: SI - h2c (Upgrade)
|       Rango afectado: 2.4.55 - 2.4.70 (corregido en 2.4.71)
|_      Version oculta: verifique localmente (httpd -v).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If you want this to read as &lt;code&gt;NO CONCLUYENTE&lt;/code&gt; instead — for a change-ticket process that won't accept "likely" — add &lt;code&gt;--script-args apache-mod-http2-scanner.hidden_version=unknown&lt;/code&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Field-Tested on Debian 13 (trixie)
&lt;/h2&gt;

&lt;p&gt;Documentation and screenshots are one thing; reproducibility is another. The script was validated end-to-end on a clean Debian 13 (trixie) VM with &lt;strong&gt;Nmap 7.95+dfsg-3&lt;/strong&gt; (from Debian repos), using the official &lt;code&gt;httpd:2.4.70&lt;/code&gt; Docker image — a real build inside the affected range. Three containers were spun up with different &lt;code&gt;Protocols&lt;/code&gt; configurations:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Container&lt;/th&gt;
&lt;th&gt;Port&lt;/th&gt;
&lt;th&gt;Configuration&lt;/th&gt;
&lt;th&gt;Expected&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h2c-lab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8081&lt;/td&gt;
&lt;td&gt;&lt;code&gt;Protocols h2c http/1.1&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VULNERABLE&lt;/code&gt; (h2c)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h2-lab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8443&lt;/td&gt;
&lt;td&gt;TLS + &lt;code&gt;Protocols h2 http/1.1&lt;/code&gt;
&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;VULNERABLE&lt;/code&gt; (h2/ALPN)&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;h1-lab&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;8082&lt;/td&gt;
&lt;td&gt;default &lt;code&gt;http/1.1&lt;/code&gt; only&lt;/td&gt;
&lt;td&gt;&lt;code&gt;NOT VULNERABLE&lt;/code&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;h3&gt;
  
  
  The baseline: default &lt;code&gt;httpd:2.4.70&lt;/code&gt; is &lt;em&gt;not&lt;/em&gt; vulnerable
&lt;/h3&gt;

&lt;p&gt;Before touching anything, the first scan against a stock &lt;code&gt;httpd:2.4.70&lt;/code&gt; container (no &lt;code&gt;Protocols&lt;/code&gt; override, no SSL) is instructive. The script does &lt;strong&gt;not&lt;/strong&gt; panic, does &lt;strong&gt;not&lt;/strong&gt; crash, and does &lt;strong&gt;not&lt;/strong&gt; guess. It correctly reports &lt;code&gt;UNKNOWN (unable to test)&lt;/code&gt; with an explicit reason:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| apache-mod-http2-scanner:
|   VULNERABLE:
|   Apache HTTP Server mod_http2 - corrupcion de memoria / RCE (severidad Critica)
|     State: UNKNOWN (unable to test)
|     IDs:  CVE:CVE-2026-119618
|     Check results:
|       Banner/Version: no disponible
|       HTTP/2 habilitado: NO detectado
|       Rango afectado: 2.4.55 - 2.4.70 (corregido en 2.4.71)
|       Script: apache-mod-http2-scanner v1.0.1
|       No se pudo determinar la version de Apache.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the correct behavior for a container that doesn't expose its version and doesn't enable HTTP/2: it isn't a false negative, it's an honest "I can't tell". That's exactly the distinction between &lt;code&gt;NOT VULNERABLE&lt;/code&gt; and &lt;code&gt;NO CONCLUYENTE&lt;/code&gt; the script was designed around.&lt;/p&gt;

&lt;h3&gt;
  
  
  The full three-container sweep
&lt;/h3&gt;

&lt;p&gt;After enabling &lt;code&gt;h2c&lt;/code&gt; on 8081, &lt;code&gt;h2&lt;/code&gt; + TLS on 8443, and leaving 8082 at HTTP/1.1-only, a single Nmap invocation against all three ports returns the three expected states side-by-side. &lt;code&gt;-sV&lt;/code&gt; is enabled so version detection feeds the script directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap -Pn -sV -p 8081,8082,8443 --script apache-mod-http2-scanner localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;PORT     STATE SERVICE  VERSION
8081/tcp open  http     Apache httpd 2.4.70 ((Unix))
| apache-mod-http2-scanner:
|   VULNERABLE:
|     State: VULNERABLE
|     Check results:
|       Banner/Version: Apache/2.4.70 (nmap -sV)
|       HTTP/2 habilitado: SI - h2c (Upgrade)
|       Rango afectado: 2.4.55 - 2.4.70 (corregido en 2.4.71)
|_      Script: apache-mod-http2-scanner v1.0.1

8082/tcp open  http     Apache httpd 2.4.70 ((Unix))
| apache-mod-http2-scanner:
|   NOT VULNERABLE:
|     State: NOT VULNERABLE
|     IDs:  CVE:CVE-2026-119618

8443/tcp open  ssl/http Apache httpd 2.4.70 ((Unix) OpenSSL/3.5.7)
| apache-mod-http2-scanner:
|   VULNERABLE:
|     State: VULNERABLE
|     Check results:
|       Banner/Version: Apache/2.4.70 (nmap -sV)
|       HTTP/2 habilitado: SI - h2 (ALPN)
|       Rango afectado: 2.4.55 - 2.4.70 (corregido en 2.4.71)
|_      Script: apache-mod-http2-scanner v1.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three different ports, &lt;strong&gt;same binary version&lt;/strong&gt;, three different verdicts — all correct. That is the core value of not stopping at the banner.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the script is doing internally (&lt;code&gt;nmap -d&lt;/code&gt;)
&lt;/h3&gt;

&lt;p&gt;The &lt;code&gt;-d&lt;/code&gt; flag on the same scan reveals exactly why 8443 is flagged and 8082 is not. The debug trace walks through the cascade in order:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;NSE: Starting apache-mod-http2-scanner against localhost (127.0.0.1:8443).
NSE: [apache-mod-http2-scanner 127.0.0.1:8443] identify: producto de -sV = Apache/2.4.70 (nmap -sV)
NSE: [apache-mod-http2-scanner 127.0.0.1:8443] modo de sondeo: tls
NSE: [apache-mod-http2-scanner 127.0.0.1:8443] ALPN: protocolo elegido = h2
NSE: Finished apache-mod-http2-scanner against localhost (127.0.0.1:8443).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Three lines that tell the whole story:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;identify: producto de -sV = Apache/2.4.70&lt;/code&gt;&lt;/strong&gt; — version detection from &lt;code&gt;-sV&lt;/code&gt; is the first source of truth; the &lt;code&gt;Server&lt;/code&gt; header is only a fallback.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;modo de sondeo: tls&lt;/code&gt;&lt;/strong&gt; — the script recognized the port as TLS and chose ALPN, not h2c. That's the "port-state-aware" branch from Challenge #1.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;ALPN: protocolo elegido = h2&lt;/code&gt;&lt;/strong&gt; — the ServerHello genuinely negotiated &lt;code&gt;h2&lt;/code&gt;. Not "Apache is present", not "HTTP/2 is suggested by a header", but "the TLS layer confirmed &lt;code&gt;h2&lt;/code&gt;".&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Independent confirmation with a plain &lt;code&gt;curl&lt;/code&gt; (the same probe the script performs, but from a different tool) agrees:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl -skI --http2 https://localhost:8443/ &lt;span class="o"&gt;|&lt;/span&gt; head -1
&lt;span class="c"&gt;# HTTP/2 200&lt;/span&gt;

curl -sI --http2-prior-knowledge http://localhost:8081/ &lt;span class="o"&gt;|&lt;/span&gt; head -1
&lt;span class="c"&gt;# HTTP/2 200&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The script's verdict is not based on a heuristic — it's based on the same wire-level signal &lt;code&gt;curl&lt;/code&gt; uses, parsed via Nmap's own &lt;code&gt;tls.record_read&lt;/code&gt;.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why the "NOT VULNERABLE" case matters as much as the vulnerable one
&lt;/h3&gt;

&lt;p&gt;The 8082 result (&lt;code&gt;NOT VULNERABLE&lt;/code&gt;) is the most commonly overlooked piece of evidence. The container runs the &lt;strong&gt;same &lt;code&gt;httpd:2.4.70&lt;/code&gt; binary&lt;/strong&gt; as the other two. If the script were a naive version-range checker, it would have flagged 8082 as vulnerable along with the rest — a false positive that would waste remediation cycles on a box with HTTP/2 disabled.&lt;/p&gt;

&lt;p&gt;It didn't, because HTTP/2 was conclusively &lt;em&gt;not&lt;/em&gt; detected. That's the point of separating "version in range" from "feature active": both must be true for a positive, and the script enforces it.&lt;/p&gt;




&lt;h2&gt;
  
  
  🚀 Try it in your lab
&lt;/h2&gt;

&lt;p&gt;The script is free, open-source, Apache-2.0 licensed, and auditable. The Debian 13 lab above is reproducible with a single shell script. The relevant container invocations:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# h2c (plaintext) — vulnerable path&lt;/span&gt;
docker run -d --name h2c-lab -p 8081:80 httpd:2.4.70 sh -c \
  &lt;span class="s2"&gt;"sed -i 's/^#LoadModule http2_module/LoadModule http2_module/' conf/httpd.conf \
   &amp;amp;&amp;amp; echo 'Protocols h2c http/1.1' &amp;gt;&amp;gt; conf/httpd.conf &amp;amp;&amp;amp; httpd-foreground"&lt;/span&gt;

&lt;span class="c"&gt;# h2 over TLS — vulnerable path&lt;/span&gt;
docker run -d --name h2-lab -p 8443:443 httpd:2.4.70 sh -c &lt;span class="s2"&gt;"
  sed -i -e 's/^#LoadModule http2_module/LoadModule http2_module/' \
         -e 's/^#LoadModule ssl_module/LoadModule ssl_module/' \
         -e 's/^#LoadModule socache_shmcb_module/LoadModule socache_shmcb_module/' conf/httpd.conf &amp;amp;&amp;amp;
  openssl req -x509 -newkey rsa:2048 -nodes -keyout conf/server.key -out conf/server.crt -days 2 -subj /CN=localhost &amp;amp;&amp;amp;
  printf 'Listen 443\nSSLSessionCache shmcb:logs/ssl_scache(512000)\n&amp;lt;VirtualHost *:443&amp;gt;\nSSLEngine on\nSSLCertificateFile conf/server.crt\nSSLCertificateKeyFile conf/server.key\nProtocols h2 http/1.1\n&amp;lt;/VirtualHost&amp;gt;\n' &amp;gt;&amp;gt; conf/httpd.conf &amp;amp;&amp;amp;
  httpd-foreground"&lt;/span&gt;

&lt;span class="c"&gt;# HTTP/1.1-only baseline — not vulnerable path&lt;/span&gt;
docker run -d --name h1-lab -p 8082:80 httpd:2.4.70

&lt;span class="c"&gt;# Scan all three&lt;/span&gt;
nmap -Pn -sV -p 8081,8082,8443 --script apache-mod-http2-scanner localhost
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;tests/lab.sh&lt;/code&gt; in the repository wraps the whole &lt;code&gt;up&lt;/code&gt; / &lt;code&gt;scan&lt;/code&gt; / &lt;code&gt;down&lt;/code&gt; cycle. To see &lt;code&gt;NOT VULNERABLE&lt;/code&gt; results too (which are silent by default), add &lt;code&gt;--script-args vulns.showall&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;A note on scope:&lt;/strong&gt; the script is non-intrusive, but scanning systems you don't own or have written authorization for is still illegal in most jurisdictions. Treat the &lt;code&gt;USO RESPONSABLE&lt;/code&gt; section of the README as the contract, not as boilerplate.&lt;/p&gt;

&lt;p&gt;🔗 &lt;strong&gt;Repository &amp;amp; Documentation:&lt;/strong&gt; &lt;a href="https://github.com/matarturo/apache-mod-http2-scanner" rel="noopener noreferrer"&gt;https://github.com/matarturo/apache-mod-http2-scanner&lt;/a&gt;&lt;/p&gt;




&lt;h2&gt;
  
  
  💬 Let's Discuss: What Does "Not Vulnerable" Actually Mean?
&lt;/h2&gt;

&lt;p&gt;To my fellow AppSec engineers and vulnerability-management folks here on dev.to:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;When your scanner returns &lt;code&gt;NOT VULNERABLE&lt;/code&gt;, how often is it actually "we couldn't tell"?&lt;/strong&gt; The &lt;code&gt;NO CONCLUYENTE&lt;/code&gt; state in this script exists because I kept seeing hosts silently dropped from reports simply because a probe timed out or a server only spoke TLS 1.3. I'd love to hear how your teams handle the unknown-vs-negative distinction — do you quarantine them, re-scan them, or let them slip?&lt;/p&gt;

&lt;p&gt;Also: if you're running Apache 2.4.55–2.4.70 with HTTP/2 enabled, the mitigation is simple — either upgrade to 2.4.71+, or strip &lt;code&gt;h2&lt;/code&gt; and &lt;code&gt;h2c&lt;/code&gt; from the &lt;code&gt;Protocols&lt;/code&gt; directive and run &lt;code&gt;apachectl configtest &amp;amp;&amp;amp; apachectl graceful&lt;/code&gt;. Either way, please re-scan afterward to confirm the change.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Useful Links &amp;amp; Documentation
&lt;/h2&gt;

&lt;p&gt;The script, the Docker lab, and the full argument reference live in the dedicated repository:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/matarturo/apache-mod-http2-scanner" rel="noopener noreferrer"&gt;https://github.com/matarturo/apache-mod-http2-scanner&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Direct script download (&lt;code&gt;.nse&lt;/code&gt;):&lt;/strong&gt; &lt;a href="https://github.com/matarturo/apache-mod-http2-scanner/blob/main/apache-mod-http2-scanner.nse" rel="noopener noreferrer"&gt;https://github.com/matarturo/apache-mod-http2-scanner/blob/main/apache-mod-http2-scanner.nse&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  📦 Installation (Linux, macOS, Windows)
&lt;/h3&gt;

&lt;p&gt;The installation is the standard Nmap NSE flow — copy the script into the scripts directory, rebuild the database, and verify it is recognised:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;sudo cp apache-mod-http2-scanner.nse /usr/share/nmap/scripts/
sudo nmap --script-updatedb
nmap --script-help apache-mod-http2-scanner   &lt;span class="c"&gt;# verify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--script-updatedb&lt;/code&gt; step is mandatory: Nmap will not resolve the script by name until the script database is rebuilt. If you prefer to run it without installing, point Nmap at the file directly:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;nmap -Pn --script apache-mod-http2-scanner.nse -p 443 &lt;span class="o"&gt;&amp;lt;&lt;/span&gt;target&lt;span class="o"&gt;&amp;gt;&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Platform-specific paths (macOS Homebrew, Windows) and the full &lt;code&gt;--script-args&lt;/code&gt; reference (&lt;code&gt;path&lt;/code&gt;, &lt;code&gt;min&lt;/code&gt;, &lt;code&gt;max&lt;/code&gt;, &lt;code&gt;timeout&lt;/code&gt;, &lt;code&gt;ssl&lt;/code&gt;, &lt;code&gt;hidden_version&lt;/code&gt;) are documented in the README.&lt;/p&gt;

&lt;h3&gt;
  
  
  ⚠️ &lt;code&gt;noexec&lt;/code&gt; mount workaround
&lt;/h3&gt;

&lt;p&gt;On systems where the Nmap scripts directory or the download location is mounted with &lt;code&gt;noexec&lt;/code&gt; (common on hardened &lt;code&gt;/usr/share&lt;/code&gt;, containers, and CI runners), copying the file is enough — Nmap loads &lt;code&gt;.nse&lt;/code&gt; files as Lua sources, it does not execute them as binaries. No &lt;code&gt;chmod +x&lt;/code&gt; and no interpreter workaround are needed for the script itself. If you download the &lt;code&gt;.nse&lt;/code&gt; to a &lt;code&gt;noexec&lt;/code&gt; location first, simply &lt;code&gt;cp&lt;/code&gt; it to &lt;code&gt;/usr/share/nmap/scripts/&lt;/code&gt; as shown above.&lt;/p&gt;

&lt;h3&gt;
  
  
  📚 What's in the repository
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;apache-mod-http2-scanner.nse&lt;/code&gt;&lt;/strong&gt; — the non-intrusive detector for CVE-2026-119618 described in this article.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;README&lt;/strong&gt; — decision table, argument reference, honest limitations (backports, &lt;code&gt;ServerTokens Prod&lt;/code&gt;, TLS 1.3-only servers, CDN fronting), and the mitigation steps.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Issue tracker&lt;/strong&gt; — if you hit a false positive or an unexpected &lt;code&gt;NO CONCLUYENTE or INCONCLUSIVE&lt;/code&gt;, attach the &lt;code&gt;nmap -d&lt;/code&gt; trace; it is the fastest way to get it triaged.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  🛡️ A final word on scope
&lt;/h3&gt;

&lt;p&gt;The script is non-intrusive: it does not exploit CVE-2026-119618, does not send crafted payloads, and does not attempt to trigger the memory corruption. But &lt;strong&gt;non-intrusive is not the same as authorized&lt;/strong&gt;. Scanning systems you do not own or do not have written permission to test is illegal in most jurisdictions, regardless of how gentle the probes are. Run it against your own infrastructure, your lab, or a client engagement with a signed scope. The &lt;code&gt;USO RESPONSABLE or RESPONSIBLE USE&lt;/code&gt; section of the README is the contract, not boilerplate.&lt;/p&gt;

&lt;p&gt;If you deploy it internally, I would love to hear what your vulnerability-management workflow does with the &lt;code&gt;INCONCLUSIVE&lt;/code&gt; bucket — that is the open question the script was built to surface, and the answer probably differs across every org reading this. 👇&lt;/p&gt;

</description>
      <category>devops</category>
      <category>automation</category>
      <category>architecture</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Beyond Header Grabbing: Detecting PHP2Shell Behind WAFs with Custom Nmap NSE</title>
      <dc:creator>Arturo Enrique Mata Garcia</dc:creator>
      <pubDate>Fri, 30 Oct 2026 11:06:17 +0000</pubDate>
      <link>https://dev.to/matarturo/beyond-header-grabbing-detecting-php2shell-behind-wafs-with-custom-nmap-nse-70a</link>
      <guid>https://dev.to/matarturo/beyond-header-grabbing-detecting-php2shell-behind-wafs-with-custom-nmap-nse-70a</guid>
      <description>&lt;p&gt;&lt;strong&gt;When the critical PHP2Shell vulnerabilities (CVE-2026-109618 / CVE-2026-109619) dropped, the security community scrambled for detection tools.&lt;/strong&gt; However, standard scanners have a fatal flaw: they fail against modern WAFs. If a server sits behind Cloudflare or Akamai, standard header grabbing returns nothing.&lt;/p&gt;

&lt;p&gt;To solve this, I developed http-vuln-php2shell.nse, an open-source Nmap Scripting Engine (NSE) module.&lt;/p&gt;

&lt;p&gt;The response has been incredible—over 217 clones on GitHub in the first few days. But getting here wasn't just about writing Lua; it was about solving specific architectural challenges to make the script reliable, cross-platform, and WAF-resistant.&lt;/p&gt;

&lt;p&gt;Here is a deep dive into the technical hurdles we solved and how the script works under the hood.&lt;/p&gt;




&lt;p&gt;🛑 The 3 Technical Challenges We Had to Solve&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The "Silent" WAF (Cloudflare/Akamai)&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If an infrastructure sits behind a CDN, the HTTP X-Powered-By and Server headers are usually stripped or replaced. A passive scan returns nil. &lt;br&gt;
The Solution: I implemented a multi-layered inspection. If the headers lie, the script relies on passive fingerprinting via the PHPSESSID cookie (Technique C). Even if Cloudflare hides the server version, it often fails to strip the session cookie, revealing the underlying PHP backend.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;False Positives on Patched Servers&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Just detecting "PHP" isn't enough; you need to know if it's vulnerable. &lt;br&gt;
The Solution: We implemented a strict syntactic evaluation of the PHP version. The script compares the detected version against the patched thresholds for October 2026 (PHP 8.2.35, 8.3.36, 8.4.27, 8.5.12). If the server runs PHP 8.5.12, the script returns INFO (not vulnerable). If it runs PHP 7.4, it flags it as LIKELY VULNERABLE.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;The Nmap vulns Library Bug on Windows&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;This was the hardest part. Nmap has a native vulns library to format vulnerability outputs beautifully. However, on Windows (Nmap 7.98+), the vulns.Report:new() object has a nasty habit of crashing the entire script (ERROR: Script execution failed).&lt;br&gt;
The Solution: I discarded the buggy vulns library and built a custom native reporting engine. By using Lua's table.insert, the script perfectly mimics Nmap's official VULNERABLE: output block without any dependencies, ensuring 100% cross-platform stability.&lt;/p&gt;



&lt;p&gt;🔍 The 5 Inspection Vectors&lt;/p&gt;

&lt;p&gt;The engine doesn't just do a single request; it runs a cascading behavioral inspection designed for black-box environments:&lt;/p&gt;

&lt;p&gt;· A &amp;amp; B (Headers): Parses X-Powered-By and Server for quick wins.&lt;br&gt;
· C (Stateful Fingerprinting): Looks for PHPSESSID cookies in the root response.&lt;br&gt;
· D (Forced Exceptions): Injects malformed parameters (/index.php?[]=) to force the server to reveal Stack Traces (if display_errors is on).&lt;br&gt;
· E (Differential Verification): Compares a baseline request against a malformed input. If the malformed input triggers a 500 error or PHP trace not present in the baseline, it flags anomalous backend behavior.&lt;/p&gt;



&lt;p&gt;💻 The Result in Terminal (Two Real Scenarios)&lt;/p&gt;

&lt;p&gt;To truly understand the power of this script, let's look at how it adapts to different server configurations. The output isn't always the same; it depends on what the server exposes and what the WAF hides.&lt;/p&gt;

&lt;p&gt;Scenario 1: Fully Exposed Environment (Modern Unpatched PHP)&lt;/p&gt;

&lt;p&gt;In this scenario, the server is completely exposed. It doesn't hide the Server header, and it leaks the X-Powered-By header. The script easily triggers Techniques A and B. Notice that it catches a modern PHP 8.x version that missed the October 2026 patch.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| http-vuln-php2shell:
|   VULNERABLE:
|   Title: PHP2Shell (CVE-2026-109618 / CVE-2026-109619) Potential Exposure
|   IDs: CVE:2026-109618, CVE:2026-109619
|   State: LIKELY VULNERABLE
|   ALERTA (A): PHP2Shell potencial via X-Powered-By -&amp;gt; PHP/8.4.25
|_  ALERTA (B): PHP2Shell potencial via cabecera Server -&amp;gt; PHP/8.4.25
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Scenario 2: WAF / Proxied Environment (Header Obfuscation)&lt;/p&gt;

&lt;p&gt;Now, look at what happens when a proxy or WAF (like Cloudflare) is trying to hide the server version. The Server header is stripped, so Technique B fails. However, the script is resilient: it still catches the X-Powered-By header (Technique A) and, more importantly, uses passive fingerprinting (Technique C) to detect the PHPSESSID cookie, confirming the PHP backend even when the WAF tries to hide it.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;| http-vuln-php2shell:
|   VULNERABLE:
|   Title: PHP2Shell (CVE-2026-109618 / CVE-2026-109619) Potential Exposure
|   IDs: CVE:2026-109618, CVE:2026-109619
|   State: LIKELY VULNERABLE
|   ALERTA (A): PHP2Shell potencial via X-Powered-By -&amp;gt; PHP/5.6.40
|_  AVISO (C): Backend PHP detectado por cookie PHPSESSID (cabeceras de version pueden estar ocultas).
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🚀 Try it in your lab&lt;/p&gt;

&lt;p&gt;The tool is completely free, open-source, and auditable. You can clone it, spin up a vulnerable Docker container, and test it in seconds:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/matarturo/php2shell-nse-scanner.git
nmap &lt;span class="nt"&gt;-p&lt;/span&gt; 8080 &lt;span class="nt"&gt;-Pn&lt;/span&gt; &lt;span class="nt"&gt;-n&lt;/span&gt; &lt;span class="nt"&gt;--script&lt;/span&gt; http-vuln-php2shell &lt;span class="nt"&gt;--script-args&lt;/span&gt; http.useragent&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"Mozilla/5.0"&lt;/span&gt; 127.0.0.1
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;🔗 Repository &amp;amp; Documentation: &lt;a href="https://github.com/matarturo/php2shell-nse-scanner" rel="noopener noreferrer"&gt;https://github.com/matarturo/php2shell-nse-scanner&lt;/a&gt;&lt;/p&gt;




&lt;p&gt;💬 Let's Discuss: WAF Evasion Tactics&lt;/p&gt;

&lt;p&gt;To my fellow AppSec engineers and Red Teamers here on dev.to:&lt;/p&gt;

&lt;p&gt;How often do you find servers that configure a reverse proxy (like NGINX or Cloudflare) to hide the Server header, but forget to filter the PHPSESSID or X-Powered-By headers?&lt;/p&gt;

&lt;p&gt;I'd love to hear your experiences in the comments below. 👇&lt;/p&gt;




&lt;p&gt;I want to thank the security community, including creators like Federico Miskinich from @hablemosdehackin in Instragram, who have independently tested this script and provided valuable feedback that helped refine its edge cases.&lt;/p&gt;




</description>
      <category>devops</category>
      <category>automation</category>
      <category>architecture</category>
      <category>cybersecurity</category>
    </item>
    <item>
      <title>Lynx: a passive security auditor for Linux</title>
      <dc:creator>Arturo Enrique Mata Garcia</dc:creator>
      <pubDate>Thu, 01 Oct 2026 19:44:25 +0000</pubDate>
      <link>https://dev.to/matarturo/lynx-a-passive-read-only-security-auditor-for-debian-and-ubuntu-2fll</link>
      <guid>https://dev.to/matarturo/lynx-a-passive-read-only-security-auditor-for-debian-and-ubuntu-2fll</guid>
      <description>&lt;p&gt;Some servers I look after run firewalls generated years ago by &lt;strong&gt;Firewall Builder (fwbuilder)&lt;/strong&gt;, on distros where &lt;code&gt;iptables&lt;/code&gt; quietly became a front end for nftables. When I need a quick answer to "is this box reasonably configured?", I don't want to install a framework, and I definitely don't want a tool that changes things while it looks.&lt;/p&gt;

&lt;p&gt;So I wrote &lt;strong&gt;Lynx&lt;/strong&gt;: a single Bash script that audits a Debian or Ubuntu host in read-only mode, prints a scored report, and saves hash-verified evidence for whoever has to review it.&lt;/p&gt;

&lt;p&gt;Lynx is &lt;strong&gt;not&lt;/strong&gt; a compliance tool, and I'll be clear about that below. It's a fast, readable first pass.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;TL;DR&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Lynx audits a Debian or Ubuntu host in &lt;strong&gt;read-only&lt;/strong&gt; mode: 29 scored controls, a 0-100 score, and SHA-256 hashed evidence.&lt;/li&gt;
&lt;li&gt;It understands iptables-legacy, iptables-nft, nftables and &lt;strong&gt;fwbuilder&lt;/strong&gt;-generated rules.&lt;/li&gt;
&lt;li&gt;On a real Debian 13 dev server it scored &lt;strong&gt;47/100&lt;/strong&gt;. An earlier build had scored 56, and the difference was a bug that let Docker's rules pass for a host firewall.&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;It is a fast first pass, not a compliance scanner.&lt;/p&gt;
&lt;h2&gt;
  
  
  What Lynx is
&lt;/h2&gt;
&lt;/li&gt;
&lt;li&gt;&lt;p&gt;One Bash script, no dependencies beyond what Debian/Ubuntu already ship (&lt;code&gt;awk&lt;/code&gt;, &lt;code&gt;grep&lt;/code&gt;, &lt;code&gt;sed&lt;/code&gt;, &lt;code&gt;find&lt;/code&gt;, &lt;code&gt;stat&lt;/code&gt;).&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Needs root, because it reads &lt;code&gt;/etc/shadow&lt;/code&gt;, firewall rulesets and kernel settings.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Passive&lt;/strong&gt;: it never edits configuration, installs, or removes packages. It doesn't even run &lt;code&gt;apt update&lt;/code&gt;, because that would be an active action.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;
&lt;p&gt;Apache-2.0 licensed.&lt;/p&gt;
&lt;h2&gt;
  
  
  What it checks
&lt;/h2&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Six sections add up to a 100-point score across 29 controls. A seventh section is informational and doesn't score.&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;Section&lt;/th&gt;
&lt;th&gt;Weight&lt;/th&gt;
&lt;th&gt;Examples&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;1&lt;/td&gt;
&lt;td&gt;Kernel (sysctl)&lt;/td&gt;
&lt;td&gt;30&lt;/td&gt;
&lt;td&gt;SYN cookies, ICMP redirects, source routing, &lt;code&gt;rp_filter&lt;/code&gt;, &lt;code&gt;kptr_restrict&lt;/code&gt;, &lt;code&gt;ptrace_scope&lt;/code&gt;, ASLR&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;2&lt;/td&gt;
&lt;td&gt;Network stack&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;conntrack, IP forwarding, &lt;code&gt;bridge-nf-call-iptables&lt;/code&gt;
&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;3&lt;/td&gt;
&lt;td&gt;Firewall&lt;/td&gt;
&lt;td&gt;25&lt;/td&gt;
&lt;td&gt;Rules on the INPUT path, stateful rule, default-deny on INPUT/FORWARD, IPv6&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;4&lt;/td&gt;
&lt;td&gt;SSH and hardening&lt;/td&gt;
&lt;td&gt;15&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;PermitRootLogin&lt;/code&gt;, &lt;code&gt;PasswordAuthentication&lt;/code&gt;, blocked legacy modules, core dumps&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;5&lt;/td&gt;
&lt;td&gt;Users&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;Single UID 0, no empty passwords, system accounts without login shells&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;6&lt;/td&gt;
&lt;td&gt;Permissions&lt;/td&gt;
&lt;td&gt;10&lt;/td&gt;
&lt;td&gt;
&lt;code&gt;/etc/shadow&lt;/code&gt;, &lt;code&gt;/etc/passwd&lt;/code&gt;, world-writable directories&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;7&lt;/td&gt;
&lt;td&gt;Info (no score)&lt;/td&gt;
&lt;td&gt;–&lt;/td&gt;
&lt;td&gt;Listening ports, sudoers &lt;code&gt;NOPASSWD&lt;/code&gt;, pending security updates, mount options, SUID inventory&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Each control returns &lt;code&gt;PASS&lt;/code&gt;, &lt;code&gt;FAIL&lt;/code&gt;, or &lt;code&gt;N/A&lt;/code&gt;. &lt;code&gt;N/A&lt;/code&gt; means "doesn't apply here" (IPv6 disabled, no &lt;code&gt;sshd&lt;/code&gt; installed) and awards the points so you aren't penalized for attack surface you don't have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Design decisions worth explaining
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Read sysctl from &lt;code&gt;/proc/sys&lt;/code&gt;.&lt;/strong&gt; Reading the files directly means Lynx doesn't depend on the &lt;code&gt;sysctl&lt;/code&gt; binary or on &lt;code&gt;PATH&lt;/code&gt; being sane under &lt;code&gt;su&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Look at all three firewall backends.&lt;/strong&gt; On modern Debian, &lt;code&gt;iptables&lt;/code&gt; may point to &lt;code&gt;iptables-nft&lt;/code&gt;, and rules loaded in legacy and in nft don't see each other. Lynx reads &lt;code&gt;iptables-legacy-save&lt;/code&gt;, &lt;code&gt;iptables-nft-save&lt;/code&gt; and &lt;code&gt;nft list ruleset&lt;/code&gt;, and warns if rules live in more than one place.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Understand fwbuilder output.&lt;/strong&gt; fwbuilder generates &lt;code&gt;-m state --state ESTABLISHED,RELATED&lt;/code&gt; rather than &lt;code&gt;-m conntrack --ctstate&lt;/code&gt;, and often ends INPUT with a jump to a user chain that finishes in &lt;code&gt;DROP&lt;/code&gt;. Lynx recognizes both styles and follows jumps (up to a depth limit) to find the final deny.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Evidence you can verify.&lt;/strong&gt; Raw dumps (rulesets, &lt;code&gt;sshd -T&lt;/code&gt;, sudoers, listening ports, a copy of any &lt;code&gt;.fw&lt;/code&gt; scripts found) go to &lt;code&gt;lynx_evidencias_&amp;lt;timestamp&amp;gt;/&lt;/code&gt; with a &lt;code&gt;SHA256SUMS&lt;/code&gt; manifest. The report gets its own &lt;code&gt;.sha256&lt;/code&gt;. The hash is computed after the report is finished, so it can't change the thing it protects.&lt;/p&gt;

&lt;h2&gt;
  
  
  Putting it to the test on a real Debian 13 dev server
&lt;/h2&gt;

&lt;p&gt;Tests with simulated dumps prove that the parsers work. They don't prove the tool says anything useful about a messy, real machine. So I ran Lynx on a development server I'm free to poke at.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The test machine&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Debian GNU/Linux 13 (trixie), kernel 6.12.96, amd64&lt;/li&gt;
&lt;li&gt;Docker installed: one bridge (&lt;code&gt;docker0&lt;/code&gt;) and a container published on port 3000&lt;/li&gt;
&lt;li&gt;OpenSSH on 22, a Python service on 3001, &lt;code&gt;avahi-daemon&lt;/code&gt;, and &lt;code&gt;dhcpcd&lt;/code&gt; on several interfaces&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;iptables&lt;/code&gt; is the nft variant (&lt;code&gt;/usr/sbin/xtables-nft-multi&lt;/code&gt;), no legacy rules, no fwbuilder, and &lt;code&gt;ufw&lt;/code&gt; enabled as a systemd service&lt;/li&gt;
&lt;li&gt;A typical dev box: working, but never through a hardening pass
One note on the output below: I kept it verbatim, and the report is in Spanish. The firewall labels read, in order: rules loaded, stateful rule, INPUT default-deny, FORWARD default-deny, IPv6 filtered.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  First run: 56/100
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[PASS] +5 pts | Firewall rules loaded into the kernel (any backend)
[PASS] +7 pts | Stateful ESTABLISHED,RELATED rule (conntrack, state, or nft ct state)
[FAIL]   0 pts | INPUT with DROP/REJECT policy or final deny rule
[PASS] +3 pts | FORWARD with DROP/REJECT policy (or forwarding disabled)
[PASS] +3 pts | IPv6 filtered by firewall (or IPv6 disabled)
...
 AUDIT SCORE: 56 / 100 (56%)  -  29 controls evaluated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Four passes and one failure reads like a firewall that mostly works. It wasn't. The same report listed nine sockets listening on all interfaces, SSH among them, and the INPUT check was the only line telling the truth.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why 56 was too generous
&lt;/h3&gt;

&lt;p&gt;Docker installs its own rules in the &lt;strong&gt;FORWARD&lt;/strong&gt; path, including an &lt;code&gt;ESTABLISHED,RELATED&lt;/code&gt; accept. My checks searched &lt;em&gt;any&lt;/em&gt; chain for those rules, so Docker's traffic rules satisfied "the host has a stateful firewall". That's a false positive that makes a machine look safer than it is, which is the worst kind of bug in a security tool.&lt;/p&gt;

&lt;p&gt;The fix was to evaluate only chains &lt;strong&gt;reachable from INPUT&lt;/strong&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Chains reachable from a starting chain (follows -j / -g into user chains)&lt;/span&gt;
ipt_reach&lt;span class="o"&gt;()&lt;/span&gt; &lt;span class="o"&gt;{&lt;/span&gt;   &lt;span class="c"&gt;# $1 = iptables-save dump (filter table), $2 = starting chain&lt;/span&gt;
    &lt;span class="nb"&gt;awk&lt;/span&gt; &lt;span class="nt"&gt;-v&lt;/span&gt; &lt;span class="nv"&gt;start&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$2&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt; &lt;span class="s1"&gt;'
        $1=="-A" { for (i=3;i&amp;lt;=NF;i++) if ($i=="-j" || $i=="-g") { n++; from[n]=$2; to[n]=$(i+1) } }
        END {
            seen[start]=1
            do { changed=0
                 for (k=1;k&amp;lt;=n;k++) if ((from[k] in seen) &amp;amp;&amp;amp; !(to[k] in seen)) { seen[to[k]]=1; changed=1 }
            } while (changed)
            for (c in seen) print c
        }'&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="nv"&gt;$1&lt;/span&gt;&lt;span class="s2"&gt;"&lt;/span&gt;
&lt;span class="o"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The stateful and "rules present" checks now only count rules in INPUT or in chains INPUT jumps to, which is how UFW's &lt;code&gt;ufw-before-input&lt;/code&gt; and fwbuilder's &lt;code&gt;RULE_N&lt;/code&gt; chains work. The nftables side does the same, starting from chains with &lt;code&gt;hook input&lt;/code&gt; and following &lt;code&gt;jump&lt;/code&gt; and &lt;code&gt;goto&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;I validated this against 25 simulated cases: Docker alone, UFW active, UFW plus Docker, fwbuilder with a direct rule, fwbuilder via a &lt;code&gt;RULE_1&lt;/code&gt; chain, native nftables with sets, and an empty ruleset. That exercise also exposed a bug in my first nft parser, which treated a rule beginning with &lt;code&gt;counter packets …&lt;/code&gt; as a named-object declaration and lost track of the chain.&lt;/p&gt;

&lt;p&gt;Two more checks needed the same kind of honesty. &lt;code&gt;bridge-nf-call-iptables&lt;/code&gt; now ignores bridges created by Docker, libvirt, LXD, CNI and Podman. And &lt;code&gt;rp_filter&lt;/code&gt; is evaluated the way the kernel does it: the maximum of the &lt;code&gt;all&lt;/code&gt; value and the per-interface value, not &lt;code&gt;all&lt;/code&gt; alone.&lt;/p&gt;

&lt;h3&gt;
  
  
  Second run: 47/100
&lt;/h3&gt;

&lt;p&gt;Same machine, corrected build:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;[FAIL]   0 pts | Firewall rules filtering incoming traffic (INPUT)
[FAIL]   0 pts | Stateful rule ESTABLISHED,RELATED (conntrack, state, or nft ct state)
[FAIL]   0 pts | INPUT with DROP/REJECT policy or final deny rule
[PASS] +3 pts | FORWARD with DROP/REJECT policy (or forwarding disabled)
[FAIL]   0 pts | IPv6: inbound traffic filtered by firewall (or IPv6 disabled)
...
 AUDIT SCORE: 47 / 100 (47%)  -  29 controls evaluated
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The score went &lt;strong&gt;down&lt;/strong&gt; by nine points, and the arithmetic is fully explained by five controls:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Control&lt;/th&gt;
&lt;th&gt;First build&lt;/th&gt;
&lt;th&gt;Second build&lt;/th&gt;
&lt;th&gt;Points&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;code&gt;rp_filter&lt;/code&gt; (effective, per interface)&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;+3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;code&gt;bridge-nf-call-iptables&lt;/code&gt;&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;N/A (only &lt;code&gt;docker0&lt;/code&gt;)&lt;/td&gt;
&lt;td&gt;+3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Rules on the INPUT path&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;−5&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;Stateful rule on INPUT&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;−7&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;IPv6 inbound filtering&lt;/td&gt;
&lt;td&gt;PASS&lt;/td&gt;
&lt;td&gt;FAIL&lt;/td&gt;
&lt;td&gt;−3&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Total&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;56&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;47&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;&lt;strong&gt;−9&lt;/strong&gt;&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;Two corrections raised the score because the old checks were too strict. Three lowered it because they were too lenient. The net effect is a lower number and a more truthful report.&lt;/p&gt;

&lt;p&gt;The backends section shows what was really loaded:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;- iptables binary: /usr/sbin/xtables-nft-multi
- iptables-legacy rules: 0 (IPv4) / 0 (IPv6)
- iptables-nft rules: 15 (IPv4) / 7 (IPv6)
- nftables (ruleset) : with rules
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;So there are 15 IPv4 and 7 IPv6 rules loaded, but Lynx finds none reachable from INPUT and no default-deny on it. The "persistence" block lists &lt;code&gt;ufw&lt;/code&gt; as enabled in systemd. That only means the service starts at boot, not that it enforces a policy, which is exactly why Lynx reports persistence as information rather than as a pass.&lt;/p&gt;

&lt;h3&gt;
  
  
  What the rest of the report says about the box
&lt;/h3&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Exposure.&lt;/strong&gt; 18 listening sockets, 9 of them bound to all interfaces: OpenSSH (22, IPv4 and IPv6), the Python service (3001), &lt;code&gt;docker-proxy&lt;/code&gt; (3000, IPv4 and IPv6), and &lt;code&gt;avahi-daemon&lt;/code&gt; (mDNS on 5353 plus random UDP ports). mDNS on a server is rarely needed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SSH.&lt;/strong&gt; Both the &lt;code&gt;PermitRootLogin&lt;/code&gt; and the &lt;code&gt;PasswordAuthentication&lt;/code&gt; checks failed. With port 22 reachable on every interface and no inbound filter, that is the combination that deserves attention first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Kernel.&lt;/strong&gt; Failures: ICMP redirects (IPv4 and IPv6), &lt;code&gt;send_redirects&lt;/code&gt;, &lt;code&gt;kptr_restrict&lt;/code&gt;, &lt;code&gt;ptrace_scope&lt;/code&gt;, and IP forwarding (most likely because of Docker). Passes: SYN cookies, source routing, &lt;code&gt;rp_filter&lt;/code&gt;, &lt;code&gt;dmesg_restrict&lt;/code&gt;, ASLR and &lt;code&gt;suid_dumpable&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patching.&lt;/strong&gt; 106 packages upgradable, 36 of them security updates, and no &lt;code&gt;unattended-upgrades&lt;/code&gt;. The apt cache was 0 days old, so the count is trustworthy.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Mount options.&lt;/strong&gt; &lt;code&gt;/tmp&lt;/code&gt; and &lt;code&gt;/dev/shm&lt;/code&gt; are separate mounts but lack &lt;code&gt;noexec&lt;/code&gt;. &lt;code&gt;/var/tmp&lt;/code&gt; and &lt;code&gt;/home&lt;/code&gt; inherit from &lt;code&gt;/&lt;/code&gt; and lack &lt;code&gt;nodev&lt;/code&gt;, &lt;code&gt;nosuid&lt;/code&gt; and &lt;code&gt;noexec&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Files.&lt;/strong&gt; 49 SUID/SGID binaries, no world-writable files, and 22,273 files with no owner or group. I haven't investigated that last number; on a Docker host I would start with container volumes and image layers.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Good news.&lt;/strong&gt; AppArmor active, NTP synchronized, journald persistent, a single UID 0, no empty passwords, correct permissions on &lt;code&gt;/etc/shadow&lt;/code&gt; and &lt;code&gt;/etc/passwd&lt;/code&gt;, no legacy network services installed (telnet, rsh, NIS, tftp, xinetd), and no world-writable directories without the sticky bit.
The final level was &lt;strong&gt;LOW (47%)&lt;/strong&gt;, which I think is a fair description of an unhardened dev server.&lt;/li&gt;
&lt;/ul&gt;

&lt;h3&gt;
  
  
  Cross-checking Lynx's findings
&lt;/h3&gt;

&lt;p&gt;A report is only worth trusting if you can verify it by hand. These commands reproduce the key findings, and the raw material is also in the evidence directory:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;ufw status verbose                          &lt;span class="c"&gt;# is UFW actually enforcing anything?&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;iptables &lt;span class="nt"&gt;-S&lt;/span&gt; INPUT                           &lt;span class="c"&gt;# INPUT policy and rules (nft backend)&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;nft list ruleset | &lt;span class="nb"&gt;grep&lt;/span&gt; &lt;span class="nt"&gt;-A3&lt;/span&gt; &lt;span class="s1"&gt;'hook input'&lt;/span&gt;    &lt;span class="c"&gt;# base chains on the input hook&lt;/span&gt;
ss &lt;span class="nt"&gt;-tulpn&lt;/span&gt;                                        &lt;span class="c"&gt;# what is listening, and who owns it&lt;/span&gt;
sysctl &lt;span class="nt"&gt;-a&lt;/span&gt; | &lt;span class="nb"&gt;grep &lt;/span&gt;rp_filter                       &lt;span class="c"&gt;# per-interface rp_filter values&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  What I took from this
&lt;/h3&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A score is only as honest as its scoping.&lt;/strong&gt; Counting rules from any chain made Docker's traffic rules look like a host firewall. Scoping the check to what the inbound path can actually reach fixed it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Simulated tests and real runs catch different bugs.&lt;/strong&gt; The simulations found a parser bug. Only the real machine showed that a perfectly working check was answering the wrong question.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A lower score is not a regression.&lt;/strong&gt; After the fix the number was worse and the report was better.
## Try it
&lt;/li&gt;
&lt;/ol&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/&amp;lt;your-username&amp;gt;/lynx.git
&lt;span class="nb"&gt;cd &lt;/span&gt;lynx
less lynx.sh          &lt;span class="c"&gt;# please read it before running anything as root&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x lynx.sh
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./lynx.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Reports go to &lt;code&gt;/var/lib/lynx/&amp;lt;hostname&amp;gt;/&lt;/code&gt; by default. Useful variables:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;REPORT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/audits ./lynx.sh        &lt;span class="c"&gt;# custom output directory&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;LYNX_FLAT&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 &lt;span class="nv"&gt;REPORT_DIR&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;/srv/audits ./lynx.sh   &lt;span class="c"&gt;# no per-host subdirectory&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;ES_ROUTER&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./lynx.sh                   &lt;span class="c"&gt;# host routes on purpose: forwarding isn't a failure&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;SKIP_SLOW&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./lynx.sh                   &lt;span class="c"&gt;# skip the slow find-based inventories&lt;/span&gt;
&lt;span class="nb"&gt;sudo &lt;/span&gt;&lt;span class="nv"&gt;SIN_EVIDENCIAS&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;1 ./lynx.sh              &lt;span class="c"&gt;# report only, no raw evidence&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One naming note: Debian and Ubuntu already have a text-mode web browser called &lt;code&gt;lynx&lt;/code&gt;. If you install the script system-wide, give it a different name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sudo install&lt;/span&gt; &lt;span class="nt"&gt;-m&lt;/span&gt; 0750 &lt;span class="nt"&gt;-o&lt;/span&gt; root &lt;span class="nt"&gt;-g&lt;/span&gt; root lynx.sh /usr/local/sbin/lynx-audit
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;To verify an audit later:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; lynx_reporte_&amp;lt;timestamp&amp;gt;_&amp;lt;pid&amp;gt;.txt.sha256
&lt;span class="nb"&gt;cd &lt;/span&gt;lynx_evidencias_&amp;lt;timestamp&amp;gt;_&amp;lt;pid&amp;gt; &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;sha256sum&lt;/span&gt; &lt;span class="nt"&gt;-c&lt;/span&gt; SHA256SUMS
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Limitations
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Narrow coverage.&lt;/strong&gt; 29 scored controls is a small slice of something like a CIS Benchmark. It doesn't cover filesystem layout, PAM and password policy, most &lt;code&gt;sshd&lt;/code&gt; options, auditd or integrity monitoring. Some checks resemble CIS recommendations but differ; for example, Lynx accepts &lt;code&gt;PermitRootLogin prohibit-password&lt;/code&gt;, while CIS requires &lt;code&gt;no&lt;/code&gt;. For formal compliance, use OpenSCAP or CIS-CAT.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Small validation base.&lt;/strong&gt; One real Debian 13 development server (the two runs described above), an Ubuntu 24.04 environment, and simulated firewall dumps for everything else. Treat it as young software and try it on a test machine first.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Static &lt;code&gt;sshd&lt;/code&gt; fallback.&lt;/strong&gt; If &lt;code&gt;sshd -T&lt;/code&gt; fails, Lynx reads the config files directly and doesn't interpret &lt;code&gt;Match&lt;/code&gt; blocks.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Patch counts depend on apt's cache.&lt;/strong&gt; Lynx warns when the cache is old.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;fwbuilder detection is heuristic.&lt;/strong&gt; It looks for the "Firewall Builder" marker in &lt;code&gt;*.fw&lt;/code&gt; files and boot scripts.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The report is in Spanish.&lt;/strong&gt; The script grew up in a Spanish-speaking environment; translations and an i18n layer would be a welcome contribution.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Who it's for
&lt;/h2&gt;

&lt;p&gt;Sysadmins and infrastructure teams who want a fast, repeatable first look at Debian/Ubuntu servers. Auditors who need evidence with checksums. Anyone migrating from iptables-legacy or fwbuilder toward nftables who needs to see what is really loaded. Students who want a readable example of how these checks work.&lt;/p&gt;

&lt;p&gt;It is not meant to replace a compliance scanner, a penetration test, or your own judgment about what "secure" means for your environment.&lt;/p&gt;

&lt;h2&gt;
  
  
  Contribute
&lt;/h2&gt;

&lt;p&gt;Lynx is Apache-2.0. Issues and pull requests are welcome. Before sending one, run &lt;code&gt;bash -n lynx.sh&lt;/code&gt; and &lt;code&gt;shellcheck lynx.sh&lt;/code&gt;, and keep new controls read-only so the "passive" promise stays true.&lt;/p&gt;

&lt;p&gt;If you run it and find a false positive or a check that misreads your setup, I want to hear about it. The Docker bug above only showed up because someone (me) ran the script on a real machine.&lt;/p&gt;

&lt;h2&gt;
  
  
  🔗 Useful Links &amp;amp; Documentation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/matarturo/lynx" rel="noopener noreferrer"&gt;https://github.com/matarturo/lynx&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Open Source Edition Install:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  curl &lt;span class="nt"&gt;-fsSLO&lt;/span&gt; https://raw.githubusercontent.com/matarturo/lynx/main/lynx.sh
  &lt;span class="nb"&gt;chmod&lt;/span&gt; +x lynx.sh
  &lt;span class="nb"&gt;sudo&lt;/span&gt; ./lynx.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Si el directorio está en un sistema de archivos montado con noexec, ejecútalo con sudo bash lynx.sh.&lt;/p&gt;




&lt;h2&gt;
  
  
  Conclusion &amp;amp; Call to Action
&lt;/h2&gt;

&lt;p&gt;Auditing modern Linux systems reveals a recurring technical limitation in generic tools: the superficial handling of hybrid network backends (iptables-legacy, iptables-nft, and nftables) and a tendency toward false positives—such as erroneously assuming that container forwarding (FORWARD) rules protect host traffic. Addressing this requires reachability analysis at the parsing level that traces jumps (-j, -g) directly from INPUT chains, reads kernel state natively from /proc/sys to avoid path dependencies, and evaluates complex metrics like the effective rp_filter per interface. Reliable infrastructure assessment requires auditing actual socket exposure across all interfaces, reviewing restrictive mount options (noexec, nodev, nosuid), correlating pending security patches, and generating verifiable evidence via SHA-256 cryptographic hashes without altering the operating system's state.&lt;/p&gt;

&lt;p&gt;Technical actions to improve infrastructure security posture:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;Ingress-plane reachability validation: Do not rely on declarative service persistence (such as active UFW or systemd states); verify via parsing that default-deny and filtering rules effectively reside in the INPUT path and are not bypassed by traffic from virtual network bridges or hypervisors.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;Exposure and attack surface control: Conduct passive audits to detect globally listening sockets (services on ports 22, 3000, 3001, mDNS, etc.), permissive sshd hardening parameters, and the inventory of SUID/SGID binaries or ownerless files. Guarantee of evidence immutability: Ensure that any control script or routine calculates hash manifests (SHA-256) after report consolidation to preserve forensic traceability for configuration analysis and configuration drift detection.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Clone the repository, test it against your servers, and see what your current scanner has been missing. Drop a star, open an issue, or share your feedback&lt;/p&gt;

</description>
      <category>bash</category>
      <category>linux</category>
      <category>opensource</category>
      <category>security</category>
    </item>
    <item>
      <title>Soft-404s: The Silent Killer of DAST Scans</title>
      <dc:creator>Arturo Enrique Mata Garcia</dc:creator>
      <pubDate>Sun, 27 Sep 2026 22:58:54 +0000</pubDate>
      <link>https://dev.to/matarturo/soft-404s-the-silent-killer-of-dast-scans-417c</link>
      <guid>https://dev.to/matarturo/soft-404s-the-silent-killer-of-dast-scans-417c</guid>
      <description>&lt;p&gt;Traditional DAST tools have a dirty secret: they don't know when a page doesn't exist. And in modern SPA architectures, this blindness doesn't just generate noise—it &lt;strong&gt;corrupts the entire assessment&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;If you're a pentester, DevSecOps engineer, or security researcher, you've seen it. You run a scan. The report says everything is fine. Then a manual test reveals a critical endpoint that was never crawled, never tested, and never reported. The scanner missed it because it couldn't distinguish a real page from a soft 404.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VORTEX Assessment Engine&lt;/strong&gt; was built to solve exactly that.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔬 What Is a Soft-404 and Why It Breaks DAST
&lt;/h2&gt;

&lt;p&gt;A soft-404 is an HTTP response that returns &lt;code&gt;200 OK&lt;/code&gt; while serving content that semantically means "not found." Unlike a hard 404, which correctly returns a &lt;code&gt;404&lt;/code&gt; status code, a soft-404 masks missing routes as legitimate pages.&lt;/p&gt;

&lt;p&gt;This is not a bug. It's a &lt;strong&gt;design pattern&lt;/strong&gt; in many modern frameworks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;React Router&lt;/strong&gt;, &lt;strong&gt;Vue Router&lt;/strong&gt;, and &lt;strong&gt;Angular Router&lt;/strong&gt; often render a "not found" component with a 200 status.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Next.js&lt;/strong&gt; and &lt;strong&gt;Nuxt&lt;/strong&gt; handle dynamic routes dynamically, sometimes returning 200 for missing paths.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;SPAs with client-side routing&lt;/strong&gt; are especially prone: the server always returns the same &lt;code&gt;index.html&lt;/code&gt;, and the router decides what to display.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For a DAST scanner, this is a minefield:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;False positives:&lt;/strong&gt; the scanner treats the fallback as a real page and tests it, then reports findings against content that isn't a distinct endpoint at all.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;False negatives:&lt;/strong&gt; the scanner assumes a path &lt;em&gt;is&lt;/em&gt; a fallback and skips it — dropping a genuinely reachable route from the test suite entirely.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Blind spot in targeted checks:&lt;/strong&gt; any check that probes a wordlist of known paths — API discovery, path scanning, dependency detection — has no way to tell "found it" from "the fallback said yes to everything."&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Broken coverage:&lt;/strong&gt; none of the above shows up as a failure in the summary line. The report still says "scan complete."&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Traditional scanners rely on &lt;strong&gt;static HTML parsing&lt;/strong&gt; and &lt;strong&gt;HTTP status codes&lt;/strong&gt;. Neither works reliably with soft-404s.&lt;/p&gt;




&lt;h2&gt;
  
  
  🕸️ Why VORTEX Handles Soft-404s Differently
&lt;/h2&gt;

&lt;p&gt;VORTEX doesn't take a &lt;code&gt;200 OK&lt;/code&gt; at face value — and that one decision is what separates a scan you can trust from a scan that just looks clean.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;You stop chasing ghosts.&lt;/strong&gt; Every "page" that's really just the SPA fallback in disguise gets filtered out before it ever reaches your report. No more triaging findings against a page that was never real.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;You stop losing real endpoints.&lt;/strong&gt; Routes that legacy scanners write off as "probably just the 404 page" get evaluated on their actual content, not assumed away.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;It happens automatically, before anything else runs.&lt;/strong&gt; No config, no manual allowlisting of fallback pages, no tuning per target. The calibration runs first and every downstream check inherits the result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;The result:&lt;/strong&gt; a report where "not found" means not found, and "found" means found — which is the entire point of running a scanner in the first place.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Real-World Impact
&lt;/h2&gt;

&lt;p&gt;Run against OWASP Juice Shop (Docker, port 3000): the moment VORTEX detected the SPA fallback pattern, it adjusted — and the rest of the assessment came back clean of the noise that pattern usually creates. Zero false findings traced back to fallback pages. Zero wasted cycles re-testing the same page under a different URL.&lt;/p&gt;

&lt;p&gt;That's the difference this one check makes: not a longer report, a &lt;strong&gt;trustworthy&lt;/strong&gt; one.&lt;/p&gt;




&lt;h2&gt;
  
  
  ⚡ Why This Matters for CI/CD
&lt;/h2&gt;

&lt;p&gt;In a CI/CD pipeline, a DAST scanner that cannot handle soft-404s is worse than no scanner at all. It gives you false confidence: the pipeline goes green, coverage looks complete, and the gap only surfaces later, usually manually, usually after deployment. You deploy. You think you're covered. You're not.&lt;/p&gt;

&lt;p&gt;Calibrating for the fallback pattern before running the rest of the suite is what keeps "clean scan" from quietly meaning "we didn't actually look."&lt;/p&gt;

&lt;p&gt;What that looks like in an actual run: the calibration check isn't a separate step someone has to remember to run — it's one of 18 checks executed in the same pass, timestamped and reproducible (&lt;code&gt;2026-09-27T17:42:36&lt;/code&gt;). It also isn't the thing slowing the pipeline down: the same run completed with a 0.01s performance result. And the consequence isn't hypothetical — in this run, both API Discovery and Path Scan came back clean after the calibration step had already flagged content-based filtering as necessary, meaning those results reflect the fallback-aware logic, not a naive status-code check that got lucky.&lt;/p&gt;

&lt;p&gt;Beyond what this specific run shows, VORTEX is positioned as pipeline-native by design — no cloud dependency, no separate runtime to provision, SARIF output for native integration with code-scanning tools, and a deterministic execution model. &lt;em&gt;(Stated product characteristics, not something demonstrated by this particular PoC run.)&lt;/em&gt;&lt;/p&gt;

&lt;p&gt;Soft-404 calibration is not a feature. It's a prerequisite for &lt;strong&gt;accurate DAST in modern web applications&lt;/strong&gt;.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔬 Evaluation Flow
&lt;/h2&gt;

&lt;p&gt;A typical security assessment with VORTEX follows this streamlined execution sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;                  WEB APP
                     │
                     ▼
            ┌─────────────────┐
            │     VORTEX      │
            │     Crawler     │
            └────────┬────────┘
                     │
                     ▼
           Soft-404 Calibration
                     │
                     ▼
          SPA / Route Discovery 
                     │
                     ▼
          JavaScript Analysis
                     │
                     ▼
          Security Assessment
                     │
                     ▼
            ┌─────────────────┐
            │    RESULTS      │
            └────────┬────────┘
                     │
            ┌────────┼────────┐
            ▼        ▼        ▼
          HTML      JSON     SARIF
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The soft-404 calibration step sits early in the pipeline for a reason: everything downstream — path scanning, API discovery, the security tests themselves — depends on knowing whether &lt;code&gt;200 OK&lt;/code&gt; means "page exists" for this specific target.&lt;/p&gt;




&lt;h2&gt;
  
  
  🧪 Proof of Concept: Soft-404 Calibration in a Modern SPA
&lt;/h2&gt;

&lt;p&gt;The following output is from a VORTEX scan (PRO account) against OWASP Juice Shop, a deliberately vulnerable SPA built with Angular, deployed locally via Docker on port 3000. It captures a real-world scenario where the server returns 200 OK for non-existent routes.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;root@sandbox-vortex:/opt/vortex# vortex &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; 3000

██╗   ██╗ ██████╗ ██████╗ ████████╗███████╗██╗  ██╗
██║   ██║██╔═══██╗██╔══██╗╚══██╔══╝██╔════╝╚██╗██╔╝
██║   ██║██║   ██║██████╔╝   ██║   █████╗   ╚███╔╝
╚██╗ ██╔╝██║   ██║██╔══██╗   ██║   ██╔══╝   ██╔██╗
 ╚████╔╝ ╚██████╔╝██║  ██║   ██║   ███████╗██╔╝ ██╗
  ╚═══╝   ╚═════╝ ╚═╝  ╚═╝   ╚═╝   ╚══════╝╚═╝  ╚═╝  v3.0.6

Assessment Engine

LOCAL · FAST · ACTIONABLE

&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt;  GUMROAD_PRO license renewed via CEREBELLUM.
&lt;span class="o"&gt;[&lt;/span&gt;&lt;span class="k"&gt;*&lt;/span&gt;&lt;span class="o"&gt;]&lt;/span&gt; Starting VORTEX Security Audit v3.0.6
    Target: http://127.0.0.1:3000
&lt;span class="o"&gt;===========================================================================&lt;/span&gt;
Audit Progress: &lt;span class="o"&gt;[&lt;/span&gt;   6% &lt;span class="o"&gt;]&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt; ██---------------------------- &lt;span class="o"&gt;]&lt;/span&gt;
&lt;span class="nt"&gt;---&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;1/15] Discovery &amp;amp; Crawler &lt;span class="nt"&gt;---&lt;/span&gt;

&lt;span class="o"&gt;===&lt;/span&gt; DISCOVERY &lt;span class="o"&gt;(&lt;/span&gt;CRAWLER&lt;span class="o"&gt;)&lt;/span&gt; &lt;span class="o"&gt;===&lt;/span&gt;
&lt;span class="o"&gt;[!]&lt;/span&gt; Soft-404 Calibration             &lt;span class="o"&gt;[&lt;/span&gt;WARN] Server responds 200 to non-existent routes &lt;span class="o"&gt;(&lt;/span&gt;SPA fallback&lt;span class="o"&gt;)&lt;/span&gt; - Path Scan and API Discovery will filter by content, not just status code
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;strong&gt;Interpretation:&lt;/strong&gt;&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The server returned 200 OK for a non-existent route.&lt;/li&gt;
&lt;li&gt;VORTEX correctly classified this as SPA fallback behavior rather than a real page.&lt;/li&gt;
&lt;li&gt;Calibration status: WARN.&lt;/li&gt;
&lt;li&gt;Downstream checks switch to content-based filtering as a direct result.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This is the exact scenario where legacy DAST tools fail silently. VORTEX makes the failure mode visible instead.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔗 Useful Links &amp;amp; Documentation
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Official Website &amp;amp; Docs:&lt;/strong&gt; &lt;a href="https://zerodayslab.co/docs" rel="noopener noreferrer"&gt;zerodayslab.co/docs&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://matarturo.github.io" rel="noopener noreferrer"&gt;matarturo.github.io&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Community Edition Install:&lt;/strong&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; install.sh https://raw.githubusercontent.com/matarturo/vortex/main/install.sh
  &lt;span class="nb"&gt;chmod&lt;/span&gt; +x install.sh
  &lt;span class="nb"&gt;sudo&lt;/span&gt; ./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;h2&gt;
  
  
  Conclusion &amp;amp; Call to Action
&lt;/h2&gt;

&lt;p&gt;Soft-404s are not a minor edge case. They are a &lt;strong&gt;fundamental blind spot&lt;/strong&gt; in traditional DAST tools, and they silently corrupt the accuracy of every scan run against a modern SPA.&lt;/p&gt;

&lt;p&gt;VORTEX was built to close that gap. The run shown above was captured on a PRO license; the soft-404 calibration step is the core mechanism this article is about, so if you're running the Community Edition, check your own output for the same &lt;code&gt;Calibracion Soft-404&lt;/code&gt; line before assuming feature parity.&lt;/p&gt;

&lt;p&gt;Clone the repository, test it against your own SPAs, and see what your current scanner has been missing. Drop a star, open an issue, or share your feedback. See you in the code.&lt;/p&gt;

</description>
      <category>cybersecurity</category>
      <category>security</category>
      <category>testing</category>
    </item>
    <item>
      <title>Taming SPAs with VORTEX: How a Crawler Actually Understands React, Vue, and Angular</title>
      <dc:creator>Arturo Enrique Mata Garcia</dc:creator>
      <pubDate>Thu, 10 Sep 2026 15:47:32 +0000</pubDate>
      <link>https://dev.to/matarturo/taming-spas-with-vortex-how-a-crawler-actually-understands-react-vue-and-angular-57dn</link>
      <guid>https://dev.to/matarturo/taming-spas-with-vortex-how-a-crawler-actually-understands-react-vue-and-angular-57dn</guid>
      <description>&lt;h3&gt;
  
  
  💡Let’s run a mental experiment.
&lt;/h3&gt;

&lt;p&gt;You open a SPA built with React. You point your favorite scanner—the one you've used for years, the one that "never fails"—to audit it. You wait. It generates a neat, polished report with a nice logo and a progress bar. And it tells you: "2 routes found. 0 critical vulnerabilities."&lt;/p&gt;

&lt;p&gt;Two routes. In an application featuring a complete dashboard, an admin panel, over forty endpoints, and half a dozen features locked behind feature flags.&lt;/p&gt;

&lt;p&gt;The scanner isn't broken. The scanner is blind. And nobody told it.&lt;/p&gt;

&lt;p&gt;That was, literally, the starting point of &lt;strong&gt;VORTEX Assessment Engine&lt;/strong&gt;. In my previous post, I covered the overall architecture: a self-hosted engine, a native binary, zero heavy dependencies, and direct CI/CD pipeline integration. Today, I want to lift the hood and show you the piece that makes all of that actually matter: the crawler, and what happens when it steps directly inside the JavaScript instead of just staring at it from the outside.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;If you're a developer&lt;/strong&gt;, this might sting a little. If you're a DevOps engineer, you'll care about how it plugs into the pipeline. And if you're a pentester... well, you already know exactly what I'm talking about.&lt;/p&gt;

&lt;h2&gt;
  
  
  🧱 HTML Is No Longer a Map. It’s a Lid.
&lt;/h2&gt;

&lt;p&gt;Let's look at the facts. This is what a &lt;code&gt;curl&lt;/code&gt; response to a modern SPA looks like:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;
&amp;lt;script src="/static/js/main.a1b2c3.js"&amp;gt;&amp;lt;/script&amp;gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That is literally all there is. No &lt;code&gt;&amp;lt;a href&amp;gt;&lt;/code&gt;, no forms, no routes, nothing that a 2000s-era HTML parser can chew on. The entire application—components, router, API calls, business logic, validations, everything—is compressed and minified inside that single &lt;code&gt;.js&lt;/code&gt; file, and it gets built right inside the browser in real-time the moment a user hits the page.&lt;/p&gt;

&lt;p&gt;A crawler that only knows how to read HTML is staring at a locked door with a sign saying "nothing to see here." And it actually believes it.&lt;/p&gt;

&lt;p&gt;This isn’t a minor limitation or an academic edge case. It’s the exact reason why, year after year, we keep seeing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Google Maps, Stripe, or Firebase API keys leaked in production for months.&lt;/li&gt;
&lt;li&gt;Complete administrative routes &lt;code&gt;(/admin, /internal, /debug)&lt;/code&gt; that nobody discovered because they only render after a specific menu click.&lt;/li&gt;
&lt;li&gt;Staging or internal microservice endpoints, referenced deep inside a bundle, that a "traditional" dynamic scan never touched.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Frontend secret leakage reports aren't going down. And they won't, because the tools meant to catch them are still playing a game that ended a decade ago.&lt;/p&gt;

&lt;h2&gt;
  
  
  ⚙️ How VORTEX Solves It: Stop Reading, Start Executing
&lt;/h2&gt;

&lt;p&gt;The core design decision behind the &lt;strong&gt;VORTEX crawler&lt;/strong&gt; is simple to state and much harder to build right: instead of parsing HTML, execute the application like a real user would, and observe what happens next.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;     WEB APPLICATION
            │
            ▼
   ┌─────────────────┐
   │     VORTEX      │
   │     Crawler     │
   └────────┬────────┘
            │
            ▼
 Rendering + Dynamic Route Discovery
            │
            ▼
    JS Bundle Extraction &amp;amp; Analysis
            │
            ▼
   Attack Surface Correlation
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  🔍 Let's break down why that actually matters.
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. 🧭 Route Discovery: Don't Read, Navigate
&lt;/h3&gt;

&lt;p&gt;&lt;strong&gt;VORTEX&lt;/strong&gt; doesn't just scrape links. It interacts: it clicks, navigates, opens menus, triggers transitions, and listens to the history changes fired on-the-fly by React Router, Vue Router, or Angular Router.&lt;/p&gt;

&lt;p&gt;Why is this a big deal? Because the most interesting routes are almost never just a single obvious click away. They hide behind:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;A dropdown menu that only renders on hover.&lt;/li&gt;
&lt;li&gt;A tab loading a lazy chunk via dynamic &lt;code&gt;import()&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;A route guard that only mounts a component if an active session exists.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of that exists in the initial HTML. It only exists after something happens inside the browser. And a crawler that doesn’t simulate that "something" will simply never see it.&lt;/p&gt;

&lt;p&gt;(Yes, authentication-protected routes are a whole separate, juicy topic—&lt;strong&gt;VORTEX&lt;/strong&gt; supports credentials, &lt;code&gt;--login-path&lt;/code&gt;, and session cookie handoff to reach them—but that deserves its own dedicated post. Today, we're sticking to the core crawler.)&lt;/p&gt;

&lt;h3&gt;
  
  
  2. 📦 Static JS Bundle Analysis: The Mistake We've All Made at Least Once
&lt;/h3&gt;

&lt;p&gt;This is, without exaggeration, my favorite part of the engine. And the most underrated feature in any DAST tool.&lt;/p&gt;

&lt;p&gt;Put yourself in the shoes of the developer writing the code, because that’s where the problem actually starts. You're deep into a sprint, you need a map to load right now, or an analytics widget to work in the staging environment. You paste the key straight into the source code, with every intention of moving it to an environment variable "as soon as I have five minutes." The PR gets reviewed—nobody notices, because nobody is checking that line by line—it gets merged, and Webpack or Vite do their job: they minify, bundle, and that key ends up shipping, completely intact, inside the &lt;code&gt;main.a1b2c3.js&lt;/code&gt; file served publicly to anyone opening the app.&lt;/p&gt;

&lt;p&gt;That exact moment—the "I'll fix it later" that never comes—is the exact kind of oversight that a static JS bundle analysis is built to catch. Nobody needs to click anything, and no suspicious requests need to be fired: the file is already public, accessible to anyone with a browser and their DevTools open, and it already contains what it shouldn't.&lt;/p&gt;

&lt;p&gt;And here’s the kicker that almost no one sees coming: most of these leaks never generate a visible HTTP request during a standard dynamic scan. They don't show up in network traffic because the issue isn't what the app does, but what the bundle contains. A purely black-box scan—one that only watches packets flying back and forth—will never touch them, because it never looked inside the file itself.&lt;/p&gt;

&lt;p&gt;If you've ever inspected your own app's bundle "out of curiosity" and found something that shouldn't be there... you know exactly what I'm talking about. And if you haven't, it’s probably worth checking before someone less friendly than you does.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. 🎯 Correlation: Active Testing Against Real Surface Area, Not Assumptions
&lt;/h3&gt;

&lt;p&gt;Once the crawler maps out the complete route tree and finishes the JS bundle analysis, &lt;strong&gt;VORTEX&lt;/strong&gt; fires active security tests—SQLi, XSS, Path Traversal, among others—directly against that discovered surface, rather than firing payloads at a handful of guessed URLs or an outdated sitemap.&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt; SPA / Route Discovery
            │
            ▼
 JavaScript Analysis
            │
            ▼
 Security Assessment

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The practical difference: scanning cycles are concentrated where there is actually something to attack, instead of wasting time hitting ghost routes that never existed or that no real user could ever reach.&lt;/p&gt;

&lt;h2&gt;
  
  
  👥Why Every Technical Role Should Care
&lt;/h2&gt;

&lt;p&gt;Because no, this isn't "just a security thing." It hits each role differently:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;👨‍💻&lt;strong&gt;If you're a developer:&lt;/strong&gt; that bundle you just pushed to production with a hardcoded &lt;code&gt;VITE_API_KEY&lt;/code&gt; "just to test on staging"—yeah, that one—is precisely what this crawler is designed to find before an attacker does. It's not personal. It's math.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🚀 &lt;strong&gt;If you're a DevOps / DevSecOps engineer:&lt;/strong&gt; finding an issue like this inside a pull request, before the merge, costs minutes of fixing and a single line in a changelog. Finding that exact same issue three weeks later in production—or worse, reported by an outside party, or worse still, exploited—costs an incident response, an awkward meeting, and likely a post-mortem. Bringing this analysis into the pipeline turns security gating into an early check rather than a late, ceremonial audit.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;🕵️‍♂️&lt;strong&gt;If you're a pentester:&lt;/strong&gt; you already know the frustration of wasting the first two hours of an engagement simply trying to manually map an application because the automated crawler gave up at the loading screen. An engine that handles that discovery for you—while handing you pre-analyzed bundle results—doesn't replace your judgment; it gives you back your time for what actually requires a human brain: exploitation and impact analysis.&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;h2&gt;
  
  
  ⚡Conclusion
&lt;/h2&gt;

&lt;p&gt;HTML stopped being the application map years ago. The real map lives inside the JavaScript your browser silently executes every single time you load a page. A DAST tool that doesn't understand that isn't evaluating your app: it's evaluating an empty shell with a &lt;code&gt;&amp;lt;div id="root"&amp;gt;&amp;lt;/div&amp;gt;&lt;/code&gt; inside, congratulating you on how clean it looks.&lt;/p&gt;

&lt;p&gt;If you want to see the crawler running against your own SPA, the Community Edition is still available with no credit card required. And if you test it against something built with Svelte, Solid, Qwik, or any framework outside the usual trio, let me know how it behaves—that feedback is absolute gold for keeping the engine sharp.&lt;/p&gt;




&lt;p&gt;🔗 Useful Links &amp;amp; Documentation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;🌐 &lt;strong&gt;Official Website &amp;amp; Docs:&lt;/strong&gt; &lt;a href="https://zerodayslab.co/docs" rel="noopener noreferrer"&gt;zerodayslab.co/docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;💻 &lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://matarturo.github.io" rel="noopener noreferrer"&gt;matarturo.github.io&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




</description>
      <category>architecture</category>
      <category>frontend</category>
      <category>javascript</category>
      <category>security</category>
    </item>
    <item>
      <title>More Than Just a Scanner: Architecture of a Self-Hosted DAST Engine for SPAs and CI/CD</title>
      <dc:creator>Arturo Enrique Mata Garcia</dc:creator>
      <pubDate>Tue, 08 Sep 2026 20:28:52 +0000</pubDate>
      <link>https://dev.to/matarturo/more-than-just-a-scanner-architecture-of-a-self-hosted-dast-engine-for-spas-and-cicd-2m3j</link>
      <guid>https://dev.to/matarturo/more-than-just-a-scanner-architecture-of-a-self-hosted-dast-engine-for-spas-and-cicd-2m3j</guid>
      <description>&lt;p&gt;Traditional DAST tools are often a massive headache for modern development teams: they are slow, tightly coupled to third-party cloud services, and worst of all, completely blind to Single Page Applications (SPAs) built with React, Vue, or Angular where routing and application logic live entirely on the client side.&lt;/p&gt;

&lt;p&gt;If you are shipping software fast, you need a tool that speaks your language: a native, portable binary with zero heavy runtime dependencies (no Python or pip required in production) that integrates seamlessly into your CI/CD pipelines to enforce real security gatekeeping.&lt;/p&gt;

&lt;p&gt;Meet &lt;strong&gt;VORTEX Assessment Engine&lt;/strong&gt;, developed by Arturo Mata.&lt;br&gt;
&lt;em&gt;Official documentation and interactive diagrams are also available at &lt;a href="https://matarturo.github.io" rel="noopener noreferrer"&gt;matarturo.github.io&lt;/a&gt;.&lt;/em&gt;&lt;/p&gt;



&lt;p&gt;If you are a pentester, DevSecOps engineer, or web developer, you have likely suffered through traditional DAST tools. They are sluggish, heavy, and above all, &lt;strong&gt;clueless about modern web applications&lt;/strong&gt;.&lt;/p&gt;

&lt;p&gt;SPAs built with React, Vue, or Angular &lt;strong&gt;rely almost entirely on JavaScript&lt;/strong&gt;. Legacy crawlers get lost, fail to discover dynamic routes, and let critical vulnerabilities slip by—alongside exposed frontend secrets.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VORTEX Assessment Engine&lt;/strong&gt; was built to solve precisely that problem. It is a &lt;strong&gt;self-hosted&lt;/strong&gt;, high-performance DAST engine engineered specifically to evaluate SPAs with a &lt;strong&gt;deep focus on JavaScript analysis&lt;/strong&gt; and &lt;strong&gt;native CI/CD pipeline integration&lt;/strong&gt;.&lt;/p&gt;


&lt;h2&gt;
  
  
  🔬 Architecture: A Self-Hosted Engine
&lt;/h2&gt;

&lt;p&gt;Unlike commercial SaaS scanners that require shipping your application code to an external cloud, VORTEX runs &lt;strong&gt;entirely within your own infrastructure&lt;/strong&gt;. This guarantees:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Privacy:&lt;/strong&gt; Your target application never leaves your secure perimeter.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Total Control:&lt;/strong&gt; You know exactly where and how the scan execution takes place.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Automation-First:&lt;/strong&gt; Easily embed it into a Bash script or native pipeline step.
&lt;/li&gt;
&lt;/ul&gt;
&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;┌───────────────────────────────────┐
│        YOUR INFRASTRUCTURE        │
│                                   │
│   ┌───────────────────────────┐   │
│   │   VORTEX Assessment       │   │
│   │        Engine             │   │
│   └─────────────┬─────────────┘   │
│                 │                 │
│                 ▼                 │
│          WEB APPLICATION          │
│            TARGET                 │
└───────────────────────────────────┘
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;




&lt;p&gt;🕸️ Why Does VORTEX Understand SPA's?&lt;/p&gt;

&lt;p&gt;Conventional DAST scanners rely heavily on static HTML parsing. VORTEX incorporates an SPA-optimized crawler that performs the following actions:&lt;/p&gt;

&lt;p&gt;Renders and discovers dynamic routing structures across React, Vue, and Angular applications.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Performs deep parsing of JavaScript bundles to hunt down:&lt;/li&gt;
&lt;li&gt;Exposed API keys&lt;/li&gt;
&lt;li&gt;Authentication tokens&lt;/li&gt;
&lt;li&gt;Hardcoded credentials&lt;/li&gt;
&lt;li&gt;Sensitive internal configuration secrets&lt;/li&gt;
&lt;/ul&gt;



&lt;p&gt;🔬 Evaluation Flow&lt;/p&gt;

&lt;p&gt;A typical security assessment with VORTEX follows this streamlined execution sequence:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;               APLICACIÓN WEB
                     │
                     ▼
            ┌─────────────────┐
            │     VORTEX      │
            │     Crawler     │
            └────────┬────────┘
                     │
                     ▼
           SPA / Route Discovery
                     │
                     ▼
          JavaScript Analysis
                     │
                     ▼
          Security Assessment
                     │
                     ▼
            ┌─────────────────┐
            │    RESULTS      │
            └────────┬────────┘
                     │
            ┌────────┼────────┐
            ▼        ▼        ▼
          HTML      JSON     SARIF

&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;VORTEX fires targeted security tests (SQLi, XSS, Path Traversal, etc.) directly against the discovered surfaces. This rigorous JavaScript inspection uncovers severe flaws that standard black-box scanners routinely miss.&lt;/p&gt;




&lt;h2&gt;
  
  
  🔬 Real-World Environment Demonstration
&lt;/h2&gt;


  
  Your browser does not support the video tag.





&lt;p&gt;⚡ A Native, Frictionless Binary&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;VORTEX&lt;/strong&gt; is distributed as a self-contained, native Linux binary. This design delivers distinct performance advantages:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Zero requirements for Python, pip, or fragile runtime dependencies.&lt;/li&gt;
&lt;li&gt;No Node.js engine required on the target execution host.&lt;/li&gt;
&lt;li&gt;Ultra-lightweight footprint (averaging under 6% CPU utilization and 62 MB of RAM during standard scans).&lt;/li&gt;
&lt;li&gt;This native portability allows you to spin up VORTEX inside staging servers, auditing workstations, or ephemeral Docker containers without complex setup overhead.&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;🔌 Native CI/CD Integration&lt;/p&gt;

&lt;p&gt;One of the greatest friction points in DevSecOps is integrating security testing without turning pipelines into delivery bottlenecks. VORTEX outputs native SARIF 2.1.0 reports, enabling strict security gatekeeping across popular developer platforms.&lt;/p&gt;

&lt;p&gt;GitHub Actions (.github/workflows/security.yml)&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="s"&gt;YAML&lt;/span&gt;
&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VORTEX Security Assessment&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;push&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;pull_request&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;vortex&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;VORTEX Security Scan&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;

    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Checkout&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;actions/checkout@v4&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Install VORTEX&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;curl -sSL [https://raw.githubusercontent.com/matarturo/vortex/main/install.sh](https://raw.githubusercontent.com/matarturo/vortex/main/install.sh) | sudo bash&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Run VORTEX&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;vortex [https://my-app.com](https://my-app.com) --output /opt/vortex/reporte/reporte.sarif&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Upload SARIF&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;github/codeql-action/upload-sarif@v3&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;sarif_file&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/opt/vortex/reporte/reporte.sarif&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;GitLab CI (.gitlab-ci.yml)&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="s"&gt;YAML&lt;/span&gt;
&lt;span class="na"&gt;vortex_security&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;stage&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;test&lt;/span&gt;

  &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;curl -sSL [https://raw.githubusercontent.com/matarturo/vortex/main/install.sh](https://raw.githubusercontent.com/matarturo/vortex/main/install.sh) | bash&lt;/span&gt;
    &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;vortex [https://my-app.com](https://my-app.com) --output /opt/vortex/reporte/reporte.sarif&lt;/span&gt;

  &lt;span class="na"&gt;artifacts&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;when&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;always&lt;/span&gt;
    &lt;span class="na"&gt;paths&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="s"&gt;/opt/vortex/reporte/&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Azure DevOps (azure-pipelines.yml)&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="s"&gt;YAML&lt;/span&gt;
&lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;checkout&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;self&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;script&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
    &lt;span class="s"&gt;curl -sSL [https://raw.githubusercontent.com/matarturo/vortex/main/install.sh](https://raw.githubusercontent.com/matarturo/vortex/main/install.sh) | sudo bash&lt;/span&gt;
    &lt;span class="s"&gt;vortex [https://my-app.com](https://my-app.com) --output /opt/vortex/reporte/reporte.sarif&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;VORTEX&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Security&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Assessment'&lt;/span&gt;

&lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;task&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;PublishBuildArtifacts@v4&lt;/span&gt;
  &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;pathToPublish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;/opt/vortex/reporte'&lt;/span&gt;
    &lt;span class="na"&gt;artifactName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;VORTEX-Reports'&lt;/span&gt;
  &lt;span class="na"&gt;displayName&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s1"&gt;'&lt;/span&gt;&lt;span class="s"&gt;Publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;VORTEX&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;Reports'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;⚡ Quick Installation&lt;/p&gt;

&lt;p&gt;Want to test the VORTEX Community Edition? No credit card is required to get started.&lt;/p&gt;

&lt;p&gt;Install on Ubuntu / Debian&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl &lt;span class="nt"&gt;-sSL&lt;/span&gt; &lt;span class="nt"&gt;-o&lt;/span&gt; install.sh &lt;span class="o"&gt;[&lt;/span&gt;https://raw.githubusercontent.com/matarturo/vortex/main/install.sh]&lt;span class="o"&gt;(&lt;/span&gt;https://raw.githubusercontent.com/matarturo/vortex/main/install.sh&lt;span class="o"&gt;)&lt;/span&gt;
&lt;span class="nb"&gt;chmod&lt;/span&gt; +x install.sh
&lt;span class="nb"&gt;sudo&lt;/span&gt; ./install.sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run Your First Local Assessment&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;vortex &lt;span class="nt"&gt;--local&lt;/span&gt; &lt;span class="nt"&gt;--port&lt;/span&gt; &lt;span class="o"&gt;[&lt;/span&gt;PORT_NUMBER]
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;






&lt;p&gt;🔗 Useful Links &amp;amp; Documentation&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;Official Website &amp;amp; Docs:&lt;/strong&gt; &lt;a href="https://zerodayslab.co/docs" rel="noopener noreferrer"&gt;zerodayslab.co/docs&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;GitHub Repository:&lt;/strong&gt; &lt;a href="https://github.com/matarturo/vortex" rel="noopener noreferrer"&gt;github.com/matarturo/vortex&lt;/a&gt;&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;Conclusion &amp;amp; Call to Action&lt;/p&gt;

&lt;p&gt;VORTEX was built to bridge the gap between fast-paced modern software development and offensive security requirements.&lt;/p&gt;

&lt;p&gt;Clone the repository, test out the Community edition against your local SPAs, and drop a star on the repo or share your feedback to help drive the project forward. See you in the code!&lt;/p&gt;

</description>
      <category>architecture</category>
      <category>cicd</category>
      <category>devops</category>
      <category>security</category>
    </item>
  </channel>
</rss>
