# Copyright 2025 Google LLC
#
# Use of this source code is governed by a BSD-style
# license that can be found in the LICENSE file or at
# https://developers.google.com/open-source/licenses/bsd

import json
from absl.testing import parameterized
import jsonpath_ng
from vanir import file_path_utils
from vanir import vulnerability_overwriter
from absl.testing import absltest

_TEST_SPECS_FILE = file_path_utils.get_root_file_path('testdata/test_overwrite_specs.json')

_SPEC_WITH_MISSING_REASON = """
    [{
      "vuln_id": "ASB-A-152496149",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific.severity",
          "value": "Test2"
        }
      ]
    }]
    """
_SPEC_WITH_EMPTY_REASON = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific.severity",
          "value": "Test2"
        }
      ]
    }]
    """

_SPEC_WITH_EMPTY_VULN_ID = """
    [{
      "vuln_id": "",
      "reason": "test",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific.severity",
          "value": "Test2"
        }
      ]
    }]
    """

_SPEC_WITH_MISSING_VULN_ID = """
    [{
      "reason": "test",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific.severity",
          "value": "Test2"
        }
      ]
    }]
    """

_REPLACE_FIELD_EMPTY = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": []
    }]
    """

_REPLACE_FIELD_MISSING = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "test"
    }]
    """

_BAD_JSON_PATH = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": [
        {
          "path": "missingbracket[",
          "value": "Test2"
        }
      ]
    }]
    """

_EMPTY_JSON_PATH = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": [
        {
          "path": "",
          "value": "Test2"
        }
      ]
    }]
    """

_NO_JSON_PATH = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": [
        {
          "value": "Test2"
        }
      ]
    }]
    """

_NO_VALUE_IN_REPLACE = """
    [{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific"
        }
      ]
    }]
    """


_SAMPLE_VULNERABILITY = """
{
    "id": "ASB-A-210498909",
    "modified": "2022-06-01T00:00:00Z",
    "published": "2022-06-01T00:00:00Z",
    "aliases": ["CVE-2018-25020"],
    "summary": "",
    "references": [
      {
        "type": "ADVISORY",
        "url": "https://source.android.com/security/bulletin/2022-06-01#2022-06-05-security-patch-level-vulnerability-details"
      },
      {
        "type": "FIX",
        "url": "https://android.googlesource.com/kernel/common/+/050fad7c4534c13c8eb1d9c2ba66012e014773cb"
      },
      {
        "type": "FIX",
        "url": "https://android.googlesource.com/kernel/common/+/99253eb750fda6a644d5188fb26c43bad8d5a745"
      }
    ],
    "affected": [
      {
        "package": {
          "ecosystem": "Android",
          "name": ":linux_kernel:"
        },
        "ecosystem_specific": {
          "type": "EoP",
          "severity": "Moderate",
          "spl": "2022-06-05",
          "fixes": [
            "https://android.googlesource.com/kernel/common/+/050fad7c4534c13c8eb1d9c2ba66012e014773cb",
            "https://android.googlesource.com/kernel/common/+/99253eb750fda6a644d5188fb26c43bad8d5a745"
          ]
        },
        "ranges": [
          {
            "type": "ECOSYSTEM",
            "events": [
              {
                "introduced": ":0"
              },
              {
                "fixed": ":2022-06-05"
              }
            ]
          }
        ]
      }
    ]
  }"""

