diff --git a/README.md b/README.md
index 959f041..69432b8 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 0745270..f62a1fc 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,7 @@
"c8": "^8.0.1",
"cross-env": "^7.0.3",
"glob": "^10.3.4",
- "tsx": "^3.12.8",
+ "tsx": "^4.7.0",
"typescript": "^4.9.5"
},
"dependencies": {
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"),