OSM Malicious Package Processor — Design
Overview
Fetches malicious package threat intelligence from the OpenSourceMalware.com
API (https://api.opensourcemalware.com/functions/v1/query-latest) and creates
CVEMetadata rows (source=osm) with minted OSM-YYYY-N identifiers.
Every OSM record is malicious — all threats are tagged as malicious packages
(isMaliciousPackage=true) with CWE-506 (Embedded Malicious Code) and CVSS v3.1
scores derived from the severity level. OSM is one of the five advisory-aggregation
sources that feed the shared
Threat-Actor Attribution
engine — and in addition to those aggregators, 16 registry scanners detect malware
first-hand; see the dedicated section below.
API
| Property | Value |
|---|---|
| URL | https://api.opensourcemalware.com/functions/v1/query-latest |
| Auth | Bearer token (OSM_API_TOKEN) |
| Method | GET with ?limit=100&ecosystem={eco} |
| Ecosystems | npm, pypi, crates, nuget, maven, go, packagist, rubygems, vscode, openvsx, repositories |
Parsing
| Field | Source |
|---|---|
| Threat ID | threats[].id (UUID) |
| Package | threats[].package_name |
| Registry | threats[].registry |
| Severity | threats[].severity_level (low/high/critical) |
| Description | threats[].threat_description + threats[].payload_description |
| Published | threats[].first_seen (RFC3339) |
| Updated | threats[].updated_at (RFC3339) |
| Version | threats[].version_info |
| OSV Link | threats[].osv_advisory_url (MAL-YYYY-N extraction) |
| GHSA Link | threats[].ghsa_advisory_url |
ID Generation
Each threat is assigned an OSM-{year}-{seq} identifier. The GCVE form uses
Vulnetix’s numeric GCVE Numbering Authority (GNA) id 110:
- On startup, query
MAX(sequenceNumber)fromGcveIssuancewheregcveId LIKE 'GCVE-110-OSM-{year}-%' - For each new threat, increment the sequence counter for the threat’s publication year
- Store
OSM-YYYY-NasCVEMetadata.cveIdandGCVE-110-OSM-YYYY-NasGcveIssuance.gcveId - Link extracted MAL-IDs and threat UUIDs as
GcveAliasrecords
CVSS Scoring
Supply chain context — malicious package installed via registry:
| severity_level | CVSS v3.1 Vector | Score | Label |
|---|---|---|---|
| critical | AV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H | 9.6 | CRITICAL |
| high | AV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H | 8.8 | HIGH |
| low | AV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N | 5.4 | MEDIUM |
Storage
Advisory rows
| Table | Rows inserted |
|---|---|
CVEMetadata | One per threat; source="osm", cveId="OSM-YYYY-N", isMaliciousPackage=true |
CVEDescription | One per threat; containerType="cna", lang="en" |
CVEMetadataReferences | 2-4 per threat; OSM link, OSV/GHSA advisories, registry URL |
CVEMetric | One per threat; CVSS v3.1 from severity |
CVEProblemType | One per threat; CWE-506 |
CVEAffected | One per threat; package + registry + version |
CVEAffectedVersion | One per threat; version_info |
PackageVersion | One per threat |
PackageVersionCVE | One per threat; relationshipType="affected", confidence="high" |
GcveIssuance | One per threat; gcveId="GCVE-110-OSM-YYYY-N" |
GcveAlias | 1-2 per threat; threat UUID + MAL-ID |
CVEAlias | 0-1 per threat; OSM-YYYY-N ↔ MAL-ID |
OSM retention tables
The OSM processor retains the full upstream API response and parsed IOCs in two OSM-only tables (camelCase columns, BigInt millisecond timestamps):
| Table | Purpose | Key columns |
|---|---|---|
OsmThreat | Full OSM API record retention, one row per threat | osmThreatUuid (OSM threat UUID), cveId, packageName, registry, severity, rawJSON, actorAttributionStatus, claimedRepoUrl, claimedRepoOwner, claimedRepoPlatform, firstSeen, lastSeen |
OsmThreatIoc | Parsed indicators of compromise from each threat | osmThreatUuid (FK), iocType, value, truncated |
The actorAttributionStatus and claimedRepo* columns on OsmThreat carry the
result of the attribution pass (see below): claimedRepo* captures a
dependency-confusion/typosquat target a package merely declares but is never
attributed to as the actor, and actorAttributionStatus records outcomes such as
hijack-victim-excluded.
Threat-actor tables (shared)
OSM attribution writes to the generic, cross-source threat-actor tables. Because
OSM is an OSV-shaped malware source it uses the OsmThreatActor edge — not
MalwareThreatActor (a (cveId, source) is attributed via exactly one of the
two; there is no double-write).
| Table | Purpose | Key columns |
|---|---|---|
ThreatActor | Resolved actor identity (shared across all sources) | uuid, platform, identifier, actorType, displayName, email, profileUrl, company, location, rawJSON, threatCount, firstSeen, lastSeen; @@unique(platform, identifier) |
ThreatActorKey | Public keys harvested from a GitHub identity (shared) | threatActorUuid (FK), keyType (ssh-auth / ssh-signing / gpg), githubKeyId, publicKey, fingerprint, title, emails |
OsmThreatActor | OSM-only edge from a threat to its actor | osmThreatUuid (FK), threatActorUuid (FK), cveId |
Threat-Actor Attribution
After each threat is committed, an inline first-time-only pass resolves the
malware author from package/repo identity plus upstream registry, GitHub,
and Docker Hub lookups, via the shared internal/actorintel engine. A threat
that already carries an OsmThreat attribution row is never re-enriched. The
full model, schema, and cross-source backfill are documented in
Threat-Actor Attribution;
the OSM-relevant points:
| Attribution form | Basis | Confidence |
|---|---|---|
| Repository report | Repo owner | High |
| Container (Docker Hub) | Namespace owner | High |
| Whole-package, non-hijack | Registry maintainer | High if typosquat / dependency-confusion tagged, else medium |
| Go module | Repo owner from module path | High |
| Commit hash present | Commit author | High |
| Hijack (compromised account) | Maintainer is the victim — actorAttributionStatus = hijack-victim-excluded, not attributed | — |
Impersonation trap. For non-Go packages, the repository a package declares
is frequently the dependency-confusion / typosquat target — a legitimate org’s
repo (e.g. microsoft). That repo is captured on OsmThreat as claimedRepo*
and is never attributed as the actor.
Key harvesting. When the resolved actor is a GitHub identity, the engine
harvests its public SSH-auth, SSH-signing, and GPG keys into ThreatActorKey
(OpenSSH SHA256 fingerprint for SSH keys, key id for GPG). A fingerprint reused
across multiple accounts links operators behind otherwise distinct identities.
OSM attribution is written through the OsmThreatActor edge only; it never
double-writes to MalwareThreatActor.
Incremental Strategy
On startup, load all sourceAdvisoryRef values from CVEMetadata where
source='osm' into a map[string]bool. Per threat: if the UUID is in the
map, skip. Otherwise process as new.
Schedule
Runs daily at 09:00 UTC (cron(0 9 * * ? *)).
Frequency check: 86400 seconds via BulkDataDumpTracker.
S3 Persistence
- Archive path:
osm/files/{sha256}/{filename}✓ - Quarantine path:
failed-feeds/osm-processor/{YYYY-MM-DD}/{reason}/{filename}✓ - Failure reasons emitted:
parse-error,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.