Metadata-Version: 2.4
Name: pytrivy
Version: 0.1.2
Summary: A Python SDK for the Trivy CLI: scan container images, query vulnerabilities, licenses, packages, and generate SBOMs from Python.
Project-URL: Homepage, https://github.com/whosthatbloke/pytrivy
Project-URL: Repository, https://github.com/whosthatbloke/pytrivy
Project-URL: Issues, https://github.com/whosthatbloke/pytrivy/issues
Author-email: whosthatbloke <github.3rqh7@passmail.net>
License: MIT License
        
        Copyright (c) 2026 whosthatbloke
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
License-File: LICENSE
Keywords: container,cve,cyclonedx,sbom,security,spdx,trivy,vulnerability
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: pyexcel
Requires-Dist: pyexcel-xls
Requires-Dist: pyexcel-xlsx
Requires-Dist: pyexcel-xlsxw
Requires-Dist: typeguard
Description-Content-Type: text/markdown

# TrivyPy

TrivyPy is a Python SDK that wraps the [Trivy](https://trivy.dev) CLI and turns its JSON output into queryable Python objects. Instead of writing shell scripts to grep through scan results or manually parsing nested JSON, you import a class and call methods.

```python
client = TrivyClient("python:3.13-slim", TrivyScanCommandEnum.Image)
client.scan()

print(client.criticals())           # ['CVE-2024-...', ...]
print(client.vulnerability_in_targets("CVE-2024-1234"))  # ['usr/bin/python3']
print(client.purl("openssl@3.0.2-0ubuntu1.12.x86_64"))  # 'pkg:deb/ubuntu/openssl@...'
```

The same API works whether you just ran a live scan or loaded a JSON file saved weeks ago. Results are stored in the client object — scan once, query as many times as you like.

---

## Installation

```bash
pip install trivypy
```

Or use Docker (Trivy is pre-installed in the image):

```bash
# Clone and cd into repo
docker buildx bake -f docker/docker-bake.hcl
docker run -it docker.io/library/trivy-py:latest python3
```

Trivy must be installed and on your `PATH` to run live scans. To install it:

```bash
brew install trivy          # macOS
# or see https://trivy.dev/latest/getting-started/installation/
```

---

## Core concepts

**Lazy scanning.** Creating a `TrivyClient` does not trigger a scan. You call `.scan()` explicitly, or use `.from_file()` / `.from_dict()` to load existing results. This means you can load a JSON file produced by your CI pipeline and query it locally without re-scanning.

**Consistent API shape.** Every data type (vulnerabilities, licenses, packages) follows the same pattern:

| Method | Returns |
|---|---|
| `raw_X()` | Raw dicts straight from the Trivy JSON |
| `X()` | List of names/IDs |
| `unique_X()` | Deduplicated names/IDs |
| `X_in_targets(name)` | Which targets contain this item |

---

## Classes

### `TrivyClient`

The core class. Wraps a single Trivy scan target.

### `TrivyComparator`

Compares two `TrivyClient` instances to find which CVEs were fixed and which persist across versions.

### `TrivyAnalyzer`

Runs aggregate queries across a collection of `TrivyClient` instances — useful for analysing an entire release or fleet.

---

## Use cases

### 1. Scan a container image

```python
from trivy.classes import TrivyClient
from trivy.enums import TrivyScanCommandEnum
from trivy.reports import generate_excel_report

client = TrivyClient("python:3.13-slim", TrivyScanCommandEnum.Image)
client.scan()

# Counts by severity
print(len(client.criticals()))
print(len(client.highs()))
print(len(client.mediums()))
print(len(client.lows()))
print(len(client.unknowns()))

# All unique CVE IDs
print(client.unique_vulnerabilities())

# Full detail for one CVE
details = client.vulnerability("CVE-2024-1234")
print(details["Severity"], details["Description"])

# Which targets inside the image contain a specific CVE
print(client.vulnerability_in_targets("CVE-2024-1234"))

# Generate an Excel report
generate_excel_report(client)
# -> python__3.13-slim_1718000000_report.xlsx
```

You can also scan a filesystem path or a config directory:

```python
client = TrivyClient("/path/to/app", TrivyScanCommandEnum.FileSystem)
client = TrivyClient("/path/to/infra", TrivyScanCommandEnum.Config)
```

---

### 2. Load saved scan results

If you already have Trivy JSON output (e.g. from CI), load it directly — no re-scan needed.

```python
# From a file on disk
client = TrivyClient.from_file("scan_results.json")

# From a dict already in memory
import json
with open("scan_results.json") as f:
    data = json.load(f)
client = TrivyClient.from_dict(data)
```

`from_dict` and `from_file` automatically detect the scan type (`image`, `fs`, `config`, `sbom`) from the JSON, so you don't need to specify it.

---

### 3. Query licenses

```python
client = TrivyClient("nginx:latest", TrivyScanCommandEnum.Image)
client.scan()

# All license names found in the image
print(client.licenses())              # ['MIT', 'Apache-2.0', 'GPL-2.0', ...]
print(client.unique_licenses())       # deduplicated

# Look up details for a specific license
details = client.license("MIT")

# Which targets inside the image use a specific license
print(client.license_in_targets("GPL-2.0"))
```

---

### 4. Query packages (SBOM)

Every scan result includes the list of packages Trivy found. Package names are **not unique** — the same name can appear in multiple targets with different versions or architectures. Use the Trivy ID or PURL for unambiguous lookups.

```python
client = TrivyClient("python:3.13-slim", TrivyScanCommandEnum.Image)
client.scan()

# All package names (may contain duplicates across targets)
print(client.packages())
print(client.unique_packages())

# Look up all packages with a given name (returns a list — there may be more than one)
matches = client.packages_named("openssl")
for pkg in matches:
    print(pkg["ID"], pkg["Version"])

# Unambiguous lookup by Trivy ID
pkg = client.package_by_id("openssl@3.0.2-0ubuntu1.12.amd64")
print(pkg["Version"])

# PURL — the standard identifier used in CycloneDX/SPDX SBOMs
print(client.purl("openssl@3.0.2-0ubuntu1.12.amd64"))
# -> pkg:deb/ubuntu/openssl@3.0.2-0ubuntu1.12?arch=amd64&distro=ubuntu-22.04

# Direct dependencies of a package
print(client.dependencies("openssl@3.0.2-0ubuntu1.12.amd64"))
# -> ['libc6@2.35-0ubuntu3.6.amd64', 'libssl3@3.0.2-0ubuntu1.12.amd64']

# All packages in a specific target
print(client.packages_in_target("python:3.13-slim (debian 12.5)"))
```

You can also scan an existing SBOM file directly:

```python
client = TrivyClient("bom.cdx.json", TrivyScanCommandEnum.SBOM)
client.scan()
```

---

### 5. Generate an SBOM

`sbom()` is a separate Trivy invocation that produces a CycloneDX or SPDX document for the target. It is independent of `scan()` — the target does not need to have been vulnerability-scanned first, and calling `sbom()` does not populate `scan_results()`.

```python
from trivy.classes import TrivyClient
from trivy.enums import TrivyScanCommandEnum, SBOMFormatEnum

client = TrivyClient("python:3.13-slim", TrivyScanCommandEnum.Image)

# Generate a CycloneDX SBOM (default) and get it back as a dict
sbom = client.sbom()
print(sbom["bomFormat"])      # CycloneDX
print(sbom["specVersion"])    # 1.5
print(sbom["components"])     # list of components

# Generate an SPDX SBOM instead
sbom = client.sbom(output_format=SBOMFormatEnum.SPDX)

# Write the output to a file as well as returning the dict
sbom = client.sbom(output_file="python-slim.cdx.json")

# Both options together
sbom = client.sbom(output_format=SBOMFormatEnum.SPDX, output_file="python-slim.spdx.json")
```

Results are cached per format — calling `sbom()` twice with the same format runs Trivy only once:

```python
client.sbom()  # runs Trivy
client.sbom()  # returns cached result

# Access cached results directly
cached = client.sbom_results()
cyclonedx_data = cached[SBOMFormatEnum.CycloneDX]
```

---

### 6. Compare two versions


Use `TrivyComparator` to understand what changed between an old and new version of the same image. Both clients are scanned automatically if they haven't been already.

```python
from trivy.classes import TrivyClient, TrivyComparator
from trivy.enums import TrivyScanCommandEnum

old = TrivyClient("myapp:1.0.0", TrivyScanCommandEnum.Image)
new = TrivyClient("myapp:1.1.0", TrivyScanCommandEnum.Image)

comp = TrivyComparator(old, new)

# CVEs present in 1.0.0 but gone in 1.1.0 — successfully remediated
print(comp.fixed_vulnerabilities())

# CVEs still present in both — carry-over risks that still need attention
print(comp.persisting_vulnerabilities())
```

You can load from saved files to compare without re-scanning:

```python
old = TrivyClient.from_file("myapp_1.0.0_scan.json")
new = TrivyClient.from_file("myapp_1.1.0_scan.json")
comp = TrivyComparator(old, new)
```

---

### 7. Analyse a fleet or release

Use `TrivyAnalyzer` when you have multiple images and want cross-cutting questions: which CVE affects the most images, which image has the most vulnerabilities, what does the severity breakdown look like across everything.

```python
from trivy.classes import TrivyClient, TrivyAnalyzer
from trivy.enums import TrivyScanCommandEnum, SeverityEnum

clients = [
    TrivyClient("myapp-api:1.0.0", TrivyScanCommandEnum.Image),
    TrivyClient("myapp-worker:1.0.0", TrivyScanCommandEnum.Image),
    TrivyClient("myapp-scheduler:1.0.0", TrivyScanCommandEnum.Image),
]

# TrivyAnalyzer scans any unscanned clients automatically
analyzer = TrivyAnalyzer(clients)

# Which image(s) have the most / fewest vulnerabilities
print(analyzer.clients_with_highest_vulnerabilities())
print(analyzer.clients_with_lowest_vulnerabilities())

# Most widespread CVEs — the ones affecting the most images
# Returns [(cve_id, count), ...] sorted by count descending
print(analyzer.most_common_cves(limit=10))

# Bottleneck CVEs — grouped by how many images they appear in,
# with the list of affected clients. Useful for prioritising base image updates.
for cve, count, affected in analyzer.bottlenecks():
    print(f"{cve} affects {count} images: {[c.trivy_target for c in affected]}")

# Severity breakdown across the entire fleet
severities = analyzer.severities()
print(severities[SeverityEnum.Critical.value])  # list of unique critical CVE IDs
print(severities[SeverityEnum.High.value])

# Find which images contain a specific CVE
affected = analyzer.find_images_with_cve("CVE-2024-1234")
print([c.trivy_target for c in affected])
```

**Comparing two releases** — load the old release from saved files and compare the severity profile:

```python
from trivy.enums import SeverityEnum

old_release = TrivyAnalyzer([
    TrivyClient.from_file("v1/api_scan.json"),
    TrivyClient.from_file("v1/worker_scan.json"),
])

new_release = TrivyAnalyzer([
    TrivyClient.from_file("v2/api_scan.json"),
    TrivyClient.from_file("v2/worker_scan.json"),
])

old_sev = old_release.severities()
new_sev = new_release.severities()

for severity in SeverityEnum:
    old_count = len(old_sev[severity.value])
    new_count = len(new_sev[severity.value])
    delta = old_count - new_count
    print(f"{severity.value}: {old_count} -> {new_count}  ({'+' if delta >= 0 else ''}{delta} resolved)")

print(f"Total unique CVEs resolved: "
      f"{len(old_release.unique_vulnerabilities()) - len(new_release.unique_vulnerabilities())}")
```

---

### 8. Generate an Excel report

`generate_excel_report` produces a multi-sheet `.xlsx` file from any scanned `TrivyClient`.

```python
from trivy.classes import TrivyClient
from trivy.enums import TrivyScanCommandEnum
from trivy.reports import generate_excel_report

client = TrivyClient("nginx:latest", TrivyScanCommandEnum.Image)
client.scan()

# Auto-generated filename: nginx__latest_1718000000_report.xlsx
filename = generate_excel_report(client)

# Or specify your own filename (no extension needed)
generate_excel_report(client, filename="nginx_report.xlsx")
```

The workbook contains four sheets:

| Sheet | Contents |
|---|---|
| **Severity Summary** | Count of CVEs per severity level plus a total |
| **Vulnerabilities** | One row per CVE with ID, severity, description, package name, installed version, fixed version, and all CVSS scores flattened to columns |
| **Packages** | One row per package with target, ecosystem type, Trivy ID, name, version, licenses, and PURL |
| **Targets** | List of all scan targets found inside the image |

---

## Exception handling

All lookup methods raise specific exceptions when an item is not found, so you can handle them cleanly.

```python
from trivy.exceptions import (
    VulnerabilityDoesntExist,
    LicenseDoesntExist,
    PackageDoesntExist,
    TargetDoesntExist,
    TrivyClientNotScanned,
    UnknownImage,
)

# Vulnerability lookup
try:
    client.vulnerability("CVE-2099-99999")
except VulnerabilityDoesntExist:
    print("CVE not found in this scan")

# Generating a report before scanning
try:
    generate_excel_report(client)
except TrivyClientNotScanned:
    client.scan()
    generate_excel_report(client)

# Scanning an image that doesn't exist
try:
    client.scan()
except UnknownImage:
    print("Image not found in registry")
```
