Skip to content

Commit a164eb5

Browse files
committed
fix(report): label package versions in scan output
1 parent 5baf3f0 commit a164eb5

3 files changed

Lines changed: 508 additions & 24 deletions

File tree

Lines changed: 360 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,360 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import {
4+
flattenReportAlerts,
5+
toJsonReport,
6+
toMarkdownReport,
7+
toPlainTextReport,
8+
} from './output-scan-report.mts'
9+
import {
10+
FOLD_SETTING_NONE,
11+
FOLD_SETTING_PKG,
12+
REPORT_LEVEL_DEFER,
13+
REPORT_LEVEL_ERROR,
14+
} from '../../constants.mts'
15+
16+
import type { ReportLeafNode, ScanReport } from './generate-report.mts'
17+
18+
const ANSI_ESCAPE = String.fromCharCode(0x1b)
19+
20+
function alert(
21+
policy: ReportLeafNode['policy'],
22+
type: string,
23+
manifest: string,
24+
url: string,
25+
): ReportLeafNode {
26+
return { manifest: [manifest], policy, type, url }
27+
}
28+
29+
function buildReport(
30+
config: {
31+
alerts?: ScanReport['alerts']
32+
fold?: ScanReport['options']['fold']
33+
healthy?: boolean
34+
reportLevel?: ScanReport['options']['reportLevel']
35+
} = {},
36+
): ScanReport {
37+
const {
38+
alerts = new Map(),
39+
fold = FOLD_SETTING_NONE,
40+
healthy = true,
41+
reportLevel = REPORT_LEVEL_ERROR,
42+
} = config
43+
return {
44+
alerts,
45+
healthy,
46+
options: { fold, reportLevel },
47+
orgSlug: 'acme',
48+
scanId: 'scan-abc-123',
49+
}
50+
}
51+
52+
function buildAlerts(): ScanReport['alerts'] {
53+
const leaf = alert(
54+
'error',
55+
'envVars',
56+
'package.json',
57+
'https://socket.dev/npm/package/acme-widget',
58+
)
59+
return new Map([
60+
[
61+
'npm',
62+
new Map([
63+
[
64+
'acme-widget-π',
65+
new Map([
66+
[
67+
'1.0.0',
68+
new Map([['package.json', new Map([['envVars at 1:2', leaf]])]]),
69+
],
70+
]),
71+
],
72+
]),
73+
],
74+
])
75+
}
76+
77+
describe('toPlainTextReport', () => {
78+
it('renders a labelled report without ANSI escapes or raw object dumps', () => {
79+
const text = toPlainTextReport(
80+
buildReport({ alerts: buildAlerts(), healthy: false }),
81+
)
82+
83+
expect(text.includes(ANSI_ESCAPE)).toBe(false)
84+
expect(text).not.toContain('Map(')
85+
expect(text).not.toContain('=>')
86+
expect(text).not.toContain('[Object')
87+
expect(text).toContain('VIOLATES')
88+
expect(text).toContain('Organization:')
89+
expect(text).toContain('acme')
90+
expect(text).toContain('Scan ID:')
91+
expect(text).toContain('scan-abc-123')
92+
expect(text).toContain('Alert folding:')
93+
expect(text).toContain('Minimum policy level:')
94+
})
95+
96+
it('preserves Unicode package values and prints alert details', () => {
97+
const text = toPlainTextReport(buildReport({ alerts: buildAlerts() }))
98+
99+
expect(text).toContain('PASSES')
100+
expect(text).toContain('acme-widget-π')
101+
expect(text).toContain('envVars')
102+
expect(text).toContain('1.0.0')
103+
expect(text).toContain('package.json')
104+
expect(text).toContain('https://socket.dev/npm/package/acme-widget')
105+
expect(text).toContain('VERSION')
106+
expect(text).not.toContain('INTRODUCED BY')
107+
})
108+
109+
it('aligns columns for one alert and keeps its URL on a separate line', () => {
110+
const text = toPlainTextReport(buildReport({ alerts: buildAlerts() }))
111+
const lines = text.split(/\r?\n/)
112+
const header = lines.find(line => line.includes('ALERT TYPE'))!
113+
const row = lines.find(line => line.includes('envVars'))!
114+
const packageColumn = header.indexOf('PACKAGE')
115+
116+
expect(
117+
row.slice(packageColumn, packageColumn + 'acme-widget-π'.length),
118+
).toBe('acme-widget-π')
119+
expect(text).toContain('1 alert with a policy')
120+
expect(lines).toContain(' https://socket.dev/npm/package/acme-widget')
121+
})
122+
123+
it('aligns short and long package names across multiple rows', () => {
124+
const alerts: ScanReport['alerts'] = new Map([
125+
[
126+
'npm',
127+
new Map([
128+
[
129+
'pkg-a',
130+
new Map([['1.0.0', alert('error', 'envVars', 'package.json', '')]]),
131+
],
132+
[
133+
'pkg-a-much-longer-name',
134+
new Map([
135+
['2.0.0', alert('warn', 'telemetry', 'package-lock.json', '')],
136+
]),
137+
],
138+
]),
139+
],
140+
])
141+
const lines = toPlainTextReport(buildReport({ alerts })).split(/\r?\n/)
142+
const header = lines.find(line => line.includes('ALERT TYPE'))!
143+
const shortRow = lines.find(line => line.includes('envVars'))!
144+
const longRow = lines.find(line => line.includes('telemetry'))!
145+
const packageColumn = header.indexOf('PACKAGE')
146+
const manifestColumn = header.indexOf('MANIFEST FILE')
147+
148+
expect(shortRow.slice(packageColumn, packageColumn + 5)).toBe('pkg-a')
149+
expect(longRow.slice(packageColumn, packageColumn + 22)).toBe(
150+
'pkg-a-much-longer-name',
151+
)
152+
expect(shortRow.slice(manifestColumn, manifestColumn + 12)).toBe(
153+
'package.json',
154+
)
155+
expect(longRow.slice(manifestColumn, manifestColumn + 18)).toBe(
156+
'package-lock.json',
157+
)
158+
expect(
159+
lines.find(line => line.includes('2 alerts with a policy')),
160+
).toBeDefined()
161+
})
162+
163+
it('describes empty alerts and omits the alert table', () => {
164+
const text = toPlainTextReport(buildReport())
165+
166+
expect(text).toContain('PASSES')
167+
expect(text).toContain('No alerts')
168+
expect(text).not.toContain('ALERT TYPE')
169+
expect(text).not.toContain('VERSION')
170+
})
171+
172+
it('includes license, fold and deferred-level settings', () => {
173+
const text = toPlainTextReport(
174+
buildReport({
175+
alerts: buildAlerts(),
176+
fold: 'version',
177+
reportLevel: REPORT_LEVEL_DEFER,
178+
}),
179+
true,
180+
)
181+
182+
expect(text).toContain('security and license policy')
183+
expect(
184+
text.split('\n').find(line => line.includes('Alert folding:')),
185+
).toContain('up to version')
186+
expect(
187+
text.split('\n').find(line => line.includes('Minimum policy level:')),
188+
).toContain('everything')
189+
expect(
190+
text.split('\n').find(line => line.includes('Include license alerts:')),
191+
).toContain('yes')
192+
})
193+
194+
it('renders a large alert report without argument-count failures', () => {
195+
const alertRow = alert('error', 'envVars', 'package.json', '')
196+
const packages = new Map<string, Map<string, ReportLeafNode>>()
197+
const alertCount = 200_000
198+
for (let i = 0; i < alertCount; i += 1) {
199+
packages.set(
200+
`package-${String(i).padStart(6, '0')}`,
201+
new Map([['1.0.0', alertRow]]),
202+
)
203+
}
204+
const alerts: ScanReport['alerts'] = new Map([['npm', packages]])
205+
const text = toPlainTextReport(buildReport({ alerts }))
206+
207+
expect(text).toContain('200000 alerts with a policy')
208+
expect(text).toContain('package-000000')
209+
expect(text).toContain('package-199999')
210+
})
211+
212+
it('keeps package versions accurate across fold modes', () => {
213+
const leaf = alert('error', 'envVars', 'package.json', '')
214+
const versionFoldAlerts: ScanReport['alerts'] = new Map([
215+
['npm', new Map([['demo-transitive', new Map([['2.3.4', leaf]])]])],
216+
])
217+
const fileFoldAlerts: ScanReport['alerts'] = new Map([
218+
[
219+
'npm',
220+
new Map([
221+
[
222+
'demo-transitive',
223+
new Map([
224+
[
225+
'2.3.4',
226+
new Map([
227+
['package.json', leaf],
228+
[
229+
'src/index.js',
230+
alert('warn', 'telemetry', 'package-lock.json', ''),
231+
],
232+
]),
233+
],
234+
]),
235+
],
236+
]),
237+
],
238+
])
239+
const packageFoldAlerts: ScanReport['alerts'] = new Map([
240+
['npm', new Map([['demo-transitive', leaf]])],
241+
])
242+
const twoVersionAlerts: ScanReport['alerts'] = new Map([
243+
[
244+
'npm',
245+
new Map([
246+
[
247+
'demo-transitive',
248+
new Map([
249+
['2.3.4', leaf],
250+
['2.3.5', alert('warn', 'telemetry', 'package.json', '')],
251+
]),
252+
],
253+
]),
254+
],
255+
])
256+
const versionFoldReport = buildReport({
257+
alerts: versionFoldAlerts,
258+
fold: 'version',
259+
healthy: false,
260+
})
261+
const fileFoldReport = buildReport({
262+
alerts: fileFoldAlerts,
263+
fold: 'file',
264+
healthy: false,
265+
})
266+
const packageFoldReport = buildReport({
267+
alerts: packageFoldAlerts,
268+
fold: FOLD_SETTING_PKG,
269+
healthy: false,
270+
})
271+
272+
expect(flattenReportAlerts(versionFoldReport)[0]?.packageVersion).toBe(
273+
'2.3.4',
274+
)
275+
expect(toPlainTextReport(versionFoldReport)).toContain('VERSION')
276+
expect(toPlainTextReport(versionFoldReport)).toContain('2.3.4')
277+
expect(flattenReportAlerts(fileFoldReport)).toEqual([
278+
expect.objectContaining({
279+
manifest: 'package.json',
280+
packageVersion: '2.3.4',
281+
}),
282+
expect.objectContaining({
283+
manifest: 'package-lock.json',
284+
packageVersion: '2.3.4',
285+
}),
286+
])
287+
expect(toPlainTextReport(fileFoldReport)).toContain('VERSION')
288+
expect(toPlainTextReport(fileFoldReport)).toContain('2.3.4')
289+
expect(toPlainTextReport(fileFoldReport)).toContain('package-lock.json')
290+
expect(flattenReportAlerts(packageFoldReport)[0]?.packageVersion).toBe(
291+
'<unknown>',
292+
)
293+
expect(toPlainTextReport(packageFoldReport)).toContain('<unknown>')
294+
expect(
295+
flattenReportAlerts(
296+
buildReport({ alerts: twoVersionAlerts, fold: 'version' }),
297+
).map(row => row.packageVersion),
298+
).toEqual(['2.3.4', '2.3.5'])
299+
})
300+
})
301+
302+
describe('structured scan reports', () => {
303+
it('keeps nested alert data and the license flag in JSON', () => {
304+
const json = toJsonReport(buildReport({ alerts: buildAlerts() }), true)
305+
306+
expect(JSON.parse(json)).toMatchObject({
307+
ok: true,
308+
data: {
309+
alerts: {
310+
npm: {
311+
'acme-widget-π': {
312+
'1.0.0': {
313+
'package.json': {
314+
'envVars at 1:2': {
315+
manifest: ['package.json'],
316+
policy: 'error',
317+
type: 'envVars',
318+
url: 'https://socket.dev/npm/package/acme-widget',
319+
},
320+
},
321+
},
322+
},
323+
},
324+
},
325+
includeLicensePolicy: true,
326+
},
327+
})
328+
expect(JSON.parse(json)).not.toHaveProperty(
329+
'data.alerts.npm.acme-widget-π.1.0.0.packageVersion',
330+
)
331+
})
332+
333+
it('keeps alert fields and URLs in Markdown', () => {
334+
const markdown = toMarkdownReport(
335+
buildReport({ alerts: buildAlerts() }),
336+
true,
337+
)
338+
339+
expect(markdown).toContain('security or license policy')
340+
expect(markdown).toContain('acme-widget-π')
341+
expect(markdown).toContain('envVars')
342+
expect(markdown).toContain('package.json')
343+
expect(markdown).toContain('https://socket.dev/npm/package/acme-widget')
344+
})
345+
346+
it('flattens one row per report alert', () => {
347+
expect(flattenReportAlerts(buildReport({ alerts: buildAlerts() }))).toEqual(
348+
[
349+
{
350+
alertType: 'envVars',
351+
manifest: 'package.json',
352+
packageName: 'acme-widget-π',
353+
packageVersion: '1.0.0',
354+
policy: 'error',
355+
url: 'https://socket.dev/npm/package/acme-widget',
356+
},
357+
],
358+
)
359+
})
360+
})

0 commit comments

Comments
 (0)