troubadix 25.6.1__py3-none-any.whl → 25.7.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
troubadix/__version__.py CHANGED
@@ -2,4 +2,4 @@
2
2
 
3
3
  # THIS IS AN AUTOGENERATED FILE. DO NOT TOUCH!
4
4
 
5
- __version__ = "25.6.1"
5
+ __version__ = "25.7.1"
@@ -0,0 +1,200 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2025 Greenbone AG
3
+
4
+ """Helper for parsing if blocks and single-expression if statements in NASL files."""
5
+
6
+ from dataclasses import dataclass
7
+
8
+ from troubadix.helper.text_utils import (
9
+ StringState,
10
+ index_to_linecol,
11
+ )
12
+
13
+ # Brace pairings
14
+ CONDITION_BRACES = ("(", ")")
15
+ BLOCK_BRACES = ("{", "}")
16
+
17
+
18
+ @dataclass
19
+ class IfStatement:
20
+ if_start: int
21
+ if_end: int
22
+ condition_start: int
23
+ condition_end: int
24
+ statement_start: int
25
+ statement_end: int
26
+ condition: str
27
+ statement: str
28
+
29
+
30
+ class IfParser:
31
+ """Parser for if statements in NASL files."""
32
+
33
+ def __init__(self, file_content: str):
34
+ self.file_content = file_content
35
+
36
+ def find_if_statements(self) -> list[IfStatement]:
37
+ """Parse the file to find all if statements (blocks and single expressions)."""
38
+ results: list[IfStatement] = []
39
+ starts = self._find_condition_starts()
40
+ if not starts:
41
+ return results
42
+
43
+ for if_start, opening_brace in starts:
44
+ line, _ = index_to_linecol(self.file_content, if_start)
45
+
46
+ condition_end = self._find_closing_brace(
47
+ opening_brace, CONDITION_BRACES, line
48
+ )
49
+ condition = self.file_content[
50
+ opening_brace + 1 : condition_end
51
+ ].strip()
52
+ statement_pos = self._find_statement_start(condition_end, line)
53
+
54
+ if self.file_content[statement_pos] == "{":
55
+ # Block statement
56
+ block_end = self._find_closing_brace(
57
+ statement_pos, BLOCK_BRACES, line
58
+ )
59
+ if_end = block_end + 1
60
+ statement_start = statement_pos + 1
61
+ statement_end = block_end
62
+ statement = self.file_content[
63
+ statement_pos + 1 : block_end
64
+ ].strip()
65
+ else:
66
+ # Single expression
67
+ expression_end = self._find_expression_end(statement_pos, line)
68
+ if_end = expression_end + 1
69
+ statement_start = statement_pos
70
+ statement_end = expression_end
71
+ statement = self.file_content[
72
+ statement_pos:expression_end
73
+ ].strip()
74
+
75
+ results.append(
76
+ IfStatement(
77
+ if_start=if_start,
78
+ if_end=if_end,
79
+ condition_start=opening_brace + 1,
80
+ condition_end=condition_end,
81
+ statement_start=statement_start,
82
+ statement_end=statement_end,
83
+ condition=condition,
84
+ statement=statement,
85
+ )
86
+ )
87
+
88
+ return results
89
+
90
+ def _find_closing_brace(
91
+ self,
92
+ start_pos: int,
93
+ brace_pair: tuple[str, str],
94
+ line: int,
95
+ ) -> int:
96
+ """Find the matching closing brace, with proper error reporting."""
97
+ opening_brace, closing_brace = brace_pair
98
+ open_count = 1
99
+ string_state = StringState()
100
+
101
+ for i in range(start_pos + 1, len(self.file_content)):
102
+ char = self.file_content[i]
103
+ string_state.process_next_char(char)
104
+
105
+ # Skip characters inside strings
106
+ if string_state.in_string:
107
+ continue
108
+
109
+ if char == opening_brace:
110
+ open_count += 1
111
+ elif char == closing_brace:
112
+ open_count -= 1
113
+ if open_count == 0:
114
+ return i
115
+
116
+ # Generate appropriate error message based on brace type
117
+ if opening_brace == "(":
118
+ raise ValueError(
119
+ f"Unclosed parenthesis in if statement at line {line}"
120
+ )
121
+ elif opening_brace == "{":
122
+ raise ValueError(f"Unclosed brace in if statement at line {line}")
123
+ else:
124
+ raise ValueError(
125
+ f"Unclosed {opening_brace} in if statement at line {line}"
126
+ )
127
+
128
+ def _find_condition_starts(self) -> list[tuple[int, int]]:
129
+ """
130
+ Find starting positions of if conditions in the file content.
131
+ Returns:
132
+ A list of tuples where each tuple contains the start position of the "if" keyword
133
+ and the position of the opening parenthesis.
134
+ """
135
+ starts = []
136
+ string_state = StringState()
137
+
138
+ for i, char in enumerate(self.file_content):
139
+ string_state.process_next_char(char)
140
+
141
+ # Skip characters inside strings
142
+ if string_state.in_string:
143
+ continue
144
+
145
+ # check for if with word boundary, valid: ["if", " if"], not valid: "xif"
146
+ if (
147
+ i == 0 or not self.file_content[i - 1].isalnum()
148
+ ) and self.file_content.startswith("if", i):
149
+ # skip whitespace
150
+ j = i + 2
151
+ while (
152
+ j < len(self.file_content)
153
+ and self.file_content[j].isspace()
154
+ ):
155
+ j += 1
156
+ # check for condition start
157
+ if j < len(self.file_content) and self.file_content[j] == "(":
158
+ starts.append((i, j))
159
+
160
+ return starts
161
+
162
+ def _find_statement_start(self, condition_end: int, line: int) -> int:
163
+ """Find the start of the statement after the condition (next non-whitespace character)."""
164
+ pos = condition_end + 1
165
+ while pos < len(self.file_content) and self.file_content[pos].isspace():
166
+ pos += 1
167
+
168
+ if pos >= len(self.file_content):
169
+ raise ValueError(
170
+ f"Missing statement after if condition at line {line}"
171
+ )
172
+
173
+ if self.file_content[pos] == ";":
174
+ raise ValueError(
175
+ f"Semicolon after if condition at line {line} makes following block "
176
+ f"always execute. Remove semicolon to fix."
177
+ )
178
+
179
+ return pos
180
+
181
+ def _find_expression_end(self, expression_start: int, line: int) -> int:
182
+ """Find the end of a single expression (semicolon outside of strings)."""
183
+ string_state = StringState()
184
+
185
+ for i in range(expression_start, len(self.file_content)):
186
+ char = self.file_content[i]
187
+ string_state.process_next_char(char)
188
+ if not string_state.in_string and char == ";":
189
+ return i
190
+
191
+ raise ValueError(
192
+ f"Missing expression after if condition at line {line}"
193
+ )
194
+
195
+
196
+ # Wrapper function to maintain backward compatibility
197
+ def find_if_statements(file_content: str) -> list[IfStatement]:
198
+ """Parse a file to find all if statements (blocks and single expressions)."""
199
+ parser = IfParser(file_content)
200
+ return parser.find_if_statements()
@@ -0,0 +1,39 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2025 Greenbone AG
3
+
4
+ from troubadix.helper.text_utils import StringState
5
+
6
+
7
+ def remove_comments(file_content: str) -> str:
8
+ """
9
+ Remove all commented portions from file content while preserving string literals.
10
+ This function:
11
+ 1. Removes content from lines that start with '#' (also after whitespace)
12
+ 2. For lines with inline comments, keeps only the content before the '#'
13
+ 3. Preserves '#' characters within string literals
14
+ 4. Maintains original line numbers by keeping empty lines
15
+ 5. Handles multiline strings
16
+ Args:
17
+ file_content: String containing the full file content
18
+ Returns:
19
+ String with comments removed
20
+ """
21
+ string_state = StringState()
22
+ return "\n".join(
23
+ [
24
+ _remove_comments_in_line(line, string_state)
25
+ for line in file_content.splitlines()
26
+ ]
27
+ )
28
+
29
+
30
+ def _remove_comments_in_line(line: str, state: StringState) -> str:
31
+ if not state.in_string and line.lstrip().startswith("#"):
32
+ return ""
33
+
34
+ for i, char in enumerate(line):
35
+ state.process_next_char(char)
36
+ if char == "#" and not state.in_string:
37
+ return line[:i]
38
+
39
+ return line
@@ -0,0 +1,80 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2025 Greenbone AG
3
+
4
+ """Utilities for text processing and string manipulation in NASL files."""
5
+
6
+
7
+ class StringState:
8
+ """
9
+ A class to track string contexts and escape sequences while parsing text.
10
+ """
11
+
12
+ def __init__(
13
+ self,
14
+ escape_next: bool = False,
15
+ in_double_quote: bool = False,
16
+ in_single_quote: bool = False,
17
+ ):
18
+ self.escape_next = escape_next
19
+ self.in_double_quote = in_double_quote
20
+ self.in_single_quote = in_single_quote
21
+
22
+ @property
23
+ def in_string(self) -> bool:
24
+ """Check if currently inside a string literal."""
25
+ return self.in_double_quote or self.in_single_quote
26
+
27
+ def process_next_char(self, char: str) -> None:
28
+ """
29
+ Process the next character and update the string state accordingly.
30
+
31
+ Args:
32
+ char: The current character being processed
33
+ """
34
+ # Handle escaped character
35
+ if self.escape_next:
36
+ self.escape_next = False
37
+ # Set escape flag if backslash is encountered in a single quote string
38
+ elif char == "\\" and self.in_single_quote:
39
+ self.escape_next = True
40
+ # Switch quote states
41
+ elif char == '"' and not self.in_single_quote:
42
+ self.in_double_quote = not self.in_double_quote
43
+ elif char == "'" and not self.in_double_quote:
44
+ self.in_single_quote = not self.in_single_quote
45
+
46
+
47
+ def index_to_linecol(text: str, index: int) -> tuple[int, int]:
48
+ """
49
+ Converts character index to (line_number, column_number) (1-based index)
50
+
51
+ Args:
52
+ text: Input string
53
+ index: Character position to locate
54
+ Returns:
55
+ (line, column) tuple (both start at 1)
56
+ """
57
+ if index < 0 or index >= len(text):
58
+ raise ValueError(
59
+ f"Index {index} out of bounds for text of length {len(text)}"
60
+ )
61
+
62
+ lines = text.splitlines(keepends=True)
63
+ line_num = 0
64
+ for line_num, line in enumerate(lines, 1):
65
+ if index < len(line):
66
+ break
67
+ index -= len(line)
68
+ return (line_num, index + 1)
69
+
70
+
71
+ def is_position_in_string(text: str, position: int) -> bool:
72
+ """Check if the given position is inside a string literal."""
73
+ string_state = StringState()
74
+
75
+ # Process characters up to (but not including) the position
76
+ # to determine the string state at that position
77
+ for char in text[:position]:
78
+ string_state.process_next_char(char)
79
+
80
+ return string_state.in_string
@@ -1,24 +1,13 @@
1
- # Copyright (C) 2022 Greenbone AG
2
- #
3
1
  # SPDX-License-Identifier: GPL-3.0-or-later
