ExplanationsVulnerability ManagementVulnerability Matching

Vulnerability Matching

When you upload an SBOM (Software Bill of Materials) to DevGuard, the system automatically scans all components to identify known vulnerabilities. This document explains how DevGuard’s vulnerability matching engine works.

Overview

The matching process consists of several steps:

  1. SBOM Parsing: The uploaded SBOM (CycloneDX format) is parsed into a graph structure
  2. Component Extraction: Each component with a Package URL (PURL) is extracted
  3. Database Query: Components are matched against DevGuard’s vulnerability database
  4. Version Comparison: Version ranges are evaluated to determine if a component is affected
  5. Result Aggregation: Matched vulnerabilities are collected and deduplicated

The Matching Pipeline

Step 1: SBOM Scanner

The sbomScanner iterates through all components in the SBOM graph. For each component that has a valid Package URL (PURL), it queries the vulnerability database:

SBOM Graph → Component Extraction → PURL Parsing → Vulnerability Lookup

Components without a PURL are skipped since they cannot be matched against the database.

Step 2: PURL Parsing and Normalization

DevGuard uses the Package URL (PURL) standard for component identification. When a PURL is parsed, DevGuard creates a match context that determines how to search the database:

Version TypeDescriptionExample
Semantic VersionStandard semver format1.2.3, 2.0.0-beta.1
Exact VersionNon-semver strings matched exactly20240101, custom-build
Ecosystem SpecificPlatform-specific versioning (deb, rpm, apk)2.47.3-0+deb13u1, 1.2.3-r4
Empty VersionNo version specifiedComponents without version info

The PURL is split into:

  • Search PURL: The package identifier without version (used for database lookup)
  • Normalized Version: The version string prepared for comparison
  • Qualifiers: Additional metadata like architecture, distro, or epoch

Step 3: Database Query

DevGuard maintains an affected_components table that stores vulnerability data from multiple sources. Each affected component record contains:

  • PURL without version: Package identifier for lookup
  • Ecosystem: The package ecosystem (e.g., Debian:12, Alpine:v3.22)
  • Type: Package type (e.g., golang, npm, deb, apk)
  • Version ranges: semver_introduced, semver_fixed, version_introduced, version_fixed
  • CVE associations: Links to CVE records with exploit and relationship data

The query is built dynamically based on the version interpretation type:

Semantic Version Matching

For semver packages, DevGuard uses range-based queries in the database:

WHERE purl = 'pkg:npm/lodash'
  AND (
    version = '4.17.10'
    OR (semver_introduced IS NULL AND semver_fixed > '4.17.10')
    OR (semver_introduced <= '4.17.10' AND semver_fixed IS NULL)
    OR (semver_introduced <= '4.17.10' AND semver_fixed > '4.17.10')
  )

This matches if:

  • The exact version is listed as affected
  • The version is below a fixed version (with no introduced version)
  • The version is at or above the introduced version (with no fixed version)
  • The version falls within the introduced-to-fixed range

Ecosystem-Specific Matching

For platform packages (Debian, Alpine, RPM), version comparison happens in Go code after the initial database query, using ecosystem-specific version parsers:

  • Debian (deb): Uses epoch-aware comparison (e.g., 1:2.47.3-0+deb13u1)
  • Alpine (apk): Supports Alpine-specific version suffixes (e.g., 1.2.3-r4)
  • RPM: Handles RPM version-release format

Ecosystem-specific versions often cannot be directly compared using semver rules. DevGuard uses specialized parsers for each ecosystem to ensure accurate version matching.

Step 4: Qualifier and Ecosystem Filtering

When a distro qualifier is present in the PURL, DevGuard filters results to match the specific distribution:

pkg:deb/debian/git@2.47.3?distro=debian-13.2

This is matched against affected components with ecosystem = 'Debian:13'.

For Alpine:

pkg:apk/alpine/curl@8.14.1-r2?distro=3.22.2

This matches ecosystem = 'Alpine:v3.22'.

Step 5: Version Range Verification

For ecosystem-specific packages, after retrieving potential matches from the database, DevGuard performs precise version checking using the CheckVersion function:

match, err := CheckVersion(
    component.Version,           // exact version if any
    component.VersionIntroduced, // first affected version
    component.VersionFixed,      // first fixed version
    lookingForVersion,           // version from SBOM
    component.Type,              // "deb", "rpm", or "apk"
)

A component is considered affected if:

  • It matches an exact affected version, OR
  • Its version is >= introduced AND < fixed

Step 6: CVE Deduplication

Vulnerabilities may be reported under multiple CVE IDs (aliases). DevGuard automatically deduplicates these:

  • If CVE-A aliases CVE-B (unidirectional), keep CVE-A
  • If CVE-A and CVE-B alias each other (bidirectional), keep the lexicographically smaller one

This prevents the same vulnerability from being counted multiple times.

Example Matching Flow

Consider an SBOM containing:

pkg:npm/lodash@4.17.10
  1. Parse PURL: Extract npm, lodash, version 4.17.10
  2. Normalize Version: Convert to semver 4.17.10
  3. Query Database: Find affected components for pkg:npm/lodash
  4. Match Ranges: Check if 4.17.10 falls within any vulnerable range
  5. Return CVEs: Return CVE-2019-10744, CVE-2020-8203, etc.

For a Debian package:

pkg:deb/debian/openssl@3.0.13-1~deb12u1?distro=debian-12
  1. Parse PURL: Extract deb, debian, openssl, version 3.0.13-1~deb12u1
  2. Interpret Version: Ecosystem-specific (Debian)
  3. Query Database: Find affected components for pkg:deb/debian/openssl with ecosystem LIKE 'Debian:12%'
  4. Fetch Candidates: Get all potentially affected entries
  5. Version Check: Use Debian version parser to compare against each candidate
  6. Return CVEs: Return matching vulnerabilities