mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-15 23:44:56 +03:00
255 lines
9.8 KiB
Python
255 lines
9.8 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
in-toto Supply Chain Verification Tool
|
|
|
|
Verifies container image supply chain integrity by checking
|
|
in-toto link metadata against the defined layout policy.
|
|
"""
|
|
|
|
import json
|
|
import subprocess
|
|
import sys
|
|
import argparse
|
|
import hashlib
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
|
|
|
|
def compute_file_hash(file_path: str, algorithm: str = "sha256") -> str:
|
|
"""Compute the hash of a file."""
|
|
h = hashlib.new(algorithm)
|
|
with open(file_path, "rb") as f:
|
|
for chunk in iter(lambda: f.read(8192), b""):
|
|
h.update(chunk)
|
|
return h.hexdigest()
|
|
|
|
|
|
def find_link_files(link_dir: str) -> list[dict]:
|
|
"""Find and parse all in-toto link metadata files."""
|
|
link_path = Path(link_dir)
|
|
if not link_path.exists():
|
|
print(f"[ERROR] Link directory not found: {link_dir}")
|
|
return []
|
|
|
|
links = []
|
|
for link_file in link_path.glob("*.link"):
|
|
try:
|
|
with open(link_file) as f:
|
|
data = json.load(f)
|
|
links.append({
|
|
"file": str(link_file),
|
|
"step_name": data.get("signed", {}).get("name", "unknown"),
|
|
"materials": data.get("signed", {}).get("materials", {}),
|
|
"products": data.get("signed", {}).get("products", {}),
|
|
"command": data.get("signed", {}).get("command", []),
|
|
"byproducts": data.get("signed", {}).get("byproducts", {}),
|
|
"signatures": data.get("signatures", []),
|
|
})
|
|
except (json.JSONDecodeError, KeyError) as e:
|
|
print(f"[WARN] Failed to parse {link_file}: {e}")
|
|
|
|
return links
|
|
|
|
|
|
def verify_artifact_chain(links: list[dict]) -> list[dict]:
|
|
"""Verify that artifact hashes chain correctly between steps."""
|
|
findings = []
|
|
products_by_step = {}
|
|
|
|
for link in sorted(links, key=lambda x: x["step_name"]):
|
|
products_by_step[link["step_name"]] = link["products"]
|
|
|
|
for link in links:
|
|
for material_path, material_hashes in link["materials"].items():
|
|
found = False
|
|
for step_name, products in products_by_step.items():
|
|
if step_name == link["step_name"]:
|
|
continue
|
|
if material_path in products:
|
|
product_hashes = products[material_path]
|
|
for algo, expected_hash in material_hashes.items():
|
|
actual_hash = product_hashes.get(algo, "")
|
|
if actual_hash and actual_hash != expected_hash:
|
|
findings.append({
|
|
"severity": "CRITICAL",
|
|
"type": "hash_mismatch",
|
|
"step": link["step_name"],
|
|
"artifact": material_path,
|
|
"expected": expected_hash[:16] + "...",
|
|
"actual": actual_hash[:16] + "...",
|
|
"description": f"Artifact {material_path} hash mismatch between steps"
|
|
})
|
|
elif actual_hash:
|
|
found = True
|
|
if not found and link["materials"]:
|
|
findings.append({
|
|
"severity": "WARNING",
|
|
"type": "untracked_material",
|
|
"step": link["step_name"],
|
|
"artifact": material_path,
|
|
"description": f"Material {material_path} not found in any prior step products"
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def verify_signatures(links: list[dict], trusted_keys: list[str]) -> list[dict]:
|
|
"""Verify that all link signatures come from trusted keys."""
|
|
findings = []
|
|
for link in links:
|
|
if not link["signatures"]:
|
|
findings.append({
|
|
"severity": "CRITICAL",
|
|
"type": "missing_signature",
|
|
"step": link["step_name"],
|
|
"description": f"Step {link['step_name']} has no signatures"
|
|
})
|
|
continue
|
|
|
|
for sig in link["signatures"]:
|
|
keyid = sig.get("keyid", "")
|
|
if trusted_keys and keyid not in trusted_keys:
|
|
findings.append({
|
|
"severity": "HIGH",
|
|
"type": "untrusted_key",
|
|
"step": link["step_name"],
|
|
"keyid": keyid[:16] + "...",
|
|
"description": f"Step {link['step_name']} signed by untrusted key"
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def check_required_steps(links: list[dict], required_steps: list[str]) -> list[dict]:
|
|
"""Check that all required pipeline steps have link metadata."""
|
|
findings = []
|
|
observed_steps = {link["step_name"] for link in links}
|
|
|
|
for step in required_steps:
|
|
if step not in observed_steps:
|
|
findings.append({
|
|
"severity": "CRITICAL",
|
|
"type": "missing_step",
|
|
"step": step,
|
|
"description": f"Required step '{step}' has no link metadata"
|
|
})
|
|
|
|
return findings
|
|
|
|
|
|
def run_in_toto_verify(layout_path: str, layout_key: str, link_dir: str) -> dict:
|
|
"""Run the official in-toto-verify command."""
|
|
try:
|
|
result = subprocess.run(
|
|
[
|
|
"in-toto-verify",
|
|
"--layout", layout_path,
|
|
"--layout-key", layout_key,
|
|
"--link-dir", link_dir,
|
|
],
|
|
capture_output=True, text=True, timeout=60
|
|
)
|
|
return {
|
|
"success": result.returncode == 0,
|
|
"stdout": result.stdout.strip(),
|
|
"stderr": result.stderr.strip(),
|
|
"returncode": result.returncode
|
|
}
|
|
except FileNotFoundError:
|
|
return {"success": False, "error": "in-toto-verify not found in PATH"}
|
|
except subprocess.TimeoutExpired:
|
|
return {"success": False, "error": "Verification timed out"}
|
|
|
|
|
|
def generate_report(links: list[dict], chain_findings: list[dict],
|
|
sig_findings: list[dict], step_findings: list[dict],
|
|
verify_result: dict, output_format: str = "text") -> str:
|
|
"""Generate a comprehensive verification report."""
|
|
all_findings = chain_findings + sig_findings + step_findings
|
|
critical_count = sum(1 for f in all_findings if f["severity"] == "CRITICAL")
|
|
high_count = sum(1 for f in all_findings if f["severity"] == "HIGH")
|
|
|
|
if output_format == "json":
|
|
report = {
|
|
"timestamp": datetime.utcnow().isoformat(),
|
|
"verification_passed": verify_result.get("success", False) and critical_count == 0,
|
|
"steps_found": len(links),
|
|
"findings": {
|
|
"critical": critical_count,
|
|
"high": high_count,
|
|
"total": len(all_findings),
|
|
"details": all_findings
|
|
},
|
|
"in_toto_verify": verify_result,
|
|
"steps": [{"name": l["step_name"], "command": l["command"],
|
|
"materials": len(l["materials"]), "products": len(l["products"])}
|
|
for l in links]
|
|
}
|
|
return json.dumps(report, indent=2)
|
|
|
|
lines = []
|
|
lines.append("=" * 70)
|
|
lines.append("IN-TOTO SUPPLY CHAIN VERIFICATION REPORT")
|
|
lines.append(f"Generated: {datetime.utcnow().isoformat()}")
|
|
lines.append("=" * 70)
|
|
|
|
passed = verify_result.get("success", False) and critical_count == 0
|
|
lines.append(f"\nVerification Result: {'PASSED' if passed else 'FAILED'}")
|
|
lines.append(f"Steps Found: {len(links)}")
|
|
|
|
lines.append("\n## Pipeline Steps")
|
|
for link in sorted(links, key=lambda x: x["step_name"]):
|
|
lines.append(f" Step: {link['step_name']}")
|
|
lines.append(f" Command: {' '.join(link['command'])}")
|
|
lines.append(f" Materials: {len(link['materials'])} | Products: {len(link['products'])}")
|
|
lines.append(f" Signatures: {len(link['signatures'])}")
|
|
|
|
if all_findings:
|
|
lines.append(f"\n## Findings ({len(all_findings)} total)")
|
|
for f in sorted(all_findings, key=lambda x: x["severity"]):
|
|
lines.append(f" [{f['severity']}] {f['description']}")
|
|
|
|
if verify_result.get("error"):
|
|
lines.append(f"\n## in-toto-verify Error")
|
|
lines.append(f" {verify_result['error']}")
|
|
|
|
lines.append("\n" + "=" * 70)
|
|
return "\n".join(lines)
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(description="in-toto Supply Chain Verification Tool")
|
|
parser.add_argument("--layout", help="Path to in-toto layout file")
|
|
parser.add_argument("--layout-key", help="Path to layout signing public key")
|
|
parser.add_argument("--link-dir", required=True, help="Directory containing link metadata")
|
|
parser.add_argument("--required-steps", nargs="+", default=["checkout", "build", "scan"],
|
|
help="Required pipeline steps")
|
|
parser.add_argument("--trusted-keys", nargs="+", default=[], help="Trusted key IDs")
|
|
parser.add_argument("--format", choices=["text", "json"], default="text")
|
|
args = parser.parse_args()
|
|
|
|
links = find_link_files(args.link_dir)
|
|
if not links:
|
|
print("[ERROR] No link metadata found")
|
|
sys.exit(1)
|
|
|
|
chain_findings = verify_artifact_chain(links)
|
|
sig_findings = verify_signatures(links, args.trusted_keys)
|
|
step_findings = check_required_steps(links, args.required_steps)
|
|
|
|
verify_result = {}
|
|
if args.layout and args.layout_key:
|
|
verify_result = run_in_toto_verify(args.layout, args.layout_key, args.link_dir)
|
|
else:
|
|
verify_result = {"success": None, "note": "Layout verification skipped (no --layout provided)"}
|
|
|
|
report = generate_report(links, chain_findings, sig_findings, step_findings, verify_result, args.format)
|
|
print(report)
|
|
|
|
critical_count = sum(1 for f in chain_findings + sig_findings + step_findings if f["severity"] == "CRITICAL")
|
|
sys.exit(1 if critical_count > 0 else 0)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|