Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
96 changes: 96 additions & 0 deletions docs/docs/coverage/language/quantum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Quantum

Trivy supports DataRobot Quantum packages - native system libraries packaged in Python wheel format.

## Overview

DataRobot uses an internal tool called "quantum-builders" that builds relocatable versions of system software (such as curl, krb5, openssl, libpng, etc.) and packages them into Python wheel format. These wheels are then installed into virtual environments using `pip`.

Trivy detects these quantum packages and maps them to their upstream equivalents for CVE detection.

## Support

The following scanners are supported.

| Package manager | SBOM | Vulnerability | License |
|-----------------|:----:|:-------------:|:-------:|
| Quantum | ✓ | ✓ | ✓ |

The following table provides an outline of the features Trivy offers.

| Package manager | File | Transitive dependencies | Dev dependencies | Position |
|-----------------|-----------------------|:-----------------------:|:----------------:|:--------:|
| Quantum | quantum.spec.json[^1] | - | - | ✓ |

## Detection

Trivy searches for `quantum.spec.json` files within `.dist-info` directories. These files are created by quantum-builders' `stamp_env()` function when packaging quantum wheels.

### Quantum Spec Structure

The `quantum.spec.json` file contains metadata about the quantum package:

```json
{
"name": "quantum-native-curl",
"library_name": "curl",
"version": "7.88.1",
"fullversion": "7.88.1.post5+dr",
"suffix": ".post5+dr",
"library_type": "native",
"license": "MIT",
"upstream": "https://curl.se/download/curl-7.88.1.tar.gz",
"platform": "linux-x86_64",
"python": "any",
"dependencies": ["quantum-native-openssl==1.1.1.post3+dr"],
"changelog": []
}
```

### Package Name Extraction

Trivy extracts the canonical package name from the `upstream` field:

1. Parses the upstream URL (e.g., `https://curl.se/download/curl-7.88.1.tar.gz`)
2. Extracts the base filename and removes archive extensions
3. Removes version suffix to get the package name (e.g., `curl-7.88.1` → `curl`)
4. Falls back to the `library_name` field if URL parsing fails

### Version Mapping

Trivy uses the `version` field (upstream version without DataRobot suffix) rather than the `fullversion` field to match against vulnerability databases.

For example:
- `version`: `7.88.1` (used for CVE matching)
- `fullversion`: `7.88.1.post5+dr` (internal DataRobot version)

## Vulnerability Detection

Quantum packages are checked against the Conan ecosystem in the vulnerability database, which covers C/C++ packages and their vulnerabilities from:

- GitLab Advisory Database
- National Vulnerability Database (NVD)
- Other C/C++ security advisories

## Example

Given a container with quantum packages installed:

```bash
$ trivy image my-image:latest
```

Trivy will:
1. Detect `.dist-info/quantum.spec.json` files
2. Parse metadata to extract library name and version
3. Create package entries (e.g., `curl@7.88.1`)
4. Check against vulnerability databases
5. Report any CVEs found

## Limitations

- Transitive dependencies are not analyzed (quantum packages list dependencies but Trivy treats each package independently)
- Only native C/C++ library quantum packages are supported
- Vulnerability data is limited to what's available in the Conan/GitLab Advisory Database

[^1]: Located in `.dist-info` directories, e.g., `quantum_native_curl-7.88.1.post5+dr.dist-info/quantum.spec.json`
128 changes: 128 additions & 0 deletions pkg/dependency/parser/python/quantum/parse.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package quantum

import (
"encoding/json"
"net/url"
"path"
"strings"

"golang.org/x/xerrors"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
"github.com/aquasecurity/trivy/pkg/log"
xio "github.com/aquasecurity/trivy/pkg/x/io"
)

type Parser struct {
logger *log.Logger
}

func NewParser() *Parser {
return &Parser{
logger: log.WithPrefix("quantum"),
}
}

// QuantumSpec represents the structure of quantum.spec.json
type QuantumSpec struct {
Name string `json:"name"`
LibraryName string `json:"library_name"`
Version string `json:"version"`
FullVersion string `json:"fullversion"`
LibraryType string `json:"library_type"`
License string `json:"license"`
Upstream string `json:"upstream"`
Platform string `json:"platform"`
Python string `json:"python"`
Dependencies []string `json:"dependencies"`
}

// Parse parses quantum.spec.json files
func (p *Parser) Parse(r xio.ReadSeekerAt) ([]ftypes.Package, []ftypes.Dependency, error) {
var spec QuantumSpec
if err := json.NewDecoder(r).Decode(&spec); err != nil {
return nil, nil, xerrors.Errorf("failed to decode quantum.spec.json: %w", err)
}

// Extract the canonical package name from upstream URL
pkgName := p.extractPackageName(spec)
if pkgName == "" {
p.logger.Debug("Unable to determine package name from quantum spec",
log.String("quantum_name", spec.Name))
return nil, nil, xerrors.New("unable to determine package name")
}

// Use the upstream version (without DataRobot suffix)
version := spec.Version
if version == "" {
p.logger.Debug("Version is empty in quantum spec", log.String("name", spec.Name))
return nil, nil, xerrors.New("version is empty")
}

pkg := ftypes.Package{
ID: pkgName + "@" + version,
Name: pkgName,
Version: version,
}

// Add license if present
if spec.License != "" {
pkg.Licenses = []string{spec.License}
}

return []ftypes.Package{pkg}, nil, nil
}

// extractPackageName extracts the canonical package name from the quantum spec
func (p *Parser) extractPackageName(spec QuantumSpec) string {
// Try to extract from upstream URL first
if spec.Upstream != "" {
if name := p.extractFromUpstream(spec.Upstream); name != "" {
return name
}
}

// Fall back to library_name if upstream parsing fails
if spec.LibraryName != "" {
return spec.LibraryName
}

return ""
}

