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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spectrum-cli",
3
- "version": "6.0.1",
3
+ "version": "6.0.3",
4
4
  "description": "🚀 Modern CLI for development workflow automation - release management, versioning, and changelog generation",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -125,10 +125,19 @@ function exec(
125
125
  encoding: binary ? null : "utf8",
126
126
  windowsHide: true,
127
127
  });
128
- ensure(
129
- !result.error && allowed.includes(result.status),
130
- `${label || path.basename(args[0]) + " " + (args[1] || "")} failed (exit ${result.status})`,
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,
@@ -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
- return exec(args, { cwd, ...options, env: clean });
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,
@@ -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
- ["npm", "audit", "--omit=dev", "--audit-level=high", "--json"],
71
- root,
72
- { allowed: [0, 1] },
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
- const container = this.run([
96
- "buildah",
97
- "from",
98
- "--platform",
99
- platform,
100
- image,
101
- ]).stdout.trim();
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
- "buildah",
105
- "run",
106
- "--isolation=chroot",
107
- container,
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
- this.run(["buildah", "rm", container]);
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("buildah");
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 [raw, values, version] = await this.scanImage(image, platform);
280
- save(path.join(output, `runtime-${index}.json`), raw);
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
  }