certone-sdk 0.2.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.
- certone_sdk-0.2.0/.gitignore +6 -0
- certone_sdk-0.2.0/PKG-INFO +113 -0
- certone_sdk-0.2.0/README.md +82 -0
- certone_sdk-0.2.0/certone_sdk/__init__.py +27 -0
- certone_sdk-0.2.0/certone_sdk/agent.py +274 -0
- certone_sdk-0.2.0/certone_sdk/client.py +262 -0
- certone_sdk-0.2.0/certone_sdk/py.typed +0 -0
- certone_sdk-0.2.0/certone_sdk/submit.py +363 -0
- certone_sdk-0.2.0/conformance/fixtures/attestation.json +31 -0
- certone_sdk-0.2.0/conformance/fixtures/check_result.json +28 -0
- certone_sdk-0.2.0/conformance/fixtures/ingest_result.json +24 -0
- certone_sdk-0.2.0/conformance/fixtures/report.json +15 -0
- certone_sdk-0.2.0/conformance/fixtures/report_document_excerpt.json +53 -0
- certone_sdk-0.2.0/conformance/fixtures/scan.json +10 -0
- certone_sdk-0.2.0/conformance/fixtures/systems.json +83 -0
- certone_sdk-0.2.0/pyproject.toml +71 -0
- certone_sdk-0.2.0/tests/test_client_ops.py +142 -0
- certone_sdk-0.2.0/tests/test_sdk.py +178 -0
- certone_sdk-0.2.0/tests/test_submit.py +217 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: certone-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: CertOne SDK — typed API client, certone-submit (confidential scan + verdict gate) and the run-at-source agent.
|
|
5
|
+
Project-URL: Homepage, https://certkor.ai
|
|
6
|
+
Author: Certkor AI
|
|
7
|
+
License: Proprietary
|
|
8
|
+
Keywords: ai-compliance,certification,ci,confidential-computing,eu-ai-act
|
|
9
|
+
Classifier: Development Status :: 4 - Beta
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: Other/Proprietary License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Software Development :: Quality Assurance
|
|
18
|
+
Classifier: Typing :: Typed
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: requests>=2.31
|
|
21
|
+
Provides-Extra: agent
|
|
22
|
+
Requires-Dist: certone-engine>=0.1.0; extra == 'agent'
|
|
23
|
+
Provides-Extra: azure
|
|
24
|
+
Requires-Dist: azure-identity>=1.15; extra == 'azure'
|
|
25
|
+
Requires-Dist: azure-keyvault-secrets>=4.7; extra == 'azure'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
28
|
+
Requires-Dist: pytest>=8.0; extra == 'dev'
|
|
29
|
+
Requires-Dist: ruff>=0.6; extra == 'dev'
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# certone-sdk
|
|
33
|
+
|
|
34
|
+
The Python SDK and command-line tools for [CertOne](https://certkor.ai), Certkor's AI compliance
|
|
35
|
+
certification platform. Send an AI project for a confidential scan from any pipeline, notebook,
|
|
36
|
+
editor or device, gate on the verdict, and fetch the evidence and the attestation.
|
|
37
|
+
|
|
38
|
+
CertOne evaluates the project inside a hardware-attested trusted execution environment (AMD SEV-SNP
|
|
39
|
+
confidential VMs, or Kata confidential containers): it builds the AI bill of materials, checks it
|
|
40
|
+
against the policy packs for the regions where the system runs (EU AI Act, NIST AI RMF, ISO/IEC
|
|
41
|
+
42001, sector rules...), runs dynamic tests and computes the evaluation metrics.
|
|
42
|
+
|
|
43
|
+
You need a CertOne tenant and an API key (Developer Hub -> API keys, `member` role).
|
|
44
|
+
|
|
45
|
+
## Install
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
pip install certone-sdk
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Python 3.10+. Extras: `certone-sdk[azure]` reads the API key from Azure Key Vault with the compute's
|
|
52
|
+
managed identity.
|
|
53
|
+
|
|
54
|
+
## Command line: `certone-submit`
|
|
55
|
+
|
|
56
|
+
One command for any CI/CD system. It packs the project, submits it for a confidential scan, shows
|
|
57
|
+
progress until the verdict, and exits `0` on pass, `1` on fail, `2` on error -- so the job stops on a
|
|
58
|
+
failing certification.
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
export CERTONE_API_KEY=ck_... # a tenant member key
|
|
62
|
+
certone-submit --project . --asset "Fraud Scorer" --country-code EU --environment ci
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
| option | |
|
|
66
|
+
|---|---|
|
|
67
|
+
| `--asset` | AI system name in CertOne (required) |
|
|
68
|
+
| `--country-code` | region the system operates in, e.g. `EU`, `US`, `CA` |
|
|
69
|
+
| `--environment` | `ci`, `jupyter`, `vscode`, `edge`, `api`, `cloud`, `cli` |
|
|
70
|
+
| `--tee` | `amd_sev_snp` (default) or `kata_cc` |
|
|
71
|
+
| `--fail-on` | lowest severity that fails the gate: `low`, `medium`, `high` (default), `none` |
|
|
72
|
+
| `--api` | platform URL (default `$CERTONE_API_URL`, else `https://platform.certkor.ai`) |
|
|
73
|
+
| `--api-key-vault` | read the key from `https://<vault>.vault.azure.net/secrets/<name>` |
|
|
74
|
+
| `--report-out`, `--attestation-out`, `--markdown-out` | write the report, the attestation (passing verdicts) or a markdown summary (e.g. `$GITHUB_STEP_SUMMARY`) |
|
|
75
|
+
|
|
76
|
+
## Python
|
|
77
|
+
|
|
78
|
+
```python
|
|
79
|
+
import os
|
|
80
|
+
from certone_sdk import CertOneClient, ProgressPrinter, submit_and_wait
|
|
81
|
+
|
|
82
|
+
client = CertOneClient("https://platform.certkor.ai", os.environ["CERTONE_API_KEY"])
|
|
83
|
+
sub = submit_and_wait(client, "rag-service", asset="RAG Answering Service", tee="kata_cc",
|
|
84
|
+
country_code="EU", on_progress=ProgressPrinter())
|
|
85
|
+
print(sub.verdict, sub.report["by_severity"])
|
|
86
|
+
for issue in sub.document["issues"]:
|
|
87
|
+
print(issue["severity"], issue["description"])
|
|
88
|
+
if sub.passed:
|
|
89
|
+
print(sub.attestation["attestation_id"])
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
The client also covers the rest of the API: `list_systems()`, `observability_summary()`,
|
|
93
|
+
`get_run()` (where the scan executed and the TEE's hardware attestation), `get_report_document()`,
|
|
94
|
+
`get_attestation()`.
|
|
95
|
+
|
|
96
|
+
## Agent: `certone-agent`
|
|
97
|
+
|
|
98
|
+
For devices and long-running services: keep certifying a project as it changes.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
# re-scan confidentially whenever the project's content changes
|
|
102
|
+
certone-agent watch --mode confidential --project /opt/model --asset "Line Robot Picker" \
|
|
103
|
+
--api https://platform.certkor.ai --environment edge --country-code EU --interval 600
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`--mode local` runs the evaluation where the code lives and submits only the results (needs the
|
|
107
|
+
CertOne engine, `certone-sdk[agent]`, available to CertOne customers). `certone-agent deliver`
|
|
108
|
+
uploads a result signed inside an air-gapped network (`certone result sign`); the platform verifies
|
|
109
|
+
it against the source keys registered for the tenant.
|
|
110
|
+
|
|
111
|
+
## Licence
|
|
112
|
+
|
|
113
|
+
Proprietary. Use requires a CertOne subscription; see https://certkor.ai.
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
# certone-sdk
|
|
2
|
+
|
|
3
|
+
The Python SDK and command-line tools for [CertOne](https://certkor.ai), Certkor's AI compliance
|
|
4
|
+
certification platform. Send an AI project for a confidential scan from any pipeline, notebook,
|
|
5
|
+
editor or device, gate on the verdict, and fetch the evidence and the attestation.
|
|
6
|
+
|
|
7
|
+
CertOne evaluates the project inside a hardware-attested trusted execution environment (AMD SEV-SNP
|
|
8
|
+
confidential VMs, or Kata confidential containers): it builds the AI bill of materials, checks it
|
|
9
|
+
against the policy packs for the regions where the system runs (EU AI Act, NIST AI RMF, ISO/IEC
|
|
10
|
+
42001, sector rules...), runs dynamic tests and computes the evaluation metrics.
|
|
11
|
+
|
|
12
|
+
You need a CertOne tenant and an API key (Developer Hub -> API keys, `member` role).
|
|
13
|
+
|
|
14
|
+
## Install
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
pip install certone-sdk
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Python 3.10+. Extras: `certone-sdk[azure]` reads the API key from Azure Key Vault with the compute's
|
|
21
|
+
managed identity.
|
|
22
|
+
|
|
23
|
+
## Command line: `certone-submit`
|
|
24
|
+
|
|
25
|
+
One command for any CI/CD system. It packs the project, submits it for a confidential scan, shows
|
|
26
|
+
progress until the verdict, and exits `0` on pass, `1` on fail, `2` on error -- so the job stops on a
|
|
27
|
+
failing certification.
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
export CERTONE_API_KEY=ck_... # a tenant member key
|
|
31
|
+
certone-submit --project . --asset "Fraud Scorer" --country-code EU --environment ci
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
| option | |
|
|
35
|
+
|---|---|
|
|
36
|
+
| `--asset` | AI system name in CertOne (required) |
|
|
37
|
+
| `--country-code` | region the system operates in, e.g. `EU`, `US`, `CA` |
|
|
38
|
+
| `--environment` | `ci`, `jupyter`, `vscode`, `edge`, `api`, `cloud`, `cli` |
|
|
39
|
+
| `--tee` | `amd_sev_snp` (default) or `kata_cc` |
|
|
40
|
+
| `--fail-on` | lowest severity that fails the gate: `low`, `medium`, `high` (default), `none` |
|
|
41
|
+
| `--api` | platform URL (default `$CERTONE_API_URL`, else `https://platform.certkor.ai`) |
|
|
42
|
+
| `--api-key-vault` | read the key from `https://<vault>.vault.azure.net/secrets/<name>` |
|
|
43
|
+
| `--report-out`, `--attestation-out`, `--markdown-out` | write the report, the attestation (passing verdicts) or a markdown summary (e.g. `$GITHUB_STEP_SUMMARY`) |
|
|
44
|
+
|
|
45
|
+
## Python
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
import os
|
|
49
|
+
from certone_sdk import CertOneClient, ProgressPrinter, submit_and_wait
|
|
50
|
+
|
|
51
|
+
client = CertOneClient("https://platform.certkor.ai", os.environ["CERTONE_API_KEY"])
|
|
52
|
+
sub = submit_and_wait(client, "rag-service", asset="RAG Answering Service", tee="kata_cc",
|
|
53
|
+
country_code="EU", on_progress=ProgressPrinter())
|
|
54
|
+
print(sub.verdict, sub.report["by_severity"])
|
|
55
|
+
for issue in sub.document["issues"]:
|
|
56
|
+
print(issue["severity"], issue["description"])
|
|
57
|
+
if sub.passed:
|
|
58
|
+
print(sub.attestation["attestation_id"])
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
The client also covers the rest of the API: `list_systems()`, `observability_summary()`,
|
|
62
|
+
`get_run()` (where the scan executed and the TEE's hardware attestation), `get_report_document()`,
|
|
63
|
+
`get_attestation()`.
|
|
64
|
+
|
|
65
|
+
## Agent: `certone-agent`
|
|
66
|
+
|
|
67
|
+
For devices and long-running services: keep certifying a project as it changes.
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
# re-scan confidentially whenever the project's content changes
|
|
71
|
+
certone-agent watch --mode confidential --project /opt/model --asset "Line Robot Picker" \
|
|
72
|
+
--api https://platform.certkor.ai --environment edge --country-code EU --interval 600
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
`--mode local` runs the evaluation where the code lives and submits only the results (needs the
|
|
76
|
+
CertOne engine, `certone-sdk[agent]`, available to CertOne customers). `certone-agent deliver`
|
|
77
|
+
uploads a result signed inside an air-gapped network (`certone result sign`); the platform verifies
|
|
78
|
+
it against the source keys registered for the tenant.
|
|
79
|
+
|
|
80
|
+
## Licence
|
|
81
|
+
|
|
82
|
+
Proprietary. Use requires a CertOne subscription; see https://certkor.ai.
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""CertOne SDK — a typed API client + the run-at-source agent.
|
|
2
|
+
|
|
3
|
+
from certone_sdk import CertOneClient
|
|
4
|
+
c = CertOneClient("https://api.yourco.com", token)
|
|
5
|
+
c.ingest_report(report, asset="Fraud Scorer", environment="ci", country_code="EU")
|
|
6
|
+
|
|
7
|
+
``certone-submit`` sends a project for a confidential scan and gates on the verdict (the path every
|
|
8
|
+
CI/MLOps/editor integration uses). The ``certone-agent`` CLI runs the engine where your code lives
|
|
9
|
+
and submits only the results, for systems that may not leave your environment.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from certone_sdk.client import CertOneClient, CertOneError, sha256_digest
|
|
15
|
+
from certone_sdk.submit import ProgressPrinter, Submission, pack_project, submit_and_wait
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"CertOneClient",
|
|
19
|
+
"CertOneError",
|
|
20
|
+
"ProgressPrinter",
|
|
21
|
+
"Submission",
|
|
22
|
+
"__version__",
|
|
23
|
+
"pack_project",
|
|
24
|
+
"sha256_digest",
|
|
25
|
+
"submit_and_wait",
|
|
26
|
+
]
|
|
27
|
+
__version__ = "0.2.0"
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""certone-agent — certify an AI project from where it lives (a gateway, a build box, a device).
|
|
2
|
+
|
|
3
|
+
Two modes, for CI/cron (`run`, one-shot) or a long-running watcher (`watch`):
|
|
4
|
+
|
|
5
|
+
* ``--mode local`` (default, run-at-source): the model/code never leaves your environment. The
|
|
6
|
+
agent builds the AI BOM + runs the verdict loop locally, then POSTs the **report** (plus a content
|
|
7
|
+
digest that binds the ⑥ attestation) to the control plane via the SDK.
|
|
8
|
+
* ``--mode confidential``: the project is sent to the platform and processed inside an attested
|
|
9
|
+
confidential worker (static + dynamic tests on the target + metrics + the in-TEE correction),
|
|
10
|
+
exactly as ``certone-submit`` does. ``watch`` re-submits only when the project's content changes,
|
|
11
|
+
so a gateway re-certifies each new model version as it lands.
|
|
12
|
+
|
|
13
|
+
The API key comes from ``--token`` or ``$CERTONE_API_KEY`` (preferred: it then never appears in the
|
|
14
|
+
process list).
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
import argparse
|
|
20
|
+
import hashlib
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
import sys
|
|
24
|
+
import time
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
from certone_sdk.client import CertOneClient, CertOneError
|
|
29
|
+
from certone_sdk.submit import SKIP_DIRS, pack_project, submit_and_wait
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def run_once(
|
|
33
|
+
project: str,
|
|
34
|
+
pack_paths: list[str],
|
|
35
|
+
*,
|
|
36
|
+
client: CertOneClient,
|
|
37
|
+
asset: str | None = None,
|
|
38
|
+
environment: str = "ci",
|
|
39
|
+
country_code: str = "ZZ",
|
|
40
|
+
fail_on: str = "high",
|
|
41
|
+
snapshot_ref: str | None = None,
|
|
42
|
+
repo: str | None = None,
|
|
43
|
+
commit_hash: str | None = None,
|
|
44
|
+
) -> dict[str, Any]:
|
|
45
|
+
"""Run the engine over ``project`` against ``pack_paths``; submit the report (run-at-source).
|
|
46
|
+
|
|
47
|
+
Pass ``repo`` + ``commit_hash`` (or run inside GitHub Actions, where they're auto-detected) to
|
|
48
|
+
close the two-way loop: the verdict is posted back as a native Check Run on that commit.
|
|
49
|
+
"""
|
|
50
|
+
from certone_engine.archive import make_targz
|
|
51
|
+
from certone_engine.evaluation.runner import run_check
|
|
52
|
+
|
|
53
|
+
result = run_check(project, list(pack_paths), fail_on=fail_on, snapshot_ref=snapshot_ref)
|
|
54
|
+
report = result.report.to_dict()
|
|
55
|
+
# Bind the attestation to the snapshot content without uploading it.
|
|
56
|
+
digest = "sha256:" + hashlib.sha256(make_targz(Path(project))).hexdigest()
|
|
57
|
+
return client.ingest_report(
|
|
58
|
+
report,
|
|
59
|
+
asset=asset,
|
|
60
|
+
environment=environment,
|
|
61
|
+
country_code=country_code,
|
|
62
|
+
snapshot_ref=snapshot_ref,
|
|
63
|
+
input_digest=digest,
|
|
64
|
+
repo=repo,
|
|
65
|
+
commit_hash=commit_hash,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def content_digest(project: str | Path) -> str:
|
|
70
|
+
"""``sha256:`` over the project's files (paths + bytes), skipping VCS metadata and caches --
|
|
71
|
+
what ``watch`` compares to decide whether a new version landed."""
|
|
72
|
+
root = Path(project).resolve()
|
|
73
|
+
h = hashlib.sha256()
|
|
74
|
+
for path in sorted(root.rglob("*")):
|
|
75
|
+
rel = path.relative_to(root)
|
|
76
|
+
if path.is_file() and not path.is_symlink() and not SKIP_DIRS.intersection(rel.parts):
|
|
77
|
+
h.update(rel.as_posix().encode() + b"\0")
|
|
78
|
+
with path.open("rb") as fh:
|
|
79
|
+
for chunk in iter(lambda: fh.read(1 << 20), b""):
|
|
80
|
+
h.update(chunk)
|
|
81
|
+
return f"sha256:{h.hexdigest()}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def run_confidential(
|
|
85
|
+
project: str | Path,
|
|
86
|
+
*,
|
|
87
|
+
client: CertOneClient,
|
|
88
|
+
asset: str | None = None,
|
|
89
|
+
environment: str = "edge",
|
|
90
|
+
country_code: str = "ZZ",
|
|
91
|
+
fail_on: str = "high",
|
|
92
|
+
tee: str = "amd_sev_snp",
|
|
93
|
+
snapshot_ref: str | None = None,
|
|
94
|
+
commit_hash: str | None = None,
|
|
95
|
+
) -> str:
|
|
96
|
+
"""Send ``project`` for a confidential scan and print its verdict; returns the verdict."""
|
|
97
|
+
root = Path(project).resolve()
|
|
98
|
+
name = asset or root.name
|
|
99
|
+
sub = submit_and_wait(
|
|
100
|
+
client,
|
|
101
|
+
pack_project(root),
|
|
102
|
+
asset=name,
|
|
103
|
+
environment=environment,
|
|
104
|
+
country_code=country_code,
|
|
105
|
+
fail_on=fail_on,
|
|
106
|
+
tee=tee,
|
|
107
|
+
snapshot_ref=snapshot_ref,
|
|
108
|
+
commit_hash=commit_hash,
|
|
109
|
+
on_submitted=lambda scan_id, digest: print(
|
|
110
|
+
f"certone-agent: {name} -> confidential scan {scan_id} ({digest})", flush=True
|
|
111
|
+
),
|
|
112
|
+
)
|
|
113
|
+
by = sub.report.get("by_severity") or {}
|
|
114
|
+
runtime = (sub.run.get("execution") or {}).get("runtime") or "the confidential worker"
|
|
115
|
+
att = sub.attestation.get("attestation_id") if sub.attestation else None
|
|
116
|
+
attestation = f"; attestation {att}" if att else ""
|
|
117
|
+
print(
|
|
118
|
+
f"certone-agent: verdict {sub.verdict.upper()} ({by.get('high', 0)} high, "
|
|
119
|
+
f"{by.get('medium', 0)} medium, {by.get('low', 0)} low) in {runtime}{attestation}",
|
|
120
|
+
flush=True,
|
|
121
|
+
)
|
|
122
|
+
return sub.verdict
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def watch_confidential(scan: Any, project: str | Path, interval: int, *, polls: int = 0) -> None:
|
|
126
|
+
"""Call ``scan()`` for the first version of ``project`` and again whenever its content changes.
|
|
127
|
+
|
|
128
|
+
A failed scan is retried on the next poll. ``polls`` > 0 bounds the loop (tests); 0 = forever.
|
|
129
|
+
"""
|
|
130
|
+
seen: str | None = None
|
|
131
|
+
done = 0
|
|
132
|
+
while polls <= 0 or done < polls:
|
|
133
|
+
digest = content_digest(project)
|
|
134
|
+
if digest != seen:
|
|
135
|
+
if seen is not None:
|
|
136
|
+
print(f"certone-agent: new version detected ({digest})", flush=True)
|
|
137
|
+
try:
|
|
138
|
+
scan()
|
|
139
|
+
seen = digest
|
|
140
|
+
except CertOneError as exc:
|
|
141
|
+
print(f"certone-agent: {exc}", file=sys.stderr, flush=True)
|
|
142
|
+
done += 1
|
|
143
|
+
if polls <= 0 or done < polls:
|
|
144
|
+
time.sleep(max(1, interval))
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _ci_source(repo: str | None, commit_hash: str | None) -> tuple[str | None, str | None]:
|
|
148
|
+
"""Fill repo/commit from the GitHub Actions environment when not given explicitly."""
|
|
149
|
+
return (
|
|
150
|
+
repo or os.environ.get("GITHUB_REPOSITORY") or None,
|
|
151
|
+
commit_hash or os.environ.get("GITHUB_SHA") or None,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
def _add_common(p: argparse.ArgumentParser) -> None:
|
|
156
|
+
p.add_argument("--project", default=".", help="path to the AI project (default: .)")
|
|
157
|
+
p.add_argument(
|
|
158
|
+
"--mode",
|
|
159
|
+
choices=("local", "confidential"),
|
|
160
|
+
default="local",
|
|
161
|
+
help="local: run at source and submit the report; confidential: scan in the platform TEE",
|
|
162
|
+
)
|
|
163
|
+
p.add_argument(
|
|
164
|
+
"--pack", action="append", default=[], dest="packs", help="PaC pack dir (repeatable)"
|
|
165
|
+
)
|
|
166
|
+
p.add_argument(
|
|
167
|
+
"--api", required=True, help="control-plane base URL, e.g. https://api.yourco.com"
|
|
168
|
+
)
|
|
169
|
+
p.add_argument("--token", default=None, help="API token, member role ($CERTONE_API_KEY)")
|
|
170
|
+
p.add_argument("--asset", help="AI system name (auto from snapshot-ref if omitted)")
|
|
171
|
+
p.add_argument("--environment", default=None, help="ci|jupyter|vscode|edge|api|cloud|cli")
|
|
172
|
+
p.add_argument("--country-code", default="ZZ", help="origin region, e.g. EU")
|
|
173
|
+
p.add_argument("--fail-on", default="high", help="low|medium|high|none")
|
|
174
|
+
p.add_argument("--tee", default="amd_sev_snp", help="amd_sev_snp | kata_cc (confidential)")
|
|
175
|
+
p.add_argument("--snapshot-ref", help="stable source ref, e.g. git:repo@sha")
|
|
176
|
+
p.add_argument("--repo", help="GitHub repo 'owner/name' for native PR checks (auto in Actions)")
|
|
177
|
+
p.add_argument("--commit", help="head SHA to post the Check Run on (auto in Actions)")
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def deliver(client: CertOneClient, envelope_path: str | Path) -> dict[str, Any]:
|
|
181
|
+
"""Deliver a result signed inside an air-gapped network (``certone result sign``)."""
|
|
182
|
+
envelope = json.loads(Path(envelope_path).read_text(encoding="utf-8"))
|
|
183
|
+
return client.ingest_signed_result(envelope)
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def main(argv: list[str] | None = None) -> int:
|
|
187
|
+
parser = argparse.ArgumentParser(prog="certone-agent", description=__doc__)
|
|
188
|
+
sub = parser.add_subparsers(dest="cmd", required=True)
|
|
189
|
+
run_p = sub.add_parser("run", help="run once and submit (CI/cron)")
|
|
190
|
+
_add_common(run_p)
|
|
191
|
+
watch_p = sub.add_parser("watch", help="keep certifying (long-running)")
|
|
192
|
+
_add_common(watch_p)
|
|
193
|
+
watch_p.add_argument("--interval", type=int, default=300, help="seconds between checks")
|
|
194
|
+
deliver_p = sub.add_parser(
|
|
195
|
+
"deliver", help="deliver a signed result carried out of an air-gapped network"
|
|
196
|
+
)
|
|
197
|
+
deliver_p.add_argument(
|
|
198
|
+
"envelope", help="the result.signed.json written by `certone result sign`"
|
|
199
|
+
)
|
|
200
|
+
deliver_p.add_argument("--api", required=True, help="control-plane base URL")
|
|
201
|
+
deliver_p.add_argument("--token", default=None, help="API token ($CERTONE_API_KEY)")
|
|
202
|
+
args = parser.parse_args(argv)
|
|
203
|
+
|
|
204
|
+
token = args.token or os.environ.get("CERTONE_API_KEY")
|
|
205
|
+
if not token:
|
|
206
|
+
parser.error("no API key: pass --token or set CERTONE_API_KEY")
|
|
207
|
+
client = CertOneClient(args.api, token)
|
|
208
|
+
|
|
209
|
+
if args.cmd == "deliver":
|
|
210
|
+
try:
|
|
211
|
+
out = deliver(client, args.envelope)
|
|
212
|
+
except CertOneError as exc:
|
|
213
|
+
print(f"certone-agent: delivery refused: {exc}", file=sys.stderr)
|
|
214
|
+
return 2
|
|
215
|
+
report = out.get("report") or {}
|
|
216
|
+
print(
|
|
217
|
+
f"certone-agent: signed result accepted (signature verified): scan "
|
|
218
|
+
f"{out['scan_run']['id']}, verdict {str(report.get('verdict_status', '?')).upper()}"
|
|
219
|
+
)
|
|
220
|
+
return 0
|
|
221
|
+
repo, commit_hash = _ci_source(args.repo, args.commit)
|
|
222
|
+
|
|
223
|
+
if args.mode == "confidential":
|
|
224
|
+
|
|
225
|
+
def scan() -> str:
|
|
226
|
+
return run_confidential(
|
|
227
|
+
args.project,
|
|
228
|
+
client=client,
|
|
229
|
+
asset=args.asset,
|
|
230
|
+
environment=args.environment or "edge",
|
|
231
|
+
country_code=args.country_code,
|
|
232
|
+
fail_on=args.fail_on,
|
|
233
|
+
tee=args.tee,
|
|
234
|
+
snapshot_ref=args.snapshot_ref,
|
|
235
|
+
commit_hash=commit_hash,
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if args.cmd == "watch":
|
|
239
|
+
print(f"certone-agent: watching {Path(args.project).resolve()}", flush=True)
|
|
240
|
+
watch_confidential(scan, args.project, args.interval)
|
|
241
|
+
return 0
|
|
242
|
+
try:
|
|
243
|
+
return 0 if scan() == "pass" else 1
|
|
244
|
+
except CertOneError as exc:
|
|
245
|
+
print(f"certone-agent: {exc}", file=sys.stderr)
|
|
246
|
+
return 2
|
|
247
|
+
|
|
248
|
+
def _go() -> str:
|
|
249
|
+
out = run_once(
|
|
250
|
+
args.project,
|
|
251
|
+
args.packs,
|
|
252
|
+
client=client,
|
|
253
|
+
asset=args.asset,
|
|
254
|
+
environment=args.environment or "ci",
|
|
255
|
+
country_code=args.country_code,
|
|
256
|
+
fail_on=args.fail_on,
|
|
257
|
+
snapshot_ref=args.snapshot_ref,
|
|
258
|
+
repo=repo,
|
|
259
|
+
commit_hash=commit_hash,
|
|
260
|
+
)
|
|
261
|
+
return str(out.get("report", {}).get("verdict_status", "?"))
|
|
262
|
+
|
|
263
|
+
if args.cmd == "watch":
|
|
264
|
+
while True:
|
|
265
|
+
verdict = _go()
|
|
266
|
+
print(f"certone-agent: submitted (verdict={verdict})")
|
|
267
|
+
time.sleep(max(5, args.interval))
|
|
268
|
+
verdict = _go()
|
|
269
|
+
print(f"certone-agent: submitted (verdict={verdict})")
|
|
270
|
+
return 0
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
if __name__ == "__main__": # pragma: no cover
|
|
274
|
+
raise SystemExit(main())
|