PURL construction guidelines

A Package URL (PURL) identifies a package by its ecosystem, name, version, and additional attributes. CodeScoring uses PURLs to match components against package and vulnerability data.

These guidelines help when preparing your own SBOM, checking a report from another tool, or investigating unknown components. Start with the verification steps, then refer to the rules for the relevant ecosystem.

PURL structure

The ECMA-427 standard defines the general structure:

pkg:type/namespace/name@version?qualifiers#subpath

Not every part is required. For example, a Maven artifact PURL looks like this:

pkg:maven/org.apache.logging.log4j/log4j-core@2.17.2
PartRequirementMeaning
pkg:Required.The identifier scheme.
typeRequired.The package type: maven in this example. Determined by the package ecosystem and origin.
namespaceDepends on the type.The namespace: org.apache.logging.log4j in this example. Its format depends on the type.
nameRequired.The package name: log4j-core in this example.
versionOptional in the general syntax; provide the actual version for analysis.The package version after @: 2.17.2 in this example.
qualifiersDepends on the type and qualifier.Additional parameters after ?, such as arch=amd64&distro=debian-12.
subpathOptional.A relative path within the package after #, when needed to identify the component.

Qualifiers describe package characteristics and origin. For example, arch specifies the architecture, distro the distribution and release, upstream the source package, and repository_url the package repository. Allowed qualifiers and their value formats depend on the PURL type. Some types require specific qualifiers, such as tag_id for SWID.

Choose the type based on the origin of the specific package. Libraries written in the same language can be distributed through different package ecosystems: for example, a Python library from PyPI has the type pypi, one from Conda has the type conda, and a Debian distribution package has the type deb.

Package sourcePURL type
PyPIpypi
Condaconda
Debian or Ubuntu package repositorydeb
Package repository of an RPM-based distributionrpm
Alpine package repositoryapk
Maven repository, including dependencies used by Gradle and sbtmaven

For the complete list, see PURL types and components.

Version for analysis

The general PURL syntax allows the version to be omitted. To analyze a specific dependency, provide its actual version: a range such as >=1.0, a latest tag, or a runtime version does not replace the installed package version.

Verification steps

  1. Find the component's original PURL in the SBOM or integration request. Compare it with the name and version fields of the same component.
  2. Establish the package origin: a registry, a system package repository, or a source code repository. Select the corresponding PURL type.
  3. Confirm the name and exact version using the lockfile, installed package metadata, or a record in the original registry. A manifest may only specify an allowed version range.
  4. Check the namespace, encoding, and qualifiers against the rules for the selected type. For system packages, also check the distribution and source package information.
  5. Correct the metadata or generator settings and regenerate the SBOM. If editing the SBOM manually, keep the component information and references to it in the dependency graph consistent.
  6. Repeat the SBOM scan or import into the platform. Check which package and version appear in the results.

Successful SBOM schema validation does not confirm that package coordinates are correct. The absence of detected vulnerabilities is not a PURL correctness check either.

Encoding and qualifiers

  • Keep the parts in order: version, qualifiers, then subpath. Do not add empty @, ?, or # sections.
  • Encode special characters within values according to PURL rules. For example, the npm scope @angular becomes %40angular, and a + in a version becomes %2B. Do not encode the entire string or encode %40 again as %2540.
  • Write qualifiers as key=value, separated by &. Use lowercase keys without duplicates or empty values; canonical PURLs have sorted keys.
  • Change the case of names and versions only as required by the ecosystem. Lowercasing every PURL can change its identity.
  • Add only verified qualifiers. repository_url identifies the package repository, not a local path or an arbitrary project link.
  • Use subpath for a path within the package. Absolute working directory paths and . or .. segments are not allowed.

When developing a generator, use a Package URL library that constructs identifiers from individual fields and handles their encoding.

Ecosystem rules

The following rules cover package ecosystems and other types accepted during SBOM analysis. Support for a PURL type and the availability of a specific package in the CodeScoring database are separate conditions.

Maven: Java, Kotlin, and Scala

pkg:maven/org.apache.logging.log4j/log4j-core@2.17.2

Use groupId as the required namespace, artifactId as name, and the resolved artifact version as version. Verify the coordinates in the POM and the Maven, Gradle, or sbt dependency tree.

