Skip to content

Commit de6b3af

Browse files
committed
fix(report): label package versions
1 parent 6d325f1 commit de6b3af

3 files changed

Lines changed: 189 additions & 43 deletions

File tree

‎src/command/scan/output-scan-report.mts‎

Lines changed: 27 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -36,17 +36,11 @@ export type OutputScanReportConfig = {
3636
short: boolean
3737
}
3838

39-
function isFullScanReport(
40-
report: ScanReport | { healthy: boolean },
41-
): report is ScanReport {
42-
return 'alerts' in report
43-
}
44-
4539
export type ReportAlertRow = {
4640
alertType: string
47-
introducedBy: string
4841
manifest: string
4942
packageName: string
43+
packageVersion: string
5044
policy: string
5145
url: string
5246
}
@@ -63,9 +57,9 @@ export function flattenReportAlerts(report: ScanReport): ReportAlertRow[] {
6357
return {
6458
__proto__: null,
6559
alertType: type,
66-
introducedBy: keys[2] || '<unknown>',
6760
manifest: joinAnd(manifest),
6861
packageName: keys[1] || '<unknown>',
62+
packageVersion: keys[2] || '<unknown>',
6963
policy,
7064
url,
7165
}
@@ -82,20 +76,32 @@ export function formatAlertTable(rows: ReportAlertRow[]): string[] {
8276
'POLICY',
8377
'ALERT TYPE',
8478
'PACKAGE',
85-
'INTRODUCED BY',
79+
'VERSION',
8680
'MANIFEST FILE',
8781
]
8882
const cells = rows.map(row => [
8983
row.policy,
9084
row.alertType,
9185
row.packageName,
92-
row.introducedBy,
86+
row.packageVersion,
9387
row.manifest,
9488
])
9589
const widths = headers.map(header => header.length)
96-
for (const cell of cells) {
97-
for (let i = 0, { length } = cell; i < length; i += 1) {
98-
widths[i] = Math.max(widths[i] ?? 0, cell[i]?.length ?? 0)
90+
for (
91+
let rowIndex = 0, { length: rowCount } = cells;
92+
rowIndex < rowCount;
93+
rowIndex += 1
94+
) {
95+
const cell = cells[rowIndex]!
96+
for (
97+
let cellIndex = 0, { length: cellLength } = cell;
98+
cellIndex < cellLength;
99+
cellIndex += 1
100+
) {
101+
widths[cellIndex] = Math.max(
102+
widths[cellIndex] ?? 0,
103+
cell[cellIndex]?.length ?? 0,
104+
)
99105
}
100106
}
101107
const toRow = (values: string[]) =>
@@ -125,6 +131,12 @@ export function formatLabelledPairs(pairs: Array<[string, string]>): string[] {
125131
)
126132
}
127133

134+
export function isFullScanReport(
135+
report: ScanReport | { healthy: boolean },
136+
): report is ScanReport {
137+
return 'alerts' in report
138+
}
139+
128140
export async function outputGeneratedScanReport(
129141
scanReport: Extract<ReturnType<typeof generateReport>, { ok: true }>,
130142
config: Pick<
@@ -295,7 +307,7 @@ export function toMarkdownReport(
295307
__proto__: null,
296308
'Alert Type': row.alertType,
297309
Package: row.packageName,
298-
'Introduced by': row.introducedBy,
310+
Version: row.packageVersion,
299311
url: row.url,
300312
'Manifest file': row.manifest,
301313
Policy: row.policy,
@@ -343,7 +355,7 @@ ${
343355
'Policy',
344356
'Alert Type',
345357
'Package',
346-
'Introduced by',
358+
'Version',
347359
'url',
348360
'Manifest file',
349361
])

‎test/unit/command/scan/output-scan-report-markdown.test.mts‎

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -100,12 +100,12 @@ describe('output-scan-report', () => {
100100
101101
All the alerts from the scan with a policy set to at least "warn".
102102
103-
| ------ | ---------- | ------- | ------------- | ------------------------------------------- | ----------------- |
104-
| Policy | Alert Type | Package | Introduced by | url | Manifest file |
105-
| ------ | ---------- | ------- | ------------- | ------------------------------------------- | ----------------- |
106-
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
107-
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
108-
| ------ | ---------- | ------- | ------------- | ------------------------------------------- | ----------------- |
103+
| ------ | ---------- | ------- | ------- | ------------------------------------------- | ----------------- |
104+
| Policy | Alert Type | Package | Version | url | Manifest file |
105+
| ------ | ---------- | ------- | ------- | ------------------------------------------- | ----------------- |
106+
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
107+
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
108+
| ------ | ---------- | ------- | ------- | ------------------------------------------- | ----------------- |
109109
"
110110
`)
111111
})

‎test/unit/command/scan/output-scan-report-text.test.mts‎

Lines changed: 156 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,17 @@ import { describe, expect, it } from 'vitest'
1313

1414
import {
1515
flattenReportAlerts,
16+
isFullScanReport,
1617
toPlainTextReport,
1718
} from '../../../../src/command/scan/output-scan-report.mts'
1819

1920
import type {
21+
EcoMap,
22+
FileMap,
23+
PackageMap,
2024
ReportLeafNode,
2125
ScanReport,
26+
VersionMap,
2227
} from '../../../../src/command/scan/generate-report.mts'
2328

2429
// Matches any ANSI escape sequence, the codes that show up as literal noise
@@ -27,6 +32,15 @@ import type {
2732
// oxlint-disable-next-line no-control-regex -- control chars intended
2833
const ANSI_PATTERN = /\u001B\[[0-9;]*m/
2934

35+
describe('isFullScanReport', () => {
36+
it('distinguishes full reports from short reports', () => {
37+
expect(
38+
isFullScanReport(buildReport({ alerts: new Map(), healthy: true })),
39+
).toBe(true)
40+
expect(isFullScanReport({ healthy: true })).toBe(false)
41+
})
42+
})
43+
3044
function buildReport(config: {
3145
alerts: ScanReport['alerts']
3246
healthy: boolean
@@ -42,30 +56,33 @@ function buildReport(config: {
4256
}
4357

4458
function buildAlerts(): ScanReport['alerts'] {
45-
return new Map([
46-
[
47-
'npm',
48-
new Map([
49-
[
50-
'acme-widget',
51-
new Map([
52-
[
53-
'1.0.0',
54-
{
55-
manifest: ['package.json'],
56-
policy: 'error',
57-
type: 'envVars',
58-
url: 'https://socket.dev/npm/package/acme-widget',
59-
},
60-
],
61-
]),
62-
],
63-
]),
64-
],
65-
]) as unknown as ScanReport['alerts']
59+
const leaf: ReportLeafNode = {
60+
manifest: ['package.json'],
61+
policy: 'error',
62+
type: 'envVars',
63+
url: 'https://socket.dev/npm/package/acme-widget',
64+
}
65+
const fileMap: FileMap = new Map([['envVars at 1:2', leaf]])
66+
const versionMap: VersionMap = new Map([['package.json', fileMap]])
67+
const packageMap: PackageMap = new Map([['1.0.0', versionMap]])
68+
const ecosystemMap: EcoMap = new Map([['acme-widget', packageMap]])
69+
70+
return new Map([['npm', ecosystemMap]])
6671
}
6772

6873
describe('toPlainTextReport', () => {
74+
it('labels the package version as a version', () => {
75+
const report = buildReport({ alerts: buildAlerts(), healthy: false })
76+
const text = toPlainTextReport(report)
77+
78+
expect(flattenReportAlerts(report)[0]).toMatchObject({
79+
packageVersion: '1.0.0',
80+
})
81+
expect(text).toContain('VERSION')
82+
expect(text).toContain('1.0.0')
83+
expect(text).not.toContain('INTRODUCED BY')
84+
})
85+
6986
it('emits no ANSI escape codes', () => {
7087
const text = toPlainTextReport(
7188
buildReport({ alerts: buildAlerts(), healthy: false }),
@@ -237,6 +254,123 @@ describe('toPlainTextReport', () => {
237254

238255
expect(text).toContain('No alerts')
239256
expect(text).not.toContain('ALERT TYPE')
257+
expect(text).not.toContain('VERSION')
258+
})
259+
260+
it('retains package versions for version and file folds', () => {
261+
const versionLeaf: ReportLeafNode = {
262+
manifest: ['package.json'],
263+
policy: 'error',
264+
type: 'envVars',
265+
url: '',
266+
}
267+
const versionFoldAlerts: ScanReport['alerts'] = new Map([
268+
[
269+
'npm',
270+
new Map([['demo-transitive', new Map([['2.3.4', versionLeaf]])]]),
271+
],
272+
])
273+
const fileFoldAlerts: ScanReport['alerts'] = new Map([
274+
[
275+
'npm',
276+
new Map([
277+
[
278+
'demo-transitive',
279+
new Map([
280+
[
281+
'2.3.4',
282+
new Map([
283+
['package.json', versionLeaf],
284+
[
285+
'src/index.js',
286+
{ ...versionLeaf, manifest: ['package-lock.json'] },
287+
],
288+
]),
289+
],
290+
]),
291+
],
292+
]),
293+
],
294+
])
295+
const versionFoldReport = {
296+
...buildReport({ alerts: versionFoldAlerts, healthy: false }),
297+
options: { fold: 'version' as const, reportLevel: 'error' as const },
298+
}
299+
const fileFoldReport = {
300+
...buildReport({ alerts: fileFoldAlerts, healthy: false }),
301+
options: { fold: 'file' as const, reportLevel: 'error' as const },
302+
}
303+
304+
expect(flattenReportAlerts(versionFoldReport)[0]?.packageVersion).toBe(
305+
'2.3.4',
306+
)
307+
expect(toPlainTextReport(versionFoldReport)).toContain('VERSION')
308+
expect(toPlainTextReport(versionFoldReport)).toContain('2.3.4')
309+
expect(flattenReportAlerts(fileFoldReport)).toEqual([
310+
expect.objectContaining({
311+
manifest: 'package.json',
312+
packageVersion: '2.3.4',
313+
}),
314+
expect.objectContaining({
315+
manifest: 'package-lock.json',
316+
packageVersion: '2.3.4',
317+
}),
318+
])
319+
expect(toPlainTextReport(fileFoldReport)).toContain('VERSION')
320+
expect(toPlainTextReport(fileFoldReport)).toContain('2.3.4')
321+
expect(toPlainTextReport(fileFoldReport)).toContain('package-lock.json')
322+
})
323+
324+
it('keeps package-fold versions unknown and distinguishes package versions', () => {
325+
const leaf: ReportLeafNode = {
326+
manifest: ['package.json'],
327+
policy: 'error',
328+
type: 'envVars',
329+
url: '',
330+
}
331+
const packageFoldAlerts: ScanReport['alerts'] = new Map([
332+
['npm', new Map([['demo-transitive', leaf]])],
333+
])
334+
const twoVersionAlerts: ScanReport['alerts'] = new Map([
335+
[
336+
'npm',
337+
new Map([
338+
[
339+
'demo-transitive',
340+
new Map([
341+
[
342+
'2.3.4',
343+
new Map([
344+
['package.json', new Map([['envVars at 1:2', leaf]])],
345+
]),
346+
],
347+
[
348+
'2.3.5',
349+
new Map([
350+
['package.json', new Map([['envVars at 3:4', leaf]])],
351+
]),
352+
],
353+
]),
354+
],
355+
]),
356+
],
357+
])
358+
const packageFoldReport = {
359+
...buildReport({ alerts: packageFoldAlerts, healthy: false }),
360+
options: { fold: 'pkg' as const, reportLevel: 'error' as const },
361+
}
362+
const twoVersionReport = {
363+
...buildReport({ alerts: twoVersionAlerts, healthy: false }),
364+
options: { fold: 'none' as const, reportLevel: 'error' as const },
365+
}
366+
367+
expect(flattenReportAlerts(packageFoldReport)[0]?.packageVersion).toBe(
368+
'<unknown>',
369+
)
370+
expect(toPlainTextReport(packageFoldReport)).toContain('<unknown>')
371+
expect(
372+
flattenReportAlerts(twoVersionReport).map(row => row.packageVersion),
373+
).toEqual(['2.3.4', '2.3.5'])
240374
})
241375
})
242376

@@ -249,9 +383,9 @@ describe('flattenReportAlerts', () => {
249383
expect(rows).toEqual([
250384
{
251385
alertType: 'envVars',
252-
introducedBy: '1.0.0',
253386
manifest: 'package.json',
254387
packageName: 'acme-widget',
388+
packageVersion: '1.0.0',
255389
policy: 'error',
256390
url: 'https://socket.dev/npm/package/acme-widget',
257391
},

0 commit comments

Comments
 (0)