From de7c092891c5776aa9826a66db4d04088c3b7a9b Mon Sep 17 00:00:00 2001 From: fraxken Date: Tue, 28 Nov 2023 06:04:28 +0100 Subject: [PATCH] chore: implement OSV api & format --- README.md | 3 ++ docs/database/osv.md | 72 ++++++++++++++++++++++++++++++ package.json | 1 + src/database/index.ts | 1 + src/database/osv.ts | 65 +++++++++++++++++++++++++++ src/formats/osv/.gitkeep | 0 src/formats/osv/index.ts | 80 ++++++++++++++++++++++++++++++++++ src/index.ts | 9 +++- src/utils.ts | 10 +++++ test/database/osv.unit.spec.ts | 79 +++++++++++++++++++++++++++++++++ test/utils.unit.spec.ts | 21 +++++++++ 11 files changed, 340 insertions(+), 1 deletion(-) create mode 100644 docs/database/osv.md create mode 100644 src/database/index.ts create mode 100644 src/database/osv.ts delete mode 100644 src/formats/osv/.gitkeep create mode 100644 src/formats/osv/index.ts create mode 100644 test/database/osv.unit.spec.ts diff --git a/README.md b/README.md index d75e7e1..4480ee4 100644 --- a/README.md +++ b/README.md @@ -165,6 +165,9 @@ export interface StandardVulnerability { } ``` +### Databases +- [OSV](./docs/database/osv.md) + ## Contributors ✨ diff --git a/docs/database/osv.md b/docs/database/osv.md new file mode 100644 index 0000000..fe169b0 --- /dev/null +++ b/docs/database/osv.md @@ -0,0 +1,72 @@ +# OSV + +OSV stand for Open Source Vulnerability database. This project is an open, precise, and distributed approach to producing and consuming vulnerability information for open source. + +All advisories in this database use the [OpenSSF OSV format](https://ossf.github.io/osv-schema/), which was developed in collaboration with open source communities. + +Lean more at [osv.dev](https://osv.dev/) + +## Format + +The OSV interface is exported as root like `StandardVulnerability`. + +```ts +export interface OSV { + schema_version: string; + id: string; + modified: string; + published: string; + withdraw: string; + aliases: string[]; + related: string[]; + summary: string; + details: string; + severity: OSVSeverity[]; + affected: OSVAffected[]; + references: { + type: OSVReferenceType; + url: string; + }[]; + credits: { + name: string; + contact: string[]; + type: OSVCreditType; + }[]; + database_specific: Record; +} +``` + +## API + +### findOne(parameters: OSVApiParameter): Promise< OSV[] > +Find the vulnerabilities of a given package using available OSV API parameters. + +```ts +export type OSVApiParameter = { + version?: string; + package: { + name: string; + /** + * @default npm + */ + ecosystem?: string; + }; +} +``` + +### findOneBySpec(spec: string): Promise< OSV[] > +Find the vulnerabilities of a given package using the NPM spec format like `packageName@version`. + +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const vulns = await vulnera.Database.osv.findOneBySpec( + "01template1" +); +console.log(vulns); +``` + +### findMany< T extends string >(specs: T[]): Promise< Record< T, OSV[] > > +Find the vulnerabilities of many packages using the spec format. + +Return a Record where keys are equals to the provided specs. diff --git a/package.json b/package.json index d90dabe..164e11e 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ "test-only": "glob -c \"tsx --test\" \"./test/**/*.spec.ts\"", "unit-test-only": "glob -c \"tsx --test\" \"./test/**/*.unit.spec.ts\"", "integration-test-only": "glob -c \"tsx --test\" \"./test/**/*.integration.spec.ts\"", + "osv": "glob -c \"tsx --test\" \"./test/**/osv.unit.spec.ts\"", "test": "npm run lint && npm run test-only", "test:unit": "npm run lint && npm run unit-test-only", "test:integration": "npm run lint && npm run integration-test-only", diff --git a/src/database/index.ts b/src/database/index.ts new file mode 100644 index 0000000..831bf2d --- /dev/null +++ b/src/database/index.ts @@ -0,0 +1 @@ +export * as osv from "./osv.js"; diff --git a/src/database/osv.ts b/src/database/osv.ts new file mode 100644 index 0000000..0e4a790 --- /dev/null +++ b/src/database/osv.ts @@ -0,0 +1,65 @@ +// Import Third-Party Dependencies +import * as httpie from "@myunisoft/httpie"; + +// Import Internal Dependencies +import { OSV } from "../formats/osv"; +import * as utils from "../utils.js"; + +// CONSTANTS +export const ROOT_API = "https://api.osv.dev"; + +export type OSVApiParameter = { + version?: string; + package: { + name: string; + /** + * @default npm + */ + ecosystem?: string; + }; +} + +export async function findOne( + parameters: OSVApiParameter +): Promise { + if (!parameters.package.ecosystem) { + parameters.package.ecosystem = "npm"; + } + + const { data } = await httpie.post<{ vulns: OSV[] }>( + new URL("v1/query", ROOT_API), + { + body: parameters + } + ); + + return data.vulns; +} + +export function findOneBySpec( + spec: string +) { + const { name, version } = utils.parseNpmSpec(spec); + + return findOne({ + version, + package: { + name + } + }); +} + +export async function findMany( + specs: T[] +): Promise> { + const packagesVulns = await Promise.all( + specs.map(async(spec) => { + return { + [spec]: await findOneBySpec(spec) + }; + }) + ); + + // @ts-ignore + return Object.assign(...packagesVulns); +} diff --git a/src/formats/osv/.gitkeep b/src/formats/osv/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/src/formats/osv/index.ts b/src/formats/osv/index.ts new file mode 100644 index 0000000..78bd28b --- /dev/null +++ b/src/formats/osv/index.ts @@ -0,0 +1,80 @@ + +/** + * @see https://ossf.github.io/osv-schema/ + */ +export interface OSV { + schema_version: string; + id: string; + modified: string; + published: string; + withdraw: string; + aliases: string[]; + related: string[]; + summary: string; + details: string; + severity: OSVSeverity[]; + affected: OSVAffected[]; + references: { + type: OSVReferenceType; + url: string; + }[]; + credits: { + name: string; + contact: string[]; + type: OSVCreditType; + }[]; + database_specific: Record; +} + +export type OSVReferenceType = "ADVISORY" | + "ARTICLE" | + "DETECTION" | + "DISCUSSION" | + "REPORT" | + "FIX" | + "GIT" | + "INTRODUCED" | + "PACKAGE" | + "EVIDENCE" | + "WEB"; + +export type OSVCreditType = "FINDER" | + "REPORTER" | + "ANALYST" | + "COORDINATOR" | + "REMEDIATION_DEVELOPER" | + "REMEDIATION_REVIEWER" | + "REMEDIATION_VERIFIER" | + "TOOL" | + "SPONSOR" | + "OTHER"; + +export interface OSVAffected { + package: { + ecosystem: "npm", + name: string; + purl: string; + }; + severity: OSVSeverity[]; + ranges: OSVRange[]; + versions: string[]; + ecosystem_specific: Record; + database_specific: Record; +} + +export interface OSVRange { + type: string; + repo: string; + events: { + introduced?: string; + fixed?: string; + last_affected?: string; + limit?: string; + }[]; + database_specific: Record; +} + +export interface OSVSeverity { + type: string; + score: string; +} diff --git a/src/index.ts b/src/index.ts index 7eaa54b..5d75c36 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,9 @@ import { import type { StandardVulnerability, Severity, StandardPatch } from "./formats/standard/index.js"; +import type { + OSV +} from "./formats/osv/index.js"; import type { Dependencies, ScannerVersionDescriptor @@ -43,6 +46,8 @@ import type { HydratePayloadDepsOptions } from "./strategies/types/api.js"; +export * as Database from "./database/index.js"; + export type AllStrategy = { "none": NoneStrategyDefinition; "github-advisory": GithubAdvisoryStrategyDefinition; @@ -110,5 +115,7 @@ export { NpmAuditAdvisory, PnpmAuditAdvisory, SnykVulnerability, - SonatypeVulnerability + SonatypeVulnerability, + + OSV }; diff --git a/src/utils.ts b/src/utils.ts index 344505a..3a14bda 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -21,6 +21,16 @@ export function standardizeNpmSeverity( return severity as Severity; } +export function parseNpmSpec( + spec: string +) { + const parts = spec.split("@"); + + return spec.startsWith("@") ? + { name: `@${parts[1]}`, version: parts[2] ?? void 0 } : + { name: parts[0], version: parts[1] ?? void 0 }; +} + export function* chunkArray( arr: T[], chunkSize: number ): IterableIterator { diff --git a/test/database/osv.unit.spec.ts b/test/database/osv.unit.spec.ts new file mode 100644 index 0000000..559fbd7 --- /dev/null +++ b/test/database/osv.unit.spec.ts @@ -0,0 +1,79 @@ +// Import Node.js Dependencies +import { describe, test, after } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { + kHttpClientHeaders, + setupHttpAgentMock +} from "../strategies/utils"; +import { osv } from "../../src/database/index"; + +describe("osv", () => { + const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); + const mockedHttpClient = mockedHttpAgent.get(osv.ROOT_API); + + after(() => { + restoreHttpAgent(); + }); + + test(`should send a POST http request to the OSV API using findOne + and then return the 'vulns' property from the JSON response`, async() => { + const expectedResponse = { vulns: "hello world" }; + mockedHttpClient + .intercept({ + path: new URL("/v1/query", osv.ROOT_API).href, + method: "POST", + body: JSON.stringify({ package: { name: "foobar", ecosystem: "npm" } }) + }) + .reply(200, expectedResponse, kHttpClientHeaders); + + const vulns = await osv.findOne({ + package: { + name: "foobar", + ecosystem: "npm" + } + }); + assert.strictEqual(vulns, expectedResponse.vulns); + }); + + test(`should send a POST http request to the OSV API using findOneBySpec + and then return the 'vulns' property from the JSON response`, async() => { + const expectedResponse = { vulns: "hello world" }; + const packageName = "@nodesecure/js-x-ray"; + + mockedHttpClient + .intercept({ + path: new URL("/v1/query", osv.ROOT_API).href, + method: "POST", + body: JSON.stringify({ + version: "2.0.0", + package: { name: packageName, ecosystem: "npm" } + }) + }) + .reply(200, expectedResponse, kHttpClientHeaders); + + const vulns = await osv.findOneBySpec(`${packageName}@2.0.0`); + assert.strictEqual(vulns, expectedResponse.vulns); + }); + + test(`should send multiple POST http requests to the OSV API using findMany`, async() => { + const expectedResponse = { vulns: [1, 2, 3] }; + + mockedHttpClient + .intercept({ + path: new URL("/v1/query", osv.ROOT_API).href, + method: "POST" + }) + .reply(200, expectedResponse, kHttpClientHeaders) + .times(2); + + const result = await osv.findMany( + ["foobar", "yoobar"] + ); + assert.deepEqual(result, { + foobar: expectedResponse.vulns, + yoobar: expectedResponse.vulns + }); + }); +}); diff --git a/test/utils.unit.spec.ts b/test/utils.unit.spec.ts index 6768e37..c3a0ee4 100644 --- a/test/utils.unit.spec.ts +++ b/test/utils.unit.spec.ts @@ -6,9 +6,30 @@ import assert from "node:assert"; import { standardizeNpmSeverity, fromMaybeStringToArray, + parseNpmSpec, chunkArray } from "../src/utils.js"; +test("parseNpmSpec", () => { + assert.deepEqual( + parseNpmSpec("foobar"), + { name: "foobar", version: undefined } + ); + assert.deepEqual( + parseNpmSpec("foobar@1.0.0"), + { name: "foobar", version: "1.0.0" } + ); + + assert.deepEqual( + parseNpmSpec("@nodesecure/js-x-ray"), + { name: "@nodesecure/js-x-ray", version: undefined } + ); + assert.deepEqual( + parseNpmSpec("@nodesecure/js-x-ray@1.0.0"), + { name: "@nodesecure/js-x-ray", version: "1.0.0" } + ); +}); + test("standardizeNpmSeverity", () => { assert.strictEqual( standardizeNpmSeverity("moderate"),