Integration
upmex reads a package and stops there. Two optional layers go further: registry mode asks the package’s own registry about it, and the API integrations ask third party services. Both are off unless you turn them on, and both make network requests.
Registry mode
--registry lets upmex fetch metadata the archive itself does not carry.
upmex extract --registry commons-lang3-3.12.0.jar
Today this is implemented for Maven Central only. Other ecosystems accept the flag and
ignore it. upmex info prints what your build supports.
It does two things for Java packages:
Following an inherited POM. A POM often declares almost nothing and inherits its licence, developers and source location from a parent, which is a separate artifact and therefore not in the jar. Registry mode fetches the parent and fills in what is missing.
Resolving a jar with no POM. Shaded, relocated and repackaged jars frequently ship
no POM at all, which leaves them with no coordinates and nothing to look up. Registry
mode hashes the file and asks the Maven Central index which artifact has that SHA-1,
then reads the published POM for it. okhttp-4.11.0.jar carries no POM and no licence,
and comes back as com.squareup.okhttp3:okhttp at 4.11.0 under Apache-2.0.
Locally declared data always wins. A registry value is only used where the package
itself said nothing, and provenance records which source supplied each field:
"provenance": {
"name": "maven_central_hash:https://search.maven.org/solrsearch/select",
"licenses": "maven_central_pom:https://repo1.maven.org/maven2/com/squareup/okhttp3/okhttp/4.11.0/okhttp-4.11.0.pom"
}
Lookups are cached by hash for the life of the process, so scanning a directory that contains the same artifact twice costs one request. The cache is not written to disk, so a fresh invocation looks everything up again.
Maven Central rate limits its search index, and it does so quickly. A failed lookup is not an error: upmex reports what it read from the file and moves on, and retries on the next run rather than remembering the failure.
API enrichment
--api queries third party services. They are independent of --registry and can be
combined with it.
| Value | Service | Adds |
|---|---|---|
clearlydefined |
ClearlyDefined | curated licence and attribution data |
ecosystems |
Ecosyste.ms | registry metadata, maintainers, keywords |
purldb |
PurlDB | package records matched by coordinates |
vulnerablecode |
VulnerableCode | known vulnerabilities, into vulnerabilities |
all |
all of the above | |
none |
nothing, the default |
upmex extract --api clearlydefined lodash-4.17.21.tgz
upmex extract --registry --api all package.jar --pretty
Every service that contributes leaves a record in enrichment, naming itself and
listing which fields it filled:
"enrichment": [
{
"source": "clearlydefined",
"source_type": "api",
"timestamp": "2026-08-11T22:00:00",
"applied_fields": ["licenses", "repository"],
"data": { }
}
]
source_type separates registry, meaning the package’s own registry, from api,
meaning a third party. That distinction matters when you need to say where an assertion
came from.
A lookup needs enough identity to be unambiguous. Maven family packages are only queried when the groupId is known, because a query by artifact name alone can match a different project and attribute its licence to yours.
Using the output
The JSON shape is stable and every section is always present, so you can index into it without guarding. A few things are worth handling deliberately.
purl can be null. It is null when the package cannot be identified well enough to
build a valid Package URL. If you are keying records on the PURL, decide what to do with
those rather than letting null become a key.
repository can be the string NO-ASSERTION. That is not a URL and not an empty
string. It means the package did not declare one.
A licence may not be an SPDX identifier. When a package declares something the
detector cannot classify, the declaration is kept with source set to declared and
the stated text in spdx_id. Check source before treating the value as SPDX.
Diagnostics currently go to standard output. A damaged archive prints an error line
ahead of the JSON, which breaks a pipe into a parser. Until
issue #99 is fixed, write to a file with
--output when the input might be bad.
In a shell script
#!/usr/bin/env bash
set -euo pipefail
for package in lib/*; do
upmex extract "$package" --output /tmp/upmex.json
name=$(jq -r '.package.name' /tmp/upmex.json)
licenses=$(jq -r '[.licensing.declared_licenses[].spdx_id] | unique | join(", ")' /tmp/upmex.json)
if [ -z "$licenses" ]; then
echo "$name: no licence declared"
else
echo "$name: $licenses"
fi
done
In CI
Failing a build on a licence that is not on an allowed list:
name: Licence check
on: [push, pull_request]
jobs:
licences:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- run: pip install upmex
- name: Check licences
run: |
allowed="MIT Apache-2.0 BSD-2-Clause BSD-3-Clause ISC"
failed=0
for package in lib/*; do
upmex extract "$package" --output result.json
name=$(jq -r '.package.name' result.json)
for spdx in $(jq -r '.licensing.declared_licenses[].spdx_id' result.json); do
case " $allowed " in
*" $spdx "*) ;;
*) echo "$name: $spdx is not allowed"; failed=1 ;;
esac
done
done
exit $failed
Leave --registry and --api off in CI unless you need them. Without them the run is
offline, which makes it reproducible and immune to a registry being slow or rate
limiting you.
Building an SBOM
upmex does not emit CycloneDX or SPDX directly, but its output carries what those formats need. A minimal CycloneDX component per package:
import json
from pathlib import Path
from upmex import PackageExtractor
extractor = PackageExtractor()
components = []
for path in sorted(Path("lib").iterdir()):
if not path.is_file():
continue
metadata = extractor.extract(str(path))
# A bom-ref has to be present and unique, and purl can be null
ref = metadata.purl or f"{metadata.name}@{metadata.version}"
components.append({
"type": "library",
"bom-ref": ref,
"name": metadata.name,
"version": metadata.version,
"purl": metadata.purl,
"hashes": [{"alg": "SHA-1", "content": metadata.file_hash}],
"licenses": [
{"license": {"id": lic.spdx_id}}
for lic in metadata.licenses if lic.spdx_id
],
})
sbom = {
"bomFormat": "CycloneDX",
"specVersion": "1.5",
"components": components,
}
print(json.dumps(sbom, indent=2))
Note the licence entry. CycloneDX distinguishes license.id, which must be a valid
SPDX identifier, from license.name, which is free text. A value upmex marked as
declared is not an SPDX identifier and belongs in name, so a stricter generator
should branch on detection_method.
Other SEMCL.ONE tools
upmex is the metadata reader in a larger toolchain. It pairs with osslili for licence identification, which it uses internally, and its PURLs are the join key for tools that work from package identity rather than files.