CI/CD Integration¶
ZIRAN integrates into your CI/CD pipeline to block insecure agents from reaching production. It provides quality gates, policy enforcement, SARIF output, and GitHub Actions annotations.
GitHub Action¶
Add ZIRAN to any GitHub Actions workflow:
# .github/workflows/security.yml
name: Agent Security Scan
on: [push, pull_request]
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run ZIRAN scan
uses: taoq-ai/ziran@v0
with:
target: target.yaml
coverage: standard
sarif: results.sarif
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
This runs a scan on every push and PR, uploads findings to GitHub's Security tab, and fails the build if critical vulnerabilities are found.
Quality Gate¶
The quality gate evaluates scan results against configurable thresholds:
ziran ci results.json --gate-config gate.yaml
Gate Configuration¶
# gate.yaml
min_trust_score: 0.7 # Minimum trust score (0.0-1.0)
max_critical_findings: 0 # Zero tolerance for critical
fail_on_policy_violation: true # Fail if policy rules violated
severity_thresholds:
critical: 0 # Max allowed critical findings
high: 3 # Max allowed high findings
medium: 10 # Max allowed medium findings
low: -1 # Unlimited low findings (-1)
require_owasp_coverage: # Required OWASP categories
- LLM01
- LLM06
- LLM07
Exit Codes¶
| Code | Meaning |
|---|---|
| 0 | Gate passed — safe to deploy |
| 1 | Gate failed — vulnerabilities exceed thresholds |
| 2 | Configuration error |
Suppressing Accepted Findings¶
A finding your team has reviewed and accepted can be recorded in a committed file so the gate stops failing on it, while any change to that finding fails the gate again.
ziran ci loads .ziran/suppressions.yaml from the working directory when it exists.
Pass --suppressions PATH to use another file. A file that cannot be read or validated
stops the run with Error loading suppressions: ... and exit code 1. A path given to
--suppressions that does not exist is a usage error (exit code 2).
# .ziran/suppressions.yaml
version: 1
entries:
- fingerprint: a8fe72c13edad12d1df1d032a83ebe7a5b0320e125cfc28de5321a0421117f6e
content_hash: c80a79447eb700e60463275c8d4d21da825727c816ab30ad956303ae64133eae
reason: "Prompt-injection echo accepted: output is sandboxed (SEC-123)"
added_by: security-team
expires: 2026-12-31 # optional; the entry is valid through this date
fingerprint and content_hash are 64-character lowercase hex values printed by ziran ci.
reason and added_by are required. Unknown keys are rejected.
Fingerprint and content hash¶
Each finding the gate counts gets two values:
- fingerprint: what the finding is. For a successful attack it is the target agent, the vector id and the category (the same fingerprint the web UI findings page uses). For a dangerous tool chain it is the target agent and the vulnerability type.
- content hash: what the finding contains. For an attack it covers the severity and the category. For a chain it covers the tools, in order, and the risk level.
Evidence (including tool_calls), agent responses, prompts and names are never hashed, because
they change between runs.
| What changed since the entry was written | State | Gate |
|---|---|---|
| Nothing material (only evidence, response, prompt or name) | suppressed | not counted |
| Attack severity, chain tools or chain risk level | regressed | counted, plus a suppression_regressed violation |
| Attack category, vector id, chain vulnerability type or target agent | new | counted |
The severity thresholds and max_critical_findings count only new and regressed findings.
A suppression_regressed violation names the entry's fingerprint and the new content hash.
Several chains with the same vulnerability type share one fingerprint; add one entry per
accepted content hash.
Expiry¶
An entry with expires in the past no longer suppresses anything, and each such entry adds a
suppression_expired violation (whether or not it still matches a finding). Renew the date
or remove the entry.
Policy rule with a suppressions file¶
Without a file, fail_on_policy_violation fails whenever the result is marked vulnerable.
With a file, policy_violation fires only if at least one of these holds:
- a finding is not suppressed;
- a critical attack path ends at a node that is not backed by a suppressed finding (the
vector id of a suppressed attack, or the composition node or vulnerability type of a
suppressed chain) and is not the tool path of a suppressed chain (the
ziran analyze-tracesshape); - a phase reported a vulnerability id that is not backed in the same way.
Critical paths that end at a data-source node (such as sensitive_data) can never be backed,
so such results still fail policy_violation even with every finding suppressed. If that is
acceptable for your agent, set fail_on_policy_violation: false in the gate config; the
severity thresholds still apply. With a file loaded, the rule is also stricter than before in
one case: any unsuppressed finding fails it, including a non-critical chain.
Bootstrapping the file¶
Fingerprints are printed only when a suppressions file is loaded. Start with an empty file:
version: 1
entries: []
Then run the gate and copy the printed values into entries for the findings you accept. Output from a scan result with two successful attacks, one critical chain and one critical path:
Unsuppressed findings (copy fingerprint/content_hash into the suppressions file to accept):
new attack v1 [critical] fingerprint=a8fe72c13edad12d1df1d032a83ebe7a5b0320e125cfc28de5321a0421117f6e content_hash=c80a79447eb700e60463275c8d4d21da825727c816ab30ad956303ae64133eae
new attack v2 [medium] fingerprint=c3b1e7f24e5b6f3695e4e53156dd95d62eff493db6db541352c4ae532905812e content_hash=4d94a27a271448b34308f94a1a936f35250691b0b6aaf611d99b667882fd7eef
new chain data_exfiltration [critical] fingerprint=64e139a4957bcaaff763cacd45656f8a63a14d6460be47291ba6f443d92d868f content_hash=652b01e461d943c024616e2cfbf55b5126ecd04fd30cec0d3cd456fcea867b7e
With all three accepted, the same result passes:
│ PASSED Trust: 0.30 | Findings: 0 (C:0 H:0 M:0 L:0) | New: 0 Suppressed: 3 Regressed: 0 │
Outputs¶
When a file is loaded:
- the summary line ends with
| Suppressions: new N, suppressed S, regressed R; $GITHUB_OUTPUTgetsnew_findings,suppressed_findingsandregressed_findings(the composite GitHub Action does not re-export these yet; it does auto-load the file, since it runsziran ciin the workspace root);- the step summary gets a
### Suppressionstable, and suppressed attacks are left out of "Vulnerabilities Found"; - no annotation is emitted for a suppressed attack;
- in SARIF, a suppressed attack result carries
"suppressions": [{"kind": "external", "justification": "<reason>"}].
Without a file, all outputs are unchanged.
Policy Engine¶
For more complex compliance rules, use the policy engine:
ziran policy results.json --policy policy.yaml
Policy Configuration¶
# policy.yaml
id: production-policy
name: Production Security Policy
version: "1.0"
description: Minimum security requirements for production agents
rules:
- rule_type: min_trust_score
description: Agent must achieve minimum trust score
severity: critical
parameters:
threshold: 0.7
- rule_type: max_critical_vulnerabilities
description: No critical vulnerabilities allowed
severity: critical
parameters:
threshold: 0
- rule_type: max_high_vulnerabilities
description: Limited high-severity findings
severity: high
parameters:
threshold: 5
- rule_type: required_owasp
description: Must test high-priority OWASP categories
severity: high
parameters:
categories: [LLM01, LLM06, LLM07, LLM08]
- rule_type: max_critical_paths
description: No critical tool chain paths
severity: critical
parameters:
threshold: 0
- rule_type: forbidden_findings
description: Block specific finding types
severity: critical
parameters:
finding_ids: [system_prompt_leaked, credentials_exposed]
Available Rule Types¶
| Rule Type | Description | Parameters |
|---|---|---|
min_trust_score |
Minimum overall trust score | threshold (0.0–1.0) |
max_critical_vulnerabilities |
Max critical findings | threshold (int) |
max_high_vulnerabilities |
Max high findings | threshold (int) |
max_total_vulnerabilities |
Max total findings | threshold (int) |
required_categories |
Attack categories that must be tested | categories (list) |
required_owasp |
OWASP categories that must be tested | categories (list) |
forbidden_findings |
Specific findings that fail the gate | finding_ids (list) |
max_critical_paths |
Max dangerous tool chain paths | threshold (int) |
SARIF Output¶
Generate SARIF v2.1.0 reports for integration with GitHub Security, Azure DevOps, and other code scanning tools:
ziran ci results.json --sarif results.sarif
Upload to GitHub's Security tab:
- uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: results.sarif
Findings appear as security alerts with:
- Severity level
- OWASP category mapping
- Remediation guidance
- Link to attack vector documentation
GitHub Actions Features¶
Annotations¶
ZIRAN emits GitHub Actions annotations for findings:
ziran ci results.json --github-annotations
This places warning/error annotations directly on PR diffs.
Step Summary¶
ziran ci results.json --github-summary
Writes a Markdown summary to $GITHUB_STEP_SUMMARY showing:
- Pass/fail status
- Trust score
- Finding counts by severity
- Top tool chain risks
Full Pipeline Example¶
name: Agent Security
on:
push:
branches: [main]
pull_request:
jobs:
scan:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install ZIRAN
run: pip install ziran[all]
- name: Run scan
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
run: |
ziran scan --target target.yaml \
--coverage standard \
--output results/
- name: Quality gate
run: |
ziran ci results/campaign_*_report.json \
--gate-config gate.yaml \
--policy policy.yaml \
--sarif results.sarif \
--github-annotations \
--github-summary
- name: Upload SARIF
uses: github/codeql-action/upload-sarif@v3
if: always()
with:
sarif_file: results.sarif
See Also¶
- Quality Gate Config Reference — CLI flags for
ziran ci - Policy Engine — OWASP-based policy rules