Metadata-Version: 2.4
Name: secpod-saner-sdk
Version: 0.1.0.dev0
Summary: Official Python SDK for SecPod Saner APIs
Author-email: Harsh Gupta <harsh.gupta@secpod.com>
License-Expression: LicenseRef-Proprietary
Project-URL: Documentation, https://apidocs-cvem.readme.io/docs/installation
Project-URL: Homepage, https://www.secpod.com/saner-cvem/
Keywords: saner,secpod,cybersecurity,vulnerability,api,sdk
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: Intended Audience :: System Administrators
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3 :: Only
Classifier: Programming Language :: Python :: 3.9
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
Classifier: Operating System :: OS Independent
Classifier: Typing :: Typed
Requires-Python: >=3.9
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: requests>=2.30.0
Provides-Extra: dev
Requires-Dist: ruff; extra == "dev"
Requires-Dist: mypy; extra == "dev"
Requires-Dist: types-requests; extra == "dev"
Requires-Dist: pytest; extra == "dev"
Requires-Dist: pytest-cov; extra == "dev"
Requires-Dist: python-dotenv; extra == "dev"
Dynamic: license-file

# Saner SDK for Python

Official Python SDK for the **SecPod Saner CVEM** platform — continuous vulnerability and exposure management, patching, compliance, and endpoint management.

