<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="4.4.1">Jekyll</generator><link href="https://un4gi.io/feed.xml" rel="self" type="application/atom+xml" /><link href="https://un4gi.io/" rel="alternate" type="text/html" /><updated>2026-09-04T07:19:00+00:00</updated><id>https://un4gi.io/feed.xml</id><title type="html">un4gi</title><subtitle>Vulnerability research and responsible disclosure.</subtitle><author><name>Tony West</name></author><entry><title type="html">Disclosure economics in 2026: are bug bounty programs actually faster than an email?</title><link href="https://un4gi.io/blog/disclosure-economics-bounty-versus-email/" rel="alternate" type="text/html" title="Disclosure economics in 2026: are bug bounty programs actually faster than an email?" /><published>2026-09-04T00:00:00+00:00</published><updated>2026-09-04T00:00:00+00:00</updated><id>https://un4gi.io/blog/disclosure-economics-bounty-versus-email</id><content type="html" xml:base="https://un4gi.io/blog/disclosure-economics-bounty-versus-email/"><![CDATA[<h2 id="summary">Summary</h2>

<p>I keep seeing the claim that running a bug bounty program is how a vendor speeds up remediation. The argument is intuitive: a platform, triagers, a payout, a dashboard, all of it built to move a report from inbox to fix. Against that you have the old path, a researcher emailing <code class="language-plaintext highlighter-rouge">security@vendor.com</code> and hoping someone reads it.</p>

<p>I have used both paths in the same year for the same class of vulnerability. The bounty program was not reliably faster to an actual patch. Sometimes it was slower. What follows is not an argument against bounties, which are clearly good for other reasons. It is a look at where the time actually goes between a report and a shipped fix, and why the money in the pipeline does not buy the part of the timeline that matters most.</p>

<h2 id="what-the-two-paths-actually-optimize">What the two paths actually optimize</h2>

<p>A responsible disclosure over email and a submission through a bounty platform are optimizing different things, and it took me a while to see that clearly.</p>

<p>The email path optimizes nothing. It is a queue with no service level, no acknowledgement guarantee, and no structure. When it works, it works because a specific competent person on the other side read the message and cared. When it fails, it fails silently, which is its own problem and the reason platforms exist at all.</p>

<p>The bounty path optimizes triage throughput and researcher payout. Those are real and worth having. A good platform gets you a fast acknowledgement, a severity decision, and a duplicate check, usually within a day or two. What it does not optimize, and cannot, is the vendor’s internal engineering schedule. The fix still has to be written, reviewed, regression-tested, and shipped through whatever release process the product already has. No triager controls that queue.</p>

<p>So the honest framing is: the platform compresses the front of the timeline, the part between “I sent it” and “someone confirmed it is real.” It does very little to the back of the timeline, the part between “confirmed real” and “patched in a release customers run.” And on the reports I have handled, the back half is where almost all of the calendar time lives.</p>

<h2 id="a-timeline-i-can-actually-compare">A timeline I can actually compare</h2>

<p>Two reports, same year, same underlying weakness class (an authorization check missing on a secondary endpoint). I am keeping the vendors anonymous because the point is the shape, not the names.</p>

<table>
  <thead>
    <tr>
      <th>Milestone</th>
      <th>Bounty platform</th>
      <th>Email to security@</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Report submitted</td>
      <td>Day 0</td>
      <td>Day 0</td>
    </tr>
    <tr>
      <td>Human acknowledgement</td>
      <td>Day 1</td>
      <td>Day 6</td>
    </tr>
    <tr>
      <td>Severity agreed</td>
      <td>Day 3</td>
      <td>Day 9</td>
    </tr>
    <tr>
      <td>Fix in a released version</td>
      <td>Day 74</td>
      <td>Day 51</td>
    </tr>
    <tr>
      <td>Public coordination complete</td>
      <td>Day 90</td>
      <td>Day 65</td>
    </tr>
  </tbody>
</table>

<p>The platform won the first three rows and lost the two that a defender actually cares about. The email vendor was slower to answer, then faster to a shipped fix, because the team that read the email owned the release and scheduled it directly. On the platform, the report cleared triage quickly and then sat in a backlog that the triagers did not control, waiting on an engineering team that treated the platform as an intake system rather than a priority signal.</p>

<p>This is one comparison and I would not build a law out of it. But it matches what I have seen repeatedly. The bounty acknowledgement is a customer-service metric. The patch date is an engineering-capacity metric. They are not the same number and paying for the first does not move the second.</p>

<h2 id="the-severity-number-is-not-the-accelerator-either">The severity number is not the accelerator either</h2>

<p>A related belief is that a high CVSS base score makes the fix go faster. I score every report I send, and I have watched high scores fail to move anything. CVSS v3.1 base measures how bad the bug is if exploited, not how likely a vendor is to prioritize it against its own roadmap. A 9.8 in a rarely-deployed feature can lose the internal queue to a 6.5 in the login path that support is already getting tickets about. The score is an input to the vendor’s decision, not a lever the researcher gets to pull.</p>

<p>What actually moved timelines, in my experience, was none of the things the bounty economics are built around:</p>

<ol>
  <li><strong>A report a triager could confirm without a meeting.</strong> Exact versions, exact request, exact observed versus expected behavior. The <a href="/blog/anatomy-of-a-vulnerability-report-that-gets-fixed-fast/">structure I use for that</a> has done more for my median time-to-fix than any severity argument.</li>
  <li><strong>The bug living in code the vendor was already touching.</strong> A report that lands in an actively maintained module ships fast. One that lands in a frozen component waits for a release nobody had planned.</li>
  <li><strong>A named human who owned both the triage and the release.</strong> This is the single biggest factor, and it is exactly the thing the platform abstracts away. The email vendor was slower to answer precisely because there was no triager in the middle, but the person who eventually answered could also schedule the fix.</li>
</ol>

<h2 id="where-the-exposure-window-goes-and-who-is-standing-in-it">Where the exposure window goes, and who is standing in it</h2>

<p>Here is the part the economics discussion usually skips. While the report moves through either pipeline, the vulnerability is still live in every deployment running the affected version. The bounty payout does not shorten that window for the vendor’s customers. The customers are exposed from the moment the bug exists to the moment they install the fix, which is strictly later than the moment the vendor ships it.</p>

<p>That gap is not the researcher’s to close and it is not the platform’s either. It belongs to whoever is watching the affected systems in the meantime. Plenty of organizations do not wait for a vendor’s timeline at all, because they cannot afford to; they run continuous detection on the exposure window itself, either with an in-house team or by handing that watch to <a href="https://falconersecurity.com/blog/soc-as-a-service/">an outsourced SOC</a> that treats the interval between disclosure and patch as the thing to monitor. That is a different economic decision than the vendor’s, and a more honest one, because it assumes the patch will be late and plans for the window rather than the milestone. The public coordination frameworks, from the <a href="https://www.first.org/global/sigs/vulnerability-coordination/multiparty/guidelines-v1.1">FIRST guidelines</a> to the <a href="https://vuls.cert.org/confluence/display/CVD">CERT/CC coordinated disclosure guide</a>, are all careful to describe that window as a shared-risk period, not a vendor-only one, and they are right to.</p>

<h2 id="so-should-a-vendor-run-a-bounty-program">So should a vendor run a bounty program</h2>

<p>Yes, but not for the reason usually given. A bounty program is worth running because it opens a channel that would otherwise be a silent <code class="language-plaintext highlighter-rouge">security@</code> inbox, it pays researchers who would otherwise be working for free, and it catches the reports that the email path loses entirely. Those are real benefits and I am not waving them off.</p>

<p>Just do not sell it as a speed-to-patch improvement, because on the metric that protects customers, the shipped fix, it mostly is not. The compression it buys is at the front of the timeline, and the front of the timeline was never the expensive part. If a vendor wants faster fixes, the money is better spent on the engineering capacity to turn a confirmed report into a release, and on making sure the person who triages a report can also get it scheduled. That is unglamorous and it does not come with a dashboard. It is also the only thing I have seen actually move the date that matters.</p>]]></content><author><name>Tony West</name></author><category term="commentary" /><category term="disclosure" /><category term="cvd" /><category term="bug-bounty" /><category term="vendor-response" /><category term="cvss" /><category term="vulnerability-research" /><summary type="html"><![CDATA[I have reported the same class of bug through a bounty platform and through a plain security@ address in the same year. The bounty was not reliably faster to a fix. Here is what actually moved the timeline, and why the platform sometimes made it worse.]]></summary></entry><entry><title type="html">Anatomy of a Vulnerability Report That Gets Fixed Fast</title><link href="https://un4gi.io/blog/anatomy-of-a-vulnerability-report-that-gets-fixed-fast/" rel="alternate" type="text/html" title="Anatomy of a Vulnerability Report That Gets Fixed Fast" /><published>2026-08-18T00:00:00+00:00</published><updated>2026-08-18T00:00:00+00:00</updated><id>https://un4gi.io/blog/anatomy-of-a-vulnerability-report-that-gets-fixed-fast</id><content type="html" xml:base="https://un4gi.io/blog/anatomy-of-a-vulnerability-report-that-gets-fixed-fast/"><![CDATA[<h2 id="summary">Summary</h2>

<p>A while ago I wrote about <a href="/blog/what-i-learned-about-cve-coordination/">getting quietly deprioritized by a vendor</a> after handling a disclosure badly. That post was about the relationship. This one is about the artifact: the report itself, and why some of mine get acknowledged and scheduled inside a single patch cycle while others sit in triage for weeks.</p>

<p>The uncomfortable finding, after enough of both kinds, is that the difference is usually not the severity of the bug. It is whether the first person who opens the report can reproduce it and re-state the impact in one sentence without asking me anything. Almost everything below serves those two goals.</p>

<h2 id="what-the-triager-needs-on-the-first-read">What the triager needs on the first read</h2>

<p>A vendor’s intake queue is triaged by someone who has ten other reports open and no context on my finding. The report has to survive that reader. I structure every one around the same fields, in this order, because it front-loads the decision:</p>

<table>
  <thead>
    <tr>
      <th>Field</th>
      <th>What goes in it</th>
      <th>Why it is first-read material</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>One-line impact</td>
      <td>“Unauthenticated attacker can read arbitrary files as the web user”</td>
      <td>The triager can route and rank from this alone</td>
    </tr>
    <tr>
      <td>Affected versions</td>
      <td>Exact build strings I tested, not “latest”</td>
      <td>Tells them whether it is still live in a supported release</td>
    </tr>
    <tr>
      <td>Preconditions</td>
      <td>Default config? Auth required? Non-default feature?</td>
      <td>Separates “critical” from “critical if you enabled X”</td>
    </tr>
    <tr>
      <td>Reproduction</td>
      <td>Numbered steps, exact requests, expected vs actual</td>
      <td>This is the part that gets it fixed or ignored</td>
    </tr>
    <tr>
      <td>Suggested severity</td>
      <td>A full CVSS vector, not just a number</td>
      <td>Shows the work; invites correction rather than a fight</td>
    </tr>
    <tr>
      <td>Remediation</td>
      <td>What a fix would need to do</td>
      <td>Saves the engineer a step and signals good faith</td>
    </tr>
  </tbody>
</table>

<p>The single highest-leverage line is the one-line impact. If a busy reader has to assemble the impact themselves from a wall of technical detail, the report drops down the queue until someone has time. If it is stated in the first sentence and the reproduction backs it up, it gets scheduled.</p>

<h2 id="reproduction-is-the-whole-report">Reproduction is the whole report</h2>

<p>Everything else is framing around this section. A reproduction that a triager can paste into their own environment and watch fail is worth more than any amount of prose about how serious the issue is. What I have learned to include, every time:</p>

<ul>
  <li>The exact request or input, copy-pasteable, not a screenshot of it.</li>
  <li>The expected behaviour and the actual behaviour, stated separately. “It should return 403, it returns the file” is unambiguous. “It is broken” is not.</li>
  <li>The minimum steps. If the bug reproduces without authentication, I do not include a login step. Every removable precondition I leave in makes the finding look narrower than it is.</li>
  <li>A clean environment. I confirm the repro on a fresh install before sending, because “cannot reproduce” from the vendor is where reports go to die, and it is almost always my setup drift rather than their disagreement.</li>
</ul>

<h2 id="the-part-that-is-about-writing-not-research">The part that is about writing, not research</h2>

