attestd 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- attestd-0.1.0/.github/workflows/publish.yml +61 -0
- attestd-0.1.0/.github/workflows/test.yml +30 -0
- attestd-0.1.0/.gitignore +34 -0
- attestd-0.1.0/PKG-INFO +316 -0
- attestd-0.1.0/README.md +289 -0
- attestd-0.1.0/attestd/__init__.py +74 -0
- attestd-0.1.0/attestd/_internal.py +118 -0
- attestd-0.1.0/attestd/_version.py +1 -0
- attestd-0.1.0/attestd/client.py +237 -0
- attestd-0.1.0/attestd/errors.py +99 -0
- attestd-0.1.0/attestd/models.py +66 -0
- attestd-0.1.0/attestd/py.typed +0 -0
- attestd-0.1.0/attestd/testing.py +241 -0
- attestd-0.1.0/pyproject.toml +46 -0
- attestd-0.1.0/tests/__init__.py +0 -0
- attestd-0.1.0/tests/conftest.py +61 -0
- attestd-0.1.0/tests/test_async_client.py +163 -0
- attestd-0.1.0/tests/test_client.py +222 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
|
|
3
|
+
# Triggers on version tags: git tag v0.1.0 && git push --tags
|
|
4
|
+
on:
|
|
5
|
+
push:
|
|
6
|
+
tags:
|
|
7
|
+
- "v*"
|
|
8
|
+
|
|
9
|
+
# Grant read access to repo contents for all jobs.
|
|
10
|
+
# The publish job additionally needs id-token: write for OIDC.
|
|
11
|
+
permissions:
|
|
12
|
+
contents: read
|
|
13
|
+
|
|
14
|
+
jobs:
|
|
15
|
+
# Run the full test matrix before publishing — never ship a broken release.
|
|
16
|
+
test:
|
|
17
|
+
name: Test Python ${{ matrix.python-version }}
|
|
18
|
+
runs-on: ubuntu-latest
|
|
19
|
+
strategy:
|
|
20
|
+
fail-fast: true
|
|
21
|
+
matrix:
|
|
22
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
23
|
+
steps:
|
|
24
|
+
- uses: actions/checkout@v4
|
|
25
|
+
- uses: actions/setup-python@v5
|
|
26
|
+
with:
|
|
27
|
+
python-version: ${{ matrix.python-version }}
|
|
28
|
+
- name: Install package and dev dependencies
|
|
29
|
+
run: pip install -e ".[dev]"
|
|
30
|
+
- name: Run tests
|
|
31
|
+
run: python -m pytest tests/ -v --tb=short
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
name: Build and publish to PyPI
|
|
35
|
+
needs: test # only runs if all test matrix jobs pass
|
|
36
|
+
runs-on: ubuntu-latest
|
|
37
|
+
environment: pypi # GitHub environment with protection rules (optional but recommended)
|
|
38
|
+
|
|
39
|
+
permissions:
|
|
40
|
+
contents: read # required for actions/checkout
|
|
41
|
+
id-token: write # required for OIDC trusted publisher auth
|
|
42
|
+
|
|
43
|
+
steps:
|
|
44
|
+
- uses: actions/checkout@v4
|
|
45
|
+
|
|
46
|
+
- uses: actions/setup-python@v5
|
|
47
|
+
with:
|
|
48
|
+
python-version: "3.12"
|
|
49
|
+
|
|
50
|
+
- name: Install build tools
|
|
51
|
+
run: pip install build
|
|
52
|
+
|
|
53
|
+
- name: Build sdist and wheel
|
|
54
|
+
run: python -m build
|
|
55
|
+
|
|
56
|
+
- name: Verify distributions
|
|
57
|
+
run: pip install twine && twine check dist/*
|
|
58
|
+
|
|
59
|
+
- name: Publish to PyPI
|
|
60
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
61
|
+
# No api-token needed — authentication via OIDC trusted publisher
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
name: Tests
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
test:
|
|
11
|
+
name: Python ${{ matrix.python-version }}
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
strategy:
|
|
14
|
+
fail-fast: false
|
|
15
|
+
matrix:
|
|
16
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
17
|
+
|
|
18
|
+
steps:
|
|
19
|
+
- uses: actions/checkout@v4
|
|
20
|
+
|
|
21
|
+
- name: Set up Python ${{ matrix.python-version }}
|
|
22
|
+
uses: actions/setup-python@v5
|
|
23
|
+
with:
|
|
24
|
+
python-version: ${{ matrix.python-version }}
|
|
25
|
+
|
|
26
|
+
- name: Install package and dev dependencies
|
|
27
|
+
run: pip install -e ".[dev]"
|
|
28
|
+
|
|
29
|
+
- name: Run tests
|
|
30
|
+
run: python -m pytest tests/ -v --tb=short
|
attestd-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*.pyo
|
|
5
|
+
*.pyd
|
|
6
|
+
*.so
|
|
7
|
+
*.egg
|
|
8
|
+
*.egg-info/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.eggs/
|
|
12
|
+
|
|
13
|
+
# Virtual environments
|
|
14
|
+
.venv*/
|
|
15
|
+
venv*/
|
|
16
|
+
env*/
|
|
17
|
+
|
|
18
|
+
# Testing
|
|
19
|
+
.pytest_cache/
|
|
20
|
+
.coverage
|
|
21
|
+
htmlcov/
|
|
22
|
+
|
|
23
|
+
# Type checking
|
|
24
|
+
.mypy_cache/
|
|
25
|
+
.ruff_cache/
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.vscode/
|
|
29
|
+
.idea/
|
|
30
|
+
*.swp
|
|
31
|
+
|
|
32
|
+
# OS
|
|
33
|
+
.DS_Store
|
|
34
|
+
Thumbs.db
|
attestd-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: attestd
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python SDK for the Attestd security risk API
|
|
5
|
+
Project-URL: Homepage, https://attestd.io
|
|
6
|
+
Project-URL: Docs, https://attestd.io/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/attestd/attestd-python
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/attestd/attestd-python/issues
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
Keywords: ai-agent,appsec,cve,devsecops,security,vulnerability
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
14
|
+
Classifier: Programming Language :: Python :: 3
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Topic :: Security
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
21
|
+
Requires-Dist: httpx>=0.27.0
|
|
22
|
+
Provides-Extra: dev
|
|
23
|
+
Requires-Dist: anyio[trio]; extra == 'dev'
|
|
24
|
+
Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
26
|
+
Description-Content-Type: text/markdown
|
|
27
|
+
|
|
28
|
+
# attestd-python
|
|
29
|
+
|
|
30
|
+
Python SDK for the [Attestd](https://attestd.io) security risk API.
|
|
31
|
+
|
|
32
|
+
Attestd returns vulnerability risk assessments for open-source software components — suitable for CI/CD deployment gates, autonomous agent tool calls, and security dashboards.
|
|
33
|
+
|
|
34
|
+
## Installation
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
pip install attestd
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Requires Python 3.10+.
|
|
41
|
+
|
|
42
|
+
## Quick start
|
|
43
|
+
|
|
44
|
+
### Sync
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import attestd
|
|
48
|
+
|
|
49
|
+
with attestd.Client(api_key="atst_...") as client:
|
|
50
|
+
result = client.check("nginx", "1.20.0")
|
|
51
|
+
|
|
52
|
+
print(result.risk_state) # "high"
|
|
53
|
+
print(result.actively_exploited) # False
|
|
54
|
+
print(result.cve_ids) # ["CVE-2021-23017", ...]
|
|
55
|
+
print(result.fixed_version) # "1.27.4"
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Async
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
import asyncio
|
|
62
|
+
import attestd
|
|
63
|
+
|
|
64
|
+
async def main():
|
|
65
|
+
async with attestd.AsyncClient(api_key="atst_...") as client:
|
|
66
|
+
result = await client.check("log4j", "2.14.1")
|
|
67
|
+
if result.risk_state in ("critical", "high"):
|
|
68
|
+
raise RuntimeError(f"Vulnerable dependency: {result.cve_ids}")
|
|
69
|
+
|
|
70
|
+
asyncio.run(main())
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### CI/CD deployment gate
|
|
74
|
+
|
|
75
|
+
```python
|
|
76
|
+
import attestd
|
|
77
|
+
|
|
78
|
+
DEPENDENCIES = [
|
|
79
|
+
("nginx", "1.20.0"),
|
|
80
|
+
("log4j", "2.17.1"),
|
|
81
|
+
("openssh", "9.2p1"),
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
with attestd.Client(api_key="atst_...") as client:
|
|
85
|
+
for product, version in DEPENDENCIES:
|
|
86
|
+
try:
|
|
87
|
+
result = client.check(product, version)
|
|
88
|
+
except attestd.AttestdUnsupportedProductError:
|
|
89
|
+
continue # not in supported list — skip
|
|
90
|
+
|
|
91
|
+
if result.risk_state in ("critical", "high"):
|
|
92
|
+
print(f"BLOCK: {product} {version} — {result.risk_state}")
|
|
93
|
+
print(f" CVEs: {', '.join(result.cve_ids)}")
|
|
94
|
+
print(f" Fix: upgrade to {result.fixed_version}")
|
|
95
|
+
exit(1)
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### AI agent tool
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import attestd
|
|
102
|
+
|
|
103
|
+
client = attestd.Client(api_key="atst_...")
|
|
104
|
+
|
|
105
|
+
def check_dependency_risk(product: str, version: str) -> dict:
|
|
106
|
+
"""
|
|
107
|
+
Check if a software dependency has known security vulnerabilities.
|
|
108
|
+
|
|
109
|
+
Returns a risk assessment including risk_state (critical/high/elevated/low/none),
|
|
110
|
+
whether it is actively exploited, and the fixed version if one exists.
|
|
111
|
+
"""
|
|
112
|
+
try:
|
|
113
|
+
result = client.check(product, version)
|
|
114
|
+
return {
|
|
115
|
+
"supported": True,
|
|
116
|
+
"risk_state": result.risk_state,
|
|
117
|
+
"actively_exploited": result.actively_exploited,
|
|
118
|
+
"fixed_version": result.fixed_version,
|
|
119
|
+
"cve_ids": result.cve_ids,
|
|
120
|
+
}
|
|
121
|
+
except attestd.AttestdUnsupportedProductError:
|
|
122
|
+
return {"supported": False}
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
## Error handling
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
import time
|
|
129
|
+
import attestd
|
|
130
|
+
|
|
131
|
+
with attestd.Client(api_key="atst_...") as client:
|
|
132
|
+
try:
|
|
133
|
+
result = client.check("nginx", "1.20.0")
|
|
134
|
+
except attestd.AttestdUnsupportedProductError:
|
|
135
|
+
# Product is outside Attestd's coverage.
|
|
136
|
+
# This does NOT mean the product is safe — it means Attestd has no data.
|
|
137
|
+
# Make an explicit policy decision: block, warn an operator, or skip.
|
|
138
|
+
# Do not silently allow — see "Outside coverage" below.
|
|
139
|
+
pass
|
|
140
|
+
except attestd.AttestdRateLimitError as e:
|
|
141
|
+
# Monthly quota exceeded
|
|
142
|
+
time.sleep(e.retry_after or 60)
|
|
143
|
+
except attestd.AttestdAuthError:
|
|
144
|
+
# API key is invalid or revoked
|
|
145
|
+
raise
|
|
146
|
+
except attestd.AttestdAPIError as e:
|
|
147
|
+
# Unexpected server error — e.status_code is 0 for connection failures
|
|
148
|
+
print(f"API error: {e.status_code}")
|
|
149
|
+
except attestd.AttestdError:
|
|
150
|
+
# Catch-all for any Attestd SDK exception
|
|
151
|
+
pass
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
### Outside coverage — not a safety signal
|
|
155
|
+
|
|
156
|
+
`AttestdUnsupportedProductError` means **Attestd has no vulnerability data for
|
|
157
|
+
this product**, not that the product is free of vulnerabilities. This distinction
|
|
158
|
+
matters most in AI agent integrations, where an agent that catches the exception
|
|
159
|
+
and infers "safe to proceed" is making a dangerous inference.
|
|
160
|
+
|
|
161
|
+
Recommended handling:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
try:
|
|
165
|
+
result = client.check(product, version)
|
|
166
|
+
except attestd.AttestdUnsupportedProductError as e:
|
|
167
|
+
# Option 1 — block: treat "outside coverage" as unknown risk
|
|
168
|
+
raise RuntimeError(
|
|
169
|
+
f"{e.product} is outside Attestd's coverage. "
|
|
170
|
+
"Manual security review required before deploying."
|
|
171
|
+
)
|
|
172
|
+
|
|
173
|
+
# Option 2 — warn: proceed but surface the gap
|
|
174
|
+
logger.warning("Attestd has no coverage for %s — proceeding without check", e.product)
|
|
175
|
+
|
|
176
|
+
# Option 3 — skip: exempted product, documented
|
|
177
|
+
if e.product in EXEMPTED_PRODUCTS:
|
|
178
|
+
return # explicitly opted out of coverage check for this product
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
## RiskResult fields
|
|
182
|
+
|
|
183
|
+
| Field | Type | Description |
|
|
184
|
+
|---|---|---|
|
|
185
|
+
| `product` | `str` | Product name |
|
|
186
|
+
| `version` | `str` | Version queried |
|
|
187
|
+
| `risk_state` | `str` | One of `critical`, `high`, `elevated`, `low`, `none` |
|
|
188
|
+
| `risk_factors` | `list[str]` | Machine-readable factors (see below) |
|
|
189
|
+
| `actively_exploited` | `bool` | On the CISA KEV list |
|
|
190
|
+
| `remote_exploitable` | `bool` | Remotely exploitable |
|
|
191
|
+
| `authentication_required` | `bool` | True only if ALL CVEs require auth |
|
|
192
|
+
| `patch_available` | `bool` | A fixed version is known |
|
|
193
|
+
| `fixed_version` | `str \| None` | Earliest version that resolves all CVEs |
|
|
194
|
+
| `confidence` | `float` | Synthesis confidence (0.0–1.0) |
|
|
195
|
+
| `cve_ids` | `list[str]` | CVE IDs in this assessment |
|
|
196
|
+
| `last_updated` | `datetime` | UTC timestamp of last synthesis run |
|
|
197
|
+
|
|
198
|
+
### Risk states
|
|
199
|
+
|
|
200
|
+
| State | Meaning |
|
|
201
|
+
|---|---|
|
|
202
|
+
| `critical` | Actively exploited in the wild (CISA KEV) |
|
|
203
|
+
| `high` | Remote unauthenticated exploitation possible |
|
|
204
|
+
| `elevated` | Remote exploitation requires authentication |
|
|
205
|
+
| `low` | Local-only or low-impact vulnerability |
|
|
206
|
+
| `none` | No known vulnerabilities affecting this version |
|
|
207
|
+
|
|
208
|
+
### Risk factors
|
|
209
|
+
|
|
210
|
+
| Factor | Meaning |
|
|
211
|
+
|---|---|
|
|
212
|
+
| `active_exploitation` | CVE on CISA KEV list |
|
|
213
|
+
| `remote_code_execution` | Remote exploitation possible |
|
|
214
|
+
| `no_authentication_required` | Remote + no auth required |
|
|
215
|
+
| `internet_exposed_service` | Remote + no auth (surface area flag) |
|
|
216
|
+
| `patch_available` | A fix is available |
|
|
217
|
+
|
|
218
|
+
## Configuration
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
client = attestd.Client(
|
|
222
|
+
api_key="atst_...",
|
|
223
|
+
base_url="https://api.attestd.io", # override for testing
|
|
224
|
+
timeout=10.0, # per-request timeout in seconds
|
|
225
|
+
max_retries=3, # retries on 5xx / connection errors
|
|
226
|
+
)
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
The SDK retries on transient 5xx responses and connection failures with
|
|
230
|
+
exponential backoff (1s, 2s, 4s between attempts). `401` and `429` are
|
|
231
|
+
surfaced immediately without retry.
|
|
232
|
+
|
|
233
|
+
## Supported products
|
|
234
|
+
|
|
235
|
+
See [attestd.io/docs/products](https://attestd.io/docs/products) for the
|
|
236
|
+
current list of supported products. Querying an unsupported product raises
|
|
237
|
+
`AttestdUnsupportedProductError` — this is not an error in your code.
|
|
238
|
+
|
|
239
|
+
## Testing your integration
|
|
240
|
+
|
|
241
|
+
The SDK ships a `attestd.testing` module with httpx transports for injecting
|
|
242
|
+
controlled API responses into your tests — no local Attestd instance required.
|
|
243
|
+
|
|
244
|
+
```python
|
|
245
|
+
import attestd
|
|
246
|
+
from attestd.testing import (
|
|
247
|
+
MockTransport,
|
|
248
|
+
MockAsyncTransport,
|
|
249
|
+
SequentialMockTransport,
|
|
250
|
+
SequentialMockAsyncTransport,
|
|
251
|
+
# Ready-made response bodies
|
|
252
|
+
NGINX_SAFE,
|
|
253
|
+
NGINX_VULNERABLE,
|
|
254
|
+
LOG4J_CRITICAL,
|
|
255
|
+
UNSUPPORTED,
|
|
256
|
+
)
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Test a deployment gate
|
|
260
|
+
|
|
261
|
+
```python
|
|
262
|
+
from attestd.testing import MockTransport, LOG4J_CRITICAL
|
|
263
|
+
|
|
264
|
+
def test_deployment_blocked_on_critical():
|
|
265
|
+
client = attestd.Client(
|
|
266
|
+
api_key="test",
|
|
267
|
+
transport=MockTransport(200, LOG4J_CRITICAL),
|
|
268
|
+
)
|
|
269
|
+
with pytest.raises(DeploymentBlockedError):
|
|
270
|
+
run_deployment_gate(client, "log4j", "2.14.1")
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
### Test retry behaviour
|
|
274
|
+
|
|
275
|
+
```python
|
|
276
|
+
from attestd.testing import SequentialMockTransport, NGINX_SAFE
|
|
277
|
+
|
|
278
|
+
def test_retry_succeeds_on_second_attempt():
|
|
279
|
+
transport = SequentialMockTransport([
|
|
280
|
+
(503, {}), # first attempt — server error
|
|
281
|
+
(200, NGINX_SAFE), # second attempt — success
|
|
282
|
+
])
|
|
283
|
+
client = attestd.Client(api_key="test", transport=transport, max_retries=1)
|
|
284
|
+
result = client.check("nginx", "1.27.4")
|
|
285
|
+
assert result.risk_state == "none"
|
|
286
|
+
assert transport.call_count == 2
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
### Test the "outside coverage" policy branch
|
|
290
|
+
|
|
291
|
+
```python
|
|
292
|
+
from attestd.testing import MockTransport, UNSUPPORTED
|
|
293
|
+
|
|
294
|
+
def test_outside_coverage_is_blocked_not_allowed():
|
|
295
|
+
"""Verify your code treats missing coverage as unknown risk, not as safe."""
|
|
296
|
+
client = attestd.Client(api_key="test", transport=MockTransport(200, UNSUPPORTED))
|
|
297
|
+
with pytest.raises(attestd.AttestdUnsupportedProductError):
|
|
298
|
+
run_check(client, "unknownproduct", "1.0.0")
|
|
299
|
+
# If run_check silently passes here, your coverage gap handling is broken.
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
### Custom response bodies
|
|
303
|
+
|
|
304
|
+
All ready-made bodies (`NGINX_SAFE`, etc.) are plain dicts — merge in overrides:
|
|
305
|
+
|
|
306
|
+
```python
|
|
307
|
+
from attestd.testing import MockTransport, NGINX_VULNERABLE
|
|
308
|
+
|
|
309
|
+
# Same as NGINX_VULNERABLE but with actively_exploited=True
|
|
310
|
+
body = {**NGINX_VULNERABLE, "actively_exploited": True, "risk_state": "critical"}
|
|
311
|
+
client = attestd.Client(api_key="test", transport=MockTransport(200, body))
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
## License
|
|
315
|
+
|
|
316
|
+
MIT
|