Metadata-Version: 2.5
Name: jfrog-xray
Version: 0.7.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"}
```

### 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(repo="docker-local",
                                      path="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). One
`Vulnerability` per Xray *issue*, each with its nested CVEs (parsed CVSS),
structured affected components, and severity counts:

```python
report = x.artifacts.report(repo="docker-local", path="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(repo="docker-local", path="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 an
endpoint or field the typed resources don't cover? Use the raw escape hatch:

```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

There are **two model layers**:

* **Layer 1 — generated DTOs.** Faithful, 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`. Reach them via `summaries.*` (e.g.
  `summaries.artifact()`) when you need the raw payload.
* **Layer 2 — enriched models.** Hand-written, ergonomic views built *from* the
  Layer 1 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 model keeps a `.raw` back-reference to its
  source DTO (excluded from `model_dump()`). **Every ergonomic resource returns
  Layer 2** — `artifacts`, `violations`, `components`, `scans`, `licenses`.
  `summaries.*` is the one exception: it returns the raw Layer 1 DTO for fidelity.

The public names in `jfrog_xray.models` are curated re-exports of the enriched
models, the enums, and the Layer 1 DTOs. Where a Layer 2 model takes a DTO's
name, the DTO is available with a `DTO` suffix (e.g. `ViolationDTO`) — the type
of that model's `.raw`.

```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 curated aliases 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 — sync (`XrayClient`)
and async (`AsyncXrayClient`).

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

## License

MIT
