"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Vulnerabilities = void 0;
const tslib_1 = require("tslib");
// TODO #22198
const osv_offline_1 = require("@renovatebot/osv-offline");
const is_1 = tslib_1.__importDefault(require("@sindresorhus/is"));
const vuln_vects_1 = require("vuln-vects");
const config_1 = require("../../../config");
const logger_1 = require("../../../logger");
const common_1 = require("../../../modules/datasource/common");
const versioning_1 = require("../../../modules/versioning");
const check_token_1 = require("../../../util/check-token");
const host_rules_1 = require("../../../util/host-rules");
const markdown_1 = require("../../../util/markdown");
const p = tslib_1.__importStar(require("../../../util/promises"));
const regex_1 = require("../../../util/regex");
const string_1 = require("../../../util/string");
class Vulnerabilities {
    osvOffline;
    static datasourceEcosystemMap = {
        crate: 'crates.io',
        go: 'Go',
        hex: 'Hex',
        maven: 'Maven',
        npm: 'npm',
        nuget: 'NuGet',
        packagist: 'Packagist',
        pypi: 'PyPI',
        rubygems: 'RubyGems',
    };
    constructor() { }
    async initialize() {
        // hard-coded logic to use authentication for github.com based on the githubToken for api.github.com
        const token = (0, check_token_1.findGithubToken)((0, host_rules_1.find)({
            hostType: 'github',
            url: 'https://api.github.com/',
        }));
        this.osvOffline = await osv_offline_1.OsvOffline.create(token);
    }
    static async create() {
        const instance = new Vulnerabilities();
        await instance.initialize();
        return instance;
    }
    async appendVulnerabilityPackageRules(config, packageFiles) {
        const dependencyVulnerabilities = await this.fetchDependencyVulnerabilities(config, packageFiles);
        config.packageRules ??= [];
        for (const { vulnerabilities, versioningApi, } of dependencyVulnerabilities) {
            const groupPackageRules = [];
            for (const vulnerability of vulnerabilities) {
                const rule = this.vulnerabilityToPackageRules(vulnerability);
                if (is_1.default.nullOrUndefined(rule)) {
                    continue;
                }
                groupPackageRules.push(rule);
            }
            this.sortByFixedVersion(groupPackageRules, versioningApi);
            config.packageRules.push(...groupPackageRules);
        }
    }
    async fetchVulnerabilities(config, packageFiles) {
        const groups = await this.fetchDependencyVulnerabilities(config, packageFiles);
        return groups.flatMap((group) => group.vulnerabilities);
    }
    async fetchDependencyVulnerabilities(config, packageFiles) {
        const managers = Object.keys(packageFiles);
        const allManagerJobs = managers.map((manager) => this.fetchManagerVulnerabilities(config, packageFiles, manager));
        return (await Promise.all(allManagerJobs)).flat();
    }
    async fetchManagerVulnerabilities(config, packageFiles, manager) {
        const managerConfig = (0, config_1.getManagerConfig)(config, manager);
        const queue = packageFiles[manager].map((pFile) => () => this.fetchManagerPackageFileVulnerabilities(managerConfig, pFile));
        logger_1.logger.trace({ manager, queueLength: queue.length }, 'fetchManagerVulnerabilities starting');
        const result = (await p.all(queue)).flat();
        logger_1.logger.trace({ manager }, 'fetchManagerVulnerabilities finished');
        return result;
    }
    async fetchManagerPackageFileVulnerabilities(managerConfig, pFile) {
        const { packageFile } = pFile;
        const packageFileConfig = (0, config_1.mergeChildConfig)(managerConfig, pFile);
        const { manager } = packageFileConfig;
        const queue = pFile.deps.map((dep) => () => this.fetchDependencyVulnerability(packageFileConfig, dep));
        logger_1.logger.trace({ manager, packageFile, queueLength: queue.length }, 'fetchManagerPackageFileVulnerabilities starting with concurrency');
        const result = await p.all(queue);
        logger_1.logger.trace({ packageFile }, 'fetchManagerPackageFileVulnerabilities finished');
        return result.filter(is_1.default.truthy);
    }
    async fetchDependencyVulnerability(packageFileConfig, dep) {
        const ecosystem = Vulnerabilities.datasourceEcosystemMap[dep.datasource];
        if (!ecosystem) {
            logger_1.logger.trace(`Cannot map datasource ${dep.datasource} to OSV ecosystem`);
            return null;
        }
        let packageName = dep.packageName ?? dep.depName;
        if (ecosystem === 'PyPI') {
            // https://peps.python.org/pep-0503/#normalized-names
            packageName = packageName.toLowerCase().replace((0, regex_1.regEx)(/[_.-]+/g), '-');
        }
        try {
            const osvVulnerabilities = await this.osvOffline?.getVulnerabilities(ecosystem, packageName);
            if (is_1.default.nullOrUndefined(osvVulnerabilities) ||
                is_1.default.emptyArray(osvVulnerabilities)) {
                logger_1.logger.trace(`No vulnerabilities found in OSV database for ${packageName}`);
                return null;
            }
            const depVersion = dep.lockedVersion ?? dep.currentVersion ?? dep.currentValue;
            const versioning = dep.versioning ?? (0, common_1.getDefaultVersioning)(dep.datasource);
            const versioningApi = (0, versioning_1.get)(versioning);
            if (!versioningApi.isVersion(depVersion)) {
                logger_1.logger.debug(`Skipping vulnerability lookup for package ${packageName} due to unsupported version ${depVersion}`);
                return null;
            }
            const vulnerabilities = [];
            for (const osvVulnerability of osvVulnerabilities) {
                if (osvVulnerability.withdrawn) {
                    logger_1.logger.trace(`Skipping withdrawn vulnerability ${osvVulnerability.id}`);
                    continue;
                }
                for (const affected of osvVulnerability.affected ?? []) {
                    const isVulnerable = this.isPackageVulnerable(ecosystem, packageName, depVersion, affected, versioningApi);
                    if (!isVulnerable) {
                        continue;
                    }
                    logger_1.logger.debug(`Vulnerability ${osvVulnerability.id} affects ${packageName} ${depVersion}`);
                    const fixedVersion = this.getFixedVersion(ecosystem, depVersion, affected, versioningApi);
                    vulnerabilities.push({
                        packageName,
                        vulnerability: osvVulnerability,
                        affected,
                        depVersion,
                        fixedVersion,
                        datasource: dep.datasource,
                        packageFileConfig,
                    });
                }
            }
            return { vulnerabilities, versioningApi };
        }
        catch (err) {
            logger_1.logger.warn({ err }, `Error fetching vulnerability information for ${packageName}`);
            return null;
        }
    }
    sortByFixedVersion(packageRules, versioningApi) {
        const versionsCleaned = {};
        for (const rule of packageRules) {
            const version = rule.allowedVersions;
            versionsCleaned[version] = version.replace((0, regex_1.regEx)(/[(),=> ]+/g), '');
        }
        packageRules.sort((a, b) => versioningApi.sortVersions(versionsCleaned[a.allowedVersions], versionsCleaned[b.allowedVersions]));
    }
    // https://ossf.github.io/osv-schema/#affectedrangesevents-fields
    sortEvents(events, versioningApi) {
        const sortedCopy = [];
        let zeroEvent = null;
        for (const event of events) {
            if (event.introduced === '0') {
                zeroEvent = event;
            }
            else if (versioningApi.isVersion(Object.values(event)[0])) {
                sortedCopy.push(event);
            }
            else {
                logger_1.logger.debug({ event }, 'Skipping OSV event with invalid version');
            }
        }
        sortedCopy.sort((a, b) => 
        // no pre-processing, as there are only very few values to sort
        versioningApi.sortVersions(Object.values(a)[0], Object.values(b)[0]));
        if (zeroEvent) {
            sortedCopy.unshift(zeroEvent);
        }
        return sortedCopy;
    }
    isPackageAffected(ecosystem, packageName, affected) {
        return (affected.package?.name === packageName &&
            affected.package?.ecosystem === ecosystem);
    }
    includedInVersions(depVersion, affected) {
        return !!affected.versions?.includes(depVersion);
    }
    includedInRanges(depVersion, affected, versioningApi) {
        for (const range of affected.ranges ?? []) {
            if (range.type === 'GIT') {
                continue;
            }
            let vulnerable = false;
            for (const event of this.sortEvents(range.events, versioningApi)) {
                if (is_1.default.nonEmptyString(event.introduced) &&
                    (event.introduced === '0' ||
                        this.isVersionGtOrEq(depVersion, event.introduced, versioningApi))) {
                    vulnerable = true;
                }
                else if (is_1.default.nonEmptyString(event.fixed) &&
                    this.isVersionGtOrEq(depVersion, event.fixed, versioningApi)) {
                    vulnerable = false;
                }
                else if (is_1.default.nonEmptyString(event.last_affected) &&
                    this.isVersionGt(depVersion, event.last_affected, versioningApi)) {
                    vulnerable = false;
                }
            }
            if (vulnerable) {
                return true;
            }
        }
        return false;
    }
    // https://ossf.github.io/osv-schema/#evaluation
    isPackageVulnerable(ecosystem, packageName, depVersion, affected, versioningApi) {
        return (this.isPackageAffected(ecosystem, packageName, affected) &&
            (this.includedInVersions(depVersion, affected) ||
                this.includedInRanges(depVersion, affected, versioningApi)));
    }
    getFixedVersion(ecosystem, depVersion, affected, versioningApi) {
        const fixedVersions = [];
        const lastAffectedVersions = [];
        for (const range of affected.ranges ?? []) {
            if (range.type === 'GIT') {
                continue;
            }
            for (const event of range.events) {
                if (is_1.default.nonEmptyString(event.fixed) &&
                    versioningApi.isVersion(event.fixed)) {
                    fixedVersions.push(event.fixed);
                }
                else if (is_1.default.nonEmptyString(event.last_affected) &&
                    versioningApi.isVersion(event.last_affected)) {
                    lastAffectedVersions.push(event.last_affected);
                }
            }
        }
        fixedVersions.sort((a, b) => versioningApi.sortVersions(a, b));
        const fixedVersion = fixedVersions.find((version) => this.isVersionGt(version, depVersion, versioningApi));
        if (fixedVersion) {
            return ecosystem === 'PyPI' ? `==${fixedVersion}` : fixedVersion;
        }
        lastAffectedVersions.sort((a, b) => versioningApi.sortVersions(a, b));
        const lastAffected = lastAffectedVersions.find((version) => this.isVersionGtOrEq(version, depVersion, versioningApi));
        if (lastAffected) {
            return this.getLastAffectedByEcosystem(lastAffected, ecosystem);
        }
        return null;
    }
    getLastAffectedByEcosystem(lastAffected, ecosystem) {
        if (ecosystem === 'Maven') {
            return `(${lastAffected},)`;
        }
        // crates.io, Go, Hex, npm, RubyGems, PyPI
        return `> ${lastAffected}`;
    }
    isVersionGt(version, other, versioningApi) {
        return (versioningApi.isVersion(version) &&
            versioningApi.isVersion(other) &&
            versioningApi.isGreaterThan(version, other));
    }
    isVersionGtOrEq(version, other, versioningApi) {
        return (versioningApi.isVersion(version) &&
            versioningApi.isVersion(other) &&
            (versioningApi.equals(version, other) ||
                versioningApi.isGreaterThan(version, other)));
    }
    vulnerabilityToPackageRules(vul) {
        const { vulnerability, affected, packageName, depVersion, fixedVersion, datasource, packageFileConfig, } = vul;
        if (is_1.default.nullOrUndefined(fixedVersion)) {
            logger_1.logger.info(`No fixed version available for vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`);
            return null;
        }
        logger_1.logger.debug(`Setting allowed version ${fixedVersion} to fix vulnerability ${vulnerability.id} in ${packageName} ${depVersion}`);
        const severityDetails = this.extractSeverityDetails(vulnerability, affected);
        return {
            matchDatasources: [datasource],
            matchPackageNames: [packageName],
            matchCurrentVersion: depVersion,
            allowedVersions: fixedVersion,
            isVulnerabilityAlert: true,
            vulnerabilitySeverity: severityDetails.severityLevel,
            prBodyNotes: this.generatePrBodyNotes(vulnerability, affected),
            force: {
                ...packageFileConfig.vulnerabilityAlerts,
            },
        };
    }
    evaluateCvssVector(vector) {
        try {
            const parsedCvss = (0, vuln_vects_1.parseCvssVector)(vector);
            const severityLevel = parsedCvss.cvss3OverallSeverityText;
            return [parsedCvss.baseScore.toFixed(1), severityLevel];
        }
        catch (err) {
            logger_1.logger.debug(`Error processing CVSS vector ${vector}`);
        }
        return ['', ''];
    }
    generatePrBodyNotes(vulnerability, affected) {
        let aliases = [vulnerability.id].concat(vulnerability.aliases ?? []).sort();
        aliases = aliases.map((id) => {
            if (id.startsWith('CVE-')) {
                return `[${id}](https://nvd.nist.gov/vuln/detail/${id})`;
            }
            else if (id.startsWith('GHSA-')) {
                return `[${id}](https://github.com/advisories/${id})`;
            }
            else if (id.startsWith('GO-')) {
                return `[${id}](https://pkg.go.dev/vuln/${id})`;
            }
            else if (id.startsWith('RUSTSEC-')) {
                return `[${id}](https://rustsec.org/advisories/${id}.html)`;
            }
            return id;
        });
        let content = '\n\n---\n\n### ';
        content += vulnerability.summary ? `${vulnerability.summary}\n` : '';
        content += `${aliases.join(' / ')}\n`;
        content += `\n<details>\n<summary>More information</summary>\n`;
        const details = vulnerability.details?.replace((0, regex_1.regEx)(/^#{1,4} /gm), '##### ');
        content += `#### Details\n${details ?? 'No details.'}\n`;
        content += '#### Severity\n';
        const severityDetails = this.extractSeverityDetails(vulnerability, affected);
        if (severityDetails.cvssVector) {
            content += `- CVSS Score: ${severityDetails.score}\n`;
            content += `- Vector String: \`${severityDetails.cvssVector}\`\n`;
        }
        else {
            content += `${(0, string_1.titleCase)(severityDetails.severityLevel)}\n`;
        }
        content += `\n#### References\n${vulnerability.references
            ?.map((ref) => {
            return `- [${ref.url}](${ref.url})`;
        })
            .join('\n') ?? 'No references.'}`;
        let attribution = '';
        if (vulnerability.id.startsWith('GHSA-')) {
            attribution = ` and the [GitHub Advisory Database](https://github.com/github/advisory-database) ([CC-BY 4.0](https://github.com/github/advisory-database/blob/main/LICENSE.md))`;
        }
        else if (vulnerability.id.startsWith('GO-')) {
            attribution = ` and the [Go Vulnerability Database](https://github.com/golang/vulndb) ([CC-BY 4.0](https://github.com/golang/vulndb#license))`;
        }
        else if (vulnerability.id.startsWith('PYSEC-')) {
            attribution = ` and the [PyPI Advisory Database](https://github.com/pypa/advisory-database) ([CC-BY 4.0](https://github.com/pypa/advisory-database/blob/main/LICENSE))`;
        }
        else if (vulnerability.id.startsWith('RUSTSEC-')) {
            attribution = ` and the [Rust Advisory Database](https://github.com/RustSec/advisory-db) ([CC0 1.0](https://github.com/rustsec/advisory-db/blob/main/LICENSE.txt))`;
        }
        content += `\n\nThis data is provided by [OSV](https://osv.dev/vulnerability/${vulnerability.id})${attribution}.\n`;
        content += `</details>`;
        return [(0, markdown_1.sanitizeMarkdown)(content)];
    }
    extractSeverityDetails(vulnerability, affected) {
        let severityLevel = 'UNKNOWN';
        let score = 'Unknown';
        const cvssVector = vulnerability.severity?.find((e) => e.type === 'CVSS_V3')?.score ??
            vulnerability.severity?.[0]?.score ??
            affected.database_specific?.cvss; // RUSTSEC
        if (cvssVector) {
            const [baseScore, severity] = this.evaluateCvssVector(cvssVector);
            severityLevel = severity.toUpperCase();
            score = baseScore
                ? `${baseScore} / 10 (${(0, string_1.titleCase)(severityLevel)})`
                : 'Unknown';
        }
        else if (vulnerability.id.startsWith('GHSA-') &&
            vulnerability.database_specific?.severity) {
            const severity = vulnerability.database_specific.severity;
            severityLevel = severity.toUpperCase();
        }
        return {
            cvssVector,
            score,
            severityLevel,
        };
    }
}
exports.Vulnerabilities = Vulnerabilities;
//# sourceMappingURL=vulnerabilities.js.map