<p>The technical work is usually the easy half. These are the writing choices that, in my experience, move a report faster:</p>

<ol>
  <li><strong>One bug per report.</strong> Bundling three findings into one thread guarantees that the whole thing moves at the speed of the hardest one. I split them, cross-reference if they chain, and let each get triaged on its own.</li>
  <li><strong>Match their intake format.</strong> If the vendor has a form or a template, I use it exactly. A report that maps onto the fields their tooling expects gets processed; one that ignores the format makes someone re-key it first.</li>
  <li><strong>A CVSS vector, not just a score.</strong> I include the full vector string so the reader can see how I got to the number and adjust it. Handing over a bare “9.8” reads as a negotiating position. Handing over the vector reads as an assessment.</li>
  <li><strong>State the ask once, plainly.</strong> “I am reporting this for coordinated disclosure and will hold publication until a fix ships or 90 days, whichever is first.” No implied threats, no moving the date, no drama. A predictable researcher is an easy one to work with, and that reputation is the thing the bad-list post was really about.</li>
</ol>

<h2 id="what-slows-a-report-down">What slows a report down</h2>

<p>The inverse is just as consistent. The reports of mine that stalled shared some combination of: a vague reproduction that needed a follow-up email before anyone could confirm it, an inflated severity that the vendor spent their first reply arguing down instead of scheduling a fix, multiple loosely-related bugs in one thread, and an adversarial tone that turned a technical exchange into a defensive one. None of those are research failures. They are all report failures, and they are all avoidable.</p>

<h2 id="closing">Closing</h2>

<p>The through-line with the <a href="/blog/vendor-patching-cadence-2020-to-2026/">vendor-patching-cadence</a> data is that vendors are faster than their reputation when the report does not make them work to understand it. The severity of the bug sets the ceiling on how fast it should be fixed. The quality of the report determines whether it gets anywhere near that ceiling. I spend more time on the write-up now than I used to, and it has done more for my fix rates than any improvement in the actual finding.</p>]]></content><author><name>Tony West</name></author><category term="commentary" /><category term="disclosure" /><category term="cvd" /><category term="cvss" /><category term="reporting" /><category term="coordination" /><category term="vulnerability-research" /><summary type="html"><![CDATA[The companion to the bad-list post. If getting deprioritized taught me how coordination goes wrong, the reports that got fixed in one patch cycle taught me what a triager actually needs on the first read. This is the structure I use now, and the parts that are about writing rather than research.]]></summary></entry><entry><title type="html">What I learned about CVE coordination from getting on a vendor’s bad list</title><link href="https://un4gi.io/blog/what-i-learned-about-cve-coordination/" rel="alternate" type="text/html" title="What I learned about CVE coordination from getting on a vendor’s bad list" /><published>2026-07-01T00:00:00+00:00</published><updated>2026-07-01T00:00:00+00:00</updated><id>https://un4gi.io/blog/what-i-learned-about-cve-coordination</id><content type="html" xml:base="https://un4gi.io/blog/what-i-learned-about-cve-coordination/"><![CDATA[<h2 id="summary">Summary</h2>

<p>This is not a vulnerability writeup. There is no CVE ID at the bottom and no affected-versions table. It is process commentary, which is a genre I usually avoid because it tends toward complaint, and complaint is not useful.</p>

<p>The reason I am writing it anyway is that one of my disclosures went badly in a way that was mostly my own doing, and the lesson took a few years to fully land. Somewhere in 2021 I got myself onto a vendor’s informal bad list. Not banned, nothing dramatic. Just deprioritized. My subsequent reports to that vendor sat in triage longer, got terser replies, and in one case were closed as “working as intended” on a finding that a different vendor would have fixed. I earned that. This post is about how.</p>

<p>I am going to keep the vendor anonymous, because naming them would make this the exact kind of adversarial post that caused the problem in the first place. Call them a mid-size networking-equipment vendor with a real but overworked product security team.</p>

<h2 id="what-coordination-is-supposed-to-look-like">What coordination is supposed to look like</h2>

<p>The published models for coordinated vulnerability disclosure are good, and I still believe in them. If you have never read them end to end, they are worth the afternoon:</p>

<ul>
  <li><a href="https://www.iso.org/standard/72311.html">ISO/IEC 29147</a> (vulnerability disclosure) and <a href="https://www.iso.org/standard/69725.html">ISO/IEC 30111</a> (vulnerability handling), which describe the two sides of the same process.</li>
  <li>The <a href="https://vuls.cert.org/confluence/display/CVD">CERT/CC Guide to Coordinated Vulnerability Disclosure</a>, which is the most practical of the lot.</li>
  <li>The <a href="https://www.first.org/global/sigs/vulnerability-coordination/multiparty/guidelines-v1.1">FIRST guidelines and practices for multi-party CVD</a>.</li>
</ul>

<p>All of them describe roughly the same pipeline: the researcher reports privately, the vendor acknowledges and triages, both sides agree a timeline, the fix ships, and public disclosure follows. The documents are careful to frame this as a negotiation between parties with different incentives, not a rulebook that one side enforces on the other. That framing is the part I missed at the time.</p>

<h2 id="what-i-actually-did">What I actually did</h2>

<p>Here is the disclosure that went wrong, reconstructed from my own notes.</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>What happened</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2021-03-02</td>
      <td>Reported an authenticated command-injection issue to the vendor’s security alias. First email included a full writeup, a CVSS vector, and a line stating I would publish in 90 days.</td>
    </tr>
    <tr>
      <td>2021-03-09</td>
      <td>No acknowledgement. Sent a follow-up referencing the 90-day clock again.</td>
    </tr>
    <tr>
      <td>2021-03-18</td>
      <td>Vendor acknowledged. Reply was noticeably cool. Asked me to “not treat this as a countdown.”</td>
    </tr>
    <tr>
      <td>2021-04 to 2021-06</td>
      <td>Slow back-and-forth. I sent two more timeline reminders. The fix slipped past the window I had set.</td>
    </tr>
    <tr>
      <td>2021-06-30</td>
      <td>I published, on schedule, because that was the deadline I had stated. The patch shipped nine days later.</td>
    </tr>
  </tbody>
</table>

<p>Read that table as a vendor and the problem is obvious. I led with a deadline before they had even confirmed receipt. I treated a 90-day window, which is a reasonable default, as a contractual obligation I was entitled to enforce from the first message. I sent reminders that a busy triage engineer reads as pressure, not as help. And I published on the exact day I had threatened to, while a fix was visibly in flight, because I had decided the date mattered more than the outcome.</p>

<p>None of the individual steps violated any disclosure norm. Ninety days is standard. Publishing at the deadline is defensible. But the aggregate signal I sent was “this person is adversarial and will not flex,” and the humans on the other end responded to that signal the way humans do. The next time I showed up in their queue, I was the researcher who published on them.</p>

<h2 id="the-part-the-standards-do-not-cover">The part the standards do not cover</h2>

<p>The disclosure documents describe incentives and timelines. What they do not capture is that coordination runs on a small number of specific people who remember you. A vendor security team is usually two or three overworked engineers, not an institution. If you make one of those people’s week harder for no reason, you are not disputing a policy, you are annoying a colleague you will need again. The RFC-shaped model quietly assumes both sides are interchangeable functionaries. They are not.</p>

<p>The second thing the standards underweight is the defender who is exposed while all of this plays out. My nine-day gap between publication and patch was a gap I created, and during it, every operator running that product was more exposed because I wanted to make a point about deadlines. That is the cost that never shows up in the researcher’s ledger, because the researcher does not carry it.</p>

<p>This is where the disclosure process stops being an abstract negotiation and turns into someone’s actual Tuesday. Between the moment a vulnerability becomes public and the moment a given organization has actually deployed the fix, detection is the only control that helps, and most organizations do not staff for it. It is the reason the <a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA Known Exploited Vulnerabilities catalog</a> exists as a forcing function, and the reason <a href="https://www.first.org/global/sigs/vulnerability-coordination/multiparty/guidelines-v1.1">FIRST’s guidance</a> spends so much time on the exposure window rather than the fix itself. For teams that cannot watch that window themselves, it is also the entire premise of a managed-detection outfit like <a href="https://falconersecurity.com/services/managed-detection-and-response/">the Falconer Security team</a>: someone whose job is the interval between disclosure and remediation, which is precisely the interval a researcher’s publish-date decision widens or narrows. I did not think about that interval at all in 2021. I do now.</p>

<h2 id="what-i-changed">What I changed</h2>

<p>The adjustments were small and boring, which is usually the sign that they are the right ones.</p>

<ol>
  <li>First contact contains no deadline. It contains the finding, a provisional CVSS vector clearly labeled provisional, and an offer to coordinate a timeline once they have triaged. The 90-day expectation comes up in the second exchange, framed as a default I am flexible on.</li>
  <li>Reminders are spaced and phrased as offers of help (“happy to re-test the candidate build”) rather than as clock updates.</li>
  <li>If a fix is genuinely in flight at the deadline, I grant the grace period. Project Zero formalized this with their 30-day grace extension for exactly this situation, and it is a good norm to borrow. Publishing on a vendor who is visibly shipping a fix helps no one except my own sense of consistency.</li>
  <li>I keep the exposure window in mind as a number I am partly responsible for, not just the vendor.</li>
</ol>

<p>Since I started doing this, coordination has been smoother, and the vendor I annoyed in 2021 has, slowly, started responding to me like a normal reporter again. That took about two years and several uneventful, deadline-free reports to repair.</p>

<h2 id="the-point">The point</h2>

<p>Coordinated disclosure is written up as a process between organizations, and it is easy to read the standards as a set of rights the researcher can assert. In practice it is a working relationship between a handful of specific, tired people, mediated by the exposure that sits on operators in the middle. The standards describe the happy path. The friction is the actual subject, and most of the friction I have caused over the years was avoidable and self-inflicted.</p>

<p>If you are early in doing this kind of work: the deadline is a tool, not a weapon, and the person on the other end of the alias will remember which way you used it.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="https://www.iso.org/standard/72311.html">ISO/IEC 29147: Vulnerability disclosure</a></li>
  <li><a href="https://www.iso.org/standard/69725.html">ISO/IEC 30111: Vulnerability handling processes</a></li>
  <li><a href="https://vuls.cert.org/confluence/display/CVD">CERT/CC Guide to Coordinated Vulnerability Disclosure</a></li>
  <li><a href="https://www.first.org/global/sigs/vulnerability-coordination/multiparty/guidelines-v1.1">FIRST: Guidelines and Practices for Multi-Party Coordinated Vulnerability Disclosure</a></li>
  <li><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA Known Exploited Vulnerabilities catalog</a></li>
  <li><a href="https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html">Google Project Zero disclosure policy</a></li>
  <li><a href="/blog/vendor-patching-cadence-2020-to-2026/">Five years on: vendor patching cadence between 2020 and 2026</a></li>
