cleanlib-sdk 0.4.1__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.
- cleanlib_sdk-0.4.1/.gitignore +31 -0
- cleanlib_sdk-0.4.1/PKG-INFO +102 -0
- cleanlib_sdk-0.4.1/README.md +77 -0
- cleanlib_sdk-0.4.1/bitbucket-pipelines.yml +55 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/__init__.py +118 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/client.py +236 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/derive_status.py +95 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/errors.py +119 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/http/__init__.py +30 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/http/remediation_client.py +160 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/http/types.py +106 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/reason_codes.py +54 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/schema/__init__.py +15 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/schema/verdict_envelope_v1.py +84 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/transport.py +56 -0
- cleanlib_sdk-0.4.1/cleanlib_sdk/types.py +227 -0
- cleanlib_sdk-0.4.1/pyproject.toml +51 -0
- cleanlib_sdk-0.4.1/tests/__init__.py +0 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/CORPUS-README.md +96 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/EXPECTED.json +50 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-allow-clean.json +7 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-allow-recommended.json +17 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-degraded-stale.json +18 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-deny-exploitation-critical.json +21 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-deny-kev.json +21 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-unreachable.json +16 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-warn-abandoned.json +14 -0
- cleanlib_sdk-0.4.1/tests/fixtures/contract-fixtures/fixtures/verdict-warn-has-remediation.json +17 -0
- cleanlib_sdk-0.4.1/tests/test_attestation.py +117 -0
- cleanlib_sdk-0.4.1/tests/test_client_construct.py +46 -0
- cleanlib_sdk-0.4.1/tests/test_error_mapping.py +90 -0
- cleanlib_sdk-0.4.1/tests/test_v041_contract_and_clients.py +221 -0
- cleanlib_sdk-0.4.1/tests/test_v04_rich_data_and_verbs.py +205 -0
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
__pycache__/
|
|
2
|
+
*.py[cod]
|
|
3
|
+
*$py.class
|
|
4
|
+
|
|
5
|
+
# Distribution / packaging
|
|
6
|
+
build/
|
|
7
|
+
dist/
|
|
8
|
+
*.egg-info/
|
|
9
|
+
.eggs/
|
|
10
|
+
*.egg
|
|
11
|
+
|
|
12
|
+
# Virtual environments
|
|
13
|
+
.venv/
|
|
14
|
+
venv/
|
|
15
|
+
env/
|
|
16
|
+
|
|
17
|
+
# Tools
|
|
18
|
+
.pytest_cache/
|
|
19
|
+
.ruff_cache/
|
|
20
|
+
.mypy_cache/
|
|
21
|
+
.tox/
|
|
22
|
+
htmlcov/
|
|
23
|
+
.coverage
|
|
24
|
+
.coverage.*
|
|
25
|
+
coverage.xml
|
|
26
|
+
|
|
27
|
+
# IDE
|
|
28
|
+
.vscode/
|
|
29
|
+
.idea/
|
|
30
|
+
*.swp
|
|
31
|
+
.DS_Store
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: cleanlib-sdk
|
|
3
|
+
Version: 0.4.1
|
|
4
|
+
Summary: CleanLibrary Python SDK — mirrors cleanlib-client (Rust SDK) HTTP surface + cycle-5 cosign verdict-attestation
|
|
5
|
+
Project-URL: Homepage, https://bitbucket.org/triamsec/cleanlib-sdk-py
|
|
6
|
+
Project-URL: Source, https://bitbucket.org/triamsec/cleanlib-sdk-py
|
|
7
|
+
Project-URL: Documentation, https://bitbucket.org/triamsec/cleanlib/src/main/workstream-specs/05-app/sdk-substrate-rev1.md
|
|
8
|
+
Author: CleanStart
|
|
9
|
+
License: Proprietary
|
|
10
|
+
Classifier: Development Status :: 2 - Pre-Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Requires-Python: >=3.10
|
|
18
|
+
Requires-Dist: httpx>=0.27
|
|
19
|
+
Requires-Dist: pydantic>=2
|
|
20
|
+
Provides-Extra: dev
|
|
21
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
22
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
23
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
24
|
+
Description-Content-Type: text/markdown
|
|
25
|
+
|
|
26
|
+
# cleanlib-sdk-py
|
|
27
|
+
|
|
28
|
+
CleanLibrary Python SDK — `asyncio` + `httpx`; mirrors the Rust `cleanlib-client` HTTP surface.
|
|
29
|
+
|
|
30
|
+
**Status**: `v0.1.0-substrate` (cycle-4 §D.1). Substrate-only — `fetch_verdict` ships; remaining verbs (`scan` / `audit_recent` / `policy_preview` / `fetch_bytes`) iterate cycle-5+.
|
|
31
|
+
|
|
32
|
+
## Install (cycle-5+; not yet on PyPI)
|
|
33
|
+
|
|
34
|
+
```bash
|
|
35
|
+
pip install cleanlib-sdk
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
For cycle-4 substrate, install from this repo:
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install git+https://bitbucket.org/triamsec/cleanlib-sdk-py.git
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Usage
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import asyncio
|
|
48
|
+
from cleanlib_sdk import Client, PolicyDenyError, RiskAcceptanceRequiredError
|
|
49
|
+
|
|
50
|
+
async def main() -> None:
|
|
51
|
+
async with Client(
|
|
52
|
+
endpoint="https://cleanapp.clnstrt.dev",
|
|
53
|
+
api_key="clk_std_...", # opaque CleanLibrary access key
|
|
54
|
+
) as c:
|
|
55
|
+
try:
|
|
56
|
+
v = await c.fetch_verdict("npm", "lodash", "4.17.21")
|
|
57
|
+
print(f"{v.decision} composite_score={v.composite_score}")
|
|
58
|
+
print(f"reasoning: {v.reasoning}")
|
|
59
|
+
except PolicyDenyError as e:
|
|
60
|
+
print(f"DENIED [{e.reason_code}]: {e.message}")
|
|
61
|
+
except RiskAcceptanceRequiredError as e:
|
|
62
|
+
print(f"RISK ACCEPT REQUIRED: {e.message}")
|
|
63
|
+
if e.docs_url:
|
|
64
|
+
print(f"see: {e.docs_url}")
|
|
65
|
+
|
|
66
|
+
asyncio.run(main())
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Error hierarchy
|
|
70
|
+
|
|
71
|
+
All errors descend from `CleanLibraryError`. Subclasses:
|
|
72
|
+
|
|
73
|
+
| Exception | HTTP | Triggered by |
|
|
74
|
+
|---|---|---|
|
|
75
|
+
| `PolicyDenyError` | 403 / 451 | `POLICY_DENY_VERDICT` / `POLICY_DENY_RULE_EXPLICIT` |
|
|
76
|
+
| `IntegrityFailureError` | 403 | `INTEGRITY_FAILURE` |
|
|
77
|
+
| `RateLimitExceededError` | 429 | tier-throttled; carries `retry_after_seconds` |
|
|
78
|
+
| `RiskAcceptanceRequiredError` | 403 | `RISK_ACCEPTANCE_REQUIRED` |
|
|
79
|
+
| `AuthenticationError` | 401 / 403 | `KEY_INVALID` / `KEY_EXPIRED` / `KEY_SCOPE_INSUFFICIENT` |
|
|
80
|
+
| `InsufficientDataError` | 403 | `INSUFFICIENT_DATA_FAIL_CLOSED` |
|
|
81
|
+
| `PackageNotFoundError` | 404 | not in catalog + ingest declined |
|
|
82
|
+
| `ServerError` | 5xx | retryable on 502/503/504 |
|
|
83
|
+
| `TransportError` | — | network / TLS / timeout / DNS |
|
|
84
|
+
| `ParseError` | — | response body shape mismatch |
|
|
85
|
+
|
|
86
|
+
## Development
|
|
87
|
+
|
|
88
|
+
```bash
|
|
89
|
+
pip install -e ".[dev]"
|
|
90
|
+
pytest
|
|
91
|
+
ruff check .
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
## Cross-references
|
|
95
|
+
|
|
96
|
+
- [App workstream substrate spec](https://bitbucket.org/triamsec/cleanlib/src/main/workstream-specs/05-app/sdk-substrate-rev1.md) — canonical surface design
|
|
97
|
+
- [Rust cleanlib-client](https://bitbucket.org/triamsec/cleanlib/src/main/cleanlib-client/) — reference implementation
|
|
98
|
+
- [App customer endpoint contract](https://bitbucket.org/triamsec/cleanlib/src/main/workstream-specs/05-app/app-scope-outline-rev4.md) §9 — HTTP surface contract
|
|
99
|
+
|
|
100
|
+
## License
|
|
101
|
+
|
|
102
|
+
Proprietary — CleanStart.
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# cleanlib-sdk-py
|
|
2
|
+
|
|
3
|
+
CleanLibrary Python SDK — `asyncio` + `httpx`; mirrors the Rust `cleanlib-client` HTTP surface.
|
|
4
|
+
|
|
5
|
+
**Status**: `v0.1.0-substrate` (cycle-4 §D.1). Substrate-only — `fetch_verdict` ships; remaining verbs (`scan` / `audit_recent` / `policy_preview` / `fetch_bytes`) iterate cycle-5+.
|
|
6
|
+
|
|
7
|
+
## Install (cycle-5+; not yet on PyPI)
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install cleanlib-sdk
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For cycle-4 substrate, install from this repo:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
pip install git+https://bitbucket.org/triamsec/cleanlib-sdk-py.git
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Usage
|
|
20
|
+
|
|
21
|
+
```python
|
|
22
|
+
import asyncio
|
|
23
|
+
from cleanlib_sdk import Client, PolicyDenyError, RiskAcceptanceRequiredError
|
|
24
|
+
|
|
25
|
+
async def main() -> None:
|
|
26
|
+
async with Client(
|
|
27
|
+
endpoint="https://cleanapp.clnstrt.dev",
|
|
28
|
+
api_key="clk_std_...", # opaque CleanLibrary access key
|
|
29
|
+
) as c:
|
|
30
|
+
try:
|
|
31
|
+
v = await c.fetch_verdict("npm", "lodash", "4.17.21")
|
|
32
|
+
print(f"{v.decision} composite_score={v.composite_score}")
|
|
33
|
+
print(f"reasoning: {v.reasoning}")
|
|
34
|
+
except PolicyDenyError as e:
|
|
35
|
+
print(f"DENIED [{e.reason_code}]: {e.message}")
|
|
36
|
+
except RiskAcceptanceRequiredError as e:
|
|
37
|
+
print(f"RISK ACCEPT REQUIRED: {e.message}")
|
|
38
|
+
if e.docs_url:
|
|
39
|
+
print(f"see: {e.docs_url}")
|
|
40
|
+
|
|
41
|
+
asyncio.run(main())
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Error hierarchy
|
|
45
|
+
|
|
46
|
+
All errors descend from `CleanLibraryError`. Subclasses:
|
|
47
|
+
|
|
48
|
+
| Exception | HTTP | Triggered by |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| `PolicyDenyError` | 403 / 451 | `POLICY_DENY_VERDICT` / `POLICY_DENY_RULE_EXPLICIT` |
|
|
51
|
+
| `IntegrityFailureError` | 403 | `INTEGRITY_FAILURE` |
|
|
52
|
+
| `RateLimitExceededError` | 429 | tier-throttled; carries `retry_after_seconds` |
|
|
53
|
+
| `RiskAcceptanceRequiredError` | 403 | `RISK_ACCEPTANCE_REQUIRED` |
|
|
54
|
+
| `AuthenticationError` | 401 / 403 | `KEY_INVALID` / `KEY_EXPIRED` / `KEY_SCOPE_INSUFFICIENT` |
|
|
55
|
+
| `InsufficientDataError` | 403 | `INSUFFICIENT_DATA_FAIL_CLOSED` |
|
|
56
|
+
| `PackageNotFoundError` | 404 | not in catalog + ingest declined |
|
|
57
|
+
| `ServerError` | 5xx | retryable on 502/503/504 |
|
|
58
|
+
| `TransportError` | — | network / TLS / timeout / DNS |
|
|
59
|
+
| `ParseError` | — | response body shape mismatch |
|
|
60
|
+
|
|
61
|
+
## Development
|
|
62
|
+
|
|
63
|
+
```bash
|
|
64
|
+
pip install -e ".[dev]"
|
|
65
|
+
pytest
|
|
66
|
+
ruff check .
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Cross-references
|
|
70
|
+
|
|
71
|
+
- [App workstream substrate spec](https://bitbucket.org/triamsec/cleanlib/src/main/workstream-specs/05-app/sdk-substrate-rev1.md) — canonical surface design
|
|
72
|
+
- [Rust cleanlib-client](https://bitbucket.org/triamsec/cleanlib/src/main/cleanlib-client/) — reference implementation
|
|
73
|
+
- [App customer endpoint contract](https://bitbucket.org/triamsec/cleanlib/src/main/workstream-specs/05-app/app-scope-outline-rev4.md) §9 — HTTP surface contract
|
|
74
|
+
|
|
75
|
+
## License
|
|
76
|
+
|
|
77
|
+
Proprietary — CleanStart.
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# CleanLibrary Python SDK CI — per workspace §C.5 + §C.6 + pen #16 forward fix.
|
|
2
|
+
#
|
|
3
|
+
# Mirrors the cleanlib repo's commit-prefix discipline + rebase-required
|
|
4
|
+
# detector. Uses a separate CLEANLIB-N counter per sdk-substrate-rev1.md §8.
|
|
5
|
+
|
|
6
|
+
image: python:3.12-slim
|
|
7
|
+
|
|
8
|
+
pipelines:
|
|
9
|
+
default:
|
|
10
|
+
- step:
|
|
11
|
+
name: Validate commit message prefix
|
|
12
|
+
script:
|
|
13
|
+
# Each commit on the PR branch (since divergence from main) must start with
|
|
14
|
+
# a CLEANLIB-N prefix per workspace §6.9 discipline. --no-merges per pen #16
|
|
15
|
+
# avoids false-positives on BB-synthesized merge commits.
|
|
16
|
+
- |
|
|
17
|
+
apt-get update -qq && apt-get install -y -qq git >/dev/null
|
|
18
|
+
BASE_REF=${BITBUCKET_PR_DESTINATION_BRANCH:-main}
|
|
19
|
+
git fetch origin "$BASE_REF" || true
|
|
20
|
+
git log --no-merges "origin/$BASE_REF..HEAD" --pretty=format:"%H %s" | while read -r line; do
|
|
21
|
+
MSG=${line#* }
|
|
22
|
+
case "$MSG" in
|
|
23
|
+
CLEANLIB-*) ;;
|
|
24
|
+
*) echo "❌ commit missing CLEANLIB-N prefix: $line"; exit 1;;
|
|
25
|
+
esac
|
|
26
|
+
done
|
|
27
|
+
echo "✅ commit prefixes OK"
|
|
28
|
+
|
|
29
|
+
- step:
|
|
30
|
+
name: Rebase-required detector
|
|
31
|
+
script:
|
|
32
|
+
# Mirrors cleanlib repo §C.6: if merge-base is not on main's HEAD, ask for rebase.
|
|
33
|
+
- |
|
|
34
|
+
apt-get update -qq && apt-get install -y -qq git >/dev/null
|
|
35
|
+
BASE_REF=${BITBUCKET_PR_DESTINATION_BRANCH:-main}
|
|
36
|
+
git fetch origin "$BASE_REF"
|
|
37
|
+
MB=$(git merge-base "origin/$BASE_REF" HEAD)
|
|
38
|
+
HEAD_REMOTE=$(git rev-parse "origin/$BASE_REF")
|
|
39
|
+
if [ "$MB" != "$HEAD_REMOTE" ]; then
|
|
40
|
+
echo "❌ Branch is behind origin/$BASE_REF; rebase before merging."
|
|
41
|
+
echo " merge-base: $MB"
|
|
42
|
+
echo " origin/$BASE_REF: $HEAD_REMOTE"
|
|
43
|
+
exit 1
|
|
44
|
+
fi
|
|
45
|
+
echo "✅ branch current with origin/$BASE_REF"
|
|
46
|
+
|
|
47
|
+
- step:
|
|
48
|
+
name: Lint + test
|
|
49
|
+
caches:
|
|
50
|
+
- pip
|
|
51
|
+
script:
|
|
52
|
+
- pip install --quiet --upgrade pip
|
|
53
|
+
- pip install --quiet -e ".[dev]"
|
|
54
|
+
- ruff check .
|
|
55
|
+
- pytest -q
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"""CleanLibrary Python SDK — mirrors @cleanstart/cleanlib-sdk v0.4.x.
|
|
2
|
+
|
|
3
|
+
Cycle-7 v0.4.1:
|
|
4
|
+
- New per-domain HTTP client split (sister-shape with sdk-js v0.4.1)
|
|
5
|
+
- `HttpRemediationClient` — sparse 7-block /remediation responses
|
|
6
|
+
- (HttpVerdictClient + HttpEnrichClient land in v0.4.2 ripple)
|
|
7
|
+
- `cleanlib_sdk.reason_codes.ReasonCode` — 15-entry canonical registry (Enum)
|
|
8
|
+
- `cleanlib_sdk.schema` — `VerdictEnvelopeV1` JSON Schema mirror (Python dict literal)
|
|
9
|
+
- `cleanlib_sdk.derive_status.derive_status(envelope)` — algorithm per App
|
|
10
|
+
dispatch §4 binding contract (substance precedence + freshness override)
|
|
11
|
+
- `cleanlib_sdk.Client` flat-method class → `DeprecationWarning` on __init__
|
|
12
|
+
(per F5 ratification; scheduled removal v1.0.0)
|
|
13
|
+
|
|
14
|
+
Schema-locked against `@cleanstart/cleanlib-sdk@0.4.1` tarball sha-1
|
|
15
|
+
`b5f00c160907a6ea1f490f14a9bda6f6de34b8b6`. Cross-SDK contract test verifies
|
|
16
|
+
identical (status, reason_code) on every fixture in
|
|
17
|
+
`cleanlib-contract-fixtures` v1.0.0.
|
|
18
|
+
|
|
19
|
+
Earlier cycle context:
|
|
20
|
+
- Cycle-5 v0.3.0 added typed `Verdict.attestation`
|
|
21
|
+
- Cycle-6 v0.4.0 added 5-verb cascade + 9 rich-data Optional fields
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from cleanlib_sdk.client import Client
|
|
25
|
+
from cleanlib_sdk.derive_status import StatusResult, derive_status
|
|
26
|
+
from cleanlib_sdk.errors import (
|
|
27
|
+
AuthenticationError,
|
|
28
|
+
CleanLibraryError,
|
|
29
|
+
InsufficientDataError,
|
|
30
|
+
IntegrityFailureError,
|
|
31
|
+
PackageNotFoundError,
|
|
32
|
+
ParseError,
|
|
33
|
+
PolicyDenyError,
|
|
34
|
+
RateLimitExceededError,
|
|
35
|
+
RiskAcceptanceRequiredError,
|
|
36
|
+
ServerError,
|
|
37
|
+
TransportError,
|
|
38
|
+
)
|
|
39
|
+
from cleanlib_sdk.http import (
|
|
40
|
+
DEFAULT_REMEDIATION_BASE_URL,
|
|
41
|
+
REMEDIATION_CACHE_TTL_SECS,
|
|
42
|
+
HttpRemediationClient,
|
|
43
|
+
RemediationBlock,
|
|
44
|
+
RemediationOrAbsent,
|
|
45
|
+
RemediationResponse,
|
|
46
|
+
)
|
|
47
|
+
from cleanlib_sdk.reason_codes import ALL_REASON_CODES, ReasonCode
|
|
48
|
+
from cleanlib_sdk.schema import (
|
|
49
|
+
AVAILABILITY_ENUM,
|
|
50
|
+
SCHEMA_ID,
|
|
51
|
+
STATUS_ENUM,
|
|
52
|
+
VERDICT_ENVELOPE_V1_SCHEMA,
|
|
53
|
+
)
|
|
54
|
+
from cleanlib_sdk.types import (
|
|
55
|
+
Attestation,
|
|
56
|
+
AuditWindow,
|
|
57
|
+
AuditWindowResponse,
|
|
58
|
+
PackageRef,
|
|
59
|
+
PackageRisk,
|
|
60
|
+
PolicyPreviewResponse,
|
|
61
|
+
PolicyPreviewResult,
|
|
62
|
+
RecommendedVersion,
|
|
63
|
+
RiskAcceptResponse,
|
|
64
|
+
ScanResponse,
|
|
65
|
+
ScanResult,
|
|
66
|
+
SignedAttestation,
|
|
67
|
+
Verdict,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
__version__ = "0.4.1"
|
|
71
|
+
|
|
72
|
+
__all__ = [
|
|
73
|
+
# v0.4.1 per-domain HTTP clients (primary API)
|
|
74
|
+
"HttpRemediationClient",
|
|
75
|
+
"DEFAULT_REMEDIATION_BASE_URL",
|
|
76
|
+
"REMEDIATION_CACHE_TTL_SECS",
|
|
77
|
+
"RemediationBlock",
|
|
78
|
+
"RemediationResponse",
|
|
79
|
+
"RemediationOrAbsent",
|
|
80
|
+
# v0.4.1 contract enforcement
|
|
81
|
+
"ReasonCode",
|
|
82
|
+
"ALL_REASON_CODES",
|
|
83
|
+
"VERDICT_ENVELOPE_V1_SCHEMA",
|
|
84
|
+
"SCHEMA_ID",
|
|
85
|
+
"STATUS_ENUM",
|
|
86
|
+
"AVAILABILITY_ENUM",
|
|
87
|
+
"derive_status",
|
|
88
|
+
"StatusResult",
|
|
89
|
+
# cycle-6 v0.4.0 types
|
|
90
|
+
"Verdict",
|
|
91
|
+
"Attestation",
|
|
92
|
+
"SignedAttestation",
|
|
93
|
+
"RecommendedVersion",
|
|
94
|
+
"PackageRisk",
|
|
95
|
+
"PackageRef",
|
|
96
|
+
"ScanResult",
|
|
97
|
+
"ScanResponse",
|
|
98
|
+
"AuditWindow",
|
|
99
|
+
"AuditWindowResponse",
|
|
100
|
+
"PolicyPreviewResult",
|
|
101
|
+
"PolicyPreviewResponse",
|
|
102
|
+
"RiskAcceptResponse",
|
|
103
|
+
# Deprecated v0.4.1 (v1.0.0 removal); kept for source-compat with v0.4.0 callers
|
|
104
|
+
"Client",
|
|
105
|
+
# Errors
|
|
106
|
+
"CleanLibraryError",
|
|
107
|
+
"PolicyDenyError",
|
|
108
|
+
"IntegrityFailureError",
|
|
109
|
+
"RateLimitExceededError",
|
|
110
|
+
"RiskAcceptanceRequiredError",
|
|
111
|
+
"AuthenticationError",
|
|
112
|
+
"InsufficientDataError",
|
|
113
|
+
"PackageNotFoundError",
|
|
114
|
+
"ServerError",
|
|
115
|
+
"TransportError",
|
|
116
|
+
"ParseError",
|
|
117
|
+
"__version__",
|
|
118
|
+
]
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
"""Async Client — DEPRECATED as of v0.4.1; scheduled removal v1.0.0.
|
|
2
|
+
|
|
3
|
+
The flat-method `Client` class is superseded by the per-domain client split
|
|
4
|
+
in `cleanlib_sdk.http`:
|
|
5
|
+
|
|
6
|
+
- `HttpRemediationClient` (v0.4.1; sparse 7-block /remediation responses)
|
|
7
|
+
- `HttpVerdictClient` (v0.4.2 ripple; mirror sdk-js)
|
|
8
|
+
- `HttpEnrichClient` (v0.4.2 ripple; mirror sdk-js)
|
|
9
|
+
|
|
10
|
+
Existing callers continue to work — instantiating `Client` emits a
|
|
11
|
+
`DeprecationWarning` per F5 ratification (sister-shape with sdk-js v0.4.1
|
|
12
|
+
`CleanlibClient` `@deprecated` shim).
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import warnings
|
|
18
|
+
from types import TracebackType
|
|
19
|
+
from typing import Optional, Type
|
|
20
|
+
from urllib.parse import quote
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
|
|
24
|
+
from cleanlib_sdk.transport import (
|
|
25
|
+
DEFAULT_TIMEOUT_SECS,
|
|
26
|
+
auth_headers,
|
|
27
|
+
map_http_error,
|
|
28
|
+
validate_endpoint,
|
|
29
|
+
)
|
|
30
|
+
from cleanlib_sdk.types import (
|
|
31
|
+
AuditWindow,
|
|
32
|
+
AuditWindowResponse,
|
|
33
|
+
PackageRef,
|
|
34
|
+
PolicyPreviewResponse,
|
|
35
|
+
RiskAcceptResponse,
|
|
36
|
+
ScanResponse,
|
|
37
|
+
Verdict,
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
class Client:
|
|
42
|
+
"""DEPRECATED — Async HTTP client for the CleanLibrary App customer endpoints.
|
|
43
|
+
|
|
44
|
+
.. deprecated:: 0.4.1
|
|
45
|
+
Use the per-domain clients in :mod:`cleanlib_sdk.http` instead.
|
|
46
|
+
Scheduled removal in v1.0.0. Per F5 cycle-7 ratification.
|
|
47
|
+
|
|
48
|
+
Example (still works; emits ``DeprecationWarning``)::
|
|
49
|
+
|
|
50
|
+
import asyncio
|
|
51
|
+
from cleanlib_sdk import Client
|
|
52
|
+
|
|
53
|
+
async def main():
|
|
54
|
+
async with Client(
|
|
55
|
+
endpoint="https://cleanapp.clnstrt.dev",
|
|
56
|
+
api_key="clk_std_test_001",
|
|
57
|
+
) as c:
|
|
58
|
+
v = await c.fetch_verdict("npm", "lodash", "4.17.21")
|
|
59
|
+
print(v.decision, v.composite_score)
|
|
60
|
+
|
|
61
|
+
asyncio.run(main())
|
|
62
|
+
"""
|
|
63
|
+
|
|
64
|
+
def __init__(
|
|
65
|
+
self,
|
|
66
|
+
endpoint: str,
|
|
67
|
+
api_key: Optional[str] = None,
|
|
68
|
+
api_version: str = "v1",
|
|
69
|
+
timeout_secs: float = DEFAULT_TIMEOUT_SECS,
|
|
70
|
+
) -> None:
|
|
71
|
+
warnings.warn(
|
|
72
|
+
"cleanlib_sdk.Client is deprecated as of v0.4.1; use the per-domain "
|
|
73
|
+
"clients in cleanlib_sdk.http (HttpRemediationClient + HttpVerdictClient "
|
|
74
|
+
"+ HttpEnrichClient). Scheduled removal v1.0.0.",
|
|
75
|
+
DeprecationWarning,
|
|
76
|
+
stacklevel=2,
|
|
77
|
+
)
|
|
78
|
+
self.base_url = validate_endpoint(endpoint)
|
|
79
|
+
self.api_key = api_key
|
|
80
|
+
self.api_version = api_version
|
|
81
|
+
self._http = httpx.AsyncClient(
|
|
82
|
+
headers=auth_headers(api_key),
|
|
83
|
+
timeout=timeout_secs,
|
|
84
|
+
)
|
|
85
|
+
|
|
86
|
+
async def __aenter__(self) -> "Client":
|
|
87
|
+
return self
|
|
88
|
+
|
|
89
|
+
async def __aexit__(
|
|
90
|
+
self,
|
|
91
|
+
exc_type: Optional[Type[BaseException]],
|
|
92
|
+
exc: Optional[BaseException],
|
|
93
|
+
tb: Optional[TracebackType],
|
|
94
|
+
) -> None:
|
|
95
|
+
await self._http.aclose()
|
|
96
|
+
|
|
97
|
+
async def aclose(self) -> None:
|
|
98
|
+
await self._http.aclose()
|
|
99
|
+
|
|
100
|
+
async def fetch_verdict(self, ecosystem: str, package: str, version: str) -> Verdict:
|
|
101
|
+
"""GET /v1/customer/verdicts/{ecosystem}/{package}/{version}.
|
|
102
|
+
|
|
103
|
+
Returns the parsed Verdict on 2xx. Raises a typed CleanLibraryError
|
|
104
|
+
subclass on non-2xx per the App Rev 4 §9.2 reason-code dispatch.
|
|
105
|
+
"""
|
|
106
|
+
path = (
|
|
107
|
+
f"{self.base_url}/{self.api_version}/customer/verdicts/"
|
|
108
|
+
f"{quote(ecosystem, safe='')}/"
|
|
109
|
+
f"{quote(package, safe='')}/"
|
|
110
|
+
f"{quote(version, safe='')}"
|
|
111
|
+
)
|
|
112
|
+
resp = await self._http.get(path)
|
|
113
|
+
if resp.is_error:
|
|
114
|
+
raise map_http_error(resp)
|
|
115
|
+
data = resp.json()
|
|
116
|
+
# Inject request-context fields the App doesn't echo back
|
|
117
|
+
# (App's customer-verdict endpoint returns canonical cleanlib_core::
|
|
118
|
+
# Verdict shape without ecosystem/package/version path-param echoes)
|
|
119
|
+
data.setdefault("ecosystem", ecosystem)
|
|
120
|
+
data.setdefault("package", package)
|
|
121
|
+
data.setdefault("version", version)
|
|
122
|
+
# Derive decision from verdict_label + severity per cycle-4 §D.7
|
|
123
|
+
# demo policy mapping
|
|
124
|
+
if data.get("decision") is None:
|
|
125
|
+
data["decision"] = _derive_decision(
|
|
126
|
+
data.get("verdict") or data.get("source"),
|
|
127
|
+
data.get("severity"),
|
|
128
|
+
)
|
|
129
|
+
return Verdict.model_validate(data)
|
|
130
|
+
|
|
131
|
+
async def scan(self, packages: list[PackageRef]) -> ScanResponse:
|
|
132
|
+
"""POST /v1/scan — resolve verdicts for a batch of packages (PR #99).
|
|
133
|
+
|
|
134
|
+
Partial-success: a per-package failure surfaces as `error` on its
|
|
135
|
+
result entry rather than raising. Mirrors cleanlib-sdk-js `scan`.
|
|
136
|
+
"""
|
|
137
|
+
body = {"packages": [p.model_dump(by_alias=True) for p in packages]}
|
|
138
|
+
resp = await self._http.post(f"{self.base_url}/{self.api_version}/scan", json=body)
|
|
139
|
+
if resp.is_error:
|
|
140
|
+
raise map_http_error(resp)
|
|
141
|
+
return ScanResponse.model_validate(resp.json())
|
|
142
|
+
|
|
143
|
+
async def audit(self, window: Optional[AuditWindow] = None) -> AuditWindowResponse:
|
|
144
|
+
"""GET /v1/audit?since=&until= — audit-window query (PR #99).
|
|
145
|
+
|
|
146
|
+
`backend_status` is an honest signal: "not_wired" + empty records means
|
|
147
|
+
the endpoint is reachable but the audit store is not yet attached
|
|
148
|
+
App-side. Mirrors cleanlib-sdk-js `audit`.
|
|
149
|
+
"""
|
|
150
|
+
params: dict[str, str] = {}
|
|
151
|
+
if window is not None:
|
|
152
|
+
if window.since is not None:
|
|
153
|
+
params["since"] = window.since
|
|
154
|
+
if window.until is not None:
|
|
155
|
+
params["until"] = window.until
|
|
156
|
+
resp = await self._http.get(
|
|
157
|
+
f"{self.base_url}/{self.api_version}/audit", params=params or None
|
|
158
|
+
)
|
|
159
|
+
if resp.is_error:
|
|
160
|
+
raise map_http_error(resp)
|
|
161
|
+
return AuditWindowResponse.model_validate(resp.json())
|
|
162
|
+
|
|
163
|
+
async def policy_preview(
|
|
164
|
+
self, policy_yaml: str, packages: list[PackageRef]
|
|
165
|
+
) -> PolicyPreviewResponse:
|
|
166
|
+
"""POST /v1/policy/preview — preview a candidate policy YAML against a
|
|
167
|
+
batch of packages WITHOUT persisting it (PR #99). Raises ParseError/
|
|
168
|
+
HttpError-equivalent on 400 when the YAML fails to parse. Mirrors
|
|
169
|
+
cleanlib-sdk-js `policyPreview`.
|
|
170
|
+
"""
|
|
171
|
+
body = {
|
|
172
|
+
"policy_yaml": policy_yaml,
|
|
173
|
+
"packages": [p.model_dump(by_alias=True) for p in packages],
|
|
174
|
+
}
|
|
175
|
+
resp = await self._http.post(
|
|
176
|
+
f"{self.base_url}/{self.api_version}/policy/preview", json=body
|
|
177
|
+
)
|
|
178
|
+
if resp.is_error:
|
|
179
|
+
raise map_http_error(resp)
|
|
180
|
+
return PolicyPreviewResponse.model_validate(resp.json())
|
|
181
|
+
|
|
182
|
+
async def risk_accept(self, verdict_id: str, justification: str) -> RiskAcceptResponse:
|
|
183
|
+
"""POST /v1/risk-accept — record a risk-acceptance for a verdict (PR #99).
|
|
184
|
+
|
|
185
|
+
`persistence` is "ephemeral" until a CDP accept-write lands. Raises on
|
|
186
|
+
400 when verdict_id or justification is empty. Mirrors cleanlib-sdk-js
|
|
187
|
+
`riskAccept`.
|
|
188
|
+
"""
|
|
189
|
+
body = {"verdict_id": verdict_id, "justification": justification}
|
|
190
|
+
resp = await self._http.post(
|
|
191
|
+
f"{self.base_url}/{self.api_version}/risk-accept", json=body
|
|
192
|
+
)
|
|
193
|
+
if resp.is_error:
|
|
194
|
+
raise map_http_error(resp)
|
|
195
|
+
return RiskAcceptResponse.model_validate(resp.json())
|
|
196
|
+
|
|
197
|
+
async def fetch_bytes(self, ecosystem: str, package: str, version: str) -> bytes:
|
|
198
|
+
"""GET /v1/fetch/{ecosystem}/{package}/{version} — raw catalog bytes
|
|
199
|
+
(PR #99). Returns the application/octet-stream body. Does NOT run the
|
|
200
|
+
policy evaluator or sign an attestation. Mirrors cleanlib-sdk-js
|
|
201
|
+
`fetchBytes`.
|
|
202
|
+
"""
|
|
203
|
+
path = (
|
|
204
|
+
f"{self.base_url}/{self.api_version}/fetch/"
|
|
205
|
+
f"{quote(ecosystem, safe='')}/"
|
|
206
|
+
f"{quote(package, safe='')}/"
|
|
207
|
+
f"{quote(version, safe='')}"
|
|
208
|
+
)
|
|
209
|
+
resp = await self._http.get(path)
|
|
210
|
+
if resp.is_error:
|
|
211
|
+
raise map_http_error(resp)
|
|
212
|
+
return resp.content
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _derive_decision(verdict_label: Optional[str], severity: Optional[str]) -> str:
|
|
216
|
+
"""Map App canonical Verdict's verdict_label + severity to decision string.
|
|
217
|
+
|
|
218
|
+
Mapping rules per cycle-4 §D.7 static demo policy + cycle-5 fixtures:
|
|
219
|
+
- VECTOR_VERDICT + High/Critical severity → DENY
|
|
220
|
+
- VECTOR_VERDICT + lower severity → WARN
|
|
221
|
+
- DM_THRESHOLD_BLOCK → DENY
|
|
222
|
+
- INSUFFICIENT_DATA → WARN
|
|
223
|
+
- ALLOWED_NO_FINDINGS → ALLOW
|
|
224
|
+
- default → ALLOW
|
|
225
|
+
"""
|
|
226
|
+
label = (verdict_label or "").upper()
|
|
227
|
+
sev = (severity or "").upper()
|
|
228
|
+
if label == "VECTOR_VERDICT":
|
|
229
|
+
return "DENY" if sev in ("HIGH", "CRITICAL") else "WARN"
|
|
230
|
+
if label == "DM_THRESHOLD_BLOCK":
|
|
231
|
+
return "DENY"
|
|
232
|
+
if label == "INSUFFICIENT_DATA":
|
|
233
|
+
return "WARN"
|
|
234
|
+
if label == "ALLOWED_NO_FINDINGS":
|
|
235
|
+
return "ALLOW"
|
|
236
|
+
return "ALLOW"
|