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:
Not every part is required. For example, a Maven artifact PURL looks like this:
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.
For the complete list, see PURL types and components.
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
- Find the component's original PURL in the SBOM or integration request. Compare it with the name and version fields of the same component.
- Establish the package origin: a registry, a system package repository, or a source code repository. Select the corresponding PURL type.
- 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.
- Check the namespace, encoding, and qualifiers against the rules for the selected type. For system packages, also check the distribution and source package information.
- 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.
- 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
@angularbecomes%40angular, and a+in a version becomes%2B. Do not encode the entire string or encode%40again 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_urlidentifies the package repository, not a local path or an arbitrary project link. - Use
subpathfor 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
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.
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:
npm: JavaScript and TypeScript
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
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
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
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.
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.
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.
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 #.
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.
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.
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.
Pub (Dart, Flutter) — use the package name and version from pubspec.lock. There is no namespace; names contain lowercase Latin letters, digits, and underscores.
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.
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.
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:
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.
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.
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:
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:
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.
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
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.
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.