Coordinate case

Preserve the original case of groupId, artifactId, and the version. For example, do not change the Final suffix to final or remove it. Copy the coordinates from the artifact metadata.

The .jar extension and classifier are not part of the version. The Maven specification provides type and classifier qualifiers for artifact variants:

pkg:maven/org.apache.xmlgraphics/batik-anim@1.9.1?classifier=sources

npm: JavaScript and TypeScript

pkg:npm/lodash@4.17.21
pkg:npm/%40angular/core@12.0.0

Obtain the name and resolved version from the lockfile or installed package metadata. For scoped packages, namespace contains the scope with an encoded @; other packages have no namespace. Do not include node_modules in the PURL or use a dependency alias instead of the original package name.

The npm definition preserves case sensitivity for historical package names. Do not change case arbitrarily.

PyPI: Python

pkg:pypi/django@4.2.0
pkg:pypi/django-allauth@0.54.0

Use the Python distribution name from package metadata, not the imported module name or wheel filename. There is no namespace. Name normalization under the Python Packaging rules lowercases the name and replaces each sequence of -, _, and . with a single hyphen.

The version must identify the package, not the Python interpreter. Preserve prerelease, post-release, and local version information. The type format is described in the PyPI definition.

Go Modules

pkg:golang/github.com/gorilla/mux@v1.8.0

For a Go Modules dependency, use the full module path and its resolved version. In this example, namespace is github.com/gorilla and name is mux. Preserve a trailing /v2 or another major version suffix if it is part of the module path. Copy pseudo-versions in full from module metadata.

Check the coordinates against go.mod, the resolved module list, or build information. For replace directives, account for the source actually used. An entry in go.sum alone does not prove that this version was included in the build.

A vendor/ path, a nested Go package path, and a toolchain version such as go1.25.9 do not replace module coordinates. Removing part of a path is not enough to reconstruct a correct PURL: first establish the module boundary and version from the source data. The Go specification specifically notes the difficulty of distinguishing modules from packages using a PURL alone.

NuGet: .NET

pkg:nuget/Newtonsoft.Json@13.0.3

Set name to the full package identifier from .nuspec or the lockfile. Dots in the name do not delimit a namespace: NuGet PURLs have none. Use the resolved package version, preserving its prerelease part; the .NET version and DLL assembly name are not substitutes.

Composer, RubyGems, and Cargo

Composer (PHP) — the required vendor is the namespace, and the package name is name. Both are lowercase. Check the version against composer.lock.

pkg:composer/laravel/laravel@5.5.0

RubyGems (Ruby) — there is no namespace; use the gem name and version from Gemfile.lock or gem metadata. A platform variant can be specified in platform instead of appending it to the version.

pkg:gem/jruby-launcher@1.1.2?platform=java

Cargo (Rust) — there is no namespace. Use the package name and version from Cargo.lock, accounting for the dependency source. Do not substitute a manifest alias or a module name from the source code for the package name.

pkg:cargo/structopt@0.3.11

Type definitions: Composer, RubyGems, Cargo.

CocoaPods and Swift Package Manager

CocoaPods — use the pod name with its original case and the version from Podfile.lock. There is no namespace; a subspec, if present, follows #.

pkg:cocoapods/AFNetworking@4.0.1
pkg:cocoapods/ShareKit@2.0#Twitter

Swift Package Manager — the required namespace contains the host and repository owner, and name is the repository name. Check the version or revision against Package.resolved; do not substitute a product's display name for dependency coordinates.

pkg:swift/github.com/Alamofire/Alamofire@5.4.3

Type definitions: CocoaPods, Swift.

CRAN, Hex, and Pub

CRAN (R) — use the package name with its original case and the full version, including any hyphenated suffix. There is no namespace. Coordinate sources include the installed package's DESCRIPTION and renv.lock.

pkg:cran/rJava@1.0-4

Hex (Erlang, Elixir, Gleam) — the package name is lowercase. The optional namespace identifies the organization for private packages. Use the Hex package name and resolved version from the lockfile, accounting for possible differences between application and package names.

pkg:hex/jason@1.1.2

Pub (Dart, Flutter) — use the package name and version from pubspec.lock. There is no namespace; names contain lowercase Latin letters, digits, and underscores.

pkg:pub/characters@1.2.0

