Skip to content

Commit 0162f2a

Browse files
authored
chore: add debug for unknown purls (#91)
* chore: add warning for unknown purls This commit adds a debug in cases where a generated sbom has a purl, but the NES/XEOL database does not have any information. This commit also improves typing in several ways that makes it easier to trace why scan details might be missing in the first place: - Use an actual Map when building scan results - set ScanResultComponent.status to optional to match current api - create ComponentStatus type union - replace optional Line properties with defined properties where feasible
1 parent f8220bd commit 0162f2a

File tree

10 files changed

+78
-62
lines changed

10 files changed

+78
-62
lines changed

.vscode/settings.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
{
2-
"cSpell.words": ["mycommand", "prerun"],
2+
"cSpell.words": ["mycommand", "prerun", "XEOL"],
33
"typescript.tsdk": "node_modules/typescript/lib"
44
}

src/hooks/prerun/CommandContextHook.ts

+2-1
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import type { Hook } from '@oclif/core';
33
import { initOclifLog, log } from '../../utils/log.util.ts';
44

55
const hook: Hook<'prerun'> = async (opts) => {
6-
initOclifLog(opts.context.log, opts.context.log);
6+
initOclifLog(opts.context.log, opts.context.log, opts.context.debug);
77
log.info = opts.context.log || log.info;
88
log.warn = opts.context.log || log.warn;
9+
log.debug = opts.context.debug || log.debug;
910
};
1011

1112
export default hook;

src/service/eol/eol.svc.ts

+18-10
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { log } from '../../utils/log.util.ts';
2-
import { daysBetween } from '../../utils/misc.ts';
2+
import { daysBetween } from '../line.ts';
33
import type { Line } from '../line.ts';
4-
import type { ScanResult, ScanResultComponent } from '../nes/modules/sbom.ts';
4+
import type { ComponentStatus, ScanResult } from '../nes/modules/sbom.ts';
55
import { NesApolloClient } from '../nes/nes.client.ts';
66
import { createBomFromDir } from './cdx.svc.ts';
77
import type { Sbom, SbomEntry, SbomMap as SbomModel, ScanOptions } from './eol.types.ts';
@@ -62,22 +62,30 @@ export async function submitScan(model: SbomModel): Promise<ScanResult> {
6262
* processing and/or rendering.
6363
*/
6464
export async function prepareRows({ components, purls }: SbomModel, scan: ScanResult): Promise<Line[]> {
65-
let lines = purls.map((purl) => {
65+
let lines: Line[] = purls.map((purl) => {
6666
const { evidence } = components[purl];
6767
const occ = evidence?.occurrences?.map((o) => o.location).join('\n\t - ');
6868
const occurrences = SHOW_OCCURRENCES && Boolean(occ) ? `\t - ${occ}\n` : '';
6969

70-
const details = scan?.components[purl];
71-
const info: ScanResultComponent['info'] = details?.info || {
72-
eolAt: undefined,
73-
isEol: false,
74-
isUnsafe: false,
75-
};
70+
const details = scan.components.get(purl);
71+
72+
if (!details) {
73+
// In this case, the purl string is in the generated sbom, but the NES/XEOL api has no data
74+
log.debug(`Unknown status: ${purl}.`);
75+
}
76+
77+
const info = details
78+
? details.info
79+
: {
80+
eolAt: null,
81+
isEol: false,
82+
isUnsafe: false,
83+
};
7684

7785
info.eolAt = typeof info.eolAt === 'string' && info.eolAt ? new Date(info.eolAt) : info.eolAt;
7886

7987
const daysEol = info.eolAt ? daysBetween(new Date(), info.eolAt) : undefined;
80-
let status: 'EOL' | 'LTS' | 'OK' = 'OK';
88+
let status: ComponentStatus = 'OK';
8189

8290
// TODO: extract this logic into the Line.ts file somehow, so that there is a unified Line model
8391
if (daysEol === undefined) {

src/service/eol/eol.types.ts

-2
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,3 @@ export interface ScanOptions {
2424
cdxgen?: CdxGenOptions;
2525
}
2626
export type CdxCreator = (dir: string, opts: CdxGenOptions) => Promise<{ bomJson: Sbom }>;
27-
28-
export type SbomScan<T> = <SB>(sbom: SB) => Promise<T>;

src/service/line.ts

+7-9
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,22 @@
11
import { ux } from '@oclif/core';
2-
import type { ScanResultComponent } from './nes/modules/sbom.ts';
2+
import type { ComponentStatus, ScanResultComponent } from './nes/modules/sbom.ts';
33

44
export interface Line {
5-
daysEol?: number;
5+
daysEol: number | undefined;
66
purl: ScanResultComponent['purl'];
7-
info?: {
8-
eolAt?: Date;
7+
info: {
8+
eolAt: Date | null;
99
isEol: boolean;
1010
};
11-
status: ScanResultComponent['status'];
11+
status: ComponentStatus;
1212
}
1313

1414
export function daysBetween(date1: Date, date2: Date) {
1515
const msPerDay = 1000 * 60 * 60 * 24 + 15; // milliseconds in a day plus 15 ms
1616
return Math.round((date2.getTime() - date1.getTime()) / msPerDay);
1717
}
1818

19-
export function getMessageAndStatus(status: string, eolAt?: Date) {
19+
export function getMessageAndStatus(status: string, eolAt: Date | null) {
2020
let msg = '';
2121
let stat = '';
2222

@@ -47,9 +47,7 @@ export function getMessageAndStatus(status: string, eolAt?: Date) {
4747
}
4848

4949
export function formatLine(l: Line, idx: number, ctx: { longest: number; total: number }) {
50-
let { info, purl, status } = l;
51-
52-
info = info || { eolAt: new Date(), isEol: false };
50+
const { info, purl, status } = l;
5351

5452
if (info.isEol && status !== 'EOL') {
5553
throw new Error(`isEol is true but status is not EOL: ${purl}`);

src/service/nes/modules/sbom.test.ts

+17-16
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { ok } from 'node:assert';
2+
import { describe, it } from 'node:test';
23

3-
import type { ApolloHelper } from '../nes.client';
4-
import { type ScanResult, SbomScanner as sbomScanner } from './sbom';
4+
import type { ApolloHelper } from '../nes.client.ts';
5+
import { type ScanResult, type ScanResultComponent, SbomScanner as sbomScanner } from './sbom.ts';
56

67
describe('SBOM Scanner', () => {
78
it('parses response and creates components record', async () => {
@@ -29,7 +30,7 @@ describe('SBOM Scanner', () => {
2930
});
3031
});
3132

32-
const STATUS_OK = { isEol: false, isUnsafe: false };
33+
const STATUS_OK = { eolAt: null, isEol: false, isUnsafe: false };
3334

3435
const DATE_MS = 1000 * 60 * 60 * 24;
3536
const STATUS_EOL = () => ({
@@ -38,29 +39,29 @@ const STATUS_EOL = () => ({
3839
isUnsafe: false,
3940
});
4041

42+
const components = new Map<string, ScanResultComponent>();
43+
components.set('pkg:npm/[email protected]', {
44+
info: STATUS_EOL(),
45+
purl: 'pkg:npm/[email protected]',
46+
status: 'EOL',
47+
});
48+
components.set('pkg:npm/[email protected]', {
49+
info: STATUS_OK,
50+
purl: 'pkg:npm/[email protected]',
51+
status: 'OK',
52+
});
4153
const mocked = {
4254
success: {
4355
insights: {
4456
scan: {
4557
eol: {
46-
components: {
47-
'pkg:npm/[email protected]': {
48-
info: STATUS_EOL(),
49-
purl: 'pkg:npm/[email protected]',
50-
status: 'EOL',
51-
},
52-
'pkg:npm/[email protected]': {
53-
info: STATUS_OK,
54-
purl: 'pkg:npm/[email protected]',
55-
status: 'OK',
56-
},
57-
},
58+
components,
5859
diagnostics: {
5960
__mock: true,
6061
},
6162
message: 'Your scan was completed successfully!',
6263
success: true,
63-
} as ScanResult,
64+
} satisfies ScanResult,
6465
},
6566
},
6667
},

src/service/nes/modules/sbom.ts

+19-14
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,17 @@ import { log } from '../../../utils/log.util.ts';
44
import type { SbomMap } from '../../eol/eol.types.ts';
55
import type { ApolloHelper } from '../nes.client.ts';
66

7-
export const buildScanResult = (scan: ScanResponseReport): ScanResult => ({
8-
components: Object.fromEntries(scan.components.map((c) => [c.purl, c])),
9-
message: scan.message,
10-
success: true,
11-
});
7+
export const buildScanResult = (scan: ScanResponseReport): ScanResult => {
8+
const components = new Map<string, ScanResultComponent>();
9+
for (const c of scan.components) {
10+
components.set(c.purl, c);
11+
}
12+
return {
13+
components,
14+
message: scan.message,
15+
success: true,
16+
};
17+
};
1218

1319
export const SbomScanner =
1420
(client: ApolloHelper) =>
@@ -25,11 +31,7 @@ export const SbomScanner =
2531
}
2632

2733
const result = buildScanResult(scan);
28-
// const result: ScanResult = {
29-
// components: Object.fromEntries(scan.components.map((c) => [c.purl, c])),
30-
// message: scan.message,
31-
// success: true
32-
// }
34+
3335
return result;
3436
};
3537

@@ -50,19 +52,22 @@ export interface ScanResponseReport {
5052
success: boolean;
5153
}
5254
export interface ScanResult {
53-
components: Record<string, ScanResultComponent>;
55+
components: Map<string, ScanResultComponent>;
5456
diagnostics?: Record<string, unknown>;
5557
message: string;
5658
success: boolean;
5759
}
60+
61+
export type ComponentStatus = 'EOL' | 'LTS' | 'OK';
62+
5863
export interface ScanResultComponent {
59-
info?: {
60-
eolAt?: Date;
64+
info: {
65+
eolAt: Date | null;
6166
isEol: boolean;
6267
isUnsafe: boolean;
6368
};
6469
purl: string;
65-
status: 'EOL' | 'LTS' | 'OK';
70+
status?: ComponentStatus;
6671
}
6772

6873
const M_SCAN = {

src/utils/log.util.ts

+5
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,17 @@ export const log = {
99
warn: (_message?: unknown, ...args: unknown[]) => {
1010
console.warn('[default_warn]', ...args);
1111
},
12+
debug: (_message?: unknown, ...args: unknown[]) => {
13+
console.debug('[default_debug]', ...args);
14+
},
1215
};
1316

1417
export const initOclifLog = (
1518
info: (message?: unknown, ...args: unknown[]) => void,
1619
warn: (message?: unknown, ...args: unknown[]) => void,
20+
debug: (message?: unknown, ...args: unknown[]) => void,
1721
) => {
1822
log.info = info;
1923
log.warn = warn;
24+
log.debug = debug;
2025
};

src/utils/misc.ts

-4
This file was deleted.

test/service/line.test.ts

+9-5
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,18 @@ describe('line', () => {
3434
});
3535

3636
it('should format OK status', () => {
37-
const { stat, msg } = getMessageAndStatus('OK');
37+
const { stat, msg } = getMessageAndStatus('OK', null);
3838
assert(stat.includes('OK'));
3939
assert.equal(msg, '');
4040
});
4141

4242
it('should handle missing eolAt date', () => {
43-
const { msg } = getMessageAndStatus('EOL');
44-
assert(msg.includes('unknown') && msg.includes('days ago'));
43+
const { msg } = getMessageAndStatus('EOL', null);
44+
assert(msg.includes("EOL'd") && msg.includes('unknown') && msg.includes('days ago'));
4545
});
4646

4747
it('should throw on unknown status', () => {
48-
assert.throws(() => getMessageAndStatus('INVALID'), /Unknown status: INVALID/);
48+
assert.throws(() => getMessageAndStatus('INVALID', null), /Unknown status: INVALID/);
4949
});
5050
});
5151

@@ -56,6 +56,7 @@ describe('line', () => {
5656
const line: Line = {
5757
purl: 'pkg:npm/[email protected]',
5858
status: 'EOL',
59+
daysEol: 30,
5960
info: { isEol: true, eolAt: new Date() },
6061
};
6162
const result = formatLine(line, 0, context);
@@ -69,7 +70,8 @@ describe('line', () => {
6970
const line: Line = {
7071
purl: 'pkg:npm/[email protected]',
7172
status: 'OK',
72-
info: { isEol: true },
73+
daysEol: undefined,
74+
info: { isEol: true, eolAt: null },
7375
};
7476
assert.throws(() => formatLine(line, 0, context), /isEol is true but status is not EOL/);
7577
});
@@ -78,6 +80,8 @@ describe('line', () => {
7880
const line: Line = {
7981
purl: 'pkg:npm/[email protected]',
8082
status: 'OK',
83+
daysEol: undefined,
84+
info: { isEol: false, eolAt: null },
8185
};
8286
const result = formatLine(line, 0, context);
8387
assert(result.name.includes('OK'));

0 commit comments

Comments
 (0)