4
- #
5
- # This program is free software: you can redistribute it and/or modify
6
- # it under the terms of the GNU General Public License as published by
7
- # the Free Software Foundation, either version 3 of the License, or
8
- # (at your option) any later version.
9
- #
10
- # This program is distributed in the hope that it will be useful,
11
- # but WITHOUT ANY WARRANTY; without even the implied warranty of
12
- # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13
- # GNU General Public License for more details.
14
- #
15
- # You should have received a copy of the GNU General Public License
16
- # along with this program. If not, see <http://www.gnu.org/licenses/>.
2
+ # SPDX-FileCopyrightText: 2025 Greenbone AG
17
3
 
18
4
  import re
19
5
  from pathlib import Path
20
6
  from typing import Iterator
21
7
 
8
+ from troubadix.helper.if_block_parser import find_if_statements
9
+ from troubadix.helper.remove_comments import remove_comments
10
+ from troubadix.helper.text_utils import index_to_linecol, is_position_in_string
22
11
  from troubadix.plugin import (
23
12
  FileContentPlugin,
24
13
  LinterError,
@@ -26,6 +15,16 @@ from troubadix.plugin import (
26
15
  LinterWarning,
27
16
  )
28
17
 
18
+ # minimal regex to match display() calls, content inside parentheses does not matter
19
+ DISPLAY_PATTERN = re.compile(r"display\s*\(.*;")
20
+ # matches any condition that contains "debug" such as ssh_debug, DEBUG, etc.
21
+ DEBUG_PATTERN = re.compile(r"debug", re.IGNORECASE)
22
+ EXCLUDED_FILES = {
23
+ "global_settings.inc",
24
+ "bin.inc",
25
+ "dump.inc",
26
+ }
27
+
29
28
 
30
29
  class CheckUsingDisplay(FileContentPlugin):
31
30
  name = "check_using_display"
@@ -35,87 +34,77 @@ class CheckUsingDisplay(FileContentPlugin):
35
34
  nasl_file: Path,
36
35
  file_content: str,
37
36
  ) -> Iterator[LinterResult]:
