<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" xml:lang="en-US"><generator uri="https://jekyllrb.com/" version="4.2.1">Jekyll</generator><link href="https://andreafortuna.org/feed.xml" rel="self" type="application/atom+xml" /><link href="https://andreafortuna.org/" rel="alternate" type="text/html" hreflang="en-US" /><updated>2026-07-11T15:06:38+00:00</updated><id>https://andreafortuna.org/feed.xml</id><title type="html">Andrea Fortuna</title><subtitle>Cybersecurity expert, software developer, experienced digital forensic analyst, musician</subtitle><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><entry><title type="html">Chat control survives, encrypted messaging doesn’t count</title><link href="https://andreafortuna.org/2026/07/10/chatcontrol-survives/" rel="alternate" type="text/html" title="Chat control survives, encrypted messaging doesn’t count" /><published>2026-07-10T00:00:00+00:00</published><updated>2026-07-10T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/07/10/chatcontrol-survives</id><content type="html" xml:base="https://andreafortuna.org/2026/07/10/chatcontrol-survives/"><![CDATA[<p>On July 9, 2026, 314 members of the European Parliament voted to reject the revival of Chat Control. Only 276 voted to keep it. By any ordinary reading of a vote, that is a defeat for the proposal. It passed anyway. The mechanism behind that outcome tells you more about how EU surveillance legislation actually survives than any of the substantive arguments about child safety or encryption ever will, and it is worth understanding in detail before the next round starts in September.</p>

<p><img src="/assets/2026/chatcontrol-2026-survives.jpg" alt="cover" /></p>

<p>I have tracked this proposal through two previous cycles, its <a href="https://andreafortuna.org/2025/11/01/chat-control-proposal-fails-again-after-massive-public-opposition/">withdrawal after public opposition in late 2025</a> and the <a href="https://andreafortuna.org/2025/12/29/chat-control-reopens-a-privacy-fault-line/">shift toward normalized scanning I flagged in December</a>. The pattern I described then, temporary derogations quietly becoming permanent baseline, has now played out almost exactly as predicted, just with an unexpected twist on encrypted messaging.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>Chat Control 1.0, the voluntary CSAM-scanning derogation from the ePrivacy Directive, expired on April 3, 2026 after Parliament rejected its extension in March by 311 votes to 228.</li>
  <li>The Council adopted a first-reading position on July 2 reinstating the derogation unchanged through April 2028.</li>
  <li>Parliament voted on July 9 under emergency procedure; a rejection motion got more no votes (314) than yes votes (276) but missed the 361-vote absolute majority required in second reading, so the extension stands.</li>
  <li>An amendment adopted the same day excludes communications protected by end-to-end encryption from the scope of voluntary scanning, meaning WhatsApp, Signal, and Telegram are formally out of Chat Control 1.0’s reach.</li>
  <li>This exclusion applies only to the voluntary derogation track. The permanent CSA Regulation (Chat Control 2.0), still in trilogue, remains the venue where client-side scanning obligations could still be imposed on encrypted services.</li>
  <li>The Council now has three months to accept Parliament’s amendments; disagreement triggers a conciliation committee, while CSAR negotiations resume in September.</li>
</ul>

<h2 id="the-procedural-mechanic-nobody-explains-clearly">The procedural mechanic nobody explains clearly</h2>

<p>Second-reading procedure under the ordinary legislative procedure (Article 294 TFEU) is where this vote’s outcome actually lives. When the Council adopts a position at first reading, Parliament can approve it, amend it, or reject it outright. Rejecting a Council position outright at second reading requires an <strong>absolute majority of Parliament’s component members</strong>, currently 361 votes out of 720, not a simple majority of votes cast in the chamber that day.</p>

<p><img src="/assets/2026/Chat_Control_2026_Voting_Paradox.jpg" alt="voting" /></p>

<p>This is structurally different from a first-reading vote, where a simple majority of votes cast decides the outcome. It means abstentions and absences function, in practice, as votes in favor of whatever the Council already adopted. The rejection motion on July 9 secured 314 votes, comfortably more than the 276 in favor, and still failed by 47 votes against the threshold. <a href="https://www.theregister.com/security/2026/07/09/meps-fail-to-prevent-chat-control-snoopfest-revival/5269379">MEPs fail to prevent Chat Control snoopfest revival</a> put it plainly:</p>

<blockquote>
  <p>“Chat Control expired on April 3, 2026, after first being introduced in August 2021. Today, however, MEPs did not reach the required threshold to reject it.”</p>
</blockquote>

<p>Anyone assessing the political durability of Chat Control needs to internalize this asymmetry. A proposal that cannot secure a straightforward parliamentary majority in its favor can still survive indefinitely as long as opposition cannot clear the higher bar required to kill a Council position outright. This is precisely the “zombie proposal” dynamic that outlets covering the file since 2022 have repeatedly noted, and the July 9 vote is the clearest procedural demonstration of it to date.</p>

<h2 id="the-end-to-end-encryption-carve-out-and-its-limitations">The end-to-end encryption carve-out and its limitations</h2>

<p>The same session that failed to reject the Council’s text adopted an amendment explicitly excluding end-to-end encrypted communications from the scope of the voluntary scanning regime. As <a href="https://www.euronews.com/my-europe/2026/07/09/european-parliament-aims-to-exclude-end-to-end-chats-from-message-scanning-regime">Euronews reported</a>:</p>

<blockquote>
  <p>“A law allowing scanning of online communications to detect child sexual abuse material was amended by MEPs to protect users’ privacy.”</p>
</blockquote>

<p>Practically, this means providers operating under E2E protocols, WhatsApp’s Signal Protocol implementation, Signal itself, Telegram’s Secret Chats, are outside the scope of the derogation Parliament just extended. That is a genuine, specific boundary, and it directly addresses the architectural concern I raised in December about client-side scanning turning “the user’s phone into a checkpoint.” If a service cannot access plaintext content to begin with, the voluntary scanning regime cannot compel it to scan.</p>

<p>But the boundary is narrower than the headlines suggest, for two reasons that matter to anyone doing compliance or threat-modeling work around this file:</p>

<ul>
  <li><strong>The exclusion applies to Chat Control 1.0 only.</strong> It is an amendment to the ePrivacy derogation, a voluntary, time-limited legal basis for providers who already scan certain content categories (typically metadata and non-E2E services like some cloud storage and unencrypted chat platforms). It does not touch the CSA Regulation still in trilogue.</li>
  <li><strong>CSAR (Chat Control 2.0) is where detection orders live.</strong> The permanent regulation under negotiation would introduce mandatory detection orders that courts or authorities could issue against specific services, including, in earlier drafts, an obligation for E2E providers to implement client-side scanning to comply. That mechanism has not been foreclosed by this week’s vote. The trilogue resumes in September, and the encryption carve-out achieved in the derogation vote creates political pressure but no legal precedent binding the CSAR negotiators.</li>
</ul>

<p>The distinction between these two tracks is the single most consequential technical detail in the entire file, and it is the one most general-audience coverage collapses into a single “Chat Control” narrative. Anyone advising a messaging provider, a CISO at a company running an E2E product, or a policy team tracking exposure needs to treat Chat Control 1.0’s encryption exclusion as a favorable signal, not as closure.</p>

<h2 id="where-the-csar-negotiation-actually-stands">Where the CSAR negotiation actually stands</h2>

<p>The permanent regulation has followed its own separate, slower track throughout 2026. Trilogue negotiations between Parliament, Council, and Commission resumed on June 29, running in parallel with the derogation fight that dominated headlines. <a href="https://eutechloop.com/double-threat/">Double threat to privacy: Chat Control 1.0 and 2.0 are back</a> captured the structural risk of treating these as one fight:</p>

<blockquote>
  <p>“In March, the European Parliament rejected the extension of the so-called Chat Control 1.0 regulation, which expired in April 2026…”</p>
</blockquote>

<p>The risk for observers, and for organizations building compliance timelines, is conflating the derogation’s fate with CSAR’s. A win on one track changes nothing about the substantive obligations still being negotiated on the other. The current CSAR compromise text under discussion reportedly narrows mandatory detection orders to known CSAM hash-matching rather than open-ended AI classification of new content, a meaningful technical distinction from earlier 2022-2023 drafts that proposed broader machine-learning based detection. Whether that narrowing survives the next round of trilogue, expected to conclude discussions by late autumn, is the actual decision point that will determine whether client-side scanning becomes a legal requirement for E2E services operating in the EU.</p>

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

<p>The Council has three months from the July 9 vote to formally respond to Parliament’s amendments, including the encryption exclusion. Two outcomes are procedurally available:</p>

<ol>
  <li>The Council accepts Parliament’s amended text as-is, in which case the derogation, encryption exclusion included, enters into force through April 2028 without further negotiation.</li>
  <li>The Council rejects or modifies the amendments, triggering a <strong>conciliation committee</strong> under Article 294(10) TFEU, composed of equal numbers of MEPs and Council representatives, tasked with reaching a joint text within six weeks, extendable by two.</li>
</ol>

<p>Given that several member states have historically pushed for broader scanning mandates than Parliament has been willing to accept, a conciliation committee is a realistic scenario, not a formality. Meanwhile, CSAR trilogue continues independently, with the next substantive session expected in September. Any organization building a compliance roadmap around this file should treat Q4 2026 as the actual decision window for the regulation that matters most, not this month’s derogation vote, however dramatic its procedural mechanics turned out to be.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What is the difference between Chat Control 1.0 and Chat Control 2.0?</strong>
Chat Control 1.0 is the temporary ePrivacy derogation that lets providers voluntarily scan communications for CSAM, now extended to 2028, while Chat Control 2.0 refers to the permanent CSA Regulation still in trilogue, which would mandate detection orders and potentially client-side scanning as a legal obligation rather than a voluntary derogation.</p>

<p><strong>Does excluding end-to-end encrypted messages from Chat Control 1.0 mean WhatsApp and Signal are now safe?</strong>
Only from the voluntary scanning regime just extended. The permanent CSAR still under negotiation could reintroduce detection obligations covering encrypted services through client-side scanning, so the exclusion is a tactical win in one legislative track, not a resolution of the underlying architecture debate.</p>

