Metadata-Version: 2.5
Name: jfrog-xray
Version: 0.10.0
Summary: A modern, typed Python client for the JFrog Xray REST API (read side).
Project-URL: Homepage, https://github.com/helic0ptr/jfrog-xray
Project-URL: Repository, https://github.com/helic0ptr/jfrog-xray
Author: helic0ptr
License: MIT
License-File: LICENSE
Keywords: artifactory,cve,jfrog,sbom,sca,security,xray
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Security
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Classifier: Typing :: Typed
Requires-Python: >=3.12
Requires-Dist: httpx>=0.27
Requires-Dist: pydantic>=2.7
Description-Content-Type: text/markdown

# jfrog-xray

A modern, typed Python client for the **JFrog Xray REST API**.

## Install

```bash
uv add jfrog-xray        # or: pip install jfrog-xray
```

## Quickstart

```python
from jfrog_xray import XrayClient

# base_url is the JFrog Platform root; token is a Bearer access token. Both fall
# back to env vars: XRAY_URL / XRAY_BASE_URL / JFROG_URL and XRAY_TOKEN /
# JFROG_ACCESS_TOKEN.
with XrayClient(base_url="https://acme.jfrog.io", token="...") as x:
    x.system.ping()  # {"status": "pong"}
```

### `XrayApi` (raw) vs `XrayClient` (enriched)

The library is two layers:

* **`XrayApi`** speaks the REST API faithfully — its resource namespaces return
  the generated DTOs verbatim (wire field names, `list | None` collections,
  unparsed CVSS), 1:1 with the endpoints, with `extra_*` escape hatches so any
  documented field is reachable.
* **`XrayClient`** is the batteries-included facade built on an `XrayApi`. It
  returns the enriched Layer 2 models (parsed CVSS, normalized names, no `None`
  collections) and hosts the higher-level helpers (`artifacts.report`, `by_cve`).

Reach the raw client from the facade with `.api`, and the source DTO of any
enriched model inline with `.raw`:

```python
x = XrayClient(base_url="https://acme.jfrog.io", token="...")   # or XrayClient(api)
v = x.violations.list(watch_name="prod").items[0]   # enriched Violation
v.raw                                                # its source DTO, inline
x.api.violations.list(watch_name="prod")            # SyncPage[Violation] — raw DTOs
```

### Async

`AsyncXrayClient` mirrors `XrayClient` exactly — every resource method is a
coroutine, and paginated resources return an `AsyncPage`:

```python
from jfrog_xray import AsyncXrayClient

async with AsyncXrayClient(base_url="https://acme.jfrog.io", token="...") as x:
    report = await x.artifacts.report(path="docker-local/nginx/1.25/manifest.json")
    page = await x.violations.list(watch_name="prod", min_severity="High")
    async for v in page.auto_paging_iter():
        print(v.issue_id, v.severity)
```

## Headline: CVEs for an artifact

Get the enriched report for a single artifact (via Summary v2). Identify it by
its full, repo-prefixed path — the Artifactory repo path, starting with the
repository key. One `Vulnerability` per Xray *issue*, each with its nested CVEs
(parsed CVSS), structured affected components, and severity counts:

```python
report = x.artifacts.report(path="docker-local/nginx/1.25/manifest.json")
print(report.severity_breakdown)     # critical/high/medium/... counts
for v in report.vulnerabilities:
    for cve in v.cves:
        print(cve.id, v.severity, cve.cvss_v3.score, v.fixed_versions)

# Just the vulnerabilities (== report(...).vulnerabilities):
x.artifacts.vulnerabilities(path="docker-local/nginx/1.25/manifest.json")

# A batch, one report per identifier (unscanned artifacts come back in-band):
reports = x.artifacts.reports(paths=["docker-local/nginx/1.25/manifest.json"])
for r in reports.errors:
    print(r.path, r.error)

# ...or identify the artifact by checksum:
x.artifacts.report(sha256="9f6c...")
```

Or download a full report / SBOM (ZIP) for it:

```python
x.artifacts.export(
    component_name="nginx",
    package_type="docker",
    vulnerabilities=True,
    sbom="cyclonedx",      # or "spdx"
    out="nginx-report.zip",
)
```

## Other read APIs

