Spaces:
Running
Running
Delete security
Browse files- security/RELEASE_EVIDENCE_GUIDE.md +0 -107
- security/SECURITY_RELEASE_GATES.md +0 -128
- security/probe_redis_authority.py +0 -162
- security/python-runtime.cdx.json +0 -721
- security/release-evidence.example.json +0 -112
- security/release_evidence_policy.toml +0 -34
- security/release_subjects.py +0 -43
- security/supply_chain_policy.toml +0 -38
- security/verify_release_evidence.py +0 -603
- security/verify_release_gate.py +0 -53
- security/verify_supply_chain.py +0 -223
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
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|