@starterlib/cli 0.11.6 → 0.12.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/README.md +28 -0
- package/package.json +1 -1
- package/src/etapes/securite.mjs +131 -8
package/README.md
CHANGED
|
@@ -148,3 +148,31 @@ rxjs, tslib, es-module-shims) s'admet dans `package.json`, avec sa raison ecrite
|
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
```
|
|
151
|
+
|
|
152
|
+
## Admissions nommees de la garde de securite (0.11.7)
|
|
153
|
+
|
|
154
|
+
Une dependance de production admise qui porte, sans reglage qui l'en retire, un motif que `securite` refuse
|
|
155
|
+
(son etat dans le stockage du navigateur, l'ecriture HTML ou les styles de ses composants d'interface) s'admet
|
|
156
|
+
NOMMEMENT, motif par motif, avec sa raison :
|
|
157
|
+
|
|
158
|
+
```json
|
|
159
|
+
{
|
|
160
|
+
"starterWeb": {
|
|
161
|
+
"securiteAdmise": {
|
|
162
|
+
"posthog-js": {
|
|
163
|
+
"motifs": ["stockage", "ecriture-html", "style-par-attribut"],
|
|
164
|
+
"raison": "Mesure du comportement apres accord aux temoins, chargee a la demande, hors du premier ecran.",
|
|
165
|
+
"dependances": []
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
- motifs admissibles : `stockage`, `ecriture-html`, `insertion-html`, `style-par-attribut` ; jamais `eval`,
|
|
173
|
+
`Function`, la lecture de cookie, une politique Trusted Types ni un puits propre a un remote ;
|
|
174
|
+
- le paquet doit figurer dans `dependancesAdmises` ; la raison compte au moins 40 caracteres ;
|
|
175
|
+
- l'admission ne vaut que pour un morceau livre dont TOUTES les sources sont du paquet (ou des paquets nommes dans
|
|
176
|
+
`dependances`), lues dans la carte de la construction (`browser-stats.json`, `statsJson: true` sur la cible esbuild) :
|
|
177
|
+
le code de l'application reste juge en entier. Charger le paquet par un import dynamique lui donne son propre morceau ;
|
|
178
|
+
- `verify` nomme chaque morceau admis, et avertit d'une admission qui ne couvre aucun morceau.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@starterlib/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
4
4
|
"description": "Chaine de verification et de construction des SPA du socle frontal starter : starter-web verify, construire, sprite, manifeste, precompresser. Jamais en dependance de production.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|
package/src/etapes/securite.mjs
CHANGED
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
* paquets construits.
|
|
28
28
|
*/
|
|
29
29
|
import { readFileSync } from 'node:fs';
|
|
30
|
-
import { join } from 'node:path';
|
|
30
|
+
import { dirname, join } from 'node:path';
|
|
31
31
|
import { fichiers, lireJson, relatif } from '../commun.mjs';
|
|
32
32
|
import { natureDesFichiers } from '../livraison.mjs';
|
|
33
33
|
|
|
@@ -40,7 +40,11 @@ const CREATION_DE_POLITIQUE = /trustedTypes\s*\.\s*createPolicy\(\s*['"]([^'"]*)
|
|
|
40
40
|
const MOTIFS = [
|
|
41
41
|
{ motif: /\beval\(/, raison: 'eval : interdit par script-src' },
|
|
42
42
|
{ motif: /new Function\(/, raison: 'new Function : interdit par script-src' },
|
|
43
|
-
{
|
|
43
|
+
{
|
|
44
|
+
id: 'stockage',
|
|
45
|
+
motif: /localStorage|sessionStorage|indexedDB/,
|
|
46
|
+
raison: 'stockage navigateur (ADR 0014)',
|
|
47
|
+
},
|
|
44
48
|
{ motif: /document\.cookie/, raison: 'lecture de cookie : le cookie de session est HttpOnly' },
|
|
45
49
|
];
|
|
46
50
|
|
|
@@ -59,11 +63,17 @@ const PUITS_DE_LA_SPA = [
|
|
|
59
63
|
"bypassSecurityTrust* : la politique angular#unsafe-bypass n'est pas admise, et le desinfectant est contourne",
|
|
60
64
|
},
|
|
61
65
|
{
|
|
66
|
+
id: 'ecriture-html',
|
|
62
67
|
motif: /\.(?:innerHTML|outerHTML)\s*=(?!=)/,
|
|
63
68
|
raison: 'ecriture HTML directe : puits Trusted Types, et vecteur XSS',
|
|
64
69
|
},
|
|
65
|
-
{ motif: /insertAdjacentHTML\s*\(|document\.write\s*\(/, raison: 'ecriture HTML directe' },
|
|
66
70
|
{
|
|
71
|
+
id: 'insertion-html',
|
|
72
|
+
motif: /insertAdjacentHTML\s*\(|document\.write\s*\(/,
|
|
73
|
+
raison: 'ecriture HTML directe',
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'style-par-attribut',
|
|
67
77
|
motif: /setAttribute\(\s*["']style["']/,
|
|
68
78
|
raison: "style pose par setAttribute : refuse sans 'unsafe-inline'",
|
|
69
79
|
},
|
|
@@ -197,6 +207,86 @@ function jugerUnePage(nom, contenu) {
|
|
|
197
207
|
return defauts;
|
|
198
208
|
}
|
|
199
209
|
|
|
210
|
+
/**
|
|
211
|
+
* Les motifs qu'une admission nommee peut lever (0.11.7) : ceux qu'une bibliotheque tierce porte sans qu'on puisse
|
|
212
|
+
* les en retirer (stockage de son etat, ecriture HTML et styles de ses composants d'interface). Jamais l'execution de
|
|
213
|
+
* chaine (`eval`, `Function`), la lecture de cookie, les politiques Trusted Types, ni les puits propres a un remote.
|
|
214
|
+
*/
|
|
215
|
+
export const MOTIFS_ADMISSIBLES = [
|
|
216
|
+
'stockage',
|
|
217
|
+
'ecriture-html',
|
|
218
|
+
'insertion-html',
|
|
219
|
+
'style-par-attribut',
|
|
220
|
+
];
|
|
221
|
+
|
|
222
|
+
/** Une raison ecrite, assez longue pour dire ce que fait le paquet et ou il se charge. */
|
|
223
|
+
const RAISON_MINIMALE = 40;
|
|
224
|
+
|
|
225
|
+
/** Le paquet d'un fichier source de la carte de construction : le dernier `node_modules/<paquet>/`, ou null. */
|
|
226
|
+
export function paquetDeLEntree(entree) {
|
|
227
|
+
const trouves = [...entree.matchAll(/node_modules\/((?:@[^/]+\/)?[^/]+)\//g)];
|
|
228
|
+
return trouves.length === 0 ? null : trouves[trouves.length - 1][1];
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* Les admissions nommees d'un projet (`package.json#starterWeb.securiteAdmise`, 0.11.7) : par paquet, les motifs
|
|
233
|
+
* qu'il porte et sa raison ; `dependances` nomme les paquets qui peuvent partager ses morceaux. Le paquet doit etre une
|
|
234
|
+
* dependance de production admise (`starterWeb.dependancesAdmises`) ; un motif hors de {@link MOTIFS_ADMISSIBLES}, une
|
|
235
|
+
* raison trop courte ou un paquet inconnu est un defaut.
|
|
236
|
+
*
|
|
237
|
+
* @returns {{ admissions: Map<string, { motifs: Set<string>, paquets: Set<string> }>, defauts: string[] }}
|
|
238
|
+
*/
|
|
239
|
+
export function admissionsDuProjet(paquet) {
|
|
240
|
+
const declarees = paquet.starterWeb?.securiteAdmise ?? {};
|
|
241
|
+
const dependances = paquet.dependencies ?? {};
|
|
242
|
+
const revues = paquet.starterWeb?.dependancesAdmises ?? {};
|
|
243
|
+
const admissions = new Map();
|
|
244
|
+
const defauts = [];
|
|
245
|
+
for (const [nom, admission] of Object.entries(declarees)) {
|
|
246
|
+
const lieu = `starterWeb.securiteAdmise["${nom}"]`;
|
|
247
|
+
if (!(nom in dependances) || !(nom in revues)) {
|
|
248
|
+
defauts.push(
|
|
249
|
+
`${lieu} : le paquet doit etre une dependance de production admise (starterWeb.dependancesAdmises).`,
|
|
250
|
+
);
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
const motifs = Array.isArray(admission?.motifs) ? admission.motifs : [];
|
|
254
|
+
const horsListe = motifs.filter((motif) => !MOTIFS_ADMISSIBLES.includes(motif));
|
|
255
|
+
if (motifs.length === 0 || horsListe.length > 0) {
|
|
256
|
+
defauts.push(
|
|
257
|
+
`${lieu} : motifs ${motifs.length === 0 ? 'absents' : `hors liste (${horsListe.join(', ')})`} ; admissibles : ` +
|
|
258
|
+
`${MOTIFS_ADMISSIBLES.join(', ')}.`,
|
|
259
|
+
);
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (typeof admission.raison !== 'string' || admission.raison.trim().length < RAISON_MINIMALE) {
|
|
263
|
+
defauts.push(`${lieu} : une raison ecrite d'au moins ${String(RAISON_MINIMALE)} caracteres.`);
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
const compagnons = Array.isArray(admission.dependances) ? admission.dependances : [];
|
|
267
|
+
admissions.set(nom, { motifs: new Set(motifs), paquets: new Set([nom, ...compagnons]) });
|
|
268
|
+
}
|
|
269
|
+
return { admissions, defauts };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Les motifs admis pour un morceau livre : ceux d'une admission dont les paquets couvrent TOUTES les sources du
|
|
274
|
+
* morceau (lues dans la carte de la construction). Un morceau qui mele du code de l'application, ou d'un autre paquet,
|
|
275
|
+
* n'en recoit aucun : le code de l'application reste juge en entier.
|
|
276
|
+
*
|
|
277
|
+
* @returns {{ motifs: Set<string>, paquet: string | null }}
|
|
278
|
+
*/
|
|
279
|
+
export function motifsAdmisDuMorceau(entrees, admissions) {
|
|
280
|
+
const paquets = entrees.map(paquetDeLEntree);
|
|
281
|
+
if (paquets.length === 0 || paquets.includes(null)) return { motifs: new Set(), paquet: null };
|
|
282
|
+
for (const [nom, admission] of admissions) {
|
|
283
|
+
if (paquets.every((paquet) => admission.paquets.has(paquet))) {
|
|
284
|
+
return { motifs: admission.motifs, paquet: nom };
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return { motifs: new Set(), paquet: null };
|
|
288
|
+
}
|
|
289
|
+
|
|
200
290
|
/**
|
|
201
291
|
* Les defauts d'un fichier livre `nom` (relatif au dossier livre) de contenu `contenu`. `role` :
|
|
202
292
|
* `remote`, `hote` ou `autonome` ; `partage` : le fichier est du code du cadre, partage par la
|
|
@@ -213,6 +303,7 @@ export function jugerUnFichierLivre(
|
|
|
213
303
|
declarees = [],
|
|
214
304
|
cadreNeutralise = false,
|
|
215
305
|
orchestrateurNeutralise = false,
|
|
306
|
+
admis = new Set(),
|
|
216
307
|
} = {},
|
|
217
308
|
) {
|
|
218
309
|
if (cadreNeutralise) {
|
|
@@ -235,10 +326,12 @@ export function jugerUnFichierLivre(
|
|
|
235
326
|
if (!/\.(?:m?js|html|css)$/.test(nom)) return defauts;
|
|
236
327
|
|
|
237
328
|
if (!nom.endsWith('.css')) {
|
|
238
|
-
for (const { motif, raison } of MOTIFS) {
|
|
239
|
-
const tolere =
|
|
240
|
-
(
|
|
241
|
-
|
|
329
|
+
for (const { id, motif, raison } of MOTIFS) {
|
|
330
|
+
const tolere =
|
|
331
|
+
(id !== undefined && admis.has(id)) ||
|
|
332
|
+
EXCEPTIONS_DU_CADRE.some(
|
|
333
|
+
(exception) => exception.fichier.test(nom) && exception.motif.source === motif.source,
|
|
334
|
+
);
|
|
242
335
|
if (!tolere && motif.test(contenu)) defauts.push(`${nom} : ${raison}.`);
|
|
243
336
|
}
|
|
244
337
|
for (const [, politique] of contenu.matchAll(CREATION_DE_POLITIQUE)) {
|
|
@@ -259,7 +352,8 @@ export function jugerUnFichierLivre(
|
|
|
259
352
|
|
|
260
353
|
if (!partage) {
|
|
261
354
|
const puits = role === 'remote' ? [...PUITS_DE_LA_SPA, ...PUITS_D_UN_REMOTE] : PUITS_DE_LA_SPA;
|
|
262
|
-
for (const { motif, raison } of puits) {
|
|
355
|
+
for (const { id, motif, raison } of puits) {
|
|
356
|
+
if (id !== undefined && admis.has(id)) continue;
|
|
263
357
|
if (motif.test(contenu)) defauts.push(`${nom} : ${raison}.`);
|
|
264
358
|
}
|
|
265
359
|
}
|
|
@@ -299,6 +393,20 @@ export default {
|
|
|
299
393
|
'(PLATFORM_WEB_CSP_TRUSTED_TYPES) : le BFF retire le remote tant qu elle n y est pas.',
|
|
300
394
|
);
|
|
301
395
|
const defauts = [];
|
|
396
|
+
// 0.11.7 : les admissions nommees, et la carte de la construction qui dit les sources de chaque morceau.
|
|
397
|
+
const { admissions, defauts: defautsDesAdmissions } = admissionsDuProjet(projet.paquet);
|
|
398
|
+
defauts.push(...defautsDesAdmissions);
|
|
399
|
+
const sorties =
|
|
400
|
+
admissions.size === 0
|
|
401
|
+
? {}
|
|
402
|
+
: (lireJson(join(dirname(projet.sortie), 'browser-stats.json'))?.outputs ?? null);
|
|
403
|
+
if (sorties === null) {
|
|
404
|
+
defauts.push(
|
|
405
|
+
"starterWeb.securiteAdmise : browser-stats.json absent (statsJson: true sur la cible esbuild d'angular.json) : " +
|
|
406
|
+
'aucune admission ne se juge sans la carte de la construction.',
|
|
407
|
+
);
|
|
408
|
+
}
|
|
409
|
+
const morceauxAdmis = [];
|
|
302
410
|
// Application autonome : la lecture de cookie du cadre, une seule fois dans tout le livrable.
|
|
303
411
|
const autonome = projet.role === 'autonome';
|
|
304
412
|
let lecturesDuCadre = 0;
|
|
@@ -314,6 +422,10 @@ export default {
|
|
|
314
422
|
lecturesDuCadre += duCadre;
|
|
315
423
|
const deLOrchestrateur = /\.m?js$/.test(nom) ? stockagesDeLOrchestrateur(contenu) : [];
|
|
316
424
|
strategies.push(...deLOrchestrateur);
|
|
425
|
+
const entrees = Object.keys(sorties?.[nom]?.inputs ?? {});
|
|
426
|
+
const { motifs: admis, paquet: admisPour } = motifsAdmisDuMorceau(entrees, admissions);
|
|
427
|
+
if (admisPour !== null)
|
|
428
|
+
morceauxAdmis.push(`${nom} (${admisPour} : ${[...admis].join(', ')})`);
|
|
317
429
|
defauts.push(
|
|
318
430
|
...jugerUnFichierLivre(nom, contenu, {
|
|
319
431
|
role: projet.role,
|
|
@@ -321,6 +433,7 @@ export default {
|
|
|
321
433
|
declarees,
|
|
322
434
|
cadreNeutralise: duCadre === 1,
|
|
323
435
|
orchestrateurNeutralise: deLOrchestrateur.length > 0,
|
|
436
|
+
admis,
|
|
324
437
|
}),
|
|
325
438
|
);
|
|
326
439
|
}
|
|
@@ -339,6 +452,16 @@ export default {
|
|
|
339
452
|
);
|
|
340
453
|
}
|
|
341
454
|
}
|
|
455
|
+
for (const nom of admissions.keys()) {
|
|
456
|
+
if (!morceauxAdmis.some((morceau) => morceau.includes(`(${nom} :`))) {
|
|
457
|
+
avertissements.push(
|
|
458
|
+
`starterWeb.securiteAdmise["${nom}"] : aucun morceau livre ne contient ce seul paquet ; l'admission ne sert a rien.`,
|
|
459
|
+
);
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (morceauxAdmis.length > 0) {
|
|
463
|
+
avertissements.push(`admissions nommees : ${morceauxAdmis.join(' ; ')}.`);
|
|
464
|
+
}
|
|
342
465
|
return {
|
|
343
466
|
defauts,
|
|
344
467
|
avertissements,
|