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.
@@ -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
@@ -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