Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 7 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@
The **vuln-*era*** has begun! Programmatically fetch security vulnerabilities with one or many strategies. Originally designed to run and analyze [Scanner](https://github.com/NodeSecure/scanner) dependencies it now also runs independently from an npm Manifest.

## Requirements
- [Node.js](https://nodejs.org/en/) v22 or higher
- [Node.js](https://nodejs.org/en/) v24 or higher

## Getting Started

Expand Down Expand Up @@ -51,13 +51,14 @@ console.log(vulnerabilities);

The default strategy is **NONE** which mean no strategy at all (we execute nothing).

[GitHub Advisory](./docs/github_advisory.md) | [Sonatype - OSS Index](./docs/sonatype.md) | Snyk
:-------------------------:|:-------------------------:|:-------------------------:
<img src="https://upload.wikimedia.org/wikipedia/commons/thumb/d/db/Npm-logo.svg/1200px-Npm-logo.svg.png" width="300"> | <img src="https://ossindex.sonatype.org/assets/images/sonatype-image.png" width="400"> | <img src="https://res.cloudinary.com/snyk/image/upload/v1537345894/press-kit/brand/logo-black.png" width="400">
- [GitHub Advisory](./docs/github_advisory.md)
- [Sonatype OSS Index](./docs/sonatype.md)
- [OSV](./docs/osv.md)
- Snyk

Those strategies are described as "string" **type** with the following TypeScript definition:
```ts
type Kind = "github-advisory" | "snyk" | "sonatype" | "none";
type Kind = "github-advisory" | "snyk" | "sonatype" | "osv" | "none";
```

To add a strategy or better understand how the code works, please consult [the following guide](./docs/adding_new_strategy.md).
Expand All @@ -72,6 +73,7 @@ const strategies: Object.freeze({
GITHUB_ADVISORY: "github-advisory",
SNYK: "snyk",
SONATYPE: "sonatype",
OSV: "osv",
NONE: "none"
});

Expand Down
80 changes: 66 additions & 14 deletions docs/database/osv.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# 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.
OSV stands 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/)
Learn more at [osv.dev](https://osv.dev/)

## Format

Expand All @@ -26,36 +26,88 @@ export interface OSVOptions {
}
```

### `findOne(parameters: OSVApiParameter): Promise<OSV[]>`
Find the vulnerabilities of a given package using available OSV API parameters.
No credentials are required to use the OSV public API. The optional `credential` can be used to attach API key headers if needed.

### `query(parameters: OSVQueryBatchEntry): Promise<OSV[]>`

Find the vulnerabilities of a given package using available OSV API parameters. Defaults the ecosystem to `npm` if not specified.

```ts
export type OSVApiParameter = {
export type OSVQueryBatchEntry = {
version?: string;
package: {
name: string;
/**
* @default npm
* @default "npm"
*/
ecosystem?: string;
};
}
};
```

### `findOneBySpec(spec: string): Promise<OSV[]>`
Find the vulnerabilities of a given package using the NPM spec format like `packageName@version`.
Example:

```ts
const vulns = await db.findOneBySpec("01template1");
import * as vulnera from "@nodesecure/vulnera";

const db = new vulnera.Database.OSV();

const vulns = await db.query({
version: "1.0.0",
package: { name: "lodash" }
});
console.log(vulns);
```

### `findMany<T extends string>(specs: T[]): Promise<Record<T, OSV[]>>`
Find the vulnerabilities of many packages using the spec format.
### `queryBySpec(spec: string): Promise<OSV[]>`

Return a Record where keys are equals to the provided specs.
Find the vulnerabilities of a given package using the npm spec format `packageName@version`.

```ts
const vulns = await db.findMany(["express@4.0.0", "lodash@4.17.0"]);
import * as vulnera from "@nodesecure/vulnera";

const db = new vulnera.Database.OSV();

const vulns = await db.queryBySpec("lodash@4.17.20");
console.log(vulns);
```

### `queryBatch(queries: OSVQueryBatchEntry[]): Promise<OSVQueryBatchResult[]>`

Query multiple packages at once using the `/v1/querybatch` OSV endpoint. Results are returned in the same order as the input queries.

```ts
export interface OSVQueryBatchResult {
vulns?: OSV[];
}
```

Example:

```ts
import * as vulnera from "@nodesecure/vulnera";

const db = new vulnera.Database.OSV();

const results = await db.queryBatch([
{ version: "4.17.20", package: { name: "lodash" } },
{ version: "1.0.0", package: { name: "minimist" } }
]);

for (const result of results) {
console.log(result.vulns ?? []);
}
```

### `findVulnById(id: string): Promise<OSV>`

Fetch a single vulnerability entry by its OSV identifier (e.g. `GHSA-xxxx-xxxx-xxxx` or `RUSTSEC-xxxx-xxxx`).

```ts
import * as vulnera from "@nodesecure/vulnera";

const db = new vulnera.Database.OSV();

const vuln = await db.findVulnById("GHSA-p6mc-m468-83gw");
console.log(vuln);
```
65 changes: 65 additions & 0 deletions docs/osv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
# OSV strategy

The OSV strategy queries the [OSV (Open Source Vulnerability)](https://osv.dev/) public API directly. It uses the `/v1/querybatch` endpoint for efficient batch lookups, resolving vulnerabilities for all dependencies at once.

No credentials or local database synchronization are required.

## How it works

1. Dependencies are extracted from the local project using `NodeDependencyExtractor` (via Arborist), which reads from `node_modules` or falls back to the lockfile.
2. All `name@version` pairs are batched into chunks of up to **1000** entries and sent to the OSV batch API.
3. Results are mapped back to each package and optionally converted to the [Standard](./formats/standard.md) or [OSV](./formats/osv.md) format.

## Usage

### `getVulnerabilities(path, options?)`

Scans a local project directory and returns all found vulnerabilities.

```js
import * as vulnera from "@nodesecure/vulnera";

const definition = vulnera.setStrategy(vulnera.strategies.OSV);

const vulnerabilities = await definition.getVulnerabilities(process.cwd());
console.log(vulnerabilities);
```

With the Standard NodeSecure format:

```js
import * as vulnera from "@nodesecure/vulnera";

const definition = vulnera.setStrategy(vulnera.strategies.OSV);

const vulnerabilities = await definition.getVulnerabilities(process.cwd(), {
useFormat: "Standard"
});
console.log(vulnerabilities);
```

### `hydratePayloadDependencies(dependencies, options?)`

Hydrates a Scanner dependencies `Map` in-place with vulnerability data.

```js
import * as vulnera from "@nodesecure/vulnera";

const dependencies = new Map();
// ...populate dependencies from Scanner...

const definition = vulnera.setStrategy(vulnera.strategies.OSV);
await definition.hydratePayloadDependencies(dependencies);
```

With the Standard NodeSecure format:

```js
await definition.hydratePayloadDependencies(dependencies, {
useFormat: "Standard"
});
```

## OSV Database

The strategy uses the [`OSV` database class](./database/osv.md) internally. You can also use it directly for lower-level access to the OSV API (single queries, batch queries, lookup by ID).
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
"@openally/config.typescript": "1.2.1",
"@slimio/is": "^2.0.0",
"@types/node": "^25.0.2",
"@types/npmcli__arborist": "6.3.3",
"c8": "^11.0.0",
"typescript": "^5.4.2"
},
Expand Down
1 change: 1 addition & 0 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export const VULN_MODE = Object.freeze({
GITHUB_ADVISORY: "github-advisory",
SNYK: "snyk",
SONATYPE: "sonatype",
OSV: "osv",
NONE: "none"
});
export type Kind = typeof VULN_MODE[keyof typeof VULN_MODE];
5 changes: 4 additions & 1 deletion src/database/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,10 @@ export type {
export { OSV } from "./osv.ts";
export type {
OSVOptions,
OSVApiParameter
OSVQueryBatchEntry,
OSVQueryBatchRequest,
OSVQueryBatchResult,
OSVQueryBatchResponse
} from "./osv.ts";

export { Snyk } from "./snyk.ts";
Expand Down
66 changes: 51 additions & 15 deletions src/database/osv.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type { OSV as OSVFormat } from "../formats/osv/index.ts";
import * as utils from "../utils.ts";
import type { ApiCredential } from "../credential.ts";

export type OSVApiParameter = {
export type OSVQueryBatchEntry = {
version?: string;
package: {
name: string;
Expand All @@ -17,6 +17,18 @@ export type OSVApiParameter = {
};
};

export interface OSVQueryBatchRequest {
queries: OSVQueryBatchEntry[];
}

export interface OSVQueryBatchResult {
vulns?: OSVFormat[];
}

export interface OSVQueryBatchResponse {
results: OSVQueryBatchResult[];
}

export interface OSVOptions {
credential?: ApiCredential;
}
Expand All @@ -32,44 +44,68 @@ export class OSV {
this.#credential = options.credential;
}

async findOne(
parameters: OSVApiParameter
async query(
query: OSVQueryBatchEntry
): Promise<OSVFormat[]> {
if (!parameters.package.ecosystem) {
parameters.package.ecosystem = "npm";
if (!query.package.ecosystem) {
query.package.ecosystem = "npm";
}

const { data } = await httpie.post<{ vulns: OSVFormat[]; }>(
new URL("v1/query", OSV.ROOT_API),
{
headers: this.#credential?.headers,
body: parameters
body: query
}
);

return data.vulns;
}

findOneBySpec(
queryBySpec(
spec: string
): Promise<OSVFormat[]> {
const { name, version } = utils.parseNpmSpec(spec);

return this.findOne({
return this.query({
version,
package: {
name
name,
ecosystem: "npm"
}
});
}

async findMany<T extends string = string>(
specs: T[]
): Promise<Record<T, OSVFormat[]>> {
const entries = await Promise.all(
specs.map(async(spec) => [spec, await this.findOneBySpec(spec)] as [T, OSVFormat[]])
async queryBatch(
queries: OSVQueryBatchEntry[]
): Promise<OSVQueryBatchResult[]> {
for (const query of queries) {
if (!query.package.ecosystem) {
query.package.ecosystem = "npm";
}
}

const { data } = await httpie.post<OSVQueryBatchResponse>(
new URL("v1/querybatch", OSV.ROOT_API),
{
headers: this.#credential?.headers,
body: { queries }
}
);

return data.results;
}

async findVulnById(
id: string
): Promise<OSVFormat> {
const { data } = await httpie.get<OSVFormat>(
new URL(`v1/vulns/${id}`, OSV.ROOT_API),
{
headers: this.#credential?.headers
}
);

return Object.fromEntries(entries) as Record<T, OSVFormat[]>;
return data;
}
}
Loading
Loading