Type definitions: CRAN, Hex, Pub.

Conan and Conda

Conan (C/C++) — separate the name, version, and package variant attributes. In a Conan reference name/version@user/channel, user and channel map to the corresponding PURL qualifiers. If recipe and package revisions are known, include rrev and prev; obtain build details from Conan metadata.

pkg:conan/openssl@3.0.3

Conda — there is no namespace. The name and version identify the Conda package, even if it contains a Python library. For a specific build, retain verified build, channel, subdir, and type values.

pkg:conda/absl-py@0.4.1?build=py36h06a4308_0&channel=main&subdir=linux-64&type=tar.bz2

Type definitions: Conan, Conda.

System packages: DEB, RPM, and APK

For system packages, use the version released by the distribution, including all distribution suffixes and revisions. The upstream project's version, such as OpenSSL's, does not replace the Debian, RHEL, or Alpine package version.

Set namespace to the package vendor in lowercase, such as debian, ubuntu, fedora, or alpine. Obtain arch from package metadata: values such as amd64 and x86_64 are not arbitrarily interchangeable across ecosystems.

DEB — preserve the full version, including the epoch before the colon and the package revision. In this example, the epoch is 1 and upstream=shadow identifies the source package:

pkg:deb/debian/login@1%3A4.13%2Bdfsg1-1?arch=amd64&distro=debian-12&upstream=shadow

RPM — combine the version and release as version-release. If the metadata includes an epoch, pass it as the epoch qualifier. Do not include the .rpm extension in the version or the architecture in the package name.

pkg:rpm/fedora/curl@7.50.3-1.fc25?arch=i386&distro=fedora-25

APK — preserve the revision, such as -r0, in the version. The apk type refers to packages from Alpine and other APK-based distributions, not Android applications.

pkg:apk/alpine/curl@7.83.0-r0?arch=x86

Distribution and source package

name contains the binary package name, while upstream describes the source package used to build it. These packages can have the same name.

CodeScoring uses additional qualifiers when matching system packages:

QualifierValue source
distroThe distribution and release in the analyzed environment, such as debian-12, ubuntu-22.04, alpine-3.18, rhel-8, or fedora-40.
upstreamThe source package used to build the binary: Source metadata for DEB, Source RPM for RPM, or origin for APK.

Check distro against the environment the package came from. When scanning a container, use the container's distribution, not the host's. Do not construct release numbers using a single template: Debian, Ubuntu, and RPM distributions use different version schemes.

If the source package name differs from the binary package name, preserve it in upstream. If the version also differs, provide the verified name and version as name@version, encoding special characters. Do not infer the source package name by simply removing a suffix from the binary package name. If a generator already supplies Source RPM as a filename, check it against the metadata; that format alone does not prove a PURL error.

Type definitions: DEB, RPM, APK.

Container images: OCI and Docker

For OCI, there is no namespace, and name is the last segment of the repository name. The version is the artifact digest; the full repository path goes in repository_url, and the tag goes in tag.

In the template below, replace <digest> with the complete actual SHA-256 digest:

pkg:oci/debian@sha256%3A<digest>?repository_url=docker.io%2Flibrary%2Fdebian&tag=bookworm

For the Docker type, the namespace may contain the registry or image owner, and the version may be a tag or a SHA-256 identifier. A digest is preferable for reproducible identification: a tag can point to a different image over time.

An image PURL describes the image as a whole. Its contents require separate PURLs for the installed packages.

Repositories, models, and custom artifacts

These types use different coordinate sources. Select them when they match the component's origin.

TypeRules
githubnamespace is the owner and name is the repository; both are lowercase. The version is a commit or tag.
huggingfacenamespace is the user or organization and name is the model repository, preserving case. The version is the commit hash of the relevant revision.
mlflowThere is no namespace. The name and version come from the Model Registry; repository_url is the MLflow tracking URI. Name case rules depend on the server.
swidThe name and version come from the SWID tag's SoftwareIdentity. A nonempty tag_id qualifier is required; software creator information may define the namespace.
genericFor artifacts without a suitable specialized type. Provide the actual name and version, and retain origin and checksum information in component metadata or the appropriate qualifiers.

For packages obtained from Maven, PyPI, or a system repository, preserve the package ecosystem. Hosting source code on GitHub does not make a package type github, and changing an unknown PURL to generic does not recover missing coordinates.

