Skip to content

Commit bf05dae

Browse files
committed
fix(scan report): render readable text
1 parent e04efc2 commit bf05dae

2 files changed

Lines changed: 398 additions & 16 deletions

File tree

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
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+
REPORT_LEVEL_DEFER,
12+
REPORT_LEVEL_ERROR,
13+
} from '../../constants.mts'
14+
15+
import type { ReportLeafNode, ScanReport } from './generate-report.mts'
16+
17+
const ANSI_ESCAPE = String.fromCharCode(0x1b)
18+
19+
function alert(
20+
policy: ReportLeafNode['policy'],
21+
type: string,
22+
manifest: string,
23+
url: string,
24+
): ReportLeafNode {
25+
return { manifest: [manifest], policy, type, url }
26+
}
27+
28+
function buildReport(
29+
config: {
30+
alerts?: ScanReport['alerts']
31+
fold?: ScanReport['options']['fold']
32+
healthy?: boolean
33+
reportLevel?: ScanReport['options']['reportLevel']
34+
} = {},
35+
): ScanReport {
36+
const {
37+
alerts = new Map(),
38+
fold = FOLD_SETTING_NONE,
39+
healthy = true,
40+
reportLevel = REPORT_LEVEL_ERROR,
41+
} = config
42+
return {
43+
alerts,
44+
healthy,
45+
options: { fold, reportLevel },
46+
orgSlug: 'acme',
47+
scanId: 'scan-abc-123',
48+
}
49+
}
50+
51+
function buildAlerts(): ScanReport['alerts'] {
52+
return new Map([
53+
[
54+
'npm',
55+
new Map([
56+
[
57+
'acme-widget-π',
58+
new Map([
59+
[
60+
'1.0.0',
61+
alert(
62+
'error',
63+
'envVars',
64+
'package.json',
65+
'https://socket.dev/npm/package/acme-widget',
66+
),
67+
],
68+
]),
69+
],
70+
]),
71+
],
72+
])
73+
}
74+
75+
describe('toPlainTextReport', () => {
76+
it('renders a labelled report without ANSI escapes or raw object dumps', () => {
77+
const text = toPlainTextReport(
78+
buildReport({ alerts: buildAlerts(), healthy: false }),
79+
)
80+
81+
expect(text.includes(ANSI_ESCAPE)).toBe(false)
82+
expect(text).not.toContain('Map(')
83+
expect(text).not.toContain('=>')
84+
expect(text).not.toContain('[Object')
85+
expect(text).toContain('VIOLATES')
86+
expect(text).toContain('Organization:')
87+
expect(text).toContain('acme')
88+
expect(text).toContain('Scan ID:')
89+
expect(text).toContain('scan-abc-123')
90+
expect(text).toContain('Alert folding:')
91+
expect(text).toContain('Minimum policy level:')
92+
})
93+
94+
it('preserves Unicode package values and prints alert details', () => {
95+
const text = toPlainTextReport(buildReport({ alerts: buildAlerts() }))
96+
97+
expect(text).toContain('PASSES')
98+
expect(text).toContain('acme-widget-π')
99+
expect(text).toContain('envVars')
100+
expect(text).toContain('1.0.0')
101+
expect(text).toContain('package.json')
102+
expect(text).toContain('https://socket.dev/npm/package/acme-widget')
103+
})
104+
105+
it('aligns columns for one alert and keeps its URL on a separate line', () => {
106+
const text = toPlainTextReport(buildReport({ alerts: buildAlerts() }))
107+
const lines = text.split(/\r?\n/)
108+
const header = lines.find(line => line.includes('ALERT TYPE'))!
109+
const row = lines.find(line => line.includes('envVars'))!
110+
const packageColumn = header.indexOf('PACKAGE')
111+
112+
expect(
113+
row.slice(packageColumn, packageColumn + 'acme-widget-π'.length),
114+
).toBe('acme-widget-π')
115+
expect(text).toContain('1 alert with a policy')
116+
expect(lines).toContain(' https://socket.dev/npm/package/acme-widget')
117+
})
118+
119+
it('aligns short and long package names across multiple rows', () => {
120+
const alerts: ScanReport['alerts'] = new Map([
121+
[
122+
'npm',
123+
new Map([
124+
[
125+
'pkg-a',
126+
new Map([['1.0.0', alert('error', 'envVars', 'package.json', '')]]),
127+
],
128+
[
129+
'pkg-a-much-longer-name',
130+
new Map([
131+
['2.0.0', alert('warn', 'telemetry', 'package-lock.json', '')],
132+
]),
133+
],
134+
]),
135+
],
136+
])
137+
const lines = toPlainTextReport(buildReport({ alerts })).split(/\r?\n/)
138+
const header = lines.find(line => line.includes('ALERT TYPE'))!
139+
const shortRow = lines.find(line => line.includes('envVars'))!
140+
const longRow = lines.find(line => line.includes('telemetry'))!
141+
const packageColumn = header.indexOf('PACKAGE')
142+
const manifestColumn = header.indexOf('MANIFEST FILE')
143+
144+
expect(shortRow.slice(packageColumn, packageColumn + 5)).toBe('pkg-a')
145+
expect(longRow.slice(packageColumn, packageColumn + 22)).toBe(
146+
'pkg-a-much-longer-name',
147+
)
148+
expect(shortRow.slice(manifestColumn, manifestColumn + 12)).toBe(
149+
'package.json',
150+
)
151+
expect(longRow.slice(manifestColumn, manifestColumn + 18)).toBe(
152+
'package-lock.json',
153+
)
154+
expect(
155+
lines.find(line => line.includes('2 alerts with a policy')),
156+
).toBeDefined()
157+
})
158+
159+
it('describes empty alerts and omits the alert table', () => {
160+
const text = toPlainTextReport(buildReport())
161+
162+
expect(text).toContain('PASSES')
163+
expect(text).toContain('No alerts')
164+
expect(text).not.toContain('ALERT TYPE')
165+
})
166+
167+
it('includes license, fold and deferred-level settings', () => {
168+
const text = toPlainTextReport(
169+
buildReport({
170+
alerts: buildAlerts(),
171+
fold: 'version',
172+
reportLevel: REPORT_LEVEL_DEFER,
173+
}),
174+
true,
175+
)
176+
177+
expect(text).toContain('security and license policy')
178+
expect(
179+
text.split('\n').find(line => line.includes('Alert folding:')),
180+
).toContain('up to version')
181+
expect(
182+
text.split('\n').find(line => line.includes('Minimum policy level:')),
183+
).toContain('everything')
184+
expect(
185+
text.split('\n').find(line => line.includes('Include license alerts:')),
186+
).toContain('yes')
187+
})
188+
189+
it('renders a large alert report without argument-count failures', () => {
190+
const alertRow = alert('error', 'envVars', 'package.json', '')
191+
const packages = new Map<string, Map<string, ReportLeafNode>>()
192+
const alertCount = 200_000
193+
for (let i = 0; i < alertCount; i += 1) {
194+
packages.set(
195+
`package-${String(i).padStart(6, '0')}`,
196+
new Map([['1.0.0', alertRow]]),
197+
)
198+
}
199+
const alerts: ScanReport['alerts'] = new Map([['npm', packages]])
200+
const text = toPlainTextReport(buildReport({ alerts }))
201+
202+
expect(text).toContain('200000 alerts with a policy')
203+
expect(text).toContain('package-000000')
204+
expect(text).toContain('package-199999')
205+
})
206+
})
207+
208+
describe('structured scan reports', () => {
209+
it('keeps nested alert data and the license flag in JSON', () => {
210+
const json = toJsonReport(buildReport({ alerts: buildAlerts() }), true)
211+
212+
expect(JSON.parse(json)).toMatchObject({
213+
ok: true,
214+
data: {
215+
alerts: {
216+
npm: {
217+
'acme-widget-π': {
218+
'1.0.0': {
219+
manifest: ['package.json'],
220+
policy: 'error',
221+
type: 'envVars',
222+
url: 'https://socket.dev/npm/package/acme-widget',
223+
},
224+
},
225+
},
226+
},
227+
includeLicensePolicy: true,
228+
},
229+
})
230+
})
231+
232+
it('keeps alert fields and URLs in Markdown', () => {
233+
const markdown = toMarkdownReport(
234+
buildReport({ alerts: buildAlerts() }),
235+
true,
236+
)
237+
238+
expect(markdown).toContain('security or license policy')
239+
expect(markdown).toContain('acme-widget-π')
240+
expect(markdown).toContain('envVars')
241+
expect(markdown).toContain('package.json')
242+
expect(markdown).toContain('https://socket.dev/npm/package/acme-widget')
243+
})
244+
245+
it('flattens one row per report alert', () => {
246+
expect(flattenReportAlerts(buildReport({ alerts: buildAlerts() }))).toEqual(
247+
[
248+
{
249+
alertType: 'envVars',
250+
introducedBy: '1.0.0',
251+
manifest: 'package.json',
252+
packageName: 'acme-widget-π',
253+
policy: 'error',
254+
url: 'https://socket.dev/npm/package/acme-widget',
255+
},
256+
],
257+
)
258+
})
259+
})

0 commit comments

Comments
 (0)