38
- if "# troubadix: disable=template_nd_test_files_fps" in file_content:
37
+ if nasl_file.name in EXCLUDED_FILES:
39
38
  return
40
39
 
41
- display_matches = re.finditer(
42
- r".*(display\s*\([^)]+\)\s*;)", file_content
43
- )
44
- if display_matches is None:
40
+ comment_free_content = remove_comments(file_content)
41
+ try:
42
+ if_statements = find_if_statements(comment_free_content)
43
+ except ValueError as e:
44
+ yield LinterError(
45
+ str(e),
46
+ file=nasl_file,
47
+ plugin=self.name,
48
+ )
45
49
  return
46
50
 
47
- for display_match in display_matches:
48
- if display_match is not None and display_match.group(0):
49
- dis_match = display_match.group(0)
50
- file_name = str(nasl_file)
51
-
52
- # Known false positives because the if_match check above can't
53
- # detect something like e.g.:
54
- # if( debug )
55
- # display("foo");
56
- if (
57
- "ssh_func.inc" in file_name
58
- and "display( debug_str )" in dis_match
59
- ):
60
- continue
51
+ # Find all display() calls in the entire file
52
+ all_display_matches = list(
53
+ DISPLAY_PATTERN.finditer(comment_free_content)
54
+ )
61
55
 
