cran-json-processor
Status: Live Source: CRAN via the crandb mirror Type:
json(crandb recent-releases feed + per-package DESCRIPTION JSON + source tarball fetch) Source slug:cranSchedule: Runs hourly (at minute 10) (cron(10 * * * ? *)).
Overview
Continuously watches CRAN (the Comprehensive R Archive Network) for new and
updated R packages and flags malicious publishes. Each run fetches the crandb
recent-releases feed (crandb.r-pkg.org/-/pkgreleases?limit=N, newest-first
after sorting), so only packages released since the previous run are processed.
Duplicate package names within a run are skipped. When the recent feed is
unavailable, the processor falls back to crandb.r-pkg.org/-/latest (the latest
version of every package), bounded by --limit.
🛑 Broken — the feed request returns the OLDEST releases, so this processor is permanently stuck in 1991.
crandb.r-pkg.org/-/pkgreleases?limit=Nis a CouchDB view and defaults to ascending order:?limit=200returns the 200 earliest release events in CRAN history (verified 2026-08-06: first1991-11-26, last2000-07-02).internal/cran/api.go:124sorts descending, but only within that ancient window, sowalkReleasesnever sees a modern release.The consequences are visible in production:
- The saved watermark is
2002-07-03T12:19:53+00:00— the newest date in the--forcewindow (limit=1000). Every later run fetches the same pre-2002 events, finds them all<= watermark, skips every one, and returnsmaxDate == watermark, so the tracker is never bumped.BulkDataDumpTracker.cran-recent.lastProcessedAthas not moved since 2026-06-17 despite an hourly schedule.- Only 191
PackageVersionrows and 10CVEMetadatarows exist, all written in one ~2-minute burst on 2026-06-17.- All 10 minted advisories are ancient, well-established, legitimate CRAN packages —
sm,pastecs,e1071,sn,RMySQL,coda,relimp,quantreg,Rmpi,car— i.e. a 100% false-positive rate on a corpus of 1990s R code whoseconfigurescripts legitimately callsystem().Fix: append
&descending=trueto thepkgreleasesrequest (internal/cran/api.go:100). Verified:?limit=5&descending=truereturns2026-08-06T06:00:08+00:00first. The watermark must then be reset (it is 24 years in the past) and the 10 false-positive advisories retracted.
For every candidate package it fetches the DESCRIPTION document
(crandb.r-pkg.org/<name>) and gathers:
- Package metadata — version, homepage (first URL from the
URLfield), repository (github URL fromBugReports/URL), author, maintainer (name + email parsed from theMaintainer "Name <email>"field), license, andDate/Publication. - Install-time scripts — the source tarball’s
configure/configure.win/cleanupshell scripts andsrc/Makevars, which execute atR CMD INSTALLtime, the classic CRAN supply-chain execution vector. These feed the install-script detectors. - R sources — DESCRIPTION +
.Rfiles +NAMESPACE(bounded ~512 KB) concatenated for deep static detection, including.onLoad/.onAttachhooks that run at library-load. - Upstream GitHub repo (only when the package’s
URL/BugReportsis agithub.comproject) — repository, contributors, and license, persisted GitHub-first in its own committed transaction.
The source tarball is downloaded from
cran.r-project.org/src/contrib/<name>_<version>.tar.gz. Archived/404 tarballs
are skipped best-effort — the PackageVersion is still recorded from metadata.
Records produced
| Condition | Records |
|---|---|
| Every scanned package | PackageVersion (ecosystem cran); updatedAt = Date/Publication; version/author/maintainer/license + non-minting context signals in metadata JSON |
| Upstream is github.com | GitHubRepository + GitHubRepoContributor + license fields |
| Malicious (≥1 evidence detection) | CVEMetadata (source="cran", GCVE-110-CRAN-YYYY-NNNNNN, isMaliciousPackage=true), one CVEDescription per detection, CVEProblemType (CWE-506 + CWE-94/CWE-200 specifics), CVEAffected (vendor cran, all versions), CVEMetadataReferences (CRAN page + repo + homepage), PackageVersionCVE, GcveIssuance |
| Malicious + actor resolved | ThreatActor (CRAN maintainer name + email + embedded contact emails) + MalwareThreatActor edges + MalwareAttribution (attributed, claimed/victim upstream GitHub) |
| Malicious | MalwareIoc rows (exfil endpoints, IPs, domains, URLs, emails, install-commands, file hashes) |
Detection
Reuses the shared malscan-engine
detect engine. Install-time script bodies (configure / configure.win /
cleanup / src/Makevars) are scanned by the install-script detectors; the
full extracted sources (DESCRIPTION + .R files + NAMESPACE) by the general /
shell / source-url detectors. R/CRAN-specific evidence rules include
system() / system2() / shell() calls in install scripts or .R code,
download.file() followed by source() / eval(), base64enc-decoded
payloads passed to eval(parse(text=)) (CWE-94), and .onLoad/.onAttach
hooks that invoke system() (CWE-506). Findings carry one of three classes: evidence (a factual malicious behaviour
— mints the advisory on its own), trigger (a weak corroborating signal such
as a high-entropy embedded payload or a supply-chain ownership/identity change
— never mints alone), and context (reputation/risk, recorded as
PackageVersion.metadata only). detect.CombinedVerdict mints on any
evidence, a known-bad package owner, a high-entropy payload combined with an
ownership/identity change, or two independent identity-change families changing
together. The full finding-class model and the per-ecosystem capability config
(22 capabilities) live in Malware Detection.
Known-bad IOC matching — the package’s declared source is also matched
against the public per-ecosystem known-bad
STIX feed (domains, IPs, URLs), loaded
once per run and matched in memory. A hit is evidence (CWE-506) folded into
the finding set before the combination gate, and is recorded as a MalwareIoc
row whose references carry the file/line and STIX provenance.
Resume
A release-date watermark (RFC3339) is stored in BulkDataDumpTracker under
source cran-recent (the sha256 column holds the last processed release
date). The first run (no tracker) sweeps the recent feed in full; subsequent
runs process only releases newer than the saved watermark. --force reprocesses
the recent window regardless of the watermark; --limit bounds packages per run.
S3 Persistence
- Archive path:
cran/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/cran-json-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
store-error
Uses s3client.Uploader from internal/s3client/uploader.go. Skipped when S3_BUCKET_NAME is unset (local dev).
See the S3 Persistence Contract for the full reason taxonomy.