diff --git a/README.md b/README.md index 387460c..2fa9aea 100644 --- a/README.md +++ b/README.md @@ -125,45 +125,9 @@ Where `dependencies` is the dependencies **Map()** object of the NodeSecure Scan > [!NOTE] > the option **hydrateDatabase** is only useful for some of the strategy (like Node.js Security WG). -### Standard vulnerability format -We provide an high level format that work for all available strategy. It can be activated with the option `useFormat`. - -```ts -export interface StandardVulnerability { - /** Unique identifier for the vulnerability **/ - id?: string; - /** Vulnerability origin, either Snyk, Sonatype, GitHub or NodeSWG **/ - origin: Origin; - /** Package associated with the vulnerability **/ - package: string; - /** Vulnerability title **/ - title: string; - /** Vulnerability description **/ - description?: string; - /** Vulnerability link references on origin's website **/ - url?: string; - /** Vulnerability severity levels given the strategy **/ - severity?: Severity; - /** Common Vulnerabilities and Exposures dictionary */ - cves?: string[]; - /** - * Common Vulnerability Scoring System (CVSS) provides a way to capture - * the principal characteristics of a vulnerability, - * and produce a numerical score reflecting its severity, - * as well as a textual representation of that score. **/ - cvssVector?: string; - /** CVSS Score **/ - cvssScore?: number; - /** The range of vulnerable versions provided when too many versions are vulnerables */ - vulnerableRanges: string[]; - /** The set of versions that are vulnerable **/ - vulnerableVersions: string[]; - /** The set of versions that are patched **/ - patchedVersions?: string; - /** Overview of available patches to get rid of listed vulnerabilities **/ - patches?: Patch[]; -} -``` +### Formats +- [Standard](./docs/formats/standard.md) +- [OSV](./docs/formats/osv.md) ### Databases - [OSV](./docs/database/osv.md) diff --git a/docs/formats/osv.md b/docs/formats/osv.md new file mode 100644 index 0000000..6b1fb79 --- /dev/null +++ b/docs/formats/osv.md @@ -0,0 +1,82 @@ +# OSV vulnerability format + +See [Open Source Vulnerability format](https://ossf.github.io/osv-schema/) + +```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; +} + +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/docs/formats/standard.md b/docs/formats/standard.md new file mode 100644 index 0000000..dd6a051 --- /dev/null +++ b/docs/formats/standard.md @@ -0,0 +1,40 @@ +# Standard vulnerability format + +We provide an high level format that work for all available strategy. It can be activated with the option `useFormat` equal `Standard`. + +```ts +export interface StandardVulnerability { + /** Unique identifier for the vulnerability **/ + id?: string; + /** Vulnerability origin, either Snyk, Sonatype, GitHub or NodeSWG **/ + origin: Origin; + /** Package associated with the vulnerability **/ + package: string; + /** Vulnerability title **/ + title: string; + /** Vulnerability description **/ + description?: string; + /** Vulnerability link references on origin's website **/ + url?: string; + /** Vulnerability severity levels given the strategy **/ + severity?: Severity; + /** Common Vulnerabilities and Exposures dictionary */ + cves?: string[]; + /** + * Common Vulnerability Scoring System (CVSS) provides a way to capture + * the principal characteristics of a vulnerability, + * and produce a numerical score reflecting its severity, + * as well as a textual representation of that score. **/ + cvssVector?: string; + /** CVSS Score **/ + cvssScore?: number; + /** The range of vulnerable versions provided when too many versions are vulnerables */ + vulnerableRanges: string[]; + /** The set of versions that are vulnerable **/ + vulnerableVersions: string[]; + /** The set of versions that are patched **/ + patchedVersions?: string; + /** Overview of available patches to get rid of listed vulnerabilities **/ + patches?: Patch[]; +} +``` diff --git a/src/formats/index.ts b/src/formats/index.ts index 5353c42..3cfb6b0 100644 --- a/src/formats/index.ts +++ b/src/formats/index.ts @@ -6,11 +6,16 @@ import { StandardizeKind } from "./standard/index.js"; +import { + osvVulnerabilityMapper, + OSVKind +} from "./osv/index.js"; + export function formatVulnsPayload( format: BaseStrategyFormat | null = null ) { return function formatVulnerabilities( - strategy: StandardizeKind, + strategy: StandardizeKind | OSVKind, vulnerabilities: any[] ) { if (format === "Standard") { @@ -20,7 +25,10 @@ export function formatVulnsPayload( ); } if (format === "OSV") { - throw new Error("Not Implemented Yet"); + return osvVulnerabilityMapper( + strategy, + vulnerabilities + ); } // identity function diff --git a/src/formats/osv/index.ts b/src/formats/osv/index.ts index 78bd28b..3c37741 100644 --- a/src/formats/osv/index.ts +++ b/src/formats/osv/index.ts @@ -1,15 +1,17 @@ +// Import Internal Dependencies +import { OSV_VULN_MAPPERS } from "./mappers.js"; /** * @see https://ossf.github.io/osv-schema/ */ export interface OSV { - schema_version: string; + schema_version?: string; id: string; modified: string; published: string; - withdraw: string; + withdraw?: string; aliases: string[]; - related: string[]; + related?: string[]; summary: string; details: string; severity: OSVSeverity[]; @@ -78,3 +80,16 @@ export interface OSVSeverity { type: string; score: string; } + +export type OSVKind = keyof typeof OSV_VULN_MAPPERS; + +export function osvVulnerabilityMapper( + strategy: OSVKind, + vulnerabilities: any[] +): OSV[] { + if (!(strategy in OSV_VULN_MAPPERS)) { + return []; + } + + return vulnerabilities.map(OSV_VULN_MAPPERS[strategy]); +} diff --git a/src/formats/osv/mappers.ts b/src/formats/osv/mappers.ts new file mode 100644 index 0000000..f8cfc82 --- /dev/null +++ b/src/formats/osv/mappers.ts @@ -0,0 +1,111 @@ +// Import Internal Dependencies +import { VULN_MODE } from "../../constants.js"; +import * as utils from "../../utils.js"; + +import type { OSV } from "./index.js"; +import type { + SonatypeVulnerability, + SnykVulnerability, + NpmAuditAdvisory, + PnpmAuditAdvisory +} from "../../index.js"; + +function mapFromNPM( + vuln: NpmAuditAdvisory +): OSV { + const hasCVSS = typeof vuln.cvss !== "undefined"; + + return { + id: String(vuln.source), + references: [ + { + type: "ADVISORY", + url: vuln.url + } + ], + package: vuln.name, + title: vuln.title, + severity: utils.standardizeNpmSeverity(vuln.severity), + vulnerableRanges: utils.fromMaybeStringToArray(vuln.range), + vulnerableVersions: utils.fromMaybeStringToArray(vuln.vulnerableVersions), + ...(hasCVSS ? + { cvssScore: vuln.cvss!.score, cvssVector: vuln.cvss!.vectorString } : + {} + ) + }; +} + +function mapFromPnpm( + vuln: PnpmAuditAdvisory +): OSV { + const hasCVSS = typeof vuln.cvss !== "undefined"; + + return { + id: String(vuln.id), + origin: VULN_MODE.GITHUB_ADVISORY, + package: vuln.module_name, + title: vuln.title, + description: vuln.overview, + url: vuln.url, + severity: utils.standardizeNpmSeverity(vuln.severity), + cves: vuln.cves, + patchedVersions: vuln.patched_versions, + vulnerableRanges: [], + vulnerableVersions: utils.fromMaybeStringToArray(vuln.vulnerable_versions), + ...(hasCVSS ? + { cvssScore: vuln.cvss.score, cvssVector: vuln.cvss.vectorString } : + {} + ) + }; +} + +function mapFromSnyk( + vuln: SnykVulnerability +): OSV { + function concatVulnerableVersions(vulnFunctions) { + return vulnFunctions + .reduce((ranges, functions) => [...ranges, ...functions.version], []); + } + + return { + id: vuln.id, + origin: VULN_MODE.SNYK, + package: vuln.package, + title: vuln.title, + url: vuln.url, + description: vuln.description, + severity: vuln.severity, + vulnerableVersions: concatVulnerableVersions(vuln.functions), + vulnerableRanges: vuln.semver.vulnerable, + cves: vuln.identifiers.CVE, + cvssVector: vuln.CVSSv3, + cvssScore: vuln.cvssScore, + patches: vuln.patches + }; +} + +function mapFromSonatype( + vuln: SonatypeVulnerability +): OSV { + return { + id: vuln.id, + origin: VULN_MODE.SONATYPE, + package: vuln.package, + title: vuln.title, + url: vuln.reference, + description: vuln.description, + vulnerableRanges: vuln.versionRanges ?? [], + vulnerableVersions: vuln.versionRanges ?? [], + cves: utils.fromMaybeStringToArray(vuln.cve), + cvssVector: vuln.cvssVector, + cvssScore: vuln.cvssScore + }; +} + +export const OSV_VULN_MAPPERS = Object.freeze({ + [VULN_MODE.GITHUB_ADVISORY]: mapFromNPM, + "github-advisory_pnpm": mapFromPnpm, + [VULN_MODE.SNYK]: mapFromSnyk, + [VULN_MODE.SONATYPE]: mapFromSonatype +}); +