62
- if (
63
- "gb_treck_ip_stack_detect.nasl" in file_name
64
- and 'display("---[' in dis_match
65
- ):
66
- continue
56
+ for display_match in all_display_matches:
57
+ display_pos = display_match.start()
67
58
 
68
- if (
69
- "ike_isakmp_func.inc" in file_name
70
- and 'display( "---[' in dis_match
71
- ):
72
- continue
59
+ # Skip if this match is inside a string literal
60
+ if is_position_in_string(comment_free_content, display_pos):
61
+ continue
73
62
 
74
- if (
75
- "pcap_func.inc" in file_name
76
- and 'display( "---[' in dis_match
77
- ):
78
- continue
63
+ # Check if this display is inside any if statement
64
+ containing_if = None
65
+ for if_statement in if_statements:
66
+ if if_statement.if_start < display_pos < if_statement.if_end:
67
+ # inner most if statement containing the display,
68
+ containing_if = if_statement
69
+ # break # Uncomment this line to get the outermost if statement
79
70
 
80
- if (
81
- "eol_os.inc" in file_name
82
- and 'display( "DEBUG: Base CPE' in dis_match
83
- ):
84
- continue
71
+ # Case 1: Not in any if statement - ERROR
72
+ if not containing_if:
73
+ line_start = (
74
+ comment_free_content.rfind("\n", 0, display_pos) + 1
75
+ )
76
+ line_end = comment_free_content.find("\n", display_pos)
77
+ if line_end == -1:
78
+ line_end = len(comment_free_content)
85
79
 
86
- if (
87
- "gsf/dicom.inc" in file_name
88
- or "enterprise/dicom.inc" in file_name
89
- or "global_settings.inc" in file_name
90
- or "rdp.inc" in file_name
91
- or "bin.inc" in file_name
92
- ):
93
- continue
80
+ context = comment_free_content[line_start:line_end].strip()
81
+ line, _ = index_to_linecol(comment_free_content, display_pos)
82
+ yield LinterError(
83
+ f"VT is using a display() without any if statement at line {line}: {context}",
84
+ file=nasl_file,
85
+ plugin=self.name,
86
+ )
87
+ continue
94
88
 
89
+ # Case 2: Check if it's inside a debug if - OKAY
90
+ in_debug_if = False
91
+ for debug_if in if_statements:
95
92
  if (
96
- "DDI_Directory_Scanner.nasl" in file_name
97
- and ":: Got a" in dis_match
93
+ DEBUG_PATTERN.search(debug_if.condition)
94
+ and debug_if.if_start < display_pos < debug_if.if_end
98
95
  ):
99
- continue
96
+ in_debug_if = True
97
+ break
100
98
 
