fg-devkit 1.28.2 → 1.29.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.
- package/README.md +8 -17
- package/configs/dependency-policy.vue-workspace.json +9 -9
- package/package.json +1 -2
- package/scaffold/vue/package.json +6 -6
- package/scripts/audit-override-commit.mjs +50 -34
- package/scripts/fg-devkit.mjs +0 -10
- package/scripts/lib/audit-fix-plan.mjs +85 -18
- package/scripts/lib/audit-fix-plan.test.mjs +50 -27
- package/configs/dependency-check-suppressions.xml +0 -36
- package/scripts/sync-dependency-check-suppressions-commit.mjs +0 -110
package/README.md
CHANGED
|
@@ -65,18 +65,6 @@ Both use fg-devkit's shared [`configs/fallowrc.json`](configs/fallowrc.json), so
|
|
|
65
65
|
the next fg-devkit bump. A repo that has its own `.fallowrc.json` (or `.fallowrc.jsonc`, `fallow.toml`, `.fallow.toml`)
|
|
66
66
|
uses that instead, and so does an explicit `--config`.
|
|
67
67
|
|
|
68
|
-
Write the OWASP Dependency-Check suppressions that apply to a consuming repo and commit them:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
pnpm exec fgd sync-dependency-check-suppressions-commit # only entries matching pnpm audit findings
|
|
72
|
-
pnpm exec fgd sync-dependency-check-suppressions-commit --all # every entry in the shared file
|
|
73
|
-
```
|
|
74
|
-
|
|
75
|
-
The command runs `pnpm audit` and keeps only the active (not expired) `<suppress>` entries from fg-devkit's shared
|
|
76
|
-
`configs/dependency-check-suppressions.xml` whose GHSA id is reported in the repo, so each repo accepts exactly the
|
|
77
|
-
risks it has. OWASP Dependency-Check reads the file when it is configured as a suppression file. Add another `<suppress>` element in the shared fg-devkit file for each additional accepted finding.
|
|
78
|
-
The command overwrites the consumer copy; if nothing applies and the repo has no copy, no file is created.
|
|
79
|
-
|
|
80
68
|
To sync standard consumer scripts to `fgd` commands:
|
|
81
69
|
|
|
82
70
|
```bash
|
|
@@ -183,7 +171,7 @@ Fix `pnpm audit` findings with the smallest change, verify, and auto-commit:
|
|
|
183
171
|
pnpm exec fgd audit-override-commit # refresh, override, prune, verify, commit
|
|
184
172
|
pnpm exec fgd audit-override-commit --no-verify # skip lint/typecheck/test/build (git hooks still run)
|
|
185
173
|
pnpm exec fgd audit-override-commit --no-commit # leave the changes in the working tree
|
|
186
|
-
pnpm exec fgd audit-override-commit --allow-major --allow-young --bump-direct # force without asking
|
|
174
|
+
pnpm exec fgd audit-override-commit --allow-major --allow-young --bump-direct --allow-unpatched # force without asking
|
|
187
175
|
```
|
|
188
176
|
|
|
189
177
|
1. Runs `pnpm audit` and refreshes the vulnerable packages in the lockfile (`pnpm update <names>`).
|
|
@@ -201,10 +189,13 @@ pnpm exec fgd audit-override-commit --allow-major --allow-young --bump-direct #
|
|
|
201
189
|
4. Prunes overrides that no longer change the audit.
|
|
202
190
|
5. Removes `minimumReleaseAgeExclude` entries that are no longer needed (see `prune-release-age-exclude` below).
|
|
203
191
|
This step also runs when `pnpm audit` is clean.
|
|
204
|
-
6.
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
192
|
+
6. Updates the repo's `dependency-check-suppressions.xml`, which OWASP Dependency-Check reads:
|
|
193
|
+
- for each high or critical finding with no patched version you are asked whether to accept it (without a
|
|
194
|
+
terminal only with `--allow-unpatched`). An accepted finding gets a `<suppress>` entry valid for 30 days.
|
|
195
|
+
The date is never pushed forward, so the finding comes back if it is still unpatched. To accept it for longer,
|
|
196
|
+
review it, change `until` and remove the `Auto-accepted by fg-devkit` line from its notes.
|
|
197
|
+
- entries whose finding is no longer reported are removed, and the file is removed when no entry is left.
|
|
198
|
+
- other findings with no patch are listed so you can add an entry by hand.
|
|
208
199
|
7. Installs and runs the repo's `lint-check`, `lint:ts`, `test`, `test:unit` and `build` scripts when present.
|
|
209
200
|
Nothing is committed if a check fails.
|
|
210
201
|
8. Commits the lockfile refresh (`vuln(package): refresh lockfile` / `vuln(): refresh N packages in lockfile`),
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"@lexical/rich-text": "0.53.0",
|
|
19
19
|
"@piwikpro/vue-piwik-pro": "2.0.0",
|
|
20
20
|
"@sentry/vue": "11.10.0",
|
|
21
|
-
"@tanstack/vue-query": "5.105.
|
|
21
|
+
"@tanstack/vue-query": "5.105.3",
|
|
22
22
|
"@unhead/vue": "3.4.3",
|
|
23
23
|
"@vuelidate/core": "2.0.3",
|
|
24
24
|
"@vuelidate/validators": "2.0.4",
|
|
@@ -29,7 +29,7 @@
|
|
|
29
29
|
"date-fns": "4.4.0",
|
|
30
30
|
"element-plus": "2.14.7",
|
|
31
31
|
"exceljs": "4.4.0",
|
|
32
|
-
"fds-vue-core": "
|
|
32
|
+
"fds-vue-core": "9.1.0",
|
|
33
33
|
"imask": "7.6.1",
|
|
34
34
|
"lexical": "0.53.0",
|
|
35
35
|
"libphonenumber-js": "1.13.15",
|
|
@@ -57,16 +57,16 @@
|
|
|
57
57
|
"@storybook/addon-vitest": "10.6.5",
|
|
58
58
|
"@storybook/vue3": "10.6.5",
|
|
59
59
|
"@storybook/vue3-vite": "10.6.5",
|
|
60
|
-
"@tanstack/vue-query-devtools": "6.3.
|
|
60
|
+
"@tanstack/vue-query-devtools": "6.3.9",
|
|
61
61
|
"@types/lodash-es": "4.17.12",
|
|
62
62
|
"@types/node": "26.6.6",
|
|
63
63
|
"@vitejs/plugin-vue": "6.0.10",
|
|
64
|
-
"@vitest/browser": "5.0
|
|
65
|
-
"@vitest/browser-playwright": "5.0
|
|
64
|
+
"@vitest/browser": "5.1.0",
|
|
65
|
+
"@vitest/browser-playwright": "5.1.0",
|
|
66
66
|
"@vue/compiler-dom": "3.5.44",
|
|
67
67
|
"@vue/language-core": "3.3.14",
|
|
68
68
|
"@vue/test-utils": "2.5.1",
|
|
69
|
-
"fg-devkit": "1.28.
|
|
69
|
+
"fg-devkit": "1.28.2",
|
|
70
70
|
"happy-dom": "21.0.4",
|
|
71
71
|
"jsdom": "30.1.3",
|
|
72
72
|
"randexp": "0.6.1",
|
|
@@ -77,15 +77,15 @@
|
|
|
77
77
|
"vite-plugin-dts": "5.1.2",
|
|
78
78
|
"vite-plugin-vue-devtools": "8.2.1",
|
|
79
79
|
"vite-plugin-vue-inspector": "7.0.0",
|
|
80
|
-
"vitest": "5.0
|
|
80
|
+
"vitest": "5.1.0"
|
|
81
81
|
}
|
|
82
82
|
},
|
|
83
83
|
"managedAnywhere": {
|
|
84
|
-
"@tailwindcss/vite": "4.3.
|
|
84
|
+
"@tailwindcss/vite": "4.3.4",
|
|
85
85
|
"@vue/devtools-api": "8.2.1",
|
|
86
86
|
"dotenv": "18.0.9",
|
|
87
87
|
"playwright": "1.64.1",
|
|
88
|
-
"tailwindcss": "4.3.
|
|
88
|
+
"tailwindcss": "4.3.4",
|
|
89
89
|
"vue": "3.5.44",
|
|
90
90
|
"vue-i18n": "11.4.17",
|
|
91
91
|
"vue-router": "5.4.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "fg-devkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.29.1",
|
|
4
4
|
"description": "Shared configs, scripts, and DX tooling for Vue / TypeScript frontend repos",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"./lefthook.yml": "./configs/lefthook.yml",
|
|
20
20
|
"./vscode-settings.json": "./configs/vscode-settings.json",
|
|
21
21
|
"./dependency-policy.vue-workspace.json": "./configs/dependency-policy.vue-workspace.json",
|
|
22
|
-
"./dependency-check-suppressions.xml": "./configs/dependency-check-suppressions.xml",
|
|
23
22
|
"./scripts/sync-vscode-settings.mjs": "./scripts/sync-vscode-settings.mjs",
|
|
24
23
|
"./scripts/fg-devkit.mjs": "./scripts/fg-devkit.mjs",
|
|
25
24
|
"./release-notes/parseChangelog": "./release-notes/parseChangelog.ts",
|
|
@@ -15,13 +15,13 @@
|
|
|
15
15
|
"poeditor:upload": "pnpm exec fg-devkit poeditor-upload"
|
|
16
16
|
},
|
|
17
17
|
"dependencies": {
|
|
18
|
-
"@tailwindcss/vite": "4.3.
|
|
19
|
-
"@tanstack/vue-query": "5.105.
|
|
18
|
+
"@tailwindcss/vite": "4.3.4",
|
|
19
|
+
"@tanstack/vue-query": "5.105.3",
|
|
20
20
|
"axios": "1.21.0",
|
|
21
21
|
"content-disposition-header": "0.6.0",
|
|
22
22
|
"date-fns": "4.4.0",
|
|
23
23
|
"dotenv": "18.0.9",
|
|
24
|
-
"fds-vue-core": "
|
|
24
|
+
"fds-vue-core": "9.1.0",
|
|
25
25
|
"pinia": "4.1.0",
|
|
26
26
|
"qrcode.vue": "3.11.0",
|
|
27
27
|
"vue": "3.5.44",
|
|
@@ -30,11 +30,11 @@
|
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|
|
32
32
|
"@intlify/unplugin-vue-i18n": "11.2.8",
|
|
33
|
-
"@tanstack/vue-query-devtools": "6.3.
|
|
33
|
+
"@tanstack/vue-query-devtools": "6.3.9",
|
|
34
34
|
"@types/node": "26.6.6",
|
|
35
|
-
"fg-devkit": "1.
|
|
35
|
+
"fg-devkit": "1.29.1",
|
|
36
36
|
"@vitejs/plugin-vue": "6.0.10",
|
|
37
|
-
"tailwindcss": "4.3.
|
|
37
|
+
"tailwindcss": "4.3.4",
|
|
38
38
|
"tsx": "4.23.15",
|
|
39
39
|
"vite": "8.3.5",
|
|
40
40
|
"vite-plugin-vue-inspector": "7.0.0"
|
|
@@ -16,16 +16,17 @@
|
|
|
16
16
|
* 4. Prune overrides that no longer change the audit (prune-overrides --fix).
|
|
17
17
|
* 5. Prune minimumReleaseAgeExclude entries that are no longer needed
|
|
18
18
|
* (prune-release-age-exclude --fix). Runs even when the audit is clean.
|
|
19
|
-
* 6.
|
|
20
|
-
*
|
|
21
|
-
*
|
|
19
|
+
* 6. Update the repo's dependency-check-suppressions.xml: entries whose finding is no
|
|
20
|
+
* longer reported are dropped, and high/critical findings with no patched version and
|
|
21
|
+
* no entry are asked about (or accepted with --allow-unpatched) and get an entry valid
|
|
22
|
+
* for 30 days that is never renewed. The file is removed when no entry applies.
|
|
22
23
|
* 7. Install and run the repo's checks (lint-check, lint:ts, test, test:unit, build).
|
|
23
24
|
* 8. Commit the refresh, the direct bumps, the overrides, the release-age excludes and
|
|
24
25
|
* the suppressions file separately.
|
|
25
26
|
*
|
|
26
27
|
* Usage:
|
|
27
28
|
* pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
|
|
28
|
-
* [--allow-major] [--allow-young] [--bump-direct]
|
|
29
|
+
* [--allow-major] [--allow-young] [--bump-direct] [--allow-unpatched]
|
|
29
30
|
*/
|
|
30
31
|
|
|
31
32
|
import fs from 'node:fs'
|
|
@@ -34,12 +35,13 @@ import { spawnSync } from 'node:child_process'
|
|
|
34
35
|
import { fileURLToPath } from 'node:url'
|
|
35
36
|
import { parseArgs } from 'node:util'
|
|
36
37
|
import {
|
|
38
|
+
AUTO_SUPPRESSION_DAYS,
|
|
39
|
+
AUTO_SUPPRESSION_SEVERITIES,
|
|
37
40
|
DEFAULT_MINIMUM_RELEASE_AGE_MINUTES,
|
|
38
|
-
activeSuppressedGhsas,
|
|
39
41
|
addReleaseAgeExcludes,
|
|
40
42
|
buildRefreshCommitMessage,
|
|
43
|
+
buildRepoSuppressions,
|
|
41
44
|
bumpDependencySpec,
|
|
42
|
-
filterSuppressions,
|
|
43
45
|
diffLockfileVersions,
|
|
44
46
|
isReleaseAgeExcluded,
|
|
45
47
|
isSameReleaseLine,
|
|
@@ -75,6 +77,7 @@ const { values: args } = parseArgs({
|
|
|
75
77
|
'allow-major': { type: 'boolean', default: false },
|
|
76
78
|
'allow-young': { type: 'boolean', default: false },
|
|
77
79
|
'bump-direct': { type: 'boolean', default: false },
|
|
80
|
+
'allow-unpatched': { type: 'boolean', default: false },
|
|
78
81
|
help: { type: 'boolean', short: 'h', default: false },
|
|
79
82
|
},
|
|
80
83
|
strict: true,
|
|
@@ -82,12 +85,13 @@ const { values: args } = parseArgs({
|
|
|
82
85
|
|
|
83
86
|
if (args.help) {
|
|
84
87
|
console.log(`Usage: pnpm exec fg-devkit audit-override-commit [--no-verify] [--no-commit]
|
|
85
|
-
[--allow-major] [--allow-young] [--bump-direct]
|
|
88
|
+
[--allow-major] [--allow-young] [--bump-direct] [--allow-unpatched]
|
|
86
89
|
|
|
87
90
|
Refreshes vulnerable packages in the lockfile within their existing ranges, adds a
|
|
88
91
|
tight override only where a parent's range blocks the patched version, prunes unused
|
|
89
|
-
overrides,
|
|
90
|
-
|
|
92
|
+
overrides, updates dependency-check-suppressions.xml (accepts high/critical findings
|
|
93
|
+
with no patch for 30 days, drops entries for fixed findings), runs the repo's checks,
|
|
94
|
+
and commits each kind of change separately.
|
|
91
95
|
|
|
92
96
|
Findings that need a decision are asked about one package at a time in a terminal.
|
|
93
97
|
Without a terminal they are only reported, unless a flag below says yes for them.
|
|
@@ -96,7 +100,10 @@ Without a terminal they are only reported, unless a flag below says yes for them
|
|
|
96
100
|
--no-commit Leave the changes in the working tree
|
|
97
101
|
--allow-major Override even when the patch is only in a new major
|
|
98
102
|
--allow-young Allow patches younger than minimumReleaseAge (adds minimumReleaseAgeExclude)
|
|
99
|
-
--bump-direct Bump vulnerable direct dependencies in package.json
|
|
103
|
+
--bump-direct Bump vulnerable direct dependencies in package.json
|
|
104
|
+
--allow-unpatched
|
|
105
|
+
Accept high/critical findings with no patched version for 30 days
|
|
106
|
+
in dependency-check-suppressions.xml`)
|
|
100
107
|
process.exit(0)
|
|
101
108
|
}
|
|
102
109
|
|
|
@@ -409,29 +416,32 @@ function pruneOverrides() {
|
|
|
409
416
|
}
|
|
410
417
|
|
|
411
418
|
/**
|
|
412
|
-
*
|
|
413
|
-
*
|
|
419
|
+
* Keep the entries of the repo's dependency-check-suppressions.xml that still match a finding
|
|
420
|
+
* and add entries for the high and critical findings with no patched version that are
|
|
421
|
+
* accepted. Removed when no entry applies.
|
|
414
422
|
*/
|
|
415
|
-
function syncSuppressions(remaining) {
|
|
416
|
-
const
|
|
417
|
-
|
|
418
|
-
const
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
423
|
+
async function syncSuppressions(remaining) {
|
|
424
|
+
const existing = readFile(SUPPRESSIONS_FILE)
|
|
425
|
+
const { candidates } = buildRepoSuppressions({ existing, advisories: remaining })
|
|
426
|
+
const accept = new Set()
|
|
427
|
+
for (const advisory of candidates) {
|
|
428
|
+
if (accept.has(advisory.ghsa)) continue
|
|
429
|
+
const message = `${advisory.name} ${advisory.versions.join(', ')} has no patched version (${advisory.severity}, ${advisory.ghsa}). Accept it in ${SUPPRESSIONS_FILE} for ${AUTO_SUPPRESSION_DAYS} days?`
|
|
430
|
+
if (await decide(message, 'allow-unpatched')) accept.add(advisory.ghsa)
|
|
431
|
+
}
|
|
432
|
+
const result = buildRepoSuppressions({ existing, advisories: remaining, accept })
|
|
433
|
+
if (result.content === null) {
|
|
434
|
+
if (existing !== undefined) {
|
|
435
|
+
fs.rmSync(path.join(cwd, SUPPRESSIONS_FILE))
|
|
436
|
+
suppressionsWritten = true
|
|
437
|
+
console.log(`${TAG} Removed ${SUPPRESSIONS_FILE}, no suppression applies to this repo.`)
|
|
438
|
+
}
|
|
439
|
+
} else if (existing !== result.content) {
|
|
440
|
+
fs.writeFileSync(path.join(cwd, SUPPRESSIONS_FILE), result.content)
|
|
431
441
|
suppressionsWritten = true
|
|
432
|
-
console.log(`${TAG}
|
|
442
|
+
console.log(`${TAG} Updated ${SUPPRESSIONS_FILE}.`)
|
|
433
443
|
}
|
|
434
|
-
return
|
|
444
|
+
return result
|
|
435
445
|
}
|
|
436
446
|
|
|
437
447
|
function verify() {
|
|
@@ -451,7 +461,9 @@ function verify() {
|
|
|
451
461
|
}
|
|
452
462
|
|
|
453
463
|
function printSummary({ fixedByRefresh, plan, forced, heldBack, applied, pruned, remaining, suppressions, excludeChanges }) {
|
|
454
|
-
const suppressed = new Set(
|
|
464
|
+
const suppressed = new Set(
|
|
465
|
+
[...suppressions.covered, ...suppressions.autoAccepted].map((advisory) => advisory.ghsa),
|
|
466
|
+
)
|
|
455
467
|
const unique = (items) => [...new Set(items)]
|
|
456
468
|
const lines = []
|
|
457
469
|
const section = (title, items) => {
|
|
@@ -499,19 +511,23 @@ function printSummary({ fixedByRefresh, plan, forced, heldBack, applied, pruned,
|
|
|
499
511
|
.map((item) => `${item.name} ${item.from} -> ${item.to} (${item.ghsas.join(', ')})`),
|
|
500
512
|
)
|
|
501
513
|
section(
|
|
502
|
-
|
|
514
|
+
`No patched version and not suppressed (accept high/critical ones on a re-run or with --allow-unpatched, or add a <suppress> entry to ${SUPPRESSIONS_FILE} by hand):`,
|
|
503
515
|
plan.noPatch
|
|
504
516
|
.filter((item) => !item.ghsas.every((ghsa) => suppressed.has(ghsa)))
|
|
505
517
|
.map((item) => `${item.name} ${item.versions.join(', ')} (${item.ghsas.join(', ')})`),
|
|
506
518
|
)
|
|
507
519
|
section(
|
|
508
|
-
`Accepted in ${SUPPRESSIONS_FILE}
|
|
520
|
+
`Accepted in ${SUPPRESSIONS_FILE}:`,
|
|
509
521
|
unique(
|
|
510
522
|
suppressions.covered.map(
|
|
511
523
|
(advisory) => `${advisory.name} ${advisory.ghsa}${advisory.until ? `, until ${advisory.until}` : ''}`,
|
|
512
524
|
),
|
|
513
525
|
),
|
|
514
526
|
)
|
|
527
|
+
section(
|
|
528
|
+
`Accepted for ${AUTO_SUPPRESSION_DAYS} days in ${SUPPRESSIONS_FILE} (${[...AUTO_SUPPRESSION_SEVERITIES].join('/')}, no patched version, not renewed):`,
|
|
529
|
+
unique(suppressions.autoAccepted.map((advisory) => `${advisory.name} ${advisory.ghsa}, until ${advisory.until}`)),
|
|
530
|
+
)
|
|
515
531
|
section(
|
|
516
532
|
'Could not read the patched range, check manually:',
|
|
517
533
|
plan.unparseable.map((item) => `${item.name}: ${item.patched} (${item.ghsas.join(', ')})`),
|
|
@@ -596,7 +612,7 @@ async function main() {
|
|
|
596
612
|
|
|
597
613
|
// 5. Release-age excludes, 6. suppressions, 7. verify.
|
|
598
614
|
const excludeChanges = pruneReleaseAgeExclude()
|
|
599
|
-
const suppressions = syncSuppressions(remaining)
|
|
615
|
+
const suppressions = await syncSuppressions(remaining)
|
|
600
616
|
printSummary({ fixedByRefresh, plan, forced, heldBack, applied: allowed, pruned, remaining, suppressions, excludeChanges })
|
|
601
617
|
|
|
602
618
|
const final = snapshot()
|
package/scripts/fg-devkit.mjs
CHANGED
|
@@ -21,7 +21,6 @@
|
|
|
21
21
|
* pnpm exec fg-devkit prune-release-age-exclude [--fix] [--commit]
|
|
22
22
|
* pnpm exec fg-devkit sync-project-configs (VS Code + tooling sync; overwrites config files)
|
|
23
23
|
* pnpm exec fg-devkit sync-tooling-configs (tooling sync only; overwrites config files)
|
|
24
|
-
* pnpm exec fg-devkit sync-dependency-check-suppressions-commit [--all]
|
|
25
24
|
* pnpm exec fg-devkit sync-vscode-settings
|
|
26
25
|
* pnpm exec fg-devkit sync-consumer-scripts
|
|
27
26
|
* pnpm exec fg-devkit setup-lefthook
|
|
@@ -516,7 +515,6 @@ function runSyncConfigTarget(target) {
|
|
|
516
515
|
if (target === 'eslint') return syncFileFromTemplate('configs/eslint.sync-stub.js', 'eslint.config.js')
|
|
517
516
|
if (target === 'prettier') return syncFileFromTemplate('configs/prettier.sync-stub.js', 'prettier.config.js')
|
|
518
517
|
if (target === 'tsconfig') return syncFileFromTemplate('configs/tsconfig.app.sync.json', 'tsconfig.json')
|
|
519
|
-
if (target === 'dependency-check') return runNode('sync-dependency-check-suppressions-commit.mjs')
|
|
520
518
|
if (target === 'vue-inspector') return runNode('sync-vite-plugin-vue-inspector.mjs')
|
|
521
519
|
if (target === 'all') return runNode('sync-project-configs.mjs')
|
|
522
520
|
console.error(`[fg-devkit] Unknown config sync target: ${target}`)
|
|
@@ -547,7 +545,6 @@ const DIRECT_COMMANDS = {
|
|
|
547
545
|
'prune-release-age-exclude': 'prune-release-age-exclude.mjs',
|
|
548
546
|
'sync-project-configs': 'sync-project-configs.mjs',
|
|
549
547
|
'sync-tooling-configs': 'sync-tooling-configs.mjs',
|
|
550
|
-
'sync-dependency-check-suppressions-commit': 'sync-dependency-check-suppressions-commit.mjs',
|
|
551
548
|
'sync-vscode-settings': 'sync-vscode-settings.mjs',
|
|
552
549
|
'sync-vite-plugin-vue-inspector': 'sync-vite-plugin-vue-inspector.mjs',
|
|
553
550
|
'sync-consumer-scripts': 'sync-consumer-scripts.mjs',
|
|
@@ -611,8 +608,6 @@ Commands:
|
|
|
611
608
|
Report minimumReleaseAgeExclude entries that are no longer needed; --fix removes them; --commit also auto-commits
|
|
612
609
|
sync-project-configs VS Code + tooling sync (overwrites config files)
|
|
613
610
|
sync-tooling-configs Tooling sync only (overwrites config files)
|
|
614
|
-
sync-dependency-check-suppressions-commit [--all]
|
|
615
|
-
Write the dependency-check suppressions that match pnpm audit findings and auto-commit (--all: copy every entry)
|
|
616
611
|
sync-vscode-settings Copy shared VS Code settings into .vscode/settings.json
|
|
617
612
|
sync-vite-plugin-vue-inspector Add/pin vite-plugin-vue-inspector, wire vite.config, install, commit
|
|
618
613
|
sync-consumer-scripts Enforce standard fgd scripts in consumer package.json
|
|
@@ -1109,11 +1104,6 @@ async function runInteractiveMenu() {
|
|
|
1109
1104
|
{ name: 'ESLint', value: 'eslint', description: 'Overwrite eslint.config.js with the shared config' },
|
|
1110
1105
|
{ name: 'Prettier', value: 'prettier', description: 'Overwrite prettier.config.js with the shared config' },
|
|
1111
1106
|
{ name: 'tsconfig', value: 'tsconfig', description: 'Overwrite tsconfig.json with the shared config' },
|
|
1112
|
-
{
|
|
1113
|
-
name: autoCommit('Dependency-Check suppressions'),
|
|
1114
|
-
value: 'dependency-check',
|
|
1115
|
-
description: 'Write the suppressions that match pnpm audit findings',
|
|
1116
|
-
},
|
|
1117
1107
|
{
|
|
1118
1108
|
name: autoCommit('Vue inspector'),
|
|
1119
1109
|
value: 'vue-inspector',
|
|
@@ -537,29 +537,96 @@ export function parseSuppressions(xmlText, now = Date.now()) {
|
|
|
537
537
|
return entries
|
|
538
538
|
}
|
|
539
539
|
|
|
540
|
-
|
|
541
|
-
export
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
540
|
+
export const AUTO_SUPPRESSION_MARKER = 'Auto-accepted by fg-devkit'
|
|
541
|
+
export const AUTO_SUPPRESSION_SEVERITIES = new Set(['high', 'critical'])
|
|
542
|
+
export const AUTO_SUPPRESSION_DAYS = 30
|
|
543
|
+
|
|
544
|
+
const GHSA_PATTERN = /^GHSA-[0-9a-z]{4}-[0-9a-z]{4}-[0-9a-z]{4}$/
|
|
545
|
+
const SUPPRESSIONS_TEMPLATE = [
|
|
546
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
547
|
+
'<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">',
|
|
548
|
+
'</suppressions>',
|
|
549
|
+
'',
|
|
550
|
+
].join('\n')
|
|
551
|
+
|
|
552
|
+
function escapeRegExp(text) {
|
|
553
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
function renderAutoSuppression(advisory, until) {
|
|
557
|
+
return [
|
|
558
|
+
` <suppress until="${until}">`,
|
|
559
|
+
' <notes><![CDATA[',
|
|
560
|
+
` ${AUTO_SUPPRESSION_MARKER}: no patched version of ${advisory.name} exists.`,
|
|
561
|
+
` ${advisory.ghsa} (${advisory.severity}): ${advisory.title ?? ''}`.trimEnd(),
|
|
562
|
+
' Not renewed automatically. To accept the risk for longer, review it and',
|
|
563
|
+
' remove the first line of these notes so the entry is kept as written.',
|
|
564
|
+
' ]]></notes>',
|
|
565
|
+
` <filePath regex="true">.*pnpm-lock\\.yaml\\?${escapeRegExp(advisory.name)}$</filePath>`,
|
|
566
|
+
` <vulnerabilityName regex="true">.*${advisory.ghsa}.*</vulnerabilityName>`,
|
|
567
|
+
' </suppress>',
|
|
568
|
+
'',
|
|
569
|
+
].join('\n')
|
|
548
570
|
}
|
|
549
571
|
|
|
550
572
|
/**
|
|
551
|
-
* The
|
|
552
|
-
*
|
|
553
|
-
*
|
|
573
|
+
* The repo's dependency-check-suppressions.xml for the advisories `pnpm audit` still reports.
|
|
574
|
+
*
|
|
575
|
+
* Entries already in the file are kept while one of their GHSA ids is still reported, and
|
|
576
|
+
* dropped otherwise. High and critical advisories with no patched version and no entry are
|
|
577
|
+
* returned as `candidates`; those whose GHSA id is in `accept` get an auto-accepted entry valid
|
|
578
|
+
* for AUTO_SUPPRESSION_DAYS. Kept entries are never changed, so an auto-accepted `until` date is
|
|
579
|
+
* never pushed forward and an expired entry stays expired.
|
|
580
|
+
*
|
|
581
|
+
* @param {{ existing?: string, advisories: ReturnType<typeof normalizeAdvisories>, accept?: Set<string>, now?: number }} input
|
|
582
|
+
* @returns {{ content: string | null, covered: object[], autoAccepted: object[], candidates: object[], uncovered: object[] }}
|
|
583
|
+
* `content` is null when no entry applies (the repo needs no file).
|
|
554
584
|
*/
|
|
555
|
-
export function
|
|
556
|
-
const
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
585
|
+
export function buildRepoSuppressions({ existing, advisories, accept = new Set(), now = Date.now() }) {
|
|
586
|
+
const reported = new Set(advisories.map((advisory) => advisory.ghsa))
|
|
587
|
+
const entryByGhsa = new Map()
|
|
588
|
+
let content = existing ?? SUPPRESSIONS_TEMPLATE
|
|
589
|
+
let kept = 0
|
|
590
|
+
for (const entry of parseSuppressions(content, now)) {
|
|
591
|
+
if (!entry.ghsas.some((ghsa) => reported.has(ghsa))) {
|
|
592
|
+
content = content.replace(entry.text, '')
|
|
593
|
+
continue
|
|
594
|
+
}
|
|
595
|
+
kept += 1
|
|
596
|
+
for (const ghsa of entry.ghsas) entryByGhsa.set(ghsa, entry)
|
|
561
597
|
}
|
|
562
|
-
|
|
598
|
+
const newUntil = `${new Date(now + AUTO_SUPPRESSION_DAYS * 86400000).toISOString().slice(0, 10)}Z`
|
|
599
|
+
|
|
600
|
+
const covered = []
|
|
601
|
+
const autoAccepted = []
|
|
602
|
+
const candidates = []
|
|
603
|
+
const uncovered = []
|
|
604
|
+
const added = new Map()
|
|
605
|
+
for (const advisory of advisories) {
|
|
606
|
+
const entry = entryByGhsa.get(advisory.ghsa)
|
|
607
|
+
if (entry) {
|
|
608
|
+
if (!entry.active) uncovered.push(advisory)
|
|
609
|
+
else if (entry.text.includes(AUTO_SUPPRESSION_MARKER)) autoAccepted.push({ ...advisory, until: entry.until })
|
|
610
|
+
else covered.push({ ...advisory, until: entry.until })
|
|
611
|
+
continue
|
|
612
|
+
}
|
|
613
|
+
const canAutoAccept =
|
|
614
|
+
AUTO_SUPPRESSION_SEVERITIES.has(advisory.severity) &&
|
|
615
|
+
GHSA_PATTERN.test(advisory.ghsa) &&
|
|
616
|
+
lowestPatchedVersion(advisory.patched, '0.0.0').status === 'none'
|
|
617
|
+
if (canAutoAccept) candidates.push(advisory)
|
|
618
|
+
if (!canAutoAccept || !accept.has(advisory.ghsa)) {
|
|
619
|
+
uncovered.push(advisory)
|
|
620
|
+
continue
|
|
621
|
+
}
|
|
622
|
+
autoAccepted.push({ ...advisory, until: newUntil })
|
|
623
|
+
if (!added.has(advisory.ghsa)) added.set(advisory.ghsa, renderAutoSuppression(advisory, newUntil))
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
if (kept === 0 && added.size === 0) return { content: null, covered, autoAccepted, candidates, uncovered }
|
|
627
|
+
const closing = content.lastIndexOf('</suppressions>')
|
|
628
|
+
content = `${content.slice(0, closing)}${[...added.values()].join('')}${content.slice(closing)}`
|
|
629
|
+
return { content, covered, autoAccepted, candidates, uncovered }
|
|
563
630
|
}
|
|
564
631
|
|
|
565
632
|
// ---------------------------------------------------------------------------
|
|
@@ -2,18 +2,18 @@ import assert from 'node:assert/strict'
|
|
|
2
2
|
import test from 'node:test'
|
|
3
3
|
|
|
4
4
|
import {
|
|
5
|
-
activeSuppressedGhsas,
|
|
6
5
|
addReleaseAgeExcludes,
|
|
7
6
|
buildRefreshCommitMessage,
|
|
7
|
+
buildRepoSuppressions,
|
|
8
8
|
bumpDependencySpec,
|
|
9
9
|
compareVersions,
|
|
10
10
|
diffLockfileVersions,
|
|
11
|
-
filterSuppressions,
|
|
12
11
|
isReleaseAgeExcluded,
|
|
13
12
|
isSameReleaseLine,
|
|
14
13
|
lowestPatchedVersion,
|
|
15
14
|
normalizeAdvisories,
|
|
16
15
|
overrideKey,
|
|
16
|
+
parseSuppressions,
|
|
17
17
|
planOverrides,
|
|
18
18
|
planReleaseAgeExcludePrune,
|
|
19
19
|
releaseLineFloor,
|
|
@@ -383,7 +383,7 @@ test('buildRefreshCommitMessage follows the vuln() convention', () => {
|
|
|
383
383
|
)
|
|
384
384
|
})
|
|
385
385
|
|
|
386
|
-
test('
|
|
386
|
+
test('parseSuppressions reads GHSA ids from matchers only and marks expired entries inactive', () => {
|
|
387
387
|
const xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
388
388
|
<suppressions>
|
|
389
389
|
<suppress until="2026-12-01Z">
|
|
@@ -400,46 +400,69 @@ test('activeSuppressedGhsas reads active entries and ignores notes and expired o
|
|
|
400
400
|
</suppressions>`
|
|
401
401
|
const now = Date.parse('2026-10-06T12:00:00Z')
|
|
402
402
|
assert.deepEqual(
|
|
403
|
-
|
|
403
|
+
parseSuppressions(xml, now).map(({ ghsas, until, active }) => ({ ghsas, until, active })),
|
|
404
404
|
[
|
|
405
|
-
['GHSA-vfj7-8cjw-p6xm', '2026-12-01Z'
|
|
406
|
-
['GHSA-
|
|
405
|
+
{ ghsas: ['GHSA-vfj7-8cjw-p6xm'], until: '2026-12-01Z', active: true },
|
|
406
|
+
{ ghsas: ['GHSA-hp3w-g68c-fv3c'], until: '2026-01-01Z', active: false },
|
|
407
|
+
{ ghsas: ['GHSA-aaaa-bbbb-cccc'], until: undefined, active: true },
|
|
407
408
|
],
|
|
408
409
|
)
|
|
409
|
-
assert.equal(
|
|
410
|
+
assert.equal(parseSuppressions(xml, Date.parse('2026-12-02T00:00:00Z'))[0].active, false)
|
|
410
411
|
})
|
|
411
412
|
|
|
412
|
-
test('
|
|
413
|
-
const
|
|
413
|
+
test('buildRepoSuppressions keeps reported entries and adds accepted unpatched high and critical findings', () => {
|
|
414
|
+
const now = Date.parse('2026-11-03T12:00:00Z')
|
|
415
|
+
const finding = (ghsa, name, severity, patched) => ({ ...advisory({ name, ghsa, patched, versions: ['1.0.0'] }), severity })
|
|
416
|
+
const existing = [
|
|
414
417
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
415
418
|
'<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">',
|
|
416
419
|
' <suppress until="2026-12-01Z">',
|
|
417
|
-
' <
|
|
418
|
-
' <vulnerabilityName regex="true">.*(GHSA-vfj7-8cjw-p6xm|1240992).*</vulnerabilityName>',
|
|
420
|
+
' <vulnerabilityName regex="true">.*(GHSA-vfj7-8cjw-p6xm).*</vulnerabilityName>',
|
|
419
421
|
' </suppress>',
|
|
420
422
|
' <suppress until="2026-12-01Z">',
|
|
421
|
-
' <notes><![CDATA[ sprintf-js ]]></notes>',
|
|
422
423
|
' <vulnerabilityName regex="true">.*(GHSA-hp3w-g68c-fv3c).*</vulnerabilityName>',
|
|
423
424
|
' </suppress>',
|
|
424
425
|
'</suppressions>',
|
|
425
426
|
'',
|
|
426
427
|
].join('\n')
|
|
427
|
-
const
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
'</suppressions>',
|
|
438
|
-
'',
|
|
439
|
-
].join('\n'),
|
|
428
|
+
const findings = [
|
|
429
|
+
finding('GHSA-vfj7-8cjw-p6xm', 'braces', 'high', '<0.0.0'),
|
|
430
|
+
finding('GHSA-aaaa-bbbb-cccc', '@scope/pkg.js', 'critical', '<0.0.0'),
|
|
431
|
+
finding('GHSA-dddd-eeee-ffff', 'moderate-pkg', 'moderate', '<0.0.0'),
|
|
432
|
+
finding('GHSA-gggg-hhhh-iiii', 'fixable', 'high', '>=2.0.0'),
|
|
433
|
+
]
|
|
434
|
+
const unaccepted = buildRepoSuppressions({ existing, advisories: findings, now })
|
|
435
|
+
assert.deepEqual(
|
|
436
|
+
unaccepted.candidates.map((item) => item.ghsa),
|
|
437
|
+
['GHSA-aaaa-bbbb-cccc'],
|
|
440
438
|
)
|
|
441
|
-
|
|
442
|
-
assert.doesNotMatch(
|
|
439
|
+
assert.equal(unaccepted.autoAccepted.length, 0)
|
|
440
|
+
assert.doesNotMatch(unaccepted.content, /GHSA-aaaa/)
|
|
441
|
+
|
|
442
|
+
const first = buildRepoSuppressions({ existing, advisories: findings, accept: new Set(['GHSA-aaaa-bbbb-cccc']), now })
|
|
443
|
+
assert.deepEqual(
|
|
444
|
+
first.covered.map((item) => item.ghsa),
|
|
445
|
+
['GHSA-vfj7-8cjw-p6xm'],
|
|
446
|
+
)
|
|
447
|
+
assert.deepEqual(
|
|
448
|
+
first.autoAccepted.map((item) => [item.ghsa, item.until]),
|
|
449
|
+
[['GHSA-aaaa-bbbb-cccc', '2026-12-03Z']],
|
|
450
|
+
)
|
|
451
|
+
assert.deepEqual(
|
|
452
|
+
first.uncovered.map((item) => item.ghsa),
|
|
453
|
+
['GHSA-dddd-eeee-ffff', 'GHSA-gggg-hhhh-iiii'],
|
|
454
|
+
)
|
|
455
|
+
assert.doesNotMatch(first.content, /GHSA-hp3w/)
|
|
456
|
+
assert.match(first.content, /pnpm-lock\\.yaml\\\?@scope\/pkg\\.js\$/)
|
|
457
|
+
|
|
458
|
+
const later = buildRepoSuppressions({ existing: first.content, advisories: findings, now: now + 10 * 86400000 })
|
|
459
|
+
assert.equal(later.content, first.content)
|
|
460
|
+
const expired = buildRepoSuppressions({ existing: first.content, advisories: findings, now: now + 40 * 86400000 })
|
|
461
|
+
assert.equal(expired.content, first.content)
|
|
462
|
+
assert.equal(expired.autoAccepted.length, 0)
|
|
463
|
+
|
|
464
|
+
assert.equal(buildRepoSuppressions({ existing: first.content, advisories: [], now }).content, null)
|
|
465
|
+
assert.equal(buildRepoSuppressions({ advisories: [findings[2]], now }).content, null)
|
|
443
466
|
})
|
|
444
467
|
|
|
445
468
|
test('planReleaseAgeExcludePrune drops versions that are old enough or no longer installed', () => {
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
-
<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">
|
|
3
|
-
<suppress until="2026-12-01Z">
|
|
4
|
-
<notes><![CDATA[
|
|
5
|
-
braces <= 3.0.3 — GHSA-vfj7-8cjw-p6xm (stack exhaustion via deeply nested patterns).
|
|
6
|
-
No patched release exists; 3.0.3 is the latest version.
|
|
7
|
-
Transitive devDependency only, via fast-glob and micromatch
|
|
8
|
-
(@intlify/unplugin-vue-i18n and fg-devkit). Not shipped in the app runtime.
|
|
9
|
-
Risk accepted until braces publishes a release after 3.0.3.
|
|
10
|
-
|
|
11
|
-
pnpm audit paths do not equal the module name, so Dependency-Check creates
|
|
12
|
-
a virtual dependency at pnpm-lock.yaml?braces with no package URL version.
|
|
13
|
-
Jenkins shows the id as NPMGHSA-vfj7-8cjw-p6xm (source NPM + GHSA id).
|
|
14
|
-
Older analyzers may use the numeric advisory id 1240992.
|
|
15
|
-
]]></notes>
|
|
16
|
-
<filePath regex="true">.*pnpm-lock\.yaml\?braces$</filePath>
|
|
17
|
-
<vulnerabilityName regex="true">.*(GHSA-vfj7-8cjw-p6xm|1240992).*</vulnerabilityName>
|
|
18
|
-
</suppress>
|
|
19
|
-
<suppress until="2026-12-01Z">
|
|
20
|
-
<notes><![CDATA[
|
|
21
|
-
sprintf-js <=1.1.3 — GHSA-hp3w-g68c-fv3c / CVE-2026-97058 (denial of service
|
|
22
|
-
through unbounded precision specifiers). No patched release exists; 1.1.3
|
|
23
|
-
is the latest version. Transitive optional devDependency only, via
|
|
24
|
-
vite-plugin-dts > @microsoft/api-extractor > @rushstack/ts-command-line >
|
|
25
|
-
argparse. Not shipped in the library runtime. Risk accepted until
|
|
26
|
-
sprintf-js publishes a release after 1.1.3.
|
|
27
|
-
|
|
28
|
-
pnpm audit paths do not equal the module name, so Dependency-Check creates
|
|
29
|
-
a virtual dependency at pnpm-lock.yaml?sprintf-js with no package URL version.
|
|
30
|
-
Jenkins shows the id as NPMGHSA-hp3w-g68c-fv3c (source NPM + GHSA id).
|
|
31
|
-
Older analyzers may use the numeric advisory id 1241202.
|
|
32
|
-
]]></notes>
|
|
33
|
-
<filePath regex="true">.*pnpm-lock\.yaml\?sprintf-js$</filePath>
|
|
34
|
-
<vulnerabilityName regex="true">.*(GHSA-hp3w-g68c-fv3c|CVE-2026-97058|1241202).*</vulnerabilityName>
|
|
35
|
-
</suppress>
|
|
36
|
-
</suppressions>
|
|
@@ -1,110 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
/**
|
|
3
|
-
* Write dependency-check-suppressions.xml into the consumer repo and auto-commit.
|
|
4
|
-
*
|
|
5
|
-
* Only the active <suppress> entries from fg-devkit's shared file that match a finding
|
|
6
|
-
* `pnpm audit` still reports in this repo are written, so OWASP Dependency-Check accepts
|
|
7
|
-
* exactly the risks this repo has. Pass --all to copy the whole shared file instead.
|
|
8
|
-
*
|
|
9
|
-
* Usage:
|
|
10
|
-
* node node_modules/fg-devkit/scripts/sync-dependency-check-suppressions-commit.mjs [--all]
|
|
11
|
-
* pnpm exec fg-devkit sync-dependency-check-suppressions-commit [--all]
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import fs from 'node:fs'
|
|
15
|
-
import path from 'node:path'
|
|
16
|
-
import { spawnSync } from 'node:child_process'
|
|
17
|
-
import { parseArgs } from 'node:util'
|
|
18
|
-
import { filterSuppressions, normalizeAdvisories, parseAuditJson, parseSuppressions } from './lib/audit-fix-plan.mjs'
|
|
19
|
-
import { fileDiffersFromHead, findFgDevkitPackageRoot, runGit } from './lib/common.mjs'
|
|
20
|
-
|
|
21
|
-
const TAG = '[sync-dependency-check-suppressions]'
|
|
22
|
-
const FILE_NAME = 'dependency-check-suppressions.xml'
|
|
23
|
-
const COMMIT_MESSAGE = 'chore(repo): sync dependency-check suppressions'
|
|
24
|
-
const projectRoot = process.cwd()
|
|
25
|
-
const GIT = { cwd: projectRoot, tag: TAG }
|
|
26
|
-
|
|
27
|
-
const { values: args } = parseArgs({
|
|
28
|
-
options: {
|
|
29
|
-
all: { type: 'boolean', default: false },
|
|
30
|
-
help: { type: 'boolean', short: 'h', default: false },
|
|
31
|
-
},
|
|
32
|
-
strict: true,
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
if (args.help) {
|
|
36
|
-
console.log(`Usage: pnpm exec fg-devkit sync-dependency-check-suppressions-commit [--all]
|
|
37
|
-
|
|
38
|
-
Writes ${FILE_NAME} with the entries from fg-devkit that match findings
|
|
39
|
-
pnpm audit reports in this repo, and auto-commits it.
|
|
40
|
-
|
|
41
|
-
--all Copy every entry from fg-devkit's shared file (no pnpm audit)`)
|
|
42
|
-
process.exit(0)
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** GHSA ids `pnpm audit` reports in this repo. Exits on audit failure so nothing is written blindly. */
|
|
46
|
-
function reportedGhsas() {
|
|
47
|
-
const result = spawnSync('pnpm', ['audit', '--json'], {
|
|
48
|
-
cwd: projectRoot,
|
|
49
|
-
env: process.env,
|
|
50
|
-
encoding: 'utf8',
|
|
51
|
-
maxBuffer: 50 * 1024 * 1024,
|
|
52
|
-
})
|
|
53
|
-
if (result.error || (result.status !== 0 && result.status !== 1)) {
|
|
54
|
-
console.error(`${TAG} pnpm audit failed, ${FILE_NAME} was not changed.`)
|
|
55
|
-
console.error((result.error?.message ?? result.stderr ?? result.stdout ?? '').trim())
|
|
56
|
-
process.exit(1)
|
|
57
|
-
}
|
|
58
|
-
return new Set(normalizeAdvisories(parseAuditJson(result.stdout)).map((advisory) => advisory.ghsa))
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
const fgRoot = findFgDevkitPackageRoot(projectRoot)
|
|
62
|
-
if (!fgRoot) {
|
|
63
|
-
console.error(`${TAG} Could not find fg-devkit (install fg-devkit or a package that bundles it, e.g. fds-vue-core).`)
|
|
64
|
-
process.exit(1)
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const sourcePath = path.join(fgRoot, 'configs', FILE_NAME)
|
|
68
|
-
if (!fs.existsSync(sourcePath)) {
|
|
69
|
-
console.error(`${TAG} Missing template: ${sourcePath}`)
|
|
70
|
-
process.exit(1)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const shared = fs.readFileSync(sourcePath, 'utf8')
|
|
74
|
-
const targetPath = path.join(projectRoot, FILE_NAME)
|
|
75
|
-
let content = shared
|
|
76
|
-
|
|
77
|
-
if (!args.all) {
|
|
78
|
-
console.log(`${TAG} Running pnpm audit to find the entries that apply to this repo…`)
|
|
79
|
-
const reported = reportedGhsas()
|
|
80
|
-
const now = Date.now()
|
|
81
|
-
const kept = parseSuppressions(shared, now).filter(
|
|
82
|
-
(entry) => entry.active && entry.ghsas.some((ghsa) => reported.has(ghsa)),
|
|
83
|
-
)
|
|
84
|
-
content = filterSuppressions(shared, reported, now)
|
|
85
|
-
|
|
86
|
-
if (kept.length === 0 && !fs.existsSync(targetPath)) {
|
|
87
|
-
console.log(`${TAG} No shared suppression applies to this repo. ${FILE_NAME} not created.`)
|
|
88
|
-
process.exit(0)
|
|
89
|
-
}
|
|
90
|
-
const keptIds = kept.flatMap((entry) => entry.ghsas).filter((ghsa) => reported.has(ghsa))
|
|
91
|
-
console.log(
|
|
92
|
-
kept.length > 0
|
|
93
|
-
? `${TAG} Keeping ${kept.length} entr${kept.length === 1 ? 'y' : 'ies'}: ${keptIds.join(', ')}`
|
|
94
|
-
: `${TAG} No shared suppression applies to this repo. Writing ${FILE_NAME} without entries.`,
|
|
95
|
-
)
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
fs.writeFileSync(targetPath, content)
|
|
99
|
-
console.log(`${TAG} Updated ${FILE_NAME}.`)
|
|
100
|
-
|
|
101
|
-
if (!fileDiffersFromHead(FILE_NAME, GIT)) {
|
|
102
|
-
console.log(`${TAG} No changes to commit.`)
|
|
103
|
-
process.exit(0)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const stage = runGit(['add', '--', FILE_NAME], GIT)
|
|
107
|
-
if (stage.status !== 0) process.exit(stage.status)
|
|
108
|
-
|
|
109
|
-
console.log(`${TAG} Auto-committing: ${COMMIT_MESSAGE}`)
|
|
110
|
-
process.exit(runGit(['commit', '-m', COMMIT_MESSAGE], GIT).status)
|