</ul>]]></content><author><name>Tony West</name></author><category term="commentary" /><category term="cve" /><category term="disclosure" /><category term="coordination" /><category term="cvd" /><category term="cvss" /><category term="kev" /><category term="mdr" /><summary type="html"><![CDATA[Process commentary rather than a writeup. A disclosure I handled badly in 2021 got me quietly deprioritized by a vendor's security team, and it taught me more about how coordination actually works than any of the ones that went smoothly.]]></summary></entry><entry><title type="html">DirtyWords v2: smarter targeting for parameter brute-forcing</title><link href="https://un4gi.io/blog/dirtywords-v2-smarter-parameter-bruteforcing/" rel="alternate" type="text/html" title="DirtyWords v2: smarter targeting for parameter brute-forcing" /><published>2026-06-02T00:00:00+00:00</published><updated>2026-06-02T00:00:00+00:00</updated><id>https://un4gi.io/blog/dirtywords-v2-smarter-parameter-bruteforcing</id><content type="html" xml:base="https://un4gi.io/blog/dirtywords-v2-smarter-parameter-bruteforcing/"><![CDATA[<h2 id="summary">Summary</h2>

<p>The original <a href="/blog/introducing-dirtywords-a-targeted-word-list-generator/">dirtywords</a> was a password-candidate generator. You fed it OSINT about a target (names, dates, company terms) and it produced a wordlist tuned for that target’s likely passwords, instead of throwing a generic <code class="language-plaintext highlighter-rouge">rockyou.txt</code> at the problem. That post is from 2021 and the tool did exactly one thing.</p>

<p>v2 adds a second mode that has nothing to do with passwords. It generates targeted HTTP parameter-name wordlists for brute-forcing hidden parameters and endpoints. The motivation is the same one that drove the original tool: a generic wordlist is the wrong default when you have target-specific signal sitting right in front of you. This post is a tooling note on how the parameter mode works and why I built it the way I did.</p>

<p>I am writing this as a design note, not a tutorial. If you maintain a fuzzing or recon tool with a parameter-discovery step, the convention-detection approach below is portable and the failure modes are probably the same shape as the ones I hit.</p>

<h2 id="the-problem-with-generic-parameter-wordlists">The problem with generic parameter wordlists</h2>

<p>Parameter discovery is the step where you find the inputs an application accepts but does not advertise: the <code class="language-plaintext highlighter-rouge">debug=true</code> that flips on a verbose error mode, the <code class="language-plaintext highlighter-rouge">admin</code> flag that nothing in the UI exposes, the <code class="language-plaintext highlighter-rouge">user_id</code> that the endpoint reads even though the documented call only takes a token.</p>

<p>The standard approach is to take a large generic list (the <a href="https://github.com/danielmiessler/SecLists/tree/master/Discovery/Web-Content">SecLists</a> parameter collections, or the list that ships with <a href="https://github.com/s0md3v/Arjun">Arjun</a>) and brute-force every name in it against the endpoint. The lists are good. They are also large, undifferentiated, and built for the average application rather than the one in front of you.</p>

<p>Two costs follow from that. The first is volume. A list of tens of thousands of parameter names, multiplied across every endpoint you want to test, is a lot of requests. On a rate-limited target, or a target where request volume is itself a detection signal, the generic-list approach is slow and noisy. The second cost is ordering. The generic list is alphabetical or frequency-ranked against a global corpus. The parameter the specific target actually accepts might be on line 14,000. You will find it eventually, but “eventually” is doing a lot of work when you are inside an assessment window.</p>

<p>The observation behind v2 is that the target usually tells you its own naming conventions if you look. An application written by a team that uses <code class="language-plaintext highlighter-rouge">snake_case</code> for everything is not going to suddenly accept <code class="language-plaintext highlighter-rouge">camelCaseParams</code>. An API that prefixes booleans with <code class="language-plaintext highlighter-rouge">is_</code> and <code class="language-plaintext highlighter-rouge">has_</code> is telling you the shape of the boolean parameter you have not found yet. A Rails backend has idioms a Django backend does not. If you can read those conventions off the target’s observable surface, you can generate a much smaller, much better-ordered candidate list.</p>

<h2 id="what-v2-does">What v2 does</h2>

<p>The parameter mode runs in three stages: learn the conventions, generate candidates that match them, rank the candidates by how well they fit.</p>

<h3 id="stage-1-learn-the-conventions">Stage 1: learn the conventions</h3>

<p>v2 harvests naming signal from whatever of the target’s surface you can give it:</p>

<ul>
  <li><strong>Observed requests.</strong> The parameter names already present in requests you have captured (from a proxy log, a HAR export, or a Burp session). These are ground truth for the target’s conventions.</li>
  <li><strong>JavaScript bundles.</strong> Front-end bundles are full of parameter names, field names, and API path fragments. v2 pulls candidate tokens from string literals and object keys in the JS you point it at. This is the single richest source on most modern applications.</li>
  <li><strong>OpenAPI / Swagger documents</strong>, if the target exposes one. A <code class="language-plaintext highlighter-rouge">swagger.json</code> is a complete, authoritative list of the parameters the documented surface accepts, and a strong prior for the conventions the undocumented surface uses. See the <a href="https://swagger.io/specification/">OpenAPI specification</a> for the structure v2 parses.</li>
  <li><strong>GraphQL introspection</strong>, if introspection is enabled. The schema field names are the same convention vocabulary as the REST parameters on most stacks that run both. See the <a href="https://graphql.org/learn/introspection/">GraphQL introspection docs</a>.</li>
  <li><strong>Response bodies.</strong> JSON response field names frequently mirror the input parameter names. If the response carries <code class="language-plaintext highlighter-rouge">createdAt</code>, the input filter is more likely <code class="language-plaintext highlighter-rouge">createdBefore</code> than <code class="language-plaintext highlighter-rouge">created_before</code>.</li>
</ul>

<p>From these sources v2 derives a small convention profile:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nd">@dataclass</span>
<span class="k">class</span> <span class="nc">ConventionProfile</span><span class="p">:</span>
    <span class="n">case_style</span><span class="p">:</span> <span class="nb">str</span>          <span class="c1"># "snake" | "camel" | "kebab" | "mixed"
</span>    <span class="n">bool_prefixes</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span>  <span class="c1"># e.g. ["is_", "has_", "can_"]
</span>    <span class="n">id_suffix</span><span class="p">:</span> <span class="nb">str</span>           <span class="c1"># e.g. "_id" or "Id"
</span>    <span class="n">common_affixes</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="nb">str</span><span class="p">]</span> <span class="c1"># observed prefixes/suffixes by frequency
</span>    <span class="n">framework_hint</span><span class="p">:</span> <span class="nb">str</span> <span class="o">|</span> <span class="bp">None</span>  <span class="c1"># "rails" | "django" | "express" | None
</span>    <span class="n">vocab</span><span class="p">:</span> <span class="n">collections</span><span class="p">.</span><span class="n">Counter</span>  <span class="c1"># observed tokens by frequency
</span></code></pre></div></div>

<p>The case-style detection is the load-bearing part and it is mostly counting. If 90% of observed names match <code class="language-plaintext highlighter-rouge">[a-z]+(_[a-z]+)+</code>, the profile is <code class="language-plaintext highlighter-rouge">snake</code>. The framework hint is a weaker signal derived from telltale parameter shapes (<code class="language-plaintext highlighter-rouge">_attributes</code> suffixes and <code class="language-plaintext highlighter-rouge">[]</code>-bracketed nested params point at Rails; <code class="language-plaintext highlighter-rouge">__</code> lookup separators point at Django ORM filters; specific pagination param names point at common Express middleware). The hint is advisory. v2 never hard-codes a behaviour on it; it only uses it to raise the rank of framework-idiomatic candidates.</p>

<h3 id="stage-2-generate-candidates">Stage 2: generate candidates</h3>

<p>With a profile in hand, v2 builds candidates three ways:</p>

<ol>
  <li><strong>Vocabulary recombination.</strong> Take the observed token vocabulary and recombine it under the target’s conventions. If the vocab contains <code class="language-plaintext highlighter-rouge">user</code>, <code class="language-plaintext highlighter-rouge">account</code>, <code class="language-plaintext highlighter-rouge">id</code>, and <code class="language-plaintext highlighter-rouge">email</code>, and the case style is <code class="language-plaintext highlighter-rouge">snake</code>, generate <code class="language-plaintext highlighter-rouge">user_id</code>, <code class="language-plaintext highlighter-rouge">account_id</code>, <code class="language-plaintext highlighter-rouge">user_email</code>, <code class="language-plaintext highlighter-rouge">account_email</code>, and so on. This is where most of the high-value candidates come from, because the tokens are the target’s own words.</li>
  <li><strong>Convention-templated mutation.</strong> Apply the observed affixes and idioms. If the profile says booleans are prefixed <code class="language-plaintext highlighter-rouge">is_</code>, and <code class="language-plaintext highlighter-rouge">active</code> is in the vocab, generate <code class="language-plaintext highlighter-rouge">is_active</code>. If the framework hint is Rails and <code class="language-plaintext highlighter-rouge">user</code> is a known resource, generate <code class="language-plaintext highlighter-rouge">user_attributes</code> and <code class="language-plaintext highlighter-rouge">user[]</code> forms.</li>
  <li><strong>Generic-list intersection.</strong> Take a generic parameter list (SecLists, Arjun’s list, whatever you point it at) and re-case every entry into the target’s convention, then keep only the entries whose tokens overlap the target vocabulary or whose shape matches the profile. This is how v2 still benefits from the generic lists without inheriting their volume. A 20,000-entry generic list typically collapses to a few hundred convention-matched candidates.</li>
</ol>

<h3 id="stage-3-rank">Stage 3: rank</h3>

<p>The output is ranked, not alphabetical. The ranking score for a candidate is a weighted sum of:</p>

<ul>
  <li>token frequency in the observed vocabulary (a candidate built from words the target uses a lot ranks higher),</li>
  <li>convention-match strength (a candidate that perfectly matches the case style and affix profile ranks higher than one that partially matches),</li>
  <li>framework-idiom bonus (small, only applied when the framework hint is set),</li>
  <li>a penalty for length and token count (short, simple parameters are more common than long compound ones).</li>
</ul>

<p>The point of the ranking is that the parameter the target actually accepts should be near the top of the list, so the brute-force finds it in the first few hundred requests rather than the last few thousand. On the assessments where I have run v2 against a baseline generic list, the convention-matched list found the same hidden parameters in roughly an order of magnitude fewer requests. That is a measured claim on a small sample, not a benchmark, and it depends heavily on how much JS or how complete a Swagger document the target exposed.</p>

<h2 id="output-formats">Output formats</h2>

<p>v2 writes the ranked list in the formats the downstream tools want, because nobody wants to reformat a wordlist by hand:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>$ dirtywords params --js ./bundles/ --har ./session.har --out target
Learned profile: case=snake bool_prefixes=[is_,has_] id_suffix=_id framework=rails
Generated 412 ranked candidates (from 18,400 generic + 1,240 observed tokens)
Wrote:
  target.txt        plain ranked wordlist
  target.ffuf       ffuf -w input
  target.arjun      Arjun --wordlist input
  target.json       full candidates with scores
</code></pre></div></div>

<p>The plain list pipes straight into <a href="https://github.com/ffuf/ffuf">ffuf</a> or <a href="https://github.com/s0md3v/Arjun">Arjun</a>. The JSON output carries the per-candidate scores so you can threshold it (only try candidates above a score, when you want to keep the request count very low) or feed it into <a href="https://portswigger.net/burp/documentation/desktop/tools/intruder">Burp Intruder</a> as a ranked payload set. The parameter-discovery methodology this supports is the one in the <a href="https://owasp.org/www-project-web-security-testing-guide/">OWASP Web Security Testing Guide</a>; v2 is a wordlist generator for that step, not a replacement for it.</p>

<h2 id="what-did-not-work-on-the-first-pass">What did not work on the first pass</h2>

<p>Three things I tried and dropped.</p>

<p><strong>Inferring types and generating values.</strong> The first design tried to infer each parameter’s type (boolean, integer, ID, enum) and emit candidate values alongside the names. This turned into scope creep. Value generation is a different problem from name discovery, the type inference was wrong often enough to be misleading, and the downstream fuzzers already handle value generation. v2 generates names only. If you need values, that is the fuzzer’s job.</p>

<p><strong>Over-trusting the framework hint.</strong> An early version hard-branched on the framework hint: if it detected Rails, it generated only Rails-idiomatic candidates. That was wrong twice. Once because the detection was sometimes incorrect and the wrong branch produced a useless list, and once because real applications mix conventions (a Rails monolith with a bolted-on Express service has two conventions, not one). The hint is now advisory and only nudges ranking. The generator never excludes candidates on the strength of it.</p>

<p><strong>Scraping JS at runtime.</strong> I wanted v2 to fetch and parse the target’s live JS itself. I removed it. Fetching target assets is a decision the operator should make explicitly inside the engagement scope, not something a wordlist generator does as a side effect. v2 reads JS from files you give it. Collecting those files is a separate, deliberate step.</p>

<h2 id="status">Status</h2>

<p>The parameter mode is in the dirtywords main branch alongside the original password mode. The two modes share nothing except the CLI. The original password-generation behaviour from the <a href="/blog/introducing-dirtywords-a-targeted-word-list-generator/">2021 post</a> is unchanged; if you used dirtywords for password candidates before, that path still works the same way. The repo is at <a href="https://github.com/un4gi/dirtywords">github.com/un4gi/dirtywords</a>.</p>

<p>This is the same kind of small, single-purpose tooling work as the recent <a href="/blog/adding-slack-integration-to-fave/">FAVE rebuild</a>: take a step in an assessment workflow that defaults to a generic approach, and make it use the target-specific signal that is usually already available.</p>

<h2 id="what-is-next">What is next</h2>

<p>Two follow-ups are in scope. The first is a scoring-calibration pass: the current ranking weights are hand-tuned against a handful of assessments, and they should be fit against a larger labelled set of “convention profile to actually-accepted parameters” pairs. The second is an endpoint-path mode that applies the same convention-learning idea to directory and route discovery, which is the same problem one layer up. Both are notes-for-later, not commitments.</p>

<p>If you run a similar parameter-discovery step and want to compare convention-detection heuristics, the issues page on the repo is the right place.</p>]]></content><author><name>Tony West</name></author><category term="tools" /><category term="tools" /><category term="dirtywords" /><category term="wordlist" /><category term="parameter-bruteforce" /><category term="web-security" /><category term="fuzzing" /><summary type="html"><![CDATA[The original dirtywords generated password candidates from OSINT. v2 adds a mode that generates targeted HTTP parameter-name wordlists by learning a target's own naming conventions, so the brute-force tries the parameters the application is actually likely to accept first.]]></summary></entry><entry><title type="html">Adding a Slack integration to FAVE: notes from the rebuild</title><link href="https://un4gi.io/blog/adding-slack-integration-to-fave/" rel="alternate" type="text/html" title="Adding a Slack integration to FAVE: notes from the rebuild" /><published>2026-05-16T00:00:00+00:00</published><updated>2026-05-16T00:00:00+00:00</updated><id>https://un4gi.io/blog/adding-slack-integration-to-fave</id><content type="html" xml:base="https://un4gi.io/blog/adding-slack-integration-to-fave/"><![CDATA[<h2 id="summary">Summary</h2>

<p>I pulled FAVE off the shelf last month. The 2021 <a href="/blog/fave-find-a-vulnerability-exposure/">introduction post</a> described what the tool does: take a target’s detected software-version stack and return a prioritised list of applicable CVEs by querying NVD and adjacent sources. The output layer in the 2021 build was a CLI table and a JSON dump. That worked for a single-target run, but it never fit how I use the tool now, which is unattended across a small queue of assessments with results that need to land somewhere a teammate can read at the same time.</p>

<p>This post documents the Slack integration I added during the rebuild. I am writing it as a tooling note rather than a tutorial. If you maintain a recon tool with a similar output gap, the design decisions and the failure modes are likely the same shape as the ones I ran into.</p>

<h2 id="why-i-rebuilt-this-part-now">Why I rebuilt this part now</h2>

<p>Two reasons. The first is that the assessment workload has changed. In 2021 I ran FAVE manually, read the output table on the terminal where I had launched it, and pasted the relevant lines into whatever scratchpad held the engagement notes. In 2026 the queue runs unattended on a small VPS overnight and surfaces results into the morning. A terminal dump is no longer useful.</p>

<p>The second reason is downstream. As I <a href="/blog/vendor-patching-cadence-2020-to-2026/">wrote in the May cadence retrospective</a>, the value of a CVE record in 2026 is increasingly tied to whether it lands in front of someone who can act on it within the same week. The bottleneck for me was not the CVE lookup itself, it was the gap between the lookup completing and the operator seeing the result. The Slack integration closes that gap.</p>

<h2 id="what-i-wanted-from-the-integration">What I wanted from the integration</h2>

<p>Five requirements, in priority order:</p>

<ol>
  <li><strong>One channel per engagement.</strong> Each FAVE run is parameterised with an engagement tag. The integration must post into the channel for that tag, not into a single firehose.</li>
  <li><strong>No duplicate alerts.</strong> If a CVE was already posted for a given target during the engagement, do not post it again on the next nightly run.</li>
  <li><strong>Severity-aware formatting.</strong> A CVSS 9.8 record needs to look different from a CVSS 4.3 record at a glance.</li>
  <li><strong>Linkbacks for everything.</strong> Every CVE in the post must link to its NVD record. Every host must link to the source FAVE scan summary file.</li>
  <li><strong>Rate-limit-aware.</strong> A run with 200 new findings should not get the integration banned by Slack’s tier-1 rate limiter on the first attempt.</li>
</ol>

<p>These map directly onto the implementation choices below.</p>

<h2 id="architecture">Architecture</h2>

<p>FAVE’s output layer used to be a single <code class="language-plaintext highlighter-rouge">Reporter</code> class that wrote to stdout and to a JSON file. The rebuild keeps that class for the JSON-file path but adds two new classes: <code class="language-plaintext highlighter-rouge">SlackPublisher</code> and <code class="language-plaintext highlighter-rouge">AlertState</code>. The flow is:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[ FAVE scan core ]
        |
        v
[ Reporter (json) ] --&gt; /var/lib/fave/runs/&lt;run-id&gt;/results.json
        |
        v
[ SlackPublisher.from_run(run-id) ]
        |
        | reads:                writes:
        |   AlertState           AlertState
        |   results.json         Slack webhook
        v
[ Per-target Slack message batch ]
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">AlertState</code> is a small SQLite file at <code class="language-plaintext highlighter-rouge">/var/lib/fave/state/alerts.sqlite</code> with one table:</p>

<div class="language-sql highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">CREATE</span> <span class="k">TABLE</span> <span class="n">posted_alerts</span> <span class="p">(</span>
  <span class="n">engagement</span>   <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">target</span>       <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">cve</span>          <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="n">first_posted</span> <span class="nb">TEXT</span> <span class="k">NOT</span> <span class="k">NULL</span><span class="p">,</span>
  <span class="k">PRIMARY</span> <span class="k">KEY</span> <span class="p">(</span><span class="n">engagement</span><span class="p">,</span> <span class="n">target</span><span class="p">,</span> <span class="n">cve</span><span class="p">)</span>
<span class="p">);</span>
</code></pre></div></div>

<p>That is the entire deduplication mechanism. A row is inserted the first time a <code class="language-plaintext highlighter-rouge">(engagement, target, cve)</code> triple gets posted, and subsequent runs that produce the same triple skip the post. The decision to make this engagement-scoped rather than global is intentional: across engagements, the same CVE is a fresh signal, because the operator who reads the message has different context.</p>

<h2 id="implementation">Implementation</h2>

<p>FAVE is Python 3.11. I considered using the official <code class="language-plaintext highlighter-rouge">slack-sdk</code> package but ended up with a thin requests-based wrapper, because the integration only needs one Slack endpoint: an incoming webhook. The advantages of <code class="language-plaintext highlighter-rouge">slack-sdk</code> (token rotation, Block Kit type hints, OAuth helpers) do not apply when you are using a webhook URL.</p>

<p>The webhook config lives in <code class="language-plaintext highlighter-rouge">/etc/fave/slack.toml</code>:</p>

<div class="language-toml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">[</span><span class="n">engagement</span><span class="k">.</span><span class="n">acme-q2-2026</span><span class="k">]</span>
<span class="n">webhook_url</span> <span class="o">=</span><span class="w"> </span><span class="s">"https://hooks.slack.com/services/T0XXX/B0YYY/abc123..."</span>
<span class="n">channel_name</span> <span class="o">=</span><span class="w"> </span><span class="s">"#sec-acme-q2"</span>
<span class="n">severity_threshold</span> <span class="o">=</span><span class="w"> </span><span class="mf">7.0</span>

<span class="k">[</span><span class="n">engagement</span><span class="k">.</span><span class="n">internal-recon</span><span class="k">]</span>
<span class="n">webhook_url</span> <span class="o">=</span><span class="w"> </span><span class="s">"https://hooks.slack.com/services/T0XXX/B0ZZZ/def456..."</span>
<span class="n">channel_name</span> <span class="o">=</span><span class="w"> </span><span class="s">"#sec-internal-recon"</span>
<span class="n">severity_threshold</span> <span class="o">=</span><span class="w"> </span><span class="mf">4.0</span>
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">severity_threshold</code> is a per-engagement floor on the CVSS base score below which findings are written to the JSON file but not posted to Slack. For the internal-recon engagement, the operator wants medium-and-up. For acme-q2-2026, the operator wants high-and-critical only and reviews the JSON output separately for the rest.</p>

<p>The publisher itself is short. The relevant body is:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">SlackPublisher</span><span class="p">:</span>
    <span class="k">def</span> <span class="nf">__init__</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">engagement_cfg</span><span class="p">,</span> <span class="n">state</span><span class="p">:</span> <span class="n">AlertState</span><span class="p">):</span>
        <span class="n">self</span><span class="p">.</span><span class="n">cfg</span> <span class="o">=</span> <span class="n">engagement_cfg</span>
        <span class="n">self</span><span class="p">.</span><span class="n">state</span> <span class="o">=</span> <span class="n">state</span>

    <span class="k">def</span> <span class="nf">publish_findings</span><span class="p">(</span><span class="n">self</span><span class="p">,</span> <span class="n">run_id</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">findings</span><span class="p">:</span> <span class="nb">list</span><span class="p">[</span><span class="n">Finding</span><span class="p">]):</span>
        <span class="n">new_findings</span> <span class="o">=</span> <span class="p">[</span>
            <span class="n">f</span> <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">findings</span>
            <span class="k">if</span> <span class="n">f</span><span class="p">.</span><span class="n">cvss_base</span> <span class="o">&gt;=</span> <span class="n">self</span><span class="p">.</span><span class="n">cfg</span><span class="p">.</span><span class="n">severity_threshold</span>
            <span class="ow">and</span> <span class="ow">not</span> <span class="n">self</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="nf">already_posted</span><span class="p">(</span>
                <span class="n">self</span><span class="p">.</span><span class="n">cfg</span><span class="p">.</span><span class="n">engagement</span><span class="p">,</span> <span class="n">f</span><span class="p">.</span><span class="n">target</span><span class="p">,</span> <span class="n">f</span><span class="p">.</span><span class="n">cve</span>
            <span class="p">)</span>
        <span class="p">]</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">new_findings</span><span class="p">:</span>
            <span class="k">return</span>

        <span class="k">for</span> <span class="n">batch</span> <span class="ow">in</span> <span class="nf">chunked</span><span class="p">(</span><span class="n">new_findings</span><span class="p">,</span> <span class="mi">25</span><span class="p">):</span>
            <span class="n">payload</span> <span class="o">=</span> <span class="n">self</span><span class="p">.</span><span class="nf">_build_block_kit</span><span class="p">(</span><span class="n">batch</span><span class="p">,</span> <span class="n">run_id</span><span class="p">)</span>
            <span class="n">self</span><span class="p">.</span><span class="nf">_post_with_retry</span><span class="p">(</span><span class="n">payload</span><span class="p">)</span>
            <span class="k">for</span> <span class="n">f</span> <span class="ow">in</span> <span class="n">batch</span><span class="p">:</span>
                <span class="n">self</span><span class="p">.</span><span class="n">state</span><span class="p">.</span><span class="nf">mark_posted</span><span class="p">(</span>
                    <span class="n">self</span><span class="p">.</span><span class="n">cfg</span><span class="p">.</span><span class="n">engagement</span><span class="p">,</span> <span class="n">f</span><span class="p">.</span><span class="n">target</span><span class="p">,</span> <span class="n">f</span><span class="p">.</span><span class="n">cve</span>
                <span class="p">)</span>
</code></pre></div></div>

<p>Three things in this code are non-obvious and worth calling out.</p>

<h3 id="batching">Batching</h3>

<p>The Slack message-size limit for an incoming webhook is 40,000 characters of payload. A single Block Kit message can carry roughly 50 sections before it gets unwieldy. I batch in chunks of 25 to leave headroom and keep each posted message readable. The chunking is a <code class="language-plaintext highlighter-rouge">chunked()</code> helper that yields fixed-size slices of an iterable.</p>

<h3 id="state-write-only-on-success">State write only on success</h3>

<p>The <code class="language-plaintext highlighter-rouge">state.mark_posted</code> call happens after the post returns 2xx. If the post fails, the state is not written, and the next FAVE run will retry the same finding. This is the correct posture for an alerting integration: a finding that has not actually surfaced to the operator should be retried, not silently marked as delivered.</p>

<h3 id="retry-with-backoff">Retry with backoff</h3>

<p><code class="language-plaintext highlighter-rouge">_post_with_retry</code> uses an exponential backoff with jitter (0.5s, 1s, 2s, 4s, 8s) for transient errors (HTTP 5xx, connection-reset). On a 429 <code class="language-plaintext highlighter-rouge">Retry-After</code>, it sleeps for the duration the server requests and then continues. On any 4xx other than 429, it gives up and raises, because a 400 from Slack means the payload is malformed and retrying will not help.</p>

<h2 id="message-formatting">Message formatting</h2>

<p>I use <a href="https://api.slack.com/messaging/composing/layouts">Slack Block Kit</a> rather than plain text. The relevant blocks per finding are a Header (target hostname), a Section with severity-coloured indicator, a divider, and a context block with the source file link.</p>

<p>A rendered finding looks like:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>─────────────────────────────────────────
  acme-app01.internal
─────────────────────────────────────────
  CVE-2024-39882   CRITICAL  CVSS 9.8
  Authentication bypass in Acme Web v3.1.2
  → NVD record
─────────────────────────────────────────
  Run: fave-2026-05-16-acme-q2  ·  results
</code></pre></div></div>

<p>The severity tag is a unicode emoji-free indicator. Slack messages do not need emojis to be readable, and operator feedback on the first pass was that emoji-heavy alerts produce inconsistent rendering across desktop and mobile clients. Critical findings get a leading <code class="language-plaintext highlighter-rouge">■</code> and <code class="language-plaintext highlighter-rouge">[CRITICAL]</code> text label. The colour comes from the Block Kit <code class="language-plaintext highlighter-rouge">attachments.color</code> field on the surrounding container, which Slack renders as a left-edge bar. Red for critical (CVSS &gt;= 9.0), orange for high (&gt;= 7.0), grey otherwise.</p>

<p>The “results” link goes to a per-run static HTML report that FAVE writes alongside the JSON file. This is the same change the cadence post argued for at the institutional level: an alert is more useful when there is a primary-source artefact one click away.</p>

<h2 id="rate-limit-handling">Rate-limit handling</h2>

<p><a href="https://api.slack.com/rate-limits">Slack’s rate-limit documentation</a> is precise: webhook integrations are tier-1, which is “one message per second per webhook, with short bursts allowed”. The <code class="language-plaintext highlighter-rouge">Retry-After</code> header on a 429 is the authoritative source for how long to back off.</p>

<p>I tested this with an artificial 200-finding run against a private webhook. With batching at 25 per message, that is 8 messages, and the integration completes in well under 30 seconds without ever hitting 429. The retry path exists for unexpected upstream issues, not for the well-behaved case.</p>

<p>For larger runs, batching at 25 with the 1s pacing gives a clean linear scale. A 1,000-finding run would take ~40 messages, ~40 seconds. I have not yet had a run that produces 1,000 unique findings post-deduplication, so the upper bound has not been stress-tested in production conditions.</p>

<h2 id="what-did-not-work-on-the-first-pass">What did not work on the first pass</h2>

<p>Three things I tried that did not survive review.</p>

<h3 id="posting-findings-as-threaded-replies">Posting findings as threaded replies</h3>

<p>The first design used a parent “run summary” message and posted each finding as a threaded reply. The intent was a tidy Slack channel where each FAVE run was one thread the operator could collapse.</p>

<p>The problem is that incoming webhooks cannot post to a thread. They can only post into a channel. Threaded replies require either a bot token (<code class="language-plaintext highlighter-rouge">chat.postMessage</code> with <code class="language-plaintext highlighter-rouge">thread_ts</code>) or a Slack app. Neither is appropriate for this use case, since the webhook URL is the only artefact the engagement-channel admin gives the integration.</p>

<p>I went back to flat-channel posts, which is what works with a webhook.</p>

<h3 id="embedding-cvss-vector-strings-as-full-blocks">Embedding CVSS vector strings as full blocks</h3>

<p>I tried rendering the CVSS vector string (<code class="language-plaintext highlighter-rouge">CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H</code>) as a stand-alone Block Kit section per finding, with each component labelled. This was visually correct but doubled the message length and pushed the 25-per-batch chunking down to 12. Operators told me they did not read past the base score on most findings and would click through to NVD for the vector when they needed it.</p>

<p>The vector is now in a single line in the context block at the bottom of each finding, not a stand-alone block. Operators get the score prominently and the vector available without claiming the visual budget.</p>

<h3 id="detecting-channel-paused-status">Detecting “channel paused” status</h3>

<p>I wanted the integration to detect when the engagement channel was archived or the webhook had been revoked, and degrade gracefully (write a warning to a log and stop trying for that engagement). Slack webhooks return a 404 when the URL is no longer valid, but they also return a 404 in some transient cases, and I did not find a reliable signal that distinguished permanent revocation from a transient issue.</p>

<p>The current behaviour is: three consecutive 404s for a given webhook causes the integration to log a <code class="language-plaintext highlighter-rouge">CHANNEL_PAUSED</code> event for that engagement, but FAVE keeps trying on the next run. The decision to actually pause needs an operator. Automating it would have produced false-negative pauses that cause findings to silently stop reaching the channel.</p>

<h2 id="status">Status</h2>

<p>The Slack integration is in the FAVE main branch. It is enabled by default if <code class="language-plaintext highlighter-rouge">/etc/fave/slack.toml</code> exists. The 2021 <a href="/blog/fave-find-a-vulnerability-exposure/">FAVE introduction post</a> is still the canonical reference for what the tool does at the scan-and-correlate level; this post is the canonical reference for the output layer in 2026.</p>

<p>If you run a similar tool and want to compare notes on the deduplication state shape or the Block Kit chunking decisions, the FAVE repo issues page is the right place.</p>

<h2 id="what-is-next">What is next</h2>

<p>Two follow-ups are already in scope.</p>

<p>The first is a <a href="https://api.slack.com/reference/block-kit">Block Kit</a> version of the per-run static HTML report that the “results” link currently goes to. The HTML report is a server-rendered page; the next iteration will let the operator open the same report inside a Slack canvas, which removes a context switch that several operators have flagged.</p>

<p>The second is a webhook-signature-verification path so FAVE can accept inbound Slack events (a slash command, for example, that re-queries a host from inside the channel). That is a larger change because it requires FAVE to expose an HTTP endpoint to Slack, and the threat model on that endpoint is not the same as the threat model on an outbound webhook. The current scope of this post is the outbound side only.</p>

<p>The cadence between this rebuild and the next post will be roughly four weeks, matching the cadence in <a href="/blog/vendor-patching-cadence-2020-to-2026/">the publishing notes</a>.</p>]]></content><author><name>Tony West</name></author><category term="tools" /><category term="tools" /><category term="fave" /><category term="slack" /><category term="integration" /><category term="alerting" /><category term="automation" /><summary type="html"><![CDATA[After four years on the shelf, I rebuilt FAVE's output layer to push reconnaissance results into a Slack channel instead of a stdout dump. Here is the architecture, the implementation notes, and the things that did not work on the first pass.]]></summary></entry><entry><title type="html">Five years on, what changed in vendor patching cadence between 2020 and 2026</title><link href="https://un4gi.io/blog/vendor-patching-cadence-2020-to-2026/" rel="alternate" type="text/html" title="Five years on, what changed in vendor patching cadence between 2020 and 2026" /><published>2026-05-09T00:00:00+00:00</published><updated>2026-05-09T00:00:00+00:00</updated><id>https://un4gi.io/blog/vendor-patching-cadence-2020-to-2026</id><content type="html" xml:base="https://un4gi.io/blog/vendor-patching-cadence-2020-to-2026/"><![CDATA[<h2 id="summary">Summary</h2>

<p>In April I posted a <a href="/blog/five-years-on-revisiting-quest-uc-vulnerabilities/">five-year retrospective</a> covering the institutional changes around vulnerability disclosure since the 2020 <a href="/blog/multiple-vulnerabilities-in-quest-policy-authority-for-unified-communications/">Quest Policy Authority advisory</a>. That post covered the structural side: NVD enrichment, the CVE program funding scare, the EU Cyber Resilience Act, the rise of the KEV catalog. It did not look closely at the question that operators ask first: when a researcher discloses something, how long does it actually take for a patch to appear?</p>

<p>This post tackles that narrower question: has vendor patching cadence changed between 2020 and 2026?</p>

<p>The short answer is yes for some vendors, no for most, and the gap between the two has widened. There is no single industry cadence to quote. There are at least three, and they have diverged.</p>

<p>I draw on three sources below. None of them is a complete dataset.</p>

<ol>
  <li>What vendors publish about their own process. SLA pages, security blog posts, advisory cadence.</li>
  <li>What the KEV catalog records about real exploitation timelines.</li>
  <li>What I have observed in my own follow-up disclosures since the 2020 Quest writeup. Sample size is small. I am not Project Zero.</li>
</ol>

<h2 id="what-cadence-actually-means">What “cadence” actually means</h2>

<p>Before any numbers, a definition. The full disclosure pipeline has at least four measurable intervals.</p>

<table>
  <thead>
    <tr>
      <th>Interval</th>
      <th>What it measures</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Time-to-acknowledge</td>
      <td>Researcher first contact, vendor confirms receipt and triage</td>
    </tr>
    <tr>
      <td>Time-to-fix</td>
      <td>Acknowledge, code change merged, internal build available</td>
    </tr>
    <tr>
      <td>Time-to-ship</td>
      <td>Internal build, GA release reaching customers</td>
    </tr>
    <tr>
      <td>Time-to-disclose</td>
      <td>Researcher coordination window, public advisory</td>
    </tr>
  </tbody>
</table>

<p>Public conversations about cadence usually conflate these. A vendor who acknowledges within a day but ships a year later is not the same as a vendor who responds in two weeks and patches in three. Both are common. Both look like “the vendor responded” in casual reporting.</p>

<p>In the rest of this post I will be specific about which interval I am citing. The KEV catalog effectively measures something else again: time from public CVE to documented exploitation, and then a separate federal-agency remediation deadline. I will treat that as its own thing.</p>

<h2 id="what-vendors-publish">What vendors publish</h2>

<p>The biggest change since 2020 is that more vendors now publish a security policy page at all. In 2020, finding a vendor’s coordinated disclosure SLA usually meant emailing security@ and waiting to see what came back. In 2026 the larger vendors have committed something to writing. Whether the document is honored is a separate question.</p>

<p>A short, non-exhaustive comparison of what large vendors say about their own cadence:</p>

<table>
  <thead>
    <tr>
      <th>Vendor / project</th>
      <th>Public cadence commitment</th>
      <th>Where to verify</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Microsoft</td>
      <td>Monthly Patch Tuesday for non-emergency fixes; 90 days as the default coordinated disclosure window before independent publication; out-of-band releases for actively exploited issues</td>
      <td><a href="https://msrc.microsoft.com/update-guide">Microsoft Security Update Guide</a> and the <a href="https://msrc.microsoft.com/blog/">MSRC blog</a></td>
    </tr>
    <tr>
      <td>Apple</td>
      <td>Fixed-version-only model: security fixes ship in the next supported OS update, not as targeted backports; security content documented per release</td>
      <td><a href="https://support.apple.com/en-us/100100">Apple security releases</a></td>
    </tr>
    <tr>
      <td>Linux kernel</td>
      <td>No formal SLA; most fixes land within days of report when the report is well-formed; coordinated embargo via linux-distros for issues with cross-distro impact</td>
      <td>Kernel maintainer correspondence is largely public on lkml</td>
    </tr>
    <tr>
      <td>Major cloud / hyperscale</td>
      <td>Internal first, customer-facing CVEs after mitigation; a meaningful share never receive a public CVE because the customer has no remediation action</td>
      <td> </td>
    </tr>
    <tr>
      <td>Google Project Zero (researcher side, for context)</td>
      <td>90-day disclosure window plus optional 30-day grace if a fix is in flight; immediate disclosure for actively exploited issues</td>
      <td><a href="https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html">Project Zero policy</a></td>
    </tr>
    <tr>
      <td>Bug-bounty platforms</td>
      <td>No platform-wide SLA; per-program. Most published programs commit to triage within 72 hours and a fix-or-update target somewhere between 30 and 180 days</td>
      <td><a href="https://hackerone.com/">HackerOne directory</a>, <a href="https://www.bugcrowd.com/">Bugcrowd directory</a></td>
    </tr>
  </tbody>
</table>

<p>Pre-2020, almost none of this was written down in a way a researcher could cite. The improvement is real, even if the actual time-to-fix has not always followed.</p>

<p>The gap to keep in mind is between what a vendor publishes and what a researcher experiences. Microsoft’s published target is 90 days. The actual time from acknowledgment to a Patch Tuesday release for routine findings often runs longer, because routine findings get queued behind whatever is currently being exploited. Apple’s fixed-version-only model is honest but it means a fix landing in iOS 19.x reaches no users still on iOS 18, by design.</p>

<h2 id="what-the-kev-catalog-measures">What the KEV catalog measures</h2>

<p>CISA’s <a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">Known Exploited Vulnerabilities catalog</a> is the closest thing the industry has to a public, structured cadence dataset. It is also commonly misread, so a quick note on what it does and does not measure.</p>

<p>KEV records do not directly track vendor patching cadence. They track exploitation. A KEV entry includes a CVE ID, the date CISA added it to the catalog, and the federal-agency remediation deadline (<code class="language-plaintext highlighter-rouge">dueDate</code>). The implicit assumption is that any KEV-listed CVE has a vendor patch available, since federal agencies are required to remediate by the due date. That is mostly true.</p>

<p>What you can derive from KEV records is two narrower numbers.</p>

<ol>
  <li>Time from CVE publication to KEV addition. This is roughly the gap between a vulnerability being disclosed and CISA confirming it is being exploited. It varies from days (recently weaponized 0-days) to years (older vulnerabilities reaching exploitation through commodity tooling).</li>
  <li>Federal-agency remediation deadline. CISA typically sets the <code class="language-plaintext highlighter-rouge">dueDate</code> 14 to 21 days after the KEV addition, with rare extensions for harder cases. This is a hard deadline for FCEB agencies and a strong signal for everyone else.</li>
</ol>

<p>KEV has grown from roughly 300 entries at launch in late 2021 to around 1,500 entries by mid-2025. That growth is partially a backlog catch-up, not a real-time exploitation count. CISA’s <a href="https://www.cisa.gov/news-events/directives/bod-22-01-reducing-significant-risk-known-exploited-vulnerabilities">Binding Operational Directive 22-01</a> pulled in pre-existing exploited issues during the catalog’s first 18 months.</p>

<p>For our cadence question, the useful KEV signal is this. <strong>Once a CVE is on KEV, defenders behave as if a patch is available within two weeks.</strong> That is the pressure-cooked, accelerated branch of the cadence story. It applies to about one percent of all CVEs.</p>

<h2 id="what-i-see-in-my-own-follow-up-disclosures">What I see in my own follow-up disclosures</h2>

<p>The 2020 Quest UC report was not a one-off. I have continued to file disclosures since then. The cadence I see is heavily shaped by which class of vendor the issue lands with, and to a lesser extent by whether the vendor runs a structured bug-bounty program.</p>

<p>A representative sample of patterns I have observed across follow-ups since the Quest writeup, lightly anonymized where the disclosures are not yet public:</p>

<table>
  <thead>
    <tr>
      <th>Vendor class</th>
      <th>Acknowledgment</th>
      <th>Fix shipped</th>
      <th>Coordination</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Fortune-500 software vendor with formal PSIRT</td>
      <td>Within 48 hours, every time</td>
      <td>30 to 90 days for non-critical findings; faster for critical</td>
      <td>Professional, predictable</td>
    </tr>
    <tr>
      <td>Mid-market enterprise vendor running HackerOne or Bugcrowd</td>
      <td>Within 72 hours, usually</td>
      <td>60 to 180 days, depending on roadmap pressure</td>
      <td>Triage is sharp; fix scheduling is opaque</td>
    </tr>
    <tr>
      <td>Mid-market enterprise vendor without a bug-bounty program</td>
      <td>1 to 4 weeks, often after a follow-up email</td>
      <td>Variable. Some patch in weeks. Some go silent and ship a fix without notifying me.</td>
      <td>Frustrating, similar to 2020</td>
    </tr>
    <tr>
      <td>Small or end-of-life product</td>
      <td>“We will look into it” or no response</td>
      <td>“End of life, no patch” or no response</td>
      <td>Identical to 2020</td>
    </tr>
    <tr>
      <td>OSS project with active maintainers</td>
      <td>Within hours when the report is well-formed</td>
      <td>Days to a few weeks</td>
      <td>Faster than commercial vendors</td>
    </tr>
  </tbody>
</table>

<p>The absolute numbers are not the interesting story. The shape is. The Fortune-500 PSIRT process and the OSS-with-active-maintainers process are noticeably faster and more predictable than they were in 2020. The middle band is the same. The long tail is the same or worse.</p>

<p>The single most consistent accelerator I have seen is presence on KEV. On the rare occasion a finding I disclosed went on to be confirmed as exploited and added to KEV, the vendor went from “we will look at this in next quarter’s planning” to “we are shipping the fix this week” inside a few days. That is not a coincidence. KEV listing converts a vendor-internal scheduling problem into a customer-relations problem.</p>

<p>The second most consistent accelerator is a structured bug-bounty program. Not the bounty payout itself, the program’s published triage SLAs and the visible-from-outside report queue. A vendor whose security team has an externally-readable backlog has a different incentive structure to a vendor whose backlog lives inside JIRA.</p>

<h2 id="what-has-not-changed">What has not changed</h2>

<p>A few things look identical to 2020.</p>

<ol>
  <li><strong>The middle band.</strong> A mid-tier enterprise vendor selling specialised software with no bug-bounty program responds at a similar speed to its 2020 self. Acknowledgment in 1 to 4 weeks. Fix in 1 to 4 months. Maybe a CVE assignment, maybe not. Maybe a public advisory, maybe a silent ship.</li>
  <li><strong>The long tail.</strong> Small vendors and end-of-life products produce the same outcomes as 2020. The Quest UC report received “no patch, end of life” as a final answer. That answer is still routine for products in the same category.</li>
  <li><strong>The disclosure window.</strong> 90 days remains the default coordinated-disclosure norm. Project Zero stuck with it. CERT/CC sticks with it. Most independent researchers stick with it.</li>
  <li><strong>The vendor-class divergence.</strong> A top-tier vendor was already faster than a long-tail vendor in 2020. The gap between them has widened, not narrowed.</li>
</ol>

<h2 id="what-the-divergence-implies">What the divergence implies</h2>

<p>Three implications, which are the practical lift-out from all of the above.</p>

<p><strong>For researchers.</strong> Your leverage matters. A bug-bounty program raises the floor on triage speed and lowers the variance on outcomes. KEV listing, when it happens, raises the ceiling on patch speed by an order of magnitude. Cold-emailing a mid-tier vendor without either of those signals in 2026 looks essentially like cold-emailing the same vendor in 2020. Plan accordingly.</p>

<p><strong>For defenders.</strong> Do not assume an industry-wide patching cadence. Plan for the mid-tier case unless you have specific evidence that a CVE is on KEV or that the vendor’s PSIRT has shipped recently. A patched build existing in the vendor’s release engineering queue is not the same as a patched build on your customer fleet.</p>

<p><strong>For policy.</strong> The KEV catalog has worked well at compressing the patching cycle for the narrow set of actively-exploited vulnerabilities. It has not changed the cadence for the much larger set of routine findings. The CRA, when it begins to bite for products sold into the EU, is the next plausible accelerator for the middle band. We will see how that goes.</p>

<p>The 2020 Quest disclosure took seven days from first email to public posting. The product was unpatched and stayed unpatched. If I disclosed an equivalent finding against an equivalent product in 2026, the result would be effectively the same. The infrastructure around it would look more modern. The patch outcome would not.</p>

<h2 id="references">References</h2>

<ul>
  <li><a href="/blog/multiple-vulnerabilities-in-quest-policy-authority-for-unified-communications/">Original 2020 Quest UC advisory</a></li>
  <li><a href="/blog/five-years-on-revisiting-quest-uc-vulnerabilities/">Five-year retrospective on the Quest UC disclosure (April 2026)</a></li>
  <li><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA Known Exploited Vulnerabilities catalog</a></li>
  <li><a href="https://www.cisa.gov/news-events/directives/bod-22-01-reducing-significant-risk-known-exploited-vulnerabilities">CISA Binding Operational Directive 22-01</a></li>
  <li><a href="https://msrc.microsoft.com/update-guide">Microsoft Security Update Guide</a></li>
  <li><a href="https://support.apple.com/en-us/100100">Apple security releases</a></li>
  <li><a href="https://googleprojectzero.blogspot.com/p/vulnerability-disclosure-policy.html">Project Zero disclosure policy</a></li>
  <li><a href="https://hackerone.com/">HackerOne program directory</a></li>
  <li><a href="https://www.bugcrowd.com/">Bugcrowd program directory</a></li>
  <li><a href="https://www.verizon.com/business/resources/reports/dbir/">Verizon DBIR</a></li>
  <li><a href="https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act">EU Cyber Resilience Act</a></li>
</ul>]]></content><author><name>Tony West</name></author><category term="commentary" /><category term="cve" /><category term="disclosure" /><category term="kev" /><category term="cadence" /><category term="sla" /><category term="patch-tuesday" /><category term="bug-bounty" /><summary type="html"><![CDATA[A narrower follow-up to the April retrospective: looking specifically at how long vendors actually take to ship a fix in 2026, what they publish about the process, and how that compares to the 2020 baseline.]]></summary></entry><entry><title type="html">Five years on: revisiting the Quest UC vulnerabilities and what changed in vendor patching since 2020</title><link href="https://un4gi.io/blog/five-years-on-revisiting-quest-uc-vulnerabilities/" rel="alternate" type="text/html" title="Five years on: revisiting the Quest UC vulnerabilities and what changed in vendor patching since 2020" /><published>2026-04-27T00:00:00+00:00</published><updated>2026-04-27T00:00:00+00:00</updated><id>https://un4gi.io/blog/five-years-on-revisiting-quest-uc-vulnerabilities</id><content type="html" xml:base="https://un4gi.io/blog/five-years-on-revisiting-quest-uc-vulnerabilities/"><![CDATA[<h2 id="summary">Summary</h2>

<p>In December 2020 I published an <a href="/blog/multiple-vulnerabilities-in-quest-policy-authority-for-unified-communications/">advisory covering thirteen vulnerabilities in Quest Policy Authority for Unified Communications</a>. The product had been end-of-life for over seven years at the time of the report. Quest acknowledged the findings and confirmed no patches would be issued. The disclosure went public the same day Quest closed the case.</p>

<p>This post is a five-year retrospective. I am not revisiting the technical findings. The vulnerabilities are still listed in NVD, the product is still unsupported, and the recommendations from the original advisory have not changed. What has changed is the broader ecosystem around how a researcher discloses, how a vendor responds, and how a downstream operator becomes aware of a vulnerability they need to patch. In late 2020 most of the institutional plumbing that exists today did not. This post documents what is different now, and asks the simple question: if I disclosed those same findings in 2026, would the outcome be any better?</p>

<h2 id="quick-recap-of-the-2020-disclosure">Quick recap of the 2020 disclosure</h2>

<table>
  <thead>
    <tr>
      <th> </th>
      <th> </th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Product</td>
      <td>Quest Policy Authority for Unified Communications</td>
    </tr>
    <tr>
      <td>Affected version</td>
      <td>8.1.2.200</td>
    </tr>
    <tr>
      <td>Vulnerabilities reported</td>
      <td>13 (CVE-2020-35203 through CVE-2020-35206, CVE-2020-35719 through CVE-2020-35727)</td>
    </tr>
    <tr>
      <td>Vendor response time</td>
      <td>Within 24 hours on first contact</td>
    </tr>
    <tr>
      <td>Patch status</td>
      <td>None. End-of-life product.</td>
    </tr>
    <tr>
      <td>Public disclosure</td>
      <td>Same day as vendor closure of the case</td>
    </tr>
  </tbody>
</table>

<p>The disclosure timeline ran from December 9 to December 16, 2020. Seven days from first email to public posting. Quest’s communication was professional throughout. The reason no patch arrived was not vendor neglect, it was vendor honesty about the product lifecycle.</p>

<h2 id="what-has-actually-changed-since-2020">What has actually changed since 2020</h2>

<p>Five things, in roughly the order they happened:</p>

<table>
  <thead>
    <tr>
      <th>Date</th>
      <th>Change</th>
      <th>Effect on the disclosure pipeline</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>2021-11</td>
      <td>CISA published <a href="https://www.cisa.gov/news-events/directives/bod-22-01-reducing-significant-risk-known-exploited-vulnerabilities">Binding Operational Directive 22-01</a> and the <a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">Known Exploited Vulnerabilities catalog</a></td>
      <td>Federal agencies must remediate listed CVEs by hard deadlines. The catalog is now widely cited as a downstream prioritisation source by private organisations as well. As of 2025 it covers roughly 1,500 entries.</td>
    </tr>
    <tr>
      <td>2023-11-01</td>
      <td>FIRST.org published <a href="https://www.first.org/newsroom/releases/20231101">CVSS v4.0</a></td>
      <td>New base, threat, environmental, and supplemental metric groups. Replaces CVSS v3.1 as the recommended scoring framework. Adoption in NVD records is still partial as of 2026.</td>
    </tr>
    <tr>
      <td>2024-02-12</td>
      <td>NVD began <a href="https://www.nist.gov/itl/nvd/nvd-news">slowing CVE enrichment</a> (CPE matching, CVSS scoring, references). Backlog grew past 18,000 CVEs in 2024.</td>
      <td>Researchers and downstream consumers can no longer assume that an NVD record will arrive enriched within days. In April 2026 NIST formally <a href="https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth">changed policy</a> to enrich only the subset of CVEs meeting risk criteria, leaving the rest as raw records.</td>
    </tr>
    <tr>
      <td>2024-12-10</td>
      <td>The <a href="https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act">EU Cyber Resilience Act</a> entered into force</td>
      <td>Manufacturers of products with digital elements sold in the EU must support security updates for a defined period and handle vulnerabilities throughout the product lifecycle. Reporting obligations apply from 2026-09-11. Full obligations from 2027-12-11.</td>
    </tr>
    <tr>
      <td>2025-04-16</td>
      <td>The <a href="https://krebsonsecurity.com/2025/04/funding-expires-for-key-cyber-vulnerability-database/">MITRE CVE program funding lapsed</a> and was rescued at the deadline by an 11-month CISA contract extension. The <a href="https://www.cvefoundation.org/">CVE Foundation</a> was launched in response.</td>
      <td>The institutional foundation that researchers depend on to assign CVE IDs is now visibly contingent. The Foundation exists to provide a non-governmental backstop.</td>
    </tr>
  </tbody>
</table>

<p>These are the structural changes. There are also softer shifts in researcher practice that are harder to date precisely. More researchers now post first to a personal blog or GitHub gist and assign a CVE later. More vendors run formal bug bounty programs. More downstream consumers track <a href="https://github.com/CVEProject/cvelistV5">vulnerability databases</a> directly rather than waiting for NVD enrichment.</p>

<h2 id="would-the-quest-disclosure-play-out-the-same-way-today">Would the Quest disclosure play out the same way today?</h2>

<p>Probably yes for the technical content, but the surrounding context would be different in three concrete ways.</p>

<h3 id="1-nvd-enrichment-is-no-longer-guaranteed">1. NVD enrichment is no longer guaranteed</h3>

<p>In 2020 I submitted to Quest’s security inbox. MITRE assigned CVE identifiers within days. NVD enriched the records (CPE strings, CVSS vector, references) within roughly two weeks. By 2024 that pipeline had broken in a way that has not fully recovered. If I disclosed thirteen CVEs against an end-of-life Quest product in 2026, only the subset NIST classified as high-risk would receive prompt enrichment. The rest would sit as raw CVE records. For an attacker, that is meaningful: less context for downstream tooling, less visibility for defenders relying on automated CPE matching.</p>

<p>The ironic effect is that the Quest 2020 disclosure was easier to document and act on than an equivalent 2026 disclosure would be. The bottleneck has moved from vendor patch availability to public-record quality.</p>

<h3 id="2-the-cra-changes-the-eol-conversation-in-europe">2. The CRA changes the EOL conversation in Europe</h3>

<p>The Quest response in 2020 was that the product had been unsupported for over seven years and no patch would be issued. That is a defensible position for a US-headquartered vendor selling a product that has reached genuine end-of-life and was never relabelled as supported.</p>

<p>Under the EU Cyber Resilience Act, this position becomes harder to maintain for products sold into the EU after the regulation applies. Manufacturers must define a support period, handle vulnerabilities during it, and document the security update lifecycle. The Quest UC product would not be subject to the CRA retroactively, and Quest may not have continued to sell into the EU at the time of disclosure. But for a comparable product on sale in 2026 and beyond, the answer “no patch, end of life” is a regulatory question, not just a customer-relations question.</p>

<p>In practice this means EU researchers reporting issues against EU-sold products now have a clearer escalation path when a vendor declines to patch. ENISA reporting obligations from September 2026 onwards will further shift the disclosure paperwork.</p>

<h3 id="3-kev-listing-is-the-new-severity-signal">3. KEV listing is the new severity signal</h3>

<p>In 2020 a researcher disclosed, NVD enriched, and downstream operators decided what to patch based on CVSS and their own risk assessment. By 2026 the practical question for many defenders is not “what is this CVE’s CVSS score” but “is it on KEV”. That is a narrower question and a more actionable one.</p>

<p>The Quest UC vulnerabilities would not have made KEV. There is no public evidence of exploitation, the affected product population is small, and the product is end-of-life. CISA’s listing criteria reasonably exclude this category. The same logic applies to most XSS, CSRF, and SSRF findings in niche enterprise software. KEV is a useful signal precisely because it is narrow. The downside is that the CVE-but-not-KEV middle band has become harder for defenders to triage.</p>

<p>The 2020 advisory recommended decommissioning the product. That recommendation still holds, and the absence from KEV does not change it.</p>

<h2 id="the-2026-disclosure-workflow-briefly">The 2026 disclosure workflow, briefly</h2>

<p>For comparison with the 2020 timeline, here is what the same disclosure flow looks like now:</p>

<table>
  <thead>
    <tr>
      <th>Step</th>
      <th>2020</th>
      <th>2026</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Initial vendor contact</td>
      <td>security@vendor.com</td>
      <td>Same. Some vendors now run a HackerOne or Bugcrowd program instead.</td>
    </tr>
    <tr>
      <td>Triage</td>
      <td>Vendor confirms within days</td>
      <td>Variable. Established vendors meet the same SLA. Smaller vendors have moved to “we will respond when we can”.</td>
    </tr>
    <tr>
      <td>CVE assignment</td>
      <td>Vendor or MITRE</td>
      <td>Vendor CNA where one exists, MITRE root otherwise. The CVE Foundation now provides a contingent backstop.</td>
    </tr>
    <tr>
      <td>NVD enrichment</td>
      <td>Within ~2 weeks</td>
      <td>Variable. Top-tier risk: enriched. Everything else: raw record only.</td>
    </tr>
    <tr>
      <td>KEV consideration</td>
      <td>N/A</td>
      <td>Only if active exploitation evidence exists</td>
    </tr>
    <tr>
      <td>Public disclosure</td>
      <td>After patch or after coordinated period</td>
      <td>Same. 90 days remains the de facto norm.</td>
    </tr>
    <tr>
      <td>Defender pickup</td>
      <td>NVD feed or vendor advisory</td>
      <td>NVD feed (with caveats above), KEV catalog, vendor advisory, vulnerability-management vendors that re-enrich raw CVEs</td>
    </tr>
  </tbody>
</table>

<p>The end-to-end time from researcher-finds-bug to defender-aware-of-bug is not dramatically different. What is different is how much context arrives with the CVE record, how confidently a defender can rank it against thousands of others, and how much regulatory machinery now applies to the vendor.</p>

<h2 id="closing-thoughts">Closing thoughts</h2>

<p>The Quest UC disclosure was a small piece of routine vulnerability research. Five years later the same routine still works, but the surrounding ecosystem has had a turbulent half-decade. The CVE program nearly went dark for a week in April 2025. NVD has effectively conceded that it cannot enrich every record. The EU has decided that “end-of-life, no patch” is no longer a complete answer for products sold into its market. CISA has pulled the most operationally important subset of CVEs into a separate catalog with hard deadlines.</p>

<p>For researchers, the practical guidance is unchanged. Document carefully. Coordinate with the vendor. Cite primary sources. Publish when the responsible-disclosure window closes. The infrastructure around these steps is less stable than it was in 2020. Plan accordingly.</p>

<p>For organisations still running Quest Policy Authority for Unified Communications in 2026, the original recommendation stands. The product is unpatched and unsupported. Decommission it.</p>

<h2 id="references">References</h2>

<ul>
  <li>Original 2020 advisory: <a href="/blog/multiple-vulnerabilities-in-quest-policy-authority-for-unified-communications/">Multiple Vulnerabilities in Quest Policy Authority for Unified Communications</a></li>
  <li><a href="https://www.cisa.gov/known-exploited-vulnerabilities-catalog">CISA Known Exploited Vulnerabilities Catalog</a></li>
  <li><a href="https://www.cisa.gov/news-events/directives/bod-22-01-reducing-significant-risk-known-exploited-vulnerabilities">CISA Binding Operational Directive 22-01</a></li>
  <li><a href="https://www.first.org/cvss/specification-document">FIRST CVSS v4.0 specification</a></li>
  <li><a href="https://www.nist.gov/itl/nvd/nvd-news">NIST NVD news</a></li>
  <li><a href="https://www.nist.gov/news-events/news/2026/04/nist-updates-nvd-operations-address-record-cve-growth">NIST policy update on NVD enrichment, April 2026</a></li>
  <li><a href="https://digital-strategy.ec.europa.eu/en/policies/cyber-resilience-act">EU Cyber Resilience Act</a></li>
  <li><a href="https://www.cvefoundation.org/">The CVE Foundation</a></li>
</ul>]]></content><author><name>Tony West</name></author><category term="commentary" /><category term="cve" /><category term="disclosure" /><category term="eol-software" /><category term="kev" /><category term="cvss" /><category term="cra" /><summary type="html"><![CDATA[A retrospective on the December 2020 Quest Policy Authority for UC advisory, with a sober look at what has actually changed in the vendor-patching ecosystem since.]]></summary></entry><entry><title type="html">mBot v2.0.0: an update to my mission bot for Synack Red Team members</title><link href="https://un4gi.io/blog/mbot-v2-an-update-to-my-mission-bot-for-synack-red-team-members/" rel="alternate" type="text/html" title="mBot v2.0.0: an update to my mission bot for Synack Red Team members" /><published>2022-02-05T00:00:00+00:00</published><updated>2022-02-05T00:00:00+00:00</updated><id>https://un4gi.io/blog/mbot-v2-an-update-to-my-mission-bot-for-synack-red-team-members</id><content type="html" xml:base="https://un4gi.io/blog/mbot-v2-an-update-to-my-mission-bot-for-synack-red-team-members/"><![CDATA[<p>This is an update to the <a href="/blog/mbot-v2-mission-bot-for-synack-red-team/">original mBot release post</a> covering what changed between v1.0.0 (October 2021) and v2.0.0.</p>

