spectrum-cli 6.0.1 → 6.0.3
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/CHANGELOG.md +14 -0
- package/README.md +19 -0
- package/package.json +1 -1
- package/src/automation/index.js +13 -4
- package/src/security/runtime.js +36 -1
- package/src/security/scan.js +97 -23
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,20 @@ This is the changelog of the `spectrum-cli` npm package. Its version comes from
|
|
|
9
9
|
`package.json`; the release commands use Git tags and `CHANGELOG.md` in the
|
|
10
10
|
**target project** and do not update that project's `package.json`.
|
|
11
11
|
|
|
12
|
+
## 🚀 [6.0.3] - 2026-10-29
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
|
|
16
|
+
- Начальный и повторный npm audit явно используют https://registry.npmjs.org/; удалены Nexus-авторизация аудита и глобальная подмена npm registry. Установка зависимостей сохраняет настройки проекта.
|
|
17
|
+
|
|
18
|
+
## 🚀 [6.0.2] - 2026-10-29
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Ночной npm audit использует временный registry-scoped user config с Nexus credentials, сохраняет существующую конфигурацию и удаляет auth-файл после команды. Ошибки registry содержат исходный HTTP-код и сообщение без секретов.
|
|
23
|
+
- Версия Node из runtime проверяется через Skopeo, rootless OCI unpack и запуск binary от непривилегированного uid в chroot. Сканирование больше не зависит от Buildah user/mount namespaces. Buildah остаётся инструментом сборки исправленного образа.
|
|
24
|
+
- При ошибке проверки Node сохраняется уже полученный Trivy-отчёт; причины неполного сканирования выводятся в job log с удалением credentials.
|
|
25
|
+
|
|
12
26
|
## 🚀 [6.0.1] - 2026-10-29
|
|
13
27
|
|
|
14
28
|
### Fixed
|
package/README.md
CHANGED
|
@@ -612,6 +612,25 @@ npm pack --dry-run
|
|
|
612
612
|
fixtures получены из прежнего Python-кода; исполняемый Python в пакет не входит.
|
|
613
613
|
Публикация CLI, образов, компонентов и запуск живого pipeline — отдельные шаги.
|
|
614
614
|
|
|
615
|
+
### npm registry и проверка runtime
|
|
616
|
+
|
|
617
|
+
`security scan` и повторный audit после исправлений всегда вызывают:
|
|
618
|
+
|
|
619
|
+
```sh
|
|
620
|
+
npm audit --registry=https://registry.npmjs.org/ --omit=dev --audit-level=high --json
|
|
621
|
+
```
|
|
622
|
+
|
|
623
|
+
Публичный registry указан прямо в argv команды, как в локальном алиасе `na`. Nexus-авторизация для аудита
|
|
624
|
+
не создаётся. Настройки registry в проектном `.npmrc` и user config не меняются: установка и обновление
|
|
625
|
+
зависимостей продолжают использовать конфигурацию проекта. Порог ночного процесса — HIGH/CRITICAL.
|
|
626
|
+
Ошибки audit сохраняют HTTP-код и причину с удалением credentials из диагностических сообщений.
|
|
627
|
+
|
|
628
|
+
Для версии Node из runtime нужны Skopeo, umoci и chroot. CLI копирует выбранную платформу immutable OCI-образа,
|
|
629
|
+
распаковывает rootfs в rootless-режиме и запускает только настроенный Node binary с `--version` от uid/gid 65534.
|
|
630
|
+
Проверка требует доступный chroot (`CAP_SYS_CHROOT`), но не Buildah user/mount namespaces. Кросс-платформенный запуск
|
|
631
|
+
требует binfmt/QEMU. Buildah нужен для пересборки runtime при автоматическом исправлении; отсутствие его нужных
|
|
632
|
+
возможностей блокирует публикацию такого исправления и отражается в отчёте.
|
|
633
|
+
|
|
615
634
|
### Preflight для команд автоматизации
|
|
616
635
|
|
|
617
636
|
Обязательные deployment-настройки задаются явно:
|
package/package.json
CHANGED
package/src/automation/index.js
CHANGED
|
@@ -125,10 +125,19 @@ function exec(
|
|
|
125
125
|
encoding: binary ? null : "utf8",
|
|
126
126
|
windowsHide: true,
|
|
127
127
|
});
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
128
|
+
if (result.error || !allowed.includes(result.status)) {
|
|
129
|
+
const error = new Error(
|
|
130
|
+
`${label || path.basename(args[0]) + " " + (args[1] || "")} failed (exit ${result.status})`,
|
|
131
|
+
);
|
|
132
|
+
// Keep subprocess output out of generic error messages and serialization.
|
|
133
|
+
// Callers that know their credential context can expose redacted diagnostics.
|
|
134
|
+
Object.defineProperties(error, {
|
|
135
|
+
stdout: { value: result.stdout },
|
|
136
|
+
stderr: { value: result.stderr },
|
|
137
|
+
status: { value: result.status },
|
|
138
|
+
});
|
|
139
|
+
throw error;
|
|
140
|
+
}
|
|
132
141
|
return {
|
|
133
142
|
stdout: result.stdout,
|
|
134
143
|
stderr: result.stderr,
|
package/src/security/runtime.js
CHANGED
|
@@ -23,7 +23,41 @@ function run(args, cwd, options = {}, env = process.env) {
|
|
|
23
23
|
if (args[0] === "npm")
|
|
24
24
|
for (const key of ["NPM_TOKEN", "NODE_AUTH_TOKEN"])
|
|
25
25
|
if (env[key]) clean[key] = env[key];
|
|
26
|
-
|
|
26
|
+
try {
|
|
27
|
+
return exec(args, { cwd, ...options, env: clean });
|
|
28
|
+
} catch (error) {
|
|
29
|
+
const detail = diagnostic(error.stderr || "", env);
|
|
30
|
+
if (detail) error.message += ": " + detail;
|
|
31
|
+
throw error;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
function diagnostic(value, env = process.env) {
|
|
35
|
+
let text = String(value || "");
|
|
36
|
+
const secrets = Object.entries(env)
|
|
37
|
+
.filter(
|
|
38
|
+
([key, value]) =>
|
|
39
|
+
/TOKEN|PASSWORD|SECRET|MATTERMOST_HOOK/i.test(key) && value,
|
|
40
|
+
)
|
|
41
|
+
.map(([, value]) => String(value));
|
|
42
|
+
if (env.NEXUS_USER && env.NEXUS_PASSWORD)
|
|
43
|
+
secrets.push(
|
|
44
|
+
Buffer.from(env.NEXUS_USER + ":" + env.NEXUS_PASSWORD).toString("base64"),
|
|
45
|
+
);
|
|
46
|
+
for (const secret of secrets.sort((a, b) => b.length - a.length))
|
|
47
|
+
text = text.split(secret).join("[REDACTED]");
|
|
48
|
+
return text
|
|
49
|
+
.replace(/(https?:\/\/)[^\s/@]+:[^\s/@]+@/g, "$1[REDACTED]@")
|
|
50
|
+
.replace(
|
|
51
|
+
/([?&](?:token|access_token|auth|password)=)[^&\s]+/gi,
|
|
52
|
+
"$1[REDACTED]",
|
|
53
|
+
)
|
|
54
|
+
.replace(
|
|
55
|
+
/\b(Bearer|Basic)\s+(?!realm=)[A-Za-z0-9+/_=.-]+/gi,
|
|
56
|
+
"$1 [REDACTED]",
|
|
57
|
+
)
|
|
58
|
+
.replace(/[\r\n]+/g, " ")
|
|
59
|
+
.slice(0, 2000)
|
|
60
|
+
.trim();
|
|
27
61
|
}
|
|
28
62
|
async function registryAuth(env, action) {
|
|
29
63
|
return temporary("nightly-registry-", async (directory) => {
|
|
@@ -182,6 +216,7 @@ async function pages(api, location, query = {}) {
|
|
|
182
216
|
}
|
|
183
217
|
module.exports = {
|
|
184
218
|
cleanEnv,
|
|
219
|
+
diagnostic,
|
|
185
220
|
run,
|
|
186
221
|
registryAuth,
|
|
187
222
|
artifactRedirect,
|
package/src/security/scan.js
CHANGED
|
@@ -6,10 +6,12 @@ const {
|
|
|
6
6
|
save,
|
|
7
7
|
json,
|
|
8
8
|
temporary,
|
|
9
|
+
temp,
|
|
10
|
+
remove,
|
|
9
11
|
repositoryPath,
|
|
10
12
|
DIGEST,
|
|
11
13
|
} = require("../automation");
|
|
12
|
-
const { run, API } = require("./runtime");
|
|
14
|
+
const { run, API, diagnostic } = require("./runtime");
|
|
13
15
|
const {
|
|
14
16
|
npmFindings,
|
|
15
17
|
trivyFindings,
|
|
@@ -67,12 +69,45 @@ class Scanner {
|
|
|
67
69
|
}
|
|
68
70
|
async auditNpm(root, evidence) {
|
|
69
71
|
const proc = this.run(
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
72
|
+
[
|
|
73
|
+
"npm",
|
|
74
|
+
"audit",
|
|
75
|
+
"--registry=https://registry.npmjs.org/",
|
|
76
|
+
"--omit=dev",
|
|
77
|
+
"--audit-level=high",
|
|
78
|
+
"--json",
|
|
79
|
+
],
|
|
80
|
+
root,
|
|
81
|
+
{ allowed: [0, 1] },
|
|
82
|
+
);
|
|
83
|
+
if (evidence)
|
|
84
|
+
save(evidence.replace(/\.json$/, "-command.json"), {
|
|
85
|
+
status: proc.status,
|
|
86
|
+
stderr: diagnostic(proc.stderr, this.env),
|
|
87
|
+
});
|
|
88
|
+
let report;
|
|
89
|
+
try {
|
|
74
90
|
report = json(proc.stdout);
|
|
91
|
+
} catch {
|
|
92
|
+
throw new Error(
|
|
93
|
+
"npm audit returned invalid JSON" +
|
|
94
|
+
(proc.stderr ? ": " + diagnostic(proc.stderr, this.env) : ""),
|
|
95
|
+
);
|
|
96
|
+
}
|
|
75
97
|
if (evidence) save(evidence, report);
|
|
98
|
+
if (!report || report.error || !report.vulnerabilities) {
|
|
99
|
+
const code = report?.statusCode || report?.error?.code || proc.status;
|
|
100
|
+
const detail = diagnostic(
|
|
101
|
+
report?.message ||
|
|
102
|
+
report?.error?.summary ||
|
|
103
|
+
report?.error?.detail ||
|
|
104
|
+
proc.stderr,
|
|
105
|
+
this.env,
|
|
106
|
+
);
|
|
107
|
+
throw new Error(
|
|
108
|
+
`npm audit returned no vulnerability report (${code})${detail ? ": " + detail : ""}`,
|
|
109
|
+
);
|
|
110
|
+
}
|
|
76
111
|
const values = await npmFindings(report, this.deps.aliases);
|
|
77
112
|
ensure(
|
|
78
113
|
proc.status === 0 || values.length,
|
|
@@ -92,21 +127,45 @@ class Scanner {
|
|
|
92
127
|
return image.split("@")[0] + "@" + digest;
|
|
93
128
|
}
|
|
94
129
|
nodeVersion(image, platform) {
|
|
95
|
-
|
|
96
|
-
"
|
|
97
|
-
"
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
130
|
+
ensure(
|
|
131
|
+
["linux/amd64", "linux/arm64"].includes(platform),
|
|
132
|
+
"Unsupported runtime platform",
|
|
133
|
+
);
|
|
134
|
+
const directory = temp("nightly-node-");
|
|
135
|
+
const imagePath = path.join(directory, "image");
|
|
136
|
+
const bundle = path.join(directory, "bundle");
|
|
137
|
+
let failure;
|
|
102
138
|
try {
|
|
139
|
+
this.run([
|
|
140
|
+
"skopeo",
|
|
141
|
+
"--override-os",
|
|
142
|
+
"linux",
|
|
143
|
+
"--override-arch",
|
|
144
|
+
platform.split("/")[1],
|
|
145
|
+
"copy",
|
|
146
|
+
"docker://" + image,
|
|
147
|
+
"oci:" + imagePath + ":runtime",
|
|
148
|
+
]);
|
|
149
|
+
this.run([
|
|
150
|
+
"umoci",
|
|
151
|
+
"unpack",
|
|
152
|
+
"--rootless",
|
|
153
|
+
"--image",
|
|
154
|
+
imagePath + ":runtime",
|
|
155
|
+
bundle,
|
|
156
|
+
]);
|
|
157
|
+
const binary = this.env.NIGHTLY_NODE_BINARY || "/nodejs/bin/node";
|
|
158
|
+
ensure(
|
|
159
|
+
path.posix.isAbsolute(binary) && !binary.split("/").includes(".."),
|
|
160
|
+
"Expected an absolute runtime Node binary path",
|
|
161
|
+
);
|
|
162
|
+
// Execute only the runtime binary, with its own libraries and an unprivileged uid.
|
|
163
|
+
// This does not need a container daemon, mount namespace or Buildah storage.
|
|
103
164
|
const value = this.run([
|
|
104
|
-
"
|
|
105
|
-
"
|
|
106
|
-
"
|
|
107
|
-
|
|
108
|
-
"--",
|
|
109
|
-
this.env.NIGHTLY_NODE_BINARY || "/nodejs/bin/node",
|
|
165
|
+
"chroot",
|
|
166
|
+
"--userspec=65534:65534",
|
|
167
|
+
path.join(bundle, "rootfs"),
|
|
168
|
+
binary,
|
|
110
169
|
"--version",
|
|
111
170
|
])
|
|
112
171
|
.stdout.trim()
|
|
@@ -116,8 +175,16 @@ class Scanner {
|
|
|
116
175
|
"Cannot determine runtime Node.js version",
|
|
117
176
|
);
|
|
118
177
|
return value;
|
|
178
|
+
} catch (error) {
|
|
179
|
+
failure = error;
|
|
180
|
+
throw error;
|
|
119
181
|
} finally {
|
|
120
|
-
|
|
182
|
+
try {
|
|
183
|
+
remove(directory);
|
|
184
|
+
} catch (cleanup) {
|
|
185
|
+
if (failure) failure.message += "; cleanup: " + cleanup.message;
|
|
186
|
+
else throw cleanup;
|
|
187
|
+
}
|
|
121
188
|
}
|
|
122
189
|
}
|
|
123
190
|
async feed() {
|
|
@@ -125,7 +192,7 @@ class Scanner {
|
|
|
125
192
|
? this.deps.feed()
|
|
126
193
|
: new API("https://raw.githubusercontent.com").get(NODE_FEED);
|
|
127
194
|
}
|
|
128
|
-
async scanImage(image, platform, feed) {
|
|
195
|
+
async scanImage(image, platform, feed, evidence) {
|
|
129
196
|
ensure(
|
|
130
197
|
["linux/amd64", "linux/arm64"].includes(platform),
|
|
131
198
|
"Unsupported runtime platform",
|
|
@@ -155,6 +222,7 @@ class Scanner {
|
|
|
155
222
|
]);
|
|
156
223
|
return read(filename);
|
|
157
224
|
});
|
|
225
|
+
if (evidence) save(evidence, report);
|
|
158
226
|
const values = trivyFindings(report);
|
|
159
227
|
let version = null;
|
|
160
228
|
if ((this.env.NIGHTLY_CHECK_NODE || "true") === "true") {
|
|
@@ -209,7 +277,7 @@ class Scanner {
|
|
|
209
277
|
tools.push("npm");
|
|
210
278
|
}
|
|
211
279
|
if ((this.env.NIGHTLY_CHECK_NODE || "true") === "true")
|
|
212
|
-
tools.push("
|
|
280
|
+
tools.push("umoci", "chroot");
|
|
213
281
|
(this.deps.checkExecutables || checkExecutables)(tools, this.env);
|
|
214
282
|
const platforms = (this.env.NIGHTLY_PLATFORMS || "linux/amd64").split(",");
|
|
215
283
|
ensure(
|
|
@@ -276,8 +344,12 @@ class Scanner {
|
|
|
276
344
|
platforms = (this.env.NIGHTLY_PLATFORMS || "linux/amd64").split(",");
|
|
277
345
|
report.runtime = { image, dockerfile, platforms };
|
|
278
346
|
for (const [index, platform] of platforms.entries()) {
|
|
279
|
-
const [
|
|
280
|
-
|
|
347
|
+
const [, values, version] = await this.scanImage(
|
|
348
|
+
image,
|
|
349
|
+
platform,
|
|
350
|
+
undefined,
|
|
351
|
+
path.join(output, `runtime-${index}.json`),
|
|
352
|
+
);
|
|
281
353
|
report.runtime[platform] = { node: version };
|
|
282
354
|
report.findings.push(...values);
|
|
283
355
|
}
|
|
@@ -294,6 +366,8 @@ class Scanner {
|
|
|
294
366
|
"NIGHTLY_SCAN_JOB_ID=" + this.env.CI_JOB_ID + "\n",
|
|
295
367
|
);
|
|
296
368
|
}
|
|
369
|
+
for (const error of report.errors)
|
|
370
|
+
console.error("ERROR security scan: " + diagnostic(error, this.env));
|
|
297
371
|
return report.errors.length ? 1 : 0;
|
|
298
372
|
}
|
|
299
373
|
}
|