Every method below returns an enriched Layer 2 model (see [Models](#models)).

```python
# Violations. Iterating a page yields ONE page; .total is the server's count.
page = x.violations.list(watch_name="prod", min_severity="High")
for v in page:                      # this page only
    print(v.issue_id, v.severity, v.details_url)
for v in page.auto_paging_iter():   # opt in to streaming every page
    ...

# Summaries — the raw (Layer 1) fidelity path
summary = x.summaries.artifact(paths=["docker-local/nginx/1.25/manifest.json"])
build = x.summaries.build(build_name="my-app", build_number="42")

# CVE / component lookups
for m in x.components.search_by_cves(["CVE-2021-44228"]):
    print(m.cve_id, m.severity, [c.name for c in m.affected_components])
x.components.search_cves_by_components(["gav://com.example:app:1.0.0"])
for r in x.components.impacted_resources(vulnerability="CVE-2021-44228"):
    print(r.repo, r.path)

# Scan status & licenses
x.scans.artifact_status(repo="docker-local", path="nginx/1.25/manifest.json").overall_status
x.licenses.list()
```

## Configuration

```python
XrayClient(
    base_url=...,          # or XRAY_URL / XRAY_BASE_URL / JFROG_URL
    token=...,             # or XRAY_TOKEN / JFROG_ACCESS_TOKEN; or auth=<httpx.Auth>
    timeout=30.0,          # float seconds or httpx.Timeout
    max_retries=2,         # connection/timeout/5xx/429, honoring Retry-After
    verify=True,           # False to disable TLS verification, or an ssl.SSLContext
    proxy="http://...",    # proxy URL
    http_client=...,       # inject a preconfigured httpx.Client (Async: httpx.AsyncClient)
)

# Per-call overrides (shares the same underlying HTTP client):
x.with_options(timeout=5.0, max_retries=0).system.ping()
```

Auth and the default headers are applied per request, so an injected
`http_client` (for a custom transport or pool) stays authenticated.

Need a field the typed resources don't cover? Prefer, in order: `.api` for the
raw DTOs on any endpoint; an `extra_*` merge for an unmodeled filter
(`violations.list(..., extra_filters={...})`,
`components.impacted_resources(..., extra_params={...})`,
`artifacts.export(..., extra_body={...})`); and finally the raw HTTP escape hatch
for an unmodeled endpoint:

```python
resp = x.request("POST", "/xray/api/v2/summary/artifact", json={"paths": ["repo/x"]})
data = resp.json()   # an httpx.Response, already retried and status-checked
```

## Errors

All errors derive from `XrayError`. HTTP failures raise an `APIStatusError`
subclass carrying `.status_code`, `.response`, and `.body`:

`BadRequestError` (400), `AuthenticationError` (401), `PermissionDeniedError`
(403), `NotFoundError` (404), `ConflictError` (409), `UnprocessableEntityError`
(422), `RateLimitError` (429, with `.retry_after`), `InternalServerError` (5xx).
Network problems raise `APIConnectionError` / `APITimeoutError`.

## Models

Two layers, one package each:

* **`jfrog_xray.models` — the faithful DTOs (Layer 1).** Permissive mirrors of
  the wire shape, **generated from JFrog's own OpenAPI**. JFrog publishes it per
  endpoint (docs reference pages), so `scripts/pull_spec.py` pulls the endpoints
  listed in `spec/manifest.txt`, hoists each inline response schema into a named
  component, and merges them into the vendored `spec/xray.openapi.yaml`;
  `scripts/gen_models.py` then runs `datamodel-code-generator` over that into
  `src/jfrog_xray/models/_generated.py`. This is what `XrayApi` returns (and what
  `summaries.*` returns on either client).
* **`jfrog_xray.enrich` — the enriched views (Layer 2).** Hand-written, ergonomic
  views built *from* the DTOs (`ArtifactReport`, `Vulnerability`, `CveMatch`,
  `Violation`, `ComponentDetail`, `CvssScore`, …): collections default to `[]`,
  wire names are normalized (`violation_details_url` → `details_url`, `isCustom` →
  `is_custom`), CVSS strings are parsed, and each keeps a `.raw` back-reference to
  its source DTO (excluded from `model_dump()`). This is what `XrayClient`
  returns; the views are also re-exported from the top-level `jfrog_xray` package.

Both layers use **clean names, disambiguated by package**: `from jfrog_xray import
Violation` (the enriched view) vs `from jfrog_xray.models import Violation` (the
DTO). You rarely import a DTO directly — `.raw` and `.api` hand them back for you.

```bash
# refresh from JFrog's docs (needs internet), then regenerate:
uv run --group codegen python scripts/pull_spec.py --refresh
uv run --group codegen python scripts/gen_models.py
git diff spec/ src/jfrog_xray/models/_generated.py   # review, run tests, commit
```

`tests/test_generated.py` guards both directions: the public DTO names in
`jfrog_xray.models` point at the right generated classes, and `gen_models.py
--check` fails if the committed models are stale versus the spec.

## Development

```bash
uv sync
uv run ruff check src tests
uv run mypy src
uv run pytest                 # unit tests (respx-mocked; no network)
# the drift check also runs when the codegen group is present:
uv run --group dev --group codegen pytest
```

### Integration tests

Live tests in `tests/test_integration.py` run read-only against a real JFrog
Platform. They **skip** unless `XRAY_URL` (or `XRAY_BASE_URL`) and `XRAY_TOKEN`
are set; individual tests skip when their resource env var is absent.

```bash
export XRAY_URL="https://acme.jfrog.io"
export XRAY_TOKEN="..."
# optional, to exercise resource-specific tests:
export XRAY_TEST_ARTIFACT_PATH="docker-local/nginx/1.25/manifest.json"
export XRAY_TEST_CVE="CVE-2021-44228"
# ...see the module docstring for the full list

uv run pytest -m integration
```

## Scope

**Read side:** system, summaries, violations, CVE/component lookups, scan
status, licenses, and the `artifacts` convenience resource — via the faithful
`XrayApi` / `AsyncXrayApi` and the enriching `XrayClient` / `AsyncXrayClient`
facade.

**Deferred:** the async Reports API (create → poll → paginated content → delete),
governance reads (watches / policies / ignore rules), and all write-side actions.

## License

MIT
