96 lines
3.3 KiB
Python
96 lines
3.3 KiB
Python
#!/usr/bin/env python
|
|
#
|
|
# tyr/tools/attest_tyr_supply_chain.py - MCP Tool for verifying supply chain integrity
|
|
#
|
|
import os
|
|
import json
|
|
import subprocess
|
|
|
|
# Use the cosign binary we compiled earlier
|
|
COSIGN_PATH = os.path.expanduser("~/go/bin/cosign")
|
|
|
|
|
|
def attest_tyr_supply_chain(p: dict) -> dict:
|
|
"""
|
|
Verifies the supply chain integrity of a container image using Cosign.
|
|
Checks for signature, SLSA provenance, and a CycloneDX SBOM.
|
|
"""
|
|
image_uri = p.get("image_uri")
|
|
if not image_uri:
|
|
raise ValueError("Missing required parameter: 'image_uri'")
|
|
|
|
# The KMS key used for verification
|
|
kms_key = "gcpkms://projects/propane-will-491900-m5/locations/global/keyRings/tyr/cryptoKeys/cosign"
|
|
|
|
report = {
|
|
"image_uri": image_uri,
|
|
"checks": []
|
|
}
|
|
|
|
# Check 1: Verify Signature
|
|
try:
|
|
cmd = [COSIGN_PATH, "verify", "--key", kms_key, image_uri]
|
|
# Cosign verify prints human-readable output to stderr on success
|
|
result = subprocess.run(cmd, capture_output=True,
|
|
text=True, check=True, timeout=45)
|
|
report["checks"].append({
|
|
"check": "signature",
|
|
"status": "PASS",
|
|
"details": result.stderr.strip()
|
|
})
|
|
except subprocess.CalledProcessError as e:
|
|
report["checks"].append({
|
|
"check": "signature",
|
|
"status": "FAIL",
|
|
"details": e.stderr.strip() or e.stdout.strip()
|
|
})
|
|
except Exception as e:
|
|
report["checks"].append(
|
|
{"check": "signature", "status": "ERROR", "message": str(e)})
|
|
|
|
# Check 2: Verify SLSA Provenance Attestation
|
|
try:
|
|
cmd = [COSIGN_PATH, "verify-attestation", "--key",
|
|
kms_key, "--type", "slsaprovenance", image_uri]
|
|
result = subprocess.run(cmd, capture_output=True,
|
|
text=True, check=True, timeout=45)
|
|
# The attestation predicate is printed to stdout
|
|
provenance = json.loads(result.stdout)
|
|
report["checks"].append({
|
|
"check": "slsa_provenance",
|
|
"status": "PASS",
|
|
"predicate": provenance.get("predicate", {})
|
|
})
|
|
except subprocess.CalledProcessError as e:
|
|
report["checks"].append({
|
|
"check": "slsa_provenance",
|
|
"status": "FAIL",
|
|
"details": e.stderr.strip() or e.stdout.strip()
|
|
})
|
|
except Exception as e:
|
|
report["checks"].append(
|
|
{"check": "slsa_provenance", "status": "ERROR", "message": str(e)})
|
|
|
|
# Check 3: Verify SBOM (CycloneDX) Attestation
|
|
try:
|
|
cmd = [COSIGN_PATH, "verify-attestation", "--key",
|
|
kms_key, "--type", "cyclonedx", image_uri]
|
|
result = subprocess.run(cmd, capture_output=True,
|
|
text=True, check=True, timeout=45)
|
|
report["checks"].append({
|
|
"check": "sbom_cyclonedx",
|
|
"status": "PASS",
|
|
"details": "CycloneDX SBOM attestation found and verified."
|
|
})
|
|
except subprocess.CalledProcessError as e:
|
|
report["checks"].append({
|
|
"check": "sbom_cyclonedx",
|
|
"status": "FAIL",
|
|
"details": e.stderr.strip() or e.stdout.strip()
|
|
})
|
|
except Exception as e:
|
|
report["checks"].append(
|
|
{"check": "sbom_cyclonedx", "status": "ERROR", "message": str(e)})
|
|
|
|
return report
|