101
- if_comment_match = re.search(
102
- r"(if[\s]*\(|#).*display\s*\(", dis_match
99
+ # Case 3: In an if but not in a debug if - WARNING
100
+ if not in_debug_if:
101
+ line, _ = index_to_linecol(comment_free_content, display_pos)
102
+ yield LinterWarning(
103
+ "VT is using a display() inside an if statement"
104
+ f" but without debug check at line {line}\n"
105
+ + comment_free_content[
106
+ containing_if.if_start : containing_if.if_end
107
+ ],
108
+ file=nasl_file,
109
+ plugin=self.name,
103
110
  )
104
- if (
105
- if_comment_match is not None
106
- and if_comment_match.group(0) is not None
107
- ):
108
- yield LinterWarning(
109
- f"VT is using a display() function which "
110
- f"is protected by a comment or an if statement at: "
111
- f"{dis_match}.",
112
- file=nasl_file,
113
- plugin=self.name,
114
- )
115
- else:
116
- yield LinterError(
117
- f"VT/Include is using a display() "
118
- f"function at: {dis_match}",
119
- file=nasl_file,
120
- plugin=self.name,
121
- )
@@ -0,0 +1,141 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2025 Greenbone AG
3
+
4
+ import logging
5
+ import os
6
+ import re
7
+ import subprocess
8
+ import sys
9
+ from argparse import ArgumentParser, Namespace
10
+ from pathlib import Path
11
+
12
+ from troubadix.argparser import file_type_existing
13
+ from troubadix.standalone_plugins.common import git
14
+
15
+ logger = logging.getLogger(__name__)
16
+
17
+ CREATION_DATE_BASE_PATTERN = (
18
+ r"\s*script_tag\s*\(\s*name\s*:\s*\"creation_date\"\s*,"
19
+ r"\s*value\s*:\s*\"(?P<creation_date>.*)\"\s*\)\s*;"
20
+ )
21
+
22
+
23
+ def parse_arguments() -> Namespace:
24
+
25
+ parser = ArgumentParser(
26
+ description="Check for changed creation date",
27
+ )
28
+ parser.add_argument(
29
+ "-c",
30
+ "--commit_range",
31
+ type=str,
32
+ required=True,
33
+ help=(
34
+ "Git commit range to check e.g. "
35
+ "2c87f4b6062804231fd508411510ca07fd270380..HEAD or"
36
+ "YOUR_BRANCH..main"
37
+ ),
38
+ )
39
+ parser.add_argument(
40
+ "-f",
41
+ "--files",
42
+ nargs="+",
43
+ type=file_type_existing,
44
+ default=[],
45
+ help=(
46
+ "List of files to diff. "
47
+ "If empty use all files added or modified in the commit range."
48
+ ),
49
+ )
50
+ args = parser.parse_args()
51
+
52
+ if not args.files:
53
+ args.files += [
54
+ Path(filename)
55
+ for filename in git(
56
+ "diff", "--name-only", "--diff-filter=d", args.commit_range
57
+ ).splitlines()
58
+ if filename.endswith(".nasl")
59
+ ]
60
+
61
+ return args
62
+
63
+
64
+ def check_changed_creation_date(
65
+ commit_range: str, nasl_files: list[Path]
66
+ ) -> bool:
67
+ """
68
+ This script checks (via git diff) if the creation date of
69
+ passed VTs has changed, which is not allowed.
70
+ """
71
+ creation_date_changed = False
72
+
73
+ for nasl_file in nasl_files:
74
+
75
+ if not nasl_file.exists():
76
+ continue
77
+
78
+ logger.info("Check file %s", nasl_file)
79
+ text = git(
80
+ "-c",
81
+ "color.status=false",
82
+ "--no-pager",
83
+ "diff",
84
+ commit_range,
85
+ nasl_file,
86
+ )
87
+
88
+ creation_date_added = re.search(
89
+ r"^\+" + CREATION_DATE_BASE_PATTERN,
90
+ text,
91
+ re.MULTILINE,
92
+ )
93
+ if not creation_date_added or not (
94
+ added := creation_date_added.group("creation_date")
95
+ ):
96
+ continue
97
+
98
+ creation_date_removed = re.search(
99
+ r"^\-" + CREATION_DATE_BASE_PATTERN,
100
+ text,
101
+ re.MULTILINE,
102
+ )
103
+ if not creation_date_removed or not (
104
+ removed := creation_date_removed.group("creation_date")
105
+ ):
106
+ continue
107
+
108
+ if added != removed:
109
+ logger.error(
110
+ "The creation date of %s was changed, "
111
+ "which is not allowed.\nNew creation date: "
112
+ "%s\nOld creation date: %s",
113
+ nasl_file,
114
+ added,
115
+ removed,
116
+ )
117
+ creation_date_changed = True
118
+
119
+ return creation_date_changed
120
+
121
+
122
+ def main() -> int:
123
+
124
+ try:
125
+ git_base = git("rev-parse", "--show-toplevel")
126
+ os.chdir(git_base.rstrip("\n"))
127
+ except subprocess.SubprocessError:
128
+ logger.error(
129
+ "Your current working directory doesn't belong to a git repository"
130
+ )
131
+ return 1
132
+
133
+ args = parse_arguments()
134
+ if check_changed_creation_date(args.commit_range, args.files):
135
+ return 2
136
+
137
+ return 0
138
+
139
+
140
+ if __name__ == "__main__":
141
+ sys.exit(main())
@@ -44,19 +44,19 @@ ENTERPRISE_FEED_CHECK_PATTERN = re.compile(
44
44
  r"(?:\{[^}]*\}\s*|[^\{;]*;)"
45
45
  ) # Matches specific if blocks used to gate code to run only for enterprise feeds
46
46
 
47
+ logging.basicConfig(format="%(levelname)s: %(message)s")
48
+ logger = logging.getLogger(__name__)
47
49
 
48
- class Reporter:
49
- def __init__(self) -> None:
50
- self.logger = logging.getLogger(__name__)
51
50
 
51
+ class Reporter:
52
52
  def report(self, results: list[Result]):
53
53
  for result in results:
54
54
  for error in result.errors:
55
- self.logger.error(f"{result.name}: {error}")
55
+ logger.error(f"{result.name}: {error}")
56
56
  for warning in result.warnings:
57
- self.logger.warning(f"{result.name}: {warning}")
57
+ logger.warning(f"{result.name}: {warning}")
58
58
  for info in result.infos:
59
- self.logger.info(f"{result.name}: {info}")
59
+ logger.info(f"{result.name}: {info}")
60
60
 
61
61
 
62
62
  def get_feed(root: Path, feed: Feed) -> list[Script]:
@@ -74,7 +74,7 @@ def get_scripts(directory: Path) -> list[Script]:
74
74
  try:
75
75
  content = path.read_text(encoding=CURRENT_ENCODING)
76
76
  except Exception as e:
77
- logging.error(f"Error reading file {path}: {e}")
77
+ logger.error(f"Error reading file {path}: {e}")
78
78
  continue
79
79
 
80
80
  try:
@@ -88,7 +88,7 @@ def get_scripts(directory: Path) -> list[Script]:
88
88
  Script(name, feed, dependencies, category, deprecated)
89
89
  )
