mirror of
https://github.com/sourcegraph/sourcegraph.git
synced 2026-02-06 17:11:49 +00:00
ci: add trivy security scanning step (#25756)
This commit is contained in:
parent
99dc72f51e
commit
88ef1f64fe
3
.gitignore
vendored
3
.gitignore
vendored
@ -150,3 +150,6 @@ sitemap_query.db
|
||||
|
||||
# Lighthouse CI reports
|
||||
.lighthouseci/
|
||||
|
||||
# Trivy security vulnerability reports
|
||||
*-security-report.html
|
||||
|
||||
@ -4,5 +4,7 @@ fd 7.4.0
|
||||
shfmt 3.2.0
|
||||
shellcheck 0.7.1
|
||||
kubectl 1.17.3
|
||||
github-cli 1.8.0
|
||||
github-cli 2.0.0
|
||||
packer 1.6.6
|
||||
trivy 0.20.0
|
||||
|
||||
|
||||
5
dev/ci/trivy/README.md
Normal file
5
dev/ci/trivy/README.md
Normal file
@ -0,0 +1,5 @@
|
||||
# Trivy
|
||||
|
||||
The folder contains the scripts that our CI pipeline uses to run vulnerability scans with [Trivy](https://aquasecurity.github.io/trivy/).
|
||||
|
||||
See https://docs.sourcegraph.com/dev/background-information/continuous_integration for more information.
|
||||
123
dev/ci/trivy/trivy-artifact-html.tpl
Normal file
123
dev/ci/trivy/trivy-artifact-html.tpl
Normal file
@ -0,0 +1,123 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
{{- if . }}
|
||||
<style>
|
||||
* {
|
||||
font-family: Arial, Helvetica, sans-serif;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
}
|
||||
.group-header th {
|
||||
font-size: 200%;
|
||||
}
|
||||
.sub-header th {
|
||||
font-size: 150%;
|
||||
}
|
||||
table, th, td {
|
||||
border: 1px solid black;
|
||||
border-collapse: collapse;
|
||||
white-space: nowrap;
|
||||
padding: .3em;
|
||||
}
|
||||
table {
|
||||
margin: 0 auto;
|
||||
}
|
||||
.severity {
|
||||
text-align: center;
|
||||
font-weight: bold;
|
||||
color: #fafafa;
|
||||
}
|
||||
.severity-LOW .severity { background-color: #5fbb31; }
|
||||
.severity-MEDIUM .severity { background-color: #e9c600; }
|
||||
.severity-HIGH .severity { background-color: #ff8800; }
|
||||
.severity-CRITICAL .severity { background-color: #e40000; }
|
||||
.severity-UNKNOWN .severity { background-color: #747474; }
|
||||
.severity-LOW { background-color: #5fbb3160; }
|
||||
.severity-MEDIUM { background-color: #e9c60060; }
|
||||
.severity-HIGH { background-color: #ff880060; }
|
||||
.severity-CRITICAL { background-color: #e4000060; }
|
||||
.severity-UNKNOWN { background-color: #74747460; }
|
||||
table tr td:first-of-type {
|
||||
font-weight: bold;
|
||||
}
|
||||
.links a,
|
||||
.links[data-more-links=on] a {
|
||||
display: block;
|
||||
}
|
||||
.links[data-more-links=off] a:nth-of-type(1n+5) {
|
||||
display: none;
|
||||
}
|
||||
a.toggle-more-links { cursor: pointer; }
|
||||
</style>
|
||||
<title>{{- escapeXML ( index . 0 ).Target }} - Trivy Report - {{ getCurrentTime }}</title>
|
||||
<script>
|
||||
window.onload = function() {
|
||||
document.querySelectorAll('td.links').forEach(function(linkCell) {
|
||||
var links = [].concat.apply([], linkCell.querySelectorAll('a'));
|
||||
[].sort.apply(links, function(a, b) {
|
||||
return a.href > b.href ? 1 : -1;
|
||||
});
|
||||
links.forEach(function(link, idx) {
|
||||
if (links.length > 3 && 3 === idx) {
|
||||
var toggleLink = document.createElement('a');
|
||||
toggleLink.innerText = "Toggle more links";
|
||||
toggleLink.href = "#toggleMore";
|
||||
toggleLink.setAttribute("class", "toggle-more-links");
|
||||
linkCell.appendChild(toggleLink);
|
||||
}
|
||||
linkCell.appendChild(link);
|
||||
});
|
||||
});
|
||||
document.querySelectorAll('a.toggle-more-links').forEach(function(toggleLink) {
|
||||
toggleLink.onclick = function() {
|
||||
var expanded = toggleLink.parentElement.getAttribute("data-more-links");
|
||||
toggleLink.parentElement.setAttribute("data-more-links", "on" === expanded ? "off" : "on");
|
||||
return false;
|
||||
};
|
||||
});
|
||||
};
|
||||
</script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>{{- escapeXML ( index . 0 ).Target }} - Trivy Report - {{ getCurrentTime }}</h1>
|
||||
<table>
|
||||
{{- range . }}
|
||||
<tr class="group-header"><th colspan="6">{{ escapeXML .Type }}</th></tr>
|
||||
{{- if (eq (len .Vulnerabilities) 0) }}
|
||||
<tr><th colspan="6">No Vulnerabilities found</th></tr>
|
||||
{{- else }}
|
||||
<tr class="sub-header">
|
||||
<th>Package</th>
|
||||
<th>Vulnerability ID</th>
|
||||
<th>Severity</th>
|
||||
<th>Installed Version</th>
|
||||
<th>Fixed Version</th>
|
||||
<th>Links</th>
|
||||
</tr>
|
||||
{{- range .Vulnerabilities }}
|
||||
<tr class="severity-{{ escapeXML .Vulnerability.Severity }}">
|
||||
<td class="pkg-name">{{ escapeXML .PkgName }}</td>
|
||||
<td>{{ escapeXML .VulnerabilityID }}</td>
|
||||
<td class="severity">{{ escapeXML .Vulnerability.Severity }}</td>
|
||||
<td class="pkg-version">{{ escapeXML .InstalledVersion }}</td>
|
||||
<td>{{ escapeXML .FixedVersion }}</td>
|
||||
<td class="links" data-more-links="off">
|
||||
{{- range .Vulnerability.References }}
|
||||
<a href={{ escapeXML . | printf "%q" }}>{{ escapeXML . }}</a>
|
||||
{{- end }}
|
||||
</td>
|
||||
</tr>
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
</table>
|
||||
{{- else }}
|
||||
</head>
|
||||
<body>
|
||||
<h1>Trivy Returned Empty Report</h1>
|
||||
{{- end }}
|
||||
</body>
|
||||
</html>
|
||||
82
dev/ci/trivy/trivy-scan-high-critical.sh
Executable file
82
dev/ci/trivy/trivy-scan-high-critical.sh
Executable file
@ -0,0 +1,82 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"/../../..
|
||||
set -euo pipefail
|
||||
|
||||
export GITHUB_TOKEN="${GH_TOKEN}"
|
||||
|
||||
# do not move this "set -x" above the GITHUB_TOKEN
|
||||
# env var alias above, we don't want this to leak
|
||||
# inside of CI's logs
|
||||
set -x
|
||||
|
||||
# This is the special exit code that we tell trivy to use
|
||||
# if finds a vulnerability
|
||||
|
||||
trivy_scan() {
|
||||
local templateFile="$1"
|
||||
local outputFile="$2"
|
||||
local target="$3"
|
||||
|
||||
TRIVY_ARGS=(
|
||||
# fail the step if there is a vulnerability
|
||||
"--exit-code"
|
||||
"${VULNERABILITY_EXIT_CODE}"
|
||||
|
||||
# ignore issues that we can't fix
|
||||
"--ignore-unfixed"
|
||||
|
||||
# we'll only take action on higher CVEs
|
||||
"--severity"
|
||||
"HIGH,CRITICAL"
|
||||
|
||||
# tell trivy to dump its output to an HTML file
|
||||
"--format"
|
||||
"template"
|
||||
|
||||
# use the custom "trivy-html" template that we have in this folder
|
||||
"--template"
|
||||
"@${templateFile}"
|
||||
|
||||
# dump the HTML output to a file named "outputFile"
|
||||
"--output"
|
||||
"${outputFile}"
|
||||
|
||||
# scan the docker image named "target"
|
||||
"${target}"
|
||||
)
|
||||
|
||||
trivy image "${TRIVY_ARGS[@]}"
|
||||
}
|
||||
|
||||
upload_annotation() {
|
||||
local path="$1"
|
||||
local imageName="$2"
|
||||
|
||||
local file
|
||||
file="$(basename "${path}")"
|
||||
|
||||
cat <<EOF | buildkite-agent annotate --style warning --context "Docker image security scan" --append
|
||||
- **${imageName}** high/critical CVE(s): [${file}](artifact://${file})
|
||||
EOF
|
||||
|
||||
echo "High or critical severity CVEs were discovered in ${IMAGE}. Please read the buildkite annotation for more info."
|
||||
}
|
||||
|
||||
ARTIFACT_FILE="$(pwd)/${IMAGE}-security-report.html"
|
||||
trivy_scan "./dev/ci/trivy/trivy-artifact-html.tpl" "${ARTIFACT_FILE}" "${IMAGE}" || exitCode="$?"
|
||||
case "${exitCode:-"0"}" in
|
||||
0)
|
||||
# no vulnerabilities were found
|
||||
exit 0
|
||||
;;
|
||||
"${VULNERABILITY_EXIT_CODE}")
|
||||
# we found vulnerabilities - upload the annotation
|
||||
upload_annotation "${ARTIFACT_FILE}" "${IMAGE}"
|
||||
exit "${VULNERABILITY_EXIT_CODE}"
|
||||
;;
|
||||
*)
|
||||
# some other kind of error occurred
|
||||
exit $exitCode
|
||||
;;
|
||||
esac
|
||||
@ -126,6 +126,23 @@ The term _secret_ refers to authentication credentials like passwords, API keys,
|
||||
- use an environment variable name with one of the following suffixes to ensure it gets redacted in the logs: `*_PASSWORD, *_SECRET, *_TOKEN, *_ACCESS_KEY, *_SECRET_KEY, *_CREDENTIALS`
|
||||
- while environment variables can be assigned when declaring steps, they should never be used for secrets, because they won't get redacted, even if they match one of the above patterns.
|
||||
|
||||
### Vulnerability Scanning
|
||||
|
||||
Our CI pipeline scans uses [Trivy](https://aquasecurity.github.io/trivy/) to scan our Docker images for security vulnerabilities.
|
||||
|
||||
Trivy will perform scans upon commits to the following branches:
|
||||
|
||||
1. `main`
|
||||
2. branches prefixed by `main-dry-run/`
|
||||
3. branches prefixed by `docker-images-patch/$IMAGE` (where only a single image is built)
|
||||
|
||||
If there are any `HIGH` or `CRITICAL` severities in a Docker image that have a known fix:
|
||||
|
||||
1. The CI pipeline will create an annotation that contains links to reports that describe the vulnerabilities
|
||||
2. The Trivy scanning step will [soft fail](https://buildkite.com/docs/pipelines/command-step#soft-fail-attributes). Note that soft failures **do not fail builds or block deployments**. They simply highlight the failing step for further analysis.
|
||||
|
||||
> NOTE: Our vulnerability management process (including this workflow) is under active development and in its early stages. All of the above is subject to change. See [https://github.com/sourcegraph/sourcegraph/pull/25756](https://github.com/sourcegraph/sourcegraph/pull/25756) for more context.
|
||||
|
||||
## GitHub Actions
|
||||
|
||||
### Third-Party Licenses
|
||||
|
||||
@ -487,6 +487,37 @@ func buildCandidateDockerImage(app, version, tag string) operations.Operation {
|
||||
}
|
||||
}
|
||||
|
||||
// Ask trivy, a security scanning tool, to scan the candidate image
|
||||
// specified by "app" and "tag".
|
||||
func trivyScanCandidateImage(app, tag string) operations.Operation {
|
||||
image := images.DevRegistryImage(app, tag)
|
||||
|
||||
// This is the special exit code that we tell trivy to use
|
||||
// if it finds a vulnerability. This is also used to soft-fail
|
||||
// this step.
|
||||
vulnerabilityExitCode := 27
|
||||
|
||||
return func(pipeline *bk.Pipeline) {
|
||||
cmds := []bk.StepOpt{
|
||||
bk.DependsOn(candidateImageStepKey(app)),
|
||||
|
||||
bk.Cmd(fmt.Sprintf("docker pull %s", image)),
|
||||
|
||||
// have trivy use a shorter name in its output
|
||||
bk.Cmd(fmt.Sprintf("docker tag %s %s", image, app)),
|
||||
|
||||
bk.Env("IMAGE", app),
|
||||
bk.Env("VULNERABILITY_EXIT_CODE", fmt.Sprintf("%d", vulnerabilityExitCode)),
|
||||
bk.ArtifactPaths("./*-security-report.html"),
|
||||
bk.SoftFail(vulnerabilityExitCode),
|
||||
|
||||
bk.Cmd("./dev/ci/trivy/trivy-scan-high-critical.sh"),
|
||||
}
|
||||
|
||||
pipeline.AddStep(fmt.Sprintf(":trivy: :docker: 🔎 %q", app), cmds...)
|
||||
}
|
||||
}
|
||||
|
||||
// Tag and push final Docker image for the service defined by `app`
|
||||
// after the e2e tests pass.
|
||||
//
|
||||
|
||||
@ -139,6 +139,9 @@ func GeneratePipeline(c Config) (*bk.Pipeline, error) {
|
||||
ops = operations.NewSet([]operations.Operation{
|
||||
buildCandidateDockerImage(patchImage, c.Version, c.candidateImageTag()),
|
||||
})
|
||||
|
||||
// Trivy security scans
|
||||
ops.Append(trivyScanCandidateImage(patchImage, c.candidateImageTag()))
|
||||
// Test images
|
||||
ops.Merge(CoreTestOperations(nil, CoreTestOperationsOptions{}))
|
||||
// Publish images
|
||||
@ -174,6 +177,11 @@ func GeneratePipeline(c Config) (*bk.Pipeline, error) {
|
||||
ops.Append(buildCandidateDockerImage(dockerImage, c.Version, c.candidateImageTag()))
|
||||
}
|
||||
|
||||
// Trivy security scans
|
||||
for _, dockerImage := range images.SourcegraphDockerImages {
|
||||
ops.Append(trivyScanCandidateImage(dockerImage, c.candidateImageTag()))
|
||||
}
|
||||
|
||||
// Executor VM image
|
||||
skipHashCompare := c.MessageFlags.SkipHashCompare || c.RunType.Is(ReleaseBranch)
|
||||
if c.RunType.Is(MainDryRun, MainBranch) {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user