import type { Osv, OsvOffline } from '@renovatebot/osv-offline';
import { codeBlock } from 'common-tags';
import { mockFn } from 'vitest-mock-extended';
import { getConfig } from '../../../config/defaults';
import type { PackageFile } from '../../../modules/manager/types';
import { Vulnerabilities } from './vulnerabilities';
import { logger } from '~test/util';
import type { RenovateConfig } from '~test/util';

const getVulnerabilitiesMock =
  mockFn<typeof OsvOffline.prototype.getVulnerabilities>();
const createMock = vi.fn();

vi.mock('@renovatebot/osv-offline', () => {
  return {
    __esModule: true,
    OsvOffline: class {
      static create() {
        return createMock();
      }
    },
  };
});

describe('workers/repository/process/vulnerabilities', () => {
  describe('create()', () => {
    it('works', async () => {
      await expect(Vulnerabilities.create()).resolves.not.toThrow();
    });

    it('throws when osv-offline error', async () => {
      createMock.mockRejectedValue(new Error());

      await expect(Vulnerabilities.create()).rejects.toThrow();
    });
  });

  describe('fetchVulnerabilities()', () => {
    let config: RenovateConfig;
    let vulnerabilities: Vulnerabilities;

    beforeAll(async () => {
      createMock.mockResolvedValue({
        getVulnerabilities: getVulnerabilitiesMock,
      });
      vulnerabilities = await Vulnerabilities.create();
    });

    beforeEach(() => {
      config = getConfig();
      config.packageRules = [];
    });

    it('return list of Vulnerabilities', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        poetry: [
          {
            deps: [
              { depName: 'django', currentValue: '3.2', datasource: 'pypi' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-qrw5-5h28-modded',
          modified: '',
          affected: [
            {
              package: {
                name: 'django',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/django',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '3.0' }, { fixed: '3.3.8' }],
                },
              ],
            },
            {
              package: {
                name: 'django',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/django',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '3.2' }, { fixed: '3.2.16' }],
                },
              ],
            },
          ],
        },
      ]);

      const vulnerabilityList = await vulnerabilities.fetchVulnerabilities(
        config,
        packageFiles,
      );
      expect(vulnerabilityList).toMatchObject([
        {
          packageName: 'django',
          depVersion: '3.2',
          fixedVersion: '>= 3.3.8',
          datasource: 'pypi',
        },
        {
          packageName: 'django',
          depVersion: '3.2',
          fixedVersion: '>= 3.2.16',
          datasource: 'pypi',
        },
      ]);
    });
  });

  describe('appendVulnerabilityPackageRules()', () => {
    let config: RenovateConfig;
    let vulnerabilities: Vulnerabilities;
    const lodashVulnerability: Osv.Vulnerability = {
      id: 'GHSA-x5rq-j2xg-h7qm',
      modified: '',
      affected: [
        {
          ranges: [
            {
              type: 'SEMVER',
              events: [{ introduced: '0.0.0' }, { fixed: '4.17.11' }],
            },
          ],
          package: { name: 'lodash', ecosystem: 'npm' },
        },
      ],
      references: [
        {
          type: 'ADVISORY',
          url: 'https://nvd.nist.gov/vuln/detail/CVE-2019-1010266',
        },
      ],
    };

    beforeAll(async () => {
      createMock.mockResolvedValue({
        getVulnerabilities: getVulnerabilitiesMock,
      });
      vulnerabilities = await Vulnerabilities.create();
    });

    beforeEach(() => {
      config = getConfig();
      config.packageRules = [];
    });

    it('unsupported datasource', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        dockerfile: [
          {
            deps: [{ depName: 'node', datasource: 'docker' }],
            packageFile: 'some-file',
          },
        ],
      };

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.trace).toHaveBeenCalledWith(
        'Cannot map datasource docker to OSV ecosystem',
      );
    });

    it('package found but no vulnerabilities', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [{ depName: 'lodash', datasource: 'npm' }],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.trace).toHaveBeenCalledWith(
        'No vulnerabilities found in OSV database for lodash',
      );
    });

    it('vulnerability without affected field', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'lodash', currentValue: '4.17.11', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-p6mc-m468-83gw',
          modified: '',
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toHaveLength(0);
    });

    it('withdrawn vulnerability', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'lodash', currentValue: '4.17.10', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          ...lodashVulnerability,
          withdrawn: '2021-11-29T18:17:00Z',
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.trace).toHaveBeenCalledWith(
        'Skipping withdrawn vulnerability GHSA-x5rq-j2xg-h7qm',
      );
      expect(config.packageRules).toHaveLength(0);
    });

    it('invalid dep version', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              {
                depName: 'lodash',
                currentValue: '#4.17.11',
                datasource: 'npm',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([lodashVulnerability]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.debug).toHaveBeenCalledWith(
        'Skipping vulnerability lookup for package lodash due to unsupported version #4.17.11',
      );
    });

    it('exception while fetching vulnerabilities', async () => {
      const err = new Error('unknown');
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            packageFile: 'some-file',
            deps: [
              {
                depName: 'lodash',
                currentValue: '4.17.11',
                datasource: 'npm',
              },
            ],
          },
        ],
      };
      getVulnerabilitiesMock.mockRejectedValueOnce(err);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.warn).toHaveBeenCalledWith(
        { err, packageName: 'lodash' },
        'Error fetching vulnerability information for package',
      );
    });

    it('log event with invalid version', async () => {
      const event = { fixed: '^6.0' };
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              {
                depName: 'lodash',
                currentValue: '4.17.11',
                datasource: 'npm',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-xxxx-yyyy-zzzz',
          modified: '',
          affected: [
            {
              package: {
                name: 'lodash',
                ecosystem: 'npm',
                purl: 'pkg:npm/lodash',
              },
              ranges: [
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0' }, event],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.debug).toHaveBeenCalledWith(
        { event },
        'Skipping OSV event with invalid version',
      );
    });

    it('no version or range affected', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'fake', currentValue: '4.17.11', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-xxxx-yyyy-zzzz',
          modified: '',
          affected: [
            {
              package: { name: 'fake', ecosystem: 'npm', purl: 'pkg:npm/fake' },
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toHaveLength(0);
    });

    it('no fixed version available', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'fake', currentValue: '4.17.11', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-xxxx-yyyy-zzzz',
          modified: '',
          affected: [
            {
              package: { name: 'fake', ecosystem: 'npm', purl: 'pkg:npm/fake' },
              versions: ['4.17.11'],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.debug).toHaveBeenCalledWith(
        'No fixed version available for vulnerability GHSA-xxxx-yyyy-zzzz in fake 4.17.11',
      );
    });

    it('does not accidentally downgrade versions due to fixed version for other range', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'fake', currentValue: '1.5.1', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-xxxx-yyyy-zzzz',
          modified: '',
          affected: [
            {
              ranges: [
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0' }, { fixed: '1.1.0' }],
                },
                {
                  type: 'SEMVER',
                  events: [{ introduced: '1.3.0' }],
                },
              ],
              package: { name: 'fake', ecosystem: 'npm' },
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.debug).toHaveBeenCalledWith(
        'No fixed version available for vulnerability GHSA-xxxx-yyyy-zzzz in fake 1.5.1',
      );
    });

    it('vulnerability with multiple unsorted events', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        gomod: [
          {
            deps: [
              { depName: 'stdlib', currentValue: '1.7.5', datasource: 'go' },
            ],
            packageFile: 'some-file',
          },
        ],
      };

      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GO-2022-0187',
          modified: '',
          aliases: ['CVE-2017-8932'],
          affected: [
            {
              package: {
                name: 'stdlib',
                ecosystem: 'Go',
                purl: 'pkg:golang/stdlib',
              },
              ranges: [
                {
                  type: 'SEMVER',
                  events: [
                    { introduced: '1.6.0' },
                    { fixed: '1.8.5' },
                    { introduced: '1.8.3' },
                    { fixed: '1.7.6' },
                  ],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.debug).toHaveBeenCalledWith(
        'Vulnerability GO-2022-0187 affects stdlib 1.7.5',
      );
      expect(logger.logger.debug).toHaveBeenCalledWith(
        'Setting allowed version >= 1.7.6 to fix vulnerability GO-2022-0187 in stdlib 1.7.5',
      );
      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['go'],
          matchPackageNames: ['stdlib'],
          matchCurrentVersion: '1.7.5',
          allowedVersions: '>= 1.7.6',
          isVulnerabilityAlert: true,
        },
      ]);
    });

    it('vulnerability with multiple affected entries and version ranges', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        poetry: [
          {
            deps: [
              { depName: 'django', currentValue: '3.2', datasource: 'pypi' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-qrw5-5h28-modded',
          modified: '',
          affected: [
            {
              package: {
                name: 'django',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/django',
              },
              ranges: [
                {
                  type: 'GIT',
                  repo: 'https://github.com/django/django',
                  events: [
                    { introduced: '0' },
                    { fixed: '5b6b257fa7ec37ff27965358800c67e2dd11c924' },
                  ],
                },
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '3.2' }, { fixed: '3.2.16' }],
                },
              ],
              versions: ['3.2.1', '3.2.10', '3.2.9'],
            },
            {
              package: {
                name: 'django',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/django',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '4.0' }, { fixed: '4.0.8' }],
                },
              ],
              versions: ['4.0', '4.0.1', '4.0.6', '4.0.7'],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['pypi'],
          matchPackageNames: ['django'],
          matchCurrentVersion: '3.2',
          allowedVersions: '>= 3.2.16',
          isVulnerabilityAlert: true,
        },
      ]);
    });

    it('package rules are sorted by fixed version even if affected is unsorted', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        poetry: [
          {
            deps: [
              { depName: 'django', currentValue: '3.2', datasource: 'pypi' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-qrw5-5h28-modded',
          modified: '',
          affected: [
            {
              package: {
                name: 'django',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/django',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '3.0' }, { fixed: '3.3.8' }],
                },
              ],
            },
            {
              package: {
                name: 'django',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/django',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '3.2' }, { fixed: '3.2.16' }],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toHaveLength(2);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['pypi'],
          matchPackageNames: ['django'],
          matchCurrentVersion: '3.2',
          allowedVersions: '>= 3.2.16',
          isVulnerabilityAlert: true,
        },
        {
          matchDatasources: ['pypi'],
          matchPackageNames: ['django'],
          matchCurrentVersion: '3.2',
          allowedVersions: '>= 3.3.8',
          isVulnerabilityAlert: true,
        },
      ]);
    });

    it('filters not applicable vulnerability', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'lodash', currentValue: '4.17.11', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([lodashVulnerability]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toHaveLength(0);
    });

    it('returns a single packageRule for regex manager', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        regex: [
          {
            deps: [
              {
                depName: 'tiny_http',
                currentValue: '0.1.2',
                datasource: 'crate',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'RUSTSEC-2020-0031',
          summary:
            'HTTP Request smuggling through malformed Transfer Encoding headers',
          details:
            'HTTP pipelining issues and request smuggling attacks are possible due to incorrect Transfer encoding header parsing.\n\nIt is possible conduct HTTP request smuggling attacks (CL:TE/TE:TE) by sending invalid Transfer Encoding headers.\n\nBy manipulating the HTTP response the attacker could poison a web-cache, perform an XSS attack, or obtain sensitive information from requests other than their own.',
          aliases: ['CVE-2020-35884', 'SOME-1234-5678'],
          modified: '',
          affected: [
            {
              package: {
                name: 'tiny_http',
                ecosystem: 'crates.io',
                purl: 'pkg:cargo/tiny_http',
              },
              ranges: [
                {
                  type: 'SEMVER',
                  events: [
                    { introduced: '0' },
                    { fixed: '0.6.3' },
                    { introduced: '0.7.0-0' },
                    { fixed: '0.8.0' },
                  ],
                },
              ],
            },
          ],
          severity: [
            {
              type: 'CVSS_V2',
              score: 'AV:N/AC:L/Au:N/C:P/I:P/A:N',
            },
            {
              type: 'CVSS_V3',
              score: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N',
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );

      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['crate'],
          matchPackageNames: ['tiny_http'],
          matchCurrentVersion: '0.1.2',
          allowedVersions: '>= 0.6.3',
          isVulnerabilityAlert: true,
          prBodyNotes: [
            '\n\n' +
              codeBlock`
              ---

              ### HTTP Request smuggling through malformed Transfer Encoding headers
              [CVE-2020-35884](https://nvd.nist.gov/vuln/detail/CVE-2020-35884) / [RUSTSEC-2020-0031](https://rustsec.org/advisories/RUSTSEC-2020-0031.html) / SOME-1234-5678

              <details>
              <summary>More information</summary>

              #### Details
              HTTP pipelining issues and request smuggling attacks are possible due to incorrect Transfer encoding header parsing.

              It is possible conduct HTTP request smuggling attacks (CL:TE/TE:TE) by sending invalid Transfer Encoding headers.

              By manipulating the HTTP response the attacker could poison a web-cache, perform an XSS attack, or obtain sensitive information from requests other than their own.

              #### Severity
              - CVSS Score: 6.5 / 10 (Medium)
              - Vector String: \`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:L/I:L/A:N\`

              #### References
              No references.

              This data is provided by [OSV](https://osv.dev/vulnerability/RUSTSEC-2020-0031) and the [Rust Advisory Database](https://github.com/RustSec/advisory-db) ([CC0 1.0](https://github.com/rustsec/advisory-db/blob/main/LICENSE.txt)).
              </details>
            `,
          ],
        },
      ]);
    });

    it('returns multiple packageRules for different vulnerabilities', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'lodash', currentValue: '4.17.10', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        lodashVulnerability,
        {
          id: 'GHSA-p6mc-m468-83gw',
          modified: '',
          affected: [
            {
              ranges: [
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0' }, { fixed: '4.17.20' }],
                },
              ],
              package: { name: 'lodash', ecosystem: 'npm' },
            },
          ],
          severity: [
            {
              type: 'CVSS_V3',
              score: 'CVSS:3.1/AV:N/AC:H/PR:N/UI:N/S:U/C:N/I:H/A:H',
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );

      expect(config.packageRules).toHaveLength(2);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['npm'],
          matchPackageNames: ['lodash'],
          matchCurrentVersion: '4.17.10',
          allowedVersions: '>= 4.17.11',
          isVulnerabilityAlert: true,
        },
        {
          matchDatasources: ['npm'],
          matchPackageNames: ['lodash'],
          matchCurrentVersion: '4.17.10',
          allowedVersions: '>= 4.17.20',
          isVulnerabilityAlert: true,
        },
      ]);
    });

    it('returns packageRules for Hackage', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        hackage: [
          {
            deps: [
              {
                depName: 'aeson',
                currentValue: '0.4.0.0',
                datasource: 'hackage',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'HSEC-2023-0001',
          summary: 'Hash flooding vulnerability in aeson',
          details:
            '# Hash flooding vulnerability in aeson\n\n*aeson* was vulnerable to hash flooding (a.k.a. hash DoS).  The\nissue is a consequence of the HashMap implementation from\n*unordered-containers*.  It results in a denial of service through\nCPU consumption.  This technique has been used in real-world attacks\nagainst a variety of languages, libraries and frameworks over the\nyears.\n',
          aliases: ['CVE-2022-3433'],
          modified: '2023-06-13T09:03:52Z',
          affected: [
            {
              package: {
                ecosystem: 'Hackage',
                name: 'aeson',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '0.4.0.0' }, { fixed: '2.0.1.0' }],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );

      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['hackage'],
          matchPackageNames: ['aeson'],
          matchCurrentVersion: '0.4.0.0',
          allowedVersions: '>= 2.0.1.0',
          isVulnerabilityAlert: true,
        },
      ]);
    });

    it('filters not applicable vulnerability based on last_affected version', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        poetry: [
          {
            deps: [
              { depName: 'quokka', currentValue: '1.2.3', datasource: 'pypi' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-xxxx-yyyy-zzzz',
          modified: '',
          affected: [
            {
              package: {
                name: 'quokka',
                ecosystem: 'PyPI',
                purl: 'pkg:pypi/quokka',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '0' }, { last_affected: '0.4.0' }],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(logger.logger.debug).not.toHaveBeenCalledWith(
        'OSV advisory GHSA-xxxx-yyyy-zzzz lists quokka 1.2.3 as vulnerable',
      );
      expect(config.packageRules).toHaveLength(0);
    });

    it('describe fixed version as ecosystem-specific version constraint', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        maven: [
          {
            deps: [
              {
                depName: 'com.guicedee.services:log4j-core',
                currentValue: '1.0.10.1',
                datasource: 'maven',
              },
            ],
            packageFile: 'some-file1',
          },
        ],
        nuget: [
          {
            deps: [
              {
                depName: 'SharpZipLib',
                currentValue: '1.3.0',
                datasource: 'nuget',
              },
            ],
            packageFile: 'some-file2',
          },
        ],
        npm: [
          {
            deps: [
              {
                depName: 'lodash',
                currentValue: '4.17.15',
                datasource: 'npm',
              },
            ],
            packageFile: 'some-file3',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValue([
        {
          id: 'GHSA-jfh8-c2jp-5v3q',
          modified: '',
          affected: [
            {
              package: {
                name: 'com.guicedee.services:log4j-core',
                ecosystem: 'Maven',
                purl: 'pkg:maven/com.guicedee.services/log4j-core',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '0' }, { fixed: '1.2.1.2-jre17' }],
                },
              ],
            },
          ],
        },
        {
          id: ' GHSA-mm6g-mmq6-53ff',
          modified: '',
          affected: [
            {
              package: {
                name: 'SharpZipLib',
                ecosystem: 'NuGet',
                purl: 'pkg:nuget/SharpZipLib',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '0' }, { fixed: '1.3.3' }],
                },
              ],
            },
          ],
        },
        {
          id: 'GHSA-29mw-wpgm-hmr9',
          modified: '',
          affected: [
            {
              package: {
                name: 'lodash',
                ecosystem: 'npm',
                purl: 'pkg:npm/lodash',
              },
              ranges: [
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0' }, { fixed: '4.17.21' }],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['maven'],
          matchPackageNames: ['com.guicedee.services:log4j-core'],
          matchCurrentVersion: '1.0.10.1',
          allowedVersions: '[1.2.1.2-jre17,)',
        },
        {
          matchDatasources: ['nuget'],
          matchPackageNames: ['SharpZipLib'],
          matchCurrentVersion: '1.3.0',
          allowedVersions: '[1.3.3,)',
        },
        {
          matchDatasources: ['npm'],
          matchPackageNames: ['lodash'],
          matchCurrentVersion: '4.17.15',
          allowedVersions: '>= 4.17.21',
        },
      ]);
    });

    it('describe last_affected version as ecosystem-specific version constraint', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        maven: [
          {
            deps: [
              {
                depName: 'com.guicedee.services:log4j-core',
                currentValue: '1.0.10.1',
                datasource: 'maven',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-jfh8-c2jp-5v3q',
          modified: '',
          affected: [
            {
              package: {
                name: 'com.guicedee.services:log4j-core',
                ecosystem: 'Maven',
                purl: 'pkg:maven/com.guicedee.services/log4j-core',
              },
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [
                    { introduced: '0' },
                    { last_affected: '1.2.1.2-jre17' },
                  ],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['maven'],
          matchPackageNames: ['com.guicedee.services:log4j-core'],
          matchCurrentVersion: '1.0.10.1',
          allowedVersions: '(1.2.1.2-jre17,)',
        },
      ]);
    });

    it('returns packageRule based on last_affected version', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'lodash', currentValue: '0.5.0', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'GHSA-xxxx-yyyy-zzzz',
          modified: '',
          affected: [
            {
              package: {
                name: 'lodash',
                ecosystem: 'npm',
                purl: 'pkg:npm/lodash',
              },
              ranges: [
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0' }, { last_affected: '0.2.0' }],
                },
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0.4.0' }, { last_affected: '0.8.0' }],
                },
              ],
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );
      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['npm'],
          matchPackageNames: ['lodash'],
          matchCurrentVersion: '0.5.0',
          allowedVersions: '> 0.8.0',
          isVulnerabilityAlert: true,
          prBodyNotes: [
            '\n\n' +
              codeBlock`
              ---

              ### [GHSA-xxxx-yyyy-zzzz](https://github.com/advisories/GHSA-xxxx-yyyy-zzzz)

              <details>
              <summary>More information</summary>

              #### Details
              No details.

              #### Severity
              Unknown

              #### References
              No references.

              This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-xxxx-yyyy-zzzz) 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)).
              </details>
            `,
          ],
        },
      ]);
    });

    it('handles invalid CVSS scores gracefully', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        poetry: [
          {
            deps: [
              {
                depName: 'django-mfa2',
                currentValue: '2.5.0',
                datasource: 'pypi',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'PYSEC-2022-303',
          modified: '',
          affected: [
            {
              ranges: [
                {
                  type: 'ECOSYSTEM',
                  events: [{ introduced: '0' }, { fixed: '2.5.1' }],
                },
              ],
              package: { name: 'django-mfa2', ecosystem: 'PyPI' },
            },
          ],
          severity: [
            {
              type: 'CVSS_V3',
              score: 'some-invalid-score',
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );

      expect(logger.logger.debug).toHaveBeenCalledWith(
        'Error processing CVSS vector some-invalid-score',
      );
      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['pypi'],
          matchPackageNames: ['django-mfa2'],
          matchCurrentVersion: '2.5.0',
          allowedVersions: '>= 2.5.1',
          isVulnerabilityAlert: true,
          prBodyNotes: [
            '\n\n' +
              codeBlock`
              ---

              ### PYSEC-2022-303

              <details>
              <summary>More information</summary>

              #### Details
              No details.

              #### Severity
              - CVSS Score: Unknown
              - Vector String: \`some-invalid-score\`

              #### References
              No references.

              This data is provided by [OSV](https://osv.dev/vulnerability/PYSEC-2022-303) and the [PyPI Advisory Database](https://github.com/pypa/advisory-database) ([CC-BY 4.0](https://github.com/pypa/advisory-database/blob/main/LICENSE)).
              </details>
            `,
          ],
        },
      ]);
    });

    it('show severity text in GHSA advisories without CVSS score', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        npm: [
          {
            deps: [
              { depName: 'lodash', currentValue: '4.17.10', datasource: 'npm' },
            ],
            packageFile: 'some-file',
          },
        ],
      };

      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          ...lodashVulnerability,
          database_specific: {
            severity: 'MODERATE',
          },
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );

      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['npm'],
          matchPackageNames: ['lodash'],
          matchCurrentVersion: '4.17.10',
          allowedVersions: '>= 4.17.11',
          isVulnerabilityAlert: true,
          prBodyNotes: [
            '\n\n' +
              codeBlock`
              ---

              ### [GHSA-x5rq-j2xg-h7qm](https://github.com/advisories/GHSA-x5rq-j2xg-h7qm)

              <details>
              <summary>More information</summary>

              #### Details
              No details.

              #### Severity
              Moderate

              #### References
              - [https://nvd.nist.gov/vuln/detail/CVE-2019-1010266](https://nvd.nist.gov/vuln/detail/CVE-2019-1010266)

              This data is provided by [OSV](https://osv.dev/vulnerability/GHSA-x5rq-j2xg-h7qm) 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)).
              </details>
            `,
          ],
        },
      ]);
    });

    it('formats headings of vulnerability details', async () => {
      const packageFiles: Record<string, PackageFile[]> = {
        regex: [
          {
            deps: [
              {
                depName: 'sys-info',
                currentValue: '0.6.0',
                datasource: 'crate',
              },
            ],
            packageFile: 'some-file',
          },
        ],
      };
      getVulnerabilitiesMock.mockResolvedValueOnce([
        {
          id: 'RUSTSEC-2020-0100',
          summary:
            'Double free when calling `sys_info::disk_info` from multiple threads',
          details:
            'Affected versions of `sys-info` use a static, global, list to store temporary disk information while running. The function that cleans up this list,\n`DFCleanup`, assumes a single threaded environment and will try to free the same memory twice in a multithreaded environment.\n\nThis results in consistent double-frees and segfaults when calling `sys_info::disk_info` from multiple threads at once.\n\nThe issue was fixed by moving the global variable into a local scope.\n\n## Safer Alternatives:\n - [`sysinfo`](https://crates.io/crates/sysinfo)',
          aliases: ['CVE-2020-36434'],
          modified: '',
          affected: [
            {
              package: {
                name: 'sys-info',
                ecosystem: 'crates.io',
                purl: 'pkg:cargo/sys-info',
              },
              ranges: [
                {
                  type: 'SEMVER',
                  events: [{ introduced: '0.0.0-0' }, { fixed: '0.8.0' }],
                },
              ],
              database_specific: {
                cvss: 'CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H',
              },
            },
          ],
        },
      ]);

      await vulnerabilities.appendVulnerabilityPackageRules(
        config,
        packageFiles,
      );

      expect(config.packageRules).toHaveLength(1);
      expect(config.packageRules).toMatchObject([
        {
          matchDatasources: ['crate'],
          matchPackageNames: ['sys-info'],
          matchCurrentVersion: '0.6.0',
          allowedVersions: '>= 0.8.0',
          isVulnerabilityAlert: true,
          prBodyNotes: [
            '\n\n' +
              codeBlock`
              ---

              ### Double free when calling \`sys_info::disk_info\` from multiple threads
              [CVE-2020-36434](https://nvd.nist.gov/vuln/detail/CVE-2020-36434) / [RUSTSEC-2020-0100](https://rustsec.org/advisories/RUSTSEC-2020-0100.html)

              <details>
              <summary>More information</summary>

              #### Details
              Affected versions of \`sys-info\` use a static, global, list to store temporary disk information while running. The function that cleans up this list,
              \`DFCleanup\`, assumes a single threaded environment and will try to free the same memory twice in a multithreaded environment.

              This results in consistent double-frees and segfaults when calling \`sys_info::disk_info\` from multiple threads at once.

              The issue was fixed by moving the global variable into a local scope.

              ##### Safer Alternatives:
               - [\`sysinfo\`](https://crates.io/crates/sysinfo)

              #### Severity
              - CVSS Score: 9.8 / 10 (Critical)
              - Vector String: \`CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:H\`

              #### References
              No references.

              This data is provided by [OSV](https://osv.dev/vulnerability/RUSTSEC-2020-0100) and the [Rust Advisory Database](https://github.com/RustSec/advisory-db) ([CC0 1.0](https://github.com/rustsec/advisory-db/blob/main/LICENSE.txt)).
              </details>
            `,
          ],
        },
      ]);
    });
  });
});
