diff --git a/tests/standalone_plugins/test_deprecate_vts.py b/tests/standalone_plugins/test_deprecate_vts.py index af89373b..ffae2933 100644 --- a/tests/standalone_plugins/test_deprecate_vts.py +++ b/tests/standalone_plugins/test_deprecate_vts.py @@ -1,6 +1,7 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: 2024 Greenbone AG # pylint: disable=line-too-long +# pylint: disable=protected-access import unittest from pathlib import Path from tests.plugins import TemporaryDirectory @@ -9,10 +10,11 @@ deprecate, parse_args, DeprecatedFile, - get_summary, - finalize_content, + _get_summary, + _finalize_content, update_summary, - get_files, + get_files_from_path, + filter_files, ) @@ -20,27 +22,43 @@ class ParseArgsTestCase(unittest.TestCase): def test_parse_args(self): testfile = "testfile.nasl" output_path = "attic/" - input_path = "nasl/common" reason = "notus" args = parse_args( [ - "--file", - str(testfile), + "--files", + testfile, "--output-path", output_path, - "--input-path", - input_path, "--deprecation-reason", reason, ] ) - self.assertEqual(args.file, Path(testfile)) - self.assertEqual(args.output_path, output_path) + self.assertEqual(args.files, [Path(testfile)]) + self.assertEqual(args.output_path, Path(output_path)) self.assertEqual(args.deprecation_reason, reason) - self.assertEqual(args.input_path, input_path) - def test_parse_args_invali_reason(self): + def test_parse_args_invalid_reason(self): + testfile = "testfile.nasl" + output_path = "attic/" + input_path = "nasl/common" + reason = "notus" + + with self.assertRaises(SystemExit): + parse_args( + [ + "--files", + testfile, + "--output-path", + output_path, + "--input-path", + input_path, + "--deprecation-reason", + reason, + ] + ) + + def test_mandatory_arg_group_both(self): output_path = "attic/" input_path = "nasl/common" reason = "foo" @@ -56,6 +74,19 @@ def test_parse_args_invali_reason(self): ] ) + def test_mandatory_arg_group_neither(self): + output_path = "attic/" + reason = "notus" + with self.assertRaises(SystemExit): + parse_args( + [ + "--output-path", + output_path, + "--deprecation-reason", + reason, + ] + ) + NASL_CONTENT = ( '...if(description)\n{\n script_oid("1.3.6.1.4.1.25623.1.0.910673");' @@ -123,7 +154,7 @@ def test_deprecate_kb_item(self): ) def test_get_summary(self): - result = get_summary(NASL_CONTENT) + result = _get_summary(NASL_CONTENT) expected = ( "The remote host is missing an update for the 'gd'\n package(s) " "announced " @@ -132,7 +163,8 @@ def test_get_summary(self): self.assertEqual(result, expected) def test_finalize_content(self): - result = finalize_content(NASL_CONTENT) + + result = _finalize_content(NASL_CONTENT) expected = ( '...if(description)\n{\n script_oid("1.3.6.1.4.1.25623.1.0.910673");\n ' 'script_version("2024-03-12T14:15:13+0000");\n script_name("RedHat: Security Advisory for gd (RHSA-2020:5443-01)");\n script_family("Red Hat Local Security Checks");\n script_dependencies("gather-package-list.nasl");\n script_mandatory_keys("ssh/login/rhel", "ssh/login/rpms", re:"ssh/login/release=RHENT_7");\n\n script_xref(name:"RHSA", value:"2020:5443-01");\n script_xref(name:"URL", value:"https://www.redhat.com/archives/rhsa-announce/2020-December/msg00044.html");\n\n script_tag(name:"summary", value:"The remote host is missing an update for the \'gd\'\n package(s) announced via the RHSA-2020:5443-01 advisory.");\n\n script_tag(name:"deprecated", value:TRUE);\n\nexit(0);\n}\n\nexit(66);\n' @@ -148,30 +180,26 @@ def test_update_summary_no_oid_match(self): result = update_summary(file, "notus") self.assertIn("This VT has been deprecated", result) - def test_get_files_dir(self): + def test_get_files_from_path(self): with TemporaryDirectory() as in_dir: testfile1 = in_dir / "gb_rhsa_2021_8383_8383.nasl" testfile1.write_text(NASL_CONTENT, encoding="utf8") - result = get_files(dir_path=in_dir) - expected = [ - DeprecatedFile( - name="gb_rhsa_2021_8383_8383.nasl", - full_path=in_dir / "gb_rhsa_2021_8383_8383.nasl", - content=NASL_CONTENT, - ) - ] + result = get_files_from_path(dir_path=in_dir) + expected = [testfile1] self.assertEqual(result, expected) - def test_get_files_dir_filtered_in(self): + def test_filter_files(self): with TemporaryDirectory() as in_dir: testfile1 = in_dir / "gb_rhsa_2021_8383_8383.nasl" testfile1.write_text(NASL_CONTENT, encoding="utf8") testfile2 = in_dir / "gb_rhsa_2020_8383_8383.nasl" testfile2.write_text(NASL_CONTENT, encoding="utf8") - result = get_files(dir_path=in_dir, filename_prefix="gb_rhsa_2021") + result = filter_files( + files=[testfile1, testfile2], filename_prefix="gb_rhsa_2021" + ) expected = [ DeprecatedFile( name="gb_rhsa_2021_8383_8383.nasl", @@ -182,47 +210,14 @@ def test_get_files_dir_filtered_in(self): self.assertEqual(result, expected) - def test_get_files_dir_filtered_out(self): + def test_filter_files_out(self): with TemporaryDirectory() as in_dir: testfile1 = in_dir / "gb_rhsa_2021_8383_8383.nasl" testfile1.write_text(NASL_CONTENT, encoding="utf8") - result = get_files(dir_path=in_dir, filename_prefix="gb_rhsa_2020") + result = filter_files( + files=[testfile1], filename_prefix="gb_rhsa_2020" + ) expected = [] self.assertEqual(result, expected) - - def test_get_files_single(self): - with TemporaryDirectory() as in_dir: - testfile1 = in_dir / "gb_rhsa_2021_8383_8383.nasl" - testfile1.write_text(NASL_CONTENT, encoding="utf8") - - result = get_files(file=testfile1) - expected = [ - DeprecatedFile( - name="gb_rhsa_2021_8383_8383.nasl", - full_path=in_dir / "gb_rhsa_2021_8383_8383.nasl", - content=NASL_CONTENT, - ) - ] - - self.assertEqual(result, expected) - - def test_get_files_single_filtered_in(self): - with TemporaryDirectory() as in_dir: - testfile1 = in_dir / "gb_rhsa_2021_8383_8383.nasl" - testfile2 = in_dir / "gb_rhsa_2022_8484.nasl" - - testfile1.write_text(NASL_CONTENT, encoding="utf8") - testfile2.write_text(NASL_CONTENT, encoding="utf8") - - result = get_files(file=testfile1, filename_prefix="gb_rhsa_2021") - expected = [ - DeprecatedFile( - name="gb_rhsa_2021_8383_8383.nasl", - full_path=in_dir / "gb_rhsa_2021_8383_8383.nasl", - content=NASL_CONTENT, - ) - ] - - self.assertEqual(result, expected) diff --git a/troubadix/standalone_plugins/deprecate_vts.py b/troubadix/standalone_plugins/deprecate_vts.py index 8f932d7b..c52fc9ae 100644 --- a/troubadix/standalone_plugins/deprecate_vts.py +++ b/troubadix/standalone_plugins/deprecate_vts.py @@ -1,14 +1,13 @@ # SPDX-License-Identifier: GPL-3.0-or-later # SPDX-FileCopyrightText: 2024 Greenbone AG -import os import re from argparse import ArgumentParser, Namespace from dataclasses import dataclass from pathlib import Path from typing import Iterable, Optional -from troubadix.argparser import file_type +from troubadix.argparser import file_type, directory_type from troubadix.helper.patterns import ( get_special_script_tag_pattern, get_script_tag_pattern, @@ -16,9 +15,12 @@ SpecialScriptTag, ) - -class PathException(Exception): - pass +DEPRECATIONS = { + "notus": "and replaced by a Notus scanner based one.", + "merged": "because it has been merged into a different VT.", + "defunct": "and is therefore no longer functional.", + "duplicate": "as a duplicate.", +} @dataclass @@ -44,27 +46,22 @@ def update_summary(file: DeprecatedFile, deprecation_reason: str) -> str: Returns: The updated content of the file """ - deprecate_text = "Note: This VT has been deprecated " - if deprecation_reason == "notus": - deprecate_text += "and replaced by a Notus scanner based one." - if deprecation_reason == "merged": - deprecate_text = "because it has been merged into a different VT." - if deprecation_reason == "defunct": - deprecate_text = "and is therefore no longer functional." - if deprecation_reason == "duplicate": - deprecate_text = "as a duplicate." - - old_summary = get_summary(file.content) - if old_summary: - new_summary = old_summary + "\n" + deprecate_text - file.content = file.content.replace(old_summary, new_summary) - else: + old_summary = _get_summary(file.content) + if not old_summary: print(f"No summary in: {file.name}") + return file.content + + deprecate_text = "Note: This VT has been deprecated " + DEPRECATIONS.get( + deprecation_reason + ) + + new_summary = old_summary + "\n" + deprecate_text + file.content = file.content.replace(old_summary, new_summary) return file.content -def finalize_content(content: str) -> str: +def _finalize_content(content: str) -> str: """Update the content field of the nasl script by adding the deprecated tag and removing the extra content.""" content_to_keep = content.split("exit(0);")[0] @@ -74,69 +71,49 @@ def finalize_content(content: str) -> str: ) -def get_files( - dir_path: Path = None, file: Path = None, filename_prefix=None -) -> list[DeprecatedFile]: - """Create a list of DeprecatedFile objects +def get_files_from_path(dir_path: Path = None) -> list: + """Get a list of files from the input path provided Args: dir_path (optional): The path to the directory with the files to be deprecated - file (optional): The path to the single file to be deprecated. - filename_prefix(optional): A filename prefix, such as 'gb_rhsa_2021', - can be used to only deprecate certain VTs + """ + return [file for file in dir_path.glob("**/*")] + + +def filter_files( + files: list, filename_prefix: str = None +) -> list[DeprecatedFile]: + """Filter the files based on a provided prefix and convert them into + DeprecatedFile objects + + Args: + filename_prefix: an optional prefix to filter only specific files Returns: List of DeprecatedFile objects + """ to_deprecate = [] - filename_filter = re.compile(rf"{filename_prefix}") if filename_prefix: - if file and re.match(filename_filter, file.name): - to_deprecate.append( - DeprecatedFile( - file.name, - file.absolute(), - file.open("r", encoding="latin-1").read(), - ) - ) - else: - valid_files = [ - file - for file in dir_path.glob("**/*") - if re.match(filename_filter, file.name) - ] - for file in valid_files: - to_deprecate.append( - DeprecatedFile( - file.name, - file.absolute(), - file.open("r", encoding="latin-1").read(), - ) - ) - else: - if file: + filename_filter = re.compile(rf"{filename_prefix}") + files[:] = [ + file for file in files if re.match(filename_filter, file.name) + ] + + for file in files: + with file.open("r", encoding="latin-1") as fh: to_deprecate.append( DeprecatedFile( file.name, file.absolute(), - file.open("r", encoding="latin-1").read(), + fh.read(), ) ) - else: - for file in dir_path.glob("**/*"): - to_deprecate.append( - DeprecatedFile( - file.name, - file.absolute(), - file.open("r", encoding="latin-1").read(), - ) - ) - return to_deprecate -def get_summary(content: str) -> Optional[str]: +def _get_summary(content: str) -> Optional[str]: """Extract the summary from the nasl script""" pattern = get_script_tag_pattern(ScriptTag.SUMMARY) if match_summary := re.search(pattern, content): @@ -162,44 +139,41 @@ def deprecate( """ output_path.mkdir(parents=True, exist_ok=True) for file in to_deprecate: - items = re.findall(KB_ITEMS, file.content) - if items: + if re.findall(KB_ITEMS, file.content): print( f"Unable to deprecate {file.name}. There are still KB keys " f"remaining." ) continue file.content = update_summary(file, deprecation_reason) - file.content = finalize_content(file.content) + file.content = _finalize_content(file.content) # Drop any unnecessary script tags like script_dependencies(), # script_require_udp_ports() or script_mandatory_keys() tags_to_remove = list() - dependencies = re.search( + + if dependencies := re.search( get_special_script_tag_pattern(SpecialScriptTag.DEPENDENCIES), file.content, - ) - if dependencies: + ): tags_to_remove.append(dependencies.group()) - udp = re.search( + if udp := re.search( get_special_script_tag_pattern(SpecialScriptTag.REQUIRE_UDP_PORTS), file.content, - ) - if udp: + ): tags_to_remove.append(udp.group()) - man_keys = re.search( + if man_keys := re.search( get_special_script_tag_pattern(SpecialScriptTag.MANDATORY_KEYS), file.content, - ) - if man_keys: + ): tags_to_remove.append(man_keys.group()) for tag in tags_to_remove: file.content = file.content.replace(" " + tag + "\n", "") - os.rename(file.full_path, output_path / file.name) + file.full_path.rename(output_path / file.name) with open(output_path / file.name, "w", encoding="latin-1") as f: f.write(file.content) @@ -212,28 +186,10 @@ def parse_args(args: Iterable[str] = None) -> Namespace: "-o", "--output-path", metavar="", - type=str, + type=directory_type, required=True, help="Path where the deprecated files should be written to.", ) - parser.add_argument( - "-f", - "--file", - metavar="", - nargs="?", - default=None, - type=file_type, - help="Single file to deprecate", - ) - parser.add_argument( - "-i", - "--input-path", - metavar="", - nargs="?", - default=None, - type=str, - help="Path to the existing nasl script directory", - ) parser.add_argument( "-p", "--filename-prefix", @@ -245,7 +201,7 @@ def parse_args(args: Iterable[str] = None) -> Namespace: "for example 'gb_rhsa_2021' to filter on the year", ) parser.add_argument( - "-d", + "-r", "--deprecation-reason", metavar="", choices=["notus", "merged", "duplicate", "defunct"], @@ -256,6 +212,24 @@ def parse_args(args: Iterable[str] = None) -> Namespace: "a still active duplicate, 'defunct': The VT is no longer " "functional.", ) + group = parser.add_mutually_exclusive_group(required=True) + group.add_argument( + "-f", + "--files", + metavar="", + nargs="*", + default=None, + type=file_type, + help="Files to deprecate", + ) + group.add_argument( + "-i", + "--input-path", + metavar="", + default=None, + type=directory_type, + help="Path to the existing nasl script directory", + ) return parser.parse_args(args) @@ -263,21 +237,12 @@ def main(): args = parse_args() output_path = Path(args.output_path) input_path = Path(args.input_path) if args.input_path else None - single_file = Path(args.file) if args.file else None - deprecation_reason = args.deprecation_reason - filename_prefix = args.filename_prefix if args.filename_prefix else None - - if not input_path and not single_file: - raise PathException( - "Please provide either the path to a single file or a directory." - ) - - if not input_path.is_dir(): - raise PathException("Input path is not a directory.") + filename_prefix = args.filename_prefix - to_deprecate = get_files(input_path, single_file, filename_prefix) + files = args.files or get_files_from_path(input_path) + filtered_files = filter_files(files, filename_prefix) - deprecate(output_path, to_deprecate, deprecation_reason) + deprecate(output_path, filtered_files, args.deprecation_reason) if __name__ == "__main__":