Skip to content

Commit 8c911d6

Browse files
committed
Close #615: Include vulnerability repos in JSON and CycloneDX output
1 parent db79fa7 commit 8c911d6

12 files changed

Lines changed: 105 additions & 18 deletions

File tree

chrome/extension/js/generated/retire-chrome.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10871,7 +10871,7 @@ var retirechrome = (() => {
1087110871
"../../node/lib/retire.js"(exports) {
1087210872
"use strict";
1087310873
var exports = exports || {};
10874-
exports.version = "5.4.3";
10874+
exports.version = "5.5.0";
1087510875
function isDefined(o) {
1087610876
return typeof o !== "undefined";
1087710877
}

node/lib/retire.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
var exports = exports || {};
7-
exports.version = '5.4.3';
7+
exports.version = '5.5.0';
88

99
function isDefined(o) {
1010
return typeof o !== 'undefined';

node/package-lock.json

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

node/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"author": "Erlend Oftedal <erlend@oftedal.no>",
33
"name": "retire",
44
"description": "Retire is a tool for detecting use of vulnerable libraries",
5-
"version": "5.4.3",
5+
"version": "5.5.0",
66
"license": "Apache-2.0",
77
"repository": {
88
"type": "git",

node/spec/tests/cyclonedx.spec.ts

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ const loggerOptions: LoggerOptions = {
3636
colors: false,
3737
path: '.',
3838
colorwarn: () => '',
39-
jsRepo: 'testrepo.json',
39+
jsRepo: ['testrepo.json'],
4040
};
4141

4242
describe('cyclonedx-json', () => {
@@ -61,6 +61,9 @@ describe('cyclonedx-json', () => {
6161
assert.strictEqual(res.valid, true);
6262
assert.strictEqual(output.bomFormat, 'CycloneDX');
6363
assert.strictEqual(output.specVersion, '1.4');
64+
assert.deepStrictEqual(output.metadata.properties, [
65+
{ name: 'retirejs:vulnerability-repository', value: 'testrepo.json' },
66+
]);
6467
});
6568

6669
it('should validate report according to schema 1.6', () => {
@@ -85,6 +88,37 @@ describe('cyclonedx-json', () => {
8588
assert.strictEqual(output.bomFormat, 'CycloneDX');
8689
assert.strictEqual(output.specVersion, '1.6');
8790
assert.strictEqual(output.components[0].evidence.occurrences[0].location, relative);
91+
assert.deepStrictEqual(output.metadata.properties, [
92+
{ name: 'retirejs:vulnerability-repository', value: 'testrepo.json' },
93+
]);
94+
});
95+
96+
it('should validate VEX report according to schema 1.6', () => {
97+
const data: unknown[] = [];
98+
const writer: Writer = {
99+
out: (a) => data.push(a),
100+
err: (a) => data.push(a),
101+
close: () => undefined,
102+
};
103+
const vexOptions: LoggerOptions = { ...loggerOptions, outputformat: 'cyclonedxJSON1_6_VEX' };
104+
const logger = reporting.open(vexOptions);
105+
jsonLogger1_6.configure(logger, writer, vexOptions, hash);
106+
const result1 = retire.scanFileContent('/*! jQuery v1.8.1 asdasd ', repo, hash);
107+
result1[0].licenses = ['MIT'];
108+
logger.logVulnerableDependency({ results: result1, file: jqFile });
109+
logger.close();
110+
const validator = new Validator();
111+
validator.addSchema(jsfSchema, 'jsf-0.82.schema.json#/definitions/signature');
112+
const output = JSON.parse(data.join(''));
113+
const res = validator.validate(output, jsonSchema1_6);
114+
assert.strictEqual(res.valid, true, res.errors.join('\n'));
115+
assert.ok(output.vulnerabilities.length > 0);
116+
const vulnerability = output.vulnerabilities[0];
117+
assert.deepStrictEqual(vulnerability.source, { name: 'Retire.js', url: 'testrepo.json' });
118+
assert.deepStrictEqual(vulnerability.ratings[0].source, { name: 'Retire.js', url: 'testrepo.json' });
119+
assert.strictEqual(vulnerability.affects[0].ref, 'pkg:npm/jquery@1.8.1');
120+
assert.strictEqual(vulnerability.affects[0].versions[0].status, 'affected');
121+
assert.ok(vulnerability.affects[0].versions[0].range.startsWith('vers:npm/'));
88122
});
89123

90124
/*it('should validate report according to xml schema', async () => {

node/src/cli.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ const scanpath = prg.path ?? prg.jspath ?? '.';
8383
const log = reporting.open({
8484
colors: !!prg.colors,
8585
colorwarn,
86-
jsRepo: jsrepolocation.join(', '),
86+
jsRepo: jsrepolocation,
8787
outputformat: prg.outputformat,
8888
outputpath: prg.outputpath,
8989
path: scanpath,

node/src/reporters/cyclonedx-1_6-json.ts

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ConfigurableLogger, Hasher, Logger, LoggerOptions, Writer } from '../re
33
import * as retire from '../retire';
44
import * as fs from 'fs';
55
import { Finding, Vulnerability } from '../types';
6-
import { generatePURL } from './utils';
6+
import { generatePURL, vulnerabilityRepositories } from './utils';
77
import * as path from 'path';
88
import * as crypto from 'crypto';
99

@@ -38,19 +38,21 @@ function configureCycloneDXJSONLogger(logger: Logger, writer: Writer, config: Lo
3838
occurrences: Array<{ location: string }>;
3939
};
4040
};
41+
type VulnerabilitySource = { name: string; url?: string };
4142
type Vulnerability = {
4243
id: string;
44+
source: VulnerabilitySource;
4345
cwes: number[];
4446
description?: string;
4547
advisories: Array<{ url: string; title?: string; }>;
4648
references: Array<{ id: string; source: { name: string; url: string } }>;
4749
ratings: Array<{
48-
source: string;
50+
source: VulnerabilitySource;
4951
severity: string;
5052
}>;
5153
affects: Array<{
5254
ref: string;
53-
range: string;
55+
versions: Array<{ range: string; status: 'affected' }>;
5456
}>;
5557
};
5658

@@ -60,6 +62,12 @@ function configureCycloneDXJSONLogger(logger: Logger, writer: Writer, config: Lo
6062
const seen = new Map<string, Component>();
6163
const vulnerabilitiesCyclone = new Map<string, Vulnerability>();
6264
const includeVEX = config.outputformat === 'cyclonedxJSON1_6_VEX';
65+
const repositories = vulnerabilityRepositories(config.jsRepo);
66+
// Only attributable to a single URL when exactly one repository was used
67+
const retireSource: VulnerabilitySource = {
68+
name: 'Retire.js',
69+
url: repositories.length === 1 ? repositories[0] : undefined,
70+
};
6371

6472
const components = finalResults.data
6573
.filter((d) => d.results)
@@ -101,23 +109,29 @@ function configureCycloneDXJSONLogger(logger: Logger, writer: Writer, config: Lo
101109
const { references, advisories } = mapUrls(vuln);
102110
vulnerabilitiesCyclone.set(id, {
103111
id,
112+
source: retireSource,
104113
cwes: vuln.cwe.map((c) => parseInt(c.split("-")[1])),
105114
description: vuln.identifiers.summary,
106115
advisories: advisories,
107116
references: references,
108117
ratings: [
109118
{
110-
source: 'Retire.js',
119+
source: retireSource,
111120
severity: vuln.severity,
112121
},
113122
],
114123
affects: [],
115124
});
116125
}
117-
vulnerabilitiesCyclone.get(id)!.affects.push({
126+
vulnerabilitiesCyclone.get(id)!.affects.push({
118127
ref: bomRef,
119-
// "vers:npm/1.2.3|>=2.0.0|<5.0.0"
120-
range: "vers:npm/" + (vuln.atOrAbove ? ">=" + vuln.atOrAbove + "|" : "") + "<" + vuln.below,
128+
versions: [
129+
{
130+
// "vers:npm/1.2.3|>=2.0.0|<5.0.0"
131+
range: 'vers:npm/' + (vuln.atOrAbove ? '>=' + vuln.atOrAbove + '|' : '') + '<' + vuln.below,
132+
status: 'affected',
133+
},
134+
],
121135
});
122136
});
123137
});
@@ -154,6 +168,12 @@ function configureCycloneDXJSONLogger(logger: Logger, writer: Writer, config: Lo
154168
version: retire.version,
155169
},
156170
],
171+
properties: repositories.length
172+
? repositories.map((repo) => ({
173+
name: 'retirejs:vulnerability-repository',
174+
value: repo,
175+
}))
176+
: undefined,
157177
},
158178
components: components,
159179
vulnerabilities: includeVEX ? Array.from(vulnerabilitiesCyclone.values()) : undefined,

node/src/reporters/cyclonedx-json.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { ConfigurableLogger, Hasher, Logger, LoggerOptions, Writer } from '../re
33
import * as retire from '../retire';
44
import * as fs from 'fs';
55
import { Finding } from '../types';
6-
import { generatePURL } from './utils';
6+
import { generatePURL, vulnerabilityRepositories } from './utils';
77
import * as path from 'path';
88
import * as crypto from 'crypto';
99

@@ -39,6 +39,7 @@ function configureCycloneDXJSONLogger(logger: Logger, writer: Writer, config: Lo
3939

4040
logger.close = function (callback) {
4141
const write = vulnsFound ? writer.err : writer.out;
42+
const repositories = vulnerabilityRepositories(config.jsRepo);
4243
const seen = new Map<string, Component>();
4344
const components = finalResults.data
4445
.filter((d) => d.results)
@@ -100,6 +101,12 @@ function configureCycloneDXJSONLogger(logger: Logger, writer: Writer, config: Lo
100101
version: retire.version,
101102
},
102103
],
104+
properties: repositories.length
105+
? repositories.map((repo) => ({
106+
name: 'retirejs:vulnerability-repository',
107+
value: repo,
108+
}))
109+
: undefined,
103110
},
104111
components: components,
105112
},

node/src/reporters/cyclonedx.ts

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import { ConfigurableLogger, Hasher, Logger, LoggerOptions, Writer } from '../re
55
import * as retire from '../retire';
66
import * as fs from 'fs';
77
import { Finding } from '../types';
8-
import { generatePURL } from './utils';
8+
import { generatePURL, vulnerabilityRepositories } from './utils';
99
import * as crypto from 'crypto';
1010

1111
function configureCycloneDXLogger(logger: Logger, writer: Writer, config: LoggerOptions, hash: Hasher) {
@@ -36,6 +36,12 @@ function configureCycloneDXLogger(logger: Logger, writer: Writer, config: Logger
3636

3737
logger.close = function (callback) {
3838
const write = vulnsFound ? writer.err : writer.out;
39+
const repositories = vulnerabilityRepositories(config.jsRepo)
40+
.map(
41+
(repo) => `
42+
<property name="retirejs:vulnerability-repository">${escapeXml(repo)}</property>`,
43+
)
44+
.join('');
3945
const seen = new Set<string>();
4046
const components = finalResults.data
4147
.filter((d) => d.results.length > 0)
@@ -81,7 +87,9 @@ function configureCycloneDXLogger(logger: Logger, writer: Writer, config: Logger
8187
<name>retire.js</name>
8288
<version>${retire.version}</version>
8389
</tool>
84-
</tools>
90+
</tools>${repositories ? `
91+
<properties>${repositories}
92+
</properties>` : ''}
8593
</metadata>
8694
<components>${components}
8795
</components>
@@ -90,6 +98,14 @@ function configureCycloneDXLogger(logger: Logger, writer: Writer, config: Logger
9098
};
9199
}
92100

101+
function escapeXml(value: string) {
102+
return value
103+
.replace(/&/g, '&amp;')
104+
.replace(/</g, '&lt;')
105+
.replace(/>/g, '&gt;')
106+
.replace(/"/g, '&quot;');
107+
}
108+
93109
function mapLicenses(licenses: string[] | undefined) {
94110
if (!licenses) return '';
95111
if (licenses.length == 0) return '';

node/src/reporters/json.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import { ConfigurableLogger } from '../reporting';
44

55
import * as retire from '../retire';
6+
import { vulnerabilityRepositories } from './utils';
67

78
export default {
89
configure: (logger, writer, config) => {
@@ -14,6 +15,7 @@ export default {
1415
messages: [] as unknown[],
1516
errors: [] as unknown[],
1617
time: undefined as undefined | number,
18+
vulnerabilityRepositories: vulnerabilityRepositories(config.jsRepo),
1719
};
1820
logger.info = finalResults.messages.push;
1921
logger.debug = config.verbose

0 commit comments

Comments
 (0)