<p><strong>Why did Chat Control pass despite more MEPs voting against it than for it?</strong>
Under the second-reading procedure, a motion to reject a Council position needs an absolute majority of all Parliament seats (361 votes), not just a simple majority of votes cast; the rejection motion got 314 votes against 276, more no votes than yes votes, but fell short of the 361 threshold, so the Council’s text stood.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="Privacy" /><category term="Legislation" /><category term="EU Policy" /><category term="Encryption" /><category term="Chat Control" /><summary type="html"><![CDATA[A technical update on the July 2026 European Parliament vote that revived Chat Control 1.0 through 2028 while carving out end-to-end encrypted messaging.]]></summary></entry><entry><title type="html">Going beneath NTFS: USN Journal, dfir_ntfs, and artefact-driven investigations</title><link href="https://andreafortuna.org/2026/07/06/ntfs-forensics-deep-dive/" rel="alternate" type="text/html" title="Going beneath NTFS: USN Journal, dfir_ntfs, and artefact-driven investigations" /><published>2026-07-06T00:00:00+00:00</published><updated>2026-07-06T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/07/06/ntfs-forensics-deep-dive</id><content type="html" xml:base="https://andreafortuna.org/2026/07/06/ntfs-forensics-deep-dive/"><![CDATA[<p>A skilled attacker who has spent any time studying forensics will know to modify file timestamps. Some will go further and delete their tools, clear event logs, and rename artefacts before exfiltrating or detonating. What most do not account for, because most training does not cover it carefully, is that NTFS keeps a layered audit trail spread across at least three separate structures, and cleaning one of them rarely touches the others.</p>

<p><img src="/assets/2026/ntfs-forensics-deep-dive.jpg" alt="cover" /></p>

<p>The <strong>Master File Table</strong>, the <strong>USN Journal</strong>, and the <strong>$LogFile</strong> each capture a different dimension of file system activity, at a different granularity and with different retention characteristics. When an analyst knows how to correlate all three, the story they tell is significantly harder to fully suppress than anything a single log or a timestamp suggests.</p>

<p>This article walks through that structure in practical terms: what each artefact contains, how to extract it, and where the intersections between them are most useful for investigations.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>NTFS stores file metadata in at least two independent locations per file: <strong>$STANDARD_INFORMATION</strong> and <strong>$FILE_NAME</strong> inside the MFT. Attackers can modify one but not both through standard APIs.</li>
  <li>The <strong>USN Journal</strong> logs every file system change event sequentially, survives file deletion, and typically retains approximately 20 days of activity on an active volume.</li>
  <li>The <strong>$LogFile</strong> is a low-level transaction log that records redo/undo operations on NTFS metadata, providing independent evidence of when attributes were last written.</li>
  <li><strong>dfir_ntfs</strong> and <strong>MFTECmd</strong> are complementary tools: one gives you programmatic access to raw NTFS structures; the other gives you analyst-ready CSV output for timeline correlation.</li>
  <li>File carving remains useful when MFT metadata is corrupted or missing, but it is best treated as a last resort.</li>
  <li>Correlating all three artefact layers produces a more robust timeline than any single source can provide.</li>
</ul>

<h2 id="the-structure-underneath-the-filesystem">The structure underneath the filesystem</h2>

<p>Before getting into tooling, it helps to have a clear model of what NTFS actually stores about a file, because the forensic value of these artefacts only becomes obvious once you understand the redundancy involved.</p>

<p>Every file on an NTFS volume has at least one entry in the <strong>Master File Table</strong>, or <strong>$MFT</strong> (<a href="https://andreafortuna.org/2017/10/11/some-thoughts-about-ntfs-filesystem/">earlier overview</a>). Each MFT entry is 1024 bytes and contains, among other things, two distinct sets of timestamps. The first set lives in the <strong>$STANDARD_INFORMATION</strong> attribute: the four MACB timestamps (Modified, Accessed, Created, $MFT entry modified) that most tools and the Windows Explorer interface expose. The second set lives in the <strong>$FILE_NAME</strong> attribute, which the NTFS kernel driver writes when the file is created and updates under a narrower set of circumstances. The critical forensic implication is that <strong>$FILE_NAME timestamps are written by the kernel and cannot be modified through standard Win32 APIs</strong> like <code class="language-plaintext highlighter-rouge">SetFileTime</code>. Most timestomping tools only reach $STANDARD_INFORMATION.</p>

<p>That asymmetry is the most reliable single indicator of timestamp manipulation available in NTFS forensics. A file where <code class="language-plaintext highlighter-rouge">$STANDARD_INFORMATION</code> Born is earlier than <code class="language-plaintext highlighter-rouge">$FILE_NAME</code> Born is, under normal filesystem operation, an impossibility. The kernel copies $SI values from $FN at creation time; any later backdating of $SI will diverge from the kernel-written $FN values. When you see that divergence, you are reading a contradiction in the metadata record, not making an inference.</p>

<p>The MFT also stores the file’s logical size, physical allocation, attribute list, and, for small files (typically under around 700 bytes), the file content itself as a <strong>resident attribute</strong> inside the MFT entry. This matters for carving: deleting a resident file does not free any cluster, it just marks the MFT entry as available for reuse. The content survives until the entry is overwritten.</p>

<h2 id="usn-journal-the-filesystems-event-stream">USN Journal: the filesystem’s event stream</h2>

<p>The <strong>USN Journal</strong> (<code class="language-plaintext highlighter-rouge">\$Extend\$UsnJrnl</code>) is an NTFS feature that has been available since Windows 2000 (<a href="https://andreafortuna.org/2025/09/06/usn-journal/">dedicated post</a>) and is enabled by default on modern Windows systems. Its primary stream, <code class="language-plaintext highlighter-rouge">$J</code>, records a sequential log of every file system change on the volume: file creates, renames, deletes, attribute changes, security descriptor updates, and more. Each record contains a 64-bit USN identifier, the filename, a parent MFT reference, a reason code bitmask (for example, <code class="language-plaintext highlighter-rouge">USN_REASON_FILE_CREATE</code>, <code class="language-plaintext highlighter-rouge">USN_REASON_RENAME_OLD_NAME</code>, <code class="language-plaintext highlighter-rouge">USN_REASON_BASIC_INFO_CHANGE</code>), and a timestamp.</p>

<p>The <code class="language-plaintext highlighter-rouge">$MAX</code> alternate data stream stores metadata about the journal itself, including the configured maximum size, which controls how far back the journal extends. On an active volume, this typically covers approximately 20 days of activity, though heavily active systems may retain significantly less. You can inspect this on a live system:
fsutil usn queryjournal C:</p>

<p>For DFIR purposes, the USN Journal is valuable because it tracks <strong>changes</strong> rather than static file states. A file that has been deleted, timestomped, or renamed still leaves records behind. Consider the case of a tool deployed by an attacker that was later cleaned up with <strong>SDelete</strong> or a similar secure deletion utility. SDelete’s characteristic behaviour is to rename the target file multiple times before deleting it (<code class="language-plaintext highlighter-rouge">AAA.AAA</code>, <code class="language-plaintext highlighter-rouge">BBB.BBB</code>, and so on), then issue the delete. Each rename operation generates a <code class="language-plaintext highlighter-rouge">RENAME_OLD_NAME</code> and <code class="language-plaintext highlighter-rouge">RENAME_NEW_NAME</code> record in the USN Journal, producing a distinctive signature that survives the deletion itself.</p>

<p>The timestomping detection case is equally clean. A <code class="language-plaintext highlighter-rouge">USN_REASON_BASIC_INFO_CHANGE</code> record at a specific timestamp, correlated with the $SI/$FN discrepancy in the MFT, gives you two independent sources converging on the same event. Adding a Prefetch file for the timestomping binary, if it executed, gives you a third.</p>

<p>To extract the <code class="language-plaintext highlighter-rouge">$J</code> stream from a forensic image, you need to bypass the filesystem layer, since Windows will not give you direct access to NTFS metadata files through normal file operations. Two approaches work well in practice.</p>

<p>Using <strong>MFTECmd</strong> (Eric Zimmerman’s tool):
MFTECmd.exe -f “E:\Evidence$J” –csv C:\output\ –csvf usn.csv</p>

<p>The resulting CSV includes <code class="language-plaintext highlighter-rouge">UpdateTimestamp</code>, <code class="language-plaintext highlighter-rouge">Name</code>, <code class="language-plaintext highlighter-rouge">FileAttributes</code>, <code class="language-plaintext highlighter-rouge">UpdateReasons</code>, <code class="language-plaintext highlighter-rouge">ParentEntryNumber</code>, and <code class="language-plaintext highlighter-rouge">ParentSequenceNumber</code>. To reconstruct full paths, you need to parse the $MFT alongside it and join on the parent MFT entry number:
MFTECmd.exe -f “E:\Evidence$MFT” –csv C:\output\ –csvf mft.csv</p>

<p>Load both in Timeline Explorer and join on <code class="language-plaintext highlighter-rouge">ParentEntryNumber</code>. From there, filtering on <code class="language-plaintext highlighter-rouge">USN_REASON_FILE_DELETE</code> within your incident window, or looking for <code class="language-plaintext highlighter-rouge">FILE_CREATE</code> followed by <code class="language-plaintext highlighter-rouge">FILE_DELETE</code> within 60 seconds, covers a large fraction of malicious tool deployment and cleanup patterns.</p>

<h2 id="dfir_ntfs-going-below-the-surface">dfir_ntfs: going below the surface</h2>

<p><strong>dfir_ntfs</strong>, the Python library developed by <a href="https://github.com/msuhanov/dfir_ntfs">Maxim Suhanov</a> (<a href="https://andreafortuna.org/2021/06/05/dfir_ntfs-a-forensic-parser-for-ntfs-filesystems/">original post</a>), targets a different use case than MFTECmd. Where MFTECmd is optimised for analyst-facing CSV output, dfir_ntfs is a programmatic interface to raw NTFS internals. It parses <code class="language-plaintext highlighter-rouge">$MFT</code>, <code class="language-plaintext highlighter-rouge">$UsnJrnl:$J</code>, and <code class="language-plaintext highlighter-rouge">$LogFile</code> directly, can work against volume images and volume shadow copies, and exposes the underlying structures in Python objects rather than spreadsheet rows.</p>

<p>Installation is straightforward:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip3 <span class="nb">install </span>https://github.com/msuhanov/dfir_ntfs/archive/1.1.19.tar.gz
</code></pre></div></div>

<p>A minimal MFT parsing session:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">dfir_ntfs.MFT</span> <span class="k">as</span> <span class="n">MFT</span>

<span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="s">'$MFT'</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">mft_file</span><span class="p">:</span>
    <span class="n">mft</span> <span class="o">=</span> <span class="n">MFT</span><span class="p">.</span><span class="n">MasterFileTableParser</span><span class="p">(</span><span class="n">mft_file</span><span class="p">)</span>
    <span class="k">for</span> <span class="n">file_record</span> <span class="ow">in</span> <span class="n">mft</span><span class="p">.</span><span class="n">file_records</span><span class="p">(</span><span class="bp">True</span><span class="p">):</span>
        <span class="k">if</span> <span class="ow">not</span> <span class="n">file_record</span><span class="p">.</span><span class="n">is_in_use</span><span class="p">():</span>
            <span class="k">continue</span>
        <span class="n">si</span> <span class="o">=</span> <span class="n">file_record</span><span class="p">.</span><span class="n">get_standard_information</span><span class="p">()</span>
        <span class="n">fn</span> <span class="o">=</span> <span class="n">file_record</span><span class="p">.</span><span class="n">get_file_name</span><span class="p">()</span>
        <span class="k">if</span> <span class="n">si</span> <span class="ow">and</span> <span class="n">fn</span><span class="p">:</span>
            <span class="n">si_created</span> <span class="o">=</span> <span class="n">si</span><span class="p">.</span><span class="n">get_created_time</span><span class="p">()</span>
            <span class="n">fn_created</span> <span class="o">=</span> <span class="n">fn</span><span class="p">.</span><span class="n">get_created_time</span><span class="p">()</span>
            <span class="k">if</span> <span class="n">si_created</span> <span class="ow">and</span> <span class="n">fn_created</span><span class="p">:</span>
                <span class="k">if</span> <span class="n">si_created</span> <span class="o">&lt;</span> <span class="n">fn_created</span><span class="p">:</span>
                    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"[TIMESTOMP] </span><span class="si">{</span><span class="n">fn</span><span class="p">.</span><span class="n">get_file_name</span><span class="p">()</span><span class="si">}</span><span class="s"> SI:</span><span class="si">{</span><span class="n">si_created</span><span class="si">}</span><span class="s"> FN:</span><span class="si">{</span><span class="n">fn_created</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p>That pattern, scanning for MFT entries where $SI Born is earlier than $FN Born, is one of the fastest automated timestomping detection passes you can run on a raw image. The NTFS driver cannot produce that condition under normal operation, so false positives are rare and usually traceable to known edge cases (volume cloning, VSS interactions) rather than attacker activity.</p>

<p>The library also provides access to <strong>volume shadow copies</strong>, which is where many investigations recover artefacts that an attacker deleted after gaining access but before VSS snapshots were removed. From a shadow copy, you can extract a $MFT state from an earlier point in time and compare it against the live MFT to identify entries that have been deleted or modified in between.</p>

<h2 id="the-third-layer-logfile-and-what-it-tells-you">The third layer: $LogFile and what it tells you</h2>

<p>The <strong>$LogFile</strong> is the NTFS transaction journal, a structure designed primarily to ensure filesystem consistency across crashes. It is also an independent source of metadata write events that most analysts overlook entirely.</p>

<p>$LogFile records redo and undo operations for NTFS metadata updates, including attribute changes, in terms of Log Sequence Numbers (LSNs). Each MFT entry carries the LSN of the most recent transaction that modified it. When you find a suspicious timestamp in $STANDARD_INFORMATION and want to independently confirm when it was last written, the corresponding $LogFile entry for that LSN gives you an out-of-band timestamp for the transaction itself.</p>

<p>Practical extraction is straightforward with <strong>NTFS Log Tracker</strong> (available at <a href="https://sites.google.com/site/forensicnote/ntfs-log-tracker">ForensicNote</a>) or through dfir_ntfs’ <code class="language-plaintext highlighter-rouge">LogFile.py</code> module. In Timeline Explorer, loading both the MFT CSV and the $LogFile CSV and cross-referencing by LSN gives you a three-source timeline per file:</p>

<ol>
  <li><strong>$FILE_NAME Born</strong>: kernel-written, reliable, rarely tampered with.</li>
  <li><strong>$STANDARD_INFORMATION timestamps</strong>: the attacker-facing values, modified by timestomping tools.</li>
  <li><strong>$LogFile LSN timestamp</strong>: when the NTFS driver last wrote the $SI attribute, independent of the value it wrote.</li>
</ol>

<p>When all three converge, you have a solid story. When $LogFile shows a write to $STANDARD_INFORMATION at a time inconsistent with the $SI value itself, you have direct evidence of manipulation, not just an anomaly.</p>

<h2 id="file-carving-a-fallback-with-limitations">File carving: a fallback with limitations</h2>

<p>Once you have exhausted MFT, USN Journal, and $LogFile analysis, file carving is what you turn to when filesystem metadata is genuinely gone, either because the attacker went after NTFS structures directly (as modern wipers like <a href="https://andreafortuna.org/2026/04/15/wiper-disk-evolution/">HermeticWiper and CaddyWiper do</a>), or because the MFT entry was reallocated before the investigation started.</p>

<p>I covered carving techniques in more detail in <a href="https://andreafortuna.org/2018/05/02/some-thought-about-file-carving/">Some thoughts about file carving</a>. <strong>Basic carving</strong> works on header and footer signatures (magic numbers): <code class="language-plaintext highlighter-rouge">%PDF</code> / <code class="language-plaintext highlighter-rouge">%EOF</code> for PDF files, <code class="language-plaintext highlighter-rouge">0xFFD8</code> / <code class="language-plaintext highlighter-rouge">0xFFD9</code> for JPEG, <code class="language-plaintext highlighter-rouge">CA FE BA BE</code> for Java class files, and so on. The <a href="https://www.garykessler.net/library/file_sigs.html">Gary Kessler signature list</a> remains the reference here. Basic carving assumes the file is not fragmented and the beginning of the file is not overwritten.</p>

<p><strong>Advanced carving</strong> handles fragmented files, where fragments may be non-sequential or have gaps. It relies on content analysis rather than just header/footer matching, which is computationally expensive but necessary for email archives, database files, or anything that grows over time.</p>

<p>The practical limitation is that carving produces no provenance. A recovered file has no MFT entry, no USN Journal record, no chain of custody in the filesystem sense. It exists as bytes. What it tells you about <em>when</em> something happened, or <em>who</em> performed an action, depends entirely on internal file metadata (EXIF data, document properties, embedded timestamps) rather than filesystem artefacts. For court purposes, and for timeline reconstruction, that is a meaningful limitation.</p>

<p>When the MFT entry survives but the file is marked as deleted, you can frequently recover both the file and its metadata. The MFT entry still contains the file’s timestamps, size, and allocation. In that case, you are accessing a deleted but intact record rather than carving from raw fragments, and the forensic value is substantially higher.</p>

<h2 id="putting-it-together-a-practical-investigation-workflow">Putting it together: a practical investigation workflow</h2>

<p>The standard starting point is always the $MFT, because it is the index to everything else. Extract it first, parse it with MFTECmd, and load the output in Timeline Explorer. At that stage you have a complete list of files the filesystem knows about, including deleted entries whose MFT records have not yet been reused.</p>

<p>From there, the sequence depends on what you find. If the incident window is clear, filter the $MFT timeline to that window and identify the files of interest. For each suspicious file, pull the USN Journal records for the corresponding MFT entry number and check whether the timestamps are consistent. Any <code class="language-plaintext highlighter-rouge">USN_REASON_BASIC_INFO_CHANGE</code> event at a time that does not match the $SI timestamps is an immediate red flag.</p>

<p>For files where you suspect timestomping specifically, compare $SI and $FN timestamps directly. A divergence greater than a few seconds, or a $SI Born earlier than $FN Born, warrants $LogFile analysis to confirm when the $SI attribute was last written.</p>

<p>If you are working with dfir_ntfs in a scripted pipeline, the library gives you the tools to automate all of this across an entire image: iterate MFT records, flag $SI/$FN discrepancies, cross-reference against USN Journal records for the same entry number, and export findings to a structured format for timeline import. For large-scale triage across multiple machines, that scripted approach scales better than loading individual CSVs in a GUI tool.</p>

<p><a href="https://andreafortuna.org/2017/10/02/volume-shadow-copies-in-forensic-analysis/">Volume shadow copies</a> are worth checking before you conclude that deleted artefacts are unrecoverable. dfir_ntfs handles VSS parsing natively, and comparing the MFT state across shadow copy snapshots can surface files that were present at a known point in time and subsequently removed.</p>

<p>The NTFS artefact layer is not indestructible. As <a href="https://andreafortuna.org/2026/04/15/wiper-disk-evolution/">the evolution of disk wipers</a> shows, modern destructive malware increasingly attacks NTFS metadata structures directly, and a sufficiently thorough wiper can corrupt or destroy the $MFT, $UsnJrnl, and $LogFile before a response team arrives. In those cases, carving is genuinely what remains. But that scenario is the exception rather than the rule. In most investigations, the NTFS layer is intact, the attacker assumed that deleting files and clearing event logs was sufficient, and the three-source correlation described here recovers considerably more than the attacker intended to leave behind.</p>

<h2 id="faq">FAQ</h2>

<h3 id="what-is-the-usn-journal-and-why-does-it-matter-in-dfir">What is the USN Journal and why does it matter in DFIR?</h3>

<p>The USN Journal is an NTFS change-log that records every file system event, including creations, renames, deletions, and metadata changes. It persists even after files are deleted, making it essential for timeline reconstruction and detection of anti-forensic techniques like timestomping.</p>

<h3 id="how-do-you-detect-timestomping-using-ntfs-artefacts">How do you detect timestomping using NTFS artefacts?</h3>

<p>By comparing the $STANDARD_INFORMATION and $FILE_NAME timestamp attributes in the MFT. If $STANDARD_INFORMATION timestamps predate $FILE_NAME timestamps for the same file, the kernel-written timestamps have been tampered with. The USN Journal and $LogFile provide independent corroboration of when the manipulation actually occurred.</p>

<h3 id="what-is-dfir_ntfs-and-how-does-it-differ-from-mftecmd">What is dfir_ntfs and how does it differ from MFTECmd?</h3>

<p>dfir_ntfs is a Python library by Maxim Suhanov that parses $MFT, $UsnJrnl, $LogFile, volume images, and shadow copies, exposing raw NTFS internals programmatically. MFTECmd is a Windows GUI/CLI tool that produces analyst-ready CSV output for Timeline Explorer. They are complementary rather than competing tools.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="DFIR" /><category term="NTFS" /><category term="Digital Forensics" /><category term="Windows" /><category term="Forensic Analysis" /><summary type="html"><![CDATA[A practical deep dive into NTFS forensic artefacts: MFT, USN Journal, $LogFile, and how to combine them with dfir_ntfs and MFTECmd to detect anti-forensic techniques.]]></summary></entry><entry><title type="html">In praise of the ordinary worker</title><link href="https://andreafortuna.org/2026/06/29/mediocrity-ordinary-worker/" rel="alternate" type="text/html" title="In praise of the ordinary worker" /><published>2026-06-29T00:00:00+00:00</published><updated>2026-06-29T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/29/mediocrity-ordinary-worker</id><content type="html" xml:base="https://andreafortuna.org/2026/06/29/mediocrity-ordinary-worker/"><![CDATA[<p>There is a moment in most performance review cycles when someone, almost certainly someone in HR, uses the phrase “high performer” without irony, and the room subtly recalibrates itself around that centre of gravity. Everyone wants to be one. Nobody wants to be the person left over after the high performers have been identified. The implicit message is not subtle: average is something to escape.</p>

<p><img src="/assets/2026/mediocrity-ordinary-worker.jpg" alt="cover" /></p>

<p>I have been in enough rooms, and enough organisations, to notice that this story has a loose relationship with how organisations actually function. The companies that do not implode, that pay their invoices on time, that do not require emergency all-hands meetings because a key person walked out, are sustained by ordinary people doing ordinary things, reliably, across thousands of working days — not by exceptional people doing exceptional things.</p>

<p>This is not a consolation prize but a more accurate description of reality than most management consultants are paid to provide.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>The performance obsession in modern workplaces is a cultural artifact rather than a management best practice.</li>
  <li>Most organisations run on distributed, competent mediocrity instead of the output of exceptional individuals.</li>
  <li>Knowing your limits is a form of professional maturity rather than a confession of inadequacy.</li>
  <li>A person who is average at their job may be genuinely excellent at chess, parenting, or running a ten-kilometre race. That does not make them less valuable at work.</li>
  <li>Chasing excellence across every dimension of life is a recipe for exhaustion rather than achievement.</li>
</ul>

<h2 id="the-mythology-of-the-superstar">The mythology of the superstar</h2>

<p>The idea that organisations are defined by their exceptional individuals is not entirely false. Some roles genuinely require rare capability. A small number of people in any field operate at a level that is qualitatively different from their peers, and pretending otherwise would be its own kind of dishonesty. But the mythology that has grown up around this observation has become almost entirely detached from its origin.</p>

<p>The modern performance cult treats exceptionalism as a universal aspiration and, implicitly, a moral obligation, rather than as a statistical rarity. You are not merely expected to do your job well. You are expected to disrupt, to go above and beyond, to bring your whole self to work, to have a personal brand, to treat every quarterly review as an opportunity to demonstrate that you are on a trajectory toward something. The language of Silicon Valley talent management has colonised organisations that have no more need for <a href="https://andreafortuna.org/2025/03/11/no-juniors-allowed-how-cybersecurity-is-shooting-itself-in-the-foot/">“10x engineers”</a> than a municipal waterworks has for a visionary chief evangelism officer.</p>

<p>What this mythology produces, in practice, is a workforce in which everyone is performing, in the theatrical sense of the word, rather than being actual high performers: <a href="https://andreafortuna.org/2025/06/13/task-masking-the-art-of-looking-busy/">staging visible indicators of exceptional output</a> while the actual work gets done in the margins. The meetings run long. The Slack messages arrive at eleven at night. The slide deck has a growth chart. The infrastructure quietly breaks because nobody had time to maintain it.</p>

<h2 id="the-quiet-arithmetic-of-ordinary-people">The quiet arithmetic of ordinary people</h2>

<p>Here is a question that is almost never asked in management literature: what is the organisational contribution of a person who is reliably present, competent, calm under pressure, and completely unspectacular?</p>

<p>The answer, if you work through it honestly, is enormous. It comes from what that person makes possible for everyone around them, not from any individual output alone. They do not create work for others by requiring management of their ego, their volatility, or their inexplicable decision to refactor a critical system two days before a product launch. They do not disappear into extended performance-coaching processes. Their name does not appear in tense email threads. They are, in the most underappreciated sense of the term, <a href="https://andreafortuna.org/2025/12/30/glue-employees-hold-teams-together/">load-bearing</a>.</p>

<p><a href="https://www.sei.cmu.edu/blog/programmer-moneyball-challenging-the-myth-of-individual-programmer-productivity/">Research on team dynamics in software engineering</a> and related fields has consistently shown that team-level reliability depends less on the ceiling of individual performance than on the floor. A team with one exceptional engineer and three chaotic ones will lose, systematically, to a team of four solid, unremarkable engineers who communicate clearly and do not introduce surprises. The exceptional individual is real. The exceptional individual as a unit of organisational strategy is a marketing story.</p>

<p>The manufacturing world learned this a long time ago. <strong>Toyota’s production system</strong>, arguably the most influential management framework of the past sixty years, is built entirely on the premise that sustainable quality comes from ordinary people following well-designed processes consistently, not from heroic individuals compensating for broken systems. The Toyota insight is that a system depending on individual heroism is fragile by design — not that people do not matter.</p>

<h2 id="the-right-size-of-ambition">The right size of ambition</h2>

<p>There is a version of the argument against performance culture that I am not making. The more defensible position is that <strong>excellence is context-dependent, and the demand for it should be proportional to what is actually required.</strong></p>

<p>A person who writes clear incident reports, responds to tickets in a reasonable time, does not introduce regressions, and leaves good documentation is, in a meaningful sense, excellent at their job. That person does not need to be a thought leader. They do not need to give conference talks. They do not need a “personal development plan” that involves becoming something they currently are not. They need to be left alone to continue doing what they are doing, with fair compensation and occasional acknowledgement.</p>

<p>More interestingly: that same person may be genuinely exceptional at something that has nothing to do with work. They might be a highly rated chess player, or an unusually good amateur musician, or a dedicated marathon runner, or the kind of parent who has the patience to actually be present. The performance cult, which evaluates the entire human through the narrow aperture of professional output, treats this as irrelevant. I think it is one of the most important facts about that person, because it tells you that human capability is a profile rather than a single variable. And profiles have peaks and troughs that have no particular obligation to align with job descriptions.</p>

<p>The person who is average at their job may be living a richer life than the person who has optimised their existence entirely around professional performance. This is allowed without being a failure condition.</p>

<h2 id="what-organisations-actually-need-to-understand">What organisations actually need to understand</h2>

<p>None of this is an argument for complacency, for turning up unprepared, for half-hearted effort, or for the particularly corrosive behaviour of someone who has quietly decided that they have checked out but has not bothered to tell anyone. <strong>There is a difference between competent mediocrity and careless indifference.</strong> The distinction matters.</p>

<p>What I am describing is the recovery of a realistic norm: that the standard professional expectation is doing your job well, without requiring extraordinary commitment beyond its scope. That this is valuable. That this is sufficient. That an organisation that cannot function unless its people are continuously operating beyond <a href="https://andreafortuna.org/2025/07/27/umbrella-management-because-burnout-isn-t-a-strategy/">sustainable limits</a> has a process problem rather than a talent problem.</p>

<p>The <a href="https://andreafortuna.org/2026/06/03/security-awareness/">security awareness training</a> parallel is instructive here. The same organisational psychology that has made annual checkbox training a substitute for actual security has made the performance review cycle a substitute for actual management. Both produce documentation. Neither produces the thing the documentation claims to measure. A culture in which every employee is being evaluated against an implicit standard of exceptional performance produces a lot of performance anxiety and a lot of visible busyness, and it tends to suppress the honest admission that something is broken until the cost of the broken thing exceeds the cost of pretending it is not.</p>

<p>The organisations that deal well with incidents, whether a ransomware case or a difficult quarter, are those with clear processes, good communication habits, and enough people who are sufficiently competent and sufficiently sane to execute under pressure — not the ones that had the most exceptional individual contributors. Resilience is a collective property of ordinary people in a well-structured system. It is not the shadow cast by an exceptional few.</p>

<p>This connects to something I noticed in thinking about <a href="https://andreafortuna.org/2026/06/05/dfir-analyst-psychological-impact/">DFIR team sustainability</a>: the teams that hold together over time, that do not crater under operational pressure, are staffed by people who found a workable equilibrium between what the work demands and what a human life can sustain — not by people who gave everything and had nothing left. That equilibrium is, almost by definition, average. It is the most important thing a team can have.</p>

<p>If you are the person in your organisation who shows up, does the work, does not create drama, and goes home to something you actually care about: that is a reasonable life, well-lived — not a failure. The machine keeps running. The invoices get paid. Someone, somewhere, should probably say thank you.</p>

<h2 id="faq">FAQ</h2>

<h3 id="is-mediocrity-in-the-workplace-actually-a-good-thing">Is mediocrity in the workplace actually a good thing?</h3>

<p>Mediocrity here means consistent, reliable, middle-of-the-distribution performance, not carelessness or incompetence. That kind of output sustains most organisations far more than the occasional superstar hire.</p>

<h3 id="why-do-companies-over-invest-in-finding-exceptional-talent">Why do companies over-invest in finding exceptional talent?</h3>

<p>Because exceptional talent is visible, narratable, and easy to put in a slide deck. The distributed competence of a solid team is invisible until it disappears.</p>

<h3 id="how-does-the-performance-obsession-damage-individuals">How does the performance obsession damage individuals?</h3>

<p>By creating a permanent background pressure to demonstrate exceptional output across every dimension of life, which is neither realistic nor healthy. Knowing your limits is a form of self-knowledge, not failure.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Technology" /><category term="Productivity" /><category term="Workplace" /><category term="Culture" /><category term="Personal" /><summary type="html"><![CDATA[The performance cult wants everyone to be exceptional. The data, and basic organisational reality, suggest otherwise.]]></summary></entry><entry><title type="html">Forensic tools as instruments of repression: Russia, Cellebrite, and the case of Andrey Pivovarov</title><link href="https://andreafortuna.org/2026/06/28/cellebrite-russia-pivovarov/" rel="alternate" type="text/html" title="Forensic tools as instruments of repression: Russia, Cellebrite, and the case of Andrey Pivovarov" /><published>2026-06-28T00:00:00+00:00</published><updated>2026-06-28T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/28/cellebrite-russia-pivovarov</id><content type="html" xml:base="https://andreafortuna.org/2026/06/28/cellebrite-russia-pivovarov/"><![CDATA[<p>Andrey Pivovarov was removed from a flight at St. Petersburg airport on May 31, 2021, and detained by the Russian security services. He never provided his passwords. He never consented to a device search. None of that mattered.</p>

<p><img src="/assets/2026/cellebrite-russia-pivovarov.jpg" alt="cover" /></p>

<p>According to a <a href="https://citizenlab.ca/research/russia-breaks-into-human-rights-activists-phone-with-cellebrite/">detailed forensic investigation by the Citizen Lab</a>, Russian authorities used <strong>Cellebrite</strong>’s <strong>UFED</strong> (Universal Forensic Extraction Device) to break into his iPhone 12 on or around June 17, 2021, while the device was in official custody and Pivovarov was awaiting trial on politically motivated charges. The company had publicly cancelled its Russian contracts three months earlier.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>The Citizen Lab forensically confirmed that Cellebrite’s UFED was used to extract data from Pivovarov’s iPhone 12 on June 17, 2021, during official custody.</li>
  <li>Russia’s own MVD forensic report explicitly names Cellebrite’s UFED Physical Analyzer and UFED 4PC as the tools used in the extraction.</li>
  <li>Russian authorities searched the device for political contacts including Mikhail Khodorkovsky and human rights lawyer Anastasiya Burakova, suggesting the extraction may have seeded further targeting campaigns.</li>
  <li>Cellebrite cancelled its Russian contracts in March 2021, but the hardware continued to operate in offline mode, effectively nullifying the cancellation.</li>
  <li>Pivovarov’s MacBook, protected by full-disk encryption, was not successfully accessed — a concrete demonstration of why encryption matters.</li>
  <li>Cellebrite’s pattern across multiple countries remains reactive: it cancels contracts only after third-party exposure, and its technical architecture has historically made those cancellations easy to circumvent.</li>
</ul>

<h2 id="who-is-andrey-pivovarov">Who is Andrey Pivovarov</h2>

<p>Pivovarov served as director of Open Russia, a non-profit organization the Russian government designated as “undesirable” in 2017, a classification the European Court of Human Rights later found incompatible with the European Convention on Human Rights. Sensing the escalating legal risk, Pivovarov dissolved the Russian branch of Open Russia on May 27, 2021. Four days later, he was arrested.</p>

<p>In July 2022, he was sentenced to four years in prison for “carrying out the activities of an undesirable organization” — charges that are, by any reasonable reading of international human rights law, politically motivated. He was released in August 2024 as part of a prisoner exchange. After his release, he made contact with Citizen Lab researchers at the World Liberty Congress in Berlin, and agreed to have his devices forensically examined. What they found was not a surprise, exactly, but it was documented for the first time with forensic precision.</p>

<h2 id="the-forensic-evidence">The forensic evidence</h2>

<p>The Citizen Lab’s analysis focused on <strong>MobileLockdown</strong> records from Pivovarov’s iPhone, specifically USB connection logs that include a <strong>Host ID</strong>, a unique identifier assigned to a Cellebrite device. The Host ID found on Pivovarov’s phone (<code class="language-plaintext highlighter-rouge">9016926980658937761372207</code>) was one the Citizen Lab had previously attributed to Cellebrite’s forensic hardware.</p>

<p>That alone would be strong evidence. But what makes this case unusual is the corroboration from an unexpected source: the Russian authorities themselves. The <strong>MVD Forensic Expert Report No. 1269-17</strong>, produced by Russia’s Forensic Expert Center of the Ministry of the Interior and provided to Pivovarov during his prosecution, explicitly confirms the use of Cellebrite’s UFED Physical Analyzer and UFED 4PC toolkit. The investigators documented extracting data from WhatsApp, Telegram, and Viber, and then searching the device contents for political terms: “Open Russia Civic Movement,” the name of opposition figure Mikhail Khodorkovsky, human rights lawyer Anastasiya Burakova, and Open Russia coordinator Tatiana Usmanova.</p>

<p>This is a useful reminder of how forensic tools actually get used in repressive contexts: to map political networks rather than to investigate crimes. As I’ve discussed before in the context of <a href="https://andreafortuna.org/2026/04/23/android-pattern-of-life-hidden-artifacts-reconstruct-daily-routine/">Android pattern-of-life forensics</a>, the real power of device extraction lies in the reconstruction of relationships, habits, and associations, not in any single message or photo. In the hands of a state prosecutor pursuing political dissidents, that capability is a tool of repression rather than a law enforcement tool.</p>

<h2 id="the-macbook-that-held">The MacBook that held</h2>

<p>There is, in this story, one piece of genuinely good news. When Russian authorities seized Pivovarov’s <strong>Apple MacBook</strong> along with his iPhone, they could not get in. The MVD report itself documents the failure: the MacBook’s full-disk encryption made it impossible to extract the file system. The document includes screenshots of the login screen and macOS recovery functionality, the digital equivalent of a photo of someone staring at a locked door.</p>

<p>The forensic analysis found what appear to be failed login attempts on June 17, 2021. There was one apparent “successful” login in the records, but the Citizen Lab assessed this was actually a backdated timestamp: the MacBook’s battery had drained during its years in official custody, causing the system clock to reset to <code class="language-plaintext highlighter-rouge">1970-01-01</code> on first boot. When the device was eventually reconnected to Wi-Fi after being returned to Pivovarov in 2024, the clock corrected itself, jumping forward more than three years. The “successful” login of June 17, 2021 was in fact a login in November 2024. The Russian authorities got nothing from that machine.</p>

<p>The contrast with the iPhone outcome is instructive, and it connects to a topic I covered in some detail when examining <a href="https://andreafortuna.org/2026/03/29/ios-lockdown-mode-forensics/">iOS Lockdown Mode and its forensic implications</a>: the security posture of a device at the moment of seizure determines what an investigator, or an adversary with forensic tools, can actually recover. Full-disk encryption on the MacBook worked. The iPhone’s protections, apparently, did not hold against Cellebrite’s extraction capabilities in 2021.</p>

<h2 id="cellebrites-architecture-of-selective-accountability">Cellebrite’s architecture of selective accountability</h2>

<p>Cellebrite cancelled its Russian and Belarusian contracts in March 2021, following a legal petition filed the previous year by Israeli lawyer Eitay Mack alleging the company’s tools had been used by Russia’s Investigative Committee, the same body responsible for prosecuting Alexei Navalny, Pussy Riot, and others, for political repression.</p>

<p>The problem is structural. Cellebrite’s UFED systems have historically featured an <strong>offline mode</strong>, meaning they continue to function without phoning home for license validation. The Citizen Lab notes that the “historic architecture” of Cellebrite’s systems means that core functionality persists long after updates and support cease. In practice, a contract cancellation with a customer like the Russian Investigative Committee is less a hard cutoff than a gradual degradation. The hardware keeps working. It just stops receiving updates for new device compatibility.</p>

<p>This is not the first time this pattern has been documented. The Citizen Lab has forensically confirmed Cellebrite abuses in <a href="https://citizenlab.ca/">Serbia</a>, Jordan, Kenya, and now Russia. In each case, Cellebrite’s response has been reactive: contracts cancelled after third-party exposure, with limited transparency about how the company evaluates customers or investigates reported abuses.</p>

<p>There is also a broader concern worth flagging. The MVD report shows Russian authorities using Cellebrite to search for Anastasiya Burakova, a human rights lawyer. In 2024, the Citizen Lab documented a global hacking campaign by <strong>COLDRIVER</strong>, a group linked to the Russian FSB, that targeted Burakova and other individuals in Pivovarov’s social network. The Citizen Lab notes the correlation: data extracted from Pivovarov’s phone under the color of legal prosecution may have contributed to identifying targets for subsequent FSB surveillance operations abroad.</p>

<p>Cellebrite markets its <strong>AI-enhanced analysis tools</strong> as capable of developing exactly the kind of pattern-of-life and social graph mapping that would be useful to a state looking to dismantle opposition networks. The increasing integration of AI into forensic platforms, something I’ve written about in the context of both <a href="https://andreafortuna.org/2024/06/12/mobile-forensics-tools-and-techniques/">mobile forensics</a> and <a href="https://andreafortuna.org/2026/04/21/apple-watch-forensics/">Apple Watch acquisition</a>, raises the stakes considerably. LLMs can also introduce errors, creating false positives in pattern matching that could implicate innocent parties. In a system where the legal process itself is weaponized, those false positives have real consequences.</p>

<h2 id="what-you-can-actually-do">What you can actually do</h2>

<p>The Citizen Lab’s recommendations are practical and worth reiterating, because they translate directly into personal security hygiene regardless of whether you face state-level adversaries.</p>

<ul>
  <li>Keep your device’s operating system up to date. Cellebrite’s extraction capabilities depend in part on known vulnerabilities in older OS versions.</li>
  <li>Use a strong, preferably alphanumeric passcode. PIN codes are significantly more susceptible to brute force.</li>
  <li>Enable <strong>Lockdown Mode</strong> on iPhone if you are at elevated risk. As I explored in detail in my <a href="https://andreafortuna.org/2026/03/29/ios-lockdown-mode-forensics/">analysis of iOS Lockdown Mode</a>, it substantially reduces the attack surface available to forensic extraction tools.</li>
  <li>Enable <strong>Full Disk Encryption</strong> on computers. Pivovarov’s MacBook is proof that this works, even against a state adversary with physical access and time.</li>
  <li>Enable <strong>Advanced Data Protection</strong> on iCloud (or Android’s equivalent). Data in transit and at rest should be encrypted with keys the provider cannot hand over.</li>
  <li>Power off your device completely before any situation where seizure is possible. A device at rest, never unlocked since last boot, is significantly harder to extract data from than one that has been recently unlocked.</li>
  <li>Use a password manager and ensure every account has a unique credential. If a device is extracted, changing passwords for accounts that were accessible from that device should be your first step.</li>
</ul>

<p>If your device is returned to you after seizure, do not factory reset it before having it examined by a qualified forensic professional. The forensic traces that allowed the Citizen Lab to identify Cellebrite’s involvement in Pivovarov’s case came from the device itself. Deleting that evidence before analysis eliminates any possibility of understanding or documenting what was done.</p>

<p>The Pivovarov case is a clean, forensically solid example of something that happens far more broadly and with far less documentation. Forensic tools sold to law enforcement do not check, at the moment of use, whether the criminal prosecution they are supporting is politically motivated or compatible with international human rights law. That responsibility falls entirely on the vendor — and Cellebrite’s track record suggests it would rather cancel contracts when the press coverage becomes uncomfortable than prevent the abuse in the first place.</p>

<h2 id="faq">FAQ</h2>

<p><strong>How did Russian authorities access Andrey Pivovarov’s iPhone?</strong></p>

<p>Forensic analysis by the Citizen Lab found traces of Cellebrite’s UFED on Pivovarov’s iPhone 12, confirmed by an official Russian MVD forensic report that explicitly named Cellebrite’s UFED Physical Analyzer and UFED 4PC toolkit as the instruments used in the extraction.</p>

<p><strong>Did Cellebrite authorize the use of its tools in Russia after 2021?</strong></p>

<p>No. Cellebrite cancelled its contracts with Russian and Belarusian customers in March 2021. However, the hardware continued to function in offline mode, and the Citizen Lab’s investigation confirms Russian authorities continued leveraging it for political prosecutions after the cancellation.</p>

<p><strong>What practical steps can activists take to protect their devices from forensic extraction?</strong></p>

<p>Key measures include keeping device software up to date, using a strong alphanumeric passcode, enabling Lockdown Mode on iPhone, enabling Full Disk Encryption on computers, and powering devices off completely before any situation involving risk of seizure.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="Privacy" /><category term="Mobile Forensics" /><category term="Cellebrite" /><category term="Russia" /><category term="Citizen Lab" /><category term="Human Rights" /><category term="DFIR" /><summary type="html"><![CDATA[How Russian authorities used Cellebrite's UFED to extract data from a political activist's iPhone, even after the company had cancelled its Russian contracts.]]></summary></entry><entry><title type="html">Building a CI/CD pipeline for Sigma rules</title><link href="https://andreafortuna.org/2026/06/22/sigma-cicd-pipeline/" rel="alternate" type="text/html" title="Building a CI/CD pipeline for Sigma rules" /><published>2026-06-22T00:00:00+00:00</published><updated>2026-06-22T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/22/sigma-cicd-pipeline</id><content type="html" xml:base="https://andreafortuna.org/2026/06/22/sigma-cicd-pipeline/"><![CDATA[<p>Nobody ships application code directly to production by typing it into the server. The idea is absurd. Yet the equivalent happens every day in detection engineering: an analyst opens the SIEM console, edits a rule, saves it, and the change is live. No diff, no review, no test, no rollback path. The rule is now in production and nobody has a record of what it looked like before.</p>

<p><img src="/assets/2026/sigma-cicd-pipeline.jpg" alt="cover" /></p>

<p>If you have been following the <a href="https://andreafortuna.org/2026/06/17/detection-as-code/">Detection as Code methodology described in a previous post</a>, you already know why this is a problem. This article is about the specific plumbing that fixes it: a CI/CD pipeline built around <strong>Sigma</strong>, the vendor-neutral detection rule format, with automated validation, test fixtures, and controlled deployment to your SIEM.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>A Sigma CI/CD pipeline has four stages: validate, translate, test, deploy. Skipping any of them defeats the purpose.</li>
  <li><code class="language-plaintext highlighter-rouge">sigma-cli</code> handles validation and conversion in CI without external services or paid tooling.</li>
  <li>Every rule needs a positive test fixture (event it must match) and a negative one (event it must not match). Rules without both should fail CI.</li>
  <li>New rules should enter shadow mode for at least seven days before producing analyst alerts.</li>
  <li>CERT-EU’s <code class="language-plaintext highlighter-rouge">droid</code> is the most complete open-source tool for the full pipeline, from Atomic Red Team simulation to multi-SIEM deployment.</li>
</ul>

<h2 id="repository-structure-before-anything-else">Repository structure before anything else</h2>

<p>Before writing a single workflow file, get the repository structure right. The layout determines how maintainable the pipeline is at 200 rules. A structure that works in practice looks like this:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>detection-rules/
├── sigma/
│ ├── windows/
│ ├── linux/
│ ├── cloud/
│ └── network/
├── tests/
│ └── fixtures/
│ ├── windows/
│ └── cloud/
├── platform-translations/
│ ├── sentinel-kql/
│ ├── splunk-spl/
│ └── elastic-lucene/
├── scripts/
│ ├── validate_metadata.py
│ └── coverage_report.py
└── .github/workflows/
├── ci.yml
└── deploy.yml
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">sigma/</code> directory is the only source-controlled detection content. Everything under <code class="language-plaintext highlighter-rouge">platform-translations/</code> is a build artifact, generated by CI, never edited by hand. If a Sentinel KQL query needs changing, you change the Sigma source and let the pipeline regenerate the translation. Editing the translated file directly is the same failure mode as editing production code on the server.</p>

<p>The <code class="language-plaintext highlighter-rouge">tests/fixtures/</code> directory holds synthetic log events, one subdirectory per platform, named after the technique or rule they are testing. The convention matters: a fixture named <code class="language-plaintext highlighter-rouge">t1059_001_positive.json</code> next to <code class="language-plaintext highlighter-rouge">t1059_001_negative.json</code> is self-documenting in a way that a flat list of numbered files is not.</p>

<h2 id="rule-validation">Rule validation</h2>

<p>The first CI job runs on every pull request and every push to any branch. It does one thing: confirm that every <code class="language-plaintext highlighter-rouge">.yml</code> file in <code class="language-plaintext highlighter-rouge">sigma/</code> is a valid Sigma rule.</p>

<p><a href="https://github.com/SigmaHQ/sigma-cli"><code class="language-plaintext highlighter-rouge">sigma-cli</code></a> is the primary tool here. It wraps the <code class="language-plaintext highlighter-rouge">pySigma</code> library and exposes a command-line interface for validation, listing, and conversion:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip <span class="nb">install </span>sigma-cli
sigma check ./sigma/
</code></pre></div></div>

<p><code class="language-plaintext highlighter-rouge">sigma check</code> validates syntax, required fields, and logical consistency. A rule with a malformed detection condition, a missing <code class="language-plaintext highlighter-rouge">logsource</code> block, or an invalid condition syntax fails with a non-zero exit code, which causes the CI job to fail and blocks the merge.</p>

<p>For JSON Schema validation, <a href="https://github.com/marketplace/actions/sigma-rules-validator">SigmaHQ’s <code class="language-plaintext highlighter-rouge">sigma-rules-validator</code> GitHub Action</a> was originally developed and donated to the community by the Grafana Labs SecOps team. It validates rules against the official JSON Schema maintained in the Sigma specification repository, and accepts a <code class="language-plaintext highlighter-rouge">paths</code> input to target specific subdirectories:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">steps</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
  <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">SigmaHQ/sigma-rules-validator@v1</span>
    <span class="na">with</span><span class="pi">:</span>
      <span class="na">paths</span><span class="pi">:</span> <span class="pi">|-</span>
        <span class="s">./sigma/windows</span>
        <span class="s">./sigma/cloud</span>
</code></pre></div></div>

<p>You can also pass a custom schema file or a schema URL, which is useful when your organization maintains a stricter internal variant of the Sigma specification.</p>

<p>A second validation job should enforce metadata completeness. Rules without MITRE ATT&amp;CK technique IDs, an <code class="language-plaintext highlighter-rouge">author</code> field, a <code class="language-plaintext highlighter-rouge">status</code>, and a <code class="language-plaintext highlighter-rouge">date</code> are operationally useless at scale. A thirty-line Python script in CI that reads each rule file and exits non-zero if any of those keys are absent costs nothing to maintain and prevents the repository from accumulating orphaned detections:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">sys</span><span class="p">,</span> <span class="n">pathlib</span><span class="p">,</span> <span class="n">yaml</span>

<span class="n">required</span> <span class="o">=</span> <span class="p">{</span><span class="s">"title"</span><span class="p">,</span> <span class="s">"status"</span><span class="p">,</span> <span class="s">"date"</span><span class="p">,</span> <span class="s">"author"</span><span class="p">,</span> <span class="s">"logsource"</span><span class="p">,</span> <span class="s">"detection"</span><span class="p">}</span>

<span class="n">failed</span> <span class="o">=</span> <span class="p">[]</span>
<span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">pathlib</span><span class="p">.</span><span class="n">Path</span><span class="p">(</span><span class="s">"sigma"</span><span class="p">).</span><span class="n">rglob</span><span class="p">(</span><span class="s">"*.yml"</span><span class="p">):</span>
    <span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
        <span class="n">rule</span> <span class="o">=</span> <span class="n">yaml</span><span class="p">.</span><span class="n">safe_load</span><span class="p">(</span><span class="n">f</span><span class="p">)</span>
    <span class="n">missing</span> <span class="o">=</span> <span class="n">required</span> <span class="o">-</span> <span class="n">rule</span><span class="p">.</span><span class="n">keys</span><span class="p">()</span>
    <span class="k">if</span> <span class="n">missing</span><span class="p">:</span>
        <span class="n">failed</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s">: missing </span><span class="si">{</span><span class="n">missing</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
    <span class="k">if</span> <span class="ow">not</span> <span class="n">rule</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"tags"</span><span class="p">):</span>
        <span class="n">failed</span><span class="p">.</span><span class="n">append</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">path</span><span class="si">}</span><span class="s">: no ATT&amp;CK tags"</span><span class="p">)</span>

<span class="k">if</span> <span class="n">failed</span><span class="p">:</span>
    <span class="k">for</span> <span class="n">msg</span> <span class="ow">in</span> <span class="n">failed</span><span class="p">:</span> <span class="k">print</span><span class="p">(</span><span class="n">msg</span><span class="p">)</span>
    <span class="n">sys</span><span class="p">.</span><span class="nb">exit</span><span class="p">(</span><span class="mi">1</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="query-translation">Query translation</h2>

<p>After validation, the pipeline generates platform-specific queries from the Sigma sources. <code class="language-plaintext highlighter-rouge">sigma convert</code> handles this with backends installed as <code class="language-plaintext highlighter-rouge">pySigma</code> plugins:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install backends for your targets</span>
pip <span class="nb">install </span>pySigma-backend-splunk pySigma-backend-microsoft365defender

<span class="c"># Generate Splunk SPL</span>
sigma convert <span class="nt">-t</span> splunk <span class="nt">-p</span> splunk_windows ./sigma/windows/ <span class="se">\</span>
  <span class="nt">-o</span> ./platform-translations/splunk-spl/

<span class="c"># Generate Sentinel KQL</span>
sigma convert <span class="nt">-t</span> microsoft365defender <span class="se">\</span>
  <span class="nt">-p</span> microsoft365defender ./sigma/windows/ <span class="se">\</span>
  <span class="nt">-o</span> ./platform-translations/sentinel-kql/
</code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">-p</code> flag applies a <strong>pipeline</strong>, which handles field name mapping between Sigma’s generic field names and the target platform’s schema. This is where most portability failures originate: rules that use <code class="language-plaintext highlighter-rouge">CommandLine</code> in the Sigma detection block translate cleanly to Sentinel’s <code class="language-plaintext highlighter-rouge">ProcessCommandLine</code>, but only if the correct pipeline is applied. A pipeline mismatch produces a syntactically valid query that matches nothing, and that failure is silent unless you have tests in the next stage.</p>

<p><a href="https://grafana.com/blog/how-to-validate-sigma-rules-with-github-actions-for-improved-security-monitoring/">Grafana Labs built a similar approach</a> for teams using Grafana Loki via the <code class="language-plaintext highlighter-rouge">pySigma-backend-loki</code> project, which compiles Sigma rules to LogQL queries. The pattern is identical regardless of backend: the CI job installs the plugin, runs <code class="language-plaintext highlighter-rouge">sigma convert</code>, and checks the output for errors.</p>

<p>The translated outputs are committed to the repository only in the deploy workflow, not in CI. During CI they are written to a temporary directory and inspected. You do not want half-translated rule sets committed on feature branches.</p>

<h2 id="automated-testing">Automated testing</h2>

<p>Validation confirms the rule is syntactically correct. Translation confirms it compiles to the target language. Neither of those things confirms it actually detects what it is supposed to detect.</p>

<p>The minimum viable test is two fixtures per rule: one event the rule must match (true positive) and one event it must not match (false negative catch). Fixtures are stored as JSON files representing log events in the format your SIEM ingests:</p>

<div class="language-json highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="p">{</span><span class="w">
  </span><span class="nl">"EventID"</span><span class="p">:</span><span class="w"> </span><span class="mi">4688</span><span class="p">,</span><span class="w">
  </span><span class="nl">"Image"</span><span class="p">:</span><span class="w"> </span><span class="s2">"C:</span><span class="se">\\</span><span class="s2">Windows</span><span class="se">\\</span><span class="s2">System32</span><span class="se">\\</span><span class="s2">WindowsPowerShell</span><span class="se">\\</span><span class="s2">v1.0</span><span class="se">\\</span><span class="s2">powershell.exe"</span><span class="p">,</span><span class="w">
  </span><span class="nl">"CommandLine"</span><span class="p">:</span><span class="w"> </span><span class="s2">"powershell.exe -EncodedCommand SQBuAHYAbwBrAGUALQBXAGUAYgBSAGUAcQB1AGUAcwB0AA=="</span><span class="p">,</span><span class="w">
  </span><span class="nl">"User"</span><span class="p">:</span><span class="w"> </span><span class="s2">"DOMAIN</span><span class="se">\\</span><span class="s2">attacker"</span><span class="w">
</span><span class="p">}</span><span class="w">
</span></code></pre></div></div>

<p>A <code class="language-plaintext highlighter-rouge">pytest</code> runner loads the compiled SIEM query, executes it against both fixtures, and asserts that the positive matches and the negative does not. <a href="https://bipi.in/blog/detection-engineering-sigma-yaral-tdd">This is the test-driven detection engineering pattern from BIPI</a>, applied with a lightweight query evaluation library.</p>

<p>For teams that want higher-fidelity testing, <a href="https://cert.europa.eu/blog/sigma-unleashed-a-realistic-implementation"><strong>droid</strong></a> is worth examining. Built and open-sourced by CERT-EU under the EUPL license, <code class="language-plaintext highlighter-rouge">droid</code> is a <code class="language-plaintext highlighter-rouge">pySigma</code> wrapper that integrates <strong>Atomic Red Team</strong> directly into the pipeline. Instead of synthetic JSON fixtures, it runs actual ATT&amp;CK technique simulations in a sandbox, captures the resulting telemetry, and then verifies that the corresponding Sigma rule fires on that telemetry. The workflow is driven by a single TOML configuration file that maps rules to their ATT&amp;CK test IDs and target platforms. For multi-SIEM environments, this approach eliminates the category of “the fixture was wrong, not the rule” failures that plague manually crafted test data.</p>

<p>A CI job that enforces test coverage at the pull request level:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="na">name</span><span class="pi">:</span> <span class="s">Detection CI</span>
<span class="na">on</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">pull_request</span><span class="pi">]</span>

<span class="na">jobs</span><span class="pi">:</span>
  <span class="na">validate</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">SigmaHQ/sigma-rules-validator@v1</span>
        <span class="na">with</span><span class="pi">:</span>
          <span class="na">paths</span><span class="pi">:</span> <span class="s">./sigma</span>

  <span class="na">metadata-check</span><span class="pi">:</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">pip install pyyaml &amp;&amp; python scripts/validate_metadata.py</span>

  <span class="na">translate</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">validate</span><span class="pi">,</span> <span class="nv">metadata-check</span><span class="pi">]</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="pi">|</span>
          <span class="s">pip install sigma-cli pySigma-backend-microsoft365defender</span>
          <span class="s">sigma convert -t microsoft365defender -p microsoft365defender \</span>
            <span class="s">./sigma/windows/ -o /tmp/sentinel-kql/</span>

  <span class="na">test</span><span class="pi">:</span>
    <span class="na">needs</span><span class="pi">:</span> <span class="s">translate</span>
    <span class="na">runs-on</span><span class="pi">:</span> <span class="s">ubuntu-latest</span>
    <span class="na">steps</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">uses</span><span class="pi">:</span> <span class="s">actions/checkout@v4</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">pip install pytest pyyaml</span>
      <span class="pi">-</span> <span class="na">run</span><span class="pi">:</span> <span class="s">pytest tests/ -v</span>
</code></pre></div></div>

<p>Merge to <code class="language-plaintext highlighter-rouge">main</code> is blocked until all four jobs pass. The pipeline is the gatekeeper, not the analyst who happens to review the pull request.</p>

<h2 id="deployment-and-shadow-mode">Deployment and shadow mode</h2>

<p>The deploy workflow runs only on merge to <code class="language-plaintext highlighter-rouge">main</code>, not on pull requests. The critical detail is shadow mode: new rules should never go directly to production alerting.</p>

<p>Shadow mode means the rule is active in the SIEM, matches events, and writes those matches to a dedicated index, but it does not create analyst alerts. After a defined observation window (typically seven days), a human reviews the match volume and event samples, then makes an explicit decision to promote, tune, or kill the rule.</p>

<p><a href="https://annoyed.engineer/2025/07/14/the-detection-rebuild-part-2-automating-detection-engineering-without-breaking-the-soc/">This discipline is the difference between detection automation and accelerating failure</a>: the same pipeline that deploys a well-tested rule deploys a poorly-tested one just as fast. Shadow mode is the structural safety rail.</p>

<p>Different SIEM platforms handle shadow mode differently:</p>

<ul>
  <li><strong>Microsoft Sentinel</strong>: deploy rules with <code class="language-plaintext highlighter-rouge">enabled: false</code> and suppress alerts via automation rules on a staging watchlist</li>
  <li><strong>Splunk</strong>: deploy the search without attaching it to a notables policy</li>
  <li><strong>Elastic</strong>: new rules start in <code class="language-plaintext highlighter-rouge">enabled: false</code> state; match results land in a dedicated index for review</li>
</ul>

<p>The mechanism varies. The pattern is the same.</p>

<h2 id="pre-commit-hooks-for-local-validation">Pre-commit hooks for local validation</h2>

<p>The pipeline should not be the first place a rule author learns their YAML is malformed. Pre-commit hooks run locally before a commit is created, catching the cheapest errors at the cheapest moment:</p>

<div class="language-yaml highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># .pre-commit-config.yaml</span>
<span class="na">repos</span><span class="pi">:</span>
  <span class="pi">-</span> <span class="na">repo</span><span class="pi">:</span> <span class="s">https://github.com/adrienverge/yamllint</span>
    <span class="na">rev</span><span class="pi">:</span> <span class="s">v1.35.0</span>
    <span class="na">hooks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">yamllint</span>
        <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">--strict</span><span class="pi">]</span>
  <span class="pi">-</span> <span class="na">repo</span><span class="pi">:</span> <span class="s">local</span>
    <span class="na">hooks</span><span class="pi">:</span>
      <span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">sigma-lint</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">Sigma syntax check</span>
        <span class="na">entry</span><span class="pi">:</span> <span class="s">sigma check</span>
        <span class="na">language</span><span class="pi">:</span> <span class="s">python</span>
        <span class="na">files</span><span class="pi">:</span> <span class="s">\.yml$</span>
        <span class="na">pass_filenames</span><span class="pi">:</span> <span class="no">false</span>
        <span class="na">args</span><span class="pi">:</span> <span class="pi">[</span><span class="nv">./sigma/</span><span class="pi">]</span>
      <span class="pi">-</span> <span class="na">id</span><span class="pi">:</span> <span class="s">metadata-check</span>
        <span class="na">name</span><span class="pi">:</span> <span class="s">Sigma metadata check</span>
        <span class="na">entry</span><span class="pi">:</span> <span class="s">python scripts/validate_metadata.py</span>
        <span class="na">language</span><span class="pi">:</span> <span class="s">python</span>
        <span class="na">files</span><span class="pi">:</span> <span class="s">\.yml$</span>
</code></pre></div></div>

<p>Install with <code class="language-plaintext highlighter-rouge">pip install pre-commit &amp;&amp; pre-commit install</code>. The YAML linter catches indentation errors and trailing whitespace. The <code class="language-plaintext highlighter-rouge">sigma check</code> hook catches detection logic errors. The metadata check catches missing ATT&amp;CK tags. None of these should require a CI round-trip to discover.</p>

<h2 id="attck-coverage-as-a-pipeline-output">ATT&amp;CK coverage as a pipeline output</h2>

<p>Once rules carry MITRE ATT&amp;CK technique IDs in their <code class="language-plaintext highlighter-rouge">tags</code> field (in the format <code class="language-plaintext highlighter-rouge">attack.t1059.001</code>), a script in the pipeline can generate an <a href="https://mitre-attack.github.io/attack-navigator/">ATT&amp;CK Navigator</a> layer file on every merge to <code class="language-plaintext highlighter-rouge">main</code>:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">pathlib</span><span class="p">,</span> <span class="n">yaml</span><span class="p">,</span> <span class="n">json</span>

<span class="n">techniques</span> <span class="o">=</span> <span class="p">{}</span>
<span class="k">for</span> <span class="n">path</span> <span class="ow">in</span> <span class="n">pathlib</span><span class="p">.</span><span class="n">Path</span><span class="p">(</span><span class="s">"sigma"</span><span class="p">).</span><span class="n">rglob</span><span class="p">(</span><span class="s">"*.yml"</span><span class="p">):</span>
    <span class="n">rule</span> <span class="o">=</span> <span class="n">yaml</span><span class="p">.</span><span class="n">safe_load</span><span class="p">(</span><span class="nb">open</span><span class="p">(</span><span class="n">path</span><span class="p">))</span>
    <span class="k">for</span> <span class="n">tag</span> <span class="ow">in</span> <span class="n">rule</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="s">"tags"</span><span class="p">,</span> <span class="p">[]):</span>
        <span class="k">if</span> <span class="n">tag</span><span class="p">.</span><span class="n">startswith</span><span class="p">(</span><span class="s">"attack.t"</span><span class="p">):</span>
            <span class="n">tid</span> <span class="o">=</span> <span class="n">tag</span><span class="p">.</span><span class="n">replace</span><span class="p">(</span><span class="s">"attack."</span><span class="p">,</span> <span class="s">""</span><span class="p">).</span><span class="n">upper</span><span class="p">()</span>
            <span class="n">techniques</span><span class="p">[</span><span class="n">tid</span><span class="p">]</span> <span class="o">=</span> <span class="n">techniques</span><span class="p">.</span><span class="n">get</span><span class="p">(</span><span class="n">tid</span><span class="p">,</span> <span class="mi">0</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>

<span class="n">layer</span> <span class="o">=</span> <span class="p">{</span>
    <span class="s">"name"</span><span class="p">:</span> <span class="s">"Detection Coverage"</span><span class="p">,</span>
    <span class="s">"versions"</span><span class="p">:</span> <span class="p">{</span><span class="s">"attack"</span><span class="p">:</span> <span class="s">"14"</span><span class="p">,</span> <span class="s">"navigator"</span><span class="p">:</span> <span class="s">"4.9"</span><span class="p">,</span> <span class="s">"layer"</span><span class="p">:</span> <span class="s">"4.5"</span><span class="p">},</span>
    <span class="s">"domain"</span><span class="p">:</span> <span class="s">"enterprise-attack"</span><span class="p">,</span>
    <span class="s">"techniques"</span><span class="p">:</span> <span class="p">[</span>
        <span class="p">{</span><span class="s">"techniqueID"</span><span class="p">:</span> <span class="n">tid</span><span class="p">,</span> <span class="s">"score"</span><span class="p">:</span> <span class="n">count</span><span class="p">}</span>
        <span class="k">for</span> <span class="n">tid</span><span class="p">,</span> <span class="n">count</span> <span class="ow">in</span> <span class="n">techniques</span><span class="p">.</span><span class="n">items</span><span class="p">()</span>
    <span class="p">]</span>
<span class="p">}</span>
<span class="n">json</span><span class="p">.</span><span class="n">dump</span><span class="p">(</span><span class="n">layer</span><span class="p">,</span> <span class="nb">open</span><span class="p">(</span><span class="s">"coverage-layer.json"</span><span class="p">,</span> <span class="s">"w"</span><span class="p">),</span> <span class="n">indent</span><span class="o">=</span><span class="mi">2</span><span class="p">)</span>
</code></pre></div></div>

<p>The output is a JSON file loadable directly into ATT&amp;CK Navigator. It updates automatically on every deploy, with no spreadsheet to maintain. The pipeline produces the coverage artifact as a side effect of doing the work, which is the only kind of documentation that stays current.</p>

<p>This is the same principle I described in <a href="https://andreafortuna.org/2026/02/05/24-7-soc/">my post on 24/7 security monitoring for small teams</a>: build a system where discipline is enforced structurally, not through willpower or process documents. A CI pipeline that blocks merges without tests, enforces metadata, and generates coverage reports is a system with structural discipline. A SOC that relies on analysts to remember to update the coverage spreadsheet is one that runs on hope.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What tools do I need to build a CI/CD pipeline for Sigma rules?</strong>
The minimum stack is <code class="language-plaintext highlighter-rouge">sigma-cli</code> for validation and conversion, a Git host with CI support such as GitHub Actions or GitLab CI, and test fixtures in JSON or EVTX format. For advanced testing, <code class="language-plaintext highlighter-rouge">droid</code> by CERT-EU integrates Atomic Red Team to simulate real attack telemetry against your rules. Nothing in this pipeline requires paid tooling.</p>

<p><strong>How do I prevent Sigma rules from breaking portability?</strong>
Avoid vendor-specific field names in the detection block. Use a field normalization layer (ECS, OCSF, or a custom mapping) and validate every translation target in CI. A rule that compiles but silently matches nothing because the target SIEM uses a different field name is worse than a rule that fails loudly in the pipeline.</p>

<p><strong>What is shadow mode and why does it matter?</strong>
Shadow mode means a rule runs in your SIEM and logs its matches but never creates an analyst alert. A new rule should stay in shadow mode for at least seven days before promotion to production, giving you time to measure match volume and tune for false positives before anyone gets paged. It is the safety rail between fast deployment and fast failure.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="Detection Engineering" /><category term="Sigma" /><category term="CI/CD" /><category term="GitHub Actions" /><category term="SOC" /><category term="Threat Detection" /><category term="Blue Team" /><summary type="html"><![CDATA[A step-by-step guide to building a CI/CD pipeline for Sigma detection rules, from repository structure and automated validation to test fixtures, shadow mode, and SIEM deployment.]]></summary></entry><entry><title type="html">iCloud Private Relay and the shrinking horizon of network forensics</title><link href="https://andreafortuna.org/2026/06/19/apple-private-relay-dfir/" rel="alternate" type="text/html" title="iCloud Private Relay and the shrinking horizon of network forensics" /><published>2026-06-19T00:00:00+00:00</published><updated>2026-06-19T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/19/apple-private-relay-dfir</id><content type="html" xml:base="https://andreafortuna.org/2026/06/19/apple-private-relay-dfir/"><![CDATA[<p>The iOS 15 release notes were long. Most analysts skimmed them. Buried in the list, between Focus mode and SharePlay, was a short paragraph about something called <strong>iCloud Private Relay</strong>. No CVE number, no exploit. Just a quiet architectural change that, in practice, punches a significant hole in the kind of passive network monitoring that security teams have relied on for the better part of two decades.</p>

<p><img src="/assets/2026/icloud-private-relay-dfir.jpg" alt="cover" /></p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>iCloud Private Relay is part of the iCloud+ subscription and is available on iOS 15, iPadOS 15, and macOS Monterey (12.0.1) and later.</li>
  <li>It routes Safari traffic and DNS queries through two separate hops operated by different entities, so no single party sees both the user’s IP address and their browsing destination.</li>
  <li>The transport uses <strong>QUIC over UDP/443 with TLS 1.3</strong>, which is opaque to most traditional inspection tools.</li>
  <li>DNS queries are protected via <strong>Oblivious DNS over HTTPS (ODoH)</strong>, eliminating the DNS log as a reliable artifact for Safari traffic.</li>
  <li>Enterprises and MDM-managed devices can block or disable the service; unmanaged personal Apple devices on corporate networks cannot be forced to disable it.</li>
  <li>DFIR teams need to recalibrate: the IP address and DNS log are no longer sufficient anchors for investigations involving Apple devices.</li>
</ul>

<h2 id="how-the-architecture-actually-works">How the architecture actually works</h2>

<p>The design is deliberately adversarial toward any single observer, including Apple itself. When a user with an active iCloud+ subscription enables Private Relay, all Safari traffic and DNS queries stop going directly to the destination.</p>

<p>Instead, the device connects to an <strong>ingress proxy</strong> operated by Apple. This first hop sees the user’s real IP address and network provider, but the requested hostname is encrypted and hidden from it. The ingress proxy passes the request, along with a coarse geohash derived from the user’s real IP, to an <strong>egress proxy</strong> operated by a third-party content delivery network (Akamai, Cloudflare, Fastly, and others are among Apple’s partners). This second hop decrypts the destination hostname and makes the outbound connection, but it only receives geolocation data at roughly city or country level, never the original IP address. The <a href="https://www.apple.com/icloud/docs/iCloud_Private_Relay_Overview_Dec2021.pdf">iCloud Private Relay Overview whitepaper</a> Apple published in December 2021 remains the most complete technical reference for this architecture.</p>

<p>DNS is handled separately through <strong>Oblivious DNS over HTTPS (ODoH)</strong>. Queries are padded and encrypted using Hybrid Public Key Encryption (HPKE) before leaving the device, so the DNS resolver cannot identify the user and the ingress proxy cannot read the queried domain. The <a href="https://support.apple.com/guide/security/welcome/web">Apple Platform Security guide</a> confirms that this coverage extends to all DNS resolution requests from the device, not just those initiated by Safari, making the classic DNS log artifact largely useless for Safari-originated activity.</p>

<p>Transport-wise, the relay uses <strong>QUIC (RFC 9000) with TLS 1.3</strong>, connecting to <code class="language-plaintext highlighter-rouge">mask.icloud.com</code> and <code class="language-plaintext highlighter-rouge">mask-h2.icloud.com</code> on UDP/443. In networks where QUIC is blocked, the client falls back to HTTP/2 CONNECT with the same TLS 1.3 requirement. As SANS Internet Storm Center’s <a href="https://isc.sans.edu/diary/27858">Johannes Ullrich noted in his first hands-on analysis</a>, the TLS client hello for the relay is negotiated with only three cipher suites (AES-128-GCM, AES-256-GCM, CHACHA20-POLY1305) and the ALPN is HTTP/3 only, which makes the relay traffic identifiable at a metadata level even if the content is opaque.</p>

<h2 id="the-dfir-implications-layer-by-layer">The DFIR implications, layer by layer</h2>

<p>The immediate reaction from network defenders when Private Relay appeared was that it was a VPN users did not ask for. The comparison misses architectural differences that change the DFIR calculus.</p>

<p>A traditional VPN tunnels all traffic through a single endpoint and typically routes everything, including non-browser traffic, through the same pipe. Private Relay is scoped: it protects Safari, unencrypted HTTP app traffic, and DNS. The Ookla Speedtest app, streaming applications with their own transport, and any app using HTTPS with its own certificate pinning all make direct connections with the device’s real IP address. The scope is meaningful, but it is not total.</p>

<p>For DFIR, the impact breaks down across several layers:</p>

<p><strong>IP attribution.</strong> The egress IP presented to a web server or to a network perimeter monitoring tool is no longer the user’s IP. Apple publishes the full list of egress ranges at the official feed <a href="https://mask-api.icloud.com/egress-ip-ranges.csv"><code class="language-plaintext highlighter-rouge">mask-api.icloud.com/egress-ip-ranges.csv</code></a>, and most geo-IP databases now annotate these with the <code class="language-plaintext highlighter-rouge">Organization</code> field set to “iCloud Private Relay” and additional boolean fields such as <code class="language-plaintext highlighter-rouge">is_relay</code> or <code class="language-plaintext highlighter-rouge">privacy_proxy</code>. An analyst correlating a suspicious IP to a specific user device will find an Apple relay IP instead. The <a href="https://developer.apple.com/icloud/prepare-your-network-for-icloud-private-relay/">Apple developer guidance for network operators</a> notes that relay IPs may be shared among multiple users in the same region, making per-user IP attribution from network logs unreliable.</p>

<p><strong>DNS logs.</strong> ODoH means that on a network where Private Relay is active and not blocked, DNS queries from Safari do not appear in the local resolver’s logs. If your threat hunting workflow depends on flagging DNS lookups for known-malicious domains as an early detection signal, Private Relay creates a blind spot for any Safari-initiated activity. The <a href="https://www.ren-isac.net/services/publications/alerts/iCloud_Private_Relay.html">REN-ISAC advisory on iCloud Private Relay</a> flagged this directly: “Web traffic, DNS queries, and potentially other traffic are tunneled via QUIC over port 443 with TLS 1.3. This traffic will typically be opaque to network monitoring.”</p>

<p><strong>Proxy and VPN anomaly detection.</strong> Detection rules that alert on connections from anonymous proxy or hosting ranges will catch Private Relay traffic, generating false positives in Apple-heavy environments. Tuning these rules requires knowing Apple’s egress IP space and treating it as a distinct category rather than a generic anonymization service. The <a href="https://blog.cloudflare.com/icloud-private-relay/">Cloudflare writeup on iCloud Private Relay</a> is useful here: it explains how Cloudflare itself had to update its IP categorization to avoid treating Private Relay users as suspicious.</p>

<p><strong>Session correlation.</strong> Relay IP addresses rotate between sessions. They remain stable during a single browsing session, which Apple designed deliberately to allow anti-fraud mechanisms on the server side to function. Across sessions, the same user will appear with different IPs from the Apple egress pool. Any investigation that attempts to reconstruct a user’s browsing history by correlating IP addresses across time will fail unless the analyst controls the endpoint directly.</p>

<h3 id="what-breaks-in-the-security-stack">What breaks in the security stack</h3>

<p>For teams that have built detection around network telemetry, the impact of Private Relay is uneven across tool categories. <strong>Zeek</strong> will still see the QUIC UDP/443 connection to <code class="language-plaintext highlighter-rouge">mask.icloud.com</code>, but the connection log is a black box with no SNI, no DNS context, and an Apple-annotated source IP. The <code class="language-plaintext highlighter-rouge">conn.log</code> and <code class="language-plaintext highlighter-rouge">ssl.log</code> outputs are useful for traffic-volume baselining and connection-count anomaly detection, but useless for destination correlation. <strong>Suricata</strong> rules that match on HTTP hostname or TLS SNI will not fire against relay traffic. Rules that match on the egress IP space can still detect that someone is using Private Relay, but that is a metadata signal, not a useful detection. <strong>NetworkMiner</strong> and similar pcap parsers will produce a host entry for <code class="language-plaintext highlighter-rouge">mask.icloud.com</code> with no children, and analysts expecting to see child connections to the actual destination will be disappointed. <strong>Arkime</strong> and <strong>ntopng</strong> will index the same opaque sessions with similar limitations. None of these tools are broken, but all of them have lost a layer of context that detection engineers often relied on implicitly.</p>

<p>The pragmatic response is to shift detection logic away from “what destination did this IP talk to” and toward “what client is talking to Apple’s relay from inside our network, and at what volume.” A simple <code class="language-plaintext highlighter-rouge">conn.log</code> query that counts relay sessions per source IP per hour will catch misuse, exfiltration patterns, and policy violations, even if every individual session is opaque.</p>

<p>A condensed view of what is and is not visible to the analyst after Private Relay:</p>

<table>
  <thead>
    <tr>
      <th>Data source</th>
      <th>Before Private Relay</th>
      <th>After Private Relay (Safari traffic)</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Client IP attribution</td>
      <td>Direct</td>
      <td>Apple relay IP, coarse city geohash</td>
    </tr>
    <tr>
      <td>DNS for Safari traffic</td>
      <td>Local resolver logs</td>
      <td>Encrypted via ODoH, not visible</td>
    </tr>
    <tr>
      <td>DNS for non-Safari apps</td>
      <td>Local resolver logs</td>
      <td>Local resolver logs (unchanged)</td>
    </tr>
    <tr>
      <td>TLS SNI on the wire</td>
      <td>Visible</td>
      <td>Not visible for relay traffic</td>
    </tr>
    <tr>
      <td>Destination server logs</td>
      <td>Full request</td>
      <td>Full request (unchanged)</td>
    </tr>
    <tr>
      <td>Endpoint browser history</td>
      <td>Visible via Safari and WebKit artifacts</td>
      <td>Visible (unchanged)</td>
    </tr>
    <tr>
      <td>MDM activity logs</td>
      <td>Visible</td>
      <td>Visible (unchanged)</td>
    </tr>
    <tr>
      <td>Application-layer data</td>
      <td>Visible</td>
      <td>Visible (unchanged)</td>
    </tr>
  </tbody>
</table>

<p>The key insight is that the relay moves the opacity boundary from the destination to the network. Server-side and endpoint-side evidence is largely unaffected, and the practical DFIR shift is to anchor investigations there rather than at the IP-and-DNS layer where Private Relay lives.</p>

<h2 id="where-the-relay-still-leaks">Where the relay still leaks</h2>

<p>Private Relay obscures a great deal, but the architecture leaks more than people often assume. A DFIR practitioner who knows where to look still has signals to work with.</p>

<p>The destination server still sees the full unencrypted request once the egress proxy forwards it. Server-side logs, application-layer data, and the actual user agent, cookies, and authentication headers remain untouched by the relay. For a web application that logs user actions, Private Relay changes nothing about the forensic value of those logs. The same applies to any TLS 1.3 session resumption cache the destination maintains, which is a far more reliable identifier of a returning user than the IP address ever was.</p>

<p>On the network side, the TLS handshake still carries a fingerprint. While the relay obscures the destination hostname, JA3 and JA4 fingerprinting can still distinguish a Private Relay connection from a direct connection in some configurations, and the relay client itself has a consistent fingerprint across Apple devices that simplifies traffic identification. The geolocation leak at the egress proxy level also means an investigator with access to logs from both the ingress and egress proxies (which neither party normally has) can correlate sessions. The two-proxy design prevents this in normal operation, but it is a structural property, not a cryptographic guarantee.</p>

<p>Timing and packet size analysis remain feasible for an observer who can see both sides of a connection. A relay adds latency in the 30-80 ms range, and QUIC packet sizes cluster around characteristic values depending on the request type. For most investigations this is too noisy to act on, but for targeted cases it can corroborate other evidence.</p>

<p>Encrypted Client Hello (ECH) deserves a mention in this broader context. Apple deployed ECH support for its own domains around the time macOS Sonoma shipped, and Private Relay’s design is compatible with ECH. As ECH adoption grows, the relay’s role in hiding the destination hostname from the ingress proxy becomes less special: the network sees a connection to a Cloudflare or Akamai front door, and the real destination is hidden inside. Private Relay in a post-ECH world remains useful for IP rotation and geohash separation, but the DNS and SNI hiding are no longer unique to Apple.</p>

<p>A related clarification: Private Relay is operationally separate from Private Cloud Compute, the architecture Apple introduced for Apple Intelligence. Private Cloud Compute routes specific on-device AI computation requests through hardened Apple server nodes with cryptographic attestation and stateless processing. The two services share a privacy brand but are independent. A user enabling Private Relay does not opt into Private Cloud Compute, and vice versa. The DFIR implications are also distinct: Private Cloud Compute is a server-side compute architecture and does not affect network telemetry for the user’s other traffic. Enterprise policy documents sometimes bundle the two together, and the confusion is worth flagging because the operational mitigations are completely different.</p>

<h2 id="what-enterprises-and-mdm-can-still-do">What enterprises and MDM can still do</h2>

<p>Apple was not oblivious to the operational needs of managed environments. The <a href="https://www.apple.com/icloud/docs/iCloud_Private_Relay_Overview_Dec2021.pdf">iCloud Private Relay Overview whitepaper</a> is explicit: enterprises with supervised devices can disable Private Relay entirely via a Mobile Device Management (MDM) configuration profile. A VPN profile installed on the device takes precedence over Private Relay, and the same applies to a Global HTTP Proxy configuration. Any traffic routed through the enterprise VPN or proxy will bypass Private Relay entirely.</p>

<p>For network-level control without MDM, the documented method is DNS-based: configure the local resolver to return NXDOMAIN (or a “no error no answer” response) for <code class="language-plaintext highlighter-rouge">mask.icloud.com</code> and <code class="language-plaintext highlighter-rouge">mask-h2.icloud.com</code>. Users will see a system notification that Private Relay is unavailable on the current network and will be prompted to either disable it or switch networks. Apple explicitly discourages silent drops or DNS timeouts, because these cause noticeable latency on client devices and, frankly, make your network look broken rather than intentionally restrictive.</p>

<p>For unmanaged personal devices on a corporate network, the options are limited to network-level DNS blocking. This is a reasonable posture for regulated environments, and it is worth noting that blocking these two hostnames is a fairly contained intervention. The whitepaper also lists several categories of traffic that Private Relay never touches regardless of settings: cellular carrier services (MMS, XCAP, Visual Voicemail), local network traffic (private IP ranges, mDNS), and any app or protocol that uses its own transport layer rather than Safari’s networking stack.</p>

<h2 id="rethinking-the-evidence-model">Rethinking the evidence model</h2>

<p>A large part of the DFIR community has built its evidence model on the assumption that network telemetry, specifically IP addresses and DNS logs, was a reliable proxy for user identity and intent. Network telemetry was never fully reliable to begin with. NAT, shared Wi-Fi, Tor, and carrier-grade NAT have been degrading the IP-to-user mapping for years. Private Relay brought the erosion into the consumer mainstream, and at a scale the rest of the industry could not ignore.</p>

<p>For investigations involving Apple devices, the practical reorientation is toward endpoint artifacts. Safari’s browsing history, WebKit cache, and download records persist locally and survive Private Relay completely. On macOS, <code class="language-plaintext highlighter-rouge">unified logs</code> and <code class="language-plaintext highlighter-rouge">FSEvents</code> capture application behavior independent of network routing. Mobile forensics workflows that extract Safari’s local databases (the <code class="language-plaintext highlighter-rouge">BrowserState.db</code> and <code class="language-plaintext highlighter-rouge">History.db</code> files within Safari’s application container) remain fully valid, and the same extraction paths used in <a href="https://andreafortuna.org/2026/02/04/ios-forensics-without-jailbreak-a-practical-guide-to-modern-mobile-evidence-acquisition">modern iOS evidence acquisition</a> apply regardless of how the device routed its traffic. On managed devices, MDM can provide application inventory, compliance status, and, in some configurations, granular activity logs that are unaffected by network routing choices.</p>

<p>For network-side detection and threat hunting, the shift is toward <strong>application-layer signals</strong> rather than IP-and-DNS correlation. HTTP/S request patterns visible to a proxy that sits between the egress relay and the destination, anomalies in certificate transparency logs, and behavioral signals from endpoint EDR agents all bypass the opacity introduced by Private Relay. The same shift in mindset applies to <a href="https://andreafortuna.org/2026/04/22/threat-hunting-yara-x">threat hunting with YARA-X</a>: the artifacts worth scanning have always been on disk and in memory, and the network layer was a shortcut, not a substitute.</p>

<p>Private Relay leaves endpoint forensics untouched. What it does break is the lazy workflow that treated network telemetry as ground truth. The shift is overdue, regardless of where one stands on Apple’s privacy stance. And for context, this is just one piece of a broader pattern: <a href="https://andreafortuna.org/2026/03/29/ios-lockdown-mode-forensics">iOS Lockdown Mode and forensic analysis</a>, <a href="https://andreafortuna.org/2024/08/20/secure-by-design-ios-18-s-privacy-evolution-and-its-impact-on-the-dfir">Secure by Design: iOS 18’s privacy evolution and its impact on the DFIR</a>, and the steady erosion of unencrypted-by-default services have all been pushing DFIR in the same direction for years. Private Relay just made it impossible to ignore.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What traffic does iCloud Private Relay actually protect?</strong>
Private Relay protects Safari web browsing and all DNS name resolution requests. It also covers unencrypted HTTP traffic from apps. Other app traffic that uses its own transport (e.g. streaming apps, VoIP) is not routed through the relay.</p>

<p><strong>Can an enterprise block iCloud Private Relay on its network?</strong>
Yes. The recommended method is to return an NXDOMAIN or “no error no answer” response from the local DNS resolver for the hostnames <code class="language-plaintext highlighter-rouge">mask.icloud.com</code> and <code class="language-plaintext highlighter-rouge">mask-h2.icloud.com</code>. Silent packet drops are discouraged by Apple as they cause delays on client devices.</p>

<p><strong>Does iCloud Private Relay prevent all network-based forensic investigation?</strong>
No. It reduces the value of IP-based and DNS-based telemetry, but it does not affect endpoint artifacts, MDM logs, application-layer data, or traffic outside Safari and unencrypted HTTP. DFIR shifts toward endpoint and application-layer sources, but the discipline does not disappear.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="DFIR" /><category term="Apple" /><category term="Privacy" /><category term="Network Forensics" /><category term="iCloud" /><category term="DNS" /><category term="QUIC" /><category term="Incident Response" /><summary type="html"><![CDATA[Apple's iCloud Private Relay reshapes what network defenders can see. For DFIR analysts, the question is what it hides, what it leaves behind, and how to investigate the rest.]]></summary></entry><entry><title type="html">Applying software engineering discipline to threat detection</title><link href="https://andreafortuna.org/2026/06/17/detection-as-code/" rel="alternate" type="text/html" title="Applying software engineering discipline to threat detection" /><published>2026-06-17T00:00:00+00:00</published><updated>2026-06-17T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/17/detection-as-code</id><content type="html" xml:base="https://andreafortuna.org/2026/06/17/detection-as-code/"><![CDATA[<p>At some point, most security teams will have a rule that fires 4,000 times in a single night. Nobody knows when it was changed, who changed it, or what it was supposed to catch in the first place. The post-mortem reveals that someone edited it directly in the SIEM console six weeks earlier, with no documentation, no peer review, and no way to roll back. This is the default state of detection engineering in most organizations. It has a name: tribal knowledge, or more precisely, the absence of any engineering discipline applied to what is, structurally, a software problem.</p>

<p><strong>Detection as Code</strong> is a methodology, not a tool, a vendor category, or a buzzword. It is the answer to that problem.</p>

<p><img src="/assets/2026/detection-as-code.jpg" alt="cover" /></p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>Detection rules are software artifacts: they deserve version control, peer review, automated testing, and a deployment pipeline.</li>
  <li>Sigma is the closest thing the industry has to a vendor-neutral detection language, compiling to Splunk SPL, Sentinel KQL, Elastic EQL, and more.</li>
  <li>A CI/CD pipeline for detections catches logic errors before they reach production, and provides a full audit trail when something goes wrong.</li>
  <li>Detection as Code fails without clean telemetry: version-controlling garbage rules is still garbage, just with a better commit history.</li>
  <li>Starting small works: a handful of high-value rules in Git with basic tests already eliminates the most damaging failure modes.</li>
</ul>

<h2 id="the-gap-that-detection-engineering-refuses-to-acknowledge">The gap that detection engineering refuses to acknowledge</h2>

<p>Compare how your development team ships code with how your detection team ships rules. Developers branch, test, review, and deploy. If something breaks, they revert in seconds. If someone asks what changed, the answer is in the commit history. Your detection team, in all likelihood, clicks a rule into the SIEM console, saves it, and hopes for the best.</p>

<p>The process gap here is not cosmetic. It maps exactly to the same set of problems that software engineering solved decades ago. No version control means no history and no rollback. No tests mean the rule fires on legitimate traffic at 3 AM and nobody knows why. No peer review means a logic error survives because the author was the only person who read the query. No deployment pipeline means “staging” and “production” are indistinguishable concepts. <a href="https://www.rapid7.com/blog/post/dr-scaling-engineering-detection-as-code/">Rapid7 summarized this gap into a blunt comparison</a>: software engineering teams operate from Git with automated validation; detection engineering teams operate from a wiki and a Save button. One of these models scales. The other one collapses under its own weight.</p>

<p>The reason this persists is cultural rather than technical. Detection rules have traditionally been treated as configuration, not code. Configuration lives in a UI. You tune it by feel. You delete it when it gets annoying. The implicit assumption is that detection is an operational task, not an engineering one. Detection as Code challenges that assumption directly, and the discomfort that sometimes follows says everything about how embedded the old model is.</p>

<h2 id="what-detection-as-code-actually-means">What Detection as Code actually means</h2>

<p>The core idea is simple: every detection rule lives in a <strong>version control system</strong> (Git, overwhelmingly), is reviewed before deployment, is validated by automated tests, and is deployed through a pipeline rather than directly from a console. The rule is an artifact. Its history is preserved. Its ownership is explicit. Its behavior is testable.</p>

<p>This sounds obvious in the abstract. In practice it means several things change simultaneously.</p>

<p>Rules need <strong>metadata</strong> to be manageable at scale: author, creation date, last review date, MITRE ATT&amp;CK technique IDs, target data source, expected false positive rate. Without metadata, a repository of 500 rules becomes a mystery archive that nobody wants to touch. With it, a simple script can emit a live coverage heatmap keyed by ATT&amp;CK technique ID, updated on every commit, with no manual spreadsheet to go stale.</p>

<p>Rules need <strong>tests</strong>. A detection without a test is a hope, not a control. The minimum viable test is two fixtures: a malicious event the rule must match, and a benign event it must not match. Pull requests that lack both test cases should be rejected by CI, the same way you would reject application code without unit tests. <a href="https://bipi.in/blog/detection-engineering-sigma-yaral-tdd">This pattern</a> sounds aggressive until you consider the alternative, which is finding out that your detection fires on legitimate admin activity during an actual incident.</p>

<p>Rules need <strong>deployment discipline</strong>. New rules should enter shadow mode for at least seven days: the rule runs and writes matches to a separate index, but creates no analyst alert. After a week you read the hit volume, sample a dozen events, and either tune, promote, or kill the rule. Detections that never leave shadow mode are still better than detections that page an analyst every twelve minutes with garbage. This is a lesson that applies equally to large enterprise SOCs and to the small teams I described in <a href="https://andreafortuna.org/2026/02/05/24-7-soc/">my post on 24/7 security monitoring</a>: the goal is fewer alerts with better context, not more coverage with more noise.</p>

<h2 id="how-sigma-makes-detection-logic-portable">How Sigma makes detection logic portable</h2>

<p>The portability problem in detection engineering is real. Most organizations run more than one security platform. Rules written natively for Splunk are useless in Sentinel. Rules written for Elastic require a rewrite for Chronicle. Multiply this by every SIEM migration you will ever conduct, and the cost of vendor-native rule formats becomes visible.</p>

<p><a href="https://sigmahq.io/docs/guide/about.html"><strong>Sigma</strong></a> is the closest thing the industry has to a solution. Released by Florian Roth in 2017, it defines detection logic in YAML: a <code class="language-plaintext highlighter-rouge">logsource</code> block that describes the data source, a <code class="language-plaintext highlighter-rouge">detection</code> block that describes what to look for, and a <code class="language-plaintext highlighter-rouge">condition</code> that ties them together. Tools like <code class="language-plaintext highlighter-rouge">sigma-cli</code> and <code class="language-plaintext highlighter-rouge">pySigma</code> compile that YAML into Splunk SPL, Microsoft Sentinel KQL, Elastic EQL, IBM QRadar, Chronicle YARA-L, Wazuh rules, and a growing list of other targets. A single Sigma file is the source of truth; the platform-specific query is a build artifact.</p>

<p>The <a href="https://github.com/SigmaHQ/sigma">SigmaHQ community repository on GitHub</a> contains thousands of maintained rules covering techniques across the MITRE ATT&amp;CK matrix, from credential dumping to living-off-the-land execution. The practical value is that you do not start a Detection as Code program from zero: you fork a curated baseline, map it to your environment, add tests, and own the result.</p>

<p>A minimal Sigma rule for detecting PowerShell encoded command execution looks like this in plain terms: logsource is Windows process creation; detection requires <code class="language-plaintext highlighter-rouge">Image</code> ending in <code class="language-plaintext highlighter-rouge">powershell.exe</code> and <code class="language-plaintext highlighter-rouge">CommandLine</code> containing <code class="language-plaintext highlighter-rouge">-enc</code> or <code class="language-plaintext highlighter-rouge">-EncodedCommand</code>; condition is <code class="language-plaintext highlighter-rouge">selection</code>. That single YAML file compiles to a working KQL query targeting <code class="language-plaintext highlighter-rouge">DeviceProcessEvents</code> in Sentinel and a working YARA-L rule targeting <code class="language-plaintext highlighter-rouge">udm.principal.process</code> in Chronicle. The portability is not theoretical. It is the reason Sigma has become, as the documentation now explicitly states, the <em>de facto</em> standard for SIEM-agnostic detection.</p>

<p>One important failure mode worth naming: Sigma rules that use vendor-specific field names in the detection block break portability silently. The rule compiles, the translation succeeds, and the query fires on nothing because the field name does not exist in the target SIEM’s schema. Field name normalization, whether through the OCSF schema, ECS, or your own data model, is not optional if you want portability to work in practice.</p>

<h2 id="yara-and-yara-x-for-content-based-detection">YARA and YARA-X for content-based detection</h2>

<p>Sigma handles log-based detection. <strong>YARA</strong> handles file and memory-based detection: matching binary patterns, strings, and structural characteristics of files rather than log events. The two are complementary, and a mature Detection as Code program uses both.</p>

<p>I covered the <a href="https://andreafortuna.org/2026/04/22/threat-hunting-yara-x/">YARA-X transition in detail earlier this year</a>: the complete Rust rewrite addresses the performance ceiling that classic YARA’s C codebase had hit, particularly on rules with complex regular expressions and nested loops. For Detection as Code specifically, YARA-X brings two important improvements. First, the <code class="language-plaintext highlighter-rouge">yr compile</code> command works cleanly as a CI validation step, catching syntax errors before a rule is deployed to production scanning. Second, the JSON and YAML output formats make integration with automated pipelines practical without fragile text parsing.</p>

<p>YARA rules, like Sigma rules, need metadata and ownership. A rule without a <code class="language-plaintext highlighter-rouge">description</code>, <code class="language-plaintext highlighter-rouge">author</code>, and <code class="language-plaintext highlighter-rouge">reference</code> field is a detection waiting to become orphaned. For malware hunting workflows, the <code class="language-plaintext highlighter-rouge">threat_name</code>, <code class="language-plaintext highlighter-rouge">malware_family</code>, and MITRE technique tags are the difference between a rule library and a pile of patterns. <a href="https://andreafortuna.org/2026/04/22/threat-hunting-yara-x/">YARA-X also fits naturally into threat hunting pipelines</a> alongside Volatility3 and other DFIR tools, where the same rule corpus used in CI can be applied to memory dumps or disk images during incident response.</p>

<h2 id="where-detection-as-code-breaks-down">Where Detection as Code breaks down</h2>

<p>The discipline only works on top of a functioning foundation. Git does not cure bad telemetry. Version-controlling rules that fire on unstructured logs, or that target fields that your parser does not reliably populate, produces a neat repository of rules that detect nothing. Before imposing engineering discipline on detection logic, it is worth confirming that your log sources are complete, that parsing is consistent, and that the fields your rules reference actually exist in your SIEM’s schema.</p>

<p>Ownership is the second failure mode. Detection as Code creates an audit trail for who wrote a rule and who approved it, but it does not automatically create a pager assignment. Rules without a named owner accumulate. They fire. Nobody investigates. After six months the SOC has learned to ignore the alert, and the detection is functionally dead while appearing alive in the repository. Every rule should carry an explicit owner and an explicit escalation path, and that information should be part of the metadata that CI validates, not an afterthought in a comment.</p>

<p>The third failure mode is scope creep in automation. The appeal of CI/CD for detections is real, but the same automation that deploys a well-tested rule can deploy a poorly-tested one just as fast. The solution is not less automation but better gates: require positive and negative test fixtures, enforce review by at least one person who did not write the rule, and run shadow mode for any rule touching high-volume data sources. <a href="https://andreafortuna.org/2026/02/05/24-7-soc/">Teams building 24/7 monitoring capability</a> know the pattern well: the safe automation ladder applies here too. Enrich and validate automatically; promote to production only after human confirmation.</p>

<p>The cultural challenge is perhaps the most stubborn. Analysts who have spent years clicking rules into a console often experience Detection as Code as friction rather than discipline. The fix is demonstrating the value through a concrete incident: show the team the pull request that changed the rule three weeks before it started misfiring. That commit history, that diff, that author name is the argument that no process document can make as effectively.</p>

<h2 id="a-practical-starting-point">A practical starting point</h2>

<p>The good news is that you do not need to boil the ocean to start. A working Detection as Code program can begin with five rules and a single Git repository.</p>

<p>Pick your five highest-value detections, the ones covering your most credible risks and the ones your team actually investigates when they fire. Write each one as a Sigma rule. Add metadata: ATT&amp;CK technique ID, data source, author, last reviewed date. Add a positive test fixture and a negative test fixture. Set up a basic CI pipeline that runs <code class="language-plaintext highlighter-rouge">sigma-cli</code> against every pull request and rejects merges without test coverage. That is version one. It is not impressive. It is sufficient.</p>

<p>From there, the program grows naturally: add YARA rules for your malware detection library, introduce shadow mode tooling for new rules before they go live, connect the rule metadata to an ATT&amp;CK Navigator layer for coverage tracking, and eventually integrate deployment automation that pushes approved rules to your SIEM rather than requiring a manual copy-paste. Each step has compound returns: the coverage map improves, the audit trail extends, the on-call analyst gets better context at 3 AM.</p>

<p><a href="https://www.rapid7.com/blog/post/dr-scaling-engineering-detection-as-code/">Rapid7’s Terraform provider for InsightIDR</a> demonstrates what the mature end of this looks like: detection rules expressed as Terraform resources with inline test cases, deployed through the same IaC pipeline your infrastructure team already uses. Governance happens as a natural side effect of the pull request workflow, not as a separate compliance exercise. Whether or not Terraform fits your environment, the model is instructive: detection logic as a first-class infrastructure artifact, subject to the same rigor as everything else you run in production.</p>

<p>Teams that have followed this path consistently report a 40 to 60 percent reduction in alert volume after six months, faster analyst onboarding because new team members can read the rule history to understand intent, and dramatically smoother SIEM migrations because the detection library is portable rather than locked into a vendor console. The coverage map becomes a credible artifact rather than a fiction. Most importantly, the team stops being afraid to delete bad rules, because the history is preserved and the test fixtures remain.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What is Detection as Code?</strong>
Detection as Code is a methodology that treats threat detection rules as software artifacts, managed through version control, peer review, automated testing, and CI/CD pipelines rather than ad-hoc console changes. The goal is a detection library that is auditable, portable, and maintainable rather than dependent on individual tribal knowledge.</p>

<p><strong>What is Sigma and why does it matter for Detection as Code?</strong>
Sigma is an open, vendor-neutral rule format that lets you write detection logic once and compile it to Splunk SPL, Microsoft Sentinel KQL, Elastic EQL, or other SIEM-specific languages. It is the most practical foundation for a portable, versionable detection library, and the SigmaHQ community repository provides a large baseline of maintained rules to start from.</p>

<p><strong>Do I need a large team to implement Detection as Code?</strong>
No. Even a small security team benefits from Detection as Code. Starting with a handful of high-value rules in a Git repository with basic CI validation already eliminates the most damaging failure modes like rules without ownership, no rollback path, and no audit trail. The overhead is low; the returns are immediate and compound over time.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="Detection Engineering" /><category term="Sigma" /><category term="YARA" /><category term="SOC" /><category term="Threat Detection" /><category term="CI/CD" /><category term="Blue Team" /><summary type="html"><![CDATA[Detection as Code applies software engineering discipline to threat detection, replacing ad-hoc SIEM rule management with versioned, tested, and continuously deployed detection logic.]]></summary></entry><entry><title type="html">FACT attribution, putting a person behind the artifact in digital forensics</title><link href="https://andreafortuna.org/2026/06/15/fact-attribution-framework/" rel="alternate" type="text/html" title="FACT attribution, putting a person behind the artifact in digital forensics" /><published>2026-06-15T00:00:00+00:00</published><updated>2026-06-15T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/15/fact-attribution-framework</id><content type="html" xml:base="https://andreafortuna.org/2026/06/15/fact-attribution-framework/"><![CDATA[<p>Every competent DFIR team can tell you <em>what</em> happened. They can reconstruct the timeline, recover deleted files, map network connections, and produce a forensically sound disk image in under an hour. The problem, one that has quietly embarrassed investigators for decades, is the next sentence. The one that starts with “and therefore, the person responsible was…”</p>

<p><img src="/assets/2026/fact-attribution-framework.jpg" alt="cover" /></p>

<p>That transition, from artifact to accountable human being, has no rigorous standard. It gets handled differently depending on the examiner, the organization, the jurisdiction, and occasionally the mood of whoever is writing the report. That gap is exactly what the <strong>FACT Attribution Framework</strong> (v1.1, published December 2025 on <a href="https://zenodo.org/records/18005597">Zenodo</a>) is designed to close.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>FACT stands for <strong>Forensic</strong> Compliance, <strong>Analyze</strong> Evidence, <strong>Correlate</strong> &amp; Sequence, <strong>Testify</strong> &amp; Transfer Findings, four stages that make up the full attribution lifecycle.</li>
  <li>The framework draws a hard, explicit line between <strong>identification</strong> (device, account, action) and <strong>attribution</strong> (person, accountability), and treats conflating the two as an error, not a shortcut.</li>
  <li>It wraps existing DFIR lifecycles in legal bookends: authority and compliance at the start, reporting and testimony-ready reasoning at the end.</li>
  <li>FACT defines who is <em>authorized</em> to make each type of claim in an investigation, keeping examiner roles clean and conclusions defensible.</li>
  <li>Version 1.1, the current release, reflects review by practitioners, attorneys, analysts, and instructors, and provides the legal and logical architecture that current DFIR processes generate evidence for, without replacing any of them.</li>
</ul>

<h2 id="the-problem-fact-is-solving">The problem FACT is solving</h2>

<p>There is a specific failure mode in digital investigations that almost nobody talks about openly, probably because it is embarrassing. An examiner recovers an artifact, ties it to an account, ties the account to a name, and writes that name into the final report next to the word “perpetrator.” The chain of reasoning in between usually gets summarized as “forensic analysis confirmed.” This works fine until the report reaches a courtroom, an HR panel, or a regulator. At that point, someone asks a simple question: <em>how do you know a specific human being performed that action?</em> And the answer, more often than it should be, is “…because the artifact was on their device.”</p>

<p><a href="https://brettshavers.com/brett-s-blog/entry/your-df-ir-tool-cant-tell-you-who-did-it-fact-tells-you-when-youre-allowed-to">Brett Shavers</a>, the framework’s author, is direct about this: “DF/IR doesn’t usually fail on the technical work. It fails at the exact moment someone gets impatient and confuses activity with identity, and then confuses identity with attribution.” FACT draws a hard line between <strong>identification</strong> (this device, this account, this action, at this time) and <strong>attribution</strong> (this specific person bears responsibility for that action). Those are not the same claim and they do not require the same evidence standard. Every examiner knows this intuitively. Very few frameworks force them to work it explicitly.</p>

<p>The distinction matters more today than it did ten years ago. Shared accounts, cloud-synced credentials, VMs, roaming profiles, and multi-user endpoints have made the device-to-person equation genuinely ambiguous in a large percentage of real investigations. The old mental shortcut, “the file was on their laptop, QED,” is increasingly a liability, both legally and professionally.</p>

<p>A typical insider-leak case makes the gap visible. An employee is accused of exfiltrating a sensitive document. The examiner recovers the file from the workstation, finds a matching copy in a cloud account tied to the corporate credentials, and reconstructs a network connection at 2 AM. The draft report names the employee. The case reaches arbitration, and defense counsel shows that the workstation was shared across a hot-desking rotation, that the corporate SSO cached the credentials on every machine in the pool, and that the cloud account was a team resource. The attribution collapses, and the organization is left with no defensible finding on a genuine incident.</p>

<h2 id="the-four-stages-of-fact">The four stages of FACT</h2>

<p>The framework is built around the acronym that gives it its name. Each letter represents a stage in the attribution lifecycle, and the stages are designed to be sequential and mutually reinforcing.</p>

<p><strong>Forensic Compliance</strong> is the opening stage, and it sets the legal and procedural boundaries for everything that follows. Before a single artifact is examined, FACT requires that the investigator establish the authority under which the investigation is being conducted, validate that evidence acquisition meets applicable legal and jurisdictional requirements, and document the chain of custody from the start. This is not a box-ticking exercise: the stage explicitly determines <em>who is authorized to conduct the investigation</em> and what conclusions they are permitted to reach. An examiner working under a corporate HR mandate has a different authorization envelope than one operating under a court order, and FACT makes that boundary visible.</p>

<p><strong>Analyze Evidence</strong> is the stage most DFIR practitioners are already comfortable with. It is where artifacts are examined, validated, and characterized: file metadata, registry keys, log entries, network telemetry, volatile memory contents. FACT does not prescribe specific tools here. You can use <a href="https://andreafortuna.org/2026/04/16/windows-memory-volatility/">Volatility</a>, <a href="https://andreafortuna.org/2026/04/22/threat-hunting-yara-x/">YARA-X</a>, or any combination of forensic tools appropriate to the case. What FACT adds is a requirement that findings at this stage stay at the identification level, with attribution deferred to the later stages. “This artifact was present on this device at this timestamp” is an identification. “This person created this artifact” is not, yet.</p>

<p><strong>Correlate &amp; Sequence</strong> is where the framework earns its complexity. This stage requires investigators to build the explicit logical bridge between the artifact evidence and the identity claim. Correlation might involve account linkage, biometric authentication records, physical access logs, behavioral patterns, communications metadata, or geolocation data. Sequencing imposes temporal coherence: the claimed attributable actions must fit into a consistent, evidence-supported timeline. For mobile evidence in particular, this is where most attribution claims either solidify or collapse, a challenge anyone who has worked on <a href="https://andreafortuna.org/2026/04/23/android-pattern-of-life-hidden-artifacts-reconstruct-daily-routine/">Android pattern-of-life analysis</a> will recognize immediately. FACT does not tell you what correlation evidence is sufficient; it forces you to state what you have, what it supports, and what its limitations are.</p>

<p><strong>Testify &amp; Transfer Findings</strong> closes the cycle. This is the stage where conclusions must be articulated in a form that can survive scrutiny: legal proceedings, administrative hearings, regulatory reviews, or organizational governance processes. FACT distinguishes between the examiner role (who can state what the evidence shows) and the attribution role (who has the authority and the full evidentiary basis to say who is responsible). Keeping those roles clean is not procedural pedantry. It is what prevents examiners from being cross-examined into corners because they overclaimed their conclusions in the report.</p>

<h2 id="legal-grounding-and-role-separation">Legal grounding and role separation</h2>

<p>What makes FACT structurally different from most existing frameworks is its explicit treatment of authority and role separation throughout the lifecycle. Most DFIR models describe <em>what to do</em>. FACT also describes <em>who is permitted to do what</em>, and what each role is allowed to conclude.</p>

<p>This matters in practice because attribution claims carry legal weight that identification claims do not. An examiner can testify to artifact characteristics without exposing themselves to challenge on the question of intent or identity. An investigator or decision-maker with broader contextual authority can make the attribution claim, provided the underlying examination record supports it. FACT creates a documented handoff between those layers rather than letting them blur together in a single report signed by someone who was only authorized to do the technical work.</p>

<p>The framework also handles the <a href="https://andreafortuna.org/2026/05/05/when-ai-lies-to-its-own-logs-forensic-readiness/">AI-era complications</a> that are starting to appear in DFIR work. When autonomous agents act on behalf of users, or when LLMs generate outputs that get confused with user-generated content, the device-to-person attribution chain becomes even less reliable. A scenario discussed in recent DFIR circles illustrates the problem: an enterprise chatbot with broad system access is manipulated through a long prompt chain to copy a customer database to an external location. The artifacts on disk include a system service account, a scheduled task, and a network connection. The naive reading is that the user whose credentials were in the logs did it. The actual actor was an agent acting on a prompt, and the user is at most negligent about their own prompt hygiene. FACT’s insistence on explicit, documented logical steps from artifact to person makes it a more future-proof methodology than frameworks that assume a simpler attribution environment.</p>

<h2 id="adoption-and-practical-fit">Adoption and practical fit</h2>

<p>FACT v1.0 was released in December 2025 after pre-release review by 36 practitioners spanning law enforcement, corporate DFIR, consulting, academia, and the legal sector. The feedback, according to Shavers, was consistent: existing frameworks do not provide a structured, defensible approach to person-level attribution, and FACT fills that gap without replacing anything already in use. Version 1.1 followed shortly after, incorporating multidisciplinary expert feedback and formalizing the attribution pathway. The full specification is published on Zenodo under a DOI-registered citation, the same record linked in the introduction.</p>

<p>For practitioners, the immediate question is where FACT fits relative to existing process. The answer is: at the edges. FACT does not replace your acquisition workflow, your tool stack, or your incident response playbook. It provides the legal and logical architecture that those processes generate evidence <em>for</em>. Think of it as the framework that answers the question your current lifecycle doesn’t ask: <em>can you defend not just what you found, but who you said did it, and on what authority?</em></p>

<p>Concretely, FACT is designed to sit alongside the established DFIR and evidence-handling guidance that most organizations already follow. <a href="https://nvlpubs.nist.gov/nistpubs/Legacy/SP/nistspecialpublication800-86.pdf">NIST SP 800-86</a> covers integrating forensic techniques into the incident response lifecycle. <a href="https://www.iso.org/standard/44381.html">ISO/IEC 27037</a> prescribes identification, collection, acquisition, and preservation of digital evidence. The older <a href="https://datatracker.ietf.org/doc/html/rfc3227">RFC 3227</a> guidelines on evidence collection and archiving still show up in many IR playbooks. What those documents do not prescribe, and what FACT adds, is the explicit attribution layer and the role separation needed to defend person-level conclusions downstream of the technical work.</p>

<p>For teams that want to adopt FACT, the on-ramp is intentionally low. A practical first step is to map your current investigative workflow against the four stages: identify where authority is established, where identification claims stop, where correlation begins, and where conclusions are documented for handoff. Then define, in writing, who in your organization is authorized to make each type of claim. Most teams complete this exercise in a single working session and immediately surface gaps that previously lived as informal practice.</p>

<p>In environments subject to DORA, NIS2, or sector-specific compliance requirements, the attribution question has stopped being optional. Regulators are starting to ask for more than technical findings. They want to know who was responsible, how that determination was made, and whether the chain of reasoning is defensible. FACT provides exactly that structure. Being free, openly licensed, and DOI-registered on Zenodo also removes the usual barriers to adoption.</p>

<p>It is worth being explicit about what FACT does not do. It does not produce evidence on its own, and it does not substitute for sound forensic acquisition or sound investigative judgment. Where authority structures are unclear, where roles overlap, or where the organization is unwilling to keep identification and attribution separate in the final report, FACT will document those weaknesses rather than resolve them. The framework’s value scales with the rigor of the process around it.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What is the FACT Attribution Framework?</strong></p>

<p>FACT (Forensic Compliance, Analyze Evidence, Correlate &amp; Sequence, Testify &amp; Transfer Findings) is a legally grounded investigative model designed to bridge technical digital evidence and defensible human attribution in DFIR and incident response contexts.</p>

<p><strong>How does FACT differ from existing DFIR frameworks?</strong></p>

<p>Existing frameworks focus on documenting activity. FACT adds a structured, legally accountable pathway from artifact to person, drawing a hard line between identification and attribution and defining who is authorized to make each type of claim.</p>

<p><strong>Who should use the FACT Attribution Framework?</strong></p>

<p>FACT is relevant for digital forensics examiners, incident responders, corporate investigators, law enforcement analysts, and attorneys involved in cases where person-level attribution must be established and defended under scrutiny.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="DFIR" /><category term="Digital Forensics" /><category term="FACT framework" /><category term="attribution" /><category term="legal forensics" /><category term="incident response" /><category term="chain of custody" /><category term="forensic testimony" /><category term="Brett Shavers" /><summary type="html"><![CDATA[The FACT Attribution Framework provides a legally grounded, four-stage methodology to bridge technical digital evidence and human attribution in DFIR investigations.]]></summary><media:thumbnail xmlns:media="http://search.yahoo.com/mrss/" url="https://andreafortuna.org/assets/2026/fact-attribution-framework.jpg" /><media:content medium="image" url="https://andreafortuna.org/assets/2026/fact-attribution-framework.jpg" xmlns:media="http://search.yahoo.com/mrss/" /></entry><entry><title type="html">The macOS Tahoe artifact that tracks every menu selection a user makes</title><link href="https://andreafortuna.org/2026/06/13/macos-tahoe-biome-menuitem/" rel="alternate" type="text/html" title="The macOS Tahoe artifact that tracks every menu selection a user makes" /><published>2026-06-13T00:00:00+00:00</published><updated>2026-06-13T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/13/macos-tahoe-biome-menuitem</id><content type="html" xml:base="https://andreafortuna.org/2026/06/13/macos-tahoe-biome-menuitem/"><![CDATA[<p>Most forensic artifacts tell you what happened. Disk logs record a file deletion. Network captures record a connection. The System.db confirms an app ran. What they almost never tell you is <em>why</em> the user did what they did, or more precisely, that what happened was deliberate rather than accidental. The gap between “this file was deleted” and “the user selected Compress, then Move to Trash, then Empty Trash in an eleven-minute window” is the difference between circumstantial and compelling.</p>

<p><img src="/assets/2026/macos-tahoe-biome-menuitem.jpg" alt="cover" /></p>

<p>With macOS Tahoe 26, Unit 42 researchers at Palo Alto Networks have identified a new artifact that begins to close that gap. It is called <strong>App.MenuItem</strong>, and it is a previously undocumented <a href="https://unit42.paloaltonetworks.com/new-macos-artifact-discovered/">Biome</a> stream that logs the exact menu selections a user makes across the entire operating system.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>macOS Tahoe 26 introduced a new Biome stream, <strong>App.MenuItem</strong>, located at <code class="language-plaintext highlighter-rouge">~/Library/Biome/streams/restricted/App.MenuItem/local</code>.</li>
  <li>The stream records the text of every menu item a user selects, along with a precise timestamp.</li>
  <li>Data is stored in <strong>SEGB-encapsulated protobuf</strong> format, requiring specific tooling to parse, and the open-source <strong>ccl-segb</strong> is currently the practical option.</li>
  <li>Most commercially available DFIR tools do not yet parse this artifact automatically.</li>
  <li>When combined with file system logs, App.MenuItem provides a narrative layer of <strong>user intent</strong> that raw technical artifacts cannot supply on their own.</li>
  <li>The artifact was likely introduced by Apple to support user suggestions or adaptive interface behavior, but its forensic value is substantial.</li>
</ul>

<h2 id="what-apple-biome-is-and-why-it-matters">What Apple Biome is and why it matters</h2>

<p>The <strong>Apple Biome</strong> framework is not new. It has been quietly accumulating forensic significance across macOS and iOS releases, tracking application usage, media consumption, device activity, and behavioral patterns in a structured, timestamped format. If you have spent any time doing <a href="https://andreafortuna.org/2024/09/10/macos-sequoia-and-dfir-what-investigators-need-to-know/">macOS forensics</a>, you have almost certainly encountered Biome streams already, even if only in passing.</p>

<p>Each Biome <strong>stream</strong> represents a specific data category. The framework stores entries in <strong>SEGB</strong> (Segment-Based) file format, which wraps <strong>protobuf</strong>-encoded records with metadata and integrity structures. This is not the most examiner-friendly format in the world, but it is consistent and well-structured once you know how to approach it. The forensic community has been building tooling around it for a while, and the <a href="https://andreafortuna.org/2020/12/07/osx-forensics-a-brief-selection-of-useful-tools/">OSX forensics tool ecosystem</a> has matured considerably over the past few years.</p>

<p>What Apple appears to have added in Tahoe 26 is a stream specifically dedicated to recording <strong>UI-level user interactions</strong>, specifically, the menu items a user selects. The likely intended purpose is adaptive UI, autofill suggestions, or behavioral learning. The forensic side effect is an extremely granular log of what the user chose to do, expressed in plain language, with timestamps.</p>

<h2 id="where-to-find-appmenuitem-and-how-to-parse-it">Where to find App.MenuItem and how to parse it</h2>

<p>The artifact lives at:
~/Library/Biome/streams/restricted/App.MenuItem/local</p>

<p>The <code class="language-plaintext highlighter-rouge">restricted</code> path designation is significant. It signals that this stream is not user-accessible under normal conditions and requires elevated acquisition privileges to export. On a <a href="https://andreafortuna.org/2019/08/15/os-x-forensic-acquisition-a-basic-workflow/">live macOS system acquisition</a>, this means you will need appropriate access before collection. On a disk image, the path is accessible through standard forensic workflows.</p>

<p>Because the file uses <strong>SEGB-encapsulated protobuf</strong>, most current commercial tools will not parse it automatically. Unit 42 confirmed that none of the major commercially available DFIR platforms they tested processed this specific stream at the time of publication. The practical solution is the open-source Python tool <a href="https://github.com/cclgroupltd/ccl-segb"><strong>ccl-segb</strong></a>, developed by CCL Group, which handles the SEGB format and exposes the underlying protobuf records.</p>

<p>The parsing workflow is straightforward:</p>

<ol>
  <li>Export the file from <code class="language-plaintext highlighter-rouge">~/Library/Biome/streams/restricted/App.MenuItem/local</code>.</li>
  <li>Run the ccl-segb CLI against it: <code class="language-plaintext highlighter-rouge">python ccl_segb_cli.py &lt;exportedfilename&gt; &gt; outputfilename.txt</code></li>
  <li>Convert the text output to CSV for easier filtering and timeline correlation using a Python script.</li>
</ol>

<p>The resulting output will contain timestamped menu item strings in plain text, human-readable action labels like “File &gt; Save…”, “Compress ‘stolendata’”, or “Empty Trash”. Each entry carries a precise UTC timestamp.</p>

<h2 id="reconstructing-user-intent-from-the-timeline">Reconstructing user intent from the timeline</h2>

<p>The investigative value of App.MenuItem becomes clearest when you look at what a sequence of entries actually communicates. Unit 42 shared a sample analysis timeline that illustrates this well:</p>

<ul>
  <li><strong>18:32:37</strong> User navigates via Go &gt; Go to Folder… in Finder</li>
  <li><strong>18:36:59</strong> In TextEdit, user selects File &gt; Save…, types “u42validation”</li>
  <li><strong>18:37:54</strong> User highlights a folder named “stolendata” and selects Compress “stolendata”</li>
  <li><strong>18:38:19</strong> User selects Move to Trash</li>
  <li><strong>18:38:41</strong> User interacts with the Dock to select Empty Trash</li>
</ul>

<p>In eleven minutes, that sequence tells a story: navigate to a directory, create or edit a file, compress a folder for likely exfiltration, and then cover the tracks. The folder name appearing verbatim in the Compress entry is particularly useful. It provides direct evidence of the target of the action, not inferred from file system metadata.</p>

<p>This is the difference between a file system artifact telling you “a <code class="language-plaintext highlighter-rouge">.zip</code> file was created at 18:37:54 and a folder entry disappeared” and an artifact telling you “the user deliberately right-clicked a folder named ‘stolendata’ and selected Compress.” The semantic content is radically different, even when the underlying event is identical. Combined with the approaches discussed in <a href="https://andreafortuna.org/2018/02/16/forensic-timeline-creation-my-own-workflow/">forensic timeline creation</a>, App.MenuItem can fill in the human layer that purely technical logs leave blank.</p>

<p>The artifact also captures Copy, Paste, and other UI interactions that leave minimal traces elsewhere. For investigations involving potential anti-forensic activity (where a user may have deliberately minimized their footprint), the menu item log may capture the very actions taken to clean up.</p>

<p>App.MenuItem is not omniscient, however. If a menu action does not include a specific filename in its label (a generic “Open” rather than “Open ‘evidence.pdf’”), you will see the action but not the target. This limitation is real, and the artifact works best as a <strong>corroborating source</strong> alongside file system events, unified logs, and other <a href="https://andreafortuna.org/2026/04/29/living-off-the-orchard-understanding-loobins-macos-attack-techniques/">macOS behavioral artifacts</a>, rather than as a standalone case-builder.</p>

<h2 id="implications-for-dfir-workflows-on-macos">Implications for DFIR workflows on macOS</h2>

<p>Every macOS release adds new artifacts, deprecates old paths, and occasionally restructures something you were quietly relying on. The transition from Sequoia to Tahoe is no exception. This mirrors a broader pattern: Apple keeps tightening privacy controls for users while simultaneously generating richer behavioral telemetry for its own purposes, telemetry that forensic investigators can, when acquisition conditions allow, turn into evidence.</p>

<p>The immediate practical implication is that <strong>examiners working with Tahoe 26 images should verify whether App.MenuItem is present</strong> and, if it is, incorporate it into their standard triage workflow. The parsing step is not complex once ccl-segb is set up, and the potential payoff (a timestamped record of deliberate user actions in plain language) justifies the extra step. It is also worth watching whether <a href="https://andreafortuna.org/2026/03/01/volatility3-top10-issues/">Volatility 3</a> and tools like <strong>mac_apt</strong> add support for this stream in the near term, as macOS memory and disk forensics tooling tends to respond fairly quickly to documented new artifact types.</p>

<p>For investigators who work across both macOS and Windows, the parallel with the recently discovered <a href="https://andreafortuna.org/2026/03/19/windows11-pca-artifact/">Windows 11 PCA artifact</a> is worth noting: both represent OS-level logging that was likely designed for user experience optimization and has turned out to carry significant forensic value. The lesson in both cases is the same: keep checking what the OS is recording about itself.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What is the App.MenuItem artifact in macOS Tahoe 26?</strong></p>

<p>App.MenuItem is a new Biome stream introduced in macOS Tahoe 26 that logs the specific menu items a user selects across the operating system, along with timestamps. It is stored at <code class="language-plaintext highlighter-rouge">~/Library/Biome/streams/restricted/App.MenuItem/local</code>.</p>

<p><strong>How do I parse the App.MenuItem Biome stream?</strong></p>

<p>The file uses SEGB-encapsulated protobuf format. You can extract it using the open-source ccl-segb tool with the command <code class="language-plaintext highlighter-rouge">python ccl_segb_cli.py &lt;exportedfilename&gt; &gt; outputfilename.txt</code>, then convert the output to CSV for analysis.</p>

<p><strong>What forensic value does App.MenuItem provide compared to standard file system logs?</strong></p>

<p>Where file system logs record that a file was deleted, App.MenuItem shows the deliberate sequence of actions, selecting “Move to Trash” then “Empty Trash”, providing the human context and intent behind technical events.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="macOS" /><category term="DFIR" /><category term="Forensics" /><category term="Biome" /><category term="macOS Tahoe" /><summary type="html"><![CDATA[macOS Tahoe 26 introduced a new Biome stream, App.MenuItem, that silently logs every menu selection a user makes, offering DFIR investigators a precise record of user intent.]]></summary></entry><entry><title type="html">Below the OS, UEFI bootkits, firmware implants, and the artifacts Volatility will never find</title><link href="https://andreafortuna.org/2026/06/12/uefi-bootkits/" rel="alternate" type="text/html" title="Below the OS, UEFI bootkits, firmware implants, and the artifacts Volatility will never find" /><published>2026-06-12T00:00:00+00:00</published><updated>2026-06-12T00:00:00+00:00</updated><id>https://andreafortuna.org/2026/06/12/uefi-bootkits</id><content type="html" xml:base="https://andreafortuna.org/2026/06/12/uefi-bootkits/"><![CDATA[<p>The investigation was going well until it stopped making sense. The malware had been removed, the infected system reimaged, the hard drive replaced just to be safe. Two weeks later, the same indicators reappeared on the same machine. It was neither a network reinfection nor a backup restore gone wrong. The machine itself was still compromised, at a layer nobody had checked, in storage that survives disk replacement just as stubbornly.</p>

<p><img src="/assets/2026/uefi-firmware-forensics.jpg" alt="cover" /></p>

<p>This is the scenario that firmware implants and UEFI bootkits are built for: persistence so deep that the normal DFIR playbook of acquiring the disk, imaging the memory, analyzing, remediating, and reimaging simply does not reach it. It is not a new problem. The first UEFI rootkit documented in the wild appeared in 2018. What has changed is that the technique has moved from nation-state novelty to something approaching a tradecraft standard, and the forensic community’s response has not quite kept up.</p>

<h2 id="in-brief">In brief</h2>

<ul>
  <li>UEFI bootkits and firmware implants persist below the operating system, in EFI System Partition boot components or in SPI flash memory on the motherboard itself.</li>
  <li>Known implants include <strong>BlackLotus</strong> (EFI bootkit, bypasses Secure Boot on patched Windows 11), <strong>CosmicStrand</strong> and <strong>MosaicRegressor</strong> (firmware-resident, survives disk replacement), and <strong>FinSpy</strong> bootkit (commercial spyware with pre-OS component).</li>
  <li>Standard DFIR tools (Volatility, disk imaging, EDR agents) are structurally blind to firmware-layer persistence.</li>
  <li>Detection requires a different toolkit: <strong>CHIPSEC</strong> for SPI flash analysis, <strong>UEFITool</strong> for EFI binary inspection, boot log analysis, and PCR value attestation via TPM.</li>
  <li>Remediation is non-trivial: EFI-resident implants may require ESP cleaning and Secure Boot re-enrollment; firmware implants may require a full BIOS reflash or, in extreme cases, hardware replacement.</li>
  <li>Forensic readiness for this threat class requires pre-incident baselines of firmware hashes and PCR values, artifacts you cannot reconstruct after the fact.</li>
</ul>

<h2 id="the-firmware-layer-and-why-it-matters">The firmware layer and why it matters</h2>

<p>Before cataloging what is exploitable, it helps to be precise about the architecture. <strong>UEFI</strong> (Unified Extensible Firmware Interface) replaced legacy BIOS on mainstream hardware from around 2010 onward. It is a complete pre-OS environment with its own networking stack, drivers, a file system (the EFI System Partition, or ESP), and a shell. It runs before any operating system component loads, which means anything it executes inherits an environment where no EDR agent, no AV engine, and no kernel patch guard exist yet.</p>

<p>The boot sequence on a modern UEFI system proceeds roughly like this: UEFI firmware (stored in SPI flash on the motherboard) initializes hardware, reads the ESP on the boot disk, loads the boot manager (<code class="language-plaintext highlighter-rouge">\EFI\Microsoft\Boot\bootmgfw.efi</code> on Windows), which in turn loads the OS loader, which loads the kernel. A bootkit can insert itself at any stage of this chain. The two most relevant levels for practical forensics are:</p>

<ul>
  <li><strong>ESP-resident</strong>: the implant lives on the EFI System Partition as a modified or additional EFI binary. It persists across OS reinstalls if the ESP is not explicitly wiped, and it can survive drive imaging if the ESP is not included in the acquisition scope.</li>
  <li><strong>Firmware-resident</strong>: the implant is written directly to SPI flash on the motherboard. It persists across disk replacement, OS reinstall, and every other remediation step short of physically reflashing the chip.</li>
</ul>

<p>The distinction matters for both detection and remediation. What these two classes have in common is that they are structurally invisible to anything that runs after the firmware has already executed, which includes every tool in a conventional DFIR toolkit.</p>

<h2 id="esp-and-firmware-implants-at-a-glance">ESP and firmware implants at a glance</h2>

<table>
  <thead>
    <tr>
      <th>Dimension</th>
      <th>ESP-resident implant</th>
      <th>Firmware-resident implant</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>Persistence scope</td>
      <td>Survives OS reinstall if ESP is not wiped</td>
      <td>Survives OS reinstall and disk replacement</td>
    </tr>
    <tr>
      <td>Typical storage location</td>
      <td>EFI System Partition (FAT32)</td>
      <td>SPI flash on motherboard</td>
    </tr>
    <tr>
      <td>Typical artifacts</td>
      <td>Unexpected EFI binaries, modified boot files, anomalous ESP paths</td>
      <td>Modified DXE modules, altered firmware volumes, SPI write-protection anomalies</td>
    </tr>
    <tr>
      <td>Visibility from endpoint tools</td>
      <td>Low, usually visible only with targeted ESP acquisition</td>
      <td>Very low, usually invisible to OS-level telemetry</td>
    </tr>
    <tr>
      <td>Detection approach</td>
      <td>ESP imaging, hash/signature checks, Secure Boot and DBX validation</td>
      <td>CHIPSEC checks, SPI dump diff against baseline, firmware module analysis</td>
    </tr>
    <tr>
      <td>Remediation complexity</td>
      <td>Medium: clean ESP and rebuild trusted boot chain</td>
      <td>High: reflash, verify integrity, and in some cases replace hardware</td>
    </tr>
  </tbody>
</table>

<h2 id="known-implants-in-the-wild">Known implants in the wild</h2>

<p>The forensic understanding of UEFI-level threats is not theoretical. Several real-world implants have been analyzed in enough detail to guide forensic work.</p>

<p><strong>BlackLotus</strong> is the most significant recent case, first publicly documented by <a href="https://www.welivesecurity.com/en/eset-research/blacklotus-uefi-bootkit-analysis/">ESET in March 2023</a>. It is a UEFI bootkit sold as a crimeware product (not exclusively nation-state) that can bypass Secure Boot on fully patched Windows 11 systems by exploiting a vulnerability in the Windows Boot Manager (CVE-2022-21894, also known as <strong>Baton Drop</strong>). BlackLotus installs a malicious EFI binary to the ESP, disables kernel protections including HVCI and Windows Defender, and deploys a kernel driver and a user-mode HTTP downloader. Even after Microsoft patched the underlying boot manager vulnerability, systems with a valid but vulnerable bootloader version still installed in their Secure Boot database remained exploitable, because the Secure Boot revocation list (the UEFI Forbidden Signature Database, or DBX) was not automatically updated.</p>

<p>The forensic artifacts of a BlackLotus infection on the ESP are reasonably concrete:</p>

<div class="language-text highlighter-rouge"><div class="highlight"><pre class="highlight"><code>\EFI\Microsoft\Boot
├── bootmgfw.efi   # legitimate, but may be replaced
├── grubx64.efi    # BlackLotus drops this as its loader
├── winload.efi    # may be patched
└── system32\drivers
  └── [randomized].sys   # kernel driver
</code></pre></div></div>

<p>ESET documented the directory <code class="language-plaintext highlighter-rouge">\EFI\Microsoft\Boot\system32\</code> as a BlackLotus artifact on the ESP, a path that has no legitimate reason to exist in a standard Windows installation.</p>

<p><strong>CosmicStrand</strong> is an order of magnitude more serious. <a href="https://securelist.com/cosmicstrand-uefi-firmware-rootkit/106973/">Kaspersky’s analysis</a> published in 2022 describes a UEFI firmware rootkit attributed to a Chinese-speaking threat actor, found embedded in the firmware images of consumer-grade ASUS and Gigabyte motherboards. CosmicStrand hooks the CSMCORE DXE driver (a UEFI component) to intercept the Windows boot process, injects a shellcode kernel patch, and ultimately deploys a user-mode agent, all before Windows has loaded a single driver. From the OS’s perspective, the system looks clean. The implant is in the SPI flash chip soldered to the motherboard.</p>

<p><strong>MosaicRegressor</strong> (attributed to APT41, documented by <a href="https://securelist.com/mosaicregressor-lurking-in-the-shadows-of-uefi/98017/">Kaspersky in 2020</a>) used a modified version of the leaked <strong>Hacking Team UEFI implant</strong> source code, embedded in the UEFI firmware of victim laptops. It was found on systems used by organizations connected to North Korea-focused research. The infection vector involved physical access or supply-chain compromise. The implant drops a downloader component during the boot process.</p>

<p><strong>MoonBounce</strong> (<a href="https://securelist.com/moonbounce-the-dark-side-of-uefi-firmware/105468/">Kaspersky, 2022</a>), attributed to APT41, went further than any of its predecessors by targeting the <strong>CORE_DXE</strong> component of the firmware, a component that is mapped into virtual memory when the OS starts, and injecting shellcode there. The injection is clean enough that the surrounding legitimate code still functions normally. MoonBounce left no files on disk; the entire attack chain ran from firmware through injected code in the kernel’s own memory.</p>

<p><strong>FinSpy</strong>, the commercial spyware sold by FinFisher, was found in 2021 to include a UEFI bootkit component. Its <a href="https://www.welivesecurity.com/2021/05/21/no-longer-safe-finspy-uefi-bootkit/">ESET analysis</a> documented infection of the Windows Boot Manager with a component that loaded the FinSpy trojan before the OS kernel initialized. This was significant because it confirmed that commercial surveillance vendors, beyond nation-state operators, had incorporated firmware-level persistence.</p>

<h2 id="the-forensic-toolkit-below-volatility">The forensic toolkit below Volatility</h2>

<p><strong>Volatility does not analyze UEFI firmware.</strong> Memory forensics tools analyze RAM, which is populated after the firmware has already handed off execution. By the time Volatility sees anything, a firmware implant has long since done its work.</p>

<p>If you are planning a workflow that mixes memory and firmware analysis, this boundary is worth revisiting alongside my notes on <a href="https://andreafortuna.org/2026/03/01/ten-problems-every-volatility2-analyst-will-hit-when-migrating-to-volatility3/">Volatility migration pitfalls</a> and <a href="https://andreafortuna.org/2026/04/16/from-ram-to-revelation-how-windows-manages-memory-and-how-volatility-reads-it/">how Windows memory structures shape forensic visibility</a>.</p>

<p>The toolkit for this threat class is different.</p>

<p><strong>CHIPSEC</strong> (<a href="https://github.com/chipsec/chipsec">github.com/chipsec/chipsec</a>) is the primary open-source framework for UEFI security analysis, maintained by Intel. It can read SPI flash contents, check write-protection status, verify firmware integrity against known-good baselines, and detect a range of UEFI vulnerability classes. On a live Windows system:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Install and run CHIPSEC from PowerShell (requires admin)</span><span class="w">
</span><span class="n">python</span><span class="w"> </span><span class="nx">chipsec_main.py</span><span class="w"> </span><span class="nt">-m</span><span class="w"> </span><span class="nx">common.bios_wp</span><span class="w">
</span><span class="c"># Checks whether SPI flash write protection is enabled</span><span class="w">
</span><span class="c"># A disabled write protection is a prerequisite for most firmware implants</span><span class="w">

</span><span class="n">python</span><span class="w"> </span><span class="nx">chipsec_main.py</span><span class="w"> </span><span class="nt">-m</span><span class="w"> </span><span class="nx">common.uefi.access_uefispec</span><span class="w">
</span><span class="c"># Checks for UEFI variables with suspicious permissions</span><span class="w">

</span><span class="c"># Dump the full SPI flash to a file for offline analysis</span><span class="w">
</span><span class="n">python</span><span class="w"> </span><span class="nx">chipsec_util.py</span><span class="w"> </span><span class="nx">spi</span><span class="w"> </span><span class="nx">dump</span><span class="w"> </span><span class="nx">firmware.bin</span><span class="w">
</span></code></pre></div></div>

<p>The <code class="language-plaintext highlighter-rouge">firmware.bin</code> dump can then be analyzed offline. A meaningful analysis requires a <strong>known-good baseline</strong> of the same motherboard model and firmware version, something you need to have collected before the incident. This is one of the fundamental constraints of firmware forensics: unlike memory where you can reason about anomalies without a baseline, firmware analysis almost always requires a reference image to compare against.</p>

<p><strong>UEFITool</strong> (<a href="https://github.com/LongSoft/UEFITool">github.com/LongSoft/UEFITool</a>) parses UEFI firmware images and EFI binaries into DXE modules, PE images, and volumes. It can extract individual modules from a firmware dump for further analysis. Combined with a disassembler or Ghidra, it becomes the primary path for analyzing suspected firmware modifications:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Extract all PE32 images from a firmware dump using UEFIExtract (CLI companion)</span>
./UEFIExtract firmware.bin all

<span class="c"># The output directory structure mirrors the firmware layout</span>
<span class="c"># Each extracted PE can be hashed and compared against vendor baselines</span>
<span class="c"># or submitted to static analysis</span>
</code></pre></div></div>

<p>For ESP-level analysis, the tools are closer to conventional DFIR work. The ESP is a FAT32 partition that is straightforward to image with standard tools. What requires attention is ensuring it is actually included in the acquisition:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># On Linux, identify the ESP and image it specifically</span>
lsblk <span class="nt">-o</span> NAME,PARTTYPE,MOUNTPOINT
<span class="c"># Look for partition type EFI System (UUID c12a7328-f81f-11d2-ba4b-00a0c93ec93b)</span>

<span class="c"># Image the ESP directly</span>
<span class="nb">dd </span><span class="k">if</span><span class="o">=</span>/dev/sda1 <span class="nv">of</span><span class="o">=</span>esp.img <span class="nv">bs</span><span class="o">=</span>4M <span class="nv">status</span><span class="o">=</span>progress

<span class="c"># Mount read-only for examination</span>
<span class="nb">mkdir</span> /mnt/esp
mount <span class="nt">-o</span> ro,loop esp.img /mnt/esp
<span class="nb">ls</span> <span class="nt">-la</span> /mnt/esp/EFI/
</code></pre></div></div>

<p>Once mounted, unexpected directories or EFI binaries can be identified. The expected contents of a clean Windows ESP are well-documented. Anything in <code class="language-plaintext highlighter-rouge">\EFI\Microsoft\Boot\</code> that is not in the standard Windows boot file list warrants examination. File timestamps on the ESP can be informative, though they can also be manipulated.</p>

<p><strong>Authenticode analysis</strong> of EFI binaries on the ESP can reveal unsigned or unusually-signed components:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c1"># Using pefile to check EFI binary signatures
</span><span class="kn">import</span> <span class="nn">pefile</span>
<span class="kn">import</span> <span class="nn">hashlib</span>

<span class="n">efi_path</span> <span class="o">=</span> <span class="s">"/mnt/esp/EFI/Microsoft/Boot/grubx64.efi"</span>
<span class="n">pe</span> <span class="o">=</span> <span class="n">pefile</span><span class="p">.</span><span class="n">PE</span><span class="p">(</span><span class="n">efi_path</span><span class="p">)</span>

<span class="c1"># Check for SECURITY_DIRECTORY (Authenticode signature)
</span><span class="k">if</span> <span class="nb">hasattr</span><span class="p">(</span><span class="n">pe</span><span class="p">,</span> <span class="s">'DIRECTORY_ENTRY_SECURITY'</span><span class="p">):</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"Signed binary"</span><span class="p">)</span>
    <span class="c1"># Extract and verify the certificate chain
</span><span class="k">else</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="s">"UNSIGNED - flag for review"</span><span class="p">)</span>

<span class="c1"># Hash for comparison against known-good databases
</span><span class="k">with</span> <span class="nb">open</span><span class="p">(</span><span class="n">efi_path</span><span class="p">,</span> <span class="s">'rb'</span><span class="p">)</span> <span class="k">as</span> <span class="n">f</span><span class="p">:</span>
    <span class="n">sha256</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha256</span><span class="p">(</span><span class="n">f</span><span class="p">.</span><span class="n">read</span><span class="p">()).</span><span class="n">hexdigest</span><span class="p">()</span>
<span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"SHA256: </span><span class="si">{</span><span class="n">sha256</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>

<p><strong>TPM PCR values</strong> are the most powerful forensic signal for detecting pre-OS tampering when they are collected in advance. The TPM’s Platform Configuration Registers record measurements of every component in the boot chain: firmware, boot manager, OS loader. If an implant modified any of these components, the PCR values change. Comparing current PCR values against a trusted baseline from initial enrollment is definitive evidence of boot chain tampering.</p>

<p>On Windows, BitLocker stores a PCR profile at enrollment time. On Linux, <code class="language-plaintext highlighter-rouge">tpm2-tools</code> provides access:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Read current PCR values</span>
tpm2_pcrread sha256

<span class="c"># PCR 0: UEFI firmware</span>
<span class="c"># PCR 2: UEFI firmware data</span>
<span class="c"># PCR 4: Boot manager (MBR or UEFI boot application)</span>
<span class="c"># PCR 7: Secure Boot policy</span>
<span class="c"># PCR 8-9: GRUB measurements (if applicable)</span>

<span class="c"># Any deviation from a known-good baseline in PCR 0, 4, or 7</span>
<span class="c"># is a strong indicator of boot chain interference</span>
</code></pre></div></div>

<p>The forensic limitation: if you do not have a pre-incident PCR baseline, the current values still tell you the state of the boot chain today, but you cannot say definitively when tampering occurred or what the original values were.</p>

<h2 id="secure-boot-limits-and-caveats">Secure Boot limits and caveats</h2>

<p><strong>Secure Boot</strong> is the primary defense against ESP-resident bootkits. When properly configured, it requires every EFI binary in the boot chain to be signed by a certificate in the UEFI Signature Database (DB). A bootkit binary that is not signed, or whose signing certificate is not trusted, will fail to load.</p>

<p>BlackLotus broke this model not by defeating the cryptographic verification, but by exploiting the fact that the Secure Boot revocation process is slow and optional. The Windows Boot Manager binary it exploited (CVE-2022-21894) was patched, but the vulnerable version remained in the DB as trusted. Booting from a vulnerable but signed bootloader allowed the bootkit to load before Windows could enforce any of its own integrity checks.</p>

<p>The forensic implication is that Secure Boot status is necessary but not sufficient evidence of a clean boot chain:</p>

<div class="language-powershell highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c"># Check Secure Boot status on Windows</span><span class="w">
</span><span class="n">Confirm-SecureBootUEFI</span><span class="w">

</span><span class="c"># Check which certificates are enrolled in DB, DBX, KEK, PK</span><span class="w">
</span><span class="c"># Using PowerShell with Get-SecureBootPolicy or native UEFI variable access</span><span class="w">
</span><span class="p">[</span><span class="n">System.Text.Encoding</span><span class="p">]::</span><span class="n">ASCII.GetString</span><span class="p">(</span><span class="w">
    </span><span class="p">(</span><span class="n">Get-SecureBootUEFI</span><span class="w"> </span><span class="nx">db</span><span class="p">)</span><span class="o">.</span><span class="nf">bytes</span><span class="w">
</span><span class="p">)</span><span class="w"> </span><span class="o">|</span><span class="w"> </span><span class="n">Select-String</span><span class="w"> </span><span class="s2">"Microsoft"</span><span class="w">

</span><span class="c"># Also check DBX (revocation list) - an outdated DBX is a vulnerability</span><span class="w">
</span><span class="c"># Microsoft periodically publishes updated DBX through Windows Update</span><span class="w">
</span><span class="c"># Compare the current DBX hash against Microsoft's published values:</span><span class="w">
</span><span class="c"># https://uefi.org/revocationlistfile</span><span class="w">
</span></code></pre></div></div>

<p>An outdated DBX combined with a Secure Boot status of “Enabled” gives a false sense of security. This is exactly the configuration that BlackLotus exploits in the wild.</p>

<h2 id="what-remediation-actually-requires">What remediation actually requires</h2>

<p>Once a firmware-level implant is confirmed, the remediation path depends on where exactly the implant lives.</p>

<p>For <strong>ESP-resident implants</strong> (BlackLotus class), the steps are:</p>
<ol>
  <li>Image the ESP before any remediation, for evidentiary preservation</li>
  <li>Boot from external trusted media (not the compromised disk)</li>
  <li>Wipe and reformat the ESP</li>
  <li>Reinstall Windows Boot Manager using <code class="language-plaintext highlighter-rouge">bootrec /fixboot</code> or a Windows recovery environment</li>
  <li>Update the DBX to the latest Microsoft-published version</li>
  <li>Re-enroll Secure Boot certificates from a known-good state</li>
</ol>

<p>For <strong>firmware-resident implants</strong> (CosmicStrand/MosaicRegressor class), the options narrow considerably:</p>
<ol>
  <li>Obtain the latest clean firmware image from the motherboard vendor for the exact board revision</li>
  <li>Flash using a hardware SPI programmer if the board is unbootable, or via vendor tools (ASUS EZ Flash, Gigabyte Q-Flash) from a trusted USB drive</li>
  <li>Verify the flashed image hash against the vendor’s published checksum</li>
  <li>In cases of confirmed nation-state-grade implants in production systems, hardware replacement is often the operationally cleaner choice because reflashing cannot guarantee the absence of implants in components beyond the main UEFI flash (Embedded Controller firmware, NIC firmware, drive firmware)</li>
</ol>

<p>The article on <a href="https://andreafortuna.org/2026/04/15/why-dfir-teams-need-to-look-beyond-the-mbr-when-analyzing-modern-wipers/">wiper disk evolution</a> noted that each generation of destructive malware removes a recovery option. The same logic applies in reverse here: each layer of firmware the attacker reaches is a layer where the defender’s standard tools provide no visibility.</p>

<h2 id="common-false-positives-in-firmware-triage">Common false positives in firmware triage</h2>

<p>Firmware triage has a high false-positive rate when baseline discipline is weak. The most common cases are:</p>

<ul>
  <li><strong>Legitimate firmware updates</strong> that alter DXE modules and hashes without any malicious activity.</li>
  <li><strong>Normal OEM variance</strong> across board revisions that causes mismatches when analysts compare against the wrong reference image.</li>
  <li><strong>Secure Boot key rotation</strong> and <strong>DBX updates</strong> that legitimately change boot measurements and PCR values.</li>
  <li><strong>Dual-boot or recovery tooling</strong> that introduces additional EFI binaries in the ESP.</li>
</ul>

<p>The practical mitigation is procedural: tie every finding to the exact hardware revision, exact firmware version, and a trusted baseline captured for that specific asset class. Without that context, firmware anomalies are easy to overstate.</p>

<h2 id="the-first-60-minutes-of-firmware-aware-triage">The first 60 minutes of firmware-aware triage</h2>

<p>When firmware persistence is plausible, the initial response window is decisive. A practical workflow for the first hour is:</p>

<ol>
  <li>Isolate the host from the network and preserve power state decisions in the case log.</li>
  <li>Capture volatile context that may disappear after reboot, including current boot configuration, Secure Boot state, and event logs tied to boot integrity.</li>
  <li>Acquire the full disk with explicit ESP inclusion, then acquire a dedicated ESP image for fast triage.</li>
  <li>Hash all EFI binaries in <code class="language-plaintext highlighter-rouge">\EFI\Microsoft\Boot\</code> and compare against known-good baselines for that OS build.</li>
  <li>Validate Secure Boot material, including DB and DBX currency, and record the exact firmware version and motherboard revision.</li>
  <li>Run CHIPSEC checks focused on SPI write protections and UEFI variable access controls.</li>
  <li>If suspicion remains high, schedule an SPI flash dump and offline module comparison against a clean firmware reference.</li>
</ol>

<p>This sequence does not replace deep analysis. It reduces the chance of losing decisive evidence while the case is still fluid.</p>

<h2 id="forensic-readiness-for-a-threat-most-teams-are-not-ready-for">Forensic readiness for a threat most teams are not ready for</h2>

<p>The operational reality of firmware forensics is that it is almost entirely reactive to decisions made before the incident. The difference between a thorough firmware investigation and an investigation that produces a shrug depends almost entirely on whether someone collected firmware baselines, TPM PCR values, and ESP snapshots before the attack.</p>

<p>Most organizations have not done this. The threat intelligence community documented CosmicStrand in 2022, MoonBounce in 2022, BlackLotus in 2023, and still the vast majority of enterprise IR teams have no firmware baseline collection in their standard asset onboarding process. Part of this is tooling immaturity: CHIPSEC is powerful but not a polished enterprise product. Part of it is a threat model problem: firmware implants have historically been associated with nation-state APTs targeting high-value individuals, not broad-based ransomware campaigns. That association is eroding. BlackLotus was sold on criminal forums.</p>

<p>The operational gap mirrors a broader readiness problem discussed in <a href="https://andreafortuna.org/2026/05/11/dfir-never-sleeps-what-always-on-incident-response-actually-demands-from-analysts/">DFIR always on</a>: teams often optimize for speed during incidents, but underinvest in baseline collection that makes deep investigations possible.</p>

<p>A minimal firmware forensic readiness posture looks like this:</p>

<ul>
  <li><strong>At asset enrollment</strong>: run <code class="language-plaintext highlighter-rouge">chipsec_util.py spi dump</code> on each new system and store the resulting firmware image with the asset record. Hash it. Note the motherboard model and firmware version.</li>
  <li><strong>Continuously</strong>: monitor DBX update status as part of patch management. An outdated DBX is a concrete, measurable risk.</li>
  <li><strong>On critical systems</strong>: enable measured boot and record TPM PCR baselines in a trusted attestation store. Remote attestation via TPM 2.0 allows continuous monitoring of boot chain integrity without manual forensic intervention.</li>
  <li><strong>On incident</strong>: always include the ESP in forensic acquisition scope. “Full disk image” must mean the full disk, including the FAT32 partition that your imaging tool might skip if not explicitly configured.</li>
</ul>

<p>The field of firmware forensics has been niche by necessity: the tools are harder to use, the hardware is less forgiving than RAM or disk, and the evidence is often only interpretable against a baseline that was never collected. As bootkits move down-market and the gap between nation-state TTPs and criminal tooling continues to shrink, treating the firmware layer as out-of-scope in DFIR work stops being pragmatism and becomes a genuine operational gap.</p>

<h2 id="faq">FAQ</h2>

<p><strong>What is a UEFI bootkit and how does it differ from a traditional rootkit?</strong>
A UEFI bootkit persists in firmware or EFI System Partition components below the operating system, surviving OS reinstallation, hard drive replacement, and conventional antivirus scans. Traditional rootkits operate within the OS kernel or userspace and are removed by reinstalling the system.</p>

<p><strong>Can a UEFI bootkit survive a full disk wipe or OS reinstallation?</strong>
Yes. Firmware-resident implants like CosmicStrand or MosaicRegressor are stored in SPI flash memory on the motherboard and survive disk replacement entirely. EFI System Partition bootkits like BlackLotus can survive OS reinstallation if the ESP is not wiped and Secure Boot is bypassed.</p>

<p><strong>What tools can detect UEFI firmware tampering during incident response?</strong>
CHIPSEC is the primary open-source tool for firmware integrity analysis, capable of detecting write-protection bypass and unauthorized firmware modifications. For EFI binaries, tools like UEFITool, efifs, and Binwalk support parsing and extraction. Volatility 3 does not analyze firmware directly.</p>]]></content><author><name>Andrea Fortuna</name><email>andrea@andreafortuna.org</email></author><category term="Security" /><category term="DFIR" /><category term="Firmware Security" /><category term="UEFI" /><category term="Bootkits" /><category term="Threat Hunting" /><category term="Incident Response" /><summary type="html"><![CDATA[When attackers move below the operating system, into UEFI firmware and boot components, most standard DFIR tools become blind. Here is what survives, where to look, and what the known implants left behind.]]></summary></entry></feed>