import { getConfig } from '../../../config/defaults';
import { NO_VULNERABILITY_ALERTS } from '../../../constants/error-messages';
import { logger } from '../../../logger';
import type { VulnerabilityAlert } from '../../../types';
import {
  detectVulnerabilityAlerts,
  getFixedVersionByDatasource,
} from './vulnerability';
import { partial, platform } from '~test/util';
import type { RenovateConfig } from '~test/util';

let config: RenovateConfig;

beforeEach(() => {
  config = getConfig();
});

describe('workers/repository/init/vulnerability', () => {
  describe('getFixedVersionByDatasource()', () => {
    it('returns ecosystem-specific version range', () => {
      expect(getFixedVersionByDatasource('1.2.3', 'maven')).toBe('[1.2.3,)');
      expect(getFixedVersionByDatasource('1.2.3', 'nuget')).toBe('[1.2.3,)');
    });

    it('returns default version range', () => {
      expect(getFixedVersionByDatasource('1.2.3', 'npm')).toBe('>= 1.2.3');
    });
  });

  describe('detectVulnerabilityAlerts()', () => {
    it('returns if alerts are missing', async () => {
      delete config.vulnerabilityAlerts;
      expect(await detectVulnerabilityAlerts(config)).toBe(config);
    });

    it('returns if alerts are disabled', async () => {
      // TODO #22198
      config.vulnerabilityAlerts!.enabled = false;
      expect(await detectVulnerabilityAlerts(config)).toEqual(config);
    });

    it('returns if no alerts', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([]);
      expect(await detectVulnerabilityAlerts(config)).toEqual(config);
    });

    it('throws if no alerts and vulnerabilityAlertsOnly', async () => {
      config.vulnerabilityAlertsOnly = true;
      platform.getVulnerabilityAlerts.mockResolvedValue([]);
      await expect(detectVulnerabilityAlerts(config)).rejects.toThrow(
        NO_VULNERABILITY_ALERTS,
      );
    });

    it('ignores alert if dismissed_reason is not nullish', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([
        {
          dismissed_reason: 'some reason',
          dependency: {
            manifest_path: 'package-lock.json',
          },
          security_advisory: {
            description:
              'GitHub Electron 1.7.15, 1.8.7, 2.0.7, and 3.0.0-beta.6, in certain scenarios involving IFRAME elements and "nativeWindowOpen: true" or "sandbox: true" options, is affected by a WebPreferences vulnerability that can be leveraged to perform remote code execution.',
            identifiers: [
              { type: 'GHSA', value: 'GHSA-hv9c-qwqg-qj3v' },
              { type: 'CVE', value: 'CVE-2018-15685' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2018-15685' },
            ],
          },
          security_vulnerability: {
            package: { name: 'electron', ecosystem: 'npm' },
            first_patched_version: { identifier: '1.8.8' },
            vulnerable_version_range: '>= 1.8.0, < 1.8.8',
          },
        },
      ]);
      const res = await detectVulnerabilityAlerts(config);
      expect(res.packageRules).toHaveLength(0);
    });

    it('ignores alert if firstPatchVersion not found', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([
        {
          // will be ignored - no firstPatchVersion
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              'The create_script function in the lxc_container module in Ansible before 1.9.6-1 and 2.x before 2.0.2.0 allows local users to write to arbitrary files or gain privileges via a symlink attack on (1) /opt/.lxc-attach-script, (2) the archived container in the archive_path directory, or the (3) lxc-attach-script.log or (4) lxc-attach-script.err files in the temporary directory.',
            identifiers: [
              { type: 'GHSA', value: 'GHSA-rh6x-qvg7-rrmj' },
              { type: 'CVE', value: 'CVE-2016-3096' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2016-3096' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            vulnerable_version_range: '< 1.9.6.1',
          },
        },
      ]);
      const res = await detectVulnerabilityAlerts(config);
      expect(res.packageRules).toHaveLength(0);
    });

    it('ignores alert if firstPatchVersion is null', async () => {
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([
        {
          // will be ignored - firstPatchVersion is null
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              'The create_script function in the lxc_container module in Ansible before 1.9.6-1 and 2.x before 2.0.2.0 allows local users to write to arbitrary files or gain privileges via a symlink attack on (1) /opt/.lxc-attach-script, (2) the archived container in the archive_path directory, or the (3) lxc-attach-script.log or (4) lxc-attach-script.err files in the temporary directory.',
            identifiers: [
              { type: 'GHSA', value: 'GHSA-rh6x-qvg7-rrmj' },
              { type: 'CVE', value: 'CVE-2016-3096' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2016-3096' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            vulnerable_version_range: '< 1.9.6.1',
            first_patched_version: null,
          },
        },
      ]);
      const res = await detectVulnerabilityAlerts(config);
      expect(res.packageRules).toHaveLength(0);
    });

    it('returns go alerts', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      delete config.packageRules; // coverage
      platform.getVulnerabilityAlerts.mockResolvedValue([
        partial<VulnerabilityAlert>(),
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'go.sum',
          },
          security_advisory: {
            description: 'go',
            identifiers: [{ type: 'GHSA', value: 'abc' }],
            references: [{ url: '' }],
          },
          security_vulnerability: {
            package: { name: 'foo', ecosystem: 'go' },
            first_patched_version: { identifier: '1.8.3' },
            vulnerable_version_range: '>= 1.8, < 1.8.3',
          },
        },
      ]);
      const res = await detectVulnerabilityAlerts(config);
      expect(res.packageRules).toMatchSnapshot();
      expect(res.packageRules).toHaveLength(1);
    });

    // coverage
    it('warns if any error occurs while parsing the alerts', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([
        partial<VulnerabilityAlert>(),
        {
          dismissed_reason: null,
          // @ts-expect-error invalid options
          dependency: {},
          security_advisory: {
            description: 'go',
            identifiers: [{ type: 'GHSA', value: 'abc' }],
            references: [{ url: '' }],
          },
          security_vulnerability: {
            package: { name: 'foo', ecosystem: 'go' },
            first_patched_version: { identifier: '1.8.3' },
            vulnerable_version_range: '>= 1.8, < 1.8.3',
          },
        },
      ]);
      await detectVulnerabilityAlerts(config);
      expect(logger.warn).toHaveBeenCalledWith(
        { err: expect.any(Object) },
        'Error parsing vulnerability alert',
      );
    });

    it('returns maven alerts', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'pom.xml',
          },
          security_advisory: {
            description:
              'An issue was discovered in FasterXML jackson-databind prior to 2.7.9.4, 2.8.11.2, and 2.9.6. When Default Typing is enabled (either globally or for a specific property), the service has the Jodd-db jar (for database access for the Jodd framework) in the classpath, and an attacker can provide an LDAP service to access, it is possible to make the service execute a malicious payload.',
            identifiers: [
              { type: 'GHSA', value: 'GHSA-cjjf-94ff-43w7' },
              { type: 'CVE', value: 'CVE-2018-12022' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2018-12022' },
            ],
          },
          security_vulnerability: {
            package: {
              name: 'com.fasterxml.jackson.core:jackson-databind',
              ecosystem: 'maven',
            },
            first_patched_version: { identifier: '2.7.9.4' },
            vulnerable_version_range: '< 2.7.9.4',
          },
        },
      ]);
      const res = await detectVulnerabilityAlerts(config);
      expect(res.packageRules).toMatchSnapshot();
      expect(res.packageRules).toHaveLength(1);
    });

    it('returns pip alerts', async () => {
      // TODO #22198
      delete config.vulnerabilityAlerts!.enabled;
      platform.getVulnerabilityAlerts.mockResolvedValue([
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              "Ansible before versions 2.3.1.0 and 2.4.0.0 fails to properly mark lookup-plugin results as unsafe. If an attacker could control the results of lookup() calls, they could inject Unicode strings to be parsed by the jinja2 templating system, resulting in code execution. By default, the jinja2 templating language is now marked as 'unsafe' and is not evaluated.",
            identifiers: [
              { type: 'GHSA', value: 'GHSA-w578-j992-554x' },
              { type: 'CVE', value: 'CVE-2017-7481' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2017-7481' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            first_patched_version: { identifier: 'abc-2.3.1.0' },
            vulnerable_version_range: '< 2.3.1.0',
          },
        },
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              "Ansible before 1.9.2 does not verify that the server hostname matches a domain name in the subject's Common Name (CN) or subjectAltName field of the X.509 certificate, which allows man-in-the-middle attackers to spoof SSL servers via an arbitrary valid certificate.",
            identifiers: [
              { type: 'GHSA', value: 'GHSA-w64c-pxjj-h866' },
              { type: 'CVE', value: 'CVE-2015-3908' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2015-3908' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            first_patched_version: { identifier: '1.9.2' },
            vulnerable_version_range: '< 1.9.2',
          },
        },
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              "An input validation vulnerability was found in Ansible's mysql_user module before 2.2.1.0, which may fail to correctly change a password in certain circumstances. Thus the previous password would still be active when it should have been changed.",
            identifiers: [
              { type: 'GHSA', value: 'GHSA-x4cm-m36h-c6qj' },
              { type: 'CVE', value: 'CVE-2016-8647' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2016-8647' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            first_patched_version: { identifier: '2.2.1.0' },
            vulnerable_version_range: '< 2.2.1.0',
          },
        },
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              'A flaw was found in Ansible before version 2.2.0. The apt_key module does not properly verify key fingerprints, allowing remote adversary to create an OpenPGP key which matches the short key ID and inject this key instead of the correct key.',
            identifiers: [
              { type: 'GHSA', value: 'GHSA-cmwx-9m2h-x7v4' },
              { type: 'CVE', value: 'CVE-2016-8614' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2016-8614' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            first_patched_version: { identifier: '2.2.0' },
            vulnerable_version_range: '< 2.2.0',
          },
        },
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              'Ansible before version 2.2.0 fails to properly sanitize fact variables sent from the Ansible controller. An attacker with the ability to create special variables on the controller could execute arbitrary commands on Ansible clients as the user Ansible runs as.',
            identifiers: [
              { type: 'GHSA', value: 'GHSA-jg4f-jqm5-4mgq' },
              { type: 'CVE', value: 'CVE-2016-8628' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2016-8628' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            first_patched_version: { identifier: '2.2.0' },
            vulnerable_version_range: '< 2.2.0',
          },
        },
        {
          dismissed_reason: null,
          dependency: {
            manifest_path: 'requirements.txt',
          },
          security_advisory: {
            description:
              "Ansible before versions 2.1.4, 2.2.1 is vulnerable to an improper input validation in Ansible's handling of data sent from client systems. An attacker with control over a client system being managed by Ansible and the ability to send facts back to the Ansible server could use this flaw to execute arbitrary code on the Ansible server using the Ansible server privileges.",
            identifiers: [
              { type: 'GHSA', value: 'GHSA-m956-frf4-m2wr' },
              { type: 'CVE', value: 'CVE-2016-9587' },
            ],
            references: [
              { url: 'https://nvd.nist.gov/vuln/detail/CVE-2016-9587' },
            ],
          },
          security_vulnerability: {
            package: { name: 'ansible', ecosystem: 'pip' },
            first_patched_version: { identifier: '2.1.4' },
            vulnerable_version_range: '< 2.1.4',
          },
        },
      ]);
      const res = await detectVulnerabilityAlerts(config);
      expect(res.packageRules).toMatchSnapshot();
      expect(res.packageRules).toHaveLength(1);
    });
  });
});
