container security

Implementing Supply Chain Security with in-toto

Implement software supply chain integrity verification for container builds using the in-toto framework to create cryptographically signed attestations across CI/CD pipeline steps.

attestationcncfcontainer-securityin-totoprovenancesbomsigstoreslsa
Install this skill
npx skills add mukul975/Anthropic-Cybersecurity-Skills
Framework mappings

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:

  1. All required steps were performed
  2. Each step was signed by an authorized functionary
  3. Artifact hashes chain correctly between steps
  4. 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/scanner

Step 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
fi

Step 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

Source materials

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.key

Generate Signing Keys

# Generate an Ed25519 keypair
in-toto-keygen --type ed25519 --output functionary-key

Python 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}.link

Verify 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 fails

Inspect 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

  1. Map your CI/CD pipeline steps (source, build, test, scan, package, push)
  2. Identify functionaries for each step (CI runner, scanner, reviewer)
  3. Define artifact flow between steps (what inputs/outputs)
  4. Generate signing keys for each functionary
  5. Create and sign the supply chain layout

Phase 2: Pipeline Integration

  1. Wrap each CI/CD step with in-toto-run to generate link metadata
  2. Configure key management (Vault, KMS, or Sigstore keyless)
  3. Store link metadata alongside build artifacts
  4. Verify supply chain at end of pipeline before push to registry
  5. Attach attestations to container images via cosign

Phase 3: Deployment Verification

  1. Deploy admission webhook for in-toto verification
  2. Configure policy engine to require valid attestations
  3. Test with known-good and known-bad attestation chains
  4. Enable enforcement mode to block unverified images
  5. 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.pub

Key Rotation Workflow

  1. Generate new key pair for the functionary
  2. Update the supply chain layout with new public key
  3. Re-sign the layout with the owner key
  4. Distribute new private key to the functionary (via secrets manager)
  5. Revoke the old key after transition period
  6. Verify builds use new key going forward

Incident Response Workflow

Supply Chain Compromise Detected

  1. Identify which step's attestation is invalid or missing
  2. Check if functionary key was compromised
  3. Review link metadata for the affected step
  4. Compare artifact hashes against known-good builds
  5. If compromise confirmed: revoke affected keys, rebuild from verified source
  6. Update layout to add additional verification requirements

Scripts 2

agent.py9.6 KB
Display-only source. This catalog never executes bundled scripts.
#!/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
Display-only source. This catalog never executes bundled scripts.
#!/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()

Assets 1

template.mdtext/markdown · 1.3 KB
Keep exploring