fg-devkit 1.20.0 → 1.21.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.
@@ -0,0 +1,287 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import {
5
+ activeSuppressedGhsas,
6
+ buildRefreshCommitMessage,
7
+ compareVersions,
8
+ diffLockfileVersions,
9
+ filterSuppressions,
10
+ isReleaseAgeExcluded,
11
+ isSameReleaseLine,
12
+ lowestPatchedVersion,
13
+ normalizeAdvisories,
14
+ planOverrides,
15
+ upsertWorkspaceOverrides,
16
+ } from './audit-fix-plan.mjs'
17
+
18
+ function advisory({ name, ghsa, vulnerable, patched, versions }) {
19
+ return { id: `${ghsa}|${name}|${vulnerable}`, ghsa, name, severity: 'high', title: '', vulnerable, patched, versions }
20
+ }
21
+
22
+ test('compareVersions orders releases and prereleases', () => {
23
+ assert.equal(compareVersions('1.2.3', '1.2.3'), 0)
24
+ assert.equal(compareVersions('1.2.3', '1.10.0'), -1)
25
+ assert.equal(compareVersions('2.0.0', '1.99.99'), 1)
26
+ assert.equal(compareVersions('1.0.0-beta.2', '1.0.0'), -1)
27
+ assert.equal(compareVersions('1.0.0-beta.10', '1.0.0-beta.2'), 1)
28
+ })
29
+
30
+ test('isSameReleaseLine follows caret semantics', () => {
31
+ assert.equal(isSameReleaseLine('1.5.6', '1.6.3'), true)
32
+ assert.equal(isSameReleaseLine('1.5.6', '2.0.0'), false)
33
+ assert.equal(isSameReleaseLine('0.4.1', '0.4.9'), true)
34
+ assert.equal(isSameReleaseLine('0.4.1', '0.5.0'), false)
35
+ assert.equal(isSameReleaseLine('0.0.1', '0.0.2'), false)
36
+ })
37
+
38
+ test('lowestPatchedVersion handles single, multi-range, and missing patches', () => {
39
+ assert.deepEqual(lowestPatchedVersion('>=1.2.2', '1.2.1'), { status: 'ok', version: '1.2.2' })
40
+ assert.deepEqual(lowestPatchedVersion('>=1.1.21 <2.0.0 || >=2.1.7', '2.1.4'), { status: 'ok', version: '2.1.7' })
41
+ assert.deepEqual(lowestPatchedVersion('>=1.1.21 <2.0.0 || >=2.1.7', '1.1.18'), {
42
+ status: 'ok',
43
+ version: '1.1.21',
44
+ })
45
+ assert.deepEqual(lowestPatchedVersion(null, '3.0.3'), { status: 'none' })
46
+ assert.deepEqual(lowestPatchedVersion('<0.0.0', '3.0.3'), { status: 'none' })
47
+ assert.deepEqual(lowestPatchedVersion('^2.0.0', '1.0.0'), { status: 'unparseable' })
48
+ })
49
+
50
+ test('normalizeAdvisories keeps one entry per GHSA and range', () => {
51
+ const report = {
52
+ advisories: {
53
+ 1: {
54
+ module_name: 'brace-expansion',
55
+ github_advisory_id: 'GHSA-a',
56
+ vulnerable_versions: '<1.1.20',
57
+ patched_versions: '>=1.1.20',
58
+ severity: 'high',
59
+ findings: [{ version: '1.1.18', paths: [] }],
60
+ },
61
+ 2: {
62
+ module_name: 'brace-expansion',
63
+ github_advisory_id: 'GHSA-a',
64
+ vulnerable_versions: '>=2.0.0 <2.1.6',
65
+ patched_versions: '>=2.1.6',
66
+ severity: 'high',
67
+ findings: [{ version: '2.1.4', paths: [] }],
68
+ },
69
+ },
70
+ }
71
+ const advisories = normalizeAdvisories(report)
72
+ assert.equal(advisories.length, 2)
73
+ assert.deepEqual(
74
+ advisories.map((item) => item.versions),
75
+ [['1.1.18'], ['2.1.4']],
76
+ )
77
+ })
78
+
79
+ test('planOverrides picks the lowest version that fixes every advisory, per release line', () => {
80
+ const plan = planOverrides([
81
+ advisory({
82
+ name: 'brace-expansion',
83
+ ghsa: 'GHSA-1',
84
+ vulnerable: '<1.1.19',
85
+ patched: '>=1.1.19',
86
+ versions: ['1.1.18'],
87
+ }),
88
+ advisory({
89
+ name: 'brace-expansion',
90
+ ghsa: 'GHSA-2',
91
+ vulnerable: '<1.1.21',
92
+ patched: '>=1.1.21',
93
+ versions: ['1.1.18'],
94
+ }),
95
+ advisory({
96
+ name: 'brace-expansion',
97
+ ghsa: 'GHSA-2',
98
+ vulnerable: '>=2.0.0 <2.1.7',
99
+ patched: '>=2.1.7',
100
+ versions: ['2.1.4'],
101
+ }),
102
+ ])
103
+ assert.deepEqual(
104
+ plan.overrides.map(({ key, value }) => ({ key, value })),
105
+ [
106
+ { key: 'brace-expansion@>=1.1.18 <1.1.21', value: '1.1.21' },
107
+ { key: 'brace-expansion@>=2.1.4 <2.1.7', value: '2.1.7' },
108
+ ],
109
+ )
110
+ assert.deepEqual(plan.overrides[0].ghsas.sort(), ['GHSA-1', 'GHSA-2'])
111
+ })
112
+
113
+ test('planOverrides merges overlapping spans of the same package', () => {
114
+ const plan = planOverrides([
115
+ advisory({ name: 'x', ghsa: 'GHSA-1', vulnerable: '<1.4.0', patched: '>=1.4.0', versions: ['1.2.0', '1.3.0'] }),
116
+ ])
117
+ assert.deepEqual(
118
+ plan.overrides.map(({ key, value }) => ({ key, value })),
119
+ [{ key: 'x@>=1.2.0 <1.4.0', value: '1.4.0' }],
120
+ )
121
+ })
122
+
123
+ test('planOverrides reports instead of overriding when it would be unsafe or useless', () => {
124
+ const plan = planOverrides(
125
+ [
126
+ advisory({ name: 'braces', ghsa: 'GHSA-none', vulnerable: '<=3.0.3', patched: null, versions: ['3.0.3'] }),
127
+ advisory({ name: 'old', ghsa: 'GHSA-major', vulnerable: '<3.0.0', patched: '>=3.0.0', versions: ['2.4.0'] }),
128
+ advisory({
129
+ name: 'eslint',
130
+ ghsa: 'GHSA-direct',
131
+ vulnerable: '<10.0.1',
132
+ patched: '>=10.0.1',
133
+ versions: ['10.0.0'],
134
+ }),
135
+ ],
136
+ { directDependencies: new Set(['eslint']) },
137
+ )
138
+ assert.equal(plan.overrides.length, 0)
139
+ assert.deepEqual(
140
+ plan.noPatch.map((item) => item.name),
141
+ ['braces'],
142
+ )
143
+ assert.deepEqual(
144
+ plan.crossMajor.map((item) => [item.name, item.from, item.to]),
145
+ [['old', '2.4.0', '3.0.0']],
146
+ )
147
+ assert.deepEqual(
148
+ plan.direct.map((item) => item.name),
149
+ ['eslint'],
150
+ )
151
+ })
152
+
153
+ test('isReleaseAgeExcluded understands names, patterns and version lists', () => {
154
+ const list = ['fg-devkit', '@fallow-cli/*', 'lefthook@2.1.15 || 2.1.17']
155
+ assert.equal(isReleaseAgeExcluded(list, 'fg-devkit', '1.0.0'), true)
156
+ assert.equal(isReleaseAgeExcluded(list, '@fallow-cli/linux-x64-gnu', '3.31.0'), true)
157
+ assert.equal(isReleaseAgeExcluded(list, 'lefthook', '2.1.17'), true)
158
+ assert.equal(isReleaseAgeExcluded(list, 'lefthook', '2.1.16'), false)
159
+ assert.equal(isReleaseAgeExcluded(list, 'seroval', '1.6.3'), false)
160
+ })
161
+
162
+ test('upsertWorkspaceOverrides creates the section and keeps other settings', () => {
163
+ const text = 'allowBuilds:\n esbuild: true\n'
164
+ const result = upsertWorkspaceOverrides(text, [{ key: 'seroval@>=1.5.6 <1.6.3', value: '1.6.3' }])
165
+ assert.equal(result, "allowBuilds:\n esbuild: true\noverrides:\n 'seroval@>=1.5.6 <1.6.3': '1.6.3'\n")
166
+ })
167
+
168
+ test('upsertWorkspaceOverrides replaces matching keys and appends new ones in place', () => {
169
+ const text = [
170
+ 'overrides:',
171
+ " 'seroval@>=1.5.6 <1.6.3': 1.6.2",
172
+ ' lodash: 4.18.1',
173
+ '',
174
+ 'allowBuilds:',
175
+ ' esbuild: true',
176
+ '',
177
+ ].join('\n')
178
+ const result = upsertWorkspaceOverrides(text, [
179
+ { key: 'seroval@>=1.5.6 <1.6.3', value: '1.6.3' },
180
+ { key: 'braces@>=3.0.0 <3.0.4', value: '3.0.4' },
181
+ ])
182
+ assert.equal(
183
+ result,
184
+ [
185
+ 'overrides:',
186
+ " 'seroval@>=1.5.6 <1.6.3': '1.6.3'",
187
+ ' lodash: 4.18.1',
188
+ " 'braces@>=3.0.0 <3.0.4': '3.0.4'",
189
+ '',
190
+ 'allowBuilds:',
191
+ ' esbuild: true',
192
+ '',
193
+ ].join('\n'),
194
+ )
195
+ })
196
+
197
+ test('diffLockfileVersions reports changed resolutions across lockfile documents', () => {
198
+ const before = [
199
+ '---',
200
+ 'packages:',
201
+ " '@pnpm/exe@12.9.1':",
202
+ '---',
203
+ 'packages:',
204
+ ' brace-expansion@1.1.18:',
205
+ ' brace-expansion@5.0.9:',
206
+ ' source-map-js@1.2.1:',
207
+ 'snapshots:',
208
+ ' brace-expansion@1.1.18:',
209
+ ].join('\n')
210
+ const after = before.replace(/1\.1\.18/g, '1.1.21').replace('5.0.9', '5.0.12')
211
+ assert.deepEqual(diffLockfileVersions(before, after), [
212
+ { name: 'brace-expansion', from: ['1.1.18', '5.0.9'], to: ['1.1.21', '5.0.12'] },
213
+ ])
214
+ })
215
+
216
+ test('buildRefreshCommitMessage follows the vuln() convention', () => {
217
+ assert.deepEqual(buildRefreshCommitMessage([{ name: 'source-map-js', from: ['1.2.1'], to: ['1.2.2'] }]), {
218
+ subject: 'vuln(source-map-js): refresh lockfile',
219
+ body: '- source-map-js: 1.2.1 -> 1.2.2',
220
+ })
221
+ assert.equal(
222
+ buildRefreshCommitMessage([
223
+ { name: 'a', from: ['1.0.0'], to: ['1.0.1'] },
224
+ { name: 'b', from: ['2.0.0'], to: ['2.0.1'] },
225
+ ]).subject,
226
+ 'vuln(): refresh 2 packages in lockfile',
227
+ )
228
+ })
229
+
230
+ test('activeSuppressedGhsas reads active entries and ignores notes and expired ones', () => {
231
+ const xml = `<?xml version="1.0" encoding="UTF-8"?>
232
+ <suppressions>
233
+ <suppress until="2026-12-01Z">
234
+ <notes><![CDATA[ braces, see also GHSA-xxxx-yyyy-zzzz in the notes ]]></notes>
235
+ <filePath regex="true">.*pnpm-lock\\.yaml\\?braces$</filePath>
236
+ <vulnerabilityName regex="true">.*(GHSA-vfj7-8cjw-p6xm|1240992).*</vulnerabilityName>
237
+ </suppress>
238
+ <suppress until="2026-01-01Z">
239
+ <vulnerabilityName regex="true">.*(GHSA-hp3w-g68c-fv3c).*</vulnerabilityName>
240
+ </suppress>
241
+ <suppress>
242
+ <vulnerabilityName>GHSA-AAAA-bbbb-cccc</vulnerabilityName>
243
+ </suppress>
244
+ </suppressions>`
245
+ const now = Date.parse('2026-10-06T12:00:00Z')
246
+ assert.deepEqual(
247
+ [...activeSuppressedGhsas(xml, now)],
248
+ [
249
+ ['GHSA-vfj7-8cjw-p6xm', '2026-12-01Z'],
250
+ ['GHSA-aaaa-bbbb-cccc', undefined],
251
+ ],
252
+ )
253
+ assert.equal(activeSuppressedGhsas(xml, Date.parse('2026-12-02T00:00:00Z')).has('GHSA-vfj7-8cjw-p6xm'), false)
254
+ })
255
+
256
+ test('filterSuppressions keeps only active entries that match the repo findings', () => {
257
+ const xml = [
258
+ '<?xml version="1.0" encoding="UTF-8"?>',
259
+ '<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">',
260
+ ' <suppress until="2026-12-01Z">',
261
+ ' <notes><![CDATA[ braces ]]></notes>',
262
+ ' <vulnerabilityName regex="true">.*(GHSA-vfj7-8cjw-p6xm|1240992).*</vulnerabilityName>',
263
+ ' </suppress>',
264
+ ' <suppress until="2026-12-01Z">',
265
+ ' <notes><![CDATA[ sprintf-js ]]></notes>',
266
+ ' <vulnerabilityName regex="true">.*(GHSA-hp3w-g68c-fv3c).*</vulnerabilityName>',
267
+ ' </suppress>',
268
+ '</suppressions>',
269
+ '',
270
+ ].join('\n')
271
+ const now = Date.parse('2026-10-06T12:00:00Z')
272
+ assert.equal(
273
+ filterSuppressions(xml, ['GHSA-vfj7-8cjw-p6xm'], now),
274
+ [
275
+ '<?xml version="1.0" encoding="UTF-8"?>',
276
+ '<suppressions xmlns="https://jeremylong.github.io/DependencyCheck/dependency-suppression.1.3.xsd">',
277
+ ' <suppress until="2026-12-01Z">',
278
+ ' <notes><![CDATA[ braces ]]></notes>',
279
+ ' <vulnerabilityName regex="true">.*(GHSA-vfj7-8cjw-p6xm|1240992).*</vulnerabilityName>',
280
+ ' </suppress>',
281
+ '</suppressions>',
282
+ '',
283
+ ].join('\n'),
284
+ )
285
+ // Expired entries are dropped even when they match.
286
+ assert.doesNotMatch(filterSuppressions(xml, ['GHSA-vfj7-8cjw-p6xm'], Date.parse('2026-12-02T00:00:00Z')), /GHSA-vfj7/)
287
+ })