github-issue-tower-defence-management 1.148.25 → 1.149.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/README.md +5 -5
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js +2 -1
- package/bin/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.js.map +1 -1
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js +1 -0
- package/bin/adapter/repositories/ConfigurableSilentSessionMessageComposer.js.map +1 -1
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js +126 -0
- package/bin/adapter/repositories/FileSystemKevReportWatermarkRepository.js.map +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js +1 -0
- package/bin/adapter/repositories/ProcessListSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js +5 -3
- package/bin/adapter/repositories/TranscriptSessionSubAgentActivityRepository.js.map +1 -1
- package/bin/domain/entities/KevReportWatermark.js +3 -0
- package/bin/domain/entities/KevReportWatermark.js.map +1 -0
- package/bin/domain/usecases/DailySecurityScanUseCase.js +73 -19
- package/bin/domain/usecases/DailySecurityScanUseCase.js.map +1 -1
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js +9 -0
- package/bin/domain/usecases/DefaultSilentSessionMessageComposer.js.map +1 -1
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js +12 -11
- package/bin/domain/usecases/NotifySilentLiveSessionsUseCase.js.map +1 -1
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js +3 -0
- package/bin/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.js.map +1 -0
- package/package.json +1 -1
- package/src/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.ts +2 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.test.ts +35 -0
- package/src/adapter/repositories/ConfigurableSilentSessionMessageComposer.ts +7 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.test.ts +253 -0
- package/src/adapter/repositories/FileSystemKevReportWatermarkRepository.ts +130 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.test.ts +2 -0
- package/src/adapter/repositories/ProcessListSessionSubAgentActivityRepository.ts +1 -0
- package/src/adapter/repositories/TranscriptOwnerCallStatusProvider.test.ts +3 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.test.ts +159 -0
- package/src/adapter/repositories/TranscriptSessionSubAgentActivityRepository.ts +7 -1
- package/src/domain/entities/KevReportWatermark.ts +4 -0
- package/src/domain/entities/LiveSessionActivitySnapshot.ts +1 -0
- package/src/domain/usecases/DailySecurityScanUseCase.test.ts +461 -0
- package/src/domain/usecases/DailySecurityScanUseCase.ts +112 -29
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.test.ts +58 -1
- package/src/domain/usecases/DefaultSilentSessionMessageComposer.ts +21 -0
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.test.ts +146 -8
- package/src/domain/usecases/NotifySilentLiveSessionsUseCase.ts +19 -10
- package/src/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.ts +11 -0
- package/src/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.ts +3 -0
- package/types/adapter/entry-points/handlers/HandleScheduledEventUseCaseHandler.d.ts.map +1 -1
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts +2 -0
- package/types/adapter/repositories/ConfigurableSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts +10 -0
- package/types/adapter/repositories/FileSystemKevReportWatermarkRepository.d.ts.map +1 -0
- package/types/adapter/repositories/ProcessListSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/adapter/repositories/TranscriptSessionSubAgentActivityRepository.d.ts.map +1 -1
- package/types/domain/entities/KevReportWatermark.d.ts +5 -0
- package/types/domain/entities/KevReportWatermark.d.ts.map +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts +1 -0
- package/types/domain/entities/LiveSessionActivitySnapshot.d.ts.map +1 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts +3 -1
- package/types/domain/usecases/DailySecurityScanUseCase.d.ts.map +1 -1
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts +2 -0
- package/types/domain/usecases/DefaultSilentSessionMessageComposer.d.ts.map +1 -1
- package/types/domain/usecases/NotifySilentLiveSessionsUseCase.d.ts.map +1 -1
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts +15 -0
- package/types/domain/usecases/adapter-interfaces/KevReportWatermarkRepository.d.ts.map +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts +1 -0
- package/types/domain/usecases/adapter-interfaces/SilentSessionMessageComposer.d.ts.map +1 -1
|
@@ -344,6 +344,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
344
344
|
filePath,
|
|
345
345
|
fileName,
|
|
346
346
|
nowEpochSeconds,
|
|
347
|
+
liveSubAgentIds !== null,
|
|
347
348
|
loadNormalizedProcessCommandLines,
|
|
348
349
|
);
|
|
349
350
|
if (activity !== null) {
|
|
@@ -357,6 +358,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
357
358
|
filePath: string,
|
|
358
359
|
fileName: string,
|
|
359
360
|
nowEpochSeconds: number,
|
|
361
|
+
listedInRunningSubAgentsRecord: boolean,
|
|
360
362
|
loadNormalizedProcessCommandLines: () => Promise<string[]>,
|
|
361
363
|
): Promise<SubAgentActivity | null> => {
|
|
362
364
|
let content: string;
|
|
@@ -368,7 +370,10 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
368
370
|
return null;
|
|
369
371
|
}
|
|
370
372
|
const transcript = parseTranscript(content);
|
|
371
|
-
if (
|
|
373
|
+
if (
|
|
374
|
+
transcript.lastEntryIndicatesCompletion &&
|
|
375
|
+
!listedInRunningSubAgentsRecord
|
|
376
|
+
) {
|
|
372
377
|
return null;
|
|
373
378
|
}
|
|
374
379
|
const silentSeconds = clampToZero(
|
|
@@ -388,6 +393,7 @@ export class TranscriptSessionSubAgentActivityRepository implements SessionSubAg
|
|
|
388
393
|
transcript.pendingToolCommands,
|
|
389
394
|
loadNormalizedProcessCommandLines,
|
|
390
395
|
)),
|
|
396
|
+
finishedResultUnconsumed: transcript.lastEntryIndicatesCompletion,
|
|
391
397
|
};
|
|
392
398
|
};
|
|
393
399
|
|
|
@@ -2,6 +2,7 @@ import { DailySecurityScanUseCase } from './DailySecurityScanUseCase';
|
|
|
2
2
|
import { LocalCommandRunner } from './adapter-interfaces/LocalCommandRunner';
|
|
3
3
|
import { IssueRepository } from './adapter-interfaces/IssueRepository';
|
|
4
4
|
import { HttpRepository } from './adapter-interfaces/HttpRepository';
|
|
5
|
+
import { KevReportWatermarkRepository } from './adapter-interfaces/KevReportWatermarkRepository';
|
|
5
6
|
import { mock } from 'jest-mock-extended';
|
|
6
7
|
|
|
7
8
|
const KEV_CATALOG_URL =
|
|
@@ -12,16 +13,21 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
12
13
|
const mockLocalCommandRunner = mock<LocalCommandRunner>();
|
|
13
14
|
const mockIssueRepository = mock<IssueRepository>();
|
|
14
15
|
const mockHttpRepository = mock<HttpRepository>();
|
|
16
|
+
const mockKevReportWatermarkRepository =
|
|
17
|
+
mock<KevReportWatermarkRepository>();
|
|
18
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({ type: 'absent' });
|
|
15
19
|
const useCase = new DailySecurityScanUseCase(
|
|
16
20
|
mockLocalCommandRunner,
|
|
17
21
|
mockIssueRepository,
|
|
18
22
|
mockHttpRepository,
|
|
23
|
+
mockKevReportWatermarkRepository,
|
|
19
24
|
);
|
|
20
25
|
return {
|
|
21
26
|
useCase,
|
|
22
27
|
mockLocalCommandRunner,
|
|
23
28
|
mockIssueRepository,
|
|
24
29
|
mockHttpRepository,
|
|
30
|
+
mockKevReportWatermarkRepository,
|
|
25
31
|
};
|
|
26
32
|
};
|
|
27
33
|
|
|
@@ -903,6 +909,210 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
903
909
|
errorSpy.mockRestore();
|
|
904
910
|
});
|
|
905
911
|
|
|
912
|
+
it('logs an error and continues scanning remaining repositories when creating the findings issue fails', async () => {
|
|
913
|
+
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
914
|
+
buildUseCase();
|
|
915
|
+
const errorSpy = jest
|
|
916
|
+
.spyOn(console, 'error')
|
|
917
|
+
.mockImplementation(() => undefined);
|
|
918
|
+
|
|
919
|
+
mockIssueRepository.searchIssue.mockResolvedValue([]);
|
|
920
|
+
mockIssueRepository.createNewIssue.mockImplementation(
|
|
921
|
+
async (_org, repositoryName) => {
|
|
922
|
+
if (repositoryName === 'archived') {
|
|
923
|
+
throw new Error(
|
|
924
|
+
'Request failed with status code 403: Repository was archived so is read-only.',
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
return 1;
|
|
928
|
+
},
|
|
929
|
+
);
|
|
930
|
+
|
|
931
|
+
mockLocalCommandRunner.runCommand.mockImplementation(
|
|
932
|
+
async (program, args) => {
|
|
933
|
+
if (program === 'find') {
|
|
934
|
+
return {
|
|
935
|
+
stdout:
|
|
936
|
+
'/repos/example-org/archived/.git\n/repos/example-org/app/.git\n',
|
|
937
|
+
stderr: '',
|
|
938
|
+
exitCode: 0,
|
|
939
|
+
};
|
|
940
|
+
}
|
|
941
|
+
if (program === 'mktemp') {
|
|
942
|
+
return {
|
|
943
|
+
stdout: `/tmp/${args[args.length - 1].replace('XXXXXX', 'abc123')}\n`,
|
|
944
|
+
stderr: '',
|
|
945
|
+
exitCode: 0,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
if (program === 'git') {
|
|
949
|
+
const dir = args[1];
|
|
950
|
+
const repo = dir.split('/').pop() ?? '';
|
|
951
|
+
return {
|
|
952
|
+
stdout: `git@github.com:example-org/${repo}.git\n`,
|
|
953
|
+
stderr: '',
|
|
954
|
+
exitCode: 0,
|
|
955
|
+
};
|
|
956
|
+
}
|
|
957
|
+
if (program === 'osv-scanner') {
|
|
958
|
+
return {
|
|
959
|
+
stdout: JSON.stringify({
|
|
960
|
+
results: [
|
|
961
|
+
{
|
|
962
|
+
source: {
|
|
963
|
+
path: args[args.indexOf('-r') + 1],
|
|
964
|
+
type: 'lockfile',
|
|
965
|
+
},
|
|
966
|
+
packages: [
|
|
967
|
+
{
|
|
968
|
+
package: {
|
|
969
|
+
name: 'example-library',
|
|
970
|
+
version: '1.2.3',
|
|
971
|
+
ecosystem: 'npm',
|
|
972
|
+
},
|
|
973
|
+
vulnerabilities: [
|
|
974
|
+
{
|
|
975
|
+
id: 'GHSA-1111-2222-3333',
|
|
976
|
+
aliases: [],
|
|
977
|
+
summary: 'Example Library Deserialization',
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
},
|
|
981
|
+
],
|
|
982
|
+
},
|
|
983
|
+
],
|
|
984
|
+
}),
|
|
985
|
+
stderr: '',
|
|
986
|
+
exitCode: 1,
|
|
987
|
+
};
|
|
988
|
+
}
|
|
989
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
990
|
+
},
|
|
991
|
+
);
|
|
992
|
+
|
|
993
|
+
await useCase.run({
|
|
994
|
+
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
995
|
+
org: 'example-org',
|
|
996
|
+
manager: 'manager-name',
|
|
997
|
+
dailySecurityScan: {
|
|
998
|
+
scanBaseDirectory: '/repos',
|
|
999
|
+
targetHourUtc: 5,
|
|
1000
|
+
},
|
|
1001
|
+
});
|
|
1002
|
+
|
|
1003
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1004
|
+
expect.stringContaining('archived'),
|
|
1005
|
+
);
|
|
1006
|
+
expect(
|
|
1007
|
+
mockIssueRepository.createNewIssue.mock.calls.map((call) => call[1]),
|
|
1008
|
+
).toEqual(['archived', 'app']);
|
|
1009
|
+
errorSpy.mockRestore();
|
|
1010
|
+
});
|
|
1011
|
+
|
|
1012
|
+
it('logs an error and continues scanning remaining repositories when commenting on the existing findings issue fails', async () => {
|
|
1013
|
+
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
1014
|
+
buildUseCase();
|
|
1015
|
+
const errorSpy = jest
|
|
1016
|
+
.spyOn(console, 'error')
|
|
1017
|
+
.mockImplementation(() => undefined);
|
|
1018
|
+
|
|
1019
|
+
mockIssueRepository.searchIssue.mockImplementation(
|
|
1020
|
+
async ({ repositoryName }) =>
|
|
1021
|
+
repositoryName === 'oversized'
|
|
1022
|
+
? [
|
|
1023
|
+
{
|
|
1024
|
+
url: 'https://github.com/example-org/oversized/issues/1',
|
|
1025
|
+
title: 'Daily security scan findings',
|
|
1026
|
+
number: '1',
|
|
1027
|
+
},
|
|
1028
|
+
]
|
|
1029
|
+
: [],
|
|
1030
|
+
);
|
|
1031
|
+
mockIssueRepository.createCommentByUrl.mockRejectedValue(
|
|
1032
|
+
new Error('Request failed with status code 422: Validation Failed'),
|
|
1033
|
+
);
|
|
1034
|
+
|
|
1035
|
+
mockLocalCommandRunner.runCommand.mockImplementation(
|
|
1036
|
+
async (program, args) => {
|
|
1037
|
+
if (program === 'find') {
|
|
1038
|
+
return {
|
|
1039
|
+
stdout:
|
|
1040
|
+
'/repos/example-org/oversized/.git\n/repos/example-org/app/.git\n',
|
|
1041
|
+
stderr: '',
|
|
1042
|
+
exitCode: 0,
|
|
1043
|
+
};
|
|
1044
|
+
}
|
|
1045
|
+
if (program === 'mktemp') {
|
|
1046
|
+
return {
|
|
1047
|
+
stdout: `/tmp/${args[args.length - 1].replace('XXXXXX', 'abc123')}\n`,
|
|
1048
|
+
stderr: '',
|
|
1049
|
+
exitCode: 0,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
if (program === 'git') {
|
|
1053
|
+
const dir = args[1];
|
|
1054
|
+
const repo = dir.split('/').pop() ?? '';
|
|
1055
|
+
return {
|
|
1056
|
+
stdout: `git@github.com:example-org/${repo}.git\n`,
|
|
1057
|
+
stderr: '',
|
|
1058
|
+
exitCode: 0,
|
|
1059
|
+
};
|
|
1060
|
+
}
|
|
1061
|
+
if (program === 'osv-scanner') {
|
|
1062
|
+
return {
|
|
1063
|
+
stdout: JSON.stringify({
|
|
1064
|
+
results: [
|
|
1065
|
+
{
|
|
1066
|
+
source: {
|
|
1067
|
+
path: args[args.indexOf('-r') + 1],
|
|
1068
|
+
type: 'lockfile',
|
|
1069
|
+
},
|
|
1070
|
+
packages: [
|
|
1071
|
+
{
|
|
1072
|
+
package: {
|
|
1073
|
+
name: 'example-library',
|
|
1074
|
+
version: '1.2.3',
|
|
1075
|
+
ecosystem: 'npm',
|
|
1076
|
+
},
|
|
1077
|
+
vulnerabilities: [
|
|
1078
|
+
{
|
|
1079
|
+
id: 'GHSA-1111-2222-3333',
|
|
1080
|
+
aliases: [],
|
|
1081
|
+
summary: 'Example Library Deserialization',
|
|
1082
|
+
},
|
|
1083
|
+
],
|
|
1084
|
+
},
|
|
1085
|
+
],
|
|
1086
|
+
},
|
|
1087
|
+
],
|
|
1088
|
+
}),
|
|
1089
|
+
stderr: '',
|
|
1090
|
+
exitCode: 1,
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
return { stdout: '', stderr: '', exitCode: 0 };
|
|
1094
|
+
},
|
|
1095
|
+
);
|
|
1096
|
+
|
|
1097
|
+
await useCase.run({
|
|
1098
|
+
targetDates: [new Date('2024-01-02T05:00:00Z')],
|
|
1099
|
+
org: 'example-org',
|
|
1100
|
+
manager: 'manager-name',
|
|
1101
|
+
dailySecurityScan: {
|
|
1102
|
+
scanBaseDirectory: '/repos',
|
|
1103
|
+
targetHourUtc: 5,
|
|
1104
|
+
},
|
|
1105
|
+
});
|
|
1106
|
+
|
|
1107
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1108
|
+
expect.stringContaining('oversized'),
|
|
1109
|
+
);
|
|
1110
|
+
expect(
|
|
1111
|
+
mockIssueRepository.createNewIssue.mock.calls.map((call) => call[1]),
|
|
1112
|
+
).toEqual(['app']);
|
|
1113
|
+
errorSpy.mockRestore();
|
|
1114
|
+
});
|
|
1115
|
+
|
|
906
1116
|
it('adds a comment to the existing open issue for the repository instead of creating a new one', async () => {
|
|
907
1117
|
const { useCase, mockLocalCommandRunner, mockIssueRepository } =
|
|
908
1118
|
buildUseCase();
|
|
@@ -1105,6 +1315,53 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
1105
1315
|
},
|
|
1106
1316
|
});
|
|
1107
1317
|
|
|
1318
|
+
it('still reports KEV additions when the scanned repository findings issue cannot be written', async () => {
|
|
1319
|
+
const { useCase, mockIssueRepository } = buildScanEnvironment(
|
|
1320
|
+
osvScanOutput([
|
|
1321
|
+
{
|
|
1322
|
+
name: 'example-library',
|
|
1323
|
+
version: '1.2.3',
|
|
1324
|
+
ecosystem: 'npm',
|
|
1325
|
+
id: 'GHSA-1111-2222-3333',
|
|
1326
|
+
aliases: ['CVE-2024-0001'],
|
|
1327
|
+
summary: 'Example Library Deserialization',
|
|
1328
|
+
},
|
|
1329
|
+
]),
|
|
1330
|
+
{
|
|
1331
|
+
vulnerabilities: [
|
|
1332
|
+
{
|
|
1333
|
+
cveID: 'CVE-2024-0001',
|
|
1334
|
+
vendorProject: 'Example',
|
|
1335
|
+
product: 'Example Library',
|
|
1336
|
+
vulnerabilityName: 'Example Library Deserialization',
|
|
1337
|
+
dateAdded: '2024-01-02',
|
|
1338
|
+
},
|
|
1339
|
+
],
|
|
1340
|
+
},
|
|
1341
|
+
);
|
|
1342
|
+
const errorSpy = jest
|
|
1343
|
+
.spyOn(console, 'error')
|
|
1344
|
+
.mockImplementation(() => undefined);
|
|
1345
|
+
mockIssueRepository.createNewIssue.mockImplementation(
|
|
1346
|
+
async (_org, repositoryName) => {
|
|
1347
|
+
if (repositoryName === 'app') {
|
|
1348
|
+
throw new Error(
|
|
1349
|
+
'Request failed with status code 403: Repository was archived so is read-only.',
|
|
1350
|
+
);
|
|
1351
|
+
}
|
|
1352
|
+
return 1;
|
|
1353
|
+
},
|
|
1354
|
+
);
|
|
1355
|
+
|
|
1356
|
+
await runWithKevReporting(useCase);
|
|
1357
|
+
|
|
1358
|
+
expect(errorSpy).toHaveBeenCalledWith(expect.stringContaining('app'));
|
|
1359
|
+
expect(
|
|
1360
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1361
|
+
).toHaveLength(1);
|
|
1362
|
+
errorSpy.mockRestore();
|
|
1363
|
+
});
|
|
1364
|
+
|
|
1108
1365
|
it('reports a KEV addition whose CVE the scanner found at an installed version', async () => {
|
|
1109
1366
|
const { useCase, mockIssueRepository } = buildScanEnvironment(
|
|
1110
1367
|
osvScanOutput([
|
|
@@ -1238,5 +1495,209 @@ describe('DailySecurityScanUseCase', () => {
|
|
|
1238
1495
|
);
|
|
1239
1496
|
expect(grepCalls).toHaveLength(0);
|
|
1240
1497
|
});
|
|
1498
|
+
|
|
1499
|
+
const scannedExampleLibrary = osvScanOutput([
|
|
1500
|
+
{
|
|
1501
|
+
name: 'example-library',
|
|
1502
|
+
version: '1.2.3',
|
|
1503
|
+
ecosystem: 'npm',
|
|
1504
|
+
id: 'GHSA-1111-2222-3333',
|
|
1505
|
+
aliases: ['CVE-2024-0001', 'CVE-2024-0002', 'CVE-2024-0003'],
|
|
1506
|
+
summary: 'Example Library Deserialization',
|
|
1507
|
+
},
|
|
1508
|
+
]);
|
|
1509
|
+
|
|
1510
|
+
const kevCatalogOf = (
|
|
1511
|
+
vulnerabilities: { cveID: string; dateAdded: string }[],
|
|
1512
|
+
) => ({
|
|
1513
|
+
vulnerabilities: vulnerabilities.map((vulnerability) => ({
|
|
1514
|
+
cveID: vulnerability.cveID,
|
|
1515
|
+
vendorProject: 'Example',
|
|
1516
|
+
product: 'ExampleLibrary',
|
|
1517
|
+
vulnerabilityName: `${vulnerability.cveID} Deserialization`,
|
|
1518
|
+
dateAdded: vulnerability.dateAdded,
|
|
1519
|
+
})),
|
|
1520
|
+
});
|
|
1521
|
+
|
|
1522
|
+
it('records every addition it considered in the watermark after the report issue is created', async () => {
|
|
1523
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1524
|
+
buildScanEnvironment(
|
|
1525
|
+
scannedExampleLibrary,
|
|
1526
|
+
kevCatalogOf([
|
|
1527
|
+
{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-01' },
|
|
1528
|
+
{ cveID: 'CVE-2024-0002', dateAdded: '2024-01-02' },
|
|
1529
|
+
]),
|
|
1530
|
+
);
|
|
1531
|
+
|
|
1532
|
+
await runWithKevReporting(useCase);
|
|
1533
|
+
|
|
1534
|
+
expect(
|
|
1535
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1536
|
+
).toHaveLength(1);
|
|
1537
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toEqual([
|
|
1538
|
+
[
|
|
1539
|
+
{
|
|
1540
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1541
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0002'],
|
|
1542
|
+
},
|
|
1543
|
+
],
|
|
1544
|
+
]);
|
|
1545
|
+
});
|
|
1546
|
+
|
|
1547
|
+
it('does not report an addition that the stored watermark already covers', async () => {
|
|
1548
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1549
|
+
buildScanEnvironment(
|
|
1550
|
+
scannedExampleLibrary,
|
|
1551
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' }]),
|
|
1552
|
+
);
|
|
1553
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1554
|
+
type: 'stored',
|
|
1555
|
+
watermark: {
|
|
1556
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1557
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0001'],
|
|
1558
|
+
},
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
await runWithKevReporting(useCase);
|
|
1562
|
+
|
|
1563
|
+
expect(
|
|
1564
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1565
|
+
).toHaveLength(0);
|
|
1566
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toHaveLength(0);
|
|
1567
|
+
});
|
|
1568
|
+
|
|
1569
|
+
it('reports an addition sharing the stored watermark date that was not reported before', async () => {
|
|
1570
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1571
|
+
buildScanEnvironment(
|
|
1572
|
+
scannedExampleLibrary,
|
|
1573
|
+
kevCatalogOf([
|
|
1574
|
+
{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' },
|
|
1575
|
+
{ cveID: 'CVE-2024-0002', dateAdded: '2024-01-02' },
|
|
1576
|
+
]),
|
|
1577
|
+
);
|
|
1578
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1579
|
+
type: 'stored',
|
|
1580
|
+
watermark: {
|
|
1581
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1582
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-0001'],
|
|
1583
|
+
},
|
|
1584
|
+
});
|
|
1585
|
+
|
|
1586
|
+
await runWithKevReporting(useCase);
|
|
1587
|
+
|
|
1588
|
+
const kevCalls = kevReportCalls(
|
|
1589
|
+
mockIssueRepository.createNewIssue.mock.calls,
|
|
1590
|
+
);
|
|
1591
|
+
expect(kevCalls).toHaveLength(1);
|
|
1592
|
+
expect(kevCalls[0][3]).toContain('CVE-2024-0002');
|
|
1593
|
+
expect(kevCalls[0][3]).not.toContain('CVE-2024-0001');
|
|
1594
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toEqual([
|
|
1595
|
+
[
|
|
1596
|
+
{
|
|
1597
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1598
|
+
reportedCveIdsOnLastReportedDateAdded: [
|
|
1599
|
+
'CVE-2024-0001',
|
|
1600
|
+
'CVE-2024-0002',
|
|
1601
|
+
],
|
|
1602
|
+
},
|
|
1603
|
+
],
|
|
1604
|
+
]);
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
it('does not select an addition dated strictly before the stored watermark date', async () => {
|
|
1608
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1609
|
+
buildScanEnvironment(
|
|
1610
|
+
scannedExampleLibrary,
|
|
1611
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-01' }]),
|
|
1612
|
+
);
|
|
1613
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1614
|
+
type: 'stored',
|
|
1615
|
+
watermark: {
|
|
1616
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1617
|
+
reportedCveIdsOnLastReportedDateAdded: [],
|
|
1618
|
+
},
|
|
1619
|
+
});
|
|
1620
|
+
|
|
1621
|
+
await runWithKevReporting(useCase);
|
|
1622
|
+
|
|
1623
|
+
expect(
|
|
1624
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1625
|
+
).toHaveLength(0);
|
|
1626
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toHaveLength(0);
|
|
1627
|
+
});
|
|
1628
|
+
|
|
1629
|
+
it('advances the watermark over an addition that affects no scanned package so it is not weighed again', async () => {
|
|
1630
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1631
|
+
buildScanEnvironment(
|
|
1632
|
+
scannedExampleLibrary,
|
|
1633
|
+
kevCatalogOf([{ cveID: 'CVE-2024-9999', dateAdded: '2024-01-02' }]),
|
|
1634
|
+
);
|
|
1635
|
+
|
|
1636
|
+
await runWithKevReporting(useCase);
|
|
1637
|
+
|
|
1638
|
+
expect(
|
|
1639
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1640
|
+
).toHaveLength(0);
|
|
1641
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toEqual([
|
|
1642
|
+
[
|
|
1643
|
+
{
|
|
1644
|
+
lastReportedDateAdded: '2024-01-02',
|
|
1645
|
+
reportedCveIdsOnLastReportedDateAdded: ['CVE-2024-9999'],
|
|
1646
|
+
},
|
|
1647
|
+
],
|
|
1648
|
+
]);
|
|
1649
|
+
});
|
|
1650
|
+
|
|
1651
|
+
it('skips the report and leaves the watermark untouched when the stored watermark is unreadable', async () => {
|
|
1652
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1653
|
+
buildScanEnvironment(
|
|
1654
|
+
scannedExampleLibrary,
|
|
1655
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' }]),
|
|
1656
|
+
);
|
|
1657
|
+
mockKevReportWatermarkRepository.load.mockResolvedValue({
|
|
1658
|
+
type: 'unreadable',
|
|
1659
|
+
reason: 'the stored watermark file holds malformed JSON',
|
|
1660
|
+
});
|
|
1661
|
+
const errorSpy = jest
|
|
1662
|
+
.spyOn(console, 'error')
|
|
1663
|
+
.mockImplementation(() => {});
|
|
1664
|
+
|
|
1665
|
+
await runWithKevReporting(useCase);
|
|
1666
|
+
|
|
1667
|
+
expect(
|
|
1668
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1669
|
+
).toHaveLength(0);
|
|
1670
|
+
expect(mockKevReportWatermarkRepository.save.mock.calls).toHaveLength(0);
|
|
1671
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1672
|
+
expect.stringContaining(
|
|
1673
|
+
'the stored watermark file holds malformed JSON',
|
|
1674
|
+
),
|
|
1675
|
+
);
|
|
1676
|
+
errorSpy.mockRestore();
|
|
1677
|
+
});
|
|
1678
|
+
|
|
1679
|
+
it('creates the report issue and logs when the watermark write fails, so the same additions are weighed again', async () => {
|
|
1680
|
+
const { useCase, mockIssueRepository, mockKevReportWatermarkRepository } =
|
|
1681
|
+
buildScanEnvironment(
|
|
1682
|
+
scannedExampleLibrary,
|
|
1683
|
+
kevCatalogOf([{ cveID: 'CVE-2024-0001', dateAdded: '2024-01-02' }]),
|
|
1684
|
+
);
|
|
1685
|
+
mockKevReportWatermarkRepository.save.mockRejectedValue(
|
|
1686
|
+
new Error('disk is read-only'),
|
|
1687
|
+
);
|
|
1688
|
+
const errorSpy = jest
|
|
1689
|
+
.spyOn(console, 'error')
|
|
1690
|
+
.mockImplementation(() => {});
|
|
1691
|
+
|
|
1692
|
+
await runWithKevReporting(useCase);
|
|
1693
|
+
|
|
1694
|
+
expect(
|
|
1695
|
+
kevReportCalls(mockIssueRepository.createNewIssue.mock.calls),
|
|
1696
|
+
).toHaveLength(1);
|
|
1697
|
+
expect(errorSpy).toHaveBeenCalledWith(
|
|
1698
|
+
expect.stringContaining('disk is read-only'),
|
|
1699
|
+
);
|
|
1700
|
+
errorSpy.mockRestore();
|
|
1701
|
+
});
|
|
1241
1702
|
});
|
|
1242
1703
|
});
|