_SAMPLE_VULNERABILITY_WITH_MULTIPLE_AFFECTED_PACKAGES = """{
    "id": "ASB-A-210498909",
    "modified": "2022-06-01T00:00:00Z",
    "published": "2022-06-01T00:00:00Z",
    "aliases": [
        "CVE-2018-25020"
    ],
    "summary": "",
    "references": [
        {
            "type": "ADVISORY",
            "url": "https://source.android.com/security/bulletin/2022-06-01#2022-06-05-security-patch-level-vulnerability-details"
        },
        {
            "type": "FIX",
            "url": "https://android.googlesource.com/kernel/common/+/050fad7c4534c13c8eb1d9c2ba66012e014773cb"
        },
        {
            "type": "FIX",
            "url": "https://android.googlesource.com/kernel/common/+/99253eb750fda6a644d5188fb26c43bad8d5a745"
        }
    ],
    "affected": [
        {
            "package": {
                "ecosystem": "Android",
                "name": ":linux_kernel:"
            },
            "ecosystem_specific": {
                "type": "EoP",
                "severity": "Moderate",
                "spl": "2022-06-05",
                "fixes": [
                    "https://android.googlesource.com/kernel/common/+/050fad7c4534c13c8eb1d9c2ba66012e014773cb",
                    "https://android.googlesource.com/kernel/common/+/99253eb750fda6a644d5188fb26c43bad8d5a745"
                ]
            },
            "ranges": [
                {
                    "type": "ECOSYSTEM",
                    "events": [
                        {
                            "introduced": ":0"
                        },
                        {
                            "fixed": ":2022-06-05"
                        }
                    ]
                }
            ]
        },
        {
            "package": {
                "ecosystem": "Android",
                "name": "test/package/2"
            },
            "ecosystem_specific": {
                "type": "EoP",
                "severity": "Moderate",
                "spl": "2022-06-05",
                "fixes": [
                    "https://android.googlesource.com/kernel/common/+/050fad7c4534c13c8eb1d9c2ba66012e014773cb",
                    "https://android.googlesource.com/kernel/common/+/99253eb750fda6a644d5188fb26c43bad8d5a745"
                ]
            },
            "ranges": [
                {
                    "type": "ECOSYSTEM",
                    "events": [
                        {
                            "introduced": ":0"
                        },
                        {
                            "fixed": ":2022-06-05"
                        }
                    ]
                }
            ]
        },
        {
            "package": {
                "ecosystem": "Android",
                "name": "test/package/3"
            },
            "ecosystem_specific": {
                "type": "EoP",
                "severity": "Moderate",
                "spl": "2022-06-05",
                "fixes": [
                    "https://android.googlesource.com/kernel/common/+/050fad7c4534c13c8eb1d9c2ba66012e014773cb",
                    "https://android.googlesource.com/kernel/common/+/99253eb750fda6a644d5188fb26c43bad8d5a745"
                ]
            },
            "ranges": [
                {
                    "type": "ECOSYSTEM",
                    "events": [
                        {
                            "introduced": ":0"
                        },
                        {
                            "fixed": ":2022-06-05"
                        }
                    ]
                }
            ]
        }
    ]
}"""


