certone-sdk 0.2.0__py3-none-any.whl

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,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"
certone_sdk/agent.py ADDED
@@ -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())
certone_sdk/client.py ADDED
@@ -0,0 +1,262 @@
1
+ """Typed HTTP client for the CertOne control-plane API (the SDK enterprises script against).
2
+
3
+ Implements the operations in ``sdk/SPEC.md``, the contract every CertOne SDK shares.
4
+ """
5
+
6
+ from __future__ import annotations
7
+
8
+ import hashlib
9
+ import time
10
+ from collections.abc import Callable
11
+ from pathlib import Path
12
+ from typing import IO, Any
13
+
14
+ # (connect, read) timeout for every API call so a hung control plane can't hang the caller's CI job.
15
+ _HTTP_TIMEOUT = (5, 30)
16
+ # Snapshot uploads can be large; the read side waits for the control plane to accept the archive.
17
+ _UPLOAD_TIMEOUT = (5, 300)
18
+
19
+ Snapshot = str | Path | bytes | IO[bytes]
20
+
21
+
22
+ class CertOneError(RuntimeError):
23
+ """A control-plane API call failed. ``status`` is the HTTP status (0 when none was received)."""
24
+
25
+ def __init__(self, message: str, status: int = 0, detail: str = "") -> None:
26
+ super().__init__(message)
27
+ self.status = status
28
+ self.detail = detail
29
+
30
+
31
+ def sha256_digest(data: Snapshot) -> str:
32
+ """``sha256:<hex>`` of a snapshot, the ``input_digest`` that binds an attestation to it."""
33
+ h = hashlib.sha256()
34
+ if isinstance(data, bytes):
35
+ h.update(data)
36
+ elif isinstance(data, str | Path):
37
+ with Path(data).open("rb") as fh:
38
+ for chunk in iter(lambda: fh.read(1 << 20), b""):
39
+ h.update(chunk)
40
+ else:
41
+ for chunk in iter(lambda: data.read(1 << 20), b""):
42
+ h.update(chunk)
43
+ return f"sha256:{h.hexdigest()}"
44
+
45
+
46
+ class CertOneClient:
47
+ """Minimal client. ``requests`` is imported lazily; inject ``session`` in tests."""
48
+
49
+ def __init__(self, base_url: str, token: str | None = None, *, session: Any = None) -> None:
50
+ self.base_url = base_url.rstrip("/")
51
+ self.token = token
52
+ self._session = session
53
+
54
+ def _sess(self) -> Any:
55
+ if self._session is None:
56
+ import requests # lazy: only when a real call is made
57
+
58
+ self._session = requests.Session()
59
+ return self._session
60
+
61
+ def _headers(self, json_body: bool = False) -> dict[str, str]:
62
+ headers: dict[str, str] = {}
63
+ if self.token:
64
+ headers["authorization"] = f"Bearer {self.token}"
65
+ if json_body:
66
+ headers["content-type"] = "application/json"
67
+ return headers
68
+
69
+ def _post(self, path: str, body: dict[str, Any]) -> dict[str, Any]:
70
+ resp = self._sess().post(
71
+ f"{self.base_url}{path}",
72
+ json=body,
73
+ headers=self._headers(json_body=True),
74
+ timeout=_HTTP_TIMEOUT,
75
+ )
76
+ result: dict[str, Any] = _result(resp)
77
+ return result
78
+
79
+ def _get(self, path: str) -> Any:
80
+ resp = self._sess().get(
81
+ f"{self.base_url}{path}", headers=self._headers(), timeout=_HTTP_TIMEOUT
82
+ )
83
+ return _result(resp)
84
+
85
+ def _submit(self, path: str, snapshot: Snapshot, form: dict[str, str]) -> dict[str, Any]:
86
+ if isinstance(snapshot, str | Path):
87
+ with Path(snapshot).open("rb") as fh:
88
+ return self._submit(path, fh, form)
89
+ resp = self._sess().post(
90
+ f"{self.base_url}{path}",
91
+ data={k: v for k, v in form.items() if v is not None},
92
+ files={"snapshot": ("snapshot.tar.gz", snapshot, "application/gzip")},
93
+ headers=self._headers(),
94
+ timeout=_UPLOAD_TIMEOUT,
95
+ )
96
+ result: dict[str, Any] = _result(resp)
97
+ return result
98
+
99
+ # --- submit a project snapshot (.tar.gz) ---
100
+ def submit_check(
101
+ self,
102
+ snapshot: Snapshot,
103
+ *,
104
+ asset: str | None = None,
105
+ environment: str = "api",
106
+ country_code: str = "ZZ",
107
+ fail_on: str = "high",
108
+ packs: str = "",
109
+ snapshot_ref: str | None = None,
110
+ commit_hash: str | None = None,
111
+ ) -> dict[str, Any]:
112
+ """Static verdict (AI BOM + policy-as-code) over a snapshot."""
113
+ return self._submit(
114
+ "/v1/checks",
115
+ snapshot,
116
+ _form(asset, environment, country_code, fail_on, packs, snapshot_ref, commit_hash),
117
+ )
118
+
119
+ def submit_process(
120
+ self,
121
+ snapshot: Snapshot,
122
+ *,
123
+ asset: str | None = None,
124
+ environment: str = "api",
125
+ country_code: str = "ZZ",
126
+ fail_on: str = "high",
127
+ packs: str = "",
128
+ snapshot_ref: str | None = None,
129
+ commit_hash: str | None = None,
130
+ tee: str = "amd_sev_snp",
131
+ ) -> dict[str, Any]:
132
+ """Full processing in the attested confidential worker (static + dynamic + metrics).
133
+
134
+ On the confidential worker pool the result is ``queued`` with no report yet: follow it with
135
+ :meth:`wait_for_report`.
136
+ """
137
+ form = _form(asset, environment, country_code, fail_on, packs, snapshot_ref, commit_hash)
138
+ form["tee"] = tee
139
+ return self._submit("/v1/process", snapshot, form)
140
+
141
+ # --- run-at-source: submit a pre-computed report (no artifact upload) ---
142
+ def ingest_report(
143
+ self,
144
+ report: dict[str, Any],
145
+ *,
146
+ asset: str | None = None,
147
+ environment: str = "api",
148
+ country_code: str = "ZZ",
149
+ snapshot_ref: str | None = None,
150
+ input_digest: str | None = None,
151
+ repo: str | None = None,
152
+ commit_hash: str | None = None,
153
+ ) -> dict[str, Any]:
154
+ return self._post(
155
+ "/v1/checks/report",
156
+ {
157
+ "report": report,
158
+ "asset": asset,
159
+ "environment": environment,
160
+ "country_code": country_code,
161
+ "snapshot_ref": snapshot_ref,
162
+ "input_digest": input_digest,
163
+ "repo": repo,
164
+ "commit_hash": commit_hash,
165
+ },
166
+ )
167
+
168
+ # --- air-gapped run-at-source: deliver a result signed inside the closed network ---
169
+ def ingest_signed_result(self, envelope: dict[str, Any]) -> dict[str, Any]:
170
+ """Submit a ``certone result sign`` envelope; the platform verifies its source-key
171
+ signature before accepting it (re-delivering the same file returns the same scan)."""
172
+ return self._post("/v1/checks/report/signed", envelope)
173
+
174
+ # --- scans, reports, attestations ---
175
+ def get_scan(self, scan_id: str) -> dict[str, Any]:
176
+ scan: dict[str, Any] = self._get(f"/v1/scans/{scan_id}")
177
+ return scan
178
+
179
+ def wait_for_report(
180
+ self,
181
+ scan_id: str,
182
+ *,
183
+ timeout: float = 900.0,
184
+ interval: float = 2.0,
185
+ on_status: Callable[[str, float], None] | None = None,
186
+ ) -> dict[str, Any]:
187
+ """Poll a scan until it finishes, then return its report summary. ``on_status(status,
188
+ elapsed_s)`` is called after every poll (``queued``, ``running``, ``succeeded``...).
189
+
190
+ Raises :class:`CertOneError` when the scan failed or did not finish within ``timeout``.
191
+ """
192
+ start = time.monotonic()
193
+ deadline = start + timeout
194
+ while True:
195
+ scan = self.get_scan(scan_id)
196
+ if on_status is not None:
197
+ on_status(str(scan.get("status", "")), time.monotonic() - start)
198
+ if scan.get("status") == "succeeded":
199
+ report: dict[str, Any] = self._get(f"/v1/scans/{scan_id}/report")
200
+ return report
201
+ if scan.get("status") == "failed":
202
+ raise CertOneError(f"scan {scan_id} failed: {scan.get('error') or 'no detail'}")
203
+ if time.monotonic() >= deadline:
204
+ raise CertOneError(f"scan {scan_id} did not finish within {timeout:.0f}s")
205
+ time.sleep(interval)
206
+
207
+ def get_run(self, scan_id: str) -> dict[str, Any]:
208
+ """How a scan ran: ``execution.runtime`` (where its code executed) and ``tee_evidence``
209
+ (the confidential VM's hardware attestation, ``attested``)."""
210
+ run: dict[str, Any] = self._get(f"/v1/scans/{scan_id}/run")
211
+ return run
212
+
213
+ def get_report_document(self, report_id: str) -> dict[str, Any]:
214
+ """The full engine report: verdict, summary, issues (``report_id``: the record ``id``)."""
215
+ doc: dict[str, Any] = self._get(f"/v1/reports/{report_id}/document")
216
+ return doc
217
+
218
+ def get_attestation(self, report_id: str) -> dict[str, Any]:
219
+ """The signed attestation of a passing report (404 -> CertOneError when it did not pass)."""
220
+ attestation: dict[str, Any] = self._get(f"/v1/reports/{report_id}/attestation")
221
+ return attestation
222
+
223
+ # --- reads ---
224
+ def observability_summary(self) -> dict[str, Any]:
225
+ summary: dict[str, Any] = self._get("/v1/observability/summary")
226
+ return summary
227
+
228
+ def list_systems(self) -> list[dict[str, Any]]:
229
+ systems: list[dict[str, Any]] = self._get("/v1/systems")
230
+ return systems
231
+
232
+
233
+ def _form(
234
+ asset: str | None,
235
+ environment: str,
236
+ country_code: str,
237
+ fail_on: str,
238
+ packs: str,
239
+ snapshot_ref: str | None,
240
+ commit_hash: str | None,
241
+ ) -> dict[str, str]:
242
+ form = {"environment": environment, "country_code": country_code, "fail_on": fail_on}
243
+ optional = {
244
+ "asset": asset,
245
+ "packs": packs,
246
+ "snapshot_ref": snapshot_ref,
247
+ "commit_hash": commit_hash,
248
+ }
249
+ form.update({k: v for k, v in optional.items() if v})
250
+ return form
251
+
252
+
253
+ def _result(resp: Any) -> Any:
254
+ status = int(getattr(resp, "status_code", 0))
255
+ if status >= 400:
256
+ detail = ""
257
+ try:
258
+ detail = str(resp.json().get("detail", ""))
259
+ except Exception: # best-effort error detail extraction
260
+ detail = getattr(resp, "text", "")
261
+ raise CertOneError(f"HTTP {status}: {detail}", status=status, detail=detail)
262
+ return resp.json()
certone_sdk/py.typed ADDED
File without changes
certone_sdk/submit.py ADDED
@@ -0,0 +1,363 @@
1
+ """certone-submit — send a project to CertOne for a confidential scan and gate on the verdict.
2
+
3
+ The one submission path every integration shares (GitHub Actions, GitLab CI, Azure ML, VS Code,
4
+ Jupyter, the edge agent): the project is packed, uploaded to the platform, and processed inside an
5
+ attested confidential worker (AMD SEV-SNP CVM or Kata confidential containers) -- static analysis,
6
+ dynamic tests on the target, metrics and the in-TEE correction. The command waits for the verdict,
7
+ prints the findings, writes the evidence, and exits non-zero unless the verdict passes, so a
8
+ pipeline step can gate on it.
9
+
10
+ certone-submit --project . --asset "Card Fraud Scorer" # key from $CERTONE_API_KEY
11
+
12
+ Exit codes: 0 pass, 1 the verdict did not pass, 2 the scan could not be run (error, timeout).
13
+ The API key is read from ``--api-key`` or ``$CERTONE_API_KEY`` and is never printed.
14
+ """
15
+
16
+ from __future__ import annotations
17
+
18
+ import argparse
19
+ import io
20
+ import json
21
+ import os
22
+ import sys
23
+ import tarfile
24
+ from collections.abc import Callable
25
+ from dataclasses import dataclass, field
26
+ from pathlib import Path
27
+ from typing import IO, Any, ClassVar
28
+
29
+ from certone_sdk.client import CertOneClient, CertOneError, sha256_digest
30
+
31
+ #: Never shipped in a snapshot: VCS metadata, caches, virtualenvs, dependency trees.
32
+ SKIP_DIRS = frozenset(
33
+ {
34
+ ".git",
35
+ ".hg",
36
+ ".svn",
37
+ "__pycache__",
38
+ ".venv",
39
+ "venv",
40
+ "node_modules",
41
+ ".mypy_cache",
42
+ ".pytest_cache",
43
+ ".ruff_cache",
44
+ ".ipynb_checkpoints",
45
+ ".certone",
46
+ }
47
+ )
48
+ DEFAULT_API = "https://platform.certkor.ai"
49
+
50
+
51
+ def pack_project(project: Path, top: str | None = None) -> bytes:
52
+ """``project`` as a gzip tarball under one top-level folder (``top``, default: its name)."""
53
+ root = project.resolve()
54
+ name = top or root.name or "project"
55
+ buf = io.BytesIO()
56
+ with tarfile.open(fileobj=buf, mode="w:gz") as tar:
57
+ for path in sorted(root.rglob("*")):
58
+ rel = path.relative_to(root)
59
+ if path.is_file() and not path.is_symlink() and not SKIP_DIRS.intersection(rel.parts):
60
+ tar.add(path, arcname=f"{name}/{rel.as_posix()}")
61
+ return buf.getvalue()
62
+
63
+
64
+ @dataclass(frozen=True)
65
+ class Submission:
66
+ """A finished confidential scan: its id, report summary, full document and attestation."""
67
+
68
+ scan_id: str
69
+ report: dict[str, Any]
70
+ document: dict[str, Any]
71
+ attestation: dict[str, Any] | None
72
+ #: How it ran (``execution.runtime``, ``tee_evidence.attested``); empty when unavailable.
73
+ run: dict[str, Any] = field(default_factory=dict)
74
+
75
+ @property
76
+ def verdict(self) -> str:
77
+ return str(self.report.get("verdict_status", "unknown"))
78
+
79
+ @property
80
+ def passed(self) -> bool:
81
+ return self.verdict == "pass"
82
+
83
+
84
+ def submit_and_wait(
85
+ client: CertOneClient,
86
+ project: Path | bytes,
87
+ *,
88
+ asset: str,
89
+ environment: str = "ci",
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
+ timeout: float = 1800.0,
96
+ on_submitted: Any = None,
97
+ on_progress: Callable[[str, float], None] | None = None,
98
+ ) -> Submission:
99
+ """Submit ``project`` (a folder, or an already-packed snapshot) for confidential processing and
100
+ wait for its verdict. ``on_submitted(scan_id, digest)`` is called once the platform accepted
101
+ it, ``on_progress(status, elapsed_s)`` on every status poll while it runs (see
102
+ :class:`ProgressPrinter`). Raises :class:`CertOneError` when the scan cannot be run or does not
103
+ finish in time."""
104
+ snapshot = project if isinstance(project, bytes) else pack_project(project)
105
+ submitted = client.submit_process(
106
+ snapshot,
107
+ asset=asset,
108
+ environment=environment,
109
+ country_code=country_code,
110
+ fail_on=fail_on,
111
+ snapshot_ref=snapshot_ref,
112
+ commit_hash=commit_hash,
113
+ tee=tee,
114
+ )
115
+ scan_id = str(submitted["scan_run"]["id"])
116
+ if on_submitted is not None:
117
+ on_submitted(scan_id, sha256_digest(snapshot))
118
+ report = client.wait_for_report(scan_id, timeout=timeout, on_status=on_progress)
119
+ document = client.get_report_document(report["id"])
120
+ attestation = (
121
+ client.get_attestation(report["id"]) if report.get("verdict_status") == "pass" else None
122
+ )
123
+ try:
124
+ run = client.get_run(scan_id)
125
+ except CertOneError:
126
+ run = {} # informational only: the verdict and evidence above are what gate the pipeline
127
+ return Submission(scan_id, report, document, attestation, run)
128
+
129
+
130
+ class ProgressPrinter:
131
+ """Shows a running scan's progress: one line per status change, plus -- on a terminal -- a
132
+ spinner with the elapsed time, or -- in CI logs -- a heartbeat line every ``heartbeat_s``.
133
+
134
+ submit_and_wait(client, project, asset="...", on_progress=ProgressPrinter())
135
+ """
136
+
137
+ LABELS: ClassVar[dict[str, str]] = {
138
+ "queued": "queued for a confidential worker",
139
+ "running": "processing inside the TEE (static analysis, dynamic tests, metrics)",
140
+ "succeeded": "verdict ready",
141
+ "failed": "scan failed",
142
+ }
143
+
144
+ def __init__(self, stream: IO[str] | None = None, heartbeat_s: float = 30.0) -> None:
145
+ self.stream = stream or sys.stdout
146
+ self.tty = bool(getattr(self.stream, "isatty", lambda: False)())
147
+ utf8 = "utf" in (getattr(self.stream, "encoding", None) or "").lower()
148
+ self.spinner = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" if utf8 else "|/-\\"
149
+ self.heartbeat_s = heartbeat_s
150
+ self._status: str | None = None
151
+ self._beat = 0.0
152
+ self._tick = 0
153
+
154
+ def __call__(self, status: str, elapsed: float) -> None:
155
+ clock = f"{int(elapsed) // 60}:{int(elapsed) % 60:02d}"
156
+ label = self.LABELS.get(status, status)
157
+ if status != self._status:
158
+ self._status, self._beat = status, elapsed
159
+ self._write(f" [{clock}] {label}\n")
160
+ elif self.tty:
161
+ self._tick += 1
162
+ frame = self.spinner[self._tick % len(self.spinner)]
163
+ self._write(f" {frame} {label} ... {clock}")
164
+ elif elapsed - self._beat >= self.heartbeat_s:
165
+ self._beat = elapsed
166
+ self._write(f" [{clock}] still {status}\n")
167
+
168
+ def _write(self, text: str) -> None:
169
+ if self.tty:
170
+ text = "\r\x1b[K" + text # replace the spinner line
171
+ self.stream.write(text)
172
+ self.stream.flush()
173
+
174
+
175
+ def key_from_vault(secret_url: str) -> str:
176
+ """The API key held in an Azure Key Vault secret (``https://<vault>.vault.azure.net/secrets/
177
+ <name>[/<version>]``), read with the default Azure credential -- on Azure ML compute that is
178
+ the cluster's managed identity. Needs ``pip install "certone-sdk[azure]"``."""
179
+ from urllib.parse import urlsplit
180
+
181
+ from azure.identity import DefaultAzureCredential
182
+ from azure.keyvault.secrets import SecretClient
183
+
184
+ parts = urlsplit(secret_url)
185
+ segments = [s for s in parts.path.split("/") if s]
186
+ if parts.scheme != "https" or len(segments) < 2 or segments[0] != "secrets":
187
+ raise ValueError("expected https://<vault>.vault.azure.net/secrets/<name>")
188
+ client = SecretClient(f"https://{parts.netloc}", DefaultAzureCredential())
189
+ value = client.get_secret(segments[1], segments[2] if len(segments) > 2 else None).value
190
+ if not value:
191
+ raise ValueError(f"secret {segments[1]!r} is empty")
192
+ return str(value)
193
+
194
+
195
+ def _git_commit(project: Path) -> str | None:
196
+ """The CI's commit (GitHub, GitLab, Azure Pipelines), else None -- never shells out."""
197
+ for var in ("GITHUB_SHA", "CI_COMMIT_SHA", "BUILD_SOURCEVERSION"):
198
+ if os.environ.get(var):
199
+ return os.environ[var]
200
+ head = project / ".git" / "HEAD"
201
+ try:
202
+ ref = head.read_text(encoding="utf-8").strip()
203
+ if ref.startswith("ref: "):
204
+ return (project / ".git" / ref[5:]).read_text(encoding="utf-8").strip() or None
205
+ return ref or None
206
+ except OSError:
207
+ return None
208
+
209
+
210
+ def _findings(sub: Submission) -> list[tuple[str, str, str]]:
211
+ """``(severity, description, where)`` for every finding, most severe first."""
212
+ order = {"high": 0, "medium": 1, "low": 2}
213
+ out: list[tuple[str, str, str]] = []
214
+ for issue in sorted(
215
+ sub.document.get("issues", []), key=lambda i: order.get(i.get("severity"), 3)
216
+ ):
217
+ loc = issue.get("location") or {}
218
+ artifact, line = loc.get("artifact"), loc.get("line_start")
219
+ where = f"{artifact}{f':{line}' if line else ''}" if artifact not in (None, "", ".") else ""
220
+ out.append((str(issue.get("severity", "")), str(issue.get("description", "")), where))
221
+ return out
222
+
223
+
224
+ def _runtime(sub: Submission) -> str:
225
+ runtime = (sub.run.get("execution") or {}).get("runtime")
226
+ if not runtime:
227
+ return ""
228
+ attested = (sub.run.get("tee_evidence") or {}).get("attested")
229
+ return str(runtime) + (" · hardware-attested" if attested else "")
230
+
231
+
232
+ def _counts(sub: Submission) -> str:
233
+ by_sev = sub.report.get("by_severity") or {}
234
+ return ", ".join(f"{by_sev.get(s, 0)} {s}" for s in ("high", "medium", "low"))
235
+
236
+
237
+ def _print_result(sub: Submission, out: Any) -> None:
238
+ total = sub.report.get("total_issues", 0)
239
+ print(f"verdict: {sub.verdict.upper()} ({total} finding(s): {_counts(sub)})", file=out)
240
+ if runtime := _runtime(sub):
241
+ print(f"executed in: {runtime}", file=out)
242
+ for severity, description, where in _findings(sub):
243
+ print(
244
+ f" [{severity.upper():6}] {description}" + (f" ({where})" if where else ""), file=out
245
+ )
246
+ if sub.attestation:
247
+ print(f"attestation: {sub.attestation.get('attestation_id')}", file=out)
248
+
249
+
250
+ def _markdown(sub: Submission, asset: str) -> str:
251
+ """A job summary (GitHub ``$GITHUB_STEP_SUMMARY``, GitLab/Azure Pipelines artifacts)."""
252
+ icon = "✅" if sub.passed else "❌"
253
+ lines = [
254
+ f"## {icon} CertOne: {asset} — {sub.verdict.upper()}",
255
+ "",
256
+ f"Confidential scan `{sub.scan_id}` · findings: {_counts(sub)}",
257
+ ]
258
+ if runtime := _runtime(sub):
259
+ lines.append(f"Executed in: **{runtime}**")
260
+ if sub.attestation:
261
+ lines.append(f"Attestation: `{sub.attestation.get('attestation_id')}`")
262
+ rows = _findings(sub)
263
+ if rows:
264
+ lines += ["", "| Severity | Finding | Where |", "|---|---|---|"]
265
+ for severity, description, where in rows:
266
+ cell = description.replace("|", "\\|")
267
+ lines.append(f"| {severity} | {cell} | {f'`{where}`' if where else ''} |")
268
+ return "\n".join(lines) + "\n"
269
+
270
+
271
+ def main(argv: list[str] | None = None) -> int:
272
+ ap = argparse.ArgumentParser(prog="certone-submit", description=__doc__.split("\n\n")[0])
273
+ ap.add_argument("--project", type=Path, default=Path(), help="project folder (default: .)")
274
+ ap.add_argument("--asset", required=True, help="AI system name in CertOne")
275
+ ap.add_argument(
276
+ "--api",
277
+ default=os.environ.get("CERTONE_API_URL", DEFAULT_API),
278
+ help="platform URL (default: $CERTONE_API_URL or %(default)s)",
279
+ )
280
+ ap.add_argument("--api-key", default=None, help="API key (default: $CERTONE_API_KEY)")
281
+ ap.add_argument(
282
+ "--api-key-vault",
283
+ default=None,
284
+ help="read the key from an Azure Key Vault secret URL "
285
+ "(https://<vault>.vault.azure.net/secrets/<name>) with the compute's managed identity",
286
+ )
287
+ ap.add_argument("--environment", default="ci", help="ci|jupyter|vscode|edge|api|cloud|cli")
288
+ ap.add_argument("--country-code", default="ZZ", help="data-residency region, e.g. EU, US")
289
+ ap.add_argument("--fail-on", default="high", help="low|medium|high|none")
290
+ ap.add_argument("--tee", default="amd_sev_snp", help="amd_sev_snp | kata_cc")
291
+ ap.add_argument("--snapshot-ref", default=None, help="stable source ref, e.g. git:repo@sha")
292
+ ap.add_argument("--commit", default=None, help="commit hash (default: from CI / .git)")
293
+ ap.add_argument("--timeout", type=float, default=1800.0, help="seconds to wait for the verdict")
294
+ ap.add_argument("--report-out", type=Path, default=None, help="write the full report JSON here")
295
+ ap.add_argument(
296
+ "--attestation-out",
297
+ type=Path,
298
+ default=None,
299
+ help="write the attestation JSON here (passing verdicts only)",
300
+ )
301
+ ap.add_argument(
302
+ "--markdown-out",
303
+ type=Path,
304
+ default=None,
305
+ help="append a markdown summary here (e.g. $GITHUB_STEP_SUMMARY)",
306
+ )
307
+ a = ap.parse_args(argv)
308
+
309
+ key = a.api_key or os.environ.get("CERTONE_API_KEY")
310
+ if not key and a.api_key_vault:
311
+ try:
312
+ key = key_from_vault(a.api_key_vault)
313
+ except Exception as exc: # any vault/identity failure: report it, never the key
314
+ print(f"certone-submit: cannot read the key from Key Vault: {exc}", file=sys.stderr)
315
+ return 2
316
+ if not key:
317
+ print(
318
+ "certone-submit: no API key (pass --api-key, --api-key-vault or set CERTONE_API_KEY)",
319
+ file=sys.stderr,
320
+ )
321
+ return 2
322
+ if not a.project.is_dir():
323
+ print(f"certone-submit: {a.project} is not a folder", file=sys.stderr)
324
+ return 2
325
+
326
+ def submitted(scan_id: str, digest: str) -> None:
327
+ print(
328
+ f"submitted {a.asset!r} for confidential processing: scan {scan_id} ({digest})",
329
+ flush=True,
330
+ )
331
+
332
+ print(f"CertOne: packing {a.project.resolve().name} -> {a.api}", flush=True)
333
+ try:
334
+ sub = submit_and_wait(
335
+ CertOneClient(a.api, key),
336
+ a.project,
337
+ asset=a.asset,
338
+ environment=a.environment,
339
+ country_code=a.country_code,
340
+ fail_on=a.fail_on,
341
+ tee=a.tee,
342
+ snapshot_ref=a.snapshot_ref,
343
+ commit_hash=a.commit or _git_commit(a.project),
344
+ timeout=a.timeout,
345
+ on_submitted=submitted,
346
+ on_progress=ProgressPrinter(),
347
+ )
348
+ except CertOneError as exc:
349
+ print(f"certone-submit: {exc}", file=sys.stderr)
350
+ return 2
351
+ _print_result(sub, sys.stdout)
352
+ if a.report_out:
353
+ a.report_out.write_text(json.dumps(sub.document, indent=2), encoding="utf-8")
354
+ if a.attestation_out and sub.attestation:
355
+ a.attestation_out.write_text(json.dumps(sub.attestation, indent=2), encoding="utf-8")
356
+ if a.markdown_out:
357
+ with a.markdown_out.open("a", encoding="utf-8") as fh:
358
+ fh.write(_markdown(sub, a.asset))
359
+ return 0 if sub.passed else 1
360
+
361
+
362
+ if __name__ == "__main__": # pragma: no cover
363
+ raise SystemExit(main())
@@ -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,9 @@
1
+ certone_sdk/__init__.py,sha256=7sVcWywbVzVTTjiH_xXPYn8NGktxEMAipZ3cMho8LGY,936
2
+ certone_sdk/agent.py,sha256=p6551qG0KR7-qdUqHcKVgJ_G_JeCAoW1CuJUq7CNHso,10829
3
+ certone_sdk/client.py,sha256=kfOYjp47LziXruFZtbgBjm4WOG6ysuvQinaZRinNyps,9672
4
+ certone_sdk/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
+ certone_sdk/submit.py,sha256=cZKd9yAWUkErjCgJSbF_5bYfEgH1NH0IA1xZc0M7oUo,14654
6
+ certone_sdk-0.2.0.dist-info/METADATA,sha256=SmWMcPIoo-c62kzSwuxUz5XFvZqHl6ktL-yEct4hapA,4891
7
+ certone_sdk-0.2.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
8
+ certone_sdk-0.2.0.dist-info/entry_points.txt,sha256=uj_QGQNe5pBWMLWfbY9tHsMwGf6GsGr-MPYdDBIPH_c,98
9
+ certone_sdk-0.2.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.33.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ certone-agent = certone_sdk.agent:main
3
+ certone-submit = certone_sdk.submit:main