Skip to content

Commit e5a8835

Browse files
committed
feat: add OSV document, validation
Signed-off-by: Rifa Achrinza <[email protected]>
1 parent e88c6e7 commit e5a8835

File tree

11 files changed

+329
-4
lines changed

11 files changed

+329
-4
lines changed

.gitmodules

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,6 @@
44
[submodule "vendors/secvisogram"]
55
path = vendors/secvisogram
66
url = [email protected]:BSI-Bund/secvisogram.git
7+
[submodule "vendors/osv-schema"]
8+
path = vendors/osv-schema
9+
url = [email protected]:ossf/osv-schema.git

.husky/pre-commit

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,3 +6,4 @@
66

77
npm run-script prettier:check
88
npm run-script validate-csaf20
9+
npm run-script validate-osv

.vscode/settings.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,12 @@
88
"advisories/lbsa-*.csaf.json"
99
],
1010
"url": "https://docs.oasis-open.org/csaf/csaf/v2.0/csaf_json_schema.json"
11+
},
12+
{
13+
"fileMatch": [
14+
"advisories/lbsa-*.osv.json"
15+
],
16+
"url": "./vendors/osv-schema/validation/schema.json"
1117
}
1218
]
1319
}

advisories/README.md

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,19 @@
1111
1212
This section of the Git repository is where all LBSAs are stored. They are
1313
written as [CSAF 2.0](https://docs.oasis-open.org/csaf/csaf/v2.0/csaf-v2.0.html)
14-
documents.
14+
documents and
15+
[OSV 1.2.0](https://github.com/ossf/osv-schema/tree/5e3cbf8abb4e192f87d00354b23e56340388cf98).
1516

1617
The naming convention is as follows:
1718

1819
```
19-
lbsa-YYYYMMDD.csaf.json
20+
lbsa-YYYYMMDD.csaf.json <-- CSAF 2.0
21+
lbsa-YYYYMMDD.osv.json <-- OSV 1.2.0
2022
```
2123

2224
Where:
2325

24-
- `YYYY` is the year
26+
- `YYYY` is the year of
2527
- `MM` is the month
2628
- `DD` is the day
2729

@@ -33,13 +35,26 @@ during a Git commit, and as part of the
3335
[CI pipeline](../.github/workflows/ci.yaml). It can also be triggered by running
3436
`npm run validate-csaf20`.
3537

38+
Validation of OSV 1.2.0 documents are done by
39+
<../scripts/advisories/validate-osv.ts>. This is triggered automatically during
40+
a Git commit, and as aprt of the [CI pipeline](../.github/workflows/ci.yaml). It
41+
can also be triggered by running `npm run validate-osv`.
42+
43+
CSAF 2.0 acts as the "source of truth" of which the other formats are validated
44+
against as it is the most comprehensive format. Hence, any deviations from the
45+
CSAF 2.0 document must also be reflected back in the CSAF 2.0 document itself.
46+
3647
## Vendors
3748

3849
This section depends on [Secvisogram](../vendors/README.md#submodules) for
3950
validation, its ports of JSON Schemas from Draft-04 (No first-class AJV support)
4051
to Draft-2019, and for a strict variant of CSAF 2.0 JSON Schema. There are plans
4152
to utilise the other parts of the codebase for more thorough validation.
4253

54+
It also depends on
55+
[Open Source Vulnerability schema](../vendors/README.md#submodules) for JSON
56+
Schema-based OSV validation.
57+
4358
## Dependents
4459

4560
There's current no known dependents on these CSAF 2.0 documents. However, there

advisories/lbsa-20201130.osv.json

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
{
2+
"schema_version": "1.2.0",
3+
"id": "LBSA-20201130",
4+
"modified": "2022-03-05T00:00:00.000Z",
5+
"severity": [
6+
{
7+
"score": "CVSS:3.0/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:L/RL:O/E:U/RC:C",
8+
"type": "CVSS_V3"
9+
}
10+
],
11+
"credits": [
12+
{
13+
"name": "Olivier Beg"
14+
},
15+
{
16+
"name": "Samuel Erb"
17+
}
18+
],
19+
"references": [
20+
{
21+
"type": "ADVISORY",
22+
"url": "https://loopback.io/doc/en/sec/lbsa-2020-11-30.csaf.json"
23+
},
24+
{
25+
"type": "ADVISORY",
26+
"url": "https://loopback.io/doc/en/sec/Security-advisory-11-30-2020.html"
27+
},
28+
{
29+
"type": "ADVISORY",
30+
"url": "https://nvd.nist.gov/vuln/detail/CVE-2020-4988"
31+
},
32+
{
33+
"type": "ADVISORY",
34+
"url": "https://www.cve.org/CVERecord?id=CVE-2020-4988"
35+
},
36+
{
37+
"type": "PACKAGE",
38+
"url": "https://www.npmjs.com/package/@loopback/rest"
39+
},
40+
{
41+
"type": "REPORT",
42+
"url": "https://exchange.xforce.ibmcloud.com/vulnerabilities/192706"
43+
}
44+
],
45+
"aliases": ["CVE-2020-4988"],
46+
"affected": [
47+
{
48+
"package": {
49+
"ecosystem": "npm",
50+
"name": "@loopback/rest"
51+
},
52+
"versions": ["8.0.0"]
53+
}
54+
],
55+
"database_specific": {
56+
"CWE": "CWE-1321"
57+
},
58+
"summary": "`@loopback/rest` allows REST APIs to have `constructor` in the JSON payload, which is vulnerable to prototype pollution.",
59+
"details": "It's a similar issue as https://snyk.io/vuln/SNYK-JS-LODASH-73638, where the following description is quoted from.\n\n> Prototype Pollution is a vulnerability affecting JavaScript. Prototype Pollution refers to the ability to inject properties into existing JavaScript language construct prototypes, such as objects. JavaScript allows all Object attributes to be altered, including their magical attributes such as `_proto_`, `constructor` and `prototype`. An attacker manipulates these attributes to overwrite, or pollute, a JavaScript application object prototype of the base object by injecting other values. Properties on the Object.prototype are then inherited by all the JavaScript objects through the prototype chain. When that happens, this leads to either denial of service by triggering JavaScript exceptions, or it tampers with the application source code to force the code path that the attacker injects, thereby leading to remote code execution.\n>\n> There are two main ways in which the pollution of prototypes occurs:\n>\n> - Unsafe Object recursive merge\n> - Property definition by path"
60+
}
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
SPDX-FileCopyrightText: LoopBack Contributors
2+
SPDX-License-Identifier: MIT

package-lock.json

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

package.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,9 @@
1717
"prettier:cli": "lb-prettier '**/*.ts' '**/*.js' 'advisories/lbsa*.csaf.json' '**/*.md'",
1818
"prettier:check": "npm run prettier:cli -- -l",
1919
"prettier:fix": "npm run prettier:cli -- --write",
20-
"validate-csaf20": "ts-node --project=scripts/tsconfig.json scripts/advisories/validate-csaf20.ts"
20+
"ts-node": "ts-node --project=scripts/tsconfig.json",
21+
"validate-csaf20": "npm run ts-node -- scripts/advisories/validate-csaf20.ts",
22+
"validate-osv": "npm run ts-node -- scripts/advisories/validate-osv.ts"
2123
},
2224
"repository": {
2325
"type": "git",
@@ -41,6 +43,8 @@
4143
"@loopback/build": "^8.1.0",
4244
"@loopback/eslint-config": "^12.0.2",
4345
"@types/glob": "^7.2.0",
46+
"ajv": "^8.10.0",
47+
"ajv-formats": "^2.1.1",
4448
"eslint": "^8.9.0",
4549
"eslint-plugin-prettier": "^4.0.0",
4650
"glob": "^7.2.0",

scripts/advisories/validate-osv.ts

Lines changed: 230 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,230 @@
1+
// SPDX-FileCopyrightText: LoopBack Contributors
2+
// SPDX-License-Identifier: MIT
3+
4+
import path from 'path';
5+
import glob from 'glob';
6+
import Ajv2020 from 'ajv/dist/2020';
7+
import addFormats from 'ajv-formats';
8+
import osvSchema from '../../vendors/osv-schema/validation/schema.json';
9+
10+
const osvDocumentGlob = '../../advisories/*.osv.json';
11+
12+
console.log(`Validating OSV 1.2.0 documents... (Glob: ${osvDocumentGlob})`);
13+
14+
interface ValidationResult {
15+
isValid: boolean;
16+
errors: {
17+
instancePath: string;
18+
message?: string;
19+
}[];
20+
}
21+
22+
glob(path.resolve(__dirname, osvDocumentGlob), async (err, matches) => {
23+
if (err) throw Error;
24+
25+
let errorCount = 0;
26+
27+
for (const filePath of matches) {
28+
process.stdout.write(
29+
` L Validating: ${path.relative(process.cwd(), filePath)}...`,
30+
);
31+
const fileContents = require(filePath);
32+
const validationResults: Record<string, ValidationResult> = {
33+
jsonSchema: validateJsonSchema(fileContents),
34+
schemaVersion: validateSchemaVersion(fileContents),
35+
csaf20Sync: validateCSAF20Sync(filePath, fileContents),
36+
};
37+
38+
const errors = Object.values(validationResults).flatMap(x => x.errors);
39+
const isValid = errors.length < 1;
40+
41+
if (isValid) console.log('Done!');
42+
else {
43+
errorCount += errors.length;
44+
console.log(`${errors.length} error(s) found:`);
45+
for (let i = 0; i < errors.length; i++) {
46+
console.log(` L Error #${i + 1}`);
47+
console.log(` L Instance path : ${errors[i].instancePath}`);
48+
console.log(` L Message : ${errors[i].message ?? 'N/A'}`);
49+
}
50+
}
51+
}
52+
53+
if (matches.length === 0) console.log('No OSV 1.2.0 documents found!');
54+
55+
if (errorCount > 0) {
56+
console.log(`${errorCount} error(s) found.`);
57+
process.exit(1);
58+
}
59+
60+
console.log('OSV 1.2.0 validation done.');
61+
});
62+
63+
function validateJsonSchema(fileContents: any): ValidationResult {
64+
const validate = addFormats(
65+
new Ajv2020({strict: false, allErrors: true}),
66+
).compile(osvSchema);
67+
const isValid = validate(fileContents);
68+
69+
return {
70+
isValid,
71+
errors: validate.errors ?? [],
72+
};
73+
}
74+
75+
function validateSchemaVersion(fileContents: any): ValidationResult {
76+
const errors: ValidationResult['errors'] = [];
77+
78+
if (fileContents.schema_version !== '1.2.0') {
79+
errors.push({
80+
instancePath: '/schema_version',
81+
message: 'schema_version must be `1.2.0`.',
82+
});
83+
}
84+
85+
return {
86+
isValid: errors.length < 1,
87+
errors,
88+
};
89+
}
90+
91+
function validateCSAF20Sync(
92+
filePath: string,
93+
osvDocument: any,
94+
): ValidationResult {
95+
const csaf20Document = require(filePath.replace('.osv.json', '.csaf.json'));
96+
const errors: ValidationResult['errors'] = [];
97+
98+
// ID sync
99+
const csaf20ID = csaf20Document.document.tracking.id;
100+
const osvID = osvDocument.id;
101+
102+
if (osvID !== csaf20ID) {
103+
errors.push({
104+
instancePath: '/id',
105+
message: 'id must match CSAF 2.0 `/document/tracking/id`.',
106+
});
107+
}
108+
109+
// Summary sync
110+
const csaf20Summary = csaf20Document.document.notes.find(
111+
x => x.category === 'summary',
112+
).text;
113+
const osvSummary = osvDocument.summary;
114+
115+
if (csaf20Summary !== osvSummary) {
116+
errors.push({
117+
instancePath: '/summary',
118+
message: 'summary must match CSAF 2.0 `/document/notes` instance.',
119+
});
120+
}
121+
122+
// Description / Details sync
123+
const csaf2SDescription = csaf20Document.document.notes.find(
124+
x => x.category === 'description',
125+
).text;
126+
const osvDetails = osvDocument.details;
127+
128+
if (csaf2SDescription !== osvDetails) {
129+
errors.push({
130+
instancePath: '/details',
131+
message: 'details must match CSAF 2.0 `/document/notes` instance.',
132+
});
133+
}
134+
135+
// CVE sync
136+
const csaf20CVE = csaf20Document.vulnerabilities[0].cve;
137+
const osvCVE = osvDocument.aliases.find(x => x.startsWith('CVE-'));
138+
139+
if (csaf20CVE !== osvCVE) {
140+
errors.push({
141+
instancePath: '/aliases',
142+
message: 'alises must match CSAF `/vulnerabilities/0/cve`.',
143+
});
144+
}
145+
146+
// CVSS V3 sync
147+
const csaf20CVSS3 =
148+
csaf20Document.vulnerabilities[0].scores[0].cvss_v3?.vectorString;
149+
const osvCVSS3Index = osvDocument.severity.findIndex(
150+
x => x.type === 'CVSS_V3',
151+
);
152+
const osvCVSS3 =
153+
osvCVSS3Index > -1 ? osvDocument.severity[osvCVSS3Index].score : undefined;
154+
155+
if (csaf20CVSS3 !== osvCVSS3) {
156+
errors.push({
157+
instancePath: `/severity/score/${osvCVSS3Index}`,
158+
message:
159+
'score must match CSAF 2.0 `/vulnerabilities/0/scores/0/cvss_v3/attackVector`.',
160+
});
161+
}
162+
163+
// CWE sync
164+
const csaf20CWE = csaf20Document.vulnerabilities[0].cwe.id;
165+
const osvCWE = osvDocument.database_specific.CWE;
166+
167+
if (csaf20CWE !== osvCWE) {
168+
errors.push({
169+
instancePath: '/database_specific/cwe',
170+
message: 'cwe must match CSAF 2.0 `/vulnerabilities/0/cwe/id`.',
171+
});
172+
}
173+
174+
// References sync
175+
const csaf20References = csaf20Document.document.references.map(x => x.url);
176+
const osvReferences = osvDocument.references.map(x => x.url);
177+
178+
if (osvReferences.length >= csaf20References.length) {
179+
for (let i = 0; i < osvReferences.length; i++) {
180+
if (!csaf20References.includes(osvReferences[i])) {
181+
errors.push({
182+
instancePath: `/references/${i}`,
183+
message: `entry \`${osvReferences[i]}\` not found in CSAF 2.0 \`/document/references\`.`,
184+
});
185+
}
186+
}
187+
} else {
188+
for (let i = 0; i < csaf20References.length; i++) {
189+
if (!osvReferences.includes(csaf20References[i])) {
190+
errors.push({
191+
instancePath: '/references',
192+
message: `references missing \`${csaf20References[i]}\` from CSAF 2.0 \`/document/references\`.`,
193+
});
194+
}
195+
}
196+
}
197+
198+
// Acknowledgments / credits sync
199+
const csaf20Acknowledgments = csaf20Document.document.acknowledgments.flatMap(
200+
x => x.names,
201+
) as string[];
202+
const osvCredits = osvDocument.credits.flatMap(x => x.name) as string[];
203+
204+
if (osvCredits.length >= csaf20Acknowledgments.length) {
205+
for (let i = 0; i < osvCredits.length; i++) {
206+
const osvCredit = osvCredits[i];
207+
if (!csaf20Acknowledgments.includes(osvCredit)) {
208+
errors.push({
209+
instancePath: `/credits/${i}`,
210+
message: `entry \`${osvCredit}\` not found in CSAF 2.0 \`/document/acknowledgments\`.`,
211+
});
212+
}
213+
}
214+
} else {
215+
for (let i = 0; i < csaf20Acknowledgments.length; i++) {
216+
const csaf20Acknowledgement = csaf20Acknowledgments[i];
217+
if (!osvCredits.includes(csaf20Acknowledgement)) {
218+
errors.push({
219+
instancePath: `/credits`,
220+
message: `missing entry \`${csaf20Acknowledgement}\` from CSAF 2.0 \`/document/acknowledgments\`.`,
221+
});
222+
}
223+
}
224+
}
225+
226+
return {
227+
isValid: errors.length < 1,
228+
errors,
229+
};
230+
}

0 commit comments

Comments
 (0)