diff --git a/tests/plugins/test_spaces_before_dots.py b/tests/plugins/test_spaces_before_dots.py new file mode 100644 index 00000000..8a31faec --- /dev/null +++ b/tests/plugins/test_spaces_before_dots.py @@ -0,0 +1,103 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: 2024 Greenbone AG + +from pathlib import Path + +from tests.plugins import PluginTestCase +from troubadix.helper import CURRENT_ENCODING +from troubadix.plugin import LinterFix +from troubadix.plugins.spaces_before_dots import CheckSpacesBeforeDots + + +class TestSpacesBeforeDots(PluginTestCase): + + def test_ok(self): + nasl_file = Path("/some/fake/directory/test.nasl") + content = """ + script_tag(name:"summary", value:"Foo Bar."); + script_tag(name:"solution", value:"Foo .NET."); + script_tag(name:"insight", value:"Foo Bar ..."); + """ + fake_context = self.create_file_plugin_context( + nasl_file=nasl_file, file_content=content + ) + plugin = CheckSpacesBeforeDots(fake_context) + results = list(plugin.run()) + self.assertEqual(len(results), 0) + + def test_fail(self): + nasl_file = Path("/some/fake/directory/test.nasl") + content = ( + """ + script_tag(name:"summary", value:"Foo Bar ."); + script_tag(name:"vuldetect", value:"Foo Bar ."); + script_tag(name:"insight", value:"Foo Bar ."); + script_tag(name:"impact", value:"Foo . Bar . Foo"); + """ + 'script_tag(name:"affected", value:"Foo\n.\nBar.");' + 'script_tag(name:"solution", value:"Foo Bar\n.");' + ) + fake_context = self.create_file_plugin_context( + nasl_file=nasl_file, file_content=content + ) + plugin = CheckSpacesBeforeDots(fake_context) + results = list(plugin.run()) + self.assertEqual(len(results), 7) + self.assertEqual( + results[0].message, + "value of script_tag summary has at least one occurence of excess" + " whitespace before a dot:\n" + " 'script_tag(name:" + '"summary", value:"Foo Bar .");' + "'", + ) + + def test_ignore(self): + nasl_file = Path("/some/fake/directory/test.inc") + fake_context = self.create_file_plugin_context(nasl_file=nasl_file) + plugin = CheckSpacesBeforeDots(fake_context) + + results = list(plugin.run()) + + self.assertEqual(len(results), 0) + + def test_fix(self): + with self.create_directory() as tempdir: + path = tempdir / "file.nasl" + content = ( + """ + script_tag(name:"summary", value:"Foo Bar ."); + script_tag(name:"vuldetect", value:"Foo Bar ."); + script_tag(name:"insight", value:"Foo .Net Bar ."); + script_tag(name:"impact", value:"Foo . Bar . Foo"); + """ + 'script_tag(name:"affected", value:"Foo\n.\nBar.");' + 'script_tag(name:"solution", value:"Foo Bar\n.");' + ) + expected_modified_content = ( + """ + script_tag(name:"summary", value:"Foo Bar."); + script_tag(name:"vuldetect", value:"Foo Bar."); + script_tag(name:"insight", value:"Foo .Net Bar."); + script_tag(name:"impact", value:"Foo. Bar. Foo"); + """ + 'script_tag(name:"affected", value:"Foo.\nBar.");' + 'script_tag(name:"solution", value:"Foo Bar.");' + ) + path.write_text(content, encoding=CURRENT_ENCODING) + + fake_context = self.create_file_plugin_context( + nasl_file=path, file_content=content + ) + + plugin = CheckSpacesBeforeDots(fake_context) + + # keep list() to consume the iterator + list(plugin.run()) + + results = list(plugin.fix()) + self.assertEqual(len(results), 1) + + self.assertIsInstance(results[0], LinterFix) + modified_content = path.read_text(encoding=CURRENT_ENCODING) + self.assertEqual(modified_content, expected_modified_content) diff --git a/troubadix/plugins/__init__.py b/troubadix/plugins/__init__.py index 82cdc5ee..76c3c18a 100644 --- a/troubadix/plugins/__init__.py +++ b/troubadix/plugins/__init__.py @@ -18,6 +18,7 @@ from typing import Iterable, List from troubadix.plugin import FilePlugin, FilesPlugin, Plugin +from troubadix.plugins.spaces_before_dots import CheckSpacesBeforeDots from .badwords import CheckBadwords from .copyright_text import CheckCopyrightText @@ -140,6 +141,7 @@ CheckVTFilePermissions, CheckVTPlacement, CheckWrongSetGetKBCalls, + CheckSpacesBeforeDots, ] # plugins checking all files diff --git a/troubadix/plugins/spaces_before_dots.py b/troubadix/plugins/spaces_before_dots.py new file mode 100644 index 00000000..ca267d5c --- /dev/null +++ b/troubadix/plugins/spaces_before_dots.py @@ -0,0 +1,130 @@ +# SPDX-License-Identifier: GPL-3.0-or-later +# SPDX-FileCopyrightText: 2024 Greenbone AG +import re +from collections.abc import Iterator +from pathlib import Path + +from troubadix.helper import CURRENT_ENCODING +from troubadix.helper.helper import is_ignore_file +from troubadix.helper.patterns import ( + ScriptTag, + get_script_tag_pattern, +) +from troubadix.plugin import ( + FileContentPlugin, + LinterFix, + LinterResult, + LinterWarning, +) + +TAGS = [ + ScriptTag.SUMMARY, + ScriptTag.VULDETECT, + ScriptTag.INSIGHT, + ScriptTag.IMPACT, + ScriptTag.AFFECTED, + ScriptTag.SOLUTION, +] + +# Regex pattern to match: +# 1. A dot preceded and/or followed by any whitespace character (floating between words) +# 2. A dot preceded by any whitespace character at the end of the string +PATTERN = re.compile(r"\s+\.(\s|$)") +IGNORE = [ + # 21.04 and 22.04 are generated and should not be touched manually + "21.04/", + "22.04/", + # uses dots for beginning of entry in enumeration + "common/2008/debian/deb_246.nasl", + "common/2008/debian/deb_266.nasl", + "common/2008/freebsd/freebsd_5e92e8a2.nasl", + "common/2008/freebsd/freebsdsa_cpio.nasl", + "common/2008/freebsd/freebsdsa_cvs2.nasl", + "common/2009/osc_photoGallery_sql_injection.nasl", + "common/2009/secpod_novell_edir_mult_vuln_jul09_lin.nasl", + "common/2009/secpod_novell_edir_mult_vuln_jul09_win.nasl", + "common/2010/freebsd/freebsd_3a7c5fc4.nasl", + "common/2012/freebsd/freebsd_a4a809d8.nasl", + "common/2015/amazon/alas-2014-455.nasl", + "common/2015/gb_mozilla_firefox_mult_vuln01_mar15_macosx.nasl", + "common/2015/gb_mozilla_firefox_mult_vuln01_mar15_win.nasl", + "common/2015/oracle/ELSA-2009-1619.nasl", + "common/2015/oracle/ELSA-2011-0586.nasl", + "common/2016/gb_perl_privilege_escalation_vuln_win.nasl", + "common/2021/dropbear/gb_dropbear_ssh_filename_vuln_may20.nasl", + "common/2021/eclipse/gb_jetty_GHSA-v7ff-8wcx-gmc5_lin.nasl", + "common/2021/eclipse/gb_jetty_GHSA-v7ff-8wcx-gmc5_win.nasl", + "common/gsf/2009/mandriva/gb_mandriva_MDVSA_2008_140.nasl", + "common/gsf/2009/mandriva/gb_mandriva_MDVSA_2008_141.nasl", + "common/gsf/2010/mandriva/gb_mandriva_MDVA_2010_173.nasl", + "common/gsf/2010/mandriva/gb_mandriva_MDVSA_2010_155.nasl", + "common/gsf/2010/mandriva/gb_mandriva_MDVSA_2010_155_1.nasl", + "common/gsf/2010/mandriva/gb_mandriva_MDVSA_2010_167.nasl", + "common/gsf/2020/f5/gb_f5_big_ip_K11315080.nasl", + "common/gsf/2020/f5/gb_f5_big_iq_K11315080.nasl", + "common/2022/opensuse/gb_opensuse_2022_1548_1.nasl", + "common/2024/opensuse/gb_opensuse_2023_3247_1.nasl", + "common/attic/debian/deb_232_1.nasl", +] + + +class CheckSpacesBeforeDots(FileContentPlugin): + name = "check_spaces_before_dots" + + def check_content( + self, nasl_file: Path, file_content: str + ) -> Iterator[LinterResult]: + """ + This plugin checks for excess whitespace before a dot + in script_tags that have full sentence values + """ + self.matches = [] + if nasl_file.suffix == ".inc" or is_ignore_file(nasl_file, IGNORE): + return + for tag in TAGS: + pattern = get_script_tag_pattern(tag) + match = pattern.search(file_content) + if not match: + continue + + value = match.group("value") + value_start = match.start("value") + + for excess_match in PATTERN.finditer(value): + whitespace_pos = excess_match.start() + value_start + self.matches.append((whitespace_pos, excess_match.group())) + fullmatch = match.group() + yield LinterWarning( + f"value of script_tag {match.group('name')} has at least" + " one occurence of excess whitespace before a dot:" + f"\n '{fullmatch}'", + file=nasl_file, + plugin=self.name, + ) + + def fix(self) -> Iterator[LinterResult]: + + if not self.matches: + return + + # Sort matches by position, descending order to avoid messing up indices during replacement + self.matches.sort(reverse=True) + + file_content = self.context.file_content + for pos, match_str in self.matches: + # Replace the match by removing the excess whitespace before the dot + fixed_str = re.sub(r"\s+\.", ".", match_str) + file_content = ( + file_content[:pos] + + fixed_str + + file_content[pos + len(match_str) :] + ) + + with open(self.context.nasl_file, "w", encoding=CURRENT_ENCODING) as f: + f.write(file_content) + + yield LinterFix( + "Excess spaces were removed", + file=self.context.nasl_file, + plugin=self.name, + )