celik-muhammed commited on
Commit
b2c2b14
·
verified ·
1 Parent(s): 3dd951c

Delete security

Browse files
security/RELEASE_EVIDENCE_GUIDE.md DELETED
@@ -1,107 +0,0 @@
1
- # Production release evidence — B39
2
-
3
- B38 made the source release path reproducible. B39 prevents **evidence substitution**:
4
- a scan, SBOM or attestation is not accepted merely because a file named “scan” exists.
5
- Every production promotion must supply one short-lived `release-evidence.json` that
6
- binds the evidence to the exact repository lock/SBOM and to one immutable final OCI
7
- image digest.
8
-
9
- ## Trust boundary
10
-
11
- The evidence manifest contains **no secrets and no deployment identity**. Do not put
12
- Redis URLs, hostnames, usernames, tokens, passwords, registry credentials, request
13
- samples, user content, IP addresses, or capability values in it. Evidence artifacts
14
- stay beside the manifest and are referenced only by relative path + SHA-256.
15
-
16
- The verifier rejects path traversal, symlinks, stale/expired manifests, source-hash
17
- drift, artifact tampering, a provenance subject that does not match the final image,
18
- missing signature-verification evidence, unsafe infrastructure logging attestations,
19
- unverified Redis lifecycle properties, and risk-exception bypasses.
20
-
21
- ## Release flow
22
-
23
- ```text
24
- exact source lock + Python SBOM
25
- |
26
- v
27
- networked dependency scan
28
- |
29
- v
30
- linux/amd64 image build -> immutable image digest
31
- | |
32
- | +--> full image CycloneDX SBOM
33
- | +--> image vulnerability scan
34
- | +--> SLSA/in-toto provenance
35
- | +--> signature verification
36
- v
37
- sanitized Redis + infrastructure logging evidence
38
- |
39
- v
40
- release-evidence.json (<= 72 h validity)
41
- |
42
- v
43
- python security/verify_release_gate.py release-evidence.json
44
- |
45
- +--> GREEN: promotion may continue
46
- `--> RED: fail closed
47
- ```
48
-
49
- SLSA provenance is expected as an in-toto Statement v1 whose predicate type is
50
- `https://slsa.dev/provenance/v1` and whose subject SHA-256 is the exact final image
51
- digest. Signature verification remains a separate artifact because provenance JSON
52
- alone does not prove who signed it.
53
-
54
- ## Redis evidence without credential leakage
55
-
56
- `probe_redis_authority.py` is explicit opt-in and receives only the **name** of the
57
- environment variable that contains a Redis URL:
58
-
59
- ```bash
60
- python security/probe_redis_authority.py \
61
- --plane share \
62
- --url-env SHARE_STORE_REDIS_URL \
63
- --output redis-share-observation.json
64
- ```
65
-
66
- It never accepts the URL as a CLI argument and never emits hostname, port, username,
67
- credentials, keys, values, replication offsets, or persistence timestamps. It uses
68
- `PING`, `ACL WHOAMI` where permitted, and bounded `INFO persistence` / `INFO replication`
69
- observations. These observations do **not** paper-prove least privilege, provider
70
- persistence guarantees, backup retention, or successful restores; those remain
71
- operator/provider evidence.
72
-
73
- For Share and Contribution lifecycle authority, production evidence additionally
74
- requires persistence, replication, and a successful backup/restore exercise no older
75
- than 90 days. Rate limiting needs TLS and non-default least-privilege identity but is
76
- not falsely classified as durable user-data storage.
77
-
78
- ## Infrastructure logging / telemetry rule
79
-
80
- Production promotion fails unless operators attest that request bodies,
81
- Authorization headers, management-capability headers, query strings, WAF body
82
- capture, APM body capture, and third-party telemetry export are all disabled for the
83
- assistant service. This is independent of browser feedback consent: infrastructure
84
- logging must never become a hidden telemetry bypass.
85
-
86
- ## What B39 still cannot prove locally
87
-
88
- The verifier validates **binding and policy**, not the truth of external scanner or
89
- provider claims. The release system must itself be trusted, scanners must run against
90
- the final artifact, and signature/provenance verification must be performed by the
91
- approved CI/registry trust root. Keep the raw external evidence according to your
92
- security retention policy; do not embed it in the application image.
93
- ## Schema-v1 fail-closed parsing
94
-
95
- The production verifier treats the evidence document as a security protocol, not
96
- as an extensible metadata bag. Unknown root or nested schema-v1 fields are
97
- rejected. The manifest itself is capped at 256 KiB, referenced evidence files
98
- are separately bounded, and every required evidence artifact declares a
99
- non-empty bounded tool name/version. The release `proxyVersion` must equal the
100
- actual runtime source constant.
101
-
102
- Full-image CycloneDX evidence must be version 1.6 or newer. The resolved
103
- platform manifest digest must be distinct from the pinned multi-platform index
104
- digest; copying the index digest into the resolved-manifest field is rejected.
105
- Runtime source binding includes every regular file copied from `_utils/`, not
106
- only Python files, so future runtime policy/data files cannot silently escape
107
- the source subject.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/SECURITY_RELEASE_GATES.md DELETED
@@ -1,128 +0,0 @@
1
- # Supply-chain, deployment, and evidence release gates
2
-
3
- Run 19 / B38 makes the source-controlled deployment path reproducible and
4
- fail-closed. Run 20 / B39 adds short-lived, content-addressed production
5
- evidence binding so stale or unrelated scanner/attestation files cannot be
6
- substituted for the current source and final image. It does **not** claim that a pinned image or lock file is free of
7
- future vulnerabilities. Release evidence must be renewed whenever the image,
8
- lock, deployment platform, or advisory database changes.
9
-
10
- ## Source-controlled gates
11
-
12
- 1. **Immutable base** — `Dockerfile` uses an exact Python tag plus immutable
13
- OCI index digest. CI resolves the requested `linux/amd64` manifest from that
14
- index and records the manifest digest used for the release.
15
- 2. **Hash-locked Python closure** — production installs `requirements.lock`
16
- with both `--require-hashes` and `--only-binary=:all:`. Source builds and
17
- resolver drift are not accepted in the release path.
18
- 3. **Minimal framework extras** — FastAPI and Uvicorn are installed without
19
- their broad optional/`standard` extras. Every transitive runtime package is
20
- named explicitly by the lock.
21
- 4. **Runtime without install tooling** — dependencies are built in an isolated
22
- venv in a builder stage. Runtime `pip`, `setuptools`, and `wheel` payloads
23
- from the base image are removed and the finished venv is copied in.
24
- 5. **Non-root strict profile** — runtime UID/GID is `1000:1000`, matching the
25
- Hugging Face Docker Spaces convention. `DEPLOYMENT_PROFILE=strict` also
26
- verifies non-root execution in application startup.
27
- 6. **Deny-by-default build context** — `.dockerignore` allows only the service
28
- runtime unit into the Docker build context, reducing accidental secret,
29
- cache, repository-history, test-fixture, and unrelated-artifact inclusion.
30
- 7. **Read-only/rootless reference** — the hardened Compose reference drops all
31
- Linux capabilities, enables `no-new-privileges`, uses a read-only root
32
- filesystem, and confines expected temporary writes to `/tmp`.
33
- 8. **Redis transport authority** — strict deployments require `rediss://` for
34
- every Redis-backed control plane. URL query parameters cannot downgrade TLS;
35
- certificate and hostname verification are forced by code.
36
- 9. **Python SBOM** — `python-runtime.cdx.json` describes the exact locked Python
37
- closure. It is not a complete image SBOM because it intentionally excludes
38
- OS/base-image packages.
39
-
40
- ## Networked CI/release evidence — mandatory before production promotion
41
-
42
- Run these in a networked, current advisory environment. Tool names are examples;
43
- organizations may use equivalent scanners, but **do not turn scanner failure
44
- into a warning-only step**.
45
-
46
- ```bash
47
- # Offline structure/ratchet verifier committed with the source.
48
- python security/verify_supply_chain.py
49
-
50
- # Dependency advisory gate. Generate/install in an isolated environment from
51
- # the exact lock first; fail on known vulnerabilities with no approved policy.
52
- pip-audit --strict --require-hashes -r requirements.lock
53
-
54
- # Build for the locked target architecture and identify the immutable result.
55
- docker build --platform linux/amd64 -t scikitplots-ai-proxy:b38 .
56
- docker image inspect scikitplots-ai-proxy:b38 --format '{{.Id}}'
57
-
58
- # Full image SBOM (includes OS packages) and vulnerability gate.
59
- syft scikitplots-ai-proxy:b38 -o cyclonedx-json > image.cdx.json
60
- trivy image --exit-code 1 --severity HIGH,CRITICAL scikitplots-ai-proxy:b38
61
-
62
- # Prefer signed provenance/SBOM attestations in the target registry (for
63
- # example, BuildKit provenance plus organization-approved signing tooling).
64
- ```
65
-
66
- Scanner output is time-sensitive evidence. Never copy an old “0 CVEs” result
67
- forward to a new release. If a base-image CVE has no upstream fix yet, document
68
- its reachability, compensating controls, owner, expiration date, and explicit
69
- risk acceptance rather than silently suppressing it.
70
-
71
- ## Dependency update protocol
72
-
73
- Update direct requirements, regenerate the complete wheel lock for the exact
74
- platform, regenerate the Python SBOM, run `verify_supply_chain.py`, review fresh
75
- advisories, run the complete proxy regression suite, build/scan the container,
76
- and only then update the maintenance checkpoint. A dependency bump is a security
77
- change even when application source is unchanged.
78
-
79
- ## Current B38 advisory ratchets
80
-
81
- The B38 review found that the previous environment's Click 8.1.8 and Starlette
82
- 0.50.0 are below current security fixes. The lock therefore ratchets Click to
83
- 8.3.3 and Starlette to the reviewed 1.6.0 release;
84
- `supply_chain_policy.toml` prevents an accidental rollback below those reviewed
85
- floors. Fresh scanning remains mandatory because
86
- new advisories can appear after this checkpoint.
87
-
88
-
89
- ## B39 machine-verifiable production evidence
90
-
91
- Before collecting external evidence, obtain the canonical non-secret subjects:
92
-
93
- ```bash
94
- python security/release_subjects.py
95
- ```
96
-
97
- The output contains only proxy version, target platform, exact lock/SBOM/runtime
98
- source digests, and the immutable base-image index digest. It contains no URLs,
99
- credentials, user data, deployment hostnames, or Redis identity.
100
-
101
- Store the fresh dependency scan, image scan, full-image CycloneDX SBOM, SLSA
102
- provenance, and signature-verification output beside `release-evidence.json`.
103
- Each referenced file is bound by relative path + SHA-256 + explicit subject.
104
- The production manifest also records the resolved base-image manifest digest,
105
- final OCI image digest, sanitized Redis operational evidence, and infrastructure
106
- logging/telemetry posture.
107
-
108
- The standard hardened B39 policy accepts no risk-exception entry in the promotion
109
- manifest. A release that needs an exception must change/review the policy rather
110
- than smuggling a waiver into evidence. Manifests expire within 72 hours.
111
-
112
- ```bash
113
- # Source-only structural gate.
114
- python security/verify_supply_chain.py
115
-
116
- # One production promotion gate: source policy + bound fresh evidence.
117
- python security/verify_release_gate.py /secure/release/release-evidence.json
118
- ```
119
-
120
- `verify_release_gate.py` fails closed on stale/expired evidence, source or artifact
121
- hash drift, path traversal/symlink substitution, mismatched artifact subjects,
122
- provenance that does not name the final image or resolved base manifest, missing
123
- signature-verification evidence, hidden infrastructure body/credential logging,
124
- unverified Share/Contribution persistence/replication, stale backup/restore
125
- exercises, or third-party telemetry export.
126
-
127
- See `RELEASE_EVIDENCE_GUIDE.md` and `release-evidence.example.json`. The example
128
- is intentionally non-authoritative and cannot pass verification unchanged.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/probe_redis_authority.py DELETED
@@ -1,162 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Collect sanitized Redis operational evidence for one control plane.
4
-
5
- The Redis URL is read only from an environment variable named by the operator;
6
- it is never accepted as a command-line value and never printed. Output contains
7
- no host, port, username, keys, values, replication offsets, or credentials.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import argparse
13
- import json
14
- import logging
15
- import os
16
- import sys
17
- from datetime import datetime, timezone
18
- from pathlib import Path
19
- from typing import Any
20
-
21
- ROOT = Path(__file__).resolve().parents[1]
22
- if str(ROOT) not in sys.path:
23
- sys.path.insert(0, str(ROOT))
24
- from _utils._redis_security import ( # noqa: E402
25
- RedisSecurityError,
26
- redis_connection_kwargs,
27
- )
28
-
29
- logger = logging.getLogger(__name__)
30
-
31
-
32
- def _now() -> str:
33
- return (
34
- datetime.now(timezone.utc).isoformat(timespec="seconds").replace("+00:00", "Z")
35
- )
36
-
37
-
38
- def _int(value: Any, default: int = 0) -> int:
39
- try:
40
- return int(value)
41
- except (TypeError, ValueError):
42
- return default
43
-
44
-
45
- def collect( # ruff: ignore[too-many-branches, undocumented-public-function]
46
- *,
47
- plane: str,
48
- url_env: str,
49
- client: Any | None = None,
50
- ) -> dict[str, Any]:
51
- if plane not in {"rateLimit", "share", "contribution"}:
52
- raise RuntimeError("PLANE_INVALID")
53
- if not url_env or any(
54
- ch not in "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_" for ch in url_env
55
- ):
56
- raise RuntimeError("URL_ENV_NAME_INVALID")
57
- url = os.environ.get(url_env, "")
58
- try:
59
- policy, kwargs = redis_connection_kwargs(
60
- url, require_tls=True, socket_timeout_seconds=3.0
61
- )
62
- except RedisSecurityError as exc:
63
- raise RuntimeError(exc.code) from exc
64
- if client is None:
65
- try:
66
- import redis # type: ignore[] # ruff: ignore[import-outside-top-level]
67
-
68
- client = redis.Redis.from_url(url, **kwargs)
69
- except Exception as exc:
70
- raise RuntimeError("REDIS_CLIENT_UNAVAILABLE") from exc
71
- try:
72
- ping = bool(client.ping())
73
- except Exception as exc:
74
- raise RuntimeError("REDIS_PING_FAILED") from exc
75
- if not ping:
76
- raise RuntimeError("REDIS_PING_FAILED")
77
-
78
- non_default_identity = False
79
- acl_identity_observed = False
80
- try:
81
- who = client.execute_command("ACL", "WHOAMI")
82
- if isinstance(who, bytes):
83
- who = who.decode("utf-8", "replace")
84
- acl_identity_observed = bool(str(who or "").strip())
85
- non_default_identity = (
86
- acl_identity_observed and str(who).strip().lower() != "default"
87
- )
88
- except Exception: # ruff: ignore[blind-except]
89
- # Managed providers may deny ACL inspection. Do not broaden permissions
90
- # just for this probe; leave the fact unproven for operator evidence.
91
- pass
92
-
93
- try:
94
- persistence = client.info("persistence") or {}
95
- replication = client.info("replication") or {}
96
- except Exception as exc:
97
- raise RuntimeError("REDIS_INFO_UNAVAILABLE") from exc
98
- aof_enabled = _int(persistence.get("aof_enabled")) == 1
99
- loading = _int(persistence.get("loading")) == 1
100
- role = str(replication.get("role") or "").lower()
101
- if role == "master":
102
- replication_observed = _int(replication.get("connected_slaves")) >= 1
103
- elif role in {"slave", "replica"}:
104
- replication_observed = (
105
- str(replication.get("master_link_status") or "").lower() == "up"
106
- )
107
- else:
108
- replication_observed = False
109
-
110
- return {
111
- "schemaVersion": 1,
112
- "plane": plane,
113
- "observedAt": _now(),
114
- "transport": policy.manifest(),
115
- "reachable": True,
116
- "aclIdentityObserved": acl_identity_observed,
117
- "nonDefaultIdentity": non_default_identity,
118
- "aofPersistenceObserved": aof_enabled,
119
- "loading": loading,
120
- "replicationObserved": replication_observed,
121
- "note": (
122
- "Observations only; least-privilege ACL review, provider durability, backups and restore tests require separate operator evidence."
123
- ),
124
- }
125
-
126
-
127
- def main(argv: list[str] | None = None) -> int:
128
- """Run."""
129
- parser = argparse.ArgumentParser(description=__doc__)
130
- parser.add_argument(
131
- "--plane", required=True, choices=("rateLimit", "share", "contribution")
132
- )
133
- parser.add_argument(
134
- "--url-env",
135
- required=True,
136
- help="Environment-variable name containing the Redis URL",
137
- )
138
- parser.add_argument("--output", type=Path)
139
- args = parser.parse_args(argv)
140
- try:
141
- result = collect(plane=args.plane, url_env=args.url_env)
142
- except RuntimeError as exc:
143
- logger.warning(
144
- json.dumps(
145
- {
146
- "ok": False,
147
- "code": str(exc),
148
- },
149
- sort_keys=True,
150
- ),
151
- )
152
- return 2
153
- payload = json.dumps(result, indent=2, sort_keys=True) + "\n"
154
- if args.output:
155
- args.output.write_text(payload)
156
- else:
157
- sys.stdout.write(payload)
158
- return 0
159
-
160
-
161
- if __name__ == "__main__":
162
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/python-runtime.cdx.json DELETED
@@ -1,721 +0,0 @@
1
- {
2
- "bomFormat": "CycloneDX",
3
- "components": [
4
- {
5
- "hashes": [
6
- {
7
- "alg": "SHA-256",
8
- "content": "571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320"
9
- }
10
- ],
11
- "name": "annotated-doc",
12
- "properties": [
13
- {
14
- "name": "scikitplot:lock-target",
15
- "value": "CPython 3.11 / linux/amd64"
16
- },
17
- {
18
- "name": "scikitplot:artifact-policy",
19
- "value": "binary-wheel-only"
20
- }
21
- ],
22
- "purl": "pkg:pypi/annotated-doc@0.0.4",
23
- "scope": "required",
24
- "type": "library",
25
- "version": "0.0.4"
26
- },
27
- {
28
- "hashes": [
29
- {
30
- "alg": "SHA-256",
31
- "content": "1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53"
32
- }
33
- ],
34
- "name": "annotated-types",
35
- "properties": [
36
- {
37
- "name": "scikitplot:lock-target",
38
- "value": "CPython 3.11 / linux/amd64"
39
- },
40
- {
41
- "name": "scikitplot:artifact-policy",
42
- "value": "binary-wheel-only"
43
- }
44
- ],
45
- "purl": "pkg:pypi/annotated-types@0.7.0",
46
- "scope": "required",
47
- "type": "library",
48
- "version": "0.7.0"
49
- },
50
- {
51
- "hashes": [
52
- {
53
- "alg": "SHA-256",
54
- "content": "08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708"
55
- }
56
- ],
57
- "name": "anyio",
58
- "properties": [
59
- {
60
- "name": "scikitplot:lock-target",
61
- "value": "CPython 3.11 / linux/amd64"
62
- },
63
- {
64
- "name": "scikitplot:artifact-policy",
65
- "value": "binary-wheel-only"
66
- }
67
- ],
68
- "purl": "pkg:pypi/anyio@4.13.0",
69
- "scope": "required",
70
- "type": "library",
71
- "version": "4.13.0"
72
- },
73
- {
74
- "hashes": [
75
- {
76
- "alg": "SHA-256",
77
- "content": "3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897"
78
- }
79
- ],
80
- "name": "certifi",
81
- "properties": [
82
- {
83
- "name": "scikitplot:lock-target",
84
- "value": "CPython 3.11 / linux/amd64"
85
- },
86
- {
87
- "name": "scikitplot:artifact-policy",
88
- "value": "binary-wheel-only"
89
- }
90
- ],
91
- "purl": "pkg:pypi/certifi@2026.5.20",
92
- "scope": "required",
93
- "type": "library",
94
- "version": "2026.5.20"
95
- },
96
- {
97
- "hashes": [
98
- {
99
- "alg": "SHA-256",
100
- "content": "a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613"
101
- }
102
- ],
103
- "name": "click",
104
- "properties": [
105
- {
106
- "name": "scikitplot:lock-target",
107
- "value": "CPython 3.11 / linux/amd64"
108
- },
109
- {
110
- "name": "scikitplot:artifact-policy",
111
- "value": "binary-wheel-only"
112
- }
113
- ],
114
- "purl": "pkg:pypi/click@8.3.3",
115
- "scope": "required",
116
- "type": "library",
117
- "version": "8.3.3"
118
- },
119
- {
120
- "hashes": [
121
- {
122
- "alg": "SHA-256",
123
- "content": "bfb91aa2d334c61cb35ba9a116fc123b3d3df31640b801cf57a7a78ec3f603b3"
124
- }
125
- ],
126
- "name": "fastapi",
127
- "properties": [
128
- {
129
- "name": "scikitplot:lock-target",
130
- "value": "CPython 3.11 / linux/amd64"
131
- },
132
- {
133
- "name": "scikitplot:artifact-policy",
134
- "value": "binary-wheel-only"
135
- }
136
- ],
137
- "purl": "pkg:pypi/fastapi@0.141.1",
138
- "scope": "required",
139
- "type": "library",
140
- "version": "0.141.1"
141
- },
142
- {
143
- "hashes": [
144
- {
145
- "alg": "SHA-256",
146
- "content": "96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258"
147
- }
148
- ],
149
- "name": "filelock",
150
- "properties": [
151
- {
152
- "name": "scikitplot:lock-target",
153
- "value": "CPython 3.11 / linux/amd64"
154
- },
155
- {
156
- "name": "scikitplot:artifact-policy",
157
- "value": "binary-wheel-only"
158
- }
159
- ],
160
- "purl": "pkg:pypi/filelock@3.29.0",
161
- "scope": "required",
162
- "type": "library",
163
- "version": "3.29.0"
164
- },
165
- {
166
- "hashes": [
167
- {
168
- "alg": "SHA-256",
169
- "content": "11ef7bb35dab8a394fde6e608221d5cf3e8499401c249bebaeaad760a1a8dec2"
170
- }
171
- ],
172
- "name": "fsspec",
173
- "properties": [
174
- {
175
- "name": "scikitplot:lock-target",
176
- "value": "CPython 3.11 / linux/amd64"
177
- },
178
- {
179
- "name": "scikitplot:artifact-policy",
180
- "value": "binary-wheel-only"
181
- }
182
- ],
183
- "purl": "pkg:pypi/fsspec@2026.4.0",
184
- "scope": "required",
185
- "type": "library",
186
- "version": "2026.4.0"
187
- },
188
- {
189
- "hashes": [
190
- {
191
- "alg": "SHA-256",
192
- "content": "63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"
193
- }
194
- ],
195
- "name": "h11",
196
- "properties": [
197
- {
198
- "name": "scikitplot:lock-target",
199
- "value": "CPython 3.11 / linux/amd64"
200
- },
201
- {
202
- "name": "scikitplot:artifact-policy",
203
- "value": "binary-wheel-only"
204
- }
205
- ],
206
- "purl": "pkg:pypi/h11@0.16.0",
207
- "scope": "required",
208
- "type": "library",
209
- "version": "0.16.0"
210
- },
211
- {
212
- "hashes": [
213
- {
214
- "alg": "SHA-256",
215
- "content": "3531b1823a0e6d77d80f9ed15ca0e00f0d115094f8ac033d5cae88f4564cc949"
216
- }
217
- ],
218
- "name": "hf-xet",
219
- "properties": [
220
- {
221
- "name": "scikitplot:lock-target",
222
- "value": "CPython 3.11 / linux/amd64"
223
- },
224
- {
225
- "name": "scikitplot:artifact-policy",
226
- "value": "binary-wheel-only"
227
- }
228
- ],
229
- "purl": "pkg:pypi/hf-xet@1.5.0",
230
- "scope": "required",
231
- "type": "library",
232
- "version": "1.5.0"
233
- },
234
- {
235
- "hashes": [
236
- {
237
- "alg": "SHA-256",
238
- "content": "2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55"
239
- }
240
- ],
241
- "name": "httpcore",
242
- "properties": [
243
- {
244
- "name": "scikitplot:lock-target",
245
- "value": "CPython 3.11 / linux/amd64"
246
- },
247
- {
248
- "name": "scikitplot:artifact-policy",
249
- "value": "binary-wheel-only"
250
- }
251
- ],
252
- "purl": "pkg:pypi/httpcore@1.0.9",
253
- "scope": "required",
254
- "type": "library",
255
- "version": "1.0.9"
256
- },
257
- {
258
- "hashes": [
259
- {
260
- "alg": "SHA-256",
261
- "content": "d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad"
262
- }
263
- ],
264
- "name": "httpx",
265
- "properties": [
266
- {
267
- "name": "scikitplot:lock-target",
268
- "value": "CPython 3.11 / linux/amd64"
269
- },
270
- {
271
- "name": "scikitplot:artifact-policy",
272
- "value": "binary-wheel-only"
273
- }
274
- ],
275
- "purl": "pkg:pypi/httpx@0.28.1",
276
- "scope": "required",
277
- "type": "library",
278
- "version": "0.28.1"
279
- },
280
- {
281
- "hashes": [
282
- {
283
- "alg": "SHA-256",
284
- "content": "64340de934b9ce37857ef85a82de72f5629e8a270f9119eabb12bf495eb53c22"
285
- }
286
- ],
287
- "name": "huggingface-hub",
288
- "properties": [
289
- {
290
- "name": "scikitplot:lock-target",
291
- "value": "CPython 3.11 / linux/amd64"
292
- },
293
- {
294
- "name": "scikitplot:artifact-policy",
295
- "value": "binary-wheel-only"
296
- }
297
- ],
298
- "purl": "pkg:pypi/huggingface-hub@1.16.1",
299
- "scope": "required",
300
- "type": "library",
301
- "version": "1.16.1"
302
- },
303
- {
304
- "hashes": [
305
- {
306
- "alg": "SHA-256",
307
- "content": "466e48829084efe2548012b855df21540b96f2e20e51bd124c851536556a592c"
308
- }
309
- ],
310
- "name": "idna",
311
- "properties": [
312
- {
313
- "name": "scikitplot:lock-target",
314
- "value": "CPython 3.11 / linux/amd64"
315
- },
316
- {
317
- "name": "scikitplot:artifact-policy",
318
- "value": "binary-wheel-only"
319
- }
320
- ],
321
- "purl": "pkg:pypi/idna@3.17",
322
- "scope": "required",
323
- "type": "library",
324
- "version": "3.17"
325
- },
326
- {
327
- "hashes": [
328
- {
329
- "alg": "SHA-256",
330
- "content": "9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a"
331
- }
332
- ],
333
- "name": "markdown-it-py",
334
- "properties": [
335
- {
336
- "name": "scikitplot:lock-target",
337
- "value": "CPython 3.11 / linux/amd64"
338
- },
339
- {
340
- "name": "scikitplot:artifact-policy",
341
- "value": "binary-wheel-only"
342
- }
343
- ],
344
- "purl": "pkg:pypi/markdown-it-py@4.2.0",
345
- "scope": "required",
346
- "type": "library",
347
- "version": "4.2.0"
348
- },
349
- {
350
- "hashes": [
351
- {
352
- "alg": "SHA-256",
353
- "content": "84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"
354
- }
355
- ],
356
- "name": "mdurl",
357
- "properties": [
358
- {
359
- "name": "scikitplot:lock-target",
360
- "value": "CPython 3.11 / linux/amd64"
361
- },
362
- {
363
- "name": "scikitplot:artifact-policy",
364
- "value": "binary-wheel-only"
365
- }
366
- ],
367
- "purl": "pkg:pypi/mdurl@0.1.2",
368
- "scope": "required",
369
- "type": "library",
370
- "version": "0.1.2"
371
- },
372
- {
373
- "hashes": [
374
- {
375
- "alg": "SHA-256",
376
- "content": "29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"
377
- }
378
- ],
379
- "name": "packaging",
380
- "properties": [
381
- {
382
- "name": "scikitplot:lock-target",
383
- "value": "CPython 3.11 / linux/amd64"
384
- },
385
- {
386
- "name": "scikitplot:artifact-policy",
387
- "value": "binary-wheel-only"
388
- }
389
- ],
390
- "purl": "pkg:pypi/packaging@25.0",
391
- "scope": "required",
392
- "type": "library",
393
- "version": "25.0"
394
- },
395
- {
396
- "hashes": [
397
- {
398
- "alg": "SHA-256",
399
- "content": "45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba"
400
- }
401
- ],
402
- "name": "pydantic",
403
- "properties": [
404
- {
405
- "name": "scikitplot:lock-target",
406
- "value": "CPython 3.11 / linux/amd64"
407
- },
408
- {
409
- "name": "scikitplot:artifact-policy",
410
- "value": "binary-wheel-only"
411
- }
412
- ],
413
- "purl": "pkg:pypi/pydantic@2.13.4",
414
- "scope": "required",
415
- "type": "library",
416
- "version": "2.13.4"
417
- },
418
- {
419
- "hashes": [
420
- {
421
- "alg": "SHA-256",
422
- "content": "f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4"
423
- }
424
- ],
425
- "name": "pydantic-core",
426
- "properties": [
427
- {
428
- "name": "scikitplot:lock-target",
429
- "value": "CPython 3.11 / linux/amd64"
430
- },
431
- {
432
- "name": "scikitplot:artifact-policy",
433
- "value": "binary-wheel-only"
434
- }
435
- ],
436
- "purl": "pkg:pypi/pydantic-core@2.46.4",
437
- "scope": "required",
438
- "type": "library",
439
- "version": "2.46.4"
440
- },
441
- {
442
- "hashes": [
443
- {
444
- "alg": "SHA-256",
445
- "content": "81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176"
446
- }
447
- ],
448
- "name": "pygments",
449
- "properties": [
450
- {
451
- "name": "scikitplot:lock-target",
452
- "value": "CPython 3.11 / linux/amd64"
453
- },
454
- {
455
- "name": "scikitplot:artifact-policy",
456
- "value": "binary-wheel-only"
457
- }
458
- ],
459
- "purl": "pkg:pypi/pygments@2.20.0",
460
- "scope": "required",
461
- "type": "library",
462
- "version": "2.20.0"
463
- },
464
- {
465
- "hashes": [
466
- {
467
- "alg": "SHA-256",
468
- "content": "b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"
469
- }
470
- ],
471
- "name": "pyyaml",
472
- "properties": [
473
- {
474
- "name": "scikitplot:lock-target",
475
- "value": "CPython 3.11 / linux/amd64"
476
- },
477
- {
478
- "name": "scikitplot:artifact-policy",
479
- "value": "binary-wheel-only"
480
- }
481
- ],
482
- "purl": "pkg:pypi/pyyaml@6.0.3",
483
- "scope": "required",
484
- "type": "library",
485
- "version": "6.0.3"
486
- },
487
- {
488
- "hashes": [
489
- {
490
- "alg": "SHA-256",
491
- "content": "a4fe1aac3d3b3cc791d4b3d5931c5a956045dc951ee74d1c913ee3ac4d2ee9fb"
492
- }
493
- ],
494
- "name": "redis",
495
- "properties": [
496
- {
497
- "name": "scikitplot:lock-target",
498
- "value": "CPython 3.11 / linux/amd64"
499
- },
500
- {
501
- "name": "scikitplot:artifact-policy",
502
- "value": "binary-wheel-only"
503
- }
504
- ],
505
- "purl": "pkg:pypi/redis@8.1.0",
506
- "scope": "required",
507
- "type": "library",
508
- "version": "8.1.0"
509
- },
510
- {
511
- "hashes": [
512
- {
513
- "alg": "SHA-256",
514
- "content": "33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb"
515
- }
516
- ],
517
- "name": "rich",
518
- "properties": [
519
- {
520
- "name": "scikitplot:lock-target",
521
- "value": "CPython 3.11 / linux/amd64"
522
- },
523
- {
524
- "name": "scikitplot:artifact-policy",
525
- "value": "binary-wheel-only"
526
- }
527
- ],
528
- "purl": "pkg:pypi/rich@15.0.0",
529
- "scope": "required",
530
- "type": "library",
531
- "version": "15.0.0"
532
- },
533
- {
534
- "hashes": [
535
- {
536
- "alg": "SHA-256",
537
- "content": "7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686"
538
- }
539
- ],
540
- "name": "shellingham",
541
- "properties": [
542
- {
543
- "name": "scikitplot:lock-target",
544
- "value": "CPython 3.11 / linux/amd64"
545
- },
546
- {
547
- "name": "scikitplot:artifact-policy",
548
- "value": "binary-wheel-only"
549
- }
550
- ],
551
- "purl": "pkg:pypi/shellingham@1.5.4",
552
- "scope": "required",
553
- "type": "library",
554
- "version": "1.5.4"
555
- },
556
- {
557
- "hashes": [
558
- {
559
- "alg": "SHA-256",
560
- "content": "a86dd39d14bb45f85a3d18525215a9ef0cfd1f192ac793220e72598c90335f0c"
561
- }
562
- ],
563
- "name": "starlette",
564
- "properties": [
565
- {
566
- "name": "scikitplot:lock-target",
567
- "value": "CPython 3.11 / linux/amd64"
568
- },
569
- {
570
- "name": "scikitplot:artifact-policy",
571
- "value": "binary-wheel-only"
572
- }
573
- ],
574
- "purl": "pkg:pypi/starlette@1.6.0",
575
- "scope": "required",
576
- "type": "library",
577
- "version": "1.6.0"
578
- },
579
- {
580
- "hashes": [
581
- {
582
- "alg": "SHA-256",
583
- "content": "ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf"
584
- }
585
- ],
586
- "name": "tqdm",
587
- "properties": [
588
- {
589
- "name": "scikitplot:lock-target",
590
- "value": "CPython 3.11 / linux/amd64"
591
- },
592
- {
593
- "name": "scikitplot:artifact-policy",
594
- "value": "binary-wheel-only"
595
- }
596
- ],
597
- "purl": "pkg:pypi/tqdm@4.67.3",
598
- "scope": "required",
599
- "type": "library",
600
- "version": "4.67.3"
601
- },
602
- {
603
- "hashes": [
604
- {
605
- "alg": "SHA-256",
606
- "content": "e70549ec5a403ca8a0bf0802ddd9f3c6ff7a14ccbb859b01b697baa943636f33"
607
- }
608
- ],
609
- "name": "typer",
610
- "properties": [
611
- {
612
- "name": "scikitplot:lock-target",
613
- "value": "CPython 3.11 / linux/amd64"
614
- },
615
- {
616
- "name": "scikitplot:artifact-policy",
617
- "value": "binary-wheel-only"
618
- }
619
- ],
620
- "purl": "pkg:pypi/typer@0.26.3",
621
- "scope": "required",
622
- "type": "library",
623
- "version": "0.26.3"
624
- },
625
- {
626
- "hashes": [
627
- {
628
- "alg": "SHA-256",
629
- "content": "481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8"
630
- }
631
- ],
632
- "name": "typing-extensions",
633
- "properties": [
634
- {
635
- "name": "scikitplot:lock-target",
636
- "value": "CPython 3.11 / linux/amd64"
637
- },
638
- {
639
- "name": "scikitplot:artifact-policy",
640
- "value": "binary-wheel-only"
641
- }
642
- ],
643
- "purl": "pkg:pypi/typing-extensions@4.16.0",
644
- "scope": "required",
645
- "type": "library",
646
- "version": "4.16.0"
647
- },
648
- {
649
- "hashes": [
650
- {
651
- "alg": "SHA-256",
652
- "content": "4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7"
653
- }
654
- ],
655
- "name": "typing-inspection",
656
- "properties": [
657
- {
658
- "name": "scikitplot:lock-target",
659
- "value": "CPython 3.11 / linux/amd64"
660
- },
661
- {
662
- "name": "scikitplot:artifact-policy",
663
- "value": "binary-wheel-only"
664
- }
665
- ],
666
- "purl": "pkg:pypi/typing-inspection@0.4.2",
667
- "scope": "required",
668
- "type": "library",
669
- "version": "0.4.2"
670
- },
671
- {
672
- "hashes": [
673
- {
674
- "alg": "SHA-256",
675
- "content": "48097851328b87ec36117d3d575234519eb58c2b22d79666e9bbc6c49a761dad"
676
- }
677
- ],
678
- "name": "uvicorn",
679
- "properties": [
680
- {
681
- "name": "scikitplot:lock-target",
682
- "value": "CPython 3.11 / linux/amd64"
683
- },
684
- {
685
- "name": "scikitplot:artifact-policy",
686
- "value": "binary-wheel-only"
687
- }
688
- ],
689
- "purl": "pkg:pypi/uvicorn@0.48.0",
690
- "scope": "required",
691
- "type": "library",
692
- "version": "0.48.0"
693
- }
694
- ],
695
- "metadata": {
696
- "component": {
697
- "bom-ref": "pkg:generic/scikit-plots-ai-proxy@6.8.0",
698
- "name": "scikit-plots-ai-proxy",
699
- "type": "application",
700
- "version": "6.8.0"
701
- },
702
- "properties": [
703
- {
704
- "name": "scikitplot:scope",
705
- "value": "python-runtime-lock-only"
706
- },
707
- {
708
- "name": "scikitplot:target",
709
- "value": "CPython 3.11 / linux/amd64"
710
- },
711
- {
712
- "name": "scikitplot:note",
713
- "value": "A full image SBOM must be generated from the built container before release."
714
- }
715
- ],
716
- "timestamp": "2026-08-30T00:00:00Z"
717
- },
718
- "serialNumber": "urn:uuid:d714f9a3-0034-5843-b870-51885c937bc8",
719
- "specVersion": "1.6",
720
- "version": 1
721
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/release-evidence.example.json DELETED
@@ -1,112 +0,0 @@
1
- {
2
- "artifacts": {
3
- "dependencyScan": {
4
- "path": "pip-audit.json",
5
- "sha256": "REPLACE_WITH_64_HEX",
6
- "status": "pass",
7
- "subject": "sha256:REPLACE_WITH_64_HEX",
8
- "tool": {
9
- "name": "pip-audit",
10
- "version": "REPLACE"
11
- }
12
- },
13
- "imageSbom": {
14
- "path": "image.cdx.json",
15
- "sha256": "REPLACE_WITH_64_HEX",
16
- "status": "pass",
17
- "subject": "sha256:REPLACE_WITH_64_HEX",
18
- "tool": {
19
- "name": "syft",
20
- "version": "REPLACE"
21
- }
22
- },
23
- "imageScan": {
24
- "path": "image-scan.json",
25
- "sha256": "REPLACE_WITH_64_HEX",
26
- "status": "pass",
27
- "subject": "sha256:REPLACE_WITH_64_HEX",
28
- "tool": {
29
- "name": "REPLACE",
30
- "version": "REPLACE"
31
- }
32
- },
33
- "provenance": {
34
- "path": "provenance.json",
35
- "predicateType": "https://slsa.dev/provenance/v1",
36
- "sha256": "REPLACE_WITH_64_HEX",
37
- "signatureVerified": true,
38
- "status": "pass",
39
- "subject": "sha256:REPLACE_WITH_64_HEX",
40
- "tool": {
41
- "name": "REPLACE",
42
- "version": "REPLACE"
43
- }
44
- },
45
- "signatureVerification": {
46
- "path": "signature-verification.json",
47
- "sha256": "REPLACE_WITH_64_HEX",
48
- "status": "pass",
49
- "subject": "sha256:REPLACE_WITH_64_HEX",
50
- "tool": {
51
- "name": "REPLACE",
52
- "version": "REPLACE"
53
- }
54
- }
55
- },
56
- "image": {
57
- "digest": "sha256:REPLACE_WITH_64_HEX",
58
- "platform": "linux/amd64"
59
- },
60
- "logging": {
61
- "apmBodyCapture": false,
62
- "authorizationHeaderLogging": false,
63
- "capabilityHeaderLogging": false,
64
- "queryStringLogging": false,
65
- "requestBodyLogging": false,
66
- "reviewedAt": "2099-01-01T00:00:00Z",
67
- "thirdPartyTelemetryExport": false,
68
- "wafBodyCapture": false
69
- },
70
- "redis": {
71
- "contribution": {
72
- "backupRestoreTestedAt": "2098-12-15T00:00:00Z",
73
- "leastPrivilegeReviewed": true,
74
- "nonDefaultIdentity": true,
75
- "persistenceVerified": true,
76
- "replicationVerified": true,
77
- "tlsVerified": true
78
- },
79
- "rateLimit": {
80
- "backupRestoreTestedAt": null,
81
- "leastPrivilegeReviewed": true,
82
- "nonDefaultIdentity": true,
83
- "persistenceVerified": false,
84
- "replicationVerified": false,
85
- "tlsVerified": true
86
- },
87
- "share": {
88
- "backupRestoreTestedAt": "2098-12-15T00:00:00Z",
89
- "leastPrivilegeReviewed": true,
90
- "nonDefaultIdentity": true,
91
- "persistenceVerified": true,
92
- "replicationVerified": true,
93
- "tlsVerified": true
94
- }
95
- },
96
- "release": {
97
- "expiresAt": "2099-01-02T00:00:00Z",
98
- "generatedAt": "2099-01-01T00:00:00Z",
99
- "proxyVersion": "7.0.0",
100
- "releaseId": "example-only-not-production",
101
- "targetPlatform": "linux/amd64"
102
- },
103
- "riskExceptions": [],
104
- "schemaVersion": 1,
105
- "source": {
106
- "baseImageIndexDigest": "sha256:REPLACE_WITH_64_HEX",
107
- "baseImageManifestDigest": "sha256:REPLACE_WITH_64_HEX",
108
- "pythonSbomSha256": "REPLACE_WITH_64_HEX",
109
- "requirementsLockSha256": "REPLACE_WITH_64_HEX",
110
- "runtimeSourceSha256": "REPLACE_WITH_64_HEX"
111
- }
112
- }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/release_evidence_policy.toml DELETED
@@ -1,34 +0,0 @@
1
- schema_version = 1
2
- evidence_schema_version = 1
3
- target_platform = "linux/amd64"
4
- max_age_hours = 72
5
- max_clock_skew_minutes = 10
6
- max_manifest_bytes = 262144
7
- max_evidence_file_bytes = 52428800
8
- minimum_cyclonedx_spec_version = "1.6"
9
- require_dependency_scan = true
10
- require_full_image_sbom = true
11
- require_image_vulnerability_scan = true
12
- require_slsa_provenance = true
13
- require_signature_verification = true
14
- require_log_privacy_attestation = true
15
- forbid_unexpired_risk_exceptions = true
16
-
17
- [redis]
18
- require_tls = true
19
- require_non_default_identity = true
20
- require_least_privilege_review = true
21
- share_require_persistence = true
22
- share_require_replication = true
23
- contribution_require_persistence = true
24
- contribution_require_replication = true
25
- backup_restore_max_age_days = 90
26
-
27
- [logging]
28
- request_body_logging = false
29
- authorization_header_logging = false
30
- capability_header_logging = false
31
- query_string_logging = false
32
- waf_body_capture = false
33
- apm_body_capture = false
34
- third_party_telemetry_export = false
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/release_subjects.py DELETED
@@ -1,43 +0,0 @@
1
- #!/usr/bin/env python3
2
- """Print canonical non-secret subjects that production release evidence must bind."""
3
-
4
- from __future__ import annotations
5
-
6
- import hashlib
7
- import json
8
- import logging
9
- import sys
10
- from pathlib import Path
11
-
12
- HERE = Path(__file__).resolve().parent
13
- ROOT = HERE.parent
14
- if str(HERE) not in sys.path:
15
- sys.path.insert(0, str(HERE))
16
- if str(ROOT) not in sys.path:
17
- sys.path.insert(0, str(ROOT))
18
-
19
- import verify_release_evidence # noqa: E402
20
- from _utils._shared_logic import PROXY_VERSION # noqa: E402
21
-
22
- logger = logging.getLogger(__name__)
23
-
24
-
25
- def subjects() -> dict[str, object]: # ruff: ignore[undocumented-public-function]
26
- supply = verify_release_evidence.SUPPLY
27
- return {
28
- "schema_version": 1,
29
- "proxy_version": PROXY_VERSION,
30
- "target_platform": verify_release_evidence.POLICY["target_platform"],
31
- "requirements_lock_sha256": (
32
- hashlib.sha256((ROOT / supply["lock_file"]).read_bytes()).hexdigest()
33
- ),
34
- "python_sbom_sha256": (
35
- hashlib.sha256((ROOT / supply["sbom_file"]).read_bytes()).hexdigest()
36
- ),
37
- "runtime_source_sha256": verify_release_evidence._runtime_source_sha256(),
38
- "base_image_index_digest": supply["base_image"]["index_digest"],
39
- }
40
-
41
-
42
- if __name__ == "__main__":
43
- sys.stdout.write(json.dumps(subjects(), sort_keys=True) + "\n")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/supply_chain_policy.toml DELETED
@@ -1,38 +0,0 @@
1
- schema_version = 1
2
- reviewed_at = "2026-08-30"
3
- target_python = "3.11.16"
4
- target_platform = "linux/amd64"
5
- lock_file = "requirements.lock"
6
- sbom_file = "security/python-runtime.cdx.json"
7
- require_hashes = true
8
- binary_only = true
9
- require_non_root = true
10
- require_read_only_reference = true
11
- require_redis_tls_in_strict = true
12
-
13
- [base_image]
14
- repository = "python"
15
- tag = "3.11.16-slim-bookworm"
16
- index_digest = "sha256:0bee7276f83efd4a1ee05bbbf4281d95ed28e079220a9457f25a93e3f1e3c31b"
17
- # This is an immutable multi-platform index. CI must resolve linux/amd64 from
18
- # this exact index and record the resulting manifest digest in release evidence.
19
-
20
- [advisory_floors]
21
- # Current explicit floors discovered during the B38 review. These are
22
- # ratchets, not a substitute for a fresh advisory database scan.
23
- click = "8.3.3"
24
- starlette = "1.6.0"
25
-
26
- [release_gates]
27
- python_advisory_scan = true
28
- container_vulnerability_scan = true
29
- container_sbom = true
30
- provenance_attestation = true
31
-
32
- [release_evidence]
33
- policy_file = "security/release_evidence_policy.toml"
34
- example_file = "security/release-evidence.example.json"
35
- verifier_file = "security/verify_release_evidence.py"
36
- combined_gate_file = "security/verify_release_gate.py"
37
- redis_probe_file = "security/probe_redis_authority.py"
38
- subject_printer_file = "security/release_subjects.py"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/verify_release_evidence.py DELETED
@@ -1,603 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Verify production release evidence without externalizing secrets.
4
-
5
- This verifier binds time-sensitive CI/deployment evidence to the exact B39
6
- source lock, Python SBOM, immutable base-image policy, target platform and final
7
- OCI image digest. It deliberately does not contact registries, scanners,
8
- Redis, or telemetry services; evidence collection is a separate explicit CI or
9
- operator action.
10
- """
11
-
12
- from __future__ import annotations
13
-
14
- import argparse
15
- import hashlib
16
- import json
17
- import logging
18
- import re
19
- import sys
20
- from datetime import datetime, timedelta, timezone
21
- from pathlib import Path
22
- from typing import Any
23
-
24
- import tomllib
25
-
26
- logger = logging.getLogger(__name__)
27
-
28
- HERE = Path(__file__).resolve().parent
29
- ROOT = HERE.parent
30
- POLICY = tomllib.loads((HERE / "release_evidence_policy.toml").read_text())
31
- SUPPLY = tomllib.loads((HERE / "supply_chain_policy.toml").read_text())
32
- HEX64 = re.compile(r"^[0-9a-f]{64}$")
33
- DIGEST = re.compile(r"^sha256:([0-9a-f]{64})$")
34
- SLSA_PREDICATE = "https://slsa.dev/provenance/v1"
35
- RELEASE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{2,127}$")
36
- RUNTIME_SOURCE_FILES = (
37
- "Dockerfile",
38
- ".dockerignore",
39
- "requirements.lock",
40
- "app.py",
41
- "deduplicate_dataset.py",
42
- )
43
- PROXY_VERSION_RE = re.compile(
44
- r'^PROXY_VERSION: str = "([0-9]+\.[0-9]+\.[0-9]+)"$', re.MULTILINE
45
- )
46
- CYCLONEDX_VERSION_RE = re.compile(r"^[0-9]+\.[0-9]+$")
47
- FORBIDDEN_SECRET_KEYS = {
48
- "authorization",
49
- "cookie",
50
- "password",
51
- "passwd",
52
- "secret",
53
- "token",
54
- "apikey",
55
- "api_key",
56
- "privatekey",
57
- "private_key",
58
- "redisurl",
59
- "redis_url",
60
- "connectionstring",
61
- "connection_string",
62
- "hostname",
63
- "username",
64
- "host",
65
- }
66
- SECRET_VALUE_PATTERNS = (
67
- re.compile(r"-----BEGIN [A-Z ]*PRIVATE KEY-----"),
68
- re.compile(r"(?i)\bbearer\s+[A-Za-z0-9._~+/-]{12,}"),
69
- re.compile(r"(?i)\b(?:redis|rediss|https?)://[^\s/@:]+:[^\s/@]+@"),
70
- )
71
-
72
-
73
- class EvidenceError(RuntimeError): # ruff: ignore[undocumented-public-class]
74
- pass
75
-
76
-
77
- def _fail(code: str) -> None:
78
- raise EvidenceError(code)
79
-
80
-
81
- def _runtime_source_sha256(root: Path = ROOT) -> str:
82
- """Hash exact application/container inputs with stable names and lengths."""
83
- paths = [root / name for name in RUNTIME_SOURCE_FILES]
84
- utils_root = root / "_utils"
85
- if not utils_root.is_dir() or utils_root.is_symlink():
86
- _fail("RUNTIME_SOURCE_UTILS_INVALID")
87
- paths.extend(
88
- sorted(
89
- path
90
- for path in utils_root.rglob("*")
91
- if path.is_file()
92
- and "__pycache__" not in path.parts
93
- and path.suffix not in {".pyc", ".pyo"}
94
- )
95
- )
96
- h = hashlib.sha256()
97
- for path in paths:
98
- if not path.is_file() or path.is_symlink():
99
- _fail("RUNTIME_SOURCE_FILE_INVALID")
100
- rel = path.relative_to(root).as_posix().encode("utf-8")
101
- data = path.read_bytes()
102
- h.update(len(rel).to_bytes(4, "big"))
103
- h.update(rel)
104
- h.update(len(data).to_bytes(8, "big"))
105
- h.update(data)
106
- return h.hexdigest()
107
-
108
-
109
- def _sha256(path: Path) -> str:
110
- h = hashlib.sha256()
111
- with path.open("rb") as handle:
112
- for chunk in iter(lambda: handle.read(1024 * 1024), b""):
113
- h.update(chunk)
114
- return h.hexdigest()
115
-
116
-
117
- def _parse_time(value: Any, code: str) -> datetime:
118
- if not isinstance(value, str) or not value.endswith("Z"):
119
- _fail(code)
120
- try:
121
- dt = datetime.fromisoformat(value[:-1] + "+00:00")
122
- except ValueError:
123
- _fail(code)
124
- if dt.tzinfo is None:
125
- _fail(code)
126
- return dt.astimezone(timezone.utc)
127
-
128
-
129
- def _hex(value: Any, code: str) -> str:
130
- if not isinstance(value, str) or HEX64.fullmatch(value) is None:
131
- _fail(code)
132
- return value
133
-
134
-
135
- def _digest(value: Any, code: str) -> str:
136
- if not isinstance(value, str) or DIGEST.fullmatch(value) is None:
137
- _fail(code)
138
- return value
139
-
140
-
141
- def _mapping(value: Any, code: str) -> dict[str, Any]:
142
- if not isinstance(value, dict):
143
- _fail(code)
144
- return value
145
-
146
-
147
- def _exact_keys(value: dict[str, Any], expected: set[str], code: str) -> None:
148
- if set(value) != expected:
149
- _fail(code)
150
-
151
-
152
- def _tool(value: Any, code: str) -> None:
153
- tool = _mapping(value, code)
154
- _exact_keys(tool, {"name", "version"}, code)
155
- for field in ("name", "version"):
156
- item = tool.get(field)
157
- if (
158
- not isinstance(item, str)
159
- or not item.strip()
160
- or len(item) > 128 # ruff: ignore[magic-value-comparison]
161
- or any(ord(ch) < 32 for ch in item) # ruff: ignore[magic-value-comparison]
162
- ):
163
- _fail(code)
164
-
165
-
166
- def _current_proxy_version() -> str:
167
- shared = ROOT / "_utils" / "_shared_logic.py"
168
- try:
169
- text = shared.read_text(encoding="utf-8")
170
- except OSError:
171
- _fail("PROXY_VERSION_SOURCE_UNREADABLE")
172
- match = PROXY_VERSION_RE.search(text)
173
- if match is None:
174
- _fail("PROXY_VERSION_SOURCE_INVALID")
175
- return match.group(1)
176
-
177
-
178
- def _version_tuple(value: Any, code: str) -> tuple[int, int]:
179
- if not isinstance(value, str) or CYCLONEDX_VERSION_RE.fullmatch(value) is None:
180
- _fail(code)
181
- major, minor = value.split(".", 1)
182
- return int(major), int(minor)
183
-
184
-
185
- def _bool(value: Any, expected: bool, code: str) -> None:
186
- if value is not expected:
187
- _fail(code)
188
-
189
-
190
- def _walk_values(value: Any) -> None:
191
- if isinstance(value, dict):
192
- for key, child in value.items():
193
- if str(key).replace("-", "_").lower() in FORBIDDEN_SECRET_KEYS:
194
- _fail("EVIDENCE_SECRET_FIELD_FORBIDDEN")
195
- _walk_values(child)
196
- elif isinstance(value, list):
197
- for child in value:
198
- _walk_values(child)
199
- elif isinstance(value, str):
200
- if "\x00" in value or len(value) > 4096: # ruff: ignore[magic-value-comparison]
201
- _fail("EVIDENCE_STRING_UNSAFE")
202
- for pattern in SECRET_VALUE_PATTERNS:
203
- if pattern.search(value):
204
- _fail("EVIDENCE_SECRET_LIKE_VALUE_FORBIDDEN")
205
-
206
-
207
- def _artifact_path(evidence_dir: Path, raw: Any) -> Path:
208
- if not isinstance(raw, str) or not raw or raw.startswith(("/", "~")) or "\\" in raw:
209
- _fail("ARTIFACT_PATH_INVALID")
210
- rel = Path(raw)
211
- if rel.is_absolute() or any(part in {"", ".", ".."} for part in rel.parts):
212
- _fail("ARTIFACT_PATH_TRAVERSAL")
213
- root = evidence_dir.resolve()
214
- candidate = root / rel
215
- current = root
216
- for part in rel.parts:
217
- current = current / part
218
- if current.is_symlink():
219
- _fail("ARTIFACT_SYMLINK_FORBIDDEN")
220
- path = candidate.resolve()
221
- if root not in path.parents:
222
- _fail("ARTIFACT_PATH_ESCAPE")
223
- if not path.is_file():
224
- _fail("ARTIFACT_FILE_MISSING")
225
- if path.stat().st_size > int(POLICY["max_evidence_file_bytes"]):
226
- _fail("ARTIFACT_FILE_TOO_LARGE")
227
- return path
228
-
229
-
230
- def _verify_artifact(
231
- evidence_dir: Path,
232
- obj: Any,
233
- name: str,
234
- *,
235
- subject: str,
236
- extra_keys: set[str] | None = None,
237
- ) -> Path:
238
- item = _mapping(obj, f"{name.upper()}_INVALID")
239
- expected_keys = {"path", "sha256", "status", "subject", "tool"} | (
240
- extra_keys or set()
241
- )
242
- _exact_keys(item, expected_keys, f"{name.upper()}_SCHEMA_INVALID")
243
- _tool(item.get("tool"), f"{name.upper()}_TOOL_INVALID")
244
- if item.get("status") != "pass":
245
- _fail(f"{name.upper()}_NOT_PASS")
246
- if item.get("subject") != subject:
247
- _fail(f"{name.upper()}_SUBJECT_MISMATCH")
248
- path = _artifact_path(evidence_dir, item.get("path"))
249
- expected = _hex(item.get("sha256"), f"{name.upper()}_SHA256_INVALID")
250
- if _sha256(path) != expected:
251
- _fail(f"{name.upper()}_HASH_MISMATCH")
252
- return path
253
-
254
-
255
- def _verify_provenance(
256
- path: Path, image_digest: str, base_manifest_digest: str, item: dict[str, Any]
257
- ) -> None:
258
- if item.get("predicateType") != SLSA_PREDICATE:
259
- _fail("PROVENANCE_PREDICATE_UNSUPPORTED")
260
- _bool(item.get("signatureVerified"), True, "PROVENANCE_SIGNATURE_NOT_VERIFIED")
261
- try:
262
- statement = json.loads(path.read_text())
263
- except Exception as exc:
264
- raise EvidenceError("PROVENANCE_JSON_INVALID") from exc
265
- if (
266
- not isinstance(statement, dict)
267
- or statement.get("_type") != "https://in-toto.io/Statement/v1"
268
- ):
269
- _fail("PROVENANCE_STATEMENT_INVALID")
270
- if statement.get("predicateType") != SLSA_PREDICATE:
271
- _fail("PROVENANCE_STATEMENT_PREDICATE_MISMATCH")
272
- wanted = image_digest.split(":", 1)[1]
273
- subjects = statement.get("subject")
274
- if not isinstance(subjects, list) or not any(
275
- isinstance(s, dict)
276
- and isinstance(s.get("digest"), dict)
277
- and s["digest"].get("sha256") == wanted
278
- for s in subjects
279
- ):
280
- _fail("PROVENANCE_SUBJECT_IMAGE_MISMATCH")
281
- base_wanted = base_manifest_digest.split(":", 1)[1]
282
- predicate = statement.get("predicate")
283
- definition = (
284
- predicate.get("buildDefinition") if isinstance(predicate, dict) else None
285
- )
286
- dependencies = (
287
- definition.get("resolvedDependencies") if isinstance(definition, dict) else None
288
- )
289
- if not isinstance(dependencies, list) or not any(
290
- isinstance(dep, dict)
291
- and isinstance(dep.get("digest"), dict)
292
- and dep["digest"].get("sha256") == base_wanted
293
- for dep in dependencies
294
- ):
295
- _fail("PROVENANCE_BASE_MANIFEST_UNBOUND")
296
-
297
-
298
- def _verify_redis(doc: dict[str, Any], now: datetime) -> None:
299
- redis_policy = POLICY["redis"]
300
- planes = _mapping(doc.get("redis"), "REDIS_EVIDENCE_MISSING")
301
- _exact_keys(planes, {"rateLimit", "share", "contribution"}, "REDIS_SCHEMA_INVALID")
302
- for name in ("rateLimit", "share", "contribution"):
303
- item = _mapping(planes.get(name), f"REDIS_{name.upper()}_MISSING")
304
- _exact_keys(
305
- item,
306
- {
307
- "tlsVerified",
308
- "nonDefaultIdentity",
309
- "leastPrivilegeReviewed",
310
- "persistenceVerified",
311
- "replicationVerified",
312
- "backupRestoreTestedAt",
313
- },
314
- f"REDIS_{name.upper()}_SCHEMA_INVALID",
315
- )
316
- if redis_policy["require_tls"]:
317
- _bool(item.get("tlsVerified"), True, f"REDIS_{name.upper()}_TLS_UNVERIFIED")
318
- if redis_policy["require_non_default_identity"]:
319
- _bool(
320
- item.get("nonDefaultIdentity"),
321
- True,
322
- f"REDIS_{name.upper()}_DEFAULT_IDENTITY",
323
- )
324
- if redis_policy["require_least_privilege_review"]:
325
- _bool(
326
- item.get("leastPrivilegeReviewed"),
327
- True,
328
- f"REDIS_{name.upper()}_ACL_UNREVIEWED",
329
- )
330
- for name, prefix in (("share", "share"), ("contribution", "contribution")):
331
- item = planes[name]
332
- if redis_policy[f"{prefix}_require_persistence"]:
333
- _bool(
334
- item.get("persistenceVerified"),
335
- True,
336
- f"REDIS_{name.upper()}_PERSISTENCE_UNVERIFIED",
337
- )
338
- if redis_policy[f"{prefix}_require_replication"]:
339
- _bool(
340
- item.get("replicationVerified"),
341
- True,
342
- f"REDIS_{name.upper()}_REPLICATION_UNVERIFIED",
343
- )
344
- tested = _parse_time(
345
- item.get("backupRestoreTestedAt"),
346
- f"REDIS_{name.upper()}_BACKUP_TIME_INVALID",
347
- )
348
- if tested > now + timedelta(minutes=int(POLICY["max_clock_skew_minutes"])):
349
- _fail(f"REDIS_{name.upper()}_BACKUP_TIME_FUTURE")
350
- if now - tested > timedelta(
351
- days=int(redis_policy["backup_restore_max_age_days"])
352
- ):
353
- _fail(f"REDIS_{name.upper()}_BACKUP_RESTORE_STALE")
354
-
355
-
356
- def _verify_logging(doc: dict[str, Any], now: datetime) -> None:
357
- logging = _mapping(doc.get("logging"), "LOGGING_EVIDENCE_MISSING")
358
- _exact_keys(
359
- logging,
360
- {
361
- "requestBodyLogging",
362
- "authorizationHeaderLogging",
363
- "capabilityHeaderLogging",
364
- "queryStringLogging",
365
- "wafBodyCapture",
366
- "apmBodyCapture",
367
- "thirdPartyTelemetryExport",
368
- "reviewedAt",
369
- },
370
- "LOGGING_SCHEMA_INVALID",
371
- )
372
- mapping = {
373
- "requestBodyLogging": "request_body_logging",
374
- "authorizationHeaderLogging": "authorization_header_logging",
375
- "capabilityHeaderLogging": "capability_header_logging",
376
- "queryStringLogging": "query_string_logging",
377
- "wafBodyCapture": "waf_body_capture",
378
- "apmBodyCapture": "apm_body_capture",
379
- "thirdPartyTelemetryExport": "third_party_telemetry_export",
380
- }
381
- for evidence_name, policy_name in mapping.items():
382
- _bool(
383
- logging.get(evidence_name),
384
- bool(POLICY["logging"][policy_name]),
385
- f"LOGGING_{evidence_name.upper()}_POLICY_MISMATCH",
386
- )
387
- reviewed = _parse_time(logging.get("reviewedAt"), "LOGGING_REVIEW_TIME_INVALID")
388
- if reviewed > now + timedelta(minutes=int(POLICY["max_clock_skew_minutes"])):
389
- _fail("LOGGING_REVIEW_TIME_FUTURE")
390
- if now - reviewed > timedelta(hours=int(POLICY["max_age_hours"])):
391
- _fail("LOGGING_REVIEW_STALE")
392
-
393
-
394
- def verify( # ruff: ignore[too-many-branches, undocumented-public-function]
395
- evidence_file: Path,
396
- *,
397
- now: datetime | None = None,
398
- ) -> dict[str, Any]:
399
- if evidence_file.is_symlink():
400
- _fail("EVIDENCE_SYMLINK_FORBIDDEN")
401
- if not evidence_file.exists() or not evidence_file.is_file():
402
- _fail("EVIDENCE_FILE_MISSING")
403
- if evidence_file.stat().st_size > int(POLICY["max_manifest_bytes"]):
404
- _fail("EVIDENCE_MANIFEST_TOO_LARGE")
405
- evidence_file = evidence_file.resolve()
406
- try:
407
- doc = json.loads(evidence_file.read_text(encoding="utf-8"))
408
- except Exception as exc:
409
- raise EvidenceError("EVIDENCE_JSON_INVALID") from exc
410
- doc = _mapping(doc, "EVIDENCE_ROOT_INVALID")
411
- _walk_values(doc)
412
- _exact_keys(
413
- doc,
414
- {
415
- "schemaVersion",
416
- "release",
417
- "source",
418
- "image",
419
- "artifacts",
420
- "redis",
421
- "logging",
422
- "riskExceptions",
423
- },
424
- "EVIDENCE_SCHEMA_FIELDS_INVALID",
425
- )
426
- if doc.get("schemaVersion") != POLICY["evidence_schema_version"]:
427
- _fail("EVIDENCE_SCHEMA_UNSUPPORTED")
428
- release = _mapping(doc.get("release"), "RELEASE_BLOCK_MISSING")
429
- _exact_keys(
430
- release,
431
- {"releaseId", "generatedAt", "expiresAt", "proxyVersion", "targetPlatform"},
432
- "RELEASE_SCHEMA_INVALID",
433
- )
434
- if (
435
- not isinstance(release.get("releaseId"), str)
436
- or RELEASE_ID.fullmatch(release["releaseId"]) is None
437
- ):
438
- _fail("RELEASE_ID_INVALID")
439
- if release.get("targetPlatform") != POLICY["target_platform"]:
440
- _fail("RELEASE_PLATFORM_MISMATCH")
441
- if release.get("proxyVersion") != _current_proxy_version():
442
- _fail("RELEASE_PROXY_VERSION_MISMATCH")
443
- current = (now or datetime.now(timezone.utc)).astimezone(timezone.utc)
444
- generated = _parse_time(release.get("generatedAt"), "RELEASE_GENERATED_AT_INVALID")
445
- expires = _parse_time(release.get("expiresAt"), "RELEASE_EXPIRES_AT_INVALID")
446
- skew = timedelta(minutes=int(POLICY["max_clock_skew_minutes"]))
447
- if generated > current + skew:
448
- _fail("RELEASE_EVIDENCE_FROM_FUTURE")
449
- if current - generated > timedelta(hours=int(POLICY["max_age_hours"])):
450
- _fail("RELEASE_EVIDENCE_STALE")
451
- if expires <= current or expires <= generated:
452
- _fail("RELEASE_EVIDENCE_EXPIRED")
453
- if expires - generated > timedelta(hours=int(POLICY["max_age_hours"])):
454
- _fail("RELEASE_EVIDENCE_EXPIRY_TOO_LONG")
455
-
456
- source = _mapping(doc.get("source"), "SOURCE_BLOCK_MISSING")
457
- _exact_keys(
458
- source,
459
- {
460
- "requirementsLockSha256",
461
- "pythonSbomSha256",
462
- "runtimeSourceSha256",
463
- "baseImageIndexDigest",
464
- "baseImageManifestDigest",
465
- },
466
- "SOURCE_SCHEMA_INVALID",
467
- )
468
- lock_sha = _hex(source.get("requirementsLockSha256"), "LOCK_SHA256_INVALID")
469
- sbom_sha = _hex(source.get("pythonSbomSha256"), "PYTHON_SBOM_SHA256_INVALID")
470
- if lock_sha != _sha256(ROOT / SUPPLY["lock_file"]):
471
- _fail("LOCK_EVIDENCE_SOURCE_MISMATCH")
472
- if sbom_sha != _sha256(ROOT / SUPPLY["sbom_file"]):
473
- _fail("PYTHON_SBOM_EVIDENCE_SOURCE_MISMATCH")
474
- runtime_sha = _hex(
475
- source.get("runtimeSourceSha256"), "RUNTIME_SOURCE_SHA256_INVALID"
476
- )
477
- if runtime_sha != _runtime_source_sha256():
478
- _fail("RUNTIME_SOURCE_EVIDENCE_MISMATCH")
479
- if source.get("baseImageIndexDigest") != SUPPLY["base_image"]["index_digest"]:
480
- _fail("BASE_IMAGE_INDEX_MISMATCH")
481
- base_manifest_digest = _digest(
482
- source.get("baseImageManifestDigest"), "BASE_IMAGE_MANIFEST_DIGEST_INVALID"
483
- )
484
- if base_manifest_digest == source.get("baseImageIndexDigest"):
485
- _fail("BASE_IMAGE_MANIFEST_UNRESOLVED")
486
-
487
- image = _mapping(doc.get("image"), "IMAGE_BLOCK_MISSING")
488
- _exact_keys(image, {"digest", "platform"}, "IMAGE_SCHEMA_INVALID")
489
- image_digest = _digest(image.get("digest"), "IMAGE_DIGEST_INVALID")
490
- if image.get("platform") != POLICY["target_platform"]:
491
- _fail("IMAGE_PLATFORM_MISMATCH")
492
-
493
- artifacts = _mapping(doc.get("artifacts"), "ARTIFACTS_BLOCK_MISSING")
494
- _exact_keys(
495
- artifacts,
496
- {
497
- "dependencyScan",
498
- "imageScan",
499
- "imageSbom",
500
- "provenance",
501
- "signatureVerification",
502
- },
503
- "ARTIFACTS_SCHEMA_INVALID",
504
- )
505
- if POLICY["require_dependency_scan"]:
506
- _verify_artifact(
507
- evidence_file.parent,
508
- artifacts.get("dependencyScan"),
509
- "dependency_scan",
510
- subject="sha256:" + lock_sha,
511
- )
512
- if POLICY["require_image_vulnerability_scan"]:
513
- _verify_artifact(
514
- evidence_file.parent,
515
- artifacts.get("imageScan"),
516
- "image_scan",
517
- subject=image_digest,
518
- )
519
- if POLICY["require_full_image_sbom"]:
520
- sbom_path = _verify_artifact(
521
- evidence_file.parent,
522
- artifacts.get("imageSbom"),
523
- "image_sbom",
524
- subject=image_digest,
525
- )
526
- try:
527
- image_sbom = json.loads(sbom_path.read_text())
528
- except Exception as exc:
529
- raise EvidenceError("IMAGE_SBOM_JSON_INVALID") from exc
530
- if (
531
- not isinstance(image_sbom, dict)
532
- or image_sbom.get("bomFormat") != "CycloneDX"
533
- ):
534
- _fail("IMAGE_SBOM_NOT_CYCLONEDX")
535
- if _version_tuple(
536
- image_sbom.get("specVersion"), "IMAGE_SBOM_SPEC_VERSION_INVALID"
537
- ) < _version_tuple(
538
- POLICY["minimum_cyclonedx_spec_version"], "POLICY_CYCLONEDX_VERSION_INVALID"
539
- ):
540
- _fail("IMAGE_SBOM_SPEC_VERSION_TOO_OLD")
541
- if POLICY["require_slsa_provenance"]:
542
- provenance_item = _mapping(artifacts.get("provenance"), "PROVENANCE_INVALID")
543
- provenance_path = _verify_artifact(
544
- evidence_file.parent,
545
- provenance_item,
546
- "provenance",
547
- subject=image_digest,
548
- extra_keys={"predicateType", "signatureVerified"},
549
- )
550
- _verify_provenance(
551
- provenance_path, image_digest, base_manifest_digest, provenance_item
552
- )
553
- if POLICY["require_signature_verification"]:
554
- _verify_artifact(
555
- evidence_file.parent,
556
- artifacts.get("signatureVerification"),
557
- "signature_verification",
558
- subject=image_digest,
559
- )
560
-
561
- _verify_redis(doc, current)
562
- if POLICY["require_log_privacy_attestation"]:
563
- _verify_logging(doc, current)
564
- exceptions = doc.get("riskExceptions")
565
- if not isinstance(exceptions, list):
566
- _fail("RISK_EXCEPTIONS_INVALID")
567
- if POLICY["forbid_unexpired_risk_exceptions"] and exceptions:
568
- _fail("RISK_EXCEPTIONS_FORBIDDEN")
569
-
570
- return {
571
- "ok": True,
572
- "schema_version": doc["schemaVersion"],
573
- "release_id": release["releaseId"],
574
- "proxy_version": release["proxyVersion"],
575
- "target_platform": release["targetPlatform"],
576
- "image_digest": image_digest,
577
- "runtime_source_sha256": runtime_sha,
578
- "evidence_sha256": _sha256(evidence_file),
579
- "evidence_expires_at": release["expiresAt"],
580
- "redis_planes": ["rateLimit", "share", "contribution"],
581
- "logging_privacy": "verified",
582
- }
583
-
584
-
585
- def main( # ruff: ignore[undocumented-public-function]
586
- argv: list[str] | None = None,
587
- ) -> int:
588
- parser = argparse.ArgumentParser(description=__doc__)
589
- parser.add_argument("evidence", type=Path, help="Path to release-evidence.json")
590
- args = parser.parse_args(argv)
591
- try:
592
- result = verify(args.evidence)
593
- except EvidenceError as exc:
594
- sys.stderr.write(
595
- json.dumps({"ok": False, "code": str(exc)}, sort_keys=True) + "\n"
596
- )
597
- return 2
598
- sys.stdout.write(json.dumps(result, sort_keys=True) + "\n")
599
- return 0
600
-
601
-
602
- if __name__ == "__main__":
603
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/verify_release_gate.py DELETED
@@ -1,53 +0,0 @@
1
- #!/usr/bin/env python3
2
- """One fail-closed command for source policy plus production evidence binding."""
3
-
4
- from __future__ import annotations
5
-
6
- import argparse
7
- import json
8
- import logging
9
- import sys
10
- from pathlib import Path
11
-
12
- HERE = Path(__file__).resolve().parent
13
- if str(HERE) not in sys.path:
14
- sys.path.insert(0, str(HERE))
15
-
16
- import verify_release_evidence # noqa: E402
17
- import verify_supply_chain # noqa: E402
18
-
19
- logger = logging.getLogger(__name__)
20
-
21
-
22
- def verify( # ruff: ignore[undocumented-public-function]
23
- evidence: Path,
24
- ) -> dict[str, object]:
25
- source = verify_supply_chain.verify()
26
- release = verify_release_evidence.verify(evidence)
27
- return {
28
- "ok": True,
29
- "source_policy": source,
30
- "release_evidence": release,
31
- }
32
-
33
-
34
- def main( # ruff: ignore[undocumented-public-function]
35
- argv: list[str] | None = None,
36
- ) -> int:
37
- parser = argparse.ArgumentParser(description=__doc__)
38
- parser.add_argument("evidence", type=Path)
39
- args = parser.parse_args(argv)
40
- try:
41
- result = verify(args.evidence)
42
- except Exception as exc: # ruff: ignore[blind-except]
43
- code = getattr(exc, "args", ["RELEASE_GATE_FAILED"])[0] or "RELEASE_GATE_FAILED"
44
- sys.stderr.write(
45
- json.dumps({"ok": False, "code": str(code)}, sort_keys=True) + "\n"
46
- )
47
- return 2
48
- sys.stdout.write(json.dumps(result, sort_keys=True) + "\n")
49
- return 0
50
-
51
-
52
- if __name__ == "__main__":
53
- raise SystemExit(main())
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
security/verify_supply_chain.py DELETED
@@ -1,223 +0,0 @@
1
- #!/usr/bin/env python3
2
- """
3
- Offline structural verifier for the B38/B39 supply-chain policy.
4
-
5
- This verifier proves committed-file consistency and downgrade ratchets. It
6
- cannot prove that today's advisory database has no newer finding; networked
7
- release scanning remains a separate mandatory gate.
8
- """
9
-
10
- from __future__ import annotations
11
-
12
- import hashlib
13
- import json
14
- import logging
15
- import re
16
- import sys
17
- from pathlib import Path
18
-
19
- import tomllib
20
-
21
- logger = logging.getLogger(__name__)
22
-
23
- ROOT = Path(__file__).resolve().parents[1]
24
- POLICY = tomllib.loads((ROOT / "security/supply_chain_policy.toml").read_text())
25
- LOCK = ROOT / POLICY["lock_file"]
26
- SBOM = ROOT / POLICY["sbom_file"]
27
- DIRECT = ROOT / "requirements.txt"
28
- DOCKER = ROOT / "Dockerfile"
29
- IGNORE = ROOT / ".dockerignore"
30
- COMPOSE = ROOT / "docker-compose.hardened.reference.yml"
31
-
32
- LINE_RE = re.compile(
33
- r"^(?P<name>[A-Za-z0-9_.-]+)==(?P<version>[^\s]+) "
34
- r"--hash=sha256:(?P<digest>[0-9a-f]{64})$"
35
- )
36
-
37
-
38
- def _version_tuple(text: str) -> tuple[int | str, ...]:
39
- # Floors in this policy are simple numeric PEP-440 releases. Keep this
40
- # parser intentionally narrow so unusual versions fail review rather than
41
- # gaining surprising ordering semantics.
42
- out: list[int | str] = []
43
- for part in re.split(r"[.-]", text):
44
- out.append(int(part) if part.isdigit() else part)
45
- return tuple(out)
46
-
47
-
48
- def _locked() -> dict[str, tuple[str, str]]:
49
- result: dict[str, tuple[str, str]] = {}
50
- for raw in LOCK.read_text().splitlines():
51
- line = raw.strip()
52
- if not line or line.startswith("#"):
53
- continue
54
- match = LINE_RE.fullmatch(line)
55
- if not match:
56
- raise AssertionError(f"LOCK_LINE_NOT_EXACT_HASHED:{line}")
57
- name = match.group("name").lower().replace("_", "-")
58
- if name in result:
59
- raise AssertionError(f"LOCK_DUPLICATE:{name}")
60
- result[name] = (match.group("version"), match.group("digest"))
61
- if not result:
62
- raise AssertionError("LOCK_EMPTY")
63
- return result
64
-
65
-
66
- def verify() -> dict[str, object]: # ruff: ignore[too-many-branches]
67
- """Verify."""
68
- locked = _locked()
69
-
70
- direct: dict[str, str] = {}
71
- for raw in DIRECT.read_text().splitlines():
72
- line = raw.strip()
73
- if not line or line.startswith("#"):
74
- continue
75
- if any(token in line for token in (">", "<", "~=", "[", "]", "@", ";")):
76
- raise AssertionError(f"DIRECT_REQUIREMENT_NOT_EXACT_MINIMAL:{line}")
77
- if line.count("==") != 1:
78
- raise AssertionError(f"DIRECT_REQUIREMENT_NOT_EXACT:{line}")
79
- name, version = line.split("==", 1)
80
- direct[name.lower().replace("_", "-")] = version
81
- for name, version in direct.items():
82
- if locked.get(name, (None,))[0] != version:
83
- raise AssertionError(f"DIRECT_LOCK_DRIFT:{name}")
84
-
85
- for name, floor in POLICY["advisory_floors"].items():
86
- normalized = name.lower().replace("_", "-")
87
- if normalized not in locked:
88
- raise AssertionError(f"ADVISORY_FLOOR_PACKAGE_MISSING:{normalized}")
89
- if _version_tuple(locked[normalized][0]) < _version_tuple(str(floor)):
90
- raise AssertionError(f"ADVISORY_FLOOR_REGRESSION:{normalized}")
91
-
92
- docker = DOCKER.read_text()
93
- image = POLICY["base_image"]
94
- expected = f"{image['repository']}:{image['tag']}@{image['index_digest']}"
95
- required_docker = (
96
- expected,
97
- "--require-hashes",
98
- "--only-binary=:all:",
99
- "USER 1000:1000",
100
- "DEPLOYMENT_PROFILE=strict",
101
- "COPY --from=builder /opt/venv /opt/venv",
102
- "FROM --platform=linux/amd64 ${PYTHON_IMAGE} AS builder",
103
- "FROM --platform=linux/amd64 ${PYTHON_IMAGE} AS runtime",
104
- )
105
- for marker in required_docker:
106
- if marker not in docker:
107
- raise AssertionError(f"DOCKER_HARDENING_MISSING:{marker}")
108
- if "uvicorn[standard]" in docker or "fastapi[standard]" in docker:
109
- raise AssertionError("DOCKER_BROAD_EXTRAS_FORBIDDEN")
110
-
111
- ignore_lines = {
112
- line.strip()
113
- for line in IGNORE.read_text().splitlines()
114
- if line.strip() and not line.startswith("#")
115
- }
116
- if "*" not in ignore_lines:
117
- raise AssertionError("DOCKERIGNORE_NOT_DENY_BY_DEFAULT")
118
- for required in ("!requirements.lock", "!app.py", "!_utils/**"):
119
- if required not in ignore_lines:
120
- raise AssertionError(f"DOCKERIGNORE_RUNTIME_ALLOWLIST_MISSING:{required}")
121
- for required in (
122
- "_utils/__pycache__/",
123
- "_utils/**/__pycache__/",
124
- "_utils/**/*.pyc",
125
- "_utils/**/*.pyo",
126
- ):
127
- if required not in ignore_lines:
128
- raise AssertionError(
129
- f"DOCKERIGNORE_GENERATED_BYTECODE_EXCLUSION_MISSING:{required}"
130
- )
131
-
132
- compose = COMPOSE.read_text()
133
- for marker in (
134
- "read_only: true",
135
- 'user: "1000:1000"',
136
- "no-new-privileges:true",
137
- "cap_drop:",
138
- "DEPLOYMENT_PROFILE: strict",
139
- ):
140
- if marker not in compose:
141
- raise AssertionError(f"HARDENED_REFERENCE_MISSING:{marker}")
142
-
143
- release_policy = POLICY.get("release_evidence", {})
144
- for field in (
145
- "policy_file",
146
- "example_file",
147
- "verifier_file",
148
- "combined_gate_file",
149
- "redis_probe_file",
150
- "subject_printer_file",
151
- ):
152
- raw = release_policy.get(field)
153
- if not isinstance(raw, str) or not raw:
154
- raise AssertionError(f"RELEASE_EVIDENCE_POLICY_MISSING:{field}")
155
- path = ROOT / raw
156
- if not path.is_file():
157
- raise AssertionError(f"RELEASE_EVIDENCE_FILE_MISSING:{field}")
158
- evidence_policy = tomllib.loads((ROOT / release_policy["policy_file"]).read_text())
159
- if (int(evidence_policy.get("max_age_hours", 0)) <= 0) or int(
160
- evidence_policy.get("max_age_hours", 999)
161
- ) > (
162
- 72 # ruff: ignore[magic-value-comparison]
163
- ):
164
- raise AssertionError("RELEASE_EVIDENCE_MAX_AGE_UNSAFE")
165
- manifest_cap = int(evidence_policy.get("max_manifest_bytes", 0))
166
- if manifest_cap <= 0 or manifest_cap > 1024 * 1024:
167
- raise AssertionError("RELEASE_EVIDENCE_MANIFEST_CAP_UNSAFE")
168
- minimum_cdx = str(evidence_policy.get("minimum_cyclonedx_spec_version", ""))
169
- if not re.fullmatch(r"[0-9]+\.[0-9]+", minimum_cdx) or _version_tuple(
170
- minimum_cdx
171
- ) < _version_tuple("1.6"):
172
- raise AssertionError("RELEASE_EVIDENCE_CYCLONEDX_FLOOR_UNSAFE")
173
- if evidence_policy.get("target_platform") != POLICY.get("target_platform"):
174
- raise AssertionError("RELEASE_EVIDENCE_PLATFORM_DRIFT")
175
- if evidence_policy.get("forbid_unexpired_risk_exceptions") is not True:
176
- raise AssertionError("RELEASE_EVIDENCE_RISK_EXCEPTION_BYPASS")
177
- if evidence_policy.get("logging", {}).get("request_body_logging") is not False:
178
- raise AssertionError("RELEASE_EVIDENCE_BODY_LOGGING_NOT_FORBIDDEN")
179
- if (
180
- evidence_policy.get("logging", {}).get("third_party_telemetry_export")
181
- is not False
182
- ):
183
- raise AssertionError("RELEASE_EVIDENCE_TELEMETRY_NOT_FORBIDDEN")
184
-
185
- sbom = json.loads(SBOM.read_text())
186
- components = {
187
- c["name"].lower().replace("_", "-"): c for c in sbom.get("components", [])
188
- }
189
- if set(components) != set(locked):
190
- missing = sorted(set(locked) - set(components))
191
- extra = sorted(set(components) - set(locked))
192
- raise AssertionError(f"SBOM_LOCK_SET_DRIFT:missing={missing}:extra={extra}")
193
- for name, (version, digest) in locked.items():
194
- comp = components[name]
195
- if comp.get("version") != version:
196
- raise AssertionError(f"SBOM_VERSION_DRIFT:{name}")
197
- hashes = {
198
- h.get("content")
199
- for h in comp.get("hashes", [])
200
- if h.get("alg") == "SHA-256"
201
- }
202
- if digest not in hashes:
203
- raise AssertionError(f"SBOM_HASH_DRIFT:{name}")
204
-
205
- return {
206
- "ok": True,
207
- "locked_packages": len(locked),
208
- "direct_packages": len(direct),
209
- "base_index_digest": image["index_digest"],
210
- "lock_sha256": hashlib.sha256(LOCK.read_bytes()).hexdigest(),
211
- "sbom_sha256": hashlib.sha256(SBOM.read_bytes()).hexdigest(),
212
- "release_evidence_policy": True,
213
- }
214
-
215
-
216
- if __name__ == "__main__":
217
- try:
218
- sys.stdout.write(json.dumps(verify(), sort_keys=True) + "\n")
219
- except Exception as exc: # ruff: ignore[blind-except]
220
- sys.stderr.write(
221
- json.dumps({"ok": False, "error": str(exc)}, sort_keys=True) + "\n"
222
- )
223
- raise SystemExit(1) from exc