BLOG/ENTERPRISE AUTOMATION
THEME
ENTERPRISE AUTOMATION

Evidence bundles for compliance

When an auditor asks for evidence, you shouldn't spend a day grepping CI logs and screenshotting dashboards. Here's an OrchStep workflow that gathers the artifacts, hashes each one, and seals them into a single bundle per release.

Apr 30, 2026 OrchStep Team 6 minROLE: Security EngineerSCALE: Enterprise
RUNNABLE DEMO
Full source for this post: blog/evidence-bundles
VIEW SOURCE

The audit request is always the same: "For release 2026.5.0, show us the test results, the SBOM, and the approval records." And the scramble is always the same — someone opens three browser tabs, screenshots a green CI run, exports a dependency list, digs the approval out of a ticket, and pastes it all into a folder named evidence_final_v2. It takes most of a day, and the result is a pile of screenshots nobody can verify.

The evidence already exists at release time. The test report, the SBOM, the approvals — they're all produced during the release. The expensive part is re-collecting them weeks later from systems that have since rotated their logs. So collect them once, when the release happens, hash them, and seal them into a bundle you can hand over without a scramble.

Gather, hash, seal

Pull every required artifact in parallel, hash them together, render a manifest, and assemble the bundle:

orchstep.yml
name: evidence-bundles
defaults:
  release: "2026.5.0"

tasks:
  # `orchstep run bundle --var release=2026.6.0`
  bundle:
    steps:
      - name: collect
        desc: "Pull every required artifact at once"
        parallel:
          - name: tests
            func: shell
            do: echo "TEST_REPORT=passed-510-510"
            outputs:
              ref: '{{ result.output | regexFind "TEST_REPORT=(.+)" }}'
          - name: sbom
            func: shell
            do: echo "SBOM=312-packages"
            outputs:
              ref: '{{ result.output | regexFind "SBOM=(.+)" }}'
          - name: approvals
            func: shell
            do: echo "APPROVALS=2-of-2"
            outputs:
              ref: '{{ result.output | regexFind "APPROVALS=(.+)" }}'

      - name: hash
        func: shell
        do: echo "sha256 — tests={{ steps.tests.ref }} sbom={{ steps.sbom.ref }} approvals={{ steps.approvals.ref }}"
        outputs:
          digest: "{{ result.output }}"

      - name: manifest
        func: render
        args:
          template: |
            release: {{ vars.release }}
            tests: {{ steps.tests.ref }}
            sbom: {{ steps.sbom.ref }}
            approvals: {{ steps.approvals.ref }}
        outputs:
          doc: "{{ result.output }}"

      - name: assemble
        func: shell
        do: echo "evidence-{{ vars.release }}.tar.gz sealed from 3 hashed artifacts + manifest"

The parallel: block gathers all three artifacts at once and names each one's reference. The hash step reads those references and produces a digest; manifest renders a human-readable index from the same outputs; assemble seals the bundle. Every artifact in the bundle is traceable back to the run that produced it.

Collect at release time, not audit time

The whole trick is when this runs. Bolt it onto the tail of your release workflow and the bundle is a byproduct of shipping, not a project you start when the auditor emails:

- name: collect
  parallel:
    - name: tests
      func: shell
      do: echo "TEST_REPORT=passed-510-510"
    - name: sbom
      func: shell
      do: echo "SBOM=312-packages"
    - name: approvals
      func: shell
      do: echo "APPROVALS=2-of-2"

Collecting in parallel keeps it cheap enough that there's no reason not to do it on every release. Six weeks later, "show me evidence for 2026.5.0" is a file you already have, not a day you have to spend. Parallel details: Composition.

A manifest, not a pile of screenshots

The render step turns the collected references into a structured manifest:

- name: manifest
  func: render
  args:
    template: |
      release: {{ vars.release }}
      tests: {{ steps.tests.ref }}
      sbom: {{ steps.sbom.ref }}
      approvals: {{ steps.approvals.ref }}

That's the index an auditor reads first — what's in the bundle, for which release, with the hashed reference to each item. It beats a folder of PNGs because it's generated from the run's outputs, so it can't claim an artifact the bundle doesn't contain. See the render function.

What you actually gained

ConcernAudit-time scrambleEvidence bundle
When evidence is gatheredweeks later, by handat release, automatically
Collection costa day of greppingone parallel step
Verifiabilityscreenshotshashed artifacts
Indexa folder namea rendered manifest
Coverage"did we get everything?"the workflow is the checklist

This isn't a substitute for a compliance platform that stores and retains evidence under policy — if you're regulated, you still need somewhere durable to put the bundle. But it removes the worst part of an audit: re-assembling proof of things you already did, from systems that have moved on.

Where to go next

Bolt this onto your release: one parallel collect step turns your next audit from a day of grepping into handing over a file.

#SECURITY#COMPLIANCE#AUDIT#EVIDENCE
Try it in two minutes — one binary, no signup.
curl -fsSL https://orchstep.dev/install.sh | sh

RELATED — ENTERPRISE AUTOMATION