Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 37 additions & 14 deletions src/command/scan/output-scan-report.mts
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,9 @@ export type OutputScanReportConfig = {

export type ReportAlertRow = {
alertType: string
introducedBy: string
manifest: string
packageName: string
packageVersion: string
policy: string
url: string
}
Expand All @@ -57,9 +57,9 @@ export function flattenReportAlerts(report: ScanReport): ReportAlertRow[] {
return {
__proto__: null,
alertType: type,
introducedBy: keys[2] || '<unknown>',
manifest: joinAnd(manifest),
packageName: keys[1] || '<unknown>',
packageVersion: keys[2] || '<unknown>',
policy,
url,
}
Expand All @@ -76,19 +76,34 @@ export function formatAlertTable(rows: ReportAlertRow[]): string[] {
'POLICY',
'ALERT TYPE',
'PACKAGE',
'INTRODUCED BY',
'VERSION',
'MANIFEST FILE',
]
const cells = rows.map(row => [
row.policy,
row.alertType,
row.packageName,
row.introducedBy,
row.packageVersion,
row.manifest,
])
const widths = headers.map((header, i) =>
Math.max(header.length, ...cells.map(cell => (cell[i] ?? '').length)),
)
const widths = headers.map(header => header.length)
for (
let rowIndex = 0, { length: rowCount } = cells;
rowIndex < rowCount;
rowIndex += 1
) {
const cell = cells[rowIndex]!
for (
let cellIndex = 0, { length: cellLength } = cell;
cellIndex < cellLength;
cellIndex += 1
) {
widths[cellIndex] = Math.max(
widths[cellIndex] ?? 0,
cell[cellIndex]?.length ?? 0,
)
}
}
const toRow = (values: string[]) =>
` ${values
.map((value, i) => value.padEnd(widths[i] ?? 0))
Expand Down Expand Up @@ -116,6 +131,12 @@ export function formatLabelledPairs(pairs: Array<[string, string]>): string[] {
)
}

export function isFullScanReport(
report: ScanReport | { healthy: boolean },
): report is ScanReport {
return 'alerts' in report
}

export async function outputGeneratedScanReport(
scanReport: Extract<ReturnType<typeof generateReport>, { ok: true }>,
config: Pick<
Expand Down Expand Up @@ -159,10 +180,10 @@ export async function outputGeneratedScanReport(

if (short) {
logger.log(scanReport.data.healthy ? 'OK' : 'ERR')
} else if (isFullScanReport(scanReport.data)) {
logger.log(toPlainTextReport(scanReport.data, includeLicensePolicy))
} else {
logger.log(
toPlainTextReport(scanReport.data as ScanReport, includeLicensePolicy),
)
logger.log(scanReport.data.healthy ? 'OK' : 'ERR')
}
}

Expand Down Expand Up @@ -286,7 +307,7 @@ export function toMarkdownReport(
__proto__: null,
'Alert Type': row.alertType,
Package: row.packageName,
'Introduced by': row.introducedBy,
Version: row.packageVersion,
url: row.url,
'Manifest file': row.manifest,
Policy: row.policy,
Expand Down Expand Up @@ -334,7 +355,7 @@ ${
'Policy',
'Alert Type',
'Package',
'Introduced by',
'Version',
'url',
'Manifest file',
])
Expand Down Expand Up @@ -401,8 +422,10 @@ export function toPlainTextReport(
lines.push(
` ${rows.length} ${pluralize('alert', { count: rows.length })} with a policy set to at least "${reportLevel}".`,
'',
...formatAlertTable(rows),
'',
)
for (const line of formatAlertTable(rows)) {
lines.push(line)
}
lines.push('')
return lines.join('\n')
}
12 changes: 6 additions & 6 deletions test/unit/command/scan/output-scan-report-markdown.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -100,12 +100,12 @@ describe('output-scan-report', () => {

All the alerts from the scan with a policy set to at least "warn".

| ------ | ---------- | ------- | ------------- | ------------------------------------------- | ----------------- |
| Policy | Alert Type | Package | Introduced by | url | Manifest file |
| ------ | ---------- | ------- | ------------- | ------------------------------------------- | ----------------- |
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
| ------ | ---------- | ------- | ------------- | ------------------------------------------- | ----------------- |
| ------ | ---------- | ------- | ------- | ------------------------------------------- | ----------------- |
| Policy | Alert Type | Package | Version | url | Manifest file |
| ------ | ---------- | ------- | ------- | ------------------------------------------- | ----------------- |
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
| error | envVars | tslib | 1.14.1 | https://socket.dev/npm/package/tslib/1.14.1 | package-lock.json |
| ------ | ---------- | ------- | ------- | ------------------------------------------- | ----------------- |
"
`)
})
Expand Down
206 changes: 183 additions & 23 deletions test/unit/command/scan/output-scan-report-text.test.mts
Original file line number Diff line number Diff line change
Expand Up @@ -13,17 +13,34 @@ import { describe, expect, it } from 'vitest'