📖 **Full API reference: [apidocs-cvem.readme.io](https://apidocs-cvem.readme.io/docs/installation)**

This README is a quickstart. Every endpoint, parameter, and response shape is documented on the developer portal.

---

## Installation

```bash
pip install secpod-saner-sdk
```

Requires Python 3.9+. The only runtime dependency is `requests`.

## Quickstart

```python
from saner import SanerClient

client = SanerClient(
    api_key="<YOUR_API_KEY>",
    accountid="<YOUR_ACCOUNT>",
    base_url="https://saner.secpod.com",
)

orgs = client.platform.Organization.get()
print(orgs)

client.close()
```

The client owns pooled HTTP connections, so close it when you're done — or let a
`with` block do it:

```python
with SanerClient(api_key="...", accountid="...") as client:
    devices = client.cvem.Device.get(ip="192.168.1.10")
```

## Configuration

`api_key`, `accountid`, `base_url`, and `verify_ssl` all fall back to
environment variables when omitted, so `SanerClient()` works unmodified across
dev, CI, and containers — only the environment differs. An explicit argument
always wins over the environment, which keeps the
[multi-client pattern](#resources) (several clients, several accounts, one
script) isolated even when the variables are set.

```python
# with SANER_API_KEY / SANER_ACCOUNT_ID set in the environment
client = SanerClient()
```

| Argument | Default | Description |
| --- | --- | --- |
| `api_key` | *required* — or `SANER_API_KEY` | Saner API key, sent as the `Authorization` header. |
| `accountid` | *required* — or `SANER_ACCOUNT_ID` | Account the client operates against. |
| `base_url` | `SANER_BASE_URL`, else `https://saner.secpod.com` | Saner server URL. Use your regional or on-prem host. |
| `verify_ssl` | `SANER_VERIFY_SSL` (`"true"`/`"false"`), else `True` | Verify TLS certificates. Only disable against a lab server or an internal host with a self-signed cert. |
| `timeout` | `30` | Per-request timeout, in seconds. |
| `max_retries` | `3` | Retries after the first attempt. `0` disables retrying. |
| `retry_backoff` | `1.0` | Base for exponential backoff, in seconds. |
| `retry_on_write` | `False` | Also retry requests that may mutate server state. See below. |
| `enable_logging` | `False` | Write a JSONL request/response log. |
| `log_dir` | `"logs"` | Directory for the JSONL log. |

## Resources

Resources are split into two products, each with its own client if you only
need one of them:

| | Resources | Client |
| --- | --- | --- |
| **platform** — shared across every Saner product | `Organization`, `Account`, `User`, `Group`, `ServicePrivision`, `Report`, `MFA` | `from saner.platform import PlatformClient` → flat: `client.Organization` |
| **cvem** — vulnerability management, patching, compliance, endpoints | `Agent`, `Device`, `Configuration`, `CyberHygiene`, `Vulnerability`, `Compliance`, `Patch`, `RiskPrioritization`, `PostureAnomaly`, `AssetExposure`, `Endpoint`, `NetworkScanner`, `AD` | `from saner.cvem import CvemClient` → flat: `client.Patch` |

`SanerClient` (the universal client) nests both under `client.platform.*` /
`client.cvem.*` instead. Prefer it when your process touches both products —
each client *instance* opens its own pair of pooled sessions, so creating a
`PlatformClient` and a `CvemClient` side by side doubles connection overhead
for no benefit over one `SanerClient`. Reach for `PlatformClient`/`CvemClient`
only when you exclusively work within one product and want flatter attribute
access.

Methods return the parsed JSON response as a `dict`. Endpoints that produce a
file (`Report.getPdf`, `AD.downloadAgent`) return `bytes` instead:

```python
with SanerClient(api_key="...", accountid="...") as client:
    report = client.platform.Report.getPdf(accountid="MyAccount", reportname="Asset Report")
    if isinstance(report, bytes):
        open("report.zip", "wb").write(report)
```

## Error handling

Every SDK error derives from `SanerError` and carries structured context, so you
can branch on it without parsing message strings.

```python
from saner import SanerClient, SanerAuthError, SanerRateLimitError, SanerError

try:
    client.cvem.Device.get(ip="192.168.1.10")
except SanerAuthError:
    ...                                  # 401 / 403 — bad or expired key
except SanerRateLimitError as e:
    print("retry after", e.retry_after)  # 429, exhausted retries
except SanerError as e:
    print(e.status_code, e.method, e.url, e.attempts, e.response_body)
```

| Exception | Raised when |
| --- | --- |
| `SanerAuthError` | HTTP 401 / 403. |
| `SanerRateLimitError` | HTTP 429 after retries are exhausted. Exposes `retry_after`. |
| `SanerRequestError` | Network failure, SSL error, or a non-retryable HTTP error. |
| `SanerResponseError` | Server replied successfully but the body was not valid JSON. |
| `SanerError` | Base class — catch this to handle everything. |

Invalid arguments raise plain `ValueError` / `TypeError` before any network call
is made.

## Retries

The SDK retries HTTP 429 and 500/502/503/504, plus connection failures, using
exponential backoff with jitter capped at 60s. A `Retry-After` header always
takes precedence over the computed delay.

**Writes are not retried by default.** Replaying a request that already reached
the server can duplicate it — creating the same remediation job twice, for
example. So retries apply to read-only calls only. Two exceptions are always
safe and always retried: HTTP 429 (the server refused to process the request)
and connect timeouts (the request never arrived).

Set `retry_on_write=True` if your workload is idempotent and you'd rather have
the retries.

## Logging

With `enable_logging=True` the SDK writes one JSON object per request to
`<log_dir>/secpod-saner-sdk__<account>__<hash>.jsonl`, rotating weekly and keeping 8
backups.

Credentials (`password`, `client_secret`, `token`, `ssl`, …) are replaced with
`***REDACTED***` and long values are truncated. Response bodies are **not**
redacted, and in a CVEM product those contain asset inventories and CVE
exposure data — treat the log directory as sensitive.

For application-level logging, the SDK also emits retry warnings through the
standard `logging` module under the `saner.*` logger namespace.

## Thread safety

None of the three client classes (`SanerClient`, `PlatformClient`, `CvemClient`)
are thread-safe — each wraps `requests.Session`, which does not guarantee
thread safety. Create one client per thread.

## Support

- API reference: <https://apidocs-cvem.readme.io/docs/installation>
- Support: <support@secpod.com>

## License

Proprietary — see [LICENSE](LICENSE). Use is governed by your SecPod license agreement.