Normalization in CodeScoring

Before matching packages, CodeScoring normalizes PURLs according to ecosystem rules. As a result, a PURL in the results may differ from the original in case, version representation, or qualifiers.

Do not remove package information from an input PURL to match an internal normalized representation. Qualifiers permitted by the specification, such as Maven's classifier or RubyGems' platform, may not participate in matching, but this does not make them invalid. For Cargo, use the name from the registry and lockfile even if normalization replaces hyphens with underscores.

Normalization cannot recover a reliable version, a missing groupId, or the package origin. Obtain this information from the source.

Common errors

ObservationWhat to check
A Maven PURL lacks groupId, or its version includes .jar and a classifier.Artifact coordinates in the POM and dependency tree.
A Go PURL contains vendor/ and a toolchain version.The path and version of the module actually used.
A DEB version is missing its epoch, or an RPM version is missing its release.The full version in installed package metadata.
A system package specifies a different distribution.Package origin and the OS release in the analyzed environment.
The package name is an import, alias, or filename.The name under which the package is published in its ecosystem.
The version is a range, latest, or an arbitrary 0.0.0.The resolved version in the lockfile or metadata. Do not replace an unknown version with an invented one.
The value after @ is an architecture instead of a package version.Specify the actual package version. For DEB, RPM, and APK, the architecture goes in the arch qualifier.
The v prefix was added to or removed from the version without following ecosystem rules.Check the version against package metadata and ecosystem rules. Go Modules versions retain the v prefix.
A runtime, application, or assembly version is used instead of the package version.Verify the version of this specific package in the lockfile or installed component metadata.

Error examples

Correct PURLs using your dependency's metadata. The names and versions below refer to specific packages and are not replacements for the coordinates in your SBOM.

ErrorExample and correction
Incomplete DEB versionIncorrect: pkg:deb/debian/libudev1@247.3-7?arch=amd64&distro=debian-11&upstream=systemd
Correct: pkg:deb/debian/libudev1@247.3-7%2Bdeb11u4?arch=amd64&distro=debian-11&upstream=systemd
If the installed package version is 247.3-7+deb11u4, retain the +deb11u4 suffix. Encode + as %2B.
Nonexistent NuGet versionIncorrect: pkg:nuget/system.reflection.primitives@6.0.0
System.Reflection.Primitives has no 6.0.0 release. Use the actual version from the .nuspec file or lockfile and confirm that it exists in the original registry. The .NET version is not the NuGet package version.
artifactId used instead of groupIdIncorrect: pkg:maven/sketches-java/sketches-java@0.8.2
Correct: pkg:maven/com.datadoghq/sketches-java@0.8.2
This artifact's POM specifies com.datadoghq as its groupId.
Maven artifact name case changedIncorrect: pkg:maven/com.zaxxer/hikaricp@7.0.2
Correct: pkg:maven/com.zaxxer/HikariCP@7.0.2
Preserve the artifactId case from the POM: HikariCP.
Go package path and toolchain version used instead of module coordinatesIncorrect: pkg:golang/vendor/golang.org/x/text/unicode/bidi@go1.25.9
Correct for module golang.org/x/text at version v0.9.0: pkg:golang/golang.org/x/text@v0.9.0
unicode/bidi is a package within this module, vendor/ is a directory, and go1.25.9 is the toolchain version. Verify the module path and version using go.mod, the resolved dependency list, or build information.

For Go, also verify which module the project uses. For example, pkg:golang/go.etcd.io/etcd/api/v3@v3.5.23 identifies an existing etcd API module. Replacing it with pkg:golang/github.com/etcd-io/etcd@v3.0.0%2Bincompatible identifies a different component and version. Do not change coordinates solely because a component is not found in the analysis results.

Package not found

An unknown component does not necessarily have an incorrect PURL. It may be an internal package, a version absent from the source, or a package for which the database does not yet have information.

After checking the PURL, confirm that the exact version exists in the original registry and corresponds to the analyzed artifact. If the package remains unknown, provide support with the original PURL, a source link or package metadata, the generator name and version, and a minimal SBOM excerpt containing the affected component. This information helps distinguish generation errors from missing package data.

Was this page helpful?