import {
flattenReportAlerts,
isFullScanReport,
toPlainTextReport,
} from '../../../../src/command/scan/output-scan-report.mts'

import type { ScanReport } from '../../../../src/command/scan/report-generation.mts'
import type {
EcoMap,
FileMap,
PackageMap,
ReportLeafNode,
ScanReport,
VersionMap,
} from '../../../../src/command/scan/report-generation.mts'

// Matches any ANSI escape sequence, the codes that show up as literal noise
// like "[32m" in a log viewer that does not interpret them.
// matching control characters is the point.
// oxlint-disable-next-line no-control-regex -- control chars intended
const ANSI_PATTERN = /\u001B\[[0-9;]*m/

describe('isFullScanReport', () => {
it('distinguishes full reports from short reports', () => {
expect(
isFullScanReport(buildReport({ alerts: new Map(), healthy: true })),
).toBe(true)
expect(isFullScanReport({ healthy: true })).toBe(false)
})
})

function buildReport(config: {
alerts: ScanReport['alerts']
healthy: boolean
Expand All @@ -39,30 +56,33 @@ function buildReport(config: {
}

function buildAlerts(): ScanReport['alerts'] {
return new Map([
[
'npm',
new Map([
[
'acme-widget',
new Map([
[
'1.0.0',
{
manifest: ['package.json'],
policy: 'error',
type: 'envVars',
url: 'https://socket.dev/npm/package/acme-widget',
},
],
]),
],
]),
],
]) as unknown as ScanReport['alerts']
const leaf: ReportLeafNode = {
manifest: ['package.json'],
policy: 'error',
type: 'envVars',
url: 'https://socket.dev/npm/package/acme-widget',
}
const fileMap: FileMap = new Map([['envVars at 1:2', leaf]])
const versionMap: VersionMap = new Map([['package.json', fileMap]])
const packageMap: PackageMap = new Map([['1.0.0', versionMap]])
const ecosystemMap: EcoMap = new Map([['acme-widget', packageMap]])

return new Map([['npm', ecosystemMap]])
}

describe('toPlainTextReport', () => {
it('labels the package version as a version', () => {
const report = buildReport({ alerts: buildAlerts(), healthy: false })
const text = toPlainTextReport(report)

expect(flattenReportAlerts(report)[0]).toMatchObject({
packageVersion: '1.0.0',
})
expect(text).toContain('VERSION')
expect(text).toContain('1.0.0')
expect(text).not.toContain('INTRODUCED BY')
})

it('emits no ANSI escape codes', () => {
const text = toPlainTextReport(
buildReport({ alerts: buildAlerts(), healthy: false }),
Expand Down Expand Up @@ -204,13 +224,153 @@ describe('toPlainTextReport', () => {
expect(overlong).toEqual([])
})

it('renders a large alert report without argument-count failures', () => {
const alertRow: ReportLeafNode = {
manifest: ['package.json'],
policy: 'error',
type: 'envVars',
url: '',
}
const packages = new Map<string, Map<string, ReportLeafNode>>()
const alertCount = 200_000
for (let i = 0; i < alertCount; i += 1) {
packages.set(
`package-${String(i).padStart(6, '0')}`,
new Map([['1.0.0', alertRow]]),
)
}
const alerts: ScanReport['alerts'] = new Map([['npm', packages]])
const text = toPlainTextReport(buildReport({ alerts, healthy: false }))

expect(text).toContain('200000 alerts with a policy')
expect(text).toContain('package-000000')
expect(text).toContain('package-199999')
})

it('says so plainly when there are no alerts', () => {
const text = toPlainTextReport(
buildReport({ alerts: new Map(), healthy: true }),
)

expect(text).toContain('No alerts')
expect(text).not.toContain('ALERT TYPE')
expect(text).not.toContain('VERSION')
})

it('retains package versions for version and file folds', () => {
const versionLeaf: ReportLeafNode = {
manifest: ['package.json'],
policy: 'error',
type: 'envVars',
url: '',
}
const versionFoldAlerts: ScanReport['alerts'] = new Map([
[
'npm',
new Map([['demo-transitive', new Map([['2.3.4', versionLeaf]])]]),
],
])
const fileFoldAlerts: ScanReport['alerts'] = new Map([
[
'npm',
new Map([
[
'demo-transitive',
new Map([
[
'2.3.4',
new Map([
['package.json', versionLeaf],
[
'src/index.js',
{ ...versionLeaf, manifest: ['package-lock.json'] },
],
]),
],
]),
],
]),
],
])
const versionFoldReport = {
...buildReport({ alerts: versionFoldAlerts, healthy: false }),
options: { fold: 'version' as const, reportLevel: 'error' as const },
}
const fileFoldReport = {
...buildReport({ alerts: fileFoldAlerts, healthy: false }),
options: { fold: 'file' as const, reportLevel: 'error' as const },
}

expect(flattenReportAlerts(versionFoldReport)[0]?.packageVersion).toBe(
'2.3.4',
)
expect(toPlainTextReport(versionFoldReport)).toContain('VERSION')
expect(toPlainTextReport(versionFoldReport)).toContain('2.3.4')
expect(flattenReportAlerts(fileFoldReport)).toEqual([
expect.objectContaining({
manifest: 'package.json',
packageVersion: '2.3.4',
}),
expect.objectContaining({
manifest: 'package-lock.json',
packageVersion: '2.3.4',
}),
])
expect(toPlainTextReport(fileFoldReport)).toContain('VERSION')
expect(toPlainTextReport(fileFoldReport)).toContain('2.3.4')
expect(toPlainTextReport(fileFoldReport)).toContain('package-lock.json')
})

it('keeps package-fold versions unknown and distinguishes package versions', () => {
const leaf: ReportLeafNode = {
manifest: ['package.json'],
policy: 'error',
type: 'envVars',
url: '',
}
const packageFoldAlerts: ScanReport['alerts'] = new Map([
['npm', new Map([['demo-transitive', leaf]])],
])
const twoVersionAlerts: ScanReport['alerts'] = new Map([
[
'npm',
new Map([
[
'demo-transitive',
new Map([
[
'2.3.4',
new Map([
['package.json', new Map([['envVars at 1:2', leaf]])],
]),
],
[
'2.3.5',
new Map([
['package.json', new Map([['envVars at 3:4', leaf]])],
]),
],
]),
],
]),
],
])
const packageFoldReport = {
...buildReport({ alerts: packageFoldAlerts, healthy: false }),
options: { fold: 'pkg' as const, reportLevel: 'error' as const },
}
const twoVersionReport = {
...buildReport({ alerts: twoVersionAlerts, healthy: false }),
options: { fold: 'none' as const, reportLevel: 'error' as const },
}

expect(flattenReportAlerts(packageFoldReport)[0]?.packageVersion).toBe(
'<unknown>',
)
expect(toPlainTextReport(packageFoldReport)).toContain('<unknown>')
expect(
flattenReportAlerts(twoVersionReport).map(row => row.packageVersion),
).toEqual(['2.3.4', '2.3.5'])
})
})

Expand All @@ -223,9 +383,9 @@ describe('flattenReportAlerts', () => {
expect(rows).toEqual([
{
alertType: 'envVars',
introducedBy: '1.0.0',
manifest: 'package.json',
packageName: 'acme-widget',
packageVersion: '1.0.0',
policy: 'error',
url: 'https://socket.dev/npm/package/acme-widget',
},
Expand Down
Loading