@scalequality/check 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/check.mjs +76 -0
- package/package.json +1 -1
package/bin/check.mjs
CHANGED
|
@@ -26,6 +26,9 @@ Options:
|
|
|
26
26
|
--token <t> SQ token (or env SQ_TOKEN)
|
|
27
27
|
--project <id> ScaleQuality project id (or env SQ_PROJECT)
|
|
28
28
|
--host <url> API host (default https://app.scalequality.io, or env SQ_HOST)
|
|
29
|
+
--image <ref> Also scan the container image this job just built, e.g.
|
|
30
|
+
myapp:\${{ github.sha }}. Needs trivy on PATH; skipped with a
|
|
31
|
+
note if it is missing, never fails the build for it.
|
|
29
32
|
--timeout <s> Max seconds to wait for the measurement (default 600)
|
|
30
33
|
--json Print the raw result as JSON
|
|
31
34
|
-h, --help Show this help
|
|
@@ -38,6 +41,7 @@ const token = flag("--token") || process.env.SQ_TOKEN
|
|
|
38
41
|
const project = flag("--project") || process.env.SQ_PROJECT
|
|
39
42
|
const host = (flag("--host") || process.env.SQ_HOST || "https://app.scalequality.io").replace(/\/+$/, "")
|
|
40
43
|
const jsonOut = has("--json")
|
|
44
|
+
const image = flag("--image") || process.env.SQ_IMAGE
|
|
41
45
|
const timeoutMs = Math.max(30, Number(flag("--timeout") || 600)) * 1000
|
|
42
46
|
|
|
43
47
|
const base = `${host}/api/insights/continuous`
|
|
@@ -49,6 +53,76 @@ if (!token || !project) {
|
|
|
49
53
|
fail("--token and --project are required (or env SQ_TOKEN / SQ_PROJECT). See --help.")
|
|
50
54
|
}
|
|
51
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Scans the image THIS job just built. Container CVEs cannot be measured from a
|
|
58
|
+
* repository: they live in the built image, and the only place that image exists
|
|
59
|
+
* is right here. So the scan runs in the customer's own pipeline and only the
|
|
60
|
+
* report travels.
|
|
61
|
+
*
|
|
62
|
+
* Two rules, both about not lying:
|
|
63
|
+
* - no `--image`, no scan, and nothing is reported as clean;
|
|
64
|
+
* - `--image` without trivy prints how to add it and CONTINUES. The container
|
|
65
|
+
* dimension comes back not-measured, but the maturity gate still runs. A
|
|
66
|
+
* missing scanner is not a reason to break someone's build.
|
|
67
|
+
*/
|
|
68
|
+
function scanImage() {
|
|
69
|
+
if (!image) return null
|
|
70
|
+
const probe = spawnSync("trivy", ["--version"], { encoding: "utf8" })
|
|
71
|
+
if (probe.error || probe.status !== 0) {
|
|
72
|
+
log("ScaleQuality: --image was given but trivy is not on PATH, so the container scan was skipped.")
|
|
73
|
+
log("ScaleQuality: add it before this step, e.g. `- uses: aquasecurity/setup-trivy@v0.2.0`.")
|
|
74
|
+
return null
|
|
75
|
+
}
|
|
76
|
+
const r = spawnSync("trivy", [
|
|
77
|
+
"image", "--format", "json", "--quiet",
|
|
78
|
+
"--scanners", "vuln",
|
|
79
|
+
// LOW is dropped for the same reason it is dropped everywhere else here: a
|
|
80
|
+
// noisy finding costs more credibility than a missed one.
|
|
81
|
+
"--severity", "CRITICAL,HIGH,MEDIUM",
|
|
82
|
+
image,
|
|
83
|
+
], { encoding: "utf8", maxBuffer: 128 * 1024 * 1024 })
|
|
84
|
+
if (r.error || r.status !== 0 || !r.stdout) {
|
|
85
|
+
log(`ScaleQuality: could not scan ${image}, continuing without the container scan.`)
|
|
86
|
+
return null
|
|
87
|
+
}
|
|
88
|
+
return slimReport(r.stdout, image)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* trivy ships the full advisory text and reference list for every finding: 933KB
|
|
93
|
+
* for a single base image, of which we read a handful of fields. Sending the rest
|
|
94
|
+
* costs the customer's pipeline upload time for nothing. Same shape, fewer bytes,
|
|
95
|
+
* so the server parser stays untouched.
|
|
96
|
+
*/
|
|
97
|
+
function slimReport(stdout, ref) {
|
|
98
|
+
let d
|
|
99
|
+
try { d = JSON.parse(stdout) } catch { return null }
|
|
100
|
+
let n = 0
|
|
101
|
+
const out = {
|
|
102
|
+
ArtifactName: d.ArtifactName || ref,
|
|
103
|
+
Metadata: { OS: d.Metadata?.OS ? { Family: d.Metadata.OS.Family, Name: d.Metadata.OS.Name, EOSL: d.Metadata.OS.EOSL } : undefined },
|
|
104
|
+
Results: (d.Results || []).map((res) => ({
|
|
105
|
+
Target: res.Target,
|
|
106
|
+
Class: res.Class,
|
|
107
|
+
Type: res.Type,
|
|
108
|
+
Vulnerabilities: (res.Vulnerabilities || []).map((v) => {
|
|
109
|
+
n++
|
|
110
|
+
return {
|
|
111
|
+
VulnerabilityID: v.VulnerabilityID,
|
|
112
|
+
PkgName: v.PkgName,
|
|
113
|
+
InstalledVersion: v.InstalledVersion,
|
|
114
|
+
FixedVersion: v.FixedVersion,
|
|
115
|
+
Severity: v.Severity,
|
|
116
|
+
Title: typeof v.Title === "string" ? v.Title.slice(0, 300) : undefined,
|
|
117
|
+
}
|
|
118
|
+
}),
|
|
119
|
+
})),
|
|
120
|
+
}
|
|
121
|
+
const json = JSON.stringify(out)
|
|
122
|
+
log(`ScaleQuality: scanned ${out.ArtifactName} — ${n} vulnerabilities`)
|
|
123
|
+
return json
|
|
124
|
+
}
|
|
125
|
+
|
|
52
126
|
async function main() {
|
|
53
127
|
const auth = { Authorization: `Bearer ${token}` }
|
|
54
128
|
|
|
@@ -78,6 +152,8 @@ async function main() {
|
|
|
78
152
|
const form = new FormData()
|
|
79
153
|
form.append("project", project)
|
|
80
154
|
form.append("file", new Blob([buf]), "checkout.tar.gz")
|
|
155
|
+
const containerReport = scanImage()
|
|
156
|
+
if (containerReport) form.append("containerReport", containerReport)
|
|
81
157
|
const r = await fetch(`${base}/report/upload`, {
|
|
82
158
|
method: "POST",
|
|
83
159
|
headers: auth,
|
package/package.json
CHANGED