npx skills add mukul975/Anthropic-Cybersecurity-SkillsMITRE ATT&CK
Overview
in-toto is a CNCF graduated project that ensures the integrity of software supply chains from initiation to end-user installation. It creates a verifiable record of the entire software development lifecycle by generating cryptographically signed attestations (called "link metadata") at each step, proving what happened, who performed it, and what artifacts were produced. For container environments, in-toto verifies that images deployed to Kubernetes followed approved build processes and have not been tampered with.
When to Use
- When deploying or configuring implementing supply chain security with in toto capabilities in your environment
- When establishing security controls aligned to compliance requirements
- When building or improving security architecture for this domain
- When conducting security assessments that require this implementation
Prerequisites
- Python 3.8+ or Go runtime for in-toto client libraries
- GPG or Ed25519 keys for signing attestations
- Container build pipeline (Docker, Buildah, or Kaniko)
- Container registry (Docker Hub, ECR, GCR, or Harbor)
- Kubernetes cluster for deployment verification
Core Concepts
Supply Chain Layout
The layout is the central policy document that defines:
- Steps: Ordered operations in the supply chain (clone, build, test, package, push)
- Functionaries: Authorized entities (people or CI systems) that perform each step
- Inspections: Client-side verification checks performed at verification time
- Expected artifacts: Input/output relationships between steps
from in_toto.models.layout import Layout, Step, Inspection
from securesystemslib.interface import import_ed25519_privatekey_from_file
# Create the supply chain layout
layout = Layout()
layout.set_relative_expiration(months=3)
# Define the code clone step
step_clone = Step(name="clone")
step_clone.expected_materials = []
step_clone.expected_products = [["CREATE", "src/*"]]
step_clone.pubkeys = [clone_functionary_keyid]
step_clone.expected_command = ["git", "clone"]
step_clone.threshold = 1
# Define the build step
step_build = Step(name="build")
step_build.expected_materials = [["MATCH", "src/*", "WITH", "PRODUCTS", "FROM", "clone"]]
step_build.expected_products = [["CREATE", "image.tar"]]
step_build.pubkeys = [build_functionary_keyid]
step_build.expected_command = ["docker", "build"]
step_build.threshold = 1
# Define the scan step
step_scan = Step(name="scan")
step_scan.expected_materials = [["MATCH", "image.tar", "WITH", "PRODUCTS", "FROM", "build"]]
step_scan.expected_products = [["CREATE", "scan-report.json"]]
step_scan.pubkeys = [scan_functionary_keyid]
step_scan.threshold = 1
layout.steps = [step_clone, step_build, step_scan]Link Metadata
Each step execution generates a link file containing:
- Materials consumed (input artifacts with hashes)
- Products created (output artifacts with hashes)
- Command executed
- Cryptographic signature of the functionary
Verification Process
At deployment time, the verifier checks:
- All required steps were performed
- Each step was signed by an authorized functionary
- Artifact hashes chain correctly between steps
- No unauthorized modifications occurred between steps
Implementation
Step 1: Generate Signing Keys
# Generate Ed25519 key pairs for each functionary
mkdir -p keys
# Project owner key (signs the layout)
in-toto-keygen --type ed25519 keys/owner
# CI builder key
in-toto-keygen --type ed25519 keys/builder
# Security scanner key
in-toto-keygen --type ed25519 keys/scannerStep 2: Create the Supply Chain Layout
#!/usr/bin/env python3
"""Generate in-toto supply chain layout for container builds."""
from in_toto.models.layout import Layout, Step, Inspection
from in_toto.models.metadata import Envelope
from securesystemslib.signer import CryptoSigner
from securesystemslib.interface import import_ed25519_publickey_from_file
def create_container_build_layout():
layout = Layout()
layout.set_relative_expiration(months=6)
# Load functionary public keys
builder_key = import_ed25519_publickey_from_file("keys/builder.pub")
scanner_key = import_ed25519_publickey_from_file("keys/scanner.pub")
layout.keys = {
builder_key["keyid"]: builder_key,
scanner_key["keyid"]: scanner_key,
}
# Step 1: Source code checkout
checkout = Step(name="checkout")
checkout.expected_materials = []
checkout.expected_products = [
["CREATE", "Dockerfile"],
["CREATE", "src/*"],
["CREATE", "requirements.txt"],
]
checkout.pubkeys = [builder_key["keyid"]]
checkout.threshold = 1
# Step 2: Build container image
build = Step(name="build")
build.expected_materials = [
["MATCH", "Dockerfile", "WITH", "PRODUCTS", "FROM", "checkout"],
["MATCH", "src/*", "WITH", "PRODUCTS", "FROM", "checkout"],
]
build.expected_products = [["CREATE", "image-digest.txt"]]
build.pubkeys = [builder_key["keyid"]]
build.threshold = 1
# Step 3: Security scan
scan = Step(name="scan")
scan.expected_materials = [
["MATCH", "image-digest.txt", "WITH", "PRODUCTS", "FROM", "build"]
]
scan.expected_products = [
["CREATE", "vulnerability-report.json"],
["CREATE", "sbom.json"],
]
scan.pubkeys = [scanner_key["keyid"]]
scan.threshold = 1
# Inspection: Verify no critical vulnerabilities
inspect_vulns = Inspection(name="verify-no-critical-vulns")
inspect_vulns.expected_materials = [
["MATCH", "vulnerability-report.json", "WITH", "PRODUCTS", "FROM", "scan"]
]
inspect_vulns.run = [
"python", "-c",
"import json,sys; r=json.load(open('vulnerability-report.json')); "
"sys.exit(1) if any(v['severity']=='CRITICAL' for v in r.get('vulnerabilities',[])) else sys.exit(0)"
]
layout.steps = [checkout, build, scan]
layout.inspect = [inspect_vulns]
return layout
if __name__ == "__main__":
layout = create_container_build_layout()
# Sign with owner key and save
owner_signer = CryptoSigner.from_priv_key_uri("file:keys/owner")
envelope = Envelope.from_signable(layout)
envelope.create_signature(owner_signer)
envelope.dump("root.layout")
print("Layout created and signed: root.layout")Step 3: Record Pipeline Steps
# In CI/CD pipeline - record each step
# Step 1: Checkout
in-toto-run --step-name checkout \
--key keys/builder \
--products Dockerfile src/* requirements.txt \
-- git clone https://github.com/org/app.git .
# Step 2: Build
in-toto-run --step-name build \
--key keys/builder \
--materials Dockerfile src/* \
--products image-digest.txt \
-- bash -c "docker build -t app:latest . && docker inspect --format='{{.Id}}' app:latest > image-digest.txt"
# Step 3: Scan
in-toto-run --step-name scan \
--key keys/scanner \
--materials image-digest.txt \
--products vulnerability-report.json sbom.json \
-- bash -c "trivy image --format json app:latest > vulnerability-report.json && syft app:latest -o json > sbom.json"Step 4: Verify Before Deployment
# Verify the entire supply chain
in-toto-verify --layout root.layout \
--layout-key keys/owner.pub \
--link-dir ./link-metadata/
# If verification passes, proceed with deployment
if [ $? -eq 0 ]; then
kubectl apply -f deployment.yaml
echo "Supply chain verification passed - deploying"
else
echo "SUPPLY CHAIN VERIFICATION FAILED - blocking deployment"
exit 1
fiStep 5: Kubernetes Admission Control
Integrate with a policy engine to verify attestations at admission:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: in-toto-verifier
webhooks:
- name: verify.in-toto.io
rules:
- apiGroups: ["apps"]
resources: ["deployments"]
operations: ["CREATE", "UPDATE"]
clientConfig:
service:
name: in-toto-webhook
namespace: security
path: /verify
failurePolicy: Fail
sideEffects: None
admissionReviewVersions: ["v1"]SLSA Integration
in-toto attestations map directly to SLSA (Supply chain Levels for Software Artifacts) requirements:
| SLSA Level | in-toto Requirement |
|---|---|
| Level 1 | Build process documented (layout exists) |
| Level 2 | Signed attestations from hosted build service |
| Level 3 | Hardened build platform, non-falsifiable provenance |
| Level 4 | Two-party review, hermetic builds |
References
References and resources
Everything below is rendered for inspection. Script files are read-only and never run.
References 3
api-reference.md4.7 KB
API Reference: in-toto Supply Chain Security
Libraries Used
| Library | Purpose |
|---|---|
in_toto |
Python reference implementation for supply chain verification |
securesystemslib |
Cryptographic key management and signing |
subprocess |
Execute in-toto-run and in-toto-verify CLI commands |
json |
Parse link metadata and layout files |
Installation
pip install in-toto 'securesystemslib[crypto]'CLI Commands
Record a Supply Chain Step
# Record a build step (creates a link metadata file)
in-toto-run --step-name build \
--key functionary-key \
--materials src/ \
--products dist/ \
-- make build
# Record a test step
in-toto-run --step-name test \
--key tester-key \
--materials dist/ \
--products test-results/ \
-- pytest tests/Verify the Supply Chain
# Verify all steps match the layout
in-toto-verify --layout root.layout \
--layout-keys project-owner-pub.keyGenerate Signing Keys
# Generate an Ed25519 keypair
in-toto-keygen --type ed25519 --output functionary-keyPython API
Create a Supply Chain Layout
from in_toto.models.layout import Layout, Step, Inspection
from in_toto.models.metadata import Metadata
from securesystemslib.interface import import_ed25519_privatekey_from_file
# Load the project owner's private key
owner_key = import_ed25519_privatekey_from_file("owner-key")
# Define the supply chain layout
layout = Layout()
layout.expires = "2026-01-01T00:00:00Z"
# Step 1: Source code checkout
step_clone = Step(name="clone")
step_clone.expected_materials = []
step_clone.expected_products = [["CREATE", "src/*"]]
step_clone.pubkeys = [functionary_keyid]
step_clone.expected_command = ["git", "clone", "https://github.com/org/repo.git"]
# Step 2: Build
step_build = Step(name="build")
step_build.expected_materials = [
["MATCH", "src/*", "WITH", "PRODUCTS", "FROM", "clone"]
]
step_build.expected_products = [["CREATE", "dist/*"]]
step_build.pubkeys = [functionary_keyid]
# Step 3: Test
step_test = Step(name="test")
step_test.expected_materials = [
["MATCH", "dist/*", "WITH", "PRODUCTS", "FROM", "build"]
]
step_test.expected_products = [["CREATE", "test-results/*"]]
step_test.pubkeys = [tester_keyid]
layout.steps = [step_clone, step_build, step_test]
# Add an inspection (run at verification time)
inspection = Inspection(name="verify-checksums")
inspection.expected_materials = [
["MATCH", "dist/*", "WITH", "PRODUCTS", "FROM", "build"]
]
inspection.run = ["sha256sum", "dist/*"]
layout.inspect = [inspection]
# Sign and write the layout
metadata = Metadata(signed=layout)
metadata.sign(owner_key)
metadata.dump("root.layout")Record a Step Programmatically
from in_toto.runlib import in_toto_run
# Record a step with materials and products
link = in_toto_run(
name="build",
material_list=["src/"],
product_list=["dist/"],
signing_key=functionary_key,
record_streams=True,
command=["make", "build"],
)
# Saves build.{keyid-prefix}.linkVerify the Supply Chain
from in_toto.verifylib import in_toto_verify
# Verify all steps and inspections
summary = in_toto_verify(
metadata=layout_metadata,
layout_key_dict={owner_keyid: owner_pubkey},
)
# Raises an exception if verification failsInspect Link Metadata
from in_toto.models.metadata import Metadata
link_metadata = Metadata.load("build.abc123.link")
link = link_metadata.signed
print(f"Step: {link.name}")
print(f"Command: {link.command}")
print(f"Materials: {list(link.materials.keys())}")
print(f"Products: {list(link.products.keys())}")
print(f"Return value: {link.byproducts.get('return-value')}")Key Concepts
| Concept | Description |
|---|---|
| Layout | Defines the expected supply chain steps, who performs them, and material/product rules |
| Step | A single supply chain operation (clone, build, test, package) |
| Link | Metadata recorded when a step is actually performed (materials, products, command) |
| Inspection | Verification commands run at verification time |
| Functionary | A person or CI system authorized to perform a step |
| Materials | Input files consumed by a step |
| Products | Output files produced by a step |
Output Format
Link Metadata
{
"signatures": [{"keyid": "abc123...", "sig": "..."}],
"signed": {
"_type": "link",
"name": "build",
"command": ["make", "build"],
"materials": {
"src/main.py": {"sha256": "a1b2c3..."}
},
"products": {
"dist/app.tar.gz": {"sha256": "d4e5f6..."}
},
"byproducts": {
"return-value": 0,
"stdout": "Build successful",
"stderr": ""
}
}
}standards.md2.1 KB
Standards and References - Supply Chain Security with in-toto
Industry Standards
SLSA (Supply chain Levels for Software Artifacts)
- in-toto provides the attestation framework that SLSA builds upon
- SLSA provenance attestations use in-toto attestation format
- Graduated to v1.0 specification with clear level requirements
NIST SSDF (Secure Software Development Framework) SP 800-218
- PO.1.1: Define and document security requirements for software
- PS.1.1: Verify third-party software components
- PW.4.1: Review and analyze source code for security vulnerabilities
NIST SP 800-204D: Strategies for Securing the Software Supply Chain
- Section 4: Build system integrity verification
- Section 5: Attestation-based supply chain verification
- Recommends cryptographic provenance tracking
Executive Order 14028 (Improving the Nation's Cybersecurity)
- Requires SBOM generation for software sold to federal government
- Mandates supply chain security for critical software
- in-toto attestations satisfy provenance requirements
Compliance Mapping
| Requirement | Framework | in-toto Capability |
|---|---|---|
| Build provenance | SLSA L2+ | Signed link metadata per build step |
| Artifact integrity | NIST 800-204D | SHA-256 hash chaining between steps |
| Authorized builders | SLSA L3+ | Functionary key verification |
| SBOM generation | EO 14028 | Inspection step for SBOM validation |
| Code review | SLSA L4 | Threshold signing with multiple reviewers |
| Tamper detection | PCI DSS 6.5 | End-to-end verification before deployment |
Ecosystem Integration
Sigstore
- Keyless signing via Fulcio certificate authority
- Transparency log via Rekor for attestation persistence
- Cosign for container image signing and verification
Witness / Archivista
- Witness: in-toto implementation focused on cloud-native CI/CD
- Archivista: Attestation storage and retrieval service
- Both used by SolarWinds for supply chain integrity post-breach
OpenVEX
- Vulnerability Exploitability eXchange format
- Complements in-toto attestations with vulnerability status
- Allows marking CVEs as "not affected" for specific builds
workflows.md2.8 KB
Workflows - Supply Chain Security with in-toto
Implementation Workflow
Phase 1: Layout Design
- Map your CI/CD pipeline steps (source, build, test, scan, package, push)
- Identify functionaries for each step (CI runner, scanner, reviewer)
- Define artifact flow between steps (what inputs/outputs)
- Generate signing keys for each functionary
- Create and sign the supply chain layout
Phase 2: Pipeline Integration
- Wrap each CI/CD step with
in-toto-runto generate link metadata - Configure key management (Vault, KMS, or Sigstore keyless)
- Store link metadata alongside build artifacts
- Verify supply chain at end of pipeline before push to registry
- Attach attestations to container images via cosign
Phase 3: Deployment Verification
- Deploy admission webhook for in-toto verification
- Configure policy engine to require valid attestations
- Test with known-good and known-bad attestation chains
- Enable enforcement mode to block unverified images
- Monitor verification failures and alert on anomalies
CI/CD Pipeline Integration
GitHub Actions Example
jobs:
build:
steps:
- uses: actions/checkout@v4
- name: Record checkout step
run: |
in-toto-run --step-name checkout \
--key ${{ secrets.BUILDER_KEY }} \
--products Dockerfile src/* \
-- echo "checkout complete"
- name: Build and record
run: |
in-toto-run --step-name build \
--key ${{ secrets.BUILDER_KEY }} \
--materials Dockerfile src/* \
--products image-digest.txt \
-- docker build -t app:${{ github.sha }} .
- name: Scan and record
run: |
in-toto-run --step-name scan \
--key ${{ secrets.SCANNER_KEY }} \
--materials image-digest.txt \
--products vuln-report.json sbom.json \
-- trivy image app:${{ github.sha }}
- name: Verify chain
run: |
in-toto-verify --layout root.layout \
--layout-key keys/owner.pubKey Rotation Workflow
- Generate new key pair for the functionary
- Update the supply chain layout with new public key
- Re-sign the layout with the owner key
- Distribute new private key to the functionary (via secrets manager)
- Revoke the old key after transition period
- Verify builds use new key going forward
Incident Response Workflow
Supply Chain Compromise Detected
- Identify which step's attestation is invalid or missing
- Check if functionary key was compromised
- Review link metadata for the affected step
- Compare artifact hashes against known-good builds
- If compromise confirmed: revoke affected keys, rebuild from verified source
- Update layout to add additional verification requirements
Scripts 2
agent.py9.6 KB
#!/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()
process.py9.8 KB
#!/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()