@theoptimalpartner/npm-security-kit 2.0.0-beta.2 → 2.0.0-beta.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 +22 -6
- package/README.md +49 -17
- package/package.json +1 -1
- package/src/checks/malware.js +2 -2
- package/src/checks/node-version.js +20 -5
- package/src/checks/npm-policy.js +20 -4
- package/src/checks/vulnerabilities.js +32 -1
- package/src/init.js +82 -21
- package/src/lib/audit.js +16 -3
- package/src/lib/node-calendar.js +28 -3
package/CHANGELOG.md
CHANGED
|
@@ -6,11 +6,27 @@ Cambios del paquete `@theoptimalpartner/npm-security-kit`. Formato basado en
|
|
|
6
6
|
## [2.0.0] - sin publicar
|
|
7
7
|
|
|
8
8
|
Reescritura completa. La v2 es una CLI de Node sin dependencias que corre en el pipeline **antes de `npm ci`**.
|
|
9
|
-
Sale primero como beta (`2.0.0-beta.N`), con el dist-tag `beta`.
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
Sale primero como beta (`2.0.0-beta.N`), con el dist-tag `beta`.
|
|
10
|
+
|
|
11
|
+
### Betas
|
|
12
|
+
|
|
13
|
+
- **2.0.0-beta.1:** el step del pipeline e `init` usan `npx --yes --min-release-age=0`: `npx` no aplica
|
|
14
|
+
`min-release-age-exclude` y, sin el flag, una versión nueva del kit no se puede usar hasta cumplir 7 días.
|
|
15
|
+
- **2.0.0-beta.2:** `init` propone el Node que ya usa el repo si es una LTS con soporte, en vez de la LTS más nueva:
|
|
16
|
+
un `engines.node` más alto que el Node del repo rompe su `npm ci` con `engine-strict`.
|
|
17
|
+
- **2.0.0-beta.3** (ajustes de una prueba en un repo real):
|
|
18
|
+
- `node-version` da error en una línea de Node que no trae npm >= 11.17 en ninguna release (la 22 trae npm 10),
|
|
19
|
+
con la LTS a la que subir.
|
|
20
|
+
- `init` solo propone una línea de Node cuya release LTS traiga npm >= 11.17: un repo en la 22 (npm 10) recibe la
|
|
21
|
+
24, con la explicación. La imagen es la release LTS más nueva de esa línea, sin parches de seguridad pendientes.
|
|
22
|
+
- `npm-policy` dice qué imagen trae npm 11 (`node:24.22.1 (npm 11.19.1) o más nueva`) y qué líneas no.
|
|
23
|
+
- `init` dice qué líneas agregar al step que ya corre `npm ci`, sin reemplazarlo, y arma la condition con las
|
|
24
|
+
ramas de `pipelines.branches`.
|
|
25
|
+
- `vulnerabilities` avisa las `critical` que están solo en devDependencies (no bloquean) y sugiere `npm audit fix`.
|
|
26
|
+
- Las recetas de lockfile usan `npm install --package-lock-only --ignore-scripts` (sin el flag, con
|
|
27
|
+
`strict-allow-scripts=true` npm corta con `ESTRICTALLOWSCRIPTS`).
|
|
28
|
+
- README: los ejemplos usan la versión del paquete (la `2.0.0` todavía no existe), `install-scripts ls` necesita
|
|
29
|
+
`node_modules`, y `approve` fija la versión (`--no-allow-scripts-pin` para no fijarla).
|
|
14
30
|
|
|
15
31
|
### BREAKING
|
|
16
32
|
|
|
@@ -41,7 +57,7 @@ nueva: un `engines.node` más alto que el Node del repo rompe su `npm ci` con `e
|
|
|
41
57
|
`scripts/security-check.sh`, `.husky/` (y el `"prepare": "husky install"`), los scripts `security:*` del
|
|
42
58
|
`package.json` y el `SECURITY.md` generado.
|
|
43
59
|
2. Si la v1 reemplazó el `bitbucket-pipelines.yml`, recuperar el del repo desde git.
|
|
44
|
-
3. Correr `npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit
|
|
60
|
+
3. Correr `npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@<versión> init` (la del README) y seguir los pasos que imprime (README).
|
|
45
61
|
|
|
46
62
|
## [1.0.2] y anteriores
|
|
47
63
|
|
package/README.md
CHANGED
|
@@ -3,13 +3,16 @@
|
|
|
3
3
|
Reglas de seguridad de dependencias npm en un solo comando que corre en el pipeline **antes de `npm ci`**. No tiene
|
|
4
4
|
dependencias: `npx` descarga un solo tarball y revisa el lockfile antes de que se ejecute un solo install script.
|
|
5
5
|
|
|
6
|
+
Los ejemplos usan la versión de este paquete (`2.0.0-beta.3`). Las publicadas, con sus dist-tags:
|
|
7
|
+
`npm view @theoptimalpartner/npm-security-kit dist-tags`.
|
|
8
|
+
|
|
6
9
|
## Qué revisa
|
|
7
10
|
|
|
8
11
|
| Chequeo | Qué mira | Error (bloquea) | Aviso |
|
|
9
12
|
|---|---|---|---|
|
|
10
13
|
| `malware` | `npm audit` sobre **todo** el lockfile, incluidas las devDependencies | Cualquier advisory de malware (CWE-506), con cualquier severidad | — |
|
|
11
|
-
| `vulnerabilities` | `npm audit --omit=dev` | Vulnerabilidades `critical` en producción | Resumen de las `high` |
|
|
12
|
-
| `node-version` | `.nvmrc`, `.node-version`, `.tool-versions`, `engines.node`, `volta.node`, `FROM node:` y `image:` del pipeline | Línea de Node sin soporte
|
|
14
|
+
| `vulnerabilities` | `npm audit --omit=dev` y, para las de desarrollo, el de todo el árbol | Vulnerabilidades `critical` en producción | Resumen de las `high` en producción; `critical` solo en devDependencies |
|
|
15
|
+
| `node-version` | `.nvmrc`, `.node-version`, `.tool-versions`, `engines.node`, `volta.node`, `FROM node:` y `image:` del pipeline | Línea de Node sin soporte, que no es LTS o que no trae npm >= 11.17 en ninguna release (Node 22 trae npm 10) | Hay una LTS más nueva (mientras no tenga su primera release LTS, solo se informa), pocos días de soporte, parche de seguridad pendiente, versiones mayores distintas, nada fijado |
|
|
13
16
|
| `npm-policy` | `.npmrc`, `engines.npm`, npm en uso, lockfile | Faltan `min-release-age`, `engine-strict` o `strict-allow-scripts`; npm < 11.17; no hay lockfile | Faltan `min-release-age-exclude[]`, `save-exact` o `audit-level`; lockfile < v3 |
|
|
14
17
|
| `install-scripts` | Paquetes con install scripts del lockfile contra `allowScripts` | Alguno sin aprobar (con `strict-allow-scripts`) | Alguno sin aprobar con el override por variable de entorno |
|
|
15
18
|
| `dockerfile` | `COPY` del `.npmrc` antes de `npm ci`, `.dockerignore` | Instala sin el `.npmrc` | Usa `npm install` en vez de `npm ci`; el `.dockerignore` deja fuera el lockfile |
|
|
@@ -40,12 +43,18 @@ audit-level=critical
|
|
|
40
43
|
|
|
41
44
|
Además, `engines.npm` en `>=11.17`: npm 10 ignora `min-release-age` sin avisar y no aplica `allowScripts`.
|
|
42
45
|
|
|
46
|
+
**Node y npm:** ninguna release de Node 20 ni de Node 22 trae npm 11 (traen npm 10). Un repo en la 22 tiene que
|
|
47
|
+
subir a una LTS que traiga npm 11 (Node 24 o más nueva: `node:24.22.1` trae npm 11.19.1). `check` da error en cada
|
|
48
|
+
lugar que fija la 22 (`.nvmrc`, `engines.node`, `FROM`, `image:`) con la versión a la que subir, e `init` propone
|
|
49
|
+
esa línea.
|
|
50
|
+
|
|
43
51
|
## Uso
|
|
44
52
|
|
|
45
53
|
### En el pipeline
|
|
46
54
|
|
|
47
|
-
|
|
48
|
-
|
|
55
|
+
En el step de test que ya existe, **sin reemplazarlo**: el kit antes de `npm ci` y `npm audit signatures` después.
|
|
56
|
+
El step corre solo en los PR a las ramas que despliegan (las de `pipelines.branches`). `init` imprime qué agregar y
|
|
57
|
+
en qué línea. Ejemplo de `bitbucket-pipelines.yml` (el pipeline que lee el chequeo de Node):
|
|
49
58
|
|
|
50
59
|
```yaml
|
|
51
60
|
definitions:
|
|
@@ -57,10 +66,10 @@ definitions:
|
|
|
57
66
|
- npm
|
|
58
67
|
script:
|
|
59
68
|
# Antes de instalar: malware, vulnerabilidades, Node, .npmrc, install scripts, Dockerfiles
|
|
60
|
-
- npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0 check
|
|
69
|
+
- npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.3 check
|
|
61
70
|
- npm ci # con strict-allow-scripts, los scripts no aprobados cortan acá
|
|
62
71
|
- npm audit signatures
|
|
63
|
-
|
|
72
|
+
# … y lo que ya corría el step: build, lint, tests
|
|
64
73
|
|
|
65
74
|
pipelines:
|
|
66
75
|
pull-requests:
|
|
@@ -68,23 +77,28 @@ pipelines:
|
|
|
68
77
|
- step:
|
|
69
78
|
<<: *test
|
|
70
79
|
condition:
|
|
71
|
-
|
|
80
|
+
# Las ramas de pipelines.branches (las que despliegan)
|
|
81
|
+
state: BITBUCKET_PR_DESTINATION_BRANCH == "main" || BITBUCKET_PR_DESTINATION_BRANCH == "develop"
|
|
82
|
+
custom:
|
|
83
|
+
ci: # para correrlo a mano ("Run pipeline")
|
|
84
|
+
- step: *test
|
|
72
85
|
```
|
|
73
86
|
|
|
74
87
|
- **Antes de `npm ci`:** `npm audit` funciona solo con el lockfile, así que el malware se detecta antes de que
|
|
75
88
|
corra su `postinstall`.
|
|
76
|
-
- **Versión fija** (`@2.0.0`): subir la política en un repo es un cambio visible en su PR.
|
|
89
|
+
- **Versión fija** (`@2.0.0-beta.3`): subir la política en un repo es un cambio visible en su PR.
|
|
77
90
|
- **`--min-release-age=0` solo para `npx`:** `npx` no aplica `min-release-age-exclude` (npm 11.19.1 lo aplica al
|
|
78
91
|
instalar, no al resolver el paquete de `npx`). Sin el flag, dentro de un repo con `min-release-age=7` una versión
|
|
79
92
|
nueva del kit no se puede usar hasta cumplir 7 días (`ETARGET … with a date before`). Afecta solo al kit: la
|
|
80
93
|
versión va fija y no tiene dependencias. El `npm ci` que sigue aplica los 7 días como siempre.
|
|
81
94
|
- **`npm audit signatures`** va después de `npm ci` porque necesita `node_modules`.
|
|
82
|
-
- Para guardar el resultado como artefacto: `npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0 check --json > npm-security-kit.json`.
|
|
95
|
+
- Para guardar el resultado como artefacto: `npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.3 check --json > npm-security-kit.json`.
|
|
83
96
|
|
|
84
97
|
### Dockerfile
|
|
85
98
|
|
|
86
99
|
El `.npmrc` se copia con el lockfile, antes de `npm ci`. Si no, dentro de la imagen no aplican
|
|
87
|
-
`strict-allow-scripts`, `min-release-age` ni `engine-strict
|
|
100
|
+
`strict-allow-scripts`, `min-release-age` ni `engine-strict`. El `FROM` tiene que traer npm 11 (Node 24 o más nueva;
|
|
101
|
+
ver **Node y npm** más arriba):
|
|
88
102
|
|
|
89
103
|
```dockerfile
|
|
90
104
|
COPY package.json package-lock.json .npmrc ./
|
|
@@ -98,20 +112,22 @@ Si el `.dockerignore` excluye el `.npmrc` (por ejemplo con `.*`), agregar `!.npm
|
|
|
98
112
|
Una vez por repo, en una rama:
|
|
99
113
|
|
|
100
114
|
```bash
|
|
101
|
-
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0 init --dry-run
|
|
115
|
+
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.3 init --dry-run
|
|
102
116
|
```
|
|
103
117
|
|
|
104
118
|
```bash
|
|
105
|
-
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0 init
|
|
119
|
+
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.3 init
|
|
106
120
|
```
|
|
107
121
|
|
|
108
122
|
`init`:
|
|
109
123
|
|
|
110
124
|
- agrega al `.npmrc` las claves de la política que faltan (con un comentario) y corrige las que tienen otro valor,
|
|
111
125
|
sin borrar ni reordenar nada;
|
|
112
|
-
- completa `engines.npm` (`>=11.17`) y, solo si falta, `engines.node
|
|
126
|
+
- completa `engines.npm` (`>=11.17`) y, solo si falta, `engines.node` (con la línea de Node del repo, si trae npm 11;
|
|
127
|
+
si no, la siguiente que lo trae);
|
|
113
128
|
- **no aprueba install scripts**: lista los pendientes y los comandos para revisarlos;
|
|
114
|
-
- **no edita** `bitbucket-pipelines.yml` ni los Dockerfiles:
|
|
129
|
+
- **no edita** `bitbucket-pipelines.yml` ni los Dockerfiles: dice qué líneas agregar al step que corre `npm ci`, la
|
|
130
|
+
condition con las ramas que despliegan y los `COPY` corregidos;
|
|
115
131
|
- se puede correr de nuevo sin cambiar nada.
|
|
116
132
|
|
|
117
133
|
Después: revisar el diff, aprobar o negar los install scripts, aplicar el step y el Dockerfile, y correr `check`.
|
|
@@ -119,7 +135,7 @@ Después: revisar el diff, aprobar o negar los install scripts, aplicar el step
|
|
|
119
135
|
### En local
|
|
120
136
|
|
|
121
137
|
```bash
|
|
122
|
-
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0 check
|
|
138
|
+
npx --yes --min-release-age=0 @theoptimalpartner/npm-security-kit@2.0.0-beta.3 check
|
|
123
139
|
```
|
|
124
140
|
|
|
125
141
|
No se instala como devDependency ni usa husky: el `.npmrc` protege el `npm install` local y el pipeline es el
|
|
@@ -144,10 +160,12 @@ Dockerfile y pipeline) va una sola vez, con la lista de archivos.
|
|
|
144
160
|
|
|
145
161
|
### `npm ci` falla con EUSAGE (lockfile incompleto)
|
|
146
162
|
|
|
147
|
-
Pasa con lockfiles generados con npm 10, a los que les faltan opcionales de plataforma. Regenerarlo con npm 11
|
|
163
|
+
Pasa con lockfiles generados con npm 10, a los que les faltan opcionales de plataforma. Regenerarlo con npm 11, sin
|
|
164
|
+
scripts (con `strict-allow-scripts=true`, sin `--ignore-scripts` npm corta con `ESTRICTALLOWSCRIPTS` antes de
|
|
165
|
+
regenerarlo):
|
|
148
166
|
|
|
149
167
|
```bash
|
|
150
|
-
npm install --package-lock-only
|
|
168
|
+
npm install --package-lock-only --ignore-scripts
|
|
151
169
|
```
|
|
152
170
|
|
|
153
171
|
### Parche urgente con menos de 7 días
|
|
@@ -170,6 +188,14 @@ npm install-scripts approve esbuild # baja el binario de su plataforma: lo nec
|
|
|
170
188
|
npm install-scripts deny core-js # solo imprime un mensaje: no hace falta
|
|
171
189
|
```
|
|
172
190
|
|
|
191
|
+
`npm install-scripts ls` lee `node_modules`: si el `npm ci --ignore-scripts` falló (por ejemplo con EUSAGE), dice que
|
|
192
|
+
no hay nada pendiente. La lista de `check` e `init` sale del lockfile y no depende de eso.
|
|
193
|
+
|
|
194
|
+
`approve` guarda la versión exacta (`"paquete@1.2.3": true`): cuando se actualiza el paquete, `check` vuelve a dar
|
|
195
|
+
error hasta aprobar la versión nueva. Es a propósito (cada versión nueva se revisa). Para un paquete de confianza que
|
|
196
|
+
se actualiza seguido, `npm install-scripts approve --no-allow-scripts-pin <paquete>` lo aprueba sin versión. `deny`
|
|
197
|
+
siempre guarda el nombre sin versión.
|
|
198
|
+
|
|
173
199
|
Se aprueba lo que necesita compilar o bajar binarios para funcionar, y se niega lo que es solo telemetría o
|
|
174
200
|
publicidad. El kit no aprueba nada en automático. Queda en `allowScripts` del `package.json`. Reglas (las mismas de
|
|
175
201
|
npm):
|
|
@@ -190,6 +216,12 @@ Si un repo no puede adoptar todavía la aprobación de install scripts, se defin
|
|
|
190
216
|
Con el override, npm ejecuta los scripts sin aprobar y el kit los informa como **aviso**, más un aviso de que el
|
|
191
217
|
override está activo. Dentro del `docker build` la variable no llega sola: hay que pasarla como `ARG`.
|
|
192
218
|
|
|
219
|
+
### Vulnerabilidades `critical` en devDependencies
|
|
220
|
+
|
|
221
|
+
No bloquean (no llegan a producción), pero corren en el pipeline y en las máquinas de los devs: `check` las informa
|
|
222
|
+
como aviso. Casi siempre las arregla `npm audit fix` (sin `--force`, que puede subir de major), o subir la
|
|
223
|
+
dependencia que las trae.
|
|
224
|
+
|
|
193
225
|
### El registro de npm no responde
|
|
194
226
|
|
|
195
227
|
El chequeo de malware falla cerrado: `no se pudo consultar los advisories del registro (…); reintentar el step`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@theoptimalpartner/npm-security-kit",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.3",
|
|
4
4
|
"description": "Revisa el lockfile antes de npm ci: malware, vulnerabilidades, versión de Node, .npmrc, install scripts y Dockerfiles",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/checks/malware.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Malware en todo el árbol (incluidas las devDependencies), con cualquier severidad.
|
|
2
2
|
// GitHub publica malware como `high` (debug@4.4.2): un umbral de severidad lo deja pasar.
|
|
3
|
-
import { AuditUnavailableError, advisoriesOf, advisoryKey, isMalware, lockfileName, runAudit, unavailableFinding } from '../lib/audit.js';
|
|
3
|
+
import { AuditUnavailableError, FULL_TREE, advisoriesOf, advisoryKey, isMalware, lockfileName, runAudit, unavailableFinding } from '../lib/audit.js';
|
|
4
4
|
|
|
5
5
|
export const id = 'malware';
|
|
6
6
|
export const title = 'Malware (todo el árbol, cualquier severidad)';
|
|
@@ -23,7 +23,7 @@ export async function run(ctx) {
|
|
|
23
23
|
if (!lockfile) return []; // sin lockfile no hay audit; lo informa npm-policy
|
|
24
24
|
let report;
|
|
25
25
|
try {
|
|
26
|
-
report = await runAudit(ctx,
|
|
26
|
+
report = await runAudit(ctx, FULL_TREE);
|
|
27
27
|
} catch (error) {
|
|
28
28
|
if (error instanceof AuditUnavailableError) return [unavailableFinding(error)];
|
|
29
29
|
throw error;
|
|
@@ -1,10 +1,11 @@
|
|
|
1
|
-
// Versión de Node: pasa cualquier LTS con soporte (Active o Maintenance)
|
|
2
|
-
// más nueva, si quedan pocos días de soporte o si hay
|
|
3
|
-
// sin soporte
|
|
1
|
+
// Versión de Node: pasa cualquier LTS con soporte (Active o Maintenance) cuyas releases traigan el npm
|
|
2
|
+
// que pide la política; se avisa si hay una LTS más nueva, si quedan pocos días de soporte o si hay
|
|
3
|
+
// parches de seguridad pendientes. Una línea sin soporte, que no es LTS o que no trae ese npm (la 22
|
|
4
|
+
// trae npm 10) es error. Todo sale del calendario de nodejs.org.
|
|
4
5
|
import { findDockerfiles, nodeImageTag, parseStages } from '../lib/dockerfile.js';
|
|
5
6
|
import { jsonKeyLine } from '../lib/json-lines.js';
|
|
6
|
-
import { loadCalendar } from '../lib/node-calendar.js';
|
|
7
|
-
import { isRange, minVersion, parseVersion } from '../lib/semver.js';
|
|
7
|
+
import { linesWithNpm, loadCalendar } from '../lib/node-calendar.js';
|
|
8
|
+
import { compareVersions, isRange, minVersion, parseVersion } from '../lib/semver.js';
|
|
8
9
|
|
|
9
10
|
export const id = 'node-version';
|
|
10
11
|
export const title = 'Versión de Node';
|
|
@@ -167,6 +168,20 @@ function evaluate(pin, r, cal, policy) {
|
|
|
167
168
|
const why = sch.lts ? `todavía no es LTS (lo será el ${sch.lts})` : 'línea impar, nunca va a ser LTS';
|
|
168
169
|
return { ...base, level: 'error', message: `${prefix}: no es LTS, ${why}. Usar ${rec}${engineHint}.`, data };
|
|
169
170
|
}
|
|
171
|
+
// La política pide npm >= minNpmVersion y la imagen de Node trae el suyo: una línea que no lo trae en
|
|
172
|
+
// ninguna release (la 22 trae npm 10) deja el pipeline y el docker build con npm 10.
|
|
173
|
+
const lineNpm = cal.npmOfLine(r.major);
|
|
174
|
+
if (lineNpm && parseVersion(lineNpm) && compareVersions(lineNpm, policy.minNpmVersion) < 0) {
|
|
175
|
+
const target = linesWithNpm(cal, policy.minNpmVersion).ok.find((m) => m > r.major);
|
|
176
|
+
const to = target ? `${cal.newestLtsOfLine(target)} (npm ${cal.npmOfLine(target)})` : rec;
|
|
177
|
+
const hint = pin.kind === 'engines' && target ? ` (en engines: ">=${target}")` : '';
|
|
178
|
+
return {
|
|
179
|
+
...base,
|
|
180
|
+
level: 'error',
|
|
181
|
+
message: `${prefix}: trae npm ${lineNpm}, y ninguna release de la ${r.major} trae npm >= ${policy.minNpmVersion}, que pide la política. Subir a una LTS con npm 11: ${to} o más nueva${hint}.`,
|
|
182
|
+
data: { ...data, npm: lineNpm },
|
|
183
|
+
};
|
|
184
|
+
}
|
|
170
185
|
|
|
171
186
|
const notes = [];
|
|
172
187
|
// Mientras la LTS nueva no tiene su primera release LTS nadie puede migrar: se informa sin avisar.
|
package/src/checks/npm-policy.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { lockfileName } from '../lib/audit.js';
|
|
4
4
|
import { parseNpmrc, requiredSettings, settingProblem } from '../lib/npmrc.js';
|
|
5
5
|
import { jsonKeyLine } from '../lib/json-lines.js';
|
|
6
|
+
import { linesWithNpm, loadCalendar } from '../lib/node-calendar.js';
|
|
6
7
|
import { compareVersions, minVersion, parseVersion } from '../lib/semver.js';
|
|
7
8
|
|
|
8
9
|
export const id = 'npm-policy';
|
|
@@ -10,6 +11,21 @@ export const title = 'npm y .npmrc';
|
|
|
10
11
|
|
|
11
12
|
const NPM10_WHY = 'npm 10 ignora min-release-age sin avisar y no aplica allowScripts';
|
|
12
13
|
|
|
14
|
+
// Qué imagen usar para tener ese npm. Sin calendario, el consejo genérico.
|
|
15
|
+
async function npmImageHint(ctx, minNpm) {
|
|
16
|
+
let cal;
|
|
17
|
+
try {
|
|
18
|
+
cal = await loadCalendar(ctx);
|
|
19
|
+
} catch {
|
|
20
|
+
return 'usar la imagen de Node LTS recomendada';
|
|
21
|
+
}
|
|
22
|
+
const { ok, without } = linesWithNpm(cal, minNpm);
|
|
23
|
+
if (!ok.length) return 'usar la imagen de Node LTS recomendada';
|
|
24
|
+
const first = ok[0];
|
|
25
|
+
const none = without.length ? `; ninguna release de Node ${without.join(' ni ')} trae npm ${parseVersion(minNpm)[0]}` : '';
|
|
26
|
+
return `usar una imagen de Node que lo traiga: node:${cal.newestLtsOfLine(first)} (npm ${cal.npmOfLine(first)}) o más nueva${none}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
13
29
|
export async function run(ctx) {
|
|
14
30
|
const { policy } = ctx;
|
|
15
31
|
const findings = [];
|
|
@@ -88,7 +104,7 @@ export async function run(ctx) {
|
|
|
88
104
|
} else if (compareVersions(used, minNpm) < 0) {
|
|
89
105
|
findings.push({
|
|
90
106
|
level: 'error',
|
|
91
|
-
message: `npm ${used} en uso: hace falta npm >= ${minNpm} (${NPM10_WHY}). En el pipeline
|
|
107
|
+
message: `npm ${used} en uso: hace falta npm >= ${minNpm} (${NPM10_WHY}). En el pipeline y en el Dockerfile, ${await npmImageHint(ctx, minNpm)}. En local, \`npm install -g npm@11\`.`,
|
|
92
108
|
data: { npm: used },
|
|
93
109
|
});
|
|
94
110
|
}
|
|
@@ -99,7 +115,7 @@ export async function run(ctx) {
|
|
|
99
115
|
const other = ['yarn.lock', 'pnpm-lock.yaml'].find((f) => ctx.exists(f));
|
|
100
116
|
findings.push({
|
|
101
117
|
level: 'error',
|
|
102
|
-
message: `No hay package-lock.json: sin lockfile no hay \`npm ci\` ni audit.${other ? ` (${other} no sirve: el kit revisa proyectos npm.)` : ' Generarlo con `npm install --package-lock-only`.'}`,
|
|
118
|
+
message: `No hay package-lock.json: sin lockfile no hay \`npm ci\` ni audit.${other ? ` (${other} no sirve: el kit revisa proyectos npm.)` : ' Generarlo con `npm install --package-lock-only --ignore-scripts`.'}`,
|
|
103
119
|
});
|
|
104
120
|
} else {
|
|
105
121
|
const lock = ctx.readJson(lockfile);
|
|
@@ -107,13 +123,13 @@ export async function run(ctx) {
|
|
|
107
123
|
findings.push({
|
|
108
124
|
level: 'error',
|
|
109
125
|
file: lockfile,
|
|
110
|
-
message: `lockfileVersion ${lock.lockfileVersion ?? '?'}: no trae \`packages\` (ni \`hasInstallScript\`), así que no se pueden revisar los install scripts. Regenerar con npm 11 (\`npm install --package-lock-only\`).`,
|
|
126
|
+
message: `lockfileVersion ${lock.lockfileVersion ?? '?'}: no trae \`packages\` (ni \`hasInstallScript\`), así que no se pueden revisar los install scripts. Regenerar con npm 11 (\`npm install --package-lock-only --ignore-scripts\`).`,
|
|
111
127
|
});
|
|
112
128
|
} else if (!(lock.lockfileVersion >= 3)) {
|
|
113
129
|
findings.push({
|
|
114
130
|
level: 'warn',
|
|
115
131
|
file: lockfile,
|
|
116
|
-
message: `lockfileVersion ${lock.lockfileVersion ?? '?'}: regenerar con npm 11 (\`npm install --package-lock-only\`).`,
|
|
132
|
+
message: `lockfileVersion ${lock.lockfileVersion ?? '?'}: regenerar con npm 11 (\`npm install --package-lock-only --ignore-scripts\`).`,
|
|
117
133
|
});
|
|
118
134
|
}
|
|
119
135
|
}
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
// Vulnerabilidades (que no son malware) en dependencias de producción. Bloquea desde
|
|
2
|
-
// `policy.vulnerabilityLevel`; las `high` van en un solo aviso de resumen.
|
|
2
|
+
// `policy.vulnerabilityLevel`; las `high` van en un solo aviso de resumen. Las de ese nivel que están
|
|
3
|
+
// solo en devDependencies van en otro aviso: no llegan a producción, pero corren en el pipeline y en
|
|
4
|
+
// las máquinas de los devs.
|
|
3
5
|
import {
|
|
4
6
|
AuditUnavailableError,
|
|
7
|
+
FULL_TREE,
|
|
5
8
|
advisoriesOf,
|
|
6
9
|
advisoryKey,
|
|
7
10
|
isMalware,
|
|
@@ -75,5 +78,33 @@ export async function run(ctx) {
|
|
|
75
78
|
data: { severity: 'high', count: high.length, packages: high },
|
|
76
79
|
});
|
|
77
80
|
}
|
|
81
|
+
|
|
82
|
+
// El reporte de todo el árbol es el mismo que pide `malware` (una sola llamada). Si el registro no
|
|
83
|
+
// responde, ese chequeo ya da el error.
|
|
84
|
+
let full = null;
|
|
85
|
+
try {
|
|
86
|
+
full = await runAudit(ctx, FULL_TREE);
|
|
87
|
+
} catch (error) {
|
|
88
|
+
if (!(error instanceof AuditUnavailableError)) throw error;
|
|
89
|
+
}
|
|
90
|
+
const devOnly = full
|
|
91
|
+
? [
|
|
92
|
+
...new Set(
|
|
93
|
+
advisoriesOf(full)
|
|
94
|
+
.filter((a) => !isMalware(a, ctx.policy) && severityAtLeast(a.severity, level) && !byPackage.has(a.name))
|
|
95
|
+
.map((a) => a.name),
|
|
96
|
+
),
|
|
97
|
+
].sort()
|
|
98
|
+
: [];
|
|
99
|
+
if (devOnly.length) {
|
|
100
|
+
findings.push({
|
|
101
|
+
level: 'warn',
|
|
102
|
+
file: lockfile,
|
|
103
|
+
message:
|
|
104
|
+
`${devOnly.length} paquete(s) con vulnerabilidades ${level} solo en devDependencies (${devOnly.join(', ')}). ` +
|
|
105
|
+
'No llegan a producción y no bloquean, pero corren en el pipeline y en las máquinas de los devs: `npm audit fix` (sin `--force`) o subir la dependencia que las trae.',
|
|
106
|
+
data: { severity: level, dev: true, count: devOnly.length, packages: devOnly },
|
|
107
|
+
});
|
|
108
|
+
}
|
|
78
109
|
return findings;
|
|
79
110
|
}
|
package/src/init.js
CHANGED
|
@@ -10,7 +10,7 @@ import * as npmPolicyCheck from './checks/npm-policy.js';
|
|
|
10
10
|
import { loadAllowPolicy, verdict } from './lib/allow-scripts.js';
|
|
11
11
|
import { lockfileName } from './lib/audit.js';
|
|
12
12
|
import { unifiedDiff } from './lib/diff.js';
|
|
13
|
-
import { loadCalendar } from './lib/node-calendar.js';
|
|
13
|
+
import { linesWithNpm, loadCalendar } from './lib/node-calendar.js';
|
|
14
14
|
import { parseNpmrc, requiredSettings, settingProblem } from './lib/npmrc.js';
|
|
15
15
|
import { compareVersions, minVersion, parseVersion } from './lib/semver.js';
|
|
16
16
|
import { formatBlocks } from './report.js';
|
|
@@ -77,28 +77,65 @@ export function mergePackageJson(text, policy, nodeMajor) {
|
|
|
77
77
|
return { text: finalNewline ? `${out}${eol}` : out, changes };
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
-
// El Node que init propone (engines.node y la imagen del
|
|
81
|
-
// de una LTS con soporte, para que adoptar el kit no obligue a cambiar de Node.
|
|
82
|
-
//
|
|
83
|
-
// la
|
|
80
|
+
// El Node que init propone (engines.node, si falta, y la imagen del pipeline): el que ya usa el repo
|
|
81
|
+
// si es de una LTS con soporte, para que adoptar el kit no obligue a cambiar de Node. Tiene que traer
|
|
82
|
+
// el npm que pide la política: si la línea del repo no lo trae en ninguna release (la 22 trae npm 10),
|
|
83
|
+
// la siguiente línea que sí, y `from` dice de cuál viene. engines.node toma la línea más baja en uso
|
|
84
|
+
// (con engine-strict, un ">=" más alto rompe el npm ci de esa línea) y la imagen es la release LTS más
|
|
85
|
+
// nueva de la línea (sin parches de seguridad pendientes). Sin Node fijado, la release recomendada.
|
|
84
86
|
export function repoNode(ctx, cal) {
|
|
85
87
|
const recommended = cal.recommended ?? null;
|
|
86
|
-
let
|
|
87
|
-
let image = null;
|
|
88
|
+
let inUse = null;
|
|
88
89
|
for (const pin of nodeVersionCheck.findPins(ctx)) {
|
|
89
90
|
const r = nodeVersionCheck.resolvePin(pin, cal);
|
|
90
91
|
if (r.major === undefined || r.major === null || r.latest) continue;
|
|
91
92
|
if (!['active', 'maintenance'].includes(cal.phaseOf(r.major))) continue;
|
|
92
|
-
if (
|
|
93
|
-
if (!image && pin.file === 'bitbucket-pipelines.yml' && pin.kind === 'docker' && r.exact) image = r.exact;
|
|
93
|
+
if (inUse === null || r.major < inUse) inUse = r.major;
|
|
94
94
|
}
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
};
|
|
95
|
+
const major = inUse === null ? null : (linesWithNpm(cal, ctx.policy.minNpmVersion).ok.find((m) => m >= inUse) ?? null);
|
|
96
|
+
if (major === null) return { major: recommended ? parseVersion(recommended)[0] : null, image: recommended, from: null };
|
|
97
|
+
return { major, image: cal.newestLtsOfLine(major), from: major !== inUse ? inUse : null };
|
|
99
98
|
}
|
|
100
99
|
|
|
101
|
-
|
|
100
|
+
// Las ramas que despliegan: las claves de `pipelines.branches` (sin globs), en orden.
|
|
101
|
+
export function deployBranches(text) {
|
|
102
|
+
const out = [];
|
|
103
|
+
let inPipelines = false;
|
|
104
|
+
let branchesIndent = null;
|
|
105
|
+
let keyIndent = null;
|
|
106
|
+
for (const raw of (text ?? '').split(/\r?\n/)) {
|
|
107
|
+
if (!raw.trim() || raw.trim().startsWith('#')) continue;
|
|
108
|
+
if (/^\S/.test(raw)) {
|
|
109
|
+
inPipelines = /^pipelines:\s*(#.*)?$/.test(raw);
|
|
110
|
+
branchesIndent = null;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
if (!inPipelines) continue;
|
|
114
|
+
const indent = raw.match(/^ */)[0].length;
|
|
115
|
+
const branches = raw.match(/^( *)branches:\s*(#.*)?$/);
|
|
116
|
+
if (branches) {
|
|
117
|
+
branchesIndent = branches[1].length;
|
|
118
|
+
keyIndent = null;
|
|
119
|
+
continue;
|
|
120
|
+
}
|
|
121
|
+
if (branchesIndent === null) continue;
|
|
122
|
+
if (indent <= branchesIndent) {
|
|
123
|
+
branchesIndent = null;
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
keyIndent ??= indent;
|
|
127
|
+
const key = raw.match(/^ *(['"]?)([^'"#:\s][^'"#:]*)\1:\s*(#.*)?$/);
|
|
128
|
+
if (indent === keyIndent && key && !/[*?{}[\]]/.test(key[2])) out.push(key[2].trim());
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
export function prCondition(branches) {
|
|
134
|
+
const names = branches.length ? branches : ['develop', 'main'];
|
|
135
|
+
return names.map((b) => `BITBUCKET_PR_DESTINATION_BRANCH == "${b}"`).join(' || ');
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export function pipelineStep(kitVersion, nodeVersion, branches = []) {
|
|
102
139
|
return `- step: &test
|
|
103
140
|
name: Test (supply chain + build)
|
|
104
141
|
image: node:${nodeVersion}
|
|
@@ -109,14 +146,14 @@ export function pipelineStep(kitVersion, nodeVersion) {
|
|
|
109
146
|
- npx --yes --min-release-age=0 ${PACKAGE}@${kitVersion} check
|
|
110
147
|
- npm ci
|
|
111
148
|
- npm audit signatures
|
|
112
|
-
|
|
149
|
+
# … y lo que ya corre el step: build, lint, tests
|
|
113
150
|
pipelines:
|
|
114
151
|
pull-requests:
|
|
115
152
|
'**':
|
|
116
153
|
- step:
|
|
117
154
|
<<: *test
|
|
118
155
|
condition:
|
|
119
|
-
state:
|
|
156
|
+
state: ${prCondition(branches)}`;
|
|
120
157
|
}
|
|
121
158
|
|
|
122
159
|
// ctx que ve los archivos como quedarían después de init (para el resumen de --dry-run) y
|
|
@@ -196,7 +233,7 @@ export async function runInit(ctx, { dryRun = false, stdout = process.stdout, co
|
|
|
196
233
|
section('Install scripts por aprobar (allowScripts en package.json)');
|
|
197
234
|
const lockfile = lockfileName(ctx);
|
|
198
235
|
if (!lockfile) {
|
|
199
|
-
say(' No hay package-lock.json: generarlo con `npm install --package-lock-only` y volver a correr init.');
|
|
236
|
+
say(' No hay package-lock.json: generarlo con `npm install --package-lock-only --ignore-scripts` y volver a correr init.');
|
|
200
237
|
} else {
|
|
201
238
|
const policy = loadAllowPolicy(JSON.parse(view.readFile('package.json')), view.readFile('.npmrc'));
|
|
202
239
|
const pending = [
|
|
@@ -210,11 +247,13 @@ export async function runInit(ctx, { dryRun = false, stdout = process.stdout, co
|
|
|
210
247
|
say(' Ninguno: todos los paquetes con install scripts están aprobados o negados.');
|
|
211
248
|
} else {
|
|
212
249
|
for (const p of pending) say(` - ${p}`);
|
|
213
|
-
say(' init no aprueba nada solo. Para revisarlos:');
|
|
214
|
-
say(' npm ci --ignore-scripts');
|
|
250
|
+
say(' init no aprueba nada solo. Para revisarlos (`npm install-scripts` lee node_modules: primero hace falta un npm ci):');
|
|
251
|
+
say(' npm ci --ignore-scripts # si da EUSAGE: npm install --package-lock-only --ignore-scripts');
|
|
215
252
|
say(' npm install-scripts ls');
|
|
216
|
-
say(' npm install-scripts approve <paquete> # lo que necesita compilar o descargar binarios');
|
|
253
|
+
say(' npm install-scripts approve <paquete> # lo que necesita compilar o descargar binarios; fija la versión');
|
|
217
254
|
say(' npm install-scripts deny <paquete> # telemetría, publicidad o lo que no haga falta');
|
|
255
|
+
say(' `approve` guarda `paquete@versión`: al actualizar el paquete hay que volver a aprobarlo (o aprobar sin');
|
|
256
|
+
say(' versión con `npm install-scripts approve --no-allow-scripts-pin <paquete>`).');
|
|
218
257
|
}
|
|
219
258
|
}
|
|
220
259
|
|
|
@@ -222,12 +261,34 @@ export async function runInit(ctx, { dryRun = false, stdout = process.stdout, co
|
|
|
222
261
|
section('Pipeline: bitbucket-pipelines.yml (init no lo edita)');
|
|
223
262
|
const pipeline = ctx.readFile('bitbucket-pipelines.yml');
|
|
224
263
|
const used = pipeline?.match(/npm-security-kit@([\w.-]+)/);
|
|
264
|
+
const image = node.image ?? '<versión LTS>';
|
|
265
|
+
if (node.from !== null && node.from !== undefined) {
|
|
266
|
+
say(
|
|
267
|
+
` El repo usa Node ${node.from}, pero ninguna release de la ${node.from} trae npm >= ${ctx.policy.minNpmVersion}, que pide la política: ` +
|
|
268
|
+
`se propone la ${node.major} (node:${image}, npm ${cal.npmOfLine(node.major)}). Pasar a la ${node.major} el .nvmrc, las imágenes del pipeline y los FROM de los Dockerfiles.`,
|
|
269
|
+
);
|
|
270
|
+
}
|
|
271
|
+
const branches = deployBranches(pipeline);
|
|
272
|
+
const ciLines = (pipeline ?? '').split(/\r?\n/).flatMap((l, i) => (/^\s*-\s*npm ci(\s|$)/.test(l) ? [i + 1] : []));
|
|
225
273
|
if (used) {
|
|
226
274
|
say(` Ya corre el kit (${used[0]}).${used[1] !== ctx.kitVersion ? ` Para esta versión: ${PACKAGE}@${ctx.kitVersion}.` : ''}`);
|
|
275
|
+
} else if (ciLines.length) {
|
|
276
|
+
say(` Agregar al step que ya corre npm ci (bitbucket-pipelines.yml:${ciLines.join(', ')}), sin reemplazar el resto del step:`);
|
|
277
|
+
say();
|
|
278
|
+
say(` - npx --yes --min-release-age=0 ${PACKAGE}@${ctx.kitVersion} check # antes de npm ci`);
|
|
279
|
+
say(' - npm audit signatures # después de npm ci');
|
|
280
|
+
say();
|
|
281
|
+
say(` Con una imagen que traiga npm >= ${ctx.policy.minNpmVersion} (image: node:${image}).`);
|
|
282
|
+
if (!/condition:[\s\S]*BITBUCKET_PR_DESTINATION_BRANCH/.test(pipeline)) {
|
|
283
|
+
say(` Y correrlo solo en los PR a las ramas que despliegan${branches.length ? ` (${branches.join(', ')})` : ''}, con la condition del step:`);
|
|
284
|
+
say();
|
|
285
|
+
say(' condition:');
|
|
286
|
+
say(` state: ${prCondition(branches)}`);
|
|
287
|
+
}
|
|
227
288
|
} else {
|
|
228
289
|
say(` ${pipeline === null ? 'No hay bitbucket-pipelines.yml. ' : ''}Agregar el kit en el step de test, ANTES de npm ci, y correrlo solo en los PR a ramas que despliegan:`);
|
|
229
290
|
say();
|
|
230
|
-
for (const line of pipelineStep(ctx.kitVersion,
|
|
291
|
+
for (const line of pipelineStep(ctx.kitVersion, image, branches).split('\n')) say(` ${line}`);
|
|
231
292
|
}
|
|
232
293
|
|
|
233
294
|
// 4. Dockerfiles (no se editan)
|
package/src/lib/audit.js
CHANGED
|
@@ -26,9 +26,22 @@ function parseReport(stdout) {
|
|
|
26
26
|
return { problem: String(detail).split('\n')[0] };
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
// `--
|
|
30
|
-
//
|
|
31
|
-
export
|
|
29
|
+
// Todo el árbol: los `--include` le ganan a un `omit` del entorno (por ejemplo NODE_ENV=production,
|
|
30
|
+
// que esconde las devDependencies).
|
|
31
|
+
export const FULL_TREE = ['--include=dev', '--include=optional', '--include=peer'];
|
|
32
|
+
|
|
33
|
+
// `malware` y `vulnerabilities` piden el mismo reporte de todo el árbol: una sola llamada por corrida.
|
|
34
|
+
const cache = new WeakMap();
|
|
35
|
+
export function runAudit(ctx, extraArgs, options) {
|
|
36
|
+
if (!cache.has(ctx)) cache.set(ctx, new Map());
|
|
37
|
+
const byArgs = cache.get(ctx);
|
|
38
|
+
const key = extraArgs.join(' ');
|
|
39
|
+
if (!byArgs.has(key)) byArgs.set(key, auditWithRetries(ctx, extraArgs, options));
|
|
40
|
+
return byArgs.get(key);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// `--package-lock-only` hace que npm audite el lockfile aunque haya node_modules.
|
|
44
|
+
async function auditWithRetries(ctx, extraArgs, { attempts = 3 } = {}) {
|
|
32
45
|
const args = ['audit', '--json', '--package-lock-only', ...extraArgs];
|
|
33
46
|
let problem;
|
|
34
47
|
for (let attempt = 1; attempt <= attempts; attempt++) {
|
package/src/lib/node-calendar.js
CHANGED
|
@@ -6,9 +6,16 @@ import { compareVersions, parseVersion } from './semver.js';
|
|
|
6
6
|
export const INDEX_URL = 'https://nodejs.org/dist/index.json';
|
|
7
7
|
export const SCHEDULE_URL = 'https://raw.githubusercontent.com/nodejs/Release/main/schedule.json';
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
// Una sola lectura por corrida: lo usan node-version, npm-policy e init.
|
|
10
|
+
const cache = new WeakMap();
|
|
11
|
+
export function loadCalendar(ctx) {
|
|
12
|
+
if (!cache.has(ctx)) {
|
|
13
|
+
cache.set(
|
|
14
|
+
ctx,
|
|
15
|
+
Promise.all([ctx.fetchJson(INDEX_URL), ctx.fetchJson(SCHEDULE_URL)]).then(([index, schedule]) => analyzeCalendar(index, schedule, ctx.today)),
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
return cache.get(ctx);
|
|
12
19
|
}
|
|
13
20
|
|
|
14
21
|
const day = (text) => new Date(`${text}T00:00:00Z`);
|
|
@@ -39,6 +46,7 @@ export function analyzeCalendar(index, schedule, today) {
|
|
|
39
46
|
const newestLts = supported.length ? Math.max(...supported) : null;
|
|
40
47
|
|
|
41
48
|
const ltsReleases = releases.filter((r) => r.lts);
|
|
49
|
+
const newestLtsOf = (major) => ltsReleases.find((r) => majorOf(r.version) === major);
|
|
42
50
|
const newestLtsRelease = newestLts !== null ? ltsReleases.find((r) => majorOf(r.version) === newestLts) : undefined;
|
|
43
51
|
const latestLtsRelease = ltsReleases[0];
|
|
44
52
|
const recommendedRelease = newestLtsRelease ?? latestLtsRelease;
|
|
@@ -52,6 +60,9 @@ export function analyzeCalendar(index, schedule, today) {
|
|
|
52
60
|
// La versión a instalar: la release LTS publicada de la LTS más nueva o, si todavía no salió,
|
|
53
61
|
// la última release LTS publicada.
|
|
54
62
|
recommended: recommendedRelease?.version.replace(/^v/, ''),
|
|
63
|
+
// La release LTS más nueva de la línea y el npm que trae (campo `npm` de index.json).
|
|
64
|
+
newestLtsOfLine: (major) => newestLtsOf(major)?.version.replace(/^v/, ''),
|
|
65
|
+
npmOfLine: (major) => newestLtsOf(major)?.npm ?? null,
|
|
55
66
|
// true si la LTS más nueva todavía no tiene release LTS en index.json.
|
|
56
67
|
newestLtsWithoutRelease: newestLts !== null && !newestLtsRelease,
|
|
57
68
|
latestLtsMajor: latestLtsRelease ? majorOf(latestLtsRelease.version) : null,
|
|
@@ -71,3 +82,17 @@ export function analyzeCalendar(index, schedule, today) {
|
|
|
71
82
|
[...lines.entries()].filter(([m, s]) => m > major && s.lts).map(([m]) => m).sort((a, b) => a - b)[0] ?? null,
|
|
72
83
|
};
|
|
73
84
|
}
|
|
85
|
+
|
|
86
|
+
// Las líneas de Node con soporte cuya release LTS más nueva trae un npm que cumple la política, de la
|
|
87
|
+
// más vieja a la más nueva, y las que no lo traen en ninguna release (index.json dice qué npm trae
|
|
88
|
+
// cada release; la 22 trae npm 10).
|
|
89
|
+
export function linesWithNpm(cal, minNpm) {
|
|
90
|
+
const supported = [...cal.lines.keys()]
|
|
91
|
+
.filter((m) => ['active', 'maintenance'].includes(cal.phaseOf(m)) && cal.newestLtsOfLine(m))
|
|
92
|
+
.sort((a, b) => a - b);
|
|
93
|
+
const meets = (m) => {
|
|
94
|
+
const npm = cal.npmOfLine(m);
|
|
95
|
+
return Boolean(npm && parseVersion(npm)) && compareVersions(npm, minNpm) >= 0;
|
|
96
|
+
};
|
|
97
|
+
return { ok: supported.filter(meets), without: supported.filter((m) => !meets(m)) };
|
|
98
|
+
}
|