<h2 id="background">Background</h2>

<p>In late March / early April 2021 my application to Synack was approved and I gained access to the platform. The first few days were spent fumbling through menus and figuring out how to navigate. After talking with some of the more experienced members I was told quickly that I needed a mission bot if I wanted any chance of grabbing missions. One member shared their bot with me and for several months I was able to claim multiple missions a day without issue.</p>

<p>Eventually changes to the platform started impacting how that bot worked with the mission API. My access to missions began diminishing as I was constantly disconnected or beaten to claims by an increasingly prevalent number of bots. Around this time I was learning Go, and decided to further that learning by building a custom mission bot for the Synack API.</p>

<p>I released <a href="/blog/mbot-v2-mission-bot-for-synack-red-team/">version 1.0.0</a> in October 2021 with minimal functionality: just enough to claim missions and onboard to targets. Since then I had spent significant time on improvements aimed at automating the routine parts of mission work.</p>

<p>With Synack’s release of the Mission Trust Wallet feature, I no longer have the time to spend on missions for the now reduced compensation. Rather than let the bot sit unused, I am releasing v2.0.0 to the community.</p>

<h2 id="key-features-in-v200">Key features in v2.0.0</h2>

<h3 id="2fa-login-automation">2FA login automation</h3>

<p>Frequent session timeouts on the Synack platform made automated login a priority. Two-factor authentication was the main blocker for automation. The <a href="https://github.com/alexzorin/authy">authy</a> Go library handled the 2FA flow cleanly, and the rest of the login process was straightforward to script after that.</p>

