From cba2ba0893ec18f4f814586c02e4e61ede7f17f5 Mon Sep 17 00:00:00 2001 From: GENTILHOMME Thomas Date: Tue, 10 Mar 2026 14:52:03 +0100 Subject: [PATCH] refactor(database)!: Use class for databases and implement ApiCredential --- .gitignore | 1 + docs/database/nvd.md | 41 ++++-- docs/database/osv.md | 31 +++-- docs/database/snyk.md | 95 ++++++++++++++ docs/database/sonatype.md | 67 ++++++++-- docs/database/synk.md | 62 ---------- src/constants.ts | 2 - src/credential.ts | 56 +++++++++ src/database/index.ts | 33 ++++- src/database/nvd.ts | 117 ++++++++++-------- src/database/osv.ts | 88 +++++++------ src/database/snyk.ts | 50 +++++--- src/database/sonatype.ts | 65 ++++++---- src/index.ts | 22 +++- src/strategies/snyk.ts | 20 ++- src/strategies/sonatype.ts | 24 +++- test/database/credential.unit.spec.ts | 58 +++++++++ test/database/nvd.unit.spec.ts | 40 +++--- test/database/osv.unit.spec.ts | 19 +-- test/database/snyk.unit.spec.ts | 21 ++-- test/database/sonatype.unit.spec.ts | 48 +++++-- test/strategies/snyk/index.unit.spec.ts | 10 +- .../sonatype/index.integration.spec.ts | 8 +- test/strategies/sonatype/index.unit.spec.ts | 10 +- test/tsconfig.json | 11 ++ 25 files changed, 709 insertions(+), 290 deletions(-) create mode 100644 docs/database/snyk.md delete mode 100644 docs/database/synk.md create mode 100644 src/credential.ts create mode 100644 test/database/credential.unit.spec.ts create mode 100644 test/tsconfig.json diff --git a/.gitignore b/.gitignore index 220bed7..85bbcb7 100644 --- a/.gitignore +++ b/.gitignore @@ -108,3 +108,4 @@ dist .vscode/ temp/ +.claude diff --git a/docs/database/nvd.md b/docs/database/nvd.md index e535da2..339af72 100644 --- a/docs/database/nvd.md +++ b/docs/database/nvd.md @@ -4,7 +4,7 @@ NVD stand for National Vulnerability Database, which is the U.S. gove ## Implementation Notes -The NVD integration uses the REST API (v2.0) available at [services.nvd.nist.gov](https://services.nvd.nist.gov/rest/json/cves/2.0). +The NVD integration uses the REST API (v2.0) available at [services.nvd.nist.gov](https://services.nvd.nist.gov/rest/json/cves/2.0). ### Search Parameters @@ -30,7 +30,27 @@ export interface NVD { ## API -### findOne(parameters: NVDApiParameter): Promise< NVD[] > +### Constructor + +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const db = new vulnera.Database.NVD({ + credential: new vulnera.ApiCredential({ + type: "querystring", + name: "apiKey", + value: "your-api-key" + }) +}); +``` + +```ts +export interface NVDOptions { + credential?: ApiCredential; +} +``` + +### `findOne(parameters: NVDApiParameter): Promise` Find the vulnerabilities of a given package using available NVD API parameters. ```ts @@ -43,19 +63,20 @@ export type NVDApiParameter = { }; ``` -### findOneBySpec(spec: string): Promise< NVD[] > +### `findOneBySpec(spec: string): Promise` 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.nvd.findOneBySpec( - "express@4.0.0" -); +const vulns = await db.findOneBySpec("express@4.0.0"); console.log(vulns); ``` -### findMany< T extends string >(specs: T[]): Promise< Record< T, NVD[] > > +### `findMany(specs: T[]): Promise>` Find the vulnerabilities of many packages using the spec format. -Returns a Record where keys are equals to the provided specs. \ No newline at end of file +Returns a Record where keys are equals to the provided specs. + +```ts +const vulns = await db.findMany(["express@4.0.0", "lodash@4.17.0"]); +console.log(vulns); +``` diff --git a/docs/database/osv.md b/docs/database/osv.md index fe169b0..c534d95 100644 --- a/docs/database/osv.md +++ b/docs/database/osv.md @@ -38,7 +38,21 @@ export interface OSV { ## API -### findOne(parameters: OSVApiParameter): Promise< OSV[] > +### Constructor + +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const db = new vulnera.Database.OSV(); +``` + +```ts +export interface OSVOptions { + credential?: ApiCredential; +} +``` + +### `findOne(parameters: OSVApiParameter): Promise` Find the vulnerabilities of a given package using available OSV API parameters. ```ts @@ -54,19 +68,20 @@ export type OSVApiParameter = { } ``` -### findOneBySpec(spec: string): Promise< OSV[] > +### `findOneBySpec(spec: string): Promise` 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" -); +const vulns = await db.findOneBySpec("01template1"); console.log(vulns); ``` -### findMany< T extends string >(specs: T[]): Promise< Record< T, OSV[] > > +### `findMany(specs: T[]): Promise>` Find the vulnerabilities of many packages using the spec format. Return a Record where keys are equals to the provided specs. + +```ts +const vulns = await db.findMany(["express@4.0.0", "lodash@4.17.0"]); +console.log(vulns); +``` diff --git a/docs/database/snyk.md b/docs/database/snyk.md new file mode 100644 index 0000000..7285f69 --- /dev/null +++ b/docs/database/snyk.md @@ -0,0 +1,95 @@ +# Snyk + +[Snyk](https://snyk.io/fr) Snyk Limited is a developer-oriented cybersecurity company, specializing in securing custom developed code, open-source dependencies and cloud infrastructure. + +## Implementation Notes + +The Snyk integration uses the REST API (v1) available at [snyk.io](https://snyk.io/api/v1/test/npm) to perform security audit. + +### Authentication + +The `Snyk` constructor requires an `org` and a `credential`. These are generated when you create an organization on Snyk. + +- `org`: Your Snyk organization ID +- `credential`: An `ApiCredential` instance using the `token` type (passed as `Authorization: token ` header) + +### Format + +The Snyk interface is exported as root like `SnykAuditResponse`. + +```ts +export interface SnykAuditResponse { + /** Does this package have one or more issues? **/ + ok: boolean; + /** The issues found. **/ + issues: { + vulnerabilities: SnykVulnerability[]; + licenses: SnykVulnerability[]; + }; + /** The number of dependencies the package has. **/ + dependencyCount: number; + /** The organization this test was carried out for. **/ + org: { + id: string; + name: string; + }; + /** The organization's licenses policy used for this test **/ + licensesPolicy: null | object; + /** The package manager for this package **/ + packageManager: string; +} +``` + +## API + +### Constructor + +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const db = new vulnera.Database.Snyk({ + org: process.env.SNYK_ORG, + credential: new vulnera.ApiCredential(process.env.SNYK_TOKEN) +}); +``` + +```ts +export interface SnykOptions { + org: string; + credential: ApiCredential; +} +``` + +### `findOne(parameters: SnykFindOneParameters): Promise` + +Find the vulnerabilities of a given package using available SnykFindOneParameters API parameters. + +```ts +export type SnykFindOneParameters = { + files: { + target: { + contents: string; + }; + additional?: { + contents: string; + }[]; + }; +}; +``` + +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const db = new vulnera.Database.Snyk({ + org: process.env.SNYK_ORG, + credential: new vulnera.ApiCredential({ + type: "token", + token: process.env.SNYK_TOKEN + }) +}); +const result = await db.findOne({ + files: { + target: { contents: packageJsonBase64 } + } +}); +``` diff --git a/docs/database/sonatype.md b/docs/database/sonatype.md index 9928e4c..aa0c730 100644 --- a/docs/database/sonatype.md +++ b/docs/database/sonatype.md @@ -2,22 +2,50 @@ Sonatype provides software supply chain security and repository management tools to help organizations manage risks in their open source dependencies. -### Implementation Notes +## Implementation Notes The Sonatype integration uses the REST API (v3) available at [ossindex.sonatype.org](https://ossindex.sonatype.org/api/v3/component-report). +### Authentication + +`Sonatype` supports optional basic auth credentials for higher rate limits. Without credentials, the API is still accessible at reduced rate limits. + ### Format -the Sonatype interface is exported as root like `SonatypeResponse`. +The Sonatype interface is exported as root like `SonatypeResponse`. + +```ts +export type SonatypeResponse = { + coordinates: string; + vulnerabilities: SonatypeVulnerability[]; +}; +``` + +## API + +### Constructor + +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const db = new vulnera.Database.Sonatype({ + credential: new vulnera.ApiCredential({ + type: "basic", + username: process.env.SONATYPE_USERNAME, + password: process.env.SONATYPE_PASSWORD + }) +}); +``` ```ts -export type SonatypeResponse = { - coordinates: string; vulnerabilities: SonatypeVulnerability[]; - }; +export interface SonatypeOptions { + credential?: ApiCredential; +} ``` -### API -### findOne(parameters: SonaTypeFindOneParameters): Promise< SonatypeResponse[] > +### `findOne(parameters: SonaTypeFindOneParameters): Promise` + +Find the vulnerabilities of a given package using available Sonatype API parameters. ```ts export type SonaTypeFindOneParameters = { @@ -25,9 +53,17 @@ export type SonaTypeFindOneParameters = { }; ``` -Find the vulnerabilities of a given package using available Sonatype API parameters. +```ts +import * as vulnera from "@nodesecure/vulnera"; -### findMany(parameters: SonaTypeFindManyParameters): Promise< SonatypeResponse[] > > +const db = new vulnera.Database.Sonatype(); +const vulns = await db.findOne({ coordinates: ["pkg:npm/express@4.0.0"] }); +console.log(vulns); +``` + +### `findMany(parameters: SonaTypeFindManyParameters): Promise` + +Find the vulnerabilities of many packages. ```ts export type SonaTypeFindManyParameters = { @@ -35,4 +71,15 @@ export type SonaTypeFindManyParameters = { }; ``` -Find the vulnerabilities of many packages. \ No newline at end of file +```ts +import * as vulnera from "@nodesecure/vulnera"; + +const db = new vulnera.Database.Sonatype(); +const vulns = await db.findMany({ + coordinates: [ + ["pkg:npm/express@4.0.0"], + ["pkg:npm/lodash@4.17.0"] + ] +}); +console.log(vulns); +``` diff --git a/docs/database/synk.md b/docs/database/synk.md deleted file mode 100644 index 2412c26..0000000 --- a/docs/database/synk.md +++ /dev/null @@ -1,62 +0,0 @@ -# Synk - -[Snyk](https://snyk.io/fr) Snyk Limited is a developer-oriented cybersecurity company, specializing in securing custom developed code, open-source dependencies and cloud infrastructure. - -## Implementation Notes - -the Snyk integration uses the REST API (v1) available at [snyk.io](https://snyk.io/api/v1/test/npm) to perfom security audit. - - -### Search Parameters - -We have to pass the `org` search paraeter who is generated when you create an organization on Synk. - -### Headers - -we also need to pass the `Authorization` header with the token generated by Synk. - -### Format - -The Synk interface is exported as root like `SnykAuditResponse`. - -```ts -export interface SnykAuditResponse { - /** Does this package have one or more issues? **/ - ok: boolean; - /** The issues found. **/ - issues: { - vulnerabilities: SnykVulnerability[]; - licenses: SnykVulnerability[]; - }; - /** The number of dependencies the package has. **/ - dependencyCount: number; - /** The organization this test was carried out for. **/ - org: { - id: string; - name: string; - }; - /** The organization's licenses policy used for this test **/ - licensesPolicy: null | object; - /** The package manager for this package **/ - packageManager: string; -} -``` - -## API - -### findOne(parameters: SnykFindOneParameters): Promise< SnykAuditResponse> - -Find the vulnerabilities of a given package using available SnykFindOneParameters API parameters. - -```ts -export type SnykFindOneParameters = { - files: { - target: { - contents: string; - }; - additional?: { - contents: string; - }[]; - }; -}; -``` \ No newline at end of file diff --git a/src/constants.ts b/src/constants.ts index 988891f..04d492b 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,7 +1,5 @@ export const NPM_TOKEN = typeof process.env.NODE_SECURE_TOKEN === "string" ? { token: process.env.NODE_SECURE_TOKEN } : {}; -export const SNYK_ORG = process.env.SNYK_ORG; -export const SNYK_TOKEN = process.env.SNYK_TOKEN; export const VULN_MODE = Object.freeze({ GITHUB_ADVISORY: "github-advisory", diff --git a/src/credential.ts b/src/credential.ts new file mode 100644 index 0000000..c4040f5 --- /dev/null +++ b/src/credential.ts @@ -0,0 +1,56 @@ +export type ApiCredentialOptions = + | { type: "bearer"; token: string; } + | { type: "token"; token: string; } + | { type: "basic"; username: string; password: string; } + | { type: "querystring"; name: string; value: string; } + | { type: "custom"; authorization: string; }; + +export class ApiCredential { + readonly #options: ApiCredentialOptions | undefined; + + constructor( + optionsOrToken?: ApiCredentialOptions | string + ) { + this.#options = typeof optionsOrToken === "string" + ? { type: "bearer", token: optionsOrToken } + : optionsOrToken; + } + + get headers(): Record { + if (!this.#options) { + return {}; + } + + const options = this.#options; + + if (options.type === "bearer") { + return { Authorization: `Bearer ${options.token}` }; + } + if (options.type === "token") { + return { Authorization: `token ${options.token}` }; + } + if (options.type === "basic") { + const encoded = Buffer.from(`${options.username}:${options.password}`).toString("base64"); + + return { Authorization: `Basic ${encoded}` }; + } + if (options.type === "querystring") { + return {}; + } + if (options.type === "custom") { + return { Authorization: options.authorization }; + } + + return {}; + } + + get queryParams(): Record { + if (this.#options?.type === "querystring") { + return { + [this.#options.name]: this.#options.value + }; + } + + return {}; + } +} diff --git a/src/database/index.ts b/src/database/index.ts index e9e022a..26e5272 100644 --- a/src/database/index.ts +++ b/src/database/index.ts @@ -1,4 +1,29 @@ -export * as osv from "./osv.ts"; -export * as snyk from "./snyk.ts"; -export * as nvd from "./nvd.ts"; -export * as sonatype from "./sonatype.ts"; +export { ApiCredential } from "../credential.ts"; +export type { ApiCredentialOptions } from "../credential.ts"; + +export { + NVD +} from "./nvd.ts"; +export type { + NVDOptions, + NVDApiParameter +} from "./nvd.ts"; + +export { OSV } from "./osv.ts"; +export type { + OSVOptions, + OSVApiParameter +} from "./osv.ts"; + +export { Snyk } from "./snyk.ts"; +export type { + SnykOptions, + SnykFindOneParameters +} from "./snyk.ts"; + +export { Sonatype } from "./sonatype.ts"; +export type { + SonatypeOptions, + SonaTypeFindOneParameters, + SonaTypeFindManyParameters +} from "./sonatype.ts"; diff --git a/src/database/nvd.ts b/src/database/nvd.ts index 769a2d8..5dc317f 100644 --- a/src/database/nvd.ts +++ b/src/database/nvd.ts @@ -3,10 +3,8 @@ import * as httpie from "@openally/httpie"; // Import Internal Dependencies import * as utils from "../utils.ts"; -import type { NVD } from "../formats/nvd/index.ts"; - -// CONSTANTS -export const ROOT_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"; +import type { NVD as NVDFormat } from "../formats/nvd/index.ts"; +import type { ApiCredential } from "../credential.ts"; /** * @description Parameters for querying the NVD API @@ -29,63 +27,76 @@ export type NVDApiParameter = { ecosystem?: string; }; -export async function findOne( - parameters: NVDApiParameter -): Promise { - const queryParams = new URLSearchParams(); - - if (parameters.packageName) { - queryParams.append("keywordSearch", parameters.packageName); - /** - * NVD doesn't support cpeMatchString - * We will only search by keyword for now - */ - } - if (parameters.cvssV3Severity) { - queryParams.append("cvssV3Severity", parameters.cvssV3Severity); - } +export interface NVDOptions { + credential: ApiCredential; +} - if (parameters.cweId) { - queryParams.append("cweId", parameters.cweId); +export class NVD { + static readonly ROOT_API = "https://services.nvd.nist.gov/rest/json/cves/2.0"; + + readonly #credential: ApiCredential; + + constructor( + options: NVDOptions + ) { + this.#credential = options.credential; } - const url = new URL(ROOT_API); - url.search = queryParams.toString(); + async findOne( + parameters: NVDApiParameter + ): Promise { + const queryParams = new URLSearchParams(); - try { - const { data } = await httpie.get<{ vulnerabilities: NVD[]; }>(url.toString()); + if (parameters.packageName) { + queryParams.append("keywordSearch", parameters.packageName); + /** + * NVD doesn't support cpeMatchString + * We will only search by keyword for now + */ + } + if (parameters.cvssV3Severity) { + queryParams.append("cvssV3Severity", parameters.cvssV3Severity); + } + if (parameters.cweId) { + queryParams.append("cweId", parameters.cweId); + } + for (const [name, value] of Object.entries(this.#credential.queryParams)) { + queryParams.append(name, value); + } - return data.vulnerabilities || []; - } - catch (error: any) { - console.error("NVD API Error:", error.message || error); + const url = new URL(NVD.ROOT_API); + url.search = queryParams.toString(); - return []; + try { + const { data } = await httpie.get<{ vulnerabilities: NVDFormat[]; }>(url.toString()); + + return data.vulnerabilities || []; + } + catch (error: any) { + console.error("NVD API Error:", error.message || error); + + return []; + } } -} -export function findOneBySpec( - spec: string -) { - const { name } = utils.parseNpmSpec(spec); + findOneBySpec( + spec: string + ): Promise { + const { name } = utils.parseNpmSpec(spec); - return findOne({ - packageName: name, - ecosystem: "npm" - }); -} + return this.findOne({ + packageName: name, + ecosystem: "npm" + }); + } + + async findMany( + specs: T[] + ): Promise> { + const entries = await Promise.all( + specs.map(async(spec) => [spec, await this.findOneBySpec(spec)] as [T, NVDFormat[]]) + ); -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); + return Object.fromEntries(entries) as Record; + } } diff --git a/src/database/osv.ts b/src/database/osv.ts index 9978f0e..2621c18 100644 --- a/src/database/osv.ts +++ b/src/database/osv.ts @@ -2,11 +2,9 @@ import * as httpie from "@openally/httpie"; // Import Internal Dependencies -import type { OSV } from "../formats/osv/index.ts"; +import type { OSV as OSVFormat } from "../formats/osv/index.ts"; import * as utils from "../utils.ts"; - -// CONSTANTS -export const ROOT_API = "https://api.osv.dev"; +import type { ApiCredential } from "../credential.ts"; export type OSVApiParameter = { version?: string; @@ -19,47 +17,59 @@ export type OSVApiParameter = { }; }; -export async function findOne( - parameters: OSVApiParameter -): Promise { - if (!parameters.package.ecosystem) { - parameters.package.ecosystem = "npm"; +export interface OSVOptions { + credential?: ApiCredential; +} + +export class OSV { + static readonly ROOT_API = "https://api.osv.dev"; + + readonly #credential: ApiCredential | undefined; + + constructor( + options: OSVOptions = {} + ) { + this.#credential = options.credential; } - const { data } = await httpie.post<{ vulns: OSV[]; }>( - new URL("v1/query", ROOT_API), - { - body: parameters + async findOne( + parameters: OSVApiParameter + ): Promise { + if (!parameters.package.ecosystem) { + parameters.package.ecosystem = "npm"; } - ); - return data.vulns; -} + const { data } = await httpie.post<{ vulns: OSVFormat[]; }>( + new URL("v1/query", OSV.ROOT_API), + { + headers: this.#credential?.headers, + body: parameters + } + ); -export function findOneBySpec( - spec: string -) { - const { name, version } = utils.parseNpmSpec(spec); + return data.vulns; + } - return findOne({ - version, - package: { - name - } - }); -} + findOneBySpec( + spec: string + ): Promise { + const { name, version } = utils.parseNpmSpec(spec); -export async function findMany( - specs: T[] -): Promise> { - const packagesVulns = await Promise.all( - specs.map(async(spec) => { - return { - [spec]: await findOneBySpec(spec) - }; - }) - ); + return this.findOne({ + version, + package: { + name + } + }); + } + + async findMany( + specs: T[] + ): Promise> { + const entries = await Promise.all( + specs.map(async(spec) => [spec, await this.findOneBySpec(spec)] as [T, OSVFormat[]]) + ); - // @ts-ignore - return Object.assign(...packagesVulns); + return Object.fromEntries(entries) as Record; + } } diff --git a/src/database/snyk.ts b/src/database/snyk.ts index feb6776..4139f92 100644 --- a/src/database/snyk.ts +++ b/src/database/snyk.ts @@ -2,11 +2,8 @@ import * as httpie from "@openally/httpie"; // Import Internal Dependencies -import { SNYK_ORG, SNYK_TOKEN } from "../constants.ts"; import type { SnykAuditResponse } from "../formats/snyk/index.ts"; - -// CONSTANTS -export const ROOT_API = "https://snyk.io"; +import type { ApiCredential } from "../credential.ts"; export type SnykFindOneParameters = { files: { @@ -19,18 +16,35 @@ export type SnykFindOneParameters = { }; }; -export async function findOne( - parameters: SnykFindOneParameters -): Promise { - const { data } = await httpie.post( - new URL(`/api/v1/test/npm?org=${SNYK_ORG}`, ROOT_API), - { - headers: { - Authorization: `token ${SNYK_TOKEN}` - }, - body: parameters - } - ); - - return data; +export interface SnykOptions { + org: string; + credential: ApiCredential; +} + +export class Snyk { + static readonly ROOT_API = "https://snyk.io"; + + readonly #org: string; + readonly #credential: ApiCredential; + + constructor( + options: SnykOptions + ) { + this.#org = options.org; + this.#credential = options.credential; + } + + async findOne( + parameters: SnykFindOneParameters + ): Promise { + const { data } = await httpie.post( + new URL(`/api/v1/test/npm?org=${this.#org}`, Snyk.ROOT_API), + { + headers: this.#credential.headers, + body: parameters + } + ); + + return data; + } } diff --git a/src/database/sonatype.ts b/src/database/sonatype.ts index 187d8e1..ebeb316 100644 --- a/src/database/sonatype.ts +++ b/src/database/sonatype.ts @@ -3,6 +3,7 @@ import * as httpie from "@openally/httpie"; // Import Internal Dependencies import type { SonatypeResponse } from "../formats/sonatype/index.ts"; +import type { ApiCredential } from "../credential.ts"; export type SonaTypeFindOneParameters = { coordinates: string[]; @@ -12,29 +13,47 @@ export type SonaTypeFindManyParameters = { coordinates: string[][]; }; -// CONSTANTS -export const ROOT_API = "https://ossindex.sonatype.org"; - -export async function findOne( - parameters: SonaTypeFindOneParameters -): Promise { - const { data } = await httpie.post( - new URL("/api/v3/component-report", ROOT_API), - { - headers: { - accept: "application/json" - }, - body: parameters - } - ); - - return data; +export interface SonatypeOptions { + credential: ApiCredential; } -export async function findMany(parameters: SonaTypeFindManyParameters): Promise { - const data = await Promise.all( - parameters.coordinates.map((coordinates) => findOne({ coordinates })) - ); - - return data.flat(); +export class Sonatype { + static readonly ROOT_API = "https://ossindex.sonatype.org"; + + readonly #credential: ApiCredential; + + constructor( + options: SonatypeOptions + ) { + this.#credential = options.credential; + } + + async findOne( + parameters: SonaTypeFindOneParameters + ): Promise { + const headers: Record = { + accept: "application/json", + ...this.#credential.headers + }; + + const { data } = await httpie.post( + new URL("/api/v3/component-report", Sonatype.ROOT_API), + { + headers, + body: parameters + } + ); + + return data; + } + + async findMany( + parameters: SonaTypeFindManyParameters + ): Promise { + const data = await Promise.all( + parameters.coordinates.map((coordinates) => this.findOne({ coordinates })) + ); + + return data.flat(); + } } diff --git a/src/index.ts b/src/index.ts index 83f7c65..f6bde63 100644 --- a/src/index.ts +++ b/src/index.ts @@ -9,12 +9,14 @@ import { import { SnykStrategy, - type SnykStrategyDefinition + type SnykStrategyDefinition, + type SnykStrategyOptions } from "./strategies/snyk.ts"; import { SonatypeStrategy, type SonatypeStrategyDefinition, + type SonatypeStrategyOptions, type SonatypeVulnerability } from "./strategies/sonatype.ts"; @@ -28,6 +30,8 @@ import { type Kind } from "./constants.ts"; +import { ApiCredential, type ApiCredentialOptions } from "./credential.ts"; + import type { SnykVulnerability } from "./formats/snyk/index.ts"; @@ -51,6 +55,8 @@ import type { } from "./strategies/types/api.ts"; export * as Database from "./database/index.ts"; +export { ApiCredential }; +export type { ApiCredentialOptions }; export type AllStrategy = { none: NoneStrategyDefinition; @@ -60,6 +66,13 @@ export type AllStrategy = { }; export type AnyStrategy = AllStrategy[keyof AllStrategy]; +type StrategyOptions = { + none: undefined; + "github-advisory": undefined; + snyk: SnykStrategyOptions; + sonatype: SonatypeStrategyOptions; +}; + // CONSTANTS const kAvailableStrategy = new Set(Object.values(VULN_MODE)); @@ -67,7 +80,8 @@ const kAvailableStrategy = new Set(Object.values(VULN_MODE)); let localVulnerabilityStrategy: AnyStrategy; export function setStrategy( - name: T + name: T, + options?: StrategyOptions[T] ): AllStrategy[T] { if (!kAvailableStrategy.has(name)) { throw new Error( @@ -79,10 +93,10 @@ export function setStrategy( localVulnerabilityStrategy = Object.seal(GitHubAdvisoryStrategy()); } else if (name === VULN_MODE.SNYK) { - localVulnerabilityStrategy = Object.seal(SnykStrategy()); + localVulnerabilityStrategy = Object.seal(SnykStrategy(options as SnykStrategyOptions)); } else if (name === VULN_MODE.SONATYPE) { - localVulnerabilityStrategy = Object.seal(SonatypeStrategy()); + localVulnerabilityStrategy = Object.seal(SonatypeStrategy(options as SonatypeStrategyOptions)); } else { localVulnerabilityStrategy = Object.seal(NoneStrategy()); diff --git a/src/strategies/snyk.ts b/src/strategies/snyk.ts index 08fd899..f79b666 100644 --- a/src/strategies/snyk.ts +++ b/src/strategies/snyk.ts @@ -11,19 +11,33 @@ import type { BaseStrategy } from "./types/api.ts"; import { type SnykAuditResponse } from "../formats/snyk/index.ts"; -import { snyk } from "../database/index.ts"; +import { Snyk } from "../database/index.ts"; import { formatVulnsPayload } from "../formats/index.ts"; +import type { ApiCredential } from "../credential.ts"; + +export interface SnykStrategyOptions { + org: string; + credential: ApiCredential; +} export type SnykStrategyDefinition = BaseStrategy<"snyk">; -export function SnykStrategy(): SnykStrategyDefinition { +export function SnykStrategy( + options: SnykStrategyOptions +): SnykStrategyDefinition { + const snyk = new Snyk({ + org: options.org, + credential: options.credential + }); + return { strategy: VULN_MODE.SNYK, - hydratePayloadDependencies + hydratePayloadDependencies: hydratePayloadDependencies.bind(null, snyk) }; } async function hydratePayloadDependencies( + snyk: Snyk, dependencies: Dependencies, options: HydratePayloadDepsOptions ) { diff --git a/src/strategies/sonatype.ts b/src/strategies/sonatype.ts index 72f7a55..192e758 100644 --- a/src/strategies/sonatype.ts +++ b/src/strategies/sonatype.ts @@ -7,11 +7,16 @@ import type { BaseStrategy } from "./types/api.ts"; import { formatVulnsPayload } from "../formats/index.ts"; -import { sonatype } from "../database/index.ts"; +import { Sonatype } from "../database/index.ts"; +import type { ApiCredential } from "../credential.ts"; // CONSTANTS const kRatelimitChunkSize = 128; +export interface SonatypeStrategyOptions { + credential: ApiCredential; +} + export interface SonatypeVulnerability { id: string; displayName: string; @@ -29,10 +34,15 @@ export interface SonatypeVulnerability { export type SonatypeStrategyDefinition = BaseStrategy<"sonatype">; -export function SonatypeStrategy(): SonatypeStrategyDefinition { +export function SonatypeStrategy( + options: SonatypeStrategyOptions +): SonatypeStrategyDefinition { + const { credential } = options; + const sonatype = new Sonatype({ credential }); + return { strategy: VULN_MODE.SONATYPE, - hydratePayloadDependencies + hydratePayloadDependencies: hydratePayloadDependencies.bind(null, sonatype) }; } @@ -70,9 +80,13 @@ function createPackageURLCoordinates( return Object.keys(versions).map((version) => toPackageURL(dependencyName, version)); } -type SonatypeHttpResponse = { coordinates: string; vulnerabilities: SonatypeVulnerability[]; }; +type SonatypeHttpResponse = { + coordinates: string; + vulnerabilities: SonatypeVulnerability[]; +}; async function fetchDataForPackageURLs( + sonatype: Sonatype, unchunkedCoordinates: string[] ): Promise { try { @@ -112,10 +126,12 @@ function vulnWithPackageName(packageName: string) { } async function hydratePayloadDependencies( + sonatype: Sonatype, dependencies: Dependencies, options: BaseStrategyOptions = {} ): Promise { const packageURLsData = await fetchDataForPackageURLs( + sonatype, Array.from(dependencies).flatMap(createPackageURLCoordinates) ); diff --git a/test/database/credential.unit.spec.ts b/test/database/credential.unit.spec.ts new file mode 100644 index 0000000..50e6ca8 --- /dev/null +++ b/test/database/credential.unit.spec.ts @@ -0,0 +1,58 @@ +// Import Node.js Dependencies +import { describe, test } from "node:test"; +import assert from "node:assert"; + +// Import Internal Dependencies +import { ApiCredential } from "../../src/credential.ts"; + +describe("ApiCredential", () => { + test("string token produces Authorization: Bearer header", () => { + const cred = new ApiCredential("mytoken"); + + assert.deepStrictEqual(cred.headers, { Authorization: "Bearer mytoken" }); + assert.deepStrictEqual(cred.queryParams, {}); + }); + + test("bearer type produces Authorization: Bearer header", () => { + const cred = new ApiCredential({ type: "bearer", token: "mytoken" }); + + assert.deepStrictEqual(cred.headers, { Authorization: "Bearer mytoken" }); + assert.deepStrictEqual(cred.queryParams, {}); + }); + + test("token type produces Authorization: token header", () => { + const cred = new ApiCredential({ type: "token", token: "mytoken" }); + + assert.deepStrictEqual(cred.headers, { Authorization: "token mytoken" }); + assert.deepStrictEqual(cred.queryParams, {}); + }); + + test("basic type produces Authorization: Basic header with base64-encoded credentials", () => { + const cred = new ApiCredential({ type: "basic", username: "user", password: "pass" }); + const expected = Buffer.from("user:pass").toString("base64"); + + assert.deepStrictEqual(cred.headers, { Authorization: `Basic ${expected}` }); + assert.deepStrictEqual(cred.queryParams, {}); + }); + + test("querystring type produces a query param and no headers", () => { + const cred = new ApiCredential({ type: "querystring", name: "apiKey", value: "secret" }); + + assert.deepStrictEqual(cred.headers, {}); + assert.deepStrictEqual(cred.queryParams, { apiKey: "secret" }); + }); + + test("custom type produces Authorization header with the raw value", () => { + const cred = new ApiCredential({ type: "custom", authorization: "SharedAccessSignature sv=..." }); + + assert.deepStrictEqual(cred.headers, { Authorization: "SharedAccessSignature sv=..." }); + assert.deepStrictEqual(cred.queryParams, {}); + }); + + test("no options produces empty headers and queryParams", () => { + const cred = new ApiCredential(); + + assert.deepStrictEqual(cred.headers, {}); + assert.deepStrictEqual(cred.queryParams, {}); + }); +}); diff --git a/test/database/nvd.unit.spec.ts b/test/database/nvd.unit.spec.ts index 04c42e5..6d937a3 100644 --- a/test/database/nvd.unit.spec.ts +++ b/test/database/nvd.unit.spec.ts @@ -7,11 +7,17 @@ import { HTTP_CLIENT_HEADERS, setupHttpAgentMock } from "../strategies/utils.ts"; -import { nvd } from "../../src/database/index.ts"; +import { NVD, ApiCredential } from "../../src/database/index.ts"; -describe("nvd", () => { +// CONSTANTS +const kTestApiKey = "test-api-key"; +const kTestCredential = new ApiCredential({ type: "querystring", name: "apiKey", value: kTestApiKey }); +const kNvdPathname = new URL(NVD.ROOT_API).pathname; + +describe("Database.NVD", () => { + const db = new NVD({ credential: kTestCredential }); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); - const mockedHttpClient = mockedHttpAgent.get(new URL(nvd.ROOT_API).origin); + const mockedHttpClient = mockedHttpAgent.get(new URL(NVD.ROOT_API).origin); after(() => { restoreHttpAgent(); @@ -22,16 +28,17 @@ describe("nvd", () => { const expectedResponse = { vulnerabilities: ["cve-data-1", "cve-data-2"] }; const params = new URLSearchParams(); params.append("keywordSearch", "express"); + params.append("apiKey", kTestApiKey); const queryString = params.toString(); mockedHttpClient .intercept({ - path: `${new URL(nvd.ROOT_API).pathname}?${queryString}`, + path: `${kNvdPathname}?${queryString}`, method: "GET" }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const vulns = await nvd.findOne({ + const vulns = await db.findOne({ packageName: "express", ecosystem: "npm" }); @@ -44,16 +51,17 @@ describe("nvd", () => { const params = new URLSearchParams(); params.append("keywordSearch", "express"); params.append("cvssV3Severity", "HIGH"); + params.append("apiKey", kTestApiKey); const queryString = params.toString(); mockedHttpClient .intercept({ - path: `${new URL(nvd.ROOT_API).pathname}?${queryString}`, + path: `${kNvdPathname}?${queryString}`, method: "GET" }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const vulns = await nvd.findOne({ + const vulns = await db.findOne({ packageName: "express", ecosystem: "npm", cvssV3Severity: "HIGH" @@ -67,16 +75,17 @@ describe("nvd", () => { const packageName = "express"; const params = new URLSearchParams(); params.append("keywordSearch", packageName); + params.append("apiKey", kTestApiKey); const queryString = params.toString(); mockedHttpClient .intercept({ - path: `${new URL(nvd.ROOT_API).pathname}?${queryString}`, + path: `${kNvdPathname}?${queryString}`, method: "GET" }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const vulns = await nvd.findOneBySpec(`${packageName}@1.0.0`); + const vulns = await db.findOneBySpec(`${packageName}@1.0.0`); assert.deepStrictEqual(vulns, expectedResponse.vulnerabilities); }); @@ -85,27 +94,29 @@ describe("nvd", () => { const paramsFirst = new URLSearchParams(); paramsFirst.append("keywordSearch", "foobar"); + paramsFirst.append("apiKey", kTestApiKey); const queryStringFirst = paramsFirst.toString(); const paramsSecond = new URLSearchParams(); paramsSecond.append("keywordSearch", "yoobar"); + paramsSecond.append("apiKey", kTestApiKey); const queryStringSecond = paramsSecond.toString(); mockedHttpClient .intercept({ - path: `${new URL(nvd.ROOT_API).pathname}?${queryStringFirst}`, + path: `${kNvdPathname}?${queryStringFirst}`, method: "GET" }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); mockedHttpClient .intercept({ - path: `${new URL(nvd.ROOT_API).pathname}?${queryStringSecond}`, + path: `${kNvdPathname}?${queryStringSecond}`, method: "GET" }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const result = await nvd.findMany( + const result = await db.findMany( ["foobar", "yoobar"] ); @@ -120,16 +131,17 @@ describe("nvd", () => { const params = new URLSearchParams(); params.append("keywordSearch", "nonexistent"); + params.append("apiKey", kTestApiKey); const queryString = params.toString(); mockedHttpClient .intercept({ - path: `${new URL(nvd.ROOT_API).pathname}?${queryString}`, + path: `${kNvdPathname}?${queryString}`, method: "GET" }) .reply(200, emptyResponse, HTTP_CLIENT_HEADERS); - const vulns = await nvd.findOne({ + const vulns = await db.findOne({ packageName: "nonexistent", ecosystem: "npm" }); diff --git a/test/database/osv.unit.spec.ts b/test/database/osv.unit.spec.ts index 3eba4ff..ef2e0af 100644 --- a/test/database/osv.unit.spec.ts +++ b/test/database/osv.unit.spec.ts @@ -7,11 +7,12 @@ import { HTTP_CLIENT_HEADERS, setupHttpAgentMock } from "../strategies/utils.ts"; -import { osv } from "../../src/database/index.ts"; +import { OSV } from "../../src/database/index.ts"; -describe("osv", () => { +describe("Database.OSV", () => { + const db = new OSV(); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); - const mockedHttpClient = mockedHttpAgent.get(osv.ROOT_API); + const mockedHttpClient = mockedHttpAgent.get(OSV.ROOT_API); after(() => { restoreHttpAgent(); @@ -22,13 +23,13 @@ describe("osv", () => { const expectedResponse = { vulns: "hello world" }; mockedHttpClient .intercept({ - path: new URL("/v1/query", osv.ROOT_API).href, + path: new URL("/v1/query", OSV.ROOT_API).href, method: "POST", body: JSON.stringify({ package: { name: "foobar", ecosystem: "npm" } }) }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const vulns = await osv.findOne({ + const vulns = await db.findOne({ package: { name: "foobar", ecosystem: "npm" @@ -44,7 +45,7 @@ describe("osv", () => { mockedHttpClient .intercept({ - path: new URL("/v1/query", osv.ROOT_API).href, + path: new URL("/v1/query", OSV.ROOT_API).href, method: "POST", body: JSON.stringify({ version: "2.0.0", @@ -53,7 +54,7 @@ describe("osv", () => { }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const vulns = await osv.findOneBySpec(`${packageName}@2.0.0`); + const vulns = await db.findOneBySpec(`${packageName}@2.0.0`); assert.strictEqual(vulns, expectedResponse.vulns); }); @@ -62,13 +63,13 @@ describe("osv", () => { mockedHttpClient .intercept({ - path: new URL("/v1/query", osv.ROOT_API).href, + path: new URL("/v1/query", OSV.ROOT_API).href, method: "POST" }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS) .times(2); - const result = await osv.findMany( + const result = await db.findMany( ["foobar", "yoobar"] ); assert.deepEqual(result, { diff --git a/test/database/snyk.unit.spec.ts b/test/database/snyk.unit.spec.ts index 770e074..177a79f 100644 --- a/test/database/snyk.unit.spec.ts +++ b/test/database/snyk.unit.spec.ts @@ -4,12 +4,17 @@ import assert from "node:assert"; // Import Internal Dependencies import { HTTP_CLIENT_HEADERS, setupHttpAgentMock } from "../strategies/utils.ts"; -import { snyk } from "../../src/database/index.ts"; -import { SNYK_ORG } from "../../src/constants.ts"; +import { Snyk, ApiCredential } from "../../src/database/index.ts"; -describe("snyk", () => { +describe("Database.Snyk", () => { + const org = process.env.SNYK_ORG ?? "test-org"; + const token = process.env.SNYK_TOKEN ?? "test-token"; + const db = new Snyk({ + org, + credential: new ApiCredential({ type: "token", token }) + }); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); - const mockedHttpClient = mockedHttpAgent.get(snyk.ROOT_API); + const mockedHttpClient = mockedHttpAgent.get(Snyk.ROOT_API); after(() => { restoreHttpAgent(); @@ -22,7 +27,7 @@ describe("snyk", () => { mockedHttpClient .intercept({ - path: new URL(`/api/v1/test/npm?org=${SNYK_ORG}`, snyk.ROOT_API).href, + path: new URL(`/api/v1/test/npm?org=${org}`, Snyk.ROOT_API).href, method: "POST", body: JSON.stringify({ files: { @@ -33,7 +38,7 @@ describe("snyk", () => { }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const data = await snyk.findOne({ + const data = await db.findOne({ files: { target: { contents: targetFile }, additional: [{ contents: additionalFile }] @@ -49,7 +54,7 @@ describe("snyk", () => { mockedHttpClient .intercept({ - path: new URL(`/api/v1/test/npm?org=${SNYK_ORG}`, snyk.ROOT_API).href, + path: new URL(`/api/v1/test/npm?org=${org}`, Snyk.ROOT_API).href, method: "POST", body: JSON.stringify({ files: { @@ -59,7 +64,7 @@ describe("snyk", () => { }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const data = await snyk.findOne({ + const data = await db.findOne({ files: { target: { contents: targetFile } } }); diff --git a/test/database/sonatype.unit.spec.ts b/test/database/sonatype.unit.spec.ts index c3f59b3..c36762d 100644 --- a/test/database/sonatype.unit.spec.ts +++ b/test/database/sonatype.unit.spec.ts @@ -7,11 +7,16 @@ import { HTTP_CLIENT_HEADERS, setupHttpAgentMock } from "../strategies/utils.ts"; -import { sonatype } from "../../src/database/index.ts"; +import { Sonatype, ApiCredential } from "../../src/database/index.ts"; -describe("sonatype", () => { +// CONSTANTS +const kTestCredential = new ApiCredential({ type: "basic", username: "user", password: "pass" }); +const kExpectedAuth = `Basic ${Buffer.from("user:pass").toString("base64")}`; + +describe("Database.Sonatype", () => { + const db = new Sonatype({ credential: kTestCredential }); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); - const mockedHttpClient = mockedHttpAgent.get(sonatype.ROOT_API); + const mockedHttpClient = mockedHttpAgent.get(Sonatype.ROOT_API); after(() => { restoreHttpAgent(); @@ -28,37 +33,62 @@ describe("sonatype", () => { const coordinates = ["coord1", "coord2"]; mockedHttpClient .intercept({ - path: new URL("/api/v3/component-report", sonatype.ROOT_API).href, + path: new URL("/api/v3/component-report", Sonatype.ROOT_API).href, method: "POST", body: JSON.stringify({ coordinates }), headers: { - accept: "application/json" + accept: "application/json", + Authorization: kExpectedAuth } }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); - const vulns = await sonatype.findOne({ + const vulns = await db.findOne({ coordinates }); assert.deepStrictEqual(vulns, expectedResponse); }); + test("should send a POST http request with Basic auth header when credential is provided", async() => { + const credential = new ApiCredential({ type: "basic", username: "user", password: "pass" }); + const dbWithAuth = new Sonatype({ credential }); + const expectedResponse = [kSonatypeVulnComponent]; + const coordinates = ["coord1"]; + const expectedAuth = `Basic ${Buffer.from("user:pass").toString("base64")}`; + + mockedHttpClient + .intercept({ + path: new URL("/api/v3/component-report", Sonatype.ROOT_API).href, + method: "POST", + body: JSON.stringify({ coordinates }), + headers: { + accept: "application/json", + Authorization: expectedAuth + } + }) + .reply(200, expectedResponse, HTTP_CLIENT_HEADERS); + + const vulns = await dbWithAuth.findOne({ coordinates }); + assert.deepStrictEqual(vulns, expectedResponse); + }); + test("should send multiple POST http requests to the SONATYPE API using findMany", async() => { const expectedResponse = [kSonatypeVulnComponent]; mockedHttpClient .intercept({ - path: new URL("/api/v3/component-report", sonatype.ROOT_API).href, + path: new URL("/api/v3/component-report", Sonatype.ROOT_API).href, method: "POST", headers: { - accept: "application/json" + accept: "application/json", + Authorization: kExpectedAuth }, body: JSON.stringify({ coordinates: ["coord1", "coord2"] }) }) .reply(200, expectedResponse, HTTP_CLIENT_HEADERS) .times(2); - const result = await sonatype.findMany( + const result = await db.findMany( { coordinates: [["coord1", "coord2"], ["coord1", "coord2"]] } diff --git a/test/strategies/snyk/index.unit.spec.ts b/test/strategies/snyk/index.unit.spec.ts index 0a5b7f3..25e6e85 100644 --- a/test/strategies/snyk/index.unit.spec.ts +++ b/test/strategies/snyk/index.unit.spec.ts @@ -7,6 +7,7 @@ import fs from "node:fs/promises"; // Import Internal Dependencies import { SnykStrategy } from "../../../src/strategies/snyk.ts"; +import { ApiCredential } from "../../../src/credential.ts"; import { expectVulnToBeNodeSecureStandardCompliant, HTTP_CLIENT_HEADERS, @@ -17,7 +18,8 @@ import { const __dirname = path.dirname(fileURLToPath(import.meta.url)); const kFixturesDir = path.join(__dirname, "..", "..", "fixtures"); const kSnykOrigin = "https://snyk.io"; -const kSnykApiPath = "/api/v1/test/npm?org=undefined"; +const kSnykApiPath = "/api/v1/test/npm?org="; +const kTestCredential = new ApiCredential({ type: "token", token: "test-token" }); async function readFileJSON(location: string): Promise { const rawText = await fs.readFile(location, "utf-8"); @@ -44,7 +46,7 @@ function isAdvisory(data: any) { } test("SnykStrategy definition must return only two keys.", () => { - const definition = SnykStrategy(); + const definition = SnykStrategy({ org: "", credential: kTestCredential }); assert.strictEqual( definition.strategy, @@ -58,7 +60,7 @@ test("SnykStrategy definition must return only two keys.", () => { }); test("snyk strategy: hydratePayloadDependencies", async() => { - const { hydratePayloadDependencies } = SnykStrategy(); + const { hydratePayloadDependencies } = SnykStrategy({ org: "", credential: kTestCredential }); const dependencies = new Map(); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); const mockedHttpClient = mockedHttpAgent.get(kSnykOrigin); @@ -95,7 +97,7 @@ test("snyk strategy: hydratePayloadDependencies", async() => { }); test("snyk strategy: hydratePayloadDependencies using NodeSecure standard format", async() => { - const { hydratePayloadDependencies } = SnykStrategy(); + const { hydratePayloadDependencies } = SnykStrategy({ org: "", credential: kTestCredential }); const dependencies = new Map(); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); const mockedHttpClient = mockedHttpAgent.get(kSnykOrigin); diff --git a/test/strategies/sonatype/index.integration.spec.ts b/test/strategies/sonatype/index.integration.spec.ts index d4c402a..45007c3 100644 --- a/test/strategies/sonatype/index.integration.spec.ts +++ b/test/strategies/sonatype/index.integration.spec.ts @@ -4,10 +4,14 @@ import assert from "node:assert"; // Import Internal Dependencies import { SonatypeStrategy } from "../../../src/strategies/sonatype.ts"; +import { ApiCredential } from "../../../src/credential.ts"; import { expectVulnToBeNodeSecureStandardCompliant } from "../utils.ts"; +// CONSTANTS +const kTestCredential = new ApiCredential({ type: "basic", username: "user", password: "pass" }); + test.skip("sonatype strategy: fetching a package with a vulnerability using the API", async() => { - const { hydratePayloadDependencies } = SonatypeStrategy(); + const { hydratePayloadDependencies } = SonatypeStrategy({ credential: kTestCredential }); const dependencies = new Map(); /** * This package is arbitrary chosen and hardcoded as there is no way to fetch @@ -51,7 +55,7 @@ test.skip("sonatype strategy: fetching a package with a vulnerability using the }); test.skip("sonatype strategy: fetching a package with a name that should be percent-encoded/decoded, using the API", async() => { - const { hydratePayloadDependencies } = SonatypeStrategy(); + const { hydratePayloadDependencies } = SonatypeStrategy({ credential: kTestCredential }); const dependencies = new Map(); const packageWithScopeThatShouldBePercentEncoded = { /** diff --git a/test/strategies/sonatype/index.unit.spec.ts b/test/strategies/sonatype/index.unit.spec.ts index 57b0f38..e2b6ee5 100644 --- a/test/strategies/sonatype/index.unit.spec.ts +++ b/test/strategies/sonatype/index.unit.spec.ts @@ -6,6 +6,7 @@ import assert from "node:assert"; import { SonatypeStrategy } from "../../../src/strategies/sonatype.ts"; +import { ApiCredential } from "../../../src/credential.ts"; import { expectVulnToBeNodeSecureStandardCompliant, HTTP_CLIENT_HEADERS, @@ -13,6 +14,7 @@ import { } from "../utils.ts"; // CONSTANTS +const kTestCredential = new ApiCredential({ type: "basic", username: "user", password: "pass" }); const kSonatypeOrigin = "https://ossindex.sonatype.org"; const kSonatypeApiPath = "/api/v3/component-report"; const kSonatypeVulnComponent = { @@ -22,7 +24,7 @@ const kSonatypeVulnComponent = { const kFakePackageURL = "pkg:npm/fake-npm-package@3.0.1"; test("SonatypeStrategy definition must return only two keys.", () => { - const definition = SonatypeStrategy(); + const definition = SonatypeStrategy({ credential: kTestCredential }); assert.strictEqual( definition.strategy, @@ -36,7 +38,7 @@ test("SonatypeStrategy definition must return only two keys.", () => { }); test("sonatype strategy: hydratePayloadDependencies", async() => { - const { hydratePayloadDependencies } = SonatypeStrategy(); + const { hydratePayloadDependencies } = SonatypeStrategy({ credential: kTestCredential }); const dependencies = new Map(); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); const mockedHttpClient = mockedHttpAgent.get(kSonatypeOrigin); @@ -81,7 +83,7 @@ test("sonatype strategy: hydratePayloadDependencies", async() => { }); test("sonatype strategy: hydratePayloadDependencies when using NodeSecure standard format", async() => { - const { hydratePayloadDependencies } = SonatypeStrategy(); + const { hydratePayloadDependencies } = SonatypeStrategy({ credential: kTestCredential }); const dependencies = new Map(); const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); const mockedHttpClient = mockedHttpAgent.get(kSonatypeOrigin); @@ -136,7 +138,7 @@ test("sonatype strategy: hydratePayloadDependencies when using NodeSecure standa }); test("sonatype strategy: fetchDataForPackageURLs with coordinates exceeding the ratelimit", async() => { - const { hydratePayloadDependencies } = SonatypeStrategy(); + const { hydratePayloadDependencies } = SonatypeStrategy({ credential: kTestCredential }); const chunkSizeApiLimit = 128; const [mockedHttpAgent, restoreHttpAgent] = setupHttpAgentMock(); const mockedHttpClient = mockedHttpAgent.get(kSonatypeOrigin); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..93b98a1 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "types": ["node"], + "lib": ["DOM", "ES2022", "ES2023", "ES2024", "ESNext"] + }, + "include": [ + "." + ] +}