Verify software before deploy and before expose
The pipeline moments that matter are before deploy and before expose. Attestd adds one deterministic check so production never receives a critical CVE version or a confirmed compromised package.
Most pipelines already run a scanner. The gap is not "no CVE data." It is a gate at the action boundary that can fail the job without a human reading the report.
- →CVSS-based gates produce noise. A score of 7.4 does not tell CI whether to exit 1 or continue.
- →Manifest scanners return issue lists. A merge bot or deploy job still needs a categorical fail condition.
- →Compromised packages often have no CVE at the moment CI pulls them. A CVE-only step returns clean.
- →Local artifact scanners need an image or SBOM first. A version pin in a lockfile is available earlier, before the build artifact exists.
Fail the job on categorical fields. Keep the exception path for humans, not the default path.
| Field | Meaning for this use case |
|---|---|
risk_state | Fail on critical by default. Optionally fail on high for production deploy jobs. |
actively_exploited | Strong fail condition even when risk_state is high rather than critical. Confirmed exploitation in the wild. |
supply_chain.compromised | Fail immediately. Confirmed malicious publish, independent of CVE coverage. |
fixed_version | Print in the job log so the fix PR has a concrete upgrade target. |
risk_factors | Human-readable reasons for the fail. Surface them with ::error:: or your CI annotation format. |
curl "https://api.attestd.io/v1/check?product=nginx&version=${VERSION}" \
-H "Authorization: Bearer ${ATTESTD_API_KEY}"- name: attestd risk check
env:
ATTESTD_API_KEY: ${{ secrets.ATTESTD_API_KEY }}
VERSION: ${{ env.NGINX_VERSION }}
run: |
RESULT=$(curl -sS "https://api.attestd.io/v1/check?product=nginx&version=$VERSION" \
-H "Authorization: Bearer $ATTESTD_API_KEY")
RISK=$(echo "$RESULT" | jq -r '.risk_state')
COMPROMISED=$(echo "$RESULT" | jq -r '.supply_chain.compromised // false')
FIXED=$(echo "$RESULT" | jq -r '.fixed_version // empty')
if [ "$COMPROMISED" = "true" ] || [ "$RISK" = "critical" ]; then
echo "::error::Deployment blocked (risk_state=$RISK compromised=$COMPROMISED)"
echo "$RESULT" | jq -r '.risk_factors // [] | join(", ")'
[ -n "$FIXED" ] && echo "Upgrade to: $FIXED"
exit 1
fi
echo "Risk check passed. Proceeding with deployment."- 01
Add a check before deploy
Call /v1/check for each pinned runtime dependency you care about: base images, language runtimes, and high-risk packages. Fail on critical or supply_chain.compromised before the deploy job runs.
- 02
Gate before expose
If the pipeline opens ingress, attaches a load balancer, or publishes a public endpoint, run the same check again at that step. Deploy and expose are separate action boundaries.
- 03
Annotate failures with the upgrade path
Log fixed_version and risk_factors in the job output so the next PR has a concrete version bump instead of a vague "security failed" message.
Nothing reaches production until software is verified.
When an actively exploited critical vulnerability or a confirmed supply chain compromise is detected, the CI step fails before deploy or before expose, with a clear error and the fixed version. No human review needed for the gate itself. Only for the exception.
[email protected] and [email protected] affected an estimated 10% of cloud environments within two hours of publication. A CI gate that checked those versions at install time would have blocked the compromised release before any downstream build pulled it.
The same supply chain pattern shows up in ViteVenom and JoyFill: packages with no CVE that return supply_chain.compromised: true. A CVE-only scanner would have greenlit the install.
For CI-adjacent tool comparisons, see Attestd vs Grype (artifact scan vs name@version lookup) and Attestd vs Sonatype Guide (deterministic risk_state vs CVSS advisory output in a gate).