class VulnerabilityOverwriterTest(parameterized.TestCase):

  def _create_vulnerability(self, test_id):
    vulnerability = json.loads(_SAMPLE_VULNERABILITY)
    vulnerability['id'] = test_id
    return vulnerability

  def test_overwriter_specs_replaces_with_empty_value(self):
    # Arrange
    # Empty value is the a special case, make sure it works.
    specs = [
        vulnerability_overwriter.OverwriteSpec(
            vuln_id='ASB-A-210498909',
            replace=[
                vulnerability_overwriter.Replace(
                    value='',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.severity'
                    ),
                ),
            ],
            reason='testing',
        )
    ]
    vulnerability = self._create_vulnerability('ASB-A-210498909')

    # Act
    vulnerability_overwriter.overwrite([vulnerability], specs)

    # Assert
    self.assertEqual(
        vulnerability['affected'][0]['ecosystem_specific']['severity'], ''
    )

  def test_overwrite_single_spec(self):
    # Arrange
    specs = [
        vulnerability_overwriter.OverwriteSpec(
            vuln_id='ASB-A-210498909',
            replace=[
                vulnerability_overwriter.Replace(
                    value='Test2',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.severity'
                    ),
                ),
                vulnerability_overwriter.Replace(
                    value='Test',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.spl'
                    ),
                ),
            ],
            reason='testing',
        )
    ]

    vulnerability = self._create_vulnerability('ASB-A-210498909')
    vulnerabilities = [
        self._create_vulnerability('TEST_1'),
        vulnerability,
        self._create_vulnerability('TEST_2'),
    ]

    # Act
    vulnerability_overwriter.overwrite(vulnerabilities, specs)

    # Assert that the target vulnerability was overwritten
    self.assertEqual(
        vulnerability['affected'][0]['ecosystem_specific']['severity'], 'Test2'
    )
    self.assertEqual(
        vulnerability['affected'][0]['ecosystem_specific']['spl'], 'Test'
    )

    # Assert that other vulnerabilities were not overwritten
    self.assertEqual(
        vulnerabilities[0]['affected'][0]['ecosystem_specific']['severity'],
        'Moderate',
    )
    self.assertEqual(
        vulnerabilities[0]['affected'][0]['ecosystem_specific']['spl'],
        '2022-06-05',
    )
    self.assertEqual(
        vulnerabilities[2]['affected'][0]['ecosystem_specific']['severity'],
        'Moderate',
    )
    self.assertEqual(
        vulnerabilities[2]['affected'][0]['ecosystem_specific']['spl'],
        '2022-06-05',
    )

  def test_overwrite_multiple_specs(self):
    # Arrange
    specs = [
        vulnerability_overwriter.OverwriteSpec(
            vuln_id='ASB-A-210498909',
            replace=[
                vulnerability_overwriter.Replace(
                    value='Test2',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.severity'
                    ),
                ),
                vulnerability_overwriter.Replace(
                    value='Test_spl2',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.spl'
                    ),
                ),
            ],
            reason='testing',
        ),
        vulnerability_overwriter.OverwriteSpec(
            vuln_id='ASB-A-210498910',
            replace=[
                vulnerability_overwriter.Replace(
                    value='Test3',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.severity'
                    ),
                ),
                vulnerability_overwriter.Replace(
                    value='Test_spl3',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.spl'
                    ),
                ),
            ],
            reason='testing',
        ),
    ]

    vulnerability_1 = self._create_vulnerability('ASB-A-210498909')
    vulnerability_2 = self._create_vulnerability('ASB-A-210498910')
    vulnerabilities = [
        self._create_vulnerability('TEST_1'),
        vulnerability_1,
        vulnerability_2,
        self._create_vulnerability('TEST_2'),
    ]

    # Act
    vulnerability_overwriter.overwrite(vulnerabilities, specs)

    # Assert
    self.assertEqual(
        vulnerability_1['affected'][0]['ecosystem_specific']['severity'],
        'Test2',
    )
    self.assertEqual(
        vulnerability_1['affected'][0]['ecosystem_specific']['spl'], 'Test_spl2'
    )
    self.assertEqual(
        vulnerability_2['affected'][0]['ecosystem_specific']['severity'],
        'Test3',
    )
    self.assertEqual(
        vulnerability_2['affected'][0]['ecosystem_specific']['spl'], 'Test_spl3'
    )

  def test_overwrite_no_matching_vulnerabilities(self):
    # Arrange
    specs = [
        vulnerability_overwriter.OverwriteSpec(
            vuln_id='NO_MATCH_VULN_ID',
            replace=[
                vulnerability_overwriter.Replace(
                    value='Test2',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.severity'
                    ),
                )
            ],
            reason='testing',
        )
    ]

    vulnerabilities = [
        self._create_vulnerability('TEST_1'),
        self._create_vulnerability('TEST_2'),
        self._create_vulnerability('TEST_3'),
    ]

    # Act
    vulnerability_overwriter.overwrite(vulnerabilities, specs)

    # Assert
    self.assertEqual(
        vulnerabilities[0]['affected'][0]['ecosystem_specific']['severity'],
        'Moderate',
    )
    self.assertEqual(
        vulnerabilities[1]['affected'][0]['ecosystem_specific']['severity'],
        'Moderate',
    )
    self.assertEqual(
        vulnerabilities[2]['affected'][0]['ecosystem_specific']['severity'],
        'Moderate',
    )

  def test_overwrite_update_multiple_affected_packages(self):
    # Arrange
    vulnerability = json.loads(
        _SAMPLE_VULNERABILITY_WITH_MULTIPLE_AFFECTED_PACKAGES
    )
    specs = [
        vulnerability_overwriter.OverwriteSpec(
            vuln_id=vulnerability['id'],
            replace=[
                vulnerability_overwriter.Replace(
                    value='Test2',
                    path=jsonpath_ng.parser.parse(
                        'affected[*].ecosystem_specific.severity'
                    ),
                )
            ],
            reason='testing',
        )
    ]

    # Act
    vulnerability_overwriter.overwrite([vulnerability], specs)

    # Assert
    self.assertEqual(
        vulnerability['affected'][0]['ecosystem_specific']['severity'],
        'Test2',
    )
    self.assertEqual(
        vulnerability['affected'][1]['ecosystem_specific']['severity'],
        'Test2',
    )
    self.assertEqual(
        vulnerability['affected'][2]['ecosystem_specific']['severity'],
        'Test2',
    )

  def test_overwrite_no_specs(self):
    # Arrange
    vulnerabilities = [
        self._create_vulnerability('TEST_1'),
        self._create_vulnerability('TEST_2'),
        self._create_vulnerability('TEST_3'),
    ]

    # Act
    vulnerability_overwriter.overwrite(vulnerabilities, [])
    vulnerability_overwriter.overwrite(vulnerabilities, None)

    # Assert
    # Should not raise an error

  def test_load_overwrite_specs_from_json(self):
    # Arrange
    json_content = """[{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific.severity",
          "value": "Test2"
        }
      ]
    }]
    """

    # Act
    overwrite_specs = vulnerability_overwriter.load_overwrite_specs_from_json(
        json_content
    )

    # Assert
    self.assertLen(overwrite_specs, 1)
    self.assertEqual(overwrite_specs[0].vuln_id, 'ASB-A-152496149')
    self.assertEqual(overwrite_specs[0].reason, 'test')
    self.assertLen(overwrite_specs[0].replace, 1)
    self.assertEqual(overwrite_specs[0].replace[0].value, 'Test2')
    self.assertEqual(
        overwrite_specs[0].replace[0].path,
        jsonpath_ng.parser.parse('affected[*].ecosystem_specific.severity'),
    )

  def test_load_overwrite_specs_from_json_replace_has_empty_value(self):
    # Arrange
    # Empty value is the a special case, make sure it works.
    json_content = """[{
      "vuln_id": "ASB-A-152496149",
      "reason": "test",
      "replace": [
        {
          "path": "affected[*].ecosystem_specific.severity",
          "value": ""
        }
      ]
    }]
    """

    # Act
    overwrite_specs = vulnerability_overwriter.load_overwrite_specs_from_json(
        json_content
    )

    # Assert
    self.assertLen(overwrite_specs, 1)
    self.assertEqual(overwrite_specs[0].vuln_id, 'ASB-A-152496149')
    self.assertEqual(overwrite_specs[0].reason, 'test')
    self.assertLen(overwrite_specs[0].replace, 1)
    self.assertEqual(overwrite_specs[0].replace[0].value, '')
    self.assertEqual(
        overwrite_specs[0].replace[0].path,
        jsonpath_ng.parser.parse('affected[*].ecosystem_specific.severity'),
    )

  def test_load_overwrite_specs_from_file(self):
    specs_file_name = _TEST_SPECS_FILE
    specs = vulnerability_overwriter.load_overwrite_specs_from_file(
        specs_file_name
    )
    self.assertNotEmpty(specs, 'No overwrite specs loaded')

  @parameterized.named_parameters(
      (
          'missing_reason',
          _SPEC_WITH_MISSING_REASON,
          'Missing reason',
      ),
      (
          'empty_reason',
          _SPEC_WITH_EMPTY_REASON,
          'Empty reason',
      ),
      (
          'empty_vuln_id',
          _SPEC_WITH_EMPTY_VULN_ID,
          'Empty vuln_id',
      ),
      (
          'missing_vuln_id',
          _SPEC_WITH_MISSING_VULN_ID,
          'Missing vuln_id',
      ),
      (
          'replace_field_empty',
          _REPLACE_FIELD_EMPTY,
          'Replace field is empty',
      ),
      (
          'replace_field_missing',
          _REPLACE_FIELD_MISSING,
          'Replace field is missing',
      ),
      (
          'bad_json_path',
          _BAD_JSON_PATH,
          'Bad JSON path in replace',
      ),
      (
          'empty_json_path',
          _EMPTY_JSON_PATH,
          'Empty JSON path in replace',
      ),
      (
          'no_json_path',
          _NO_JSON_PATH,
          'No JSON path in replace',
      ),
      (
          'no_value_in_replace',
          _NO_VALUE_IN_REPLACE,
          'No value in replace',
      ),
  )
  def test_load_overwrite_specs_from_json_invalid_data(
      self, json_content, error_message
  ):
    with self.assertRaises(ValueError, msg=error_message):
      vulnerability_overwriter.load_overwrite_specs_from_json(json_content)


if __name__ == '__main__':
  absltest.main()
