ESP CI Runner
Reference implementation for running ESP compliance policies in CI/CD pipelines with cryptographically signed attestations.
Overview
The ESP CI Runner is a standalone tool that:
- Executes security scanning tools within a constrained, trusted environment
- Evaluates scan results against declarative compliance policies
- Produces cryptographically signed attestations with verifiable provenance
- Generates signed artifacts (SBOMs) with integrity guarantees
- Collects signed evidence that ties provenance to specific actions and commands
- Gates CI/CD pipelines based on policy outcomes
- Supports SSDF (Secure Software Development Framework) compliance evidence generation
Design Philosophy
- Policy is a first-class executable construct
- Tools operate only withβ¦
ESP CI Runner
Reference implementation for running ESP compliance policies in CI/CD pipelines with cryptographically signed attestations.
Overview
The ESP CI Runner is a standalone tool that:
- Executes security scanning tools within a constrained, trusted environment
- Evaluates scan results against declarative compliance policies
- Produces cryptographically signed attestations with verifiable provenance
- Generates signed artifacts (SBOMs) with integrity guarantees
- Collects signed evidence that ties provenance to specific actions and commands
- Gates CI/CD pipelines based on policy outcomes
- Supports SSDF (Secure Software Development Framework) compliance evidence generation
Design Philosophy
- Policy is a first-class executable construct
- Tools operate only within explicitly defined contracts
- Evidence is produced by execution, not inferred post hoc
- Outcomes are deterministic and explainable
- Attestations are cryptographically signed for provenance and integrity
- Recordkeeping is downstream and integrable, not a trust anchor
Signed Attestation Model
The ESP CI Runner produces signed attestations that provide an unbroken chain of evidence:
Policy (signed hash) β Execution (signed summary) β Artifacts (signed references) β Signature (OIDC identity)
| Element | Description | SSDF Alignment |
|---|---|---|
| Policy Provenance | SHA-256 hash of the .esp policy file executed | PS.1.1 - Protect all forms of code |
| Execution Evidence | CTN types executed, duration, pass/fail counts | PW.7.2 - Review code for vulnerabilities |
| Artifact References | External artifacts (SBOMs) with SHA-256 digests | PS.3.2 - Protect integrity of releases |
| Findings Hash | SHA-256 of detailed findings (CUI kept separate) | PW.4.1 - Analyze code for vulnerabilities |
| Cryptographic Signature | Sigstore OIDC (keyless in CI) or local key | PS.2.1 - Protect integrity verification |
Attestation Structure
SignedAttestation
βββ payload
β βββ policy # Policy identity
β β βββ policy_id # From META block
β β βββ policy_hash # SHA256 of .esp file
β β βββ source # File path
β βββ agent # Agent identity
β β βββ name # "esp-ci-agent"
β β βββ version # Semantic version
β βββ execution # Execution summary
β β βββ ctn_types # ["sast_scan", "sbom_scan"]
β β βββ duration_ms # Total duration
β β βββ criteria_count
β β βββ criteria_passed
β βββ outcome # "pass" or "fail"
β βββ findings_hash # SHA256 of findings
β βββ evidence_hash # SHA256 of evidence
β βββ artifacts # External artifact references
β βββ timestamp # RFC 3339
βββ signature
βββ algorithm # "sigstore-oidc" or "cosign-key"
βββ value # Base64-encoded signature
βββ key_id # OIDC identity or key path
Evidence Output
Quick Start
# Build
cargo build -p agent --release
# Run single policy
cargo run -p agent -- esp/sast-no-critical.esp
# Run all policies in directory
cargo run -p agent -- esp/
# Run with signing (requires cosign)
cargo run -p agent -- --sign esp/sbom-basic.esp
Local Testing
Prerequisites:
- Rust toolchain (1.70+)
- Semgrep (
pip install semgrep) - Syft (
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin) - Cosign (optional, for signing:
go install github.com/sigstore/cosign/v2/cmd/cosign@latest)
Run tests:
# Unit tests
cargo test -p contract_kit -p agent
# Run SAST scan locally
cargo run -p agent -- esp/sast-no-critical.esp
# Run SBOM generation locally
cargo run -p agent -- esp/sbom-basic.esp
# View generated SBOM
cat sbom.cdx.json
# Run with local key signing
cosign generate-key-pair # Creates cosign.key and cosign.pub
cargo run -p agent -- --sign esp/sbom-basic.esp
CI/CD Integration
The runner integrates with CI/CD pipelines via standard exit codes:
| Code | Meaning | Pipeline Behavior |
|---|---|---|
| 0 | All policies passed | Continue |
| 1 | One or more policies failed | Fail |
| 2 | Execution error | Fail |
GitHub Actions Example
name: CI
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
CARGO_TERM_COLOR: always
jobs:
ci:
name: CI Checks
runs-on: ubuntu-latest
permissions:
id-token: write # Required for keyless signing
contents: read
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: dtolnay/rust-toolchain@stable
with:
components: rustfmt, clippy
- name: Cache cargo
uses: actions/cache@v4
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}-${{ hashFiles('rust-toolchain.toml') }}
restore-keys: |
${{ runner.os }}-cargo-
- name: Format Check
run: cargo fmt -p contract_kit -p agent -- --check
- name: Clippy
run: |
cargo clippy -p contract_kit -p agent --all-targets --all-features -- \
-D warnings \
-D clippy::unwrap_used \
-D clippy::expect_used \
-D clippy::panic \
-D clippy::indexing_slicing
- name: Build
run: cargo build -p contract_kit -p agent --all-features
- name: Test
run: cargo test -p contract_kit -p agent --all-features
- name: Install cargo-audit
run: cargo install cargo-audit --locked
- name: Security Audit
run: cargo audit
# ESP Compliance Scanning
uses: sigstore/cosign-installer@v3
- name: Install Syft
run: |
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
- name: Install Semgrep
run: |
sudo pip install semgrep --break-system-packages --ignore-installed
semgrep --version
- name: ESP Compliance Scan (Signed)
run: cargo run -p agent --release -- --sign esp/
continue-on-error: true
- name: Display SBOM
if: always()
run: cat sbom.cdx.json
Tool Path Whitelisting
Important: The ESP CI Runner enforces strict command whitelisting as a security measure. When installing tools in a pipeline job (rather than using a containerized runner), you must ensure the installation paths are whitelisted.
Default Whitelisted Paths
Semgrep:
const SEMGREP_PATHS: &[&str] = &[
"/usr/local/bin/semgrep",
"/usr/bin/semgrep",
"/home/runner/.local/bin/semgrep", // GitHub Actions pip install
"semgrep",
];
Syft:
const SYFT_PATHS: &[&str] = &[
"/usr/local/bin/syft",
"/usr/bin/syft",
"syft",
];
If Tools Install to Different Paths
If your CI environment installs tools to non-whitelisted paths, you must either:
Install to a whitelisted path (recommended):
# Syft to /usr/local/bin
curl -sSfL https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
# Semgrep - add ~/.local/bin to PATH
pip install semgrep
echo "$HOME/.local/bin" >> $GITHUB_PATH
Add the path to the whitelist in the command executor source code 1.
Use a containerized runner with pre-installed tools at known paths
This whitelisting is a security feature β arbitrary command execution is explicitly prevented.
Constrained Execution Model
All tool execution occurs through ESPβs constrained execution model:
| Constraint | Enforcement |
|---|---|
| Whitelisted commands only | SystemCommandExecutor with explicit allow list |
| Timeout enforcement | Per-tool configurable timeouts |
| No shell expansion | Arguments passed directly, no shell invocation |
| Deterministic evaluation | Same policy + same code = same result |
| Contract-bound execution | Only registered CTN types execute |
Trust Model
- Tools are executed by ESP β not pre-run by the pipeline
- Collectors invoke tools via
SystemCommandExecutorwith whitelisted commands - Tool outputs are collected and parsed within the trust boundary
- Attestations reflect what ESP observed β not external claims
- Signatures bind identity to execution β OIDC in CI, keypair locally
This ensures the compliance evidence chain is:
Policy Source β ESP Compiler β Validated AST β Constrained Execution β Signed Attestation
Supported CTN Types
| Tool | CTN Type | SSDF Practice |
|---|---|---|
| Semgrep | sast_scan | PW.7.2 |
| Syft | sbom_scan | PS.3.2 |
Contract: sast_scan
Static Application Security Testing via Semgrep.
Object Fields:
| Field | Type | Required | Description |
|---|---|---|---|
target | string | Yes | Path to scan (file or directory) |
ruleset | string | No | Semgrep config (default: auto) |
timeout | int | No | Timeout in seconds (default: 300) |
exclude | string | No | Comma-separated exclude patterns |
State Fields:
| Field | Type | Operations | Description |
|---|---|---|---|
critical_count | int | =, !=, <, <=, >, >= | Critical severity findings |
high_count | int | =, !=, <, <=, >, >= | High severity findings |
medium_count | int | =, !=, <, <=, >, >= | Medium severity findings |
low_count | int | =, !=, <, <=, >, >= | Low severity findings |
total_count | int | =, !=, <, <=, >, >= | Total findings |
Severity Mapping (Semgrep β ESP):
| Semgrep | ESP |
|---|---|
| CRITICAL | critical |
| ERROR | high |
| WARNING | medium |
| INFO | low |
SSDF Mapping: PW.7.2 - Review and/or analyze human-readable code to identify vulnerabilities
Example Policy:
META
esp_scan_id `sast-no-critical-findings`
platform `cicd`
criticality `high`
control_mapping `SSDF:PW.7.2`
title `No Critical or High SAST Findings`
META_END
DEF
VAR scan_target string `.`
VAR scan_ruleset string `auto`
OBJECT source_code
target VAR scan_target
ruleset VAR scan_ruleset
OBJECT_END
STATE no_critical
critical_count int = 0
STATE_END
STATE no_high
high_count int = 0
STATE_END
CRI AND
CTN sast_scan
TEST all all AND
STATE_REF no_critical
STATE_REF no_high
OBJECT_REF source_code
CTN_END
CRI_END
DEF_END
Contract: sbom_scan
Software Bill of Materials generation via Syft with NTIA minimum elements validation.
Object Fields:
| Field | Type | Required | Description |
|---|---|---|---|
target | string | Yes | Path to scan (file, directory, or image) |
format | string | No | Output format (default: cyclonedx-json) |
output_path | string | No | Path to write SBOM (default: sbom.json) |
timeout | int | No | Timeout in seconds (default: 300) |
State Fields:
| Field | Type | Operations | Description |
|---|---|---|---|
component_count | int | =, !=, <, <=, >, >= | Total components in SBOM |
missing_supplier_count | int | =, !=, <, <=, >, >= | Components missing supplier |
missing_version_count | int | =, !=, <, <=, >, >= | Components missing version |
missing_purl_count | int | =, !=, <, <=, >, >= | Components missing PURL |
has_author | bool | =, != | SBOM has author metadata |
has_timestamp | bool | =, != | SBOM has timestamp |
NTIA Minimum Elements Validation:
| NTIA Element | State Field | Validation |
|---|---|---|
| Component name | component_count | >= 1 |
| Component version | missing_version_count | = 0 |
| Component supplier | missing_supplier_count | = 0 |
| Unique identifier | missing_purl_count | = 0 |
| SBOM author | has_author | = true |
| Timestamp | has_timestamp | = true |
| Dependency relationships | (captured in SBOM structure) | β |
SSDF Mapping: PS.3.2 - Collect, safeguard, maintain, and share provenance data
Generated Artifact:
Example Policy (Basic SBOM Generation):
META
esp_scan_id `sbom-generated`
platform `cicd`
criticality `medium`
control_mapping `SSDF:PS.3.2`
title `SBOM Generated with Components`
META_END
DEF
VAR scan_target string `Cargo.lock`
VAR sbom_format string `cyclonedx-json`
VAR sbom_output string `sbom.cdx.json`
OBJECT project_sbom
target VAR scan_target
format VAR sbom_format
output_path VAR sbom_output
OBJECT_END
STATE has_components
component_count int >= 1
STATE_END
STATE has_metadata
has_author bool = true
has_timestamp bool = true
STATE_END
CRI AND
CTN sbom_scan
TEST all all AND
STATE_REF has_components
STATE_REF has_metadata
OBJECT_REF project_sbom
CTN_END
CRI_END
DEF_END
Example Policy (Strict NTIA Compliance):
META
esp_scan_id `sbom-ntia-compliant`
platform `cicd`
criticality `high`
control_mapping `SSDF:PS.3.2`
title `SBOM NTIA Minimum Elements Compliance`
META_END
DEF
VAR scan_target string `.`
OBJECT project_sbom
target VAR scan_target
OBJECT_END
STATE has_components
component_count int >= 1
STATE_END
STATE ntia_supplier_compliant
missing_supplier_count int = 0
STATE_END
STATE ntia_version_compliant
missing_version_count int = 0
STATE_END
STATE ntia_metadata_compliant
has_author bool = true
has_timestamp bool = true
STATE_END
CRI AND
CTN sbom_scan
TEST all all AND
STATE_REF has_components
STATE_REF ntia_supplier_compliant
STATE_REF ntia_version_compliant
STATE_REF ntia_metadata_compliant
OBJECT_REF project_sbom
CTN_END
CRI_END
DEF_END
Signing and Verification
Signing Modes
| Mode | Environment | Identity |
|---|---|---|
| Keyless | CI (GitHub Actions, GitLab CI) | OIDC token from CI provider |
| Key-based | Local development | Local keypair (cosign.key) |
Local Development Signing
# Generate a keypair (one-time)
cosign generate-key-pair
# Run with signing
cargo run -p agent -- --sign esp/sbom-basic.esp
Verification
# Extract payload and signature from attestation
jq -c '.payload' attestation.json > payload.json
jq -r '.signature.value' attestation.json > payload.sig
# Verify with local key
cosign verify-blob --key cosign.pub --signature payload.sig --insecure-ignore-tlog payload.json
# Verify keyless (CI-signed)
cosign verify-blob \
--certificate-identity "https://github.com/org/repo/.github/workflows/ci.yml@refs/heads/main" \
--certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
--signature payload.sig \
payload.json
Result Structure
The runner produces a PolicyExecutionResult containing:
PolicyExecutionResult
βββ outcome # Policy pass/fail with metadata
βββ findings # What policy checks failed
βββ evidence # Tool output and artifacts
βββ tree_passed # Final pass/fail respecting CRI logic
Embedded vs External Artifacts
| Mode | Description | Use Cases |
|---|---|---|
| Embedded | Tool output in result JSON | SAST findings, vulnerability scan |
| External | Artifact written to file, referenced by hash | SBOM, license manifests |
SSDF Practice Mapping
| SSDF Practice | CTN Type | Description |
|---|---|---|
| PW.7.2 | sast_scan | Review code for security vulnerabilities |
| PS.1.1 | (policy hash) | Protect all forms of code from tampering |
| PS.2.1 | (signature) | Protect integrity verification mechanisms |
| PS.3.2 | sbom_scan | Protect integrity of software releases |
Development
# Build
cargo build -p contract_kit -p agent --all-features
# Test
cargo test -p contract_kit -p agent --all-features
# Lint
cargo clippy -p contract_kit -p agent --all-targets --all-features -- \
-D warnings \
-D clippy::unwrap_used \
-D clippy::expect_used \
-D clippy::panic \
-D clippy::indexing_slicing
# Format
cargo fmt -p contract_kit -p agent
# Pre-commit checks
cargo fmt -p contract_kit -p agent -- --check && \
cargo clippy -p contract_kit -p agent --all-targets --all-features -- -D warnings && \
cargo test -p contract_kit -p agent --all-features
Project Structure
βββ agent/ # CLI binary
β βββ src/
β βββ main.rs # Entry point with signing
β βββ registry.rs # CTN strategy registration
βββ contract_kit/ # CTN implementations
β βββ src/
β βββ commands/ # Tool executors
β β βββ semgrep.rs # Semgrep path whitelist
β β βββ syft.rs # Syft path whitelist
β βββ contracts/ # CTN contracts
β β βββ sast_contracts.rs
β β βββ sbom_contracts.rs
β βββ collectors/ # Data collectors
β β βββ sast.rs # Semgrep execution
β β βββ sbom.rs # Syft execution + NTIA analysis
β βββ executors/ # Validators
β β βββ sast.rs
β β βββ sbom.rs
β βββ signing/ # Attestation signing
β βββ payload.rs
β βββ signature.rs
β βββ envelope.rs
β βββ backends/
β βββ cosign.rs
βββ esp/ # Example policies
βββ docs/ # Documentation and evidence
β βββ Evidence.png # Attestation output screenshot
β βββ Artifact.png # SBOM artifact screenshot
βββ test_code/ # Vulnerable code for testing
Adding a New CTN Type
Create command executor (commands/<tool>.rs):
- Define whitelisted binary paths
- Set appropriate timeout
- Export paths constant for collector use
Create contract (contracts/<type>_contracts.rs):
- Define object fields (inputs)
- Define state fields (outputs to validate)
- Set field mappings
Create collector (collectors/<type>.rs):
- Import whitelisted paths from command module
- Execute the tool via
SystemCommandExecutor - Parse output
- Return
CollectedDatawith fields and evidence
Create executor (executors/<type>.rs):
- Validate collected data against policy states
- Return
CtnExecutionResultwith evidence in details
Register in registry (agent/src/registry.rs):
let executor = commands::create_<tool>_command_executor();
let collector = collectors::<Type>Collector::new("id", executor);
let contract = contracts::create_<type>_contract();
registry.register_ctn_strategy(
Box::new(collector),
Box::new(executors::<Type>Executor::new(contract)),
)?;
License
Apache 2.0