90
90
  except Exception as e:
91
- logging.error(f"Error processing {path}: {e}")
91
+ logger.error(f"Error processing {path}: {e}")
92
92
 
93
93
  return scripts
94
94
 
@@ -153,17 +153,15 @@ def create_graph(scripts: list[Script]):
153
153
  def main():
154
154
  args = parse_args()
155
155
 
156
- logging.basicConfig(
157
- level=args.log.upper(), format="%(levelname)s: %(message)s"
158
- )
156
+ logger.setLevel(args.log)
159
157
 
160
- logging.info("starting troubadix dependency analysis")
158
+ logger.info("starting troubadix dependency analysis")
161
159
 
162
160
  scripts = get_feed(args.root, args.feed)
163
161
  graph = create_graph(scripts)
164
162
 
165
- logging.info(f"nodes (scripts) in graph: {graph.number_of_nodes()}")
166
- logging.info(f"edges (dependencies) in graph: {graph.number_of_edges()}")
163
+ logger.info(f"nodes (scripts) in graph: {graph.number_of_nodes()}")
164
+ logger.info(f"edges (dependencies) in graph: {graph.number_of_edges()}")
167
165
 
168
166
  results = [
169
167
  check_duplicates(scripts),
@@ -0,0 +1,27 @@
1
+ # SPDX-License-Identifier: GPL-3.0-or-later
2
+ # SPDX-FileCopyrightText: 2025 Greenbone AG
3
+
4
+ from contextlib import contextmanager
5
+ from os import chdir
6
+ from pathlib import Path
7
+ from tempfile import TemporaryDirectory
8
+
9
+ from troubadix.standalone_plugins.changed_creation_date import git
10
+
11
+
12
+ @contextmanager
13
+ def temporary_git_directory():
14
+
15
+ cwd = Path.cwd()
16
+
17
+ with TemporaryDirectory() as tempdir:
18
+
19
+ try:
20
+ chdir(tempdir)
21
+ git("init", "-b", "main")
22
+ git("config", "--local", "user.email", "max.mustermann@example.com")
23
+ git("config", "--local", "user.name", "Max Mustermann")
24
+ yield Path(tempdir)
25
+
26
+ finally:
27
+ chdir(cwd)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: troubadix
3
- Version: 25.6.1
3
+ Version: 25.7.1
4
4
  Summary: A linting and QA check tool for NASL files
5
5
  License: GPL-3.0-or-later
6
6
  Author: Greenbone
@@ -1,5 +1,5 @@
1
1
  troubadix/__init__.py,sha256=K7sIXXDrC7YRb7BvIpdQ6ZfG_QkT0qUH_wAlHROVRfM,716
2
- troubadix/__version__.py,sha256=2JsvA-ZUAmnlYulTht-dHiCPoWdAAwJ4kZzLSqfgQkA,103
2
+ troubadix/__version__.py,sha256=s9wUZaxMg1UcS_yFk7qOs9hPH3B74hRj6Rm9Ownkqz0,103
3
3
  troubadix/argparser.py,sha256=-H07Jhqh68_M4Mbjq9qJjTr3MShy_N2pxl2qHA6cfRU,7481
4
4
  troubadix/codespell/codespell.additions,sha256=E62zoHJavUhcbMoyBqO2rOUe3OqIXikh7uAiGmNs5uw,524
5
5
  troubadix/codespell/codespell.exclude,sha256=8blBv8KRrKU4KQhneHOPth1tSCYdrEoOCI5kYRfhOEA,147451
@@ -7,8 +7,11 @@ troubadix/codespell/codespell.ignore,sha256=2CP8u6O2VENcDpt2FfEDNmfa1Eh3D80yeYHT
7
7
  troubadix/helper/__init__.py,sha256=tp2fPLzwGEA_2eiJbvuePiY6rjYSFxx7VUsCV4fSwvw,1110
8
8
  troubadix/helper/date_format.py,sha256=2m8EWZPmTQ1kxgv4i5hrPoPlAA8usjz28aRff352zNU,2488
9
9
  troubadix/helper/helper.py,sha256=GXapYLii2rLKwkX2ok31YoAdUSizBnyPjWz-aPP6HM8,3105
10
+ troubadix/helper/if_block_parser.py,sha256=mPcx5XYca4Rr3F1jTo53DRMQ2DZlgY2I7ydymMjeLB0,6813
10
11
  troubadix/helper/linguistic_exception_handler.py,sha256=Bq7ULjDdWTKUpFNTUX6XMPdD4s4v8eIjZPyqBe8VLws,6811
11
12
  troubadix/helper/patterns.py,sha256=qhiAtNQs89Hk6-yRGrjjRO4DdVIMhYLd8IEQ48REpu4,9752
13
+ troubadix/helper/remove_comments.py,sha256=zjhXPQXgKaEOparAdu4aBXcJlEul8LbNaP3uLCYGwHw,1244
14
+ troubadix/helper/text_utils.py,sha256=gBX0box9VtgA0CMfE0u2Vvi1IuOopxUfCQFYn_1wVjE,2543
12
15
  troubadix/plugin.py,sha256=3fQPj3Qe_hgwHerlYE4hbdzYMzRU557NxJ-UwtE9mOI,3525
13
16
  troubadix/plugins/__init__.py,sha256=w5ItmwNALfW4tIHuPuxqi2PMExA6eKbLiXCmviY9CSE,8661
14
17
  troubadix/plugins/badwords.py,sha256=k1A1d2pdXzie87FGGXrykP2BgdZbY5QtmQItupHtNyw,4701
@@ -67,7 +70,7 @@ troubadix/plugins/spelling.py,sha256=3AW5sNtLL67OthKLaCH_y2HCVJ5YH_eyF9xjTJMIDG4
67
70
  troubadix/plugins/tabs.py,sha256=7zXaTZe4cZoZvrLyqntVfTeNN_W3D8dfQl67QevXxtc,1319
68
71
  troubadix/plugins/todo_tbd.py,sha256=MN5fFwBhPmt3JDQ2Hx20B8yUy1vz7LIZC3rDIOzfW9M,1758
69
72
  troubadix/plugins/trailing_spaces_tabs.py,sha256=nMly8ZsmGprxHvwCDclKBDRB0eq6JEkjERYKvtStkY4,1873
70
- troubadix/plugins/using_display.py,sha256=qbPJl-rYWpQJK92bz98RIciaU4Mt_9ty1pGquWRWDnM,4046
73
+ troubadix/plugins/using_display.py,sha256=BBqjMzRhlVZbbeOAAktdhqysuYoHNaMmqO6-6l_-Hy4,3983
71
74
  troubadix/plugins/valid_oid.py,sha256=20E2BW1TSoEuT2j8dJcYedqdUwXzEGrLbRdQpgAPoms,17788
72
75
  troubadix/plugins/valid_script_tag_names.py,sha256=6uMJsBdV-Zx-k1F2_MWmQPHXNo1u0ifuosbftbg-27E,3447
73
76
  troubadix/plugins/variable_assigned_in_if.py,sha256=NNz8iuzyQ4rSM6My4WYC1s5TABQqgs7us15PkDA-VV0,3285
@@ -79,6 +82,7 @@ troubadix/results.py,sha256=PAe3n0A99XXNYeP8gLliuDFt0V9J5a7oPun_bKU4LG4,2599
79
82
  troubadix/runner.py,sha256=RHyZe7YMBzJpCGBVfBUg2BMkVi13CmI9u0DVG2z4518,5195
80
83
  troubadix/standalone_plugins/__init__.py,sha256=kUR5RAFc7HCeiqdlX36dZOHkUI5wI6V_43RpEcD8b-0,22
81
84
  troubadix/standalone_plugins/allowed_rev_diff.py,sha256=5Zc8xTZlkMOPVNRdxNkosFFtwaQ6J8RiJYzaYXEuN40,4145
85
+ troubadix/standalone_plugins/changed_creation_date.py,sha256=6iaWnvWPlhkAM2FKtBVgEZOsMIctz3f6dgPR4k2LcTY,3518
82
86
  troubadix/standalone_plugins/changed_cves.py,sha256=nEWwDa33QXekvpwcmnGMrdPHrJISz9p9j6lX09teDlk,2921
83
87
  troubadix/standalone_plugins/changed_oid.py,sha256=9eLvuBuPgZYnHHst-Y-J0TgCcgwmlOWjszWJ57kG9cg,3934
84
88
  troubadix/standalone_plugins/changed_packages/changed_packages.py,sha256=tyNwpJgaZS2o0X6xywXAQ_i7LB9HsEQYbDZ3Tcvtsdo,5742
@@ -94,16 +98,17 @@ troubadix/standalone_plugins/common.py,sha256=PkScV-lisNY4WyrzwjV3dK1DF26hJv5JXT
94
98
  troubadix/standalone_plugins/dependency_graph/__init__.py,sha256=SQSaQXWmpq5-5ozpqMgvnvoYTK8oj64A5kie1m_5bWQ,88
95
99
  troubadix/standalone_plugins/dependency_graph/checks.py,sha256=O4KyiVKpKxrgdmADXiduKY794nZndyy7epilbbY7JLo,4074
96
100
  troubadix/standalone_plugins/dependency_graph/cli.py,sha256=RIyGeSm83XJ88N7Ati6z417hXkMajzzAtqdxnOg-urI,1305
97
- troubadix/standalone_plugins/dependency_graph/dependency_graph.py,sha256=G_Ilm_EENpVHTw8wItknbfytVPRuTnVN1OoZEYvlTaU,5613
101
+ troubadix/standalone_plugins/dependency_graph/dependency_graph.py,sha256=_4x_btU9GTdh5MZw56R7hfUwpdejNa2iXXxRRIPiEGU,5536
98
102
  troubadix/standalone_plugins/dependency_graph/models.py,sha256=7kLrjFRdyReTRTgxte6-3KCBve4evg91AcqkX4I8VJU,1002
99
103
  troubadix/standalone_plugins/deprecate_vts.py,sha256=mLt2DV9Y1YAEuh6c4nFweZYIOprsBzO7115dihEn4lA,7602
100
104
  troubadix/standalone_plugins/file_extensions.py,sha256=fqswrhCcQqygIszcnobS9hFQmSpv3gDkvlufoaTckBg,2355
101
105
  troubadix/standalone_plugins/last_modification.py,sha256=ROzwVzzYilXJ0llVt4Lv0w8b9BJKoahl6YxPDiub614,4338
102
106
  troubadix/standalone_plugins/no_solution.py,sha256=p_-az9Igl4GH6HnhLLYbYlWIiEP64OTQLpX-z3JAshs,8760
107
+ troubadix/standalone_plugins/util.py,sha256=JTXGmi-_BJouTNe6QzEosLlXUt9jKW-3fz4db05RJJw,696
103
108
  troubadix/standalone_plugins/version_updated.py,sha256=6YHF0OjL5NWszQdsSh7XzlSji1e6Uaqwu_Y6m3R0mvI,4203
104
109
  troubadix/troubadix.py,sha256=5__Jz3bYSrya4aG6RCBWxqnsDepXfwXZ3v0bjCzEFi0,6039
105
- troubadix-25.6.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
106
- troubadix-25.6.1.dist-info/METADATA,sha256=9rwqkTrr9ZEMC4n54tWbDu0A3bwBj7_HKKsmpBzZgpg,4462
107
- troubadix-25.6.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
108
- troubadix-25.6.1.dist-info/entry_points.txt,sha256=SnhEUe4W76P-ADmO9J355gRztTyHU_PTxRewKy3-e5o,832
109
- troubadix-25.6.1.dist-info/RECORD,,
110
+ troubadix-25.7.1.dist-info/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
111
+ troubadix-25.7.1.dist-info/METADATA,sha256=25a8sYPpn4ZehTgpyyCLlTVWk2gmspMJ44vsn2_5qFw,4462
112
+ troubadix-25.7.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
113
+ troubadix-25.7.1.dist-info/entry_points.txt,sha256=ETEPBi4fKv3o7hzkzceX4838G6g5_5wRdEddYot8N6A,920
114
+ troubadix-25.7.1.dist-info/RECORD,,
@@ -1,6 +1,7 @@
1
1
  [console_scripts]
2
2
  troubadix=troubadix.troubadix:main
3
3
  troubadix-allowed-rev-diff=troubadix.standalone_plugins.allowed_rev_diff:main
4
+ troubadix-changed-creation-date=troubadix.standalone_plugins.changed_creation_date:main
4
5
  troubadix-changed-cves=troubadix.standalone_plugins.changed_cves:main
5
6
  troubadix-changed-oid=troubadix.standalone_plugins.changed_oid:main
6
7
  troubadix-changed-packages=troubadix.standalone_plugins.changed_packages.changed_packages:main