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

PropertyValue
URLhttps://api.opensourcemalware.com/functions/v1/query-latest
AuthBearer token (OSM_API_TOKEN)
MethodGET with ?limit=100&ecosystem={eco}
Ecosystemsnpm, pypi, crates, nuget, maven, go, packagist, rubygems, vscode, openvsx, repositories

Parsing

FieldSource
Threat IDthreats[].id (UUID)
Packagethreats[].package_name
Registrythreats[].registry
Severitythreats[].severity_level (low/high/critical)
Descriptionthreats[].threat_description + threats[].payload_description
Publishedthreats[].first_seen (RFC3339)
Updatedthreats[].updated_at (RFC3339)
Versionthreats[].version_info
OSV Linkthreats[].osv_advisory_url (MAL-YYYY-N extraction)
GHSA Linkthreats[].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:

  1. On startup, query MAX(sequenceNumber) from GcveIssuance where gcveId LIKE 'GCVE-110-OSM-{year}-%'
  2. For each new threat, increment the sequence counter for the threat’s publication year
  3. Store OSM-YYYY-N as CVEMetadata.cveId and GCVE-110-OSM-YYYY-N as GcveIssuance.gcveId
  4. Link extracted MAL-IDs and threat UUIDs as GcveAlias records

CVSS Scoring

Supply chain context — malicious package installed via registry:

severity_levelCVSS v3.1 VectorScoreLabel
criticalAV:N/AC:L/PR:N/UI:R/S:C/C:H/I:H/A:H9.6CRITICAL
highAV:N/AC:L/PR:N/UI:R/S:U/C:H/I:H/A:H8.8HIGH
lowAV:N/AC:L/PR:N/UI:R/S:U/C:L/I:L/A:N5.4MEDIUM

Storage

Advisory rows

TableRows inserted
CVEMetadataOne per threat; source="osm", cveId="OSM-YYYY-N", isMaliciousPackage=true
CVEDescriptionOne per threat; containerType="cna", lang="en"
CVEMetadataReferences2-4 per threat; OSM link, OSV/GHSA advisories, registry URL
CVEMetricOne per threat; CVSS v3.1 from severity
CVEProblemTypeOne per threat; CWE-506
CVEAffectedOne per threat; package + registry + version
CVEAffectedVersionOne per threat; version_info
PackageVersionOne per threat
PackageVersionCVEOne per threat; relationshipType="affected", confidence="high"
GcveIssuanceOne per threat; gcveId="GCVE-110-OSM-YYYY-N"
GcveAlias1-2 per threat; threat UUID + MAL-ID
CVEAlias0-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):

TablePurposeKey columns
OsmThreatFull OSM API record retention, one row per threatosmThreatUuid (OSM threat UUID), cveId, packageName, registry, severity, rawJSON, actorAttributionStatus, claimedRepoUrl, claimedRepoOwner, claimedRepoPlatform, firstSeen, lastSeen
OsmThreatIocParsed indicators of compromise from each threatosmThreatUuid (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).

TablePurposeKey columns
ThreatActorResolved actor identity (shared across all sources)uuid, platform, identifier, actorType, displayName, email, profileUrl, company, location, rawJSON, threatCount, firstSeen, lastSeen; @@unique(platform, identifier)
ThreatActorKeyPublic keys harvested from a GitHub identity (shared)threatActorUuid (FK), keyType (ssh-auth / ssh-signing / gpg), githubKeyId, publicKey, fingerprint, title, emails
OsmThreatActorOSM-only edge from a threat to its actorosmThreatUuid (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 formBasisConfidence
Repository reportRepo ownerHigh
Container (Docker Hub)Namespace ownerHigh
Whole-package, non-hijackRegistry maintainerHigh if typosquat / dependency-confusion tagged, else medium
Go moduleRepo owner from module pathHigh
Commit hash presentCommit authorHigh
Hijack (compromised account)Maintainer is the victimactorAttributionStatus = 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).

flowchart LR SRC[Source feed] --> PROC[osm-processor] PROC -->|success| ARCHIVE[("S3: osm/files/{sha256}/{filename}")] PROC -->|failure| Q[("S3: failed-feeds/osm-processor/{date}/{reason}/{filename}")] PROC --> DB[(PostgreSQL)]

See the S3 Persistence Contract for the full reason taxonomy.