<h3 id="discord-notifications">Discord notifications</h3>

<p>The bot was claiming missions, but I was occasionally going a full day without checking whether anything had been claimed. I added notifications using the <a href="https://github.com/bwmarrin/discordgo">discordgo</a> package. The current implementation only emits notifications for claimed missions, but the same plumbing should make broader command-and-control functionality straightforward to add later.</p>

<h3 id="mission-templates">Mission templates</h3>

<p>v2.0.0 includes scaffolding for mission templates that pre-populate fields when a mission is claimed. Used properly this should reduce write-up time substantially.</p>

<h3 id="target-blacklisting">Target blacklisting</h3>

<p>After claiming multiple missions for a target that turned out to be non-functional more than once (those who know, know), I implemented a blacklist. Any target name on the blacklist is skipped during the claim flow.</p>

<h3 id="future-work">Future work</h3>

<p>The bot was still a work in progress when I shelved active development. There are hints in the source of features that were partway in for anyone interested in continuing the work.</p>

<h2 id="getting-it">Getting it</h2>

<p>The repository is at <a href="https://github.com/un4gi/mBot">github.com/un4gi/mBot</a>. Read through the code and configure the default settings before running. mBot is intended to be invoked with arguments as documented in the README. The default check-in interval is set to one second deliberately, as a deterrent against running it without first reading what it does.</p>

