🤖 Tutorial: Scan for Vulnerabilities in CI
Wire cpeskills into a CI job so that a build fails when any component is critical-risk. The job builds an SBOM, scores every component against NVD, filters to critical priority, and exits non-zero if any remain.
Goal
A single Go program that returns exit code 1 when the project contains at least one critical-priority vulnerability, and 0 otherwise. Drop it into GitHub Actions / GitLab CI as a gate.
Prerequisites
- Go 1.25+
go get github.com/scagogogo/cpe-skills- The project's
go.modaccessible at build time
Steps
1. Build the SBOM and pull NVD
package main
import (
"fmt"
"os"
cpeskills "github.com/scagogogo/cpe-skills"
)
func main() {
content, _ := os.ReadFile("go.mod")
sbom, err := cpeskills.BuildSBOMFromManifest("go.mod", string(content), "ci-build")
if err != nil {
fmt.Printf("build sbom: %v\n", err)
os.Exit(2)
}
nvdData, err := cpeskills.DownloadAllNVDData(nil)
if err != nil {
fmt.Printf("download nvd: %v\n", err)
os.Exit(2)
}2. Score every component
ScoreComponents walks the SBOM, looks up CVEs for each component's CPE via FindCVEsForCPE, and runs the default risk scorer (CVSS + EPSS + KEV + reachability) per component. Components without a CPE are scored zero. Returns []*RiskScore.
// Components must carry a CPE for the scorer to find CVEs.
// Stamp any you know explicitly here (see the build-sbom tutorial).
scores := cpeskills.ScoreComponents(componentsOf(sbom), nvdData)
cpeskills.SortByRisk(scores)3. Filter to critical and gate
critical := cpeskills.FilterByPriority(scores, cpeskills.RiskPriorityCritical)
for _, s := range critical {
fmt.Printf("BLOCKING: %s priority=%s score=%.1f\n",
s.ComponentID, s.Priority, s.OverallScore)
}
if len(critical) > 0 {
os.Exit(1)
}
fmt.Println("no critical vulnerabilities, build passes")
}
// componentsOf flattens the SBOM into a component slice for the scorer.
// Use the slice your BuildSBOMFromManifest produced or track components as you add them.
func componentsOf(sbom *cpeskills.SBOM) []*cpeskills.SBOMComponent {
var out []*cpeskills.SBOMComponent
for i := 0; i < sbom.ComponentCount(); i++ {
// In practice, keep the []*SBOMComponent returned/added while building the SBOM.
}
return out
}The
componentsOfhelper is a placeholder — keep the[]*SBOMComponentslice you assemble while building the SBOM and pass it directly.ScoreComponentsdoes its own CVE lookup, so you do not need to enrich findings first.
CI pipeline
CI timing
The pipeline above runs on every push. The sequence diagram shows where the slowest operations sit and when the pass/fail decision is made:
Expected output (failing run)
BLOCKING: pkg:golang/github.com/apache/log4j@v2.14.0 priority=critical score=9.4
##vso[task.complete result=Failed;]critical vulnerabilities found
exit status 1Notes
- Run this job on a schedule (e.g. nightly), not only on PRs — newly disclosed CVEs affect unchanged code.
ScoreComponentshonors the KEV flag indeterminePriority: a KEV-listed CVE with score ≥ 7.0 jumps straight tocritical. To get the KEV flag, enrich findings yourself withNewDefaultRiskScorer().Scoreinstead ofScoreComponents(the latter builds minimal findings without EPSS/KEV).- Cache the NVD download between runs using
NewFileStorage(see the Storage Strategies tutorial) to keep CI fast.
Recap
You built an SBOM, scored it, filtered to critical, and turned the result into a pass/fail gate. Combined with the storage and batch tutorials, this scales to large monorepos.