"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
    var ownKeys = function(o) {
        ownKeys = Object.getOwnPropertyNames || function (o) {
            var ar = [];
            for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
            return ar;
        };
        return ownKeys(o);
    };
    return function (mod) {
        if (mod && mod.__esModule) return mod;
        var result = {};
        if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
        __setModuleDefault(result, mod);
        return result;
    };
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateRepository = validateRepository;
exports.asbowerrepo = asbowerrepo;
exports.loadrepository = loadrepository;
exports.loadrepositoryFromFile = loadrepositoryFromFile;
const fs = __importStar(require("fs"));
const path = __importStar(require("path"));
const http = __importStar(require("http"));
const https = __importStar(require("https"));
const retire = __importStar(require("./retire"));
const URL = __importStar(require("url"));
const proxy_agent_1 = require("proxy-agent");
const z = __importStar(require("zod"));
const types_1 = require("./types");
function validateRepository(repo, replacer) {
    const keys = Object.keys(types_1.severityLevels);
    const versionValidator = z.string().regex(/^[\d.]+([a-zA-Z\d.-]+)?$/);
    const numericString = z.string().regex(/^[\d]+$/);
    const vulnValidator = z
        .object({
        below: versionValidator,
        atOrAbove: versionValidator.optional(),
        excludes: z.array(versionValidator).optional(),
        severity: z.enum(keys),
        cwe: z.array(z.string().regex(/^CWE-[0-9]+$/)).min(1),
        identifiers: z
            .object({
            CVE: z.array(z.string().regex(/^CVE-[0-9X-]+$/)).optional(),
            bug: z
                .string()
                .regex(/^[a-z0-9-]+$/i)
                .optional(),
            issue: numericString.optional(),
            summary: z.string().min(3).optional(),
            githubID: z
                .string()
                .regex(/^GHSA[A-Z0-9-]+$/i)
                .optional(),
            osvdb: z.array(numericString).optional(),
            gist: z
                .string()
                .regex(/^[a-z0-9-]+\/[a-f0-9]+$/i)
                .optional(),
            tenable: numericString.optional(),
            blog: z
                .string()
                .min(10)
                .regex(/^[:a-z0-9/-]+$/)
                .optional(),
            release: z.string().min(5).optional(),
            PR: numericString.optional(),
            retid: z
                .string()
                .regex(/^[\d]+$/)
                .optional(),
        })
            .strict()
            .superRefine((o, ctx) => {
            if (Object.keys(o).filter((k) => k != 'summary').length == 0)
                ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must have at least one identifier' });
            const ids = Object.values(o)
                .map((x) => (Array.isArray(x) ? x : [x]))
                .reduce((a, b) => a.concat(b), []).length;
            if (ids == 0)
                ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Must have at least one identifier' });
        }),
        info: z.array(z.string().regex(/^https?:\/\/.+/)),
    })
        .strict();
    const regexValidator = z.string().superRefine((s, ctx) => {
        if (ctx.path[0] == 'dont check')
            return;
        try {
            new RegExp(s);
        }
        catch (error) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: 'Invalid regex: ' + s,
            });
        }
        if (s.includes('[]'))
            ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must not contain []: ' + s });
        if (s.includes('{}'))
            ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must not contain {}: ' + s });
        [/.*[^\\]\{[^0-9,\\]\}.*/, /[^,0-9\\]\}/, /[^\\]\{[^,0-9]/].forEach((r) => {
            if (r.test(s))
                ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'There is something odd with this regex: ' + s });
        });
        let versionMatcher = '§§version§§';
        if (replacer)
            versionMatcher = JSON.parse(`"${replacer(versionMatcher)}"`);
        const versionIndex = s.indexOf(versionMatcher);
        if (versionIndex == -1 || (versionIndex > 0 && s.substring(versionIndex - 1, versionIndex) == '\\')) {
            ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Regex must contain (§§version§§): ' + s });
        }
        else if (s.replace(/\(\?:/g, '').replace(/\\\(/g, '').split(/\(/)[1].indexOf(versionMatcher) != 0) {
            ctx.addIssue({
                code: z.ZodIssueCode.custom,
                message: 'Regex must contain (§§version§§) as first capture group: ' + s.replace(/\(\?:/g, '').replace(/\\\(/g, ''),
            });
        }
    });
    const replaceValidator = z
        .string()
        .regex(/^\/(.*[^\\])\/([^/]+)\/$/, 'RegExp error - should be on format "/search/replacement/"');
    const validator = z.record(z
        .object({
        bowername: z.array(z.string().regex(/^[a-z0-9.-]+$/i)).optional(),
        basePurl: z
            .string()
            .regex(/^pkg:[a-z0-9%.-/]+$/i)
            .optional(),
        npmname: z
            .string()
            .regex(/^[a-z0-9.-]+$/i)
            .optional(),
        vulnerabilities: z.array(vulnValidator),
        extractors: z
            .object({
            func: z.array(z.string().min(5)).optional(),
            uri: z.array(regexValidator).optional(),
            filename: z.array(regexValidator).optional(),
            filecontent: z.array(regexValidator).optional(),
            filecontentreplace: z.array(replaceValidator).optional(),
            hashes: z.record(z.string().regex(/^[a-f0-9]+$/i), versionValidator).optional(),
            ast: z.array(z.string()).optional(),
        })
            .strict(),
        licenses: z
            .array(z
            .string()
            .regex(/(\([A-Za-z\-0-9.]+( OR [A-Za-z\-0-9.]+)+\)|[A-Za-z\-0-9.]+) >=[0-9.]+( <[0-9.]+)?(;>=[0-9.]+( <[0-9.]+)?)*/))
            .optional(),
    })
        .strict());
    return validator.safeParse(repo);
}
function formatValidationError(error) {
    return JSON.stringify(error.format(), (key, value) => (Array.isArray(value) && value.length === 0 ? undefined : value), 2);
}
async function loadJson(url, options) {
    return new Promise((resolve, reject) => {
        options.log.info('Downloading ' + url + ' ...');
        const reqOptions = { ...URL.parse(url), method: 'GET' };
        const proxyUri = options.proxy || process.env.http_proxy;
        if (proxyUri) {
            reqOptions.agent = new proxy_agent_1.ProxyAgent({
                getProxyForUrl: () => proxyUri,
            });
        }
        if (options.insecure) {
            reqOptions.rejectUnauthorized = false;
        }
        if (options.cacertbuf) {
            reqOptions.ca = [options.cacertbuf];
        }
        const req = (url.startsWith('http:') ? http : https).get(reqOptions, (res) => {
            if (res.statusCode != 200)
                return reject(`Error downloading: ${url}: HTTP ${res.statusCode} ${res.statusMessage}`);
            const data = [];
            res.on('data', (c) => data.push(c));
            res.on('end', () => {
                let d = Buffer.concat(data).toString();
                d = options.process ? options.process(d) : d;
                const json = JSON.parse(d);
                const vresult = validateRepository(json, options.process);
                if (vresult.success) {
                    resolve(json);
                }
                else {
                    reject(`Invalid repository from ${url}: ${formatValidationError(vresult.error)}`);
                }
            });
        });
        req.on('error', (e) => reject(`Error downloading: ${url}: ${e}`));
        req.end();
    });
}
async function loadJsonFromFile(file, options) {
    options.log.debug('Reading ' + file + ' ...');
    return new Promise((resolve, reject) => {
        fs.readFile(file, { encoding: 'utf8' }, (err, data) => {
            if (err) {
                return reject(err.toString());
            }
            data = options.process ? options.process(data) : data;
            const json = JSON.parse(data);
            const vresult = validateRepository(json, options.process);
            if (vresult.success) {
                resolve(json);
            }
            else {
                reject(`Invalid repository from ${file}: ${formatValidationError(vresult.error)}`);
            }
        });
    });
}
async function loadFromCache(url, cachedir, options) {
    const cacheIndex = path.resolve(cachedir, 'index.json');
    if (!fs.existsSync(cachedir))
        fs.mkdirSync(cachedir);
    const cache = fs.existsSync(cacheIndex) ? JSON.parse(fs.readFileSync(cacheIndex, 'utf-8')) : {};
    const now = new Date().getTime();
    if (cache[url]) {
        if (now - cache[url].date < 60 * 60 * 1000) {
            options.log.info('Loading from cache: ' + url);
            return loadJsonFromFile(path.resolve(cachedir, cache[url].file), options);
        }
        else {
            if (fs.existsSync(path.resolve(cachedir, cache[url].date + '.json'))) {
                try {
                    fs.unlinkSync(path.resolve(cachedir, cache[url].date + '.json'));
                }
                catch (error) {
                    if (error != null && typeof error == 'object' && 'code' in error && error.code !== 'ENOENT') {
                        throw error;
                    }
                    else {
                        console.warn('Could not delete cache. Ignore this error if you are running multiple retire.js in parallel');
                    }
                }
            }
        }
    }
    const data = await loadJson(url, options);
    cache[url] = { date: now, file: now + '.json' };
    fs.writeFileSync(path.resolve(cachedir, cache[url].file), JSON.stringify(data), { encoding: 'utf8' });
    fs.writeFileSync(cacheIndex, JSON.stringify(cache), { encoding: 'utf8' });
    return data;
}
function asbowerrepo(jsRepo) {
    const result = {};
    Object.keys(jsRepo).map((k) => {
        (jsRepo[k].bowername || [k]).map((b) => {
            result[b] = result[b] || { vulnerabilities: [] };
            result[b].vulnerabilities = result[b].vulnerabilities.concat(jsRepo[k].vulnerabilities);
        });
    });
    return result;
}
async function loadrepository(repoUrl, options) {
    options = { ...options, process: retire.replaceVersion };
    if (options.nocache) {
        return await loadJson(repoUrl, options);
    }
    return await loadFromCache(repoUrl, options.cachedir, options);
}
async function loadrepositoryFromFile(filepath, options) {
    options = { ...options, process: retire.replaceVersion };
    return await loadJsonFromFile(filepath, options);
}