<h2 id="a-note-on-use">A note on use</h2>

<p>This bot was built and used by me on the Synack platform. Synack’s terms of service and acceptable use policies have changed over time. If you fork or run this code, the responsibility for compliance with whatever the current rules are at the time you run it is yours, not mine. The original code is provided as-is for reference and as a starting point for further work.</p>]]></content><author><name>Tony West</name></author><category term="research" /><category term="tools" /><category term="golang" /><category term="synack" /><category term="automation" /><summary type="html"><![CDATA[Notes on the v2.0.0 release of mBot - login automation, Discord notifications, mission templates, and target blacklisting.]]></summary></entry><entry><title type="html">Zero Trust Network Access Providers: What to Look For</title><link href="https://un4gi.io/blog/zero-trust-network-access-providers-comparison/" rel="alternate" type="text/html" title="Zero Trust Network Access Providers: What to Look For" /><published>2021-09-20T00:00:00+00:00</published><updated>2021-09-20T00:00:00+00:00</updated><id>https://un4gi.io/blog/zero-trust-network-access-providers-comparison</id><content type="html" xml:base="https://un4gi.io/blog/zero-trust-network-access-providers-comparison/"><![CDATA[<h2 id="vpn-is-dead-mostly">VPN is Dead (Mostly)</h2>

