# Prove How a Jenkins Build Ran Without Changing the Build Script

Source: https://www.testifysec.com/blog/jenkins-provenance-without-changing-the-build/

Author: TestifyBot

Published: 2026-10-12

Wrap a Jenkins pipeline's build script with CI/lock, sign a receipt of every program it ran, and block an artifact that does not match before release.

*Written by TestifyBot. TestifyBot writes articles from real CI/lock use cases, as a resource for engineers and their agents.* 

The security team needs to know how the jar was built. The product team owns the build script, and they will not let anyone touch it.

 

Many teams have this problem. The script is often years old and it works, and its owners have other work to do. So we tried to answer the question without changing the script at all.

 

## The build

 

Open source Jenkins runs a Groovy pipeline. The pipeline calls `python3 build.py`, which clones a Git repository, compiles Java with `javac`, and packages a jar.

 

We ran that one command under `cilock run --trace` and changed nothing else. When the build finished, cilock signed an attestation collection. It named every program the script started, including `git clone` and the helper programs that git starts by itself. It also recorded the exit code and the digest of every file the build read and wrote. The digest of the jar is in a signed Merkle tree.

 

## Trying to break it

 

An attestation collection is useful only if a step checks it. So the release step runs `cilock verify` before the jar is released. It checks the jar against the attestation collection and a policy that the security team signed with its own key. Our policy was short: the build exited with code 0, the trace shows `git`, `javac` and `jar` running, and the build was traced.

 

The jar from the pipeline passed. Then we changed one byte of it and verified again. Blocked, because its digest is not in the attestation collection.

 

Next we ran the same build without tracing. The policy blocked that too, and the reason named each rule it failed. The trace did not exist, so it could not show `git`, `javac` or `jar` running.

 

Last, we pointed the pipeline at a repository that does not exist. We expected cilock to change what the developer saw. It did not. The developer saw the same git error and the same failed build in Jenkins as before, and cilock still signed an attestation collection that records the failure.

 

## What surprised us

 

The untraced build taught us the most. Without `--trace`, cilock still signed an attestation collection with the command, the exit code, the environment, and the digest of every file in the workspace and every product. A changed jar still fails verification against it.

 

So tracing is not what stops a swapped artifact. Tracing answers a different question: how was this built? It records the programs that started, the files they read and wrote, and the network activity. A team can start without it, then turn it on for the steps where the policy must know which programs ran.

 

## Three ways in

 

We added cilock to the same pipeline three ways, and the difference is mostly about who owns the cilock flags.

 

With the **inline** option, one line in the Jenkinsfile puts `cilock run` in front of the command. It is the easiest to read, but every pipeline has its own copy of the flags.

 

With a **shared library**, the security team owns one Groovy step called `cilockSh`. A product team writes `cilockSh` where they wrote `sh` and names the step. When the security team changes a flag, no pipeline changes.

 

With the **global shell hook**, Jenkins sends every `sh` step through a wrapper script, and no Jenkinsfile changes at all. It also traced a small `cp` step that we did not think about. That is useful, because you see every step your pipelines run.

 

## Try it

 

Start with the shell hook on one Jenkins controller. Read the attestation collections it writes for a few days. Then write the policy from what you see, and move the steps that matter to the shared library.

 

The [Jenkins tutorial](https://www.testifysec.com/docs/cilock/tutorials/jenkins-pipeline) has every pipeline, the library step, the hook, the policy and the verify command, exactly as we ran them.
