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 +1 -1
- troubadix/helper/if_block_parser.py +200 -0
- troubadix/helper/remove_comments.py +39 -0
- troubadix/helper/text_utils.py +80 -0
- troubadix/plugins/using_display.py +74 -85
- troubadix/standalone_plugins/changed_creation_date.py +141 -0
- troubadix/standalone_plugins/dependency_graph/dependency_graph.py +12 -14
- troubadix/standalone_plugins/util.py +27 -0
- {troubadix-25.6.1.dist-info → troubadix-25.7.1.dist-info}/METADATA +1 -1
- {troubadix-25.6.1.dist-info → troubadix-25.7.1.dist-info}/RECORD +13 -8
- {troubadix-25.6.1.dist-info → troubadix-25.7.1.dist-info}/entry_points.txt +1 -0
- {troubadix-25.6.1.dist-info → troubadix-25.7.1.dist-info}/LICENSE +0 -0
- {troubadix-25.6.1.dist-info → troubadix-25.7.1.dist-info}/WHEEL +0 -0
troubadix/__version__.py
CHANGED
|
@@ -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
|
|
37
|
+
if nasl_file.name in EXCLUDED_FILES:
|
|
39
38
|
return
|
|
40
39
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
63
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
)
|
|
93
|
-
|
|
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
|
-
|
|
97
|
-
and
|
|
93
|
+
DEBUG_PATTERN.search(debug_if.condition)
|
|
94
|
+
and debug_if.if_start < display_pos < debug_if.if_end
|
|
98
95
|
):
|
|
99
|
-
|
|
96
|
+
in_debug_if = True
|
|
97
|
+
break
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
55
|
+
logger.error(f"{result.name}: {error}")
|
|
56
56
|
for warning in result.warnings:
|
|
57
|
-
|
|
57
|
+
logger.warning(f"{result.name}: {warning}")
|
|
58
58
|
for info in result.infos:
|
|
59
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
157
|
-
level=args.log.upper(), format="%(levelname)s: %(message)s"
|
|
158
|
-
)
|
|
156
|
+
logger.setLevel(args.log)
|
|
159
157
|
|
|
160
|
-
|
|
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
|
-
|
|
166
|
-
|
|
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,5 +1,5 @@
|
|
|
1
1
|
troubadix/__init__.py,sha256=K7sIXXDrC7YRb7BvIpdQ6ZfG_QkT0qUH_wAlHROVRfM,716
|
|
2
|
-
troubadix/__version__.py,sha256=
|
|
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=
|
|
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=
|
|
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.
|
|
106
|
-
troubadix-25.
|
|
107
|
-
troubadix-25.
|
|
108
|
-
troubadix-25.
|
|
109
|
-
troubadix-25.
|
|
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
|
|
File without changes
|
|
File without changes
|