Skip to content
Snippets Groups Projects
Select Git revision
  • 26337ac124f4dd1885a44ed65822f2944ad69f3d
  • master default
2 results

vulnerabilities.spec.ts

Blame
  • vulnerabilities.spec.ts 39.62 KiB
    import type { Osv, OsvOffline } from '@renovatebot/osv-offline';
    import { codeBlock } from 'common-tags';
    import { mockFn } from 'jest-mock-extended';
    import { RenovateConfig, logger } from '../../../../test/util';
    import { getConfig } from '../../../config/defaults';
    import type { PackageFile } from '../../../modules/manager/types';
    import { Vulnerabilities } from './vulnerabilities';
    
    const getVulnerabilitiesMock =
      mockFn<typeof OsvOffline.prototype.getVulnerabilities>();
    const createMock = jest.fn();
    
    jest.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 },
            'Error fetching vulnerability information for lodash',
          );
        });
    
        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.info).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.info).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('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>
                `,
              ],
            },
          ]);
        });
      });
    });