mirror of
https://github.com/mukul975/Anthropic-Cybersecurity-Skills.git
synced 2026-06-15 23:44:56 +03:00
254 lines
9.6 KiB
Python
254 lines
9.6 KiB
Python
#!/usr/bin/env python3
|
|
"""in-toto supply chain security agent.
|
|
|
|
Implements software supply chain verification using the in-toto framework.
|
|
Creates and verifies supply chain layouts, generates link metadata for
|
|
build steps, and validates that all steps were performed by authorized
|
|
functionaries with correct materials and products.
|
|
"""
|
|
import argparse
|
|
import json
|
|
import os
|
|
import subprocess
|
|
import sys
|
|
from datetime import datetime, timezone
|
|
|
|
|
|
def find_intoto_binary():
|
|
"""Locate in-toto CLI tools."""
|
|
tools = {}
|
|
for name in ["in-toto-run", "in-toto-verify", "in-toto-record", "in-toto-sign"]:
|
|
for ext in ["", ".exe"]:
|
|
for d in os.environ.get("PATH", "").split(os.pathsep):
|
|
full = os.path.join(d, name + ext)
|
|
if os.path.isfile(full):
|
|
tools[name] = full
|
|
break
|
|
return tools
|
|
|
|
|
|
def create_layout_template(output_path, project_name, steps=None):
|
|
"""Generate a supply chain layout template."""
|
|
if steps is None:
|
|
steps = [
|
|
{"name": "clone", "expected_command": ["git", "clone"],
|
|
"threshold": 1, "materials": [], "products": ["src/*"]},
|
|
{"name": "build", "expected_command": ["make"],
|
|
"threshold": 1, "materials": ["src/*"], "products": ["dist/*"]},
|
|
{"name": "test", "expected_command": ["make", "test"],
|
|
"threshold": 1, "materials": ["src/*", "dist/*"], "products": []},
|
|
{"name": "package", "expected_command": ["tar", "czf"],
|
|
"threshold": 1, "materials": ["dist/*"], "products": ["*.tar.gz"]},
|
|
]
|
|
|
|
layout = {
|
|
"_type": "layout",
|
|
"expires": (datetime.now(timezone.utc).replace(year=datetime.now().year + 1)).isoformat(),
|
|
"readme": f"Supply chain layout for {project_name}",
|
|
"steps": [],
|
|
"inspect": [
|
|
{
|
|
"name": "verify-signature",
|
|
"expected_materials": [["MATCH", "*.tar.gz", "WITH", "PRODUCTS", "FROM", "package"]],
|
|
"expected_products": [],
|
|
"run": ["sha256sum", "*.tar.gz"],
|
|
}
|
|
],
|
|
"keys": {},
|
|
}
|
|
|
|
for step in steps:
|
|
layout["steps"].append({
|
|
"name": step["name"],
|
|
"expected_command": step.get("expected_command", []),
|
|
"threshold": step.get("threshold", 1),
|
|
"expected_materials": [
|
|
["MATCH", m, "WITH", "PRODUCTS", "FROM", steps[i-1]["name"]]
|
|
if i > 0 else ["ALLOW", m]
|
|
for i, m in enumerate(step.get("materials", []))
|
|
] or [["ALLOW", "*"]],
|
|
"expected_products": [
|
|
["CREATE", p] for p in step.get("products", [])
|
|
] or [["ALLOW", "*"]],
|
|
"pubkeys": [],
|
|
})
|
|
|
|
with open(output_path, "w") as f:
|
|
json.dump(layout, f, indent=2)
|
|
print(f"[+] Layout template written to {output_path}")
|
|
return layout
|
|
|
|
|
|
def run_step(tools, step_name, key_path, command, materials=None, products=None):
|
|
"""Execute a supply chain step and record link metadata."""
|
|
intoto_run = tools.get("in-toto-run")
|
|
if not intoto_run:
|
|
print("[!] in-toto-run not found", file=sys.stderr)
|
|
return None
|
|
|
|
cmd = [intoto_run, "--step-name", step_name, "--key", key_path]
|
|
if materials:
|
|
for m in materials:
|
|
cmd.extend(["--materials", m])
|
|
if products:
|
|
for p in products:
|
|
cmd.extend(["--products", p])
|
|
cmd.append("--")
|
|
cmd.extend(command)
|
|
|
|
print(f"[*] Running step '{step_name}': {' '.join(command)}")
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=300)
|
|
if result.returncode != 0:
|
|
print(f"[!] Step failed: {result.stderr[:200]}", file=sys.stderr)
|
|
return {"step": step_name, "status": "FAIL", "error": result.stderr[:200]}
|
|
|
|
link_file = f"{step_name}.link"
|
|
if os.path.isfile(link_file):
|
|
print(f"[+] Link metadata: {link_file}")
|
|
with open(link_file, "r") as f:
|
|
link_data = json.load(f)
|
|
return {"step": step_name, "status": "OK", "link_file": link_file,
|
|
"materials_count": len(link_data.get("signed", {}).get("materials", {})),
|
|
"products_count": len(link_data.get("signed", {}).get("products", {}))}
|
|
return {"step": step_name, "status": "OK", "link_file": "generated"}
|
|
|
|
|
|
def verify_layout(tools, layout_path, layout_key_path, link_dir="."):
|
|
"""Verify the supply chain against the layout."""
|
|
intoto_verify = tools.get("in-toto-verify")
|
|
if not intoto_verify:
|
|
print("[!] in-toto-verify not found", file=sys.stderr)
|
|
return {"status": "FAIL", "error": "in-toto-verify not found"}
|
|
|
|
cmd = [intoto_verify,
|
|
"--layout", layout_path,
|
|
"--layout-keys", layout_key_path,
|
|
"--link-dir", link_dir]
|
|
|
|
print(f"[*] Verifying supply chain layout: {layout_path}")
|
|
result = subprocess.run(cmd, capture_output=True, text=True, timeout=120)
|
|
if result.returncode == 0:
|
|
print(f"[+] Verification PASSED")
|
|
return {"status": "PASS", "detail": "All steps verified successfully"}
|
|
else:
|
|
print(f"[!] Verification FAILED: {result.stderr[:300]}")
|
|
return {"status": "FAIL", "detail": result.stderr[:300]}
|
|
|
|
|
|
def audit_existing_links(link_dir="."):
|
|
"""Audit existing link metadata files."""
|
|
findings = []
|
|
for fname in os.listdir(link_dir):
|
|
if not fname.endswith(".link"):
|
|
continue
|
|
fpath = os.path.join(link_dir, fname)
|
|
try:
|
|
with open(fpath, "r") as f:
|
|
link = json.load(f)
|
|
signed = link.get("signed", {})
|
|
step_name = signed.get("name", fname)
|
|
materials = signed.get("materials", {})
|
|
products = signed.get("products", {})
|
|
command = signed.get("command", [])
|
|
byproducts = signed.get("byproducts", {})
|
|
|
|
findings.append({
|
|
"link_file": fname,
|
|
"step_name": step_name,
|
|
"materials_count": len(materials),
|
|
"products_count": len(products),
|
|
"command": " ".join(command)[:80] if command else "N/A",
|
|
"return_code": byproducts.get("return-value", "N/A"),
|
|
"has_signature": bool(link.get("signatures")),
|
|
})
|
|
except (json.JSONDecodeError, IOError) as e:
|
|
findings.append({"link_file": fname, "error": str(e)})
|
|
|
|
return findings
|
|
|
|
|
|
def format_summary(results):
|
|
"""Print supply chain audit summary."""
|
|
print(f"\n{'='*60}")
|
|
print(f" in-toto Supply Chain Security Report")
|
|
print(f"{'='*60}")
|
|
if isinstance(results, list):
|
|
print(f" Link Files Found: {len(results)}")
|
|
for r in results:
|
|
if "error" in r:
|
|
print(f" [ERR] {r['link_file']}: {r['error']}")
|
|
else:
|
|
sig = "signed" if r.get("has_signature") else "UNSIGNED"
|
|
print(f" [{sig:8s}] {r['step_name']:20s} | "
|
|
f"{r['materials_count']} materials, {r['products_count']} products | "
|
|
f"cmd: {r.get('command', 'N/A')[:40]}")
|
|
|
|
|
|
def main():
|
|
parser = argparse.ArgumentParser(
|
|
description="in-toto supply chain security agent"
|
|
)
|
|
sub = parser.add_subparsers(dest="command")
|
|
|
|
p_layout = sub.add_parser("create-layout", help="Generate a layout template")
|
|
p_layout.add_argument("--project", required=True, help="Project name")
|
|
p_layout.add_argument("--output-path", default="root.layout", help="Layout output file")
|
|
|
|
p_run = sub.add_parser("run-step", help="Execute a supply chain step")
|
|
p_run.add_argument("--step-name", required=True)
|
|
p_run.add_argument("--key", required=True, help="Functionary key path")
|
|
p_run.add_argument("--materials", nargs="*")
|
|
p_run.add_argument("--products", nargs="*")
|
|
p_run.add_argument("cmd", nargs="+", help="Command to execute")
|
|
|
|
p_verify = sub.add_parser("verify", help="Verify supply chain")
|
|
p_verify.add_argument("--layout", required=True)
|
|
p_verify.add_argument("--layout-key", required=True)
|
|
p_verify.add_argument("--link-dir", default=".")
|
|
|
|
p_audit = sub.add_parser("audit", help="Audit existing link metadata")
|
|
p_audit.add_argument("--link-dir", default=".")
|
|
|
|
parser.add_argument("--output", "-o", help="Output JSON report")
|
|
parser.add_argument("--verbose", "-v", action="store_true")
|
|
args = parser.parse_args()
|
|
|
|
if not args.command:
|
|
parser.print_help()
|
|
sys.exit(1)
|
|
|
|
tools = find_intoto_binary()
|
|
result = {}
|
|
|
|
if args.command == "create-layout":
|
|
layout = create_layout_template(args.output_path, args.project)
|
|
result = {"action": "create-layout", "layout": layout}
|
|
elif args.command == "run-step":
|
|
step_result = run_step(tools, args.step_name, args.key,
|
|
args.cmd, args.materials, args.products)
|
|
result = {"action": "run-step", "result": step_result}
|
|
elif args.command == "verify":
|
|
verify_result = verify_layout(tools, args.layout, args.layout_key, args.link_dir)
|
|
result = {"action": "verify", "result": verify_result}
|
|
elif args.command == "audit":
|
|
link_findings = audit_existing_links(args.link_dir)
|
|
format_summary(link_findings)
|
|
result = {"action": "audit", "links": link_findings}
|
|
|
|
report = {
|
|
"timestamp": datetime.now(timezone.utc).isoformat(),
|
|
"tool": "in-toto",
|
|
"result": result,
|
|
}
|
|
|
|
if args.output:
|
|
with open(args.output, "w") as f:
|
|
json.dump(report, f, indent=2)
|
|
print(f"\n[+] Report saved to {args.output}")
|
|
elif args.verbose:
|
|
print(json.dumps(report, indent=2))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|