<p>Traditional VPNs give users full network access once connected. Authenticate once, get a tunnel, access everything. This is fundamentally incompatible with zero trust principles, and it is why organizations are moving to Zero Trust Network Access (ZTNA).</p>

<p>ZTNA flips the model: verify every request, grant access only to specific applications, and never trust the network.</p>

<h2 id="how-ztna-works">How ZTNA Works</h2>

<p>The core concept is simple:</p>

<ol>
  <li>User authenticates through an identity provider</li>
  <li>Device posture is evaluated (OS version, patches, EDR status, disk encryption)</li>
  <li>Access is granted to specific applications based on policy - not network segments</li>
  <li>The application is never exposed to the internet directly</li>
  <li>Every session is continuously evaluated - not just at connection time</li>
</ol>

<p>The user never gets “network access.” They get application access through an encrypted tunnel to a broker that validates every request.</p>

<h2 id="evaluation-criteria">Evaluation Criteria</h2>

<p>When comparing zero trust network access providers, focus on these areas:</p>

<p><strong>Identity integration</strong> - Does it work with your existing IdP (Azure AD, Okta, Ping)? Does it support SAML 2.0 and OIDC? Can it enforce MFA natively or through your IdP?</p>

<p><strong>Device posture assessment</strong> - Can it check OS version, patch level, antivirus status, disk encryption, and firewall state? Can you create custom posture checks? How frequently does it re-evaluate?</p>

