@scalequality/check 0.2.1 → 0.3.1

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.
Files changed (2) hide show
  1. package/bin/check.mjs +84 -1
  2. 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,
@@ -98,7 +174,14 @@ async function main() {
98
174
  // 5s quantizes every verdict to a 5s boundary: an analysis that finishes in 7s is
99
175
  // only reported at 10s, and the pipeline pays 3s of pure waiting. Start tight and
100
176
  // back off, so a fast repo answers fast and a slow one still polls cheaply.
101
- const POLL_MS = [1500, 1500, 2000, 2000, 3000]
177
+ //
178
+ // The ladder is DENSE from 4s to 9s because that is where analyses actually land.
179
+ // Measured 2026-08-09: the engine finishes in ~5.3s (semgrep is the slowest stage),
180
+ // and the previous ladder's 5s->7s gap meant three consecutive gate runs all
181
+ // reported 7.1-7.3s. Two full seconds of the number a customer sees, and of the
182
+ // number we quote against Sonar, were the client sleeping. Extra polls are cheap:
183
+ // they are small status reads, and being wrong about our own headline number is not.
184
+ const POLL_MS = [1200, 1200, 900, 700, 700, 700, 700, 700, 1000, 1500, 2000, 3000]
102
185
  const deadline = Date.now() + timeoutMs
103
186
  let last
104
187
  let poll = 0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@scalequality/check",
3
- "version": "0.2.1",
3
+ "version": "0.3.1",
4
4
  "description": "ScaleQuality continuous code-maturity check for CI. Re-measures your repo on every build and fails the pipeline below your maturity line.",
5
5
  "type": "module",
6
6
  "bin": {