Skip to content

Commit

Permalink
chore: implement OSV api & format
Browse files Browse the repository at this point in the history
  • Loading branch information
fraxken committed Jan 23, 2024
1 parent 6ec82b9 commit de7c092
Show file tree
Hide file tree
Showing 11 changed files with 340 additions and 1 deletion.
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,9 @@ export interface StandardVulnerability {
}
```

### Databases
- [OSV](./docs/database/osv.md)

## Contributors ✨

<!-- ALL-CONTRIBUTORS-BADGE:START - Do not remove or modify this section -->
Expand Down
72 changes: 72 additions & 0 deletions docs/database/osv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# OSV

OSV stand for <kbd>Open Source Vulnerability</kbd> 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<string, any>;
}
```

## 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.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions src/database/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * as osv from "./osv.js";
65 changes: 65 additions & 0 deletions src/database/osv.ts
Original file line number Diff line number Diff line change
@@ -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<OSV[]> {
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<T extends string = string>(
specs: T[]
): Promise<Record<T, OSV[]>> {
const packagesVulns = await Promise.all(
specs.map(async(spec) => {
return {
[spec]: await findOneBySpec(spec)
};
})
);

// @ts-ignore
return Object.assign(...packagesVulns);
}
Empty file removed src/formats/osv/.gitkeep
Empty file.
80 changes: 80 additions & 0 deletions src/formats/osv/index.ts
Original file line number Diff line number Diff line change
@@ -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<string, any>;
}

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<string, any>;
database_specific: Record<string, any>;
}

export interface OSVRange {
type: string;
repo: string;
events: {
introduced?: string;
fixed?: string;
last_affected?: string;
limit?: string;
}[];
database_specific: Record<string, any>;
}

export interface OSVSeverity {
type: string;
score: string;
}
9 changes: 8 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
Expand Down Expand Up @@ -110,5 +115,7 @@ export {
NpmAuditAdvisory,
PnpmAuditAdvisory,
SnykVulnerability,
SonatypeVulnerability
SonatypeVulnerability,

OSV
};
10 changes: 10 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<T = any>(
arr: T[], chunkSize: number
): IterableIterator<T[]> {
Expand Down
79 changes: 79 additions & 0 deletions test/database/osv.unit.spec.ts
Original file line number Diff line number Diff line change
@@ -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
});
});
});
21 changes: 21 additions & 0 deletions test/utils.unit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
Expand Down

0 comments on commit de7c092

Please sign in to comment.