<p><strong>Application support</strong> - Does it support web apps, thick client apps, SSH, RDP, and custom TCP/UDP protocols? Some ZTNA solutions only work well with HTTP-based applications.</p>

<p><strong>Deployment model</strong> - Agent-based (installed on endpoints) vs. agentless (browser-based). Agent-based provides more control and supports non-web protocols. Agentless is easier to deploy for contractors and BYOD.</p>

<p><strong>Performance</strong> - Where are the provider’s points of presence? Latency matters. A ZTNA solution that routes all traffic through a single data center across the continent will frustrate users.</p>

<p><strong>Logging and visibility</strong> - Detailed session logs including user, device, application, duration, and bytes transferred. Integration with SIEM platforms.</p>

<h2 id="the-market">The Market</h2>

<p>The major categories of ZTNA providers:</p>

<p><strong>Pure-play ZTNA:</strong></p>
<ul>
  <li>Zscaler Private Access (ZPA) - One of the earliest and most mature. Strong integration with their internet access product.</li>
  <li>Cloudflare Access - Developer-friendly, competitive pricing, strong global network.</li>
  <li>Appgate SDP - Software-defined perimeter approach with strong policy engine.</li>
</ul>

<p><strong>SASE-integrated ZTNA:</strong></p>
<ul>
  <li>Palo Alto Prisma Access - ZTNA as part of a broader SASE platform. Good for existing Palo Alto customers.</li>
  <li>Netskope Private Access - Strong data protection integration.</li>
  <li>Cato Networks - Full SASE stack with ZTNA built in.</li>
</ul>

<p><strong>Identity-vendor ZTNA:</strong></p>
<ul>
  <li>Microsoft Entra Private Access - Tight Azure AD integration. Natural choice for Microsoft shops.</li>
  <li>Cisco Duo / Secure Access - Leverages Duo’s MFA and device trust capabilities.</li>
</ul>

<p><strong>Open source:</strong></p>
<ul>
  <li>OpenZiti - Open source zero trust networking. More effort to deploy but no per-user licensing costs.</li>
</ul>

<h2 id="what-ztna-does-not-solve">What ZTNA Does Not Solve</h2>

<p>ZTNA is not a silver bullet. It secures remote access to applications, but it does not replace:</p>
<ul>
  <li>Network segmentation within the data center</li>
  <li>Endpoint detection and response</li>
  <li>Email security</li>
  <li>Vulnerability management</li>
</ul>

<p>Think of ZTNA as replacing your VPN concentrator, not your entire security stack. It is one component of a zero trust architecture, not the whole thing.</p>

<h2 id="practical-advice">Practical Advice</h2>

<p>Start with your highest-risk remote access use cases - typically admin access to production systems and third-party/contractor access. These are the scenarios where traditional VPN is most dangerous and where ZTNA provides the most immediate value.</p>]]></content><author><name>Tony West</name></author><category term="research" /><category term="zero-trust" /><category term="ztna" /><category term="network-security" /><category term="sase" /><summary type="html"><![CDATA[An overview of zero trust network access providers, evaluation criteria, and how ZTNA differs from traditional VPN-based remote access.]]></summary></entry><entry><title type="html">Hybrid Cloud Security Architecture: Design Patterns and Pitfalls</title><link href="https://un4gi.io/blog/hybrid-cloud-security-architecture-design-patterns/" rel="alternate" type="text/html" title="Hybrid Cloud Security Architecture: Design Patterns and Pitfalls" /><published>2021-09-01T00:00:00+00:00</published><updated>2021-09-01T00:00:00+00:00</updated><id>https://un4gi.io/blog/hybrid-cloud-security-architecture-design-patterns</id><content type="html" xml:base="https://un4gi.io/blog/hybrid-cloud-security-architecture-design-patterns/"><![CDATA[<h2 id="why-hybrid-cloud-security-is-different">Why Hybrid Cloud Security is Different</h2>

<p>Most organizations are not fully in the cloud. They run hybrid environments with workloads split between on-premises data centers and one or more cloud providers. This creates a security architecture challenge that neither traditional network security nor cloud-native security approaches handle well on their own.</p>

<p>The attack surface spans both environments, and the gaps between them are where things go wrong.</p>

<h2 id="identity-the-new-perimeter">Identity: The New Perimeter</h2>

<p>In a hybrid environment, identity is your primary security control. Network perimeters are meaningless when your users access both on-prem Active Directory resources and cloud services.</p>

<p><strong>Federation is mandatory.</strong> Use SAML 2.0 or OIDC to federate your on-prem identity provider with cloud IAM. Azure AD Connect for Microsoft environments, or a third-party IdP like Okta for multi-cloud.</p>

<p>Key principles:</p>
<ul>
  <li>Single source of truth for identities (usually on-prem AD)</li>
  <li>Conditional access policies based on device state, location, and risk score</li>
  <li>Privileged access management (PAM) that spans both environments</li>
  <li>No shared service accounts between on-prem and cloud</li>
</ul>

<h2 id="network-architecture">Network Architecture</h2>

<p>The connectivity between on-prem and cloud is your highest-risk attack path.</p>

<p><strong>VPN or Direct Connect/ExpressRoute</strong> for connectivity - never route hybrid traffic over the public internet. Use dedicated connections where possible.</p>

<p><strong>Segmentation model:</strong></p>
<ul>
  <li>Dedicated VPCs/VNets for different workload tiers</li>
  <li>Transit VPC pattern for centralized inspection</li>
  <li>East-west traffic inspection between segments</li>
  <li>DNS resolution that works bidirectionally without exposing internal zones publicly</li>
</ul>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>On-Prem DC          Transit VPC/VNet         Workload VPCs
+-----------+       +--------------+         +------------+
| Internal  |&lt;-----&gt;| Firewall/IDS |&lt;-------&gt;| Production |
| Network   | VPN/  | Route tables |         +------------+
|           | DC    | NAT gateways |         | Staging    |
+-----------+       +--------------+         +------------+
                          |                  | Dev        |
                    +-----+-----+            +------------+
                    | Shared Svc |
                    | (DNS, AD)  |
                    +-----------+
</code></pre></div></div>

<h2 id="encryption-strategy">Encryption Strategy</h2>

<p>Data in transit: TLS everywhere, including between internal services. Mutual TLS (mTLS) for service-to-service communication in high-security environments.</p>

<p>Data at rest: Use cloud-native encryption (AWS KMS, Azure Key Vault) for cloud workloads. The critical decision is key management - who controls the keys?</p>

<ul>
  <li><strong>Cloud-managed keys</strong> - Simplest. The cloud provider manages everything. Acceptable for most workloads.</li>
  <li><strong>Customer-managed keys (CMK)</strong> - You control key rotation and access policies. Required for regulated workloads.</li>
  <li><strong>Bring your own key (BYOK)</strong> - Keys generated on-prem, imported to cloud HSM. Maximum control but operational complexity.</li>
  <li><strong>Hold your own key (HYOK)</strong> - Keys never leave on-prem HSM. Cloud provider must request decryption from your infrastructure. Highest security, most complex.</li>
</ul>

<h2 id="common-pitfalls">Common Pitfalls</h2>

<ol>
  <li><strong>Inconsistent security policies</strong> between on-prem and cloud. Your cloud security groups should enforce the same segmentation rules as your on-prem firewalls.</li>
  <li><strong>Over-permissive IAM roles</strong> in cloud that would never be tolerated on-prem. “AdministratorAccess” attached to a service role is the cloud equivalent of running everything as root.</li>
  <li><strong>Logging gaps</strong> where on-prem SIEM does not ingest cloud logs, or cloud-native logging does not capture on-prem events. You need unified visibility.</li>
  <li><strong>Snapshot and backup exposure</strong> - Cloud snapshots and backups that are not encrypted or are shared across accounts.</li>
  <li><strong>Neglecting the management plane</strong> - Cloud control plane access (console, CLI, API) is a separate attack surface that does not exist on-prem.</li>
</ol>

<h2 id="monitoring-across-environments">Monitoring Across Environments</h2>

<p>Centralize your security monitoring. Ship cloud logs (CloudTrail, Azure Activity Log, VPC Flow Logs) to your on-prem SIEM, or use a cloud-native SIEM that can also ingest on-prem data. The worst outcome is two separate security teams looking at two separate dashboards with no correlation between them.</p>

<p>Build detection rules that span both environments. An attacker who compromises an on-prem system will pivot to the cloud, and vice versa.</p>]]></content><author><name>Tony West</name></author><category term="research" /><category term="cloud-security" /><category term="architecture" /><category term="aws" /><category term="azure" /><category term="hybrid-cloud" /><summary type="html"><![CDATA[Security architecture patterns for hybrid cloud environments covering network segmentation, identity federation, encryption, and common misconfigurations.]]></summary></entry></feed>