// extractFromUpstream extracts the package name from an upstream URL
// For example: "https://curl.se/download/curl-7.88.1.tar.gz" -> "curl"
func (p *Parser) extractFromUpstream(upstream string) string {
u, err := url.Parse(upstream)
if err != nil {
p.logger.Debug("Failed to parse upstream URL", log.String("url", upstream), log.Err(err))
return ""
}

// Get the base name from the URL path
// e.g., "curl-7.88.1.tar.gz"
basename := path.Base(u.Path)

// Remove common archive extensions
for _, ext := range []string{".tar.gz", ".tar.bz2", ".tar.xz", ".tgz", ".zip"} {
basename = strings.TrimSuffix(basename, ext)
}

// Try to extract package name by removing version pattern
// Examples:
// "curl-7.88.1" -> "curl"
// "openssl-1.1.1" -> "openssl"
// "libpng-1.6.37" -> "libpng"
parts := strings.Split(basename, "-")
if len(parts) >= 2 {
// Check if the last part looks like a version (starts with a digit)
lastPart := parts[len(parts)-1]
if len(lastPart) > 0 && (lastPart[0] >= '0' && lastPart[0] <= '9') {

Check failure on line 120 in pkg/dependency/parser/python/quantum/parse.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

emptyStringTest: replace `len(lastPart) > 0` with `lastPart != ""` (gocritic)
// Return everything except the last part (the version)
return strings.Join(parts[:len(parts)-1], "-")
}
}

// If we can't determine from the pattern, return the whole basename
return basename
}
128 changes: 128 additions & 0 deletions pkg/dependency/parser/python/quantum/parse_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
package quantum

import (
"os"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

ftypes "github.com/aquasecurity/trivy/pkg/fanal/types"
)

func TestParser_Parse(t *testing.T) {
tests := []struct {
name string

Check failure on line 15 in pkg/dependency/parser/python/quantum/parse_test.go

View workflow job for this annotation

GitHub Actions / Test (ubuntu-latest)

File is not properly formatted (gci)
file string
want []ftypes.Package
wantErr string
}{
{
name: "curl quantum package",
file: "testdata/curl-quantum.spec.json",
want: []ftypes.Package{
{
ID: "curl@7.88.1",
Name: "curl",
Version: "7.88.1",
Licenses: []string{"MIT"},
},
},
},
{
name: "openssl quantum package",
file: "testdata/openssl-quantum.spec.json",
want: []ftypes.Package{
{
ID: "openssl@1.1.1",
Name: "openssl",
Version: "1.1.1",
Licenses: []string{"OpenSSL"},
},
},
},
{
name: "libpng quantum package",
file: "testdata/libpng-quantum.spec.json",
want: []ftypes.Package{
{
ID: "libpng@1.6.37",
Name: "libpng",
Version: "1.6.37",
Licenses: []string{"libpng"},
},
},
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
f, err := os.Open(tt.file)
require.NoError(t, err)
defer f.Close()

p := NewParser()
got, _, err := p.Parse(f)

if tt.wantErr != "" {
require.ErrorContains(t, err, tt.wantErr)
return
}

require.NoError(t, err)
assert.Equal(t, tt.want, got)
})
}
}

func TestParser_extractFromUpstream(t *testing.T) {
tests := []struct {
name string
upstream string
want string
}{
{
name: "curl with tar.gz",
upstream: "https://curl.se/download/curl-7.88.1.tar.gz",
want: "curl",
},
{
name: "openssl with tar.gz",
upstream: "https://www.openssl.org/source/openssl-1.1.1.tar.gz",
want: "openssl",
},
{
name: "libpng with tar.xz",
upstream: "https://downloads.sourceforge.net/libpng/libpng-1.6.37.tar.xz",
want: "libpng",
},
{
name: "krb5 with tar.gz",
upstream: "https://web.mit.edu/kerberos/dist/krb5/krb5-1.20.tar.gz",
want: "krb5",
},
{
name: "package with zip",
upstream: "https://example.com/download/package-1.0.0.zip",
want: "package",
},
{
name: "package with tgz",
upstream: "https://example.com/download/package-2.1.5.tgz",
want: "package",
},
{
name: "no version in name",
upstream: "https://example.com/download/mypackage.tar.gz",
want: "mypackage",
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
p := NewParser()
got := p.extractFromUpstream(tt.upstream)
assert.Equal(t, tt.want, got)
})
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "quantum-native-curl",
"library_name": "curl",
"version": "7.88.1",
"fullversion": "7.88.1.post5+dr",
"suffix": ".post5+dr",
"library_type": "native",
"license": "MIT",
"upstream": "https://curl.se/download/curl-7.88.1.tar.gz",
"platform": "linux-x86_64",
"python": "any",
"dependencies": ["quantum-native-openssl==1.1.1.post3+dr"],
"changelog": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "quantum-native-libpng",
"library_name": "libpng",
"version": "1.6.37",
"fullversion": "1.6.37.post1+dr",
"suffix": ".post1+dr",
"library_type": "native",
"license": "libpng",
"upstream": "https://downloads.sourceforge.net/libpng/libpng-1.6.37.tar.xz",
"platform": "linux-x86_64",
"python": "any",
"dependencies": [],
"changelog": []
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"name": "quantum-native-openssl",
"library_name": "openssl",
"version": "1.1.1",
"fullversion": "1.1.1.post3+dr",
"suffix": ".post3+dr",
"library_type": "native",
"license": "OpenSSL",
"upstream": "https://www.openssl.org/source/openssl-1.1.1.tar.gz",
"platform": "linux-x86_64",
"python": "any",
"dependencies": [],
"changelog": []
}
Loading
Loading