secator 0.22.0__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.
- secator/.gitignore +162 -0
- secator/__init__.py +0 -0
- secator/celery.py +453 -0
- secator/celery_signals.py +138 -0
- secator/celery_utils.py +320 -0
- secator/cli.py +2035 -0
- secator/cli_helper.py +395 -0
- secator/click.py +87 -0
- secator/config.py +670 -0
- secator/configs/__init__.py +0 -0
- secator/configs/profiles/__init__.py +0 -0
- secator/configs/profiles/aggressive.yaml +8 -0
- secator/configs/profiles/all_ports.yaml +7 -0
- secator/configs/profiles/full.yaml +31 -0
- secator/configs/profiles/http_headless.yaml +7 -0
- secator/configs/profiles/http_record.yaml +8 -0
- secator/configs/profiles/insane.yaml +8 -0
- secator/configs/profiles/paranoid.yaml +8 -0
- secator/configs/profiles/passive.yaml +11 -0
- secator/configs/profiles/polite.yaml +8 -0
- secator/configs/profiles/sneaky.yaml +8 -0
- secator/configs/profiles/tor.yaml +5 -0
- secator/configs/scans/__init__.py +0 -0
- secator/configs/scans/domain.yaml +31 -0
- secator/configs/scans/host.yaml +23 -0
- secator/configs/scans/network.yaml +30 -0
- secator/configs/scans/subdomain.yaml +27 -0
- secator/configs/scans/url.yaml +19 -0
- secator/configs/workflows/__init__.py +0 -0
- secator/configs/workflows/cidr_recon.yaml +48 -0
- secator/configs/workflows/code_scan.yaml +29 -0
- secator/configs/workflows/domain_recon.yaml +46 -0
- secator/configs/workflows/host_recon.yaml +95 -0
- secator/configs/workflows/subdomain_recon.yaml +120 -0
- secator/configs/workflows/url_bypass.yaml +15 -0
- secator/configs/workflows/url_crawl.yaml +98 -0
- secator/configs/workflows/url_dirsearch.yaml +62 -0
- secator/configs/workflows/url_fuzz.yaml +68 -0
- secator/configs/workflows/url_params_fuzz.yaml +66 -0
- secator/configs/workflows/url_secrets_hunt.yaml +23 -0
- secator/configs/workflows/url_vuln.yaml +91 -0
- secator/configs/workflows/user_hunt.yaml +29 -0
- secator/configs/workflows/wordpress.yaml +38 -0
- secator/cve.py +718 -0
- secator/decorators.py +7 -0
- secator/definitions.py +168 -0
- secator/exporters/__init__.py +14 -0
- secator/exporters/_base.py +3 -0
- secator/exporters/console.py +10 -0
- secator/exporters/csv.py +37 -0
- secator/exporters/gdrive.py +123 -0
- secator/exporters/json.py +16 -0
- secator/exporters/table.py +36 -0
- secator/exporters/txt.py +28 -0
- secator/hooks/__init__.py +0 -0
- secator/hooks/gcs.py +80 -0
- secator/hooks/mongodb.py +281 -0
- secator/installer.py +694 -0
- secator/loader.py +128 -0
- secator/output_types/__init__.py +49 -0
- secator/output_types/_base.py +108 -0
- secator/output_types/certificate.py +78 -0
- secator/output_types/domain.py +50 -0
- secator/output_types/error.py +42 -0
- secator/output_types/exploit.py +58 -0
- secator/output_types/info.py +24 -0
- secator/output_types/ip.py +47 -0
- secator/output_types/port.py +55 -0
- secator/output_types/progress.py +36 -0
- secator/output_types/record.py +36 -0
- secator/output_types/stat.py +41 -0
- secator/output_types/state.py +29 -0
- secator/output_types/subdomain.py +45 -0
- secator/output_types/tag.py +69 -0
- secator/output_types/target.py +38 -0
- secator/output_types/url.py +112 -0
- secator/output_types/user_account.py +41 -0
- secator/output_types/vulnerability.py +101 -0
- secator/output_types/warning.py +30 -0
- secator/report.py +140 -0
- secator/rich.py +130 -0
- secator/runners/__init__.py +14 -0
- secator/runners/_base.py +1240 -0
- secator/runners/_helpers.py +218 -0
- secator/runners/celery.py +18 -0
- secator/runners/command.py +1178 -0
- secator/runners/python.py +126 -0
- secator/runners/scan.py +87 -0
- secator/runners/task.py +81 -0
- secator/runners/workflow.py +168 -0
- secator/scans/__init__.py +29 -0
- secator/serializers/__init__.py +8 -0
- secator/serializers/dataclass.py +39 -0
- secator/serializers/json.py +45 -0
- secator/serializers/regex.py +25 -0
- secator/tasks/__init__.py +8 -0
- secator/tasks/_categories.py +487 -0
- secator/tasks/arjun.py +113 -0
- secator/tasks/arp.py +53 -0
- secator/tasks/arpscan.py +70 -0
- secator/tasks/bbot.py +372 -0
- secator/tasks/bup.py +118 -0
- secator/tasks/cariddi.py +193 -0
- secator/tasks/dalfox.py +87 -0
- secator/tasks/dirsearch.py +84 -0
- secator/tasks/dnsx.py +186 -0
- secator/tasks/feroxbuster.py +93 -0
- secator/tasks/ffuf.py +135 -0
- secator/tasks/fping.py +85 -0
- secator/tasks/gau.py +102 -0
- secator/tasks/getasn.py +60 -0
- secator/tasks/gf.py +36 -0
- secator/tasks/gitleaks.py +96 -0
- secator/tasks/gospider.py +84 -0
- secator/tasks/grype.py +109 -0
- secator/tasks/h8mail.py +75 -0
- secator/tasks/httpx.py +167 -0
- secator/tasks/jswhois.py +36 -0
- secator/tasks/katana.py +203 -0
- secator/tasks/maigret.py +87 -0
- secator/tasks/mapcidr.py +42 -0
- secator/tasks/msfconsole.py +179 -0
- secator/tasks/naabu.py +85 -0
- secator/tasks/nmap.py +487 -0
- secator/tasks/nuclei.py +151 -0
- secator/tasks/search_vulns.py +225 -0
- secator/tasks/searchsploit.py +109 -0
- secator/tasks/sshaudit.py +299 -0
- secator/tasks/subfinder.py +48 -0
- secator/tasks/testssl.py +283 -0
- secator/tasks/trivy.py +130 -0
- secator/tasks/trufflehog.py +240 -0
- secator/tasks/urlfinder.py +100 -0
- secator/tasks/wafw00f.py +106 -0
- secator/tasks/whois.py +34 -0
- secator/tasks/wpprobe.py +116 -0
- secator/tasks/wpscan.py +202 -0
- secator/tasks/x8.py +94 -0
- secator/tasks/xurlfind3r.py +83 -0
- secator/template.py +294 -0
- secator/thread.py +24 -0
- secator/tree.py +196 -0
- secator/utils.py +922 -0
- secator/utils_test.py +297 -0
- secator/workflows/__init__.py +29 -0
- secator-0.22.0.dist-info/METADATA +447 -0
- secator-0.22.0.dist-info/RECORD +150 -0
- secator-0.22.0.dist-info/WHEEL +4 -0
- secator-0.22.0.dist-info/entry_points.txt +2 -0
- secator-0.22.0.dist-info/licenses/LICENSE +60 -0
secator/cve.py
ADDED
|
@@ -0,0 +1,718 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
from packaging import version
|
|
4
|
+
from typing import List, Dict, Any, Optional, Tuple
|
|
5
|
+
|
|
6
|
+
from secator.utils import get_versions_from_string
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def extract_software_and_version(version_string: str) -> Tuple[Optional[str], Optional[str]]:
|
|
10
|
+
"""Extract software name and version from a version string."""
|
|
11
|
+
# Try to match software name followed by version
|
|
12
|
+
match = re.search(r'([a-zA-Z][a-zA-Z\s]*?)\s+([0-9]+\.[0-9]+(?:\.[0-9]+)*)', version_string.strip())
|
|
13
|
+
if match:
|
|
14
|
+
return match.group(1).strip().lower(), match.group(2).strip()
|
|
15
|
+
|
|
16
|
+
# If no software name, try to extract just version
|
|
17
|
+
versions = get_versions_from_string(version_string)
|
|
18
|
+
return (None, versions[0]) if versions else (None, None)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def compare_versions(v1: str, v2: str) -> int:
|
|
22
|
+
"""Compare versions. Returns -1 if v1<v2, 0 if equal, 1 if v1>v2."""
|
|
23
|
+
try:
|
|
24
|
+
parsed_v1 = version.parse(v1.strip())
|
|
25
|
+
parsed_v2 = version.parse(v2.strip())
|
|
26
|
+
|
|
27
|
+
if parsed_v1 < parsed_v2:
|
|
28
|
+
return -1
|
|
29
|
+
elif parsed_v1 > parsed_v2:
|
|
30
|
+
return 1
|
|
31
|
+
return 0
|
|
32
|
+
except Exception:
|
|
33
|
+
# Fallback to string comparison
|
|
34
|
+
if v1 < v2:
|
|
35
|
+
return -1
|
|
36
|
+
elif v1 > v2:
|
|
37
|
+
return 1
|
|
38
|
+
return 0
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def normalize_software_name(name: str) -> str:
|
|
42
|
+
"""Normalize software name for comparison."""
|
|
43
|
+
return name.lower().strip()
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def software_names_match(name1: Optional[str], name2: Optional[str]) -> bool:
|
|
47
|
+
"""Check if two software names match."""
|
|
48
|
+
if not name1 and not name2:
|
|
49
|
+
return True
|
|
50
|
+
if not name1 or not name2:
|
|
51
|
+
return True # Allow matching when one is missing
|
|
52
|
+
|
|
53
|
+
norm1 = normalize_software_name(name1)
|
|
54
|
+
norm2 = normalize_software_name(name2)
|
|
55
|
+
|
|
56
|
+
return norm1 in norm2 or norm2 in norm1
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def versions_match(current_version: str, target_version: str) -> bool:
|
|
60
|
+
"""Check if current version matches target version with flexible software name matching."""
|
|
61
|
+
current_sw, current_ver = extract_software_and_version(current_version)
|
|
62
|
+
target_sw, target_ver = extract_software_and_version(target_version)
|
|
63
|
+
|
|
64
|
+
# Both must have valid version numbers
|
|
65
|
+
if not current_ver or not target_ver:
|
|
66
|
+
return False
|
|
67
|
+
|
|
68
|
+
# Version numbers must match exactly
|
|
69
|
+
if compare_versions(current_ver, target_ver) != 0:
|
|
70
|
+
return False
|
|
71
|
+
|
|
72
|
+
# Check software name compatibility
|
|
73
|
+
return software_names_match(current_sw, target_sw)
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def parse_complex_version_ranges(version_str: str, current_version: str) -> bool:
|
|
77
|
+
"""
|
|
78
|
+
Parse complex version strings with multiple ranges and conditions.
|
|
79
|
+
Example: "Nginx Web Server versions 0.6.18 thru 1.20.0 before 1.20.1, Nginx plus versions R13 thru R23 before R23 P1" # noqa: E501
|
|
80
|
+
"""
|
|
81
|
+
current_sw, current_ver = extract_software_and_version(current_version)
|
|
82
|
+
|
|
83
|
+
if not current_ver:
|
|
84
|
+
return False
|
|
85
|
+
|
|
86
|
+
# Split by comma to handle multiple conditions
|
|
87
|
+
conditions = [cond.strip() for cond in version_str.split(',')]
|
|
88
|
+
|
|
89
|
+
for condition in conditions:
|
|
90
|
+
# Check if this condition matches the current software
|
|
91
|
+
condition_lower = condition.lower()
|
|
92
|
+
|
|
93
|
+
# Extract software name from condition
|
|
94
|
+
if current_sw:
|
|
95
|
+
# Check if condition mentions the same software
|
|
96
|
+
if current_sw not in condition_lower and not any(part in condition_lower for part in current_sw.split()):
|
|
97
|
+
continue
|
|
98
|
+
|
|
99
|
+
# Handle "versions X thru Y before Z" pattern
|
|
100
|
+
thru_before_match = re.search(r'versions?\s+([0-9.]+)\s+(?:thru|through|to)\s+([0-9.]+)\s+before\s+([0-9.]+)', condition, re.IGNORECASE) # noqa: E501
|
|
101
|
+
if thru_before_match:
|
|
102
|
+
start_ver = thru_before_match.group(1)
|
|
103
|
+
end_ver = thru_before_match.group(2)
|
|
104
|
+
before_ver = thru_before_match.group(3)
|
|
105
|
+
|
|
106
|
+
# Check if current version is in range [start_ver, end_ver] and before before_ver
|
|
107
|
+
if (compare_versions(current_ver, start_ver) >= 0 and
|
|
108
|
+
compare_versions(current_ver, end_ver) <= 0 and
|
|
109
|
+
compare_versions(current_ver, before_ver) < 0):
|
|
110
|
+
return True
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
# Handle "version X before Y" pattern
|
|
114
|
+
version_before_match = re.search(r'versions?\s+([0-9.]+)\s+before\s+([0-9.]+)', condition, re.IGNORECASE)
|
|
115
|
+
if version_before_match:
|
|
116
|
+
base_ver = version_before_match.group(1)
|
|
117
|
+
before_ver = version_before_match.group(2)
|
|
118
|
+
|
|
119
|
+
if (compare_versions(current_ver, base_ver) >= 0 and
|
|
120
|
+
compare_versions(current_ver, before_ver) < 0):
|
|
121
|
+
return True
|
|
122
|
+
|
|
123
|
+
# Handle simple ranges "X to Y" or "X thru Y"
|
|
124
|
+
range_match = re.search(r'([0-9.]+)\s+(?:to|thru|through)\s+([0-9.]+)', condition, re.IGNORECASE)
|
|
125
|
+
if range_match:
|
|
126
|
+
start_ver = range_match.group(1)
|
|
127
|
+
end_ver = range_match.group(2)
|
|
128
|
+
|
|
129
|
+
if (compare_versions(current_ver, start_ver) >= 0 and
|
|
130
|
+
compare_versions(current_ver, end_ver) <= 0):
|
|
131
|
+
return True
|
|
132
|
+
|
|
133
|
+
# Handle simple version match
|
|
134
|
+
versions_in_condition = get_versions_from_string(condition)
|
|
135
|
+
for ver in versions_in_condition:
|
|
136
|
+
if compare_versions(current_ver, ver) == 0:
|
|
137
|
+
return True
|
|
138
|
+
|
|
139
|
+
return False
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def parse_version_string_for_affected_version(version_str: str) -> str:
|
|
143
|
+
"""
|
|
144
|
+
Parse version string and return the affected version.
|
|
145
|
+
"""
|
|
146
|
+
# Handle "Fixed in" format with "Affected" in parentheses
|
|
147
|
+
if "fixed in" in version_str.lower() and "affected" in version_str.lower():
|
|
148
|
+
# Extract the affected version from parentheses
|
|
149
|
+
affected_match = re.search(r'$ \s*affected\s+([^)]+)\s* $ ', version_str, re.IGNORECASE)
|
|
150
|
+
if affected_match:
|
|
151
|
+
affected_part = affected_match.group(1).strip()
|
|
152
|
+
|
|
153
|
+
# Extract software name from "Fixed in" part (before the parentheses)
|
|
154
|
+
fixed_part = re.sub(r'$ .*? $ ', '', version_str).replace('Fixed in', '', 1).strip()
|
|
155
|
+
|
|
156
|
+
# Get software name from fixed part
|
|
157
|
+
fixed_sw, _ = extract_software_and_version(fixed_part)
|
|
158
|
+
|
|
159
|
+
# Check if affected part already has software name
|
|
160
|
+
affected_sw, affected_ver = extract_software_and_version(affected_part)
|
|
161
|
+
|
|
162
|
+
if fixed_sw and affected_ver:
|
|
163
|
+
result = f"{fixed_sw} {affected_ver}"
|
|
164
|
+
return result
|
|
165
|
+
else:
|
|
166
|
+
return affected_part
|
|
167
|
+
|
|
168
|
+
# Check for range patterns BEFORE checking for multiple versions
|
|
169
|
+
if ' to ' in version_str:
|
|
170
|
+
return version_str # Return as-is, will be handled as range
|
|
171
|
+
|
|
172
|
+
# Check for comma-separated versions
|
|
173
|
+
if ',' in version_str:
|
|
174
|
+
return version_str # Return as-is, will be handled as comma-separated
|
|
175
|
+
|
|
176
|
+
# Handle strings with multiple versions (like "Apache HTTP Server 2.4 2.4.49")
|
|
177
|
+
# but ONLY if no range keywords are present
|
|
178
|
+
if not any(keyword in version_str.lower() for keyword in ['to', 'thru', 'through', 'before', 'after']):
|
|
179
|
+
versions_in_string = get_versions_from_string(version_str)
|
|
180
|
+
|
|
181
|
+
if len(versions_in_string) >= 2:
|
|
182
|
+
# Find where the first version starts to extract software name
|
|
183
|
+
first_version = versions_in_string[0]
|
|
184
|
+
first_version_pos = version_str.find(first_version)
|
|
185
|
+
software_part = version_str[:first_version_pos].strip()
|
|
186
|
+
|
|
187
|
+
if software_part:
|
|
188
|
+
result = f"{software_part} {versions_in_string[-1]}"
|
|
189
|
+
return result
|
|
190
|
+
|
|
191
|
+
return version_str
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def check_version_against_entry(current_version: str, version_entry: Dict[str, Any]) -> bool:
|
|
195
|
+
"""Check if current version matches a single CVE version entry."""
|
|
196
|
+
# Skip non-affected entries
|
|
197
|
+
if version_entry.get('status') != 'affected':
|
|
198
|
+
return False
|
|
199
|
+
|
|
200
|
+
current_sw, current_ver = extract_software_and_version(current_version)
|
|
201
|
+
|
|
202
|
+
# Check changes array for unaffected versions
|
|
203
|
+
changes = version_entry.get('changes', [])
|
|
204
|
+
for change in changes:
|
|
205
|
+
if (change.get('status') == 'unaffected' and
|
|
206
|
+
change.get('at') and current_ver and
|
|
207
|
+
compare_versions(current_ver, change['at']) == 0):
|
|
208
|
+
return False
|
|
209
|
+
|
|
210
|
+
# Handle lessThan with semver - this means all versions >= base version are affected
|
|
211
|
+
# UNLESS they're specifically marked as unaffected in changes array
|
|
212
|
+
if (version_entry.get('lessThan') == '*' and
|
|
213
|
+
version_entry.get('versionType') == 'semver'):
|
|
214
|
+
base_version = version_entry.get('version', '')
|
|
215
|
+
if current_ver and base_version:
|
|
216
|
+
return compare_versions(current_ver, base_version) >= 0
|
|
217
|
+
|
|
218
|
+
# Handle lessThan with wildcard (for version ranges like "2.4*")
|
|
219
|
+
if ('lessThan' in version_entry and
|
|
220
|
+
version_entry.get('lessThan', '').endswith('*')):
|
|
221
|
+
less_than = version_entry['lessThan']
|
|
222
|
+
base_version = version_entry.get('version', '')
|
|
223
|
+
|
|
224
|
+
# Extract info from lessThan field
|
|
225
|
+
target_sw, target_base = extract_software_and_version(less_than.replace('*', '').strip())
|
|
226
|
+
|
|
227
|
+
if not current_ver or not target_base or not base_version:
|
|
228
|
+
return False
|
|
229
|
+
|
|
230
|
+
# Software must match if target has software name
|
|
231
|
+
if target_sw and current_sw:
|
|
232
|
+
if not software_names_match(current_sw, target_sw):
|
|
233
|
+
return False
|
|
234
|
+
elif target_sw and not current_sw:
|
|
235
|
+
return False
|
|
236
|
+
|
|
237
|
+
# Version must be >= base_version
|
|
238
|
+
if compare_versions(current_ver, base_version) < 0:
|
|
239
|
+
return False
|
|
240
|
+
|
|
241
|
+
# For "Apache HTTP Server 2.4*", current version must be 2.4.x, not 2.5.x
|
|
242
|
+
current_major_minor = '.'.join(current_ver.split('.')[:2])
|
|
243
|
+
target_major_minor = '.'.join(target_base.split('.')[:2])
|
|
244
|
+
|
|
245
|
+
return current_major_minor == target_major_minor
|
|
246
|
+
|
|
247
|
+
# Handle lessThanOrEqual
|
|
248
|
+
if 'lessThanOrEqual' in version_entry:
|
|
249
|
+
less_equal = version_entry['lessThanOrEqual']
|
|
250
|
+
target_sw, target_ver = extract_software_and_version(less_equal)
|
|
251
|
+
|
|
252
|
+
if target_sw and current_sw and not software_names_match(current_sw, target_sw):
|
|
253
|
+
return False
|
|
254
|
+
|
|
255
|
+
if current_ver and target_ver:
|
|
256
|
+
return compare_versions(current_ver, target_ver) <= 0
|
|
257
|
+
|
|
258
|
+
# Handle version field
|
|
259
|
+
version_str = version_entry.get('version', '')
|
|
260
|
+
if not version_str:
|
|
261
|
+
return False
|
|
262
|
+
|
|
263
|
+
# Check for complex version ranges first
|
|
264
|
+
if any(keyword in version_str.lower() for keyword in ['thru', 'through', 'before']) and ',' in version_str:
|
|
265
|
+
return parse_complex_version_ranges(version_str, current_version)
|
|
266
|
+
|
|
267
|
+
# Parse the version string to get the affected version
|
|
268
|
+
affected_version = parse_version_string_for_affected_version(version_str)
|
|
269
|
+
|
|
270
|
+
# Handle comma-separated versions
|
|
271
|
+
if ',' in affected_version:
|
|
272
|
+
for target in affected_version.split(','):
|
|
273
|
+
target = target.strip()
|
|
274
|
+
if versions_match(current_version, target):
|
|
275
|
+
return True
|
|
276
|
+
return False
|
|
277
|
+
|
|
278
|
+
# Handle ranges with "to"
|
|
279
|
+
if ' to ' in affected_version:
|
|
280
|
+
parts = affected_version.split(' to ')
|
|
281
|
+
if len(parts) == 2:
|
|
282
|
+
start_versions = get_versions_from_string(parts[0])
|
|
283
|
+
end_versions = get_versions_from_string(parts[1])
|
|
284
|
+
if start_versions and end_versions and current_ver:
|
|
285
|
+
return (compare_versions(current_ver, start_versions[0]) >= 0 and
|
|
286
|
+
compare_versions(current_ver, end_versions[0]) <= 0)
|
|
287
|
+
|
|
288
|
+
# Direct version matching
|
|
289
|
+
return versions_match(current_version, affected_version)
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def is_version_affected(current_version: str, versions_data: List[Dict[str, Any]]) -> bool:
|
|
293
|
+
"""
|
|
294
|
+
Check if the current version is affected by the CVE.
|
|
295
|
+
|
|
296
|
+
Args:
|
|
297
|
+
current_version (str): The current software version
|
|
298
|
+
versions_data (List[Dict[str, Any]]): List of CVE version objects
|
|
299
|
+
|
|
300
|
+
Returns:
|
|
301
|
+
bool: True if the version is affected, False otherwise
|
|
302
|
+
"""
|
|
303
|
+
for version_entry in versions_data:
|
|
304
|
+
if check_version_against_entry(current_version, version_entry):
|
|
305
|
+
return True
|
|
306
|
+
|
|
307
|
+
return False
|
|
308
|
+
|
|
309
|
+
|
|
310
|
+
def create_test_cases():
|
|
311
|
+
"""Create test cases for all the CVE JSON inputs provided."""
|
|
312
|
+
|
|
313
|
+
test_cases = [
|
|
314
|
+
{
|
|
315
|
+
"name": "Simple affected version - dnsmasq",
|
|
316
|
+
"versions": [{"status": "affected", "version": "dnsmasq 2.83"}],
|
|
317
|
+
"tests": [
|
|
318
|
+
("dnsmasq 2.83", True),
|
|
319
|
+
("dnsmasq 2.84", False),
|
|
320
|
+
("dnsmasq 2.82", False),
|
|
321
|
+
("dnsmasq 2.8", False),
|
|
322
|
+
("2.83", True), # No software name match
|
|
323
|
+
]
|
|
324
|
+
},
|
|
325
|
+
{
|
|
326
|
+
"name": "Multiple simple affected versions",
|
|
327
|
+
"versions": [
|
|
328
|
+
{"status": "affected", "version": "2.4.46"},
|
|
329
|
+
{"status": "affected", "version": "2.4.43"}
|
|
330
|
+
],
|
|
331
|
+
"tests": [
|
|
332
|
+
("2.4.46", True),
|
|
333
|
+
("2.4.43", True),
|
|
334
|
+
("2.4.44", False),
|
|
335
|
+
("2.4.47", False),
|
|
336
|
+
("2.4.42", False),
|
|
337
|
+
("2.4", False),
|
|
338
|
+
]
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
"name": "Multiple software in one entry",
|
|
342
|
+
"versions": [
|
|
343
|
+
{"version": "vsftpd 3.0.4, nginx 1.21.0, sendmail 8.17", "status": "affected"}
|
|
344
|
+
],
|
|
345
|
+
"tests": [
|
|
346
|
+
("vsftpd 3.0.4", True),
|
|
347
|
+
("nginx 1.21.0", True),
|
|
348
|
+
("1.21.0", True),
|
|
349
|
+
("sendmail 8.17", True),
|
|
350
|
+
("vsftpd 3.0.5", False),
|
|
351
|
+
("nginx 1.21.1", False),
|
|
352
|
+
("sendmail 8.16", False),
|
|
353
|
+
("3.0.4", True),
|
|
354
|
+
]
|
|
355
|
+
},
|
|
356
|
+
{
|
|
357
|
+
"name": "Complex nginx version ranges",
|
|
358
|
+
"versions": [
|
|
359
|
+
{
|
|
360
|
+
"status": "affected",
|
|
361
|
+
"version": "Nginx Web Server versions 0.6.18 thru 1.20.0 before 1.20.1, Nginx plus versions R13 thru R23 before R23 P1. Nginx plus version R24 before R24 P1" # noqa: E501
|
|
362
|
+
}
|
|
363
|
+
],
|
|
364
|
+
"tests": [
|
|
365
|
+
("nginx 1.18.0", True),
|
|
366
|
+
("nginx 1.20.0", True),
|
|
367
|
+
("nginx 0.6.18", True),
|
|
368
|
+
("nginx 1.20.1", False),
|
|
369
|
+
("nginx 0.6.17", False),
|
|
370
|
+
("nginx 1.21.0", False),
|
|
371
|
+
]
|
|
372
|
+
},
|
|
373
|
+
{
|
|
374
|
+
"name": "Apache with lessThan and custom versionType",
|
|
375
|
+
"versions": [
|
|
376
|
+
{
|
|
377
|
+
"lessThan": "Apache HTTP Server 2.4*",
|
|
378
|
+
"status": "affected",
|
|
379
|
+
"version": "2.4.7",
|
|
380
|
+
"versionType": "custom"
|
|
381
|
+
}
|
|
382
|
+
],
|
|
383
|
+
"tests": [
|
|
384
|
+
("Apache HTTP Server 2.4.7", True),
|
|
385
|
+
("Apache HTTP Server 2.4.8", True),
|
|
386
|
+
("Apache HTTP Server 2.4.6", False),
|
|
387
|
+
("Apache HTTP Server 2.5.0", False), # lessThan 2.4*
|
|
388
|
+
]
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
"name": "Simple version range",
|
|
392
|
+
"versions": [
|
|
393
|
+
{"status": "affected", "version": "2.4.20 to 2.4.43"}
|
|
394
|
+
],
|
|
395
|
+
"tests": [
|
|
396
|
+
("2.4.20", True),
|
|
397
|
+
("2.4.43", True),
|
|
398
|
+
("2.4.30", True),
|
|
399
|
+
("2.4.19", False),
|
|
400
|
+
("2.4.44", False),
|
|
401
|
+
]
|
|
402
|
+
},
|
|
403
|
+
{
|
|
404
|
+
"name": "Apache with lessThanOrEqual",
|
|
405
|
+
"versions": [
|
|
406
|
+
{
|
|
407
|
+
"lessThanOrEqual": "2.4.48",
|
|
408
|
+
"status": "affected",
|
|
409
|
+
"version": "Apache HTTP Server 2.4",
|
|
410
|
+
"versionType": "custom"
|
|
411
|
+
}
|
|
412
|
+
],
|
|
413
|
+
"tests": [
|
|
414
|
+
("Apache HTTP Server 2.4.48", True),
|
|
415
|
+
("Apache HTTP Server 2.4.30", True),
|
|
416
|
+
("Apache HTTP Server 2.4.0", True),
|
|
417
|
+
("Apache HTTP Server 2.4.49", False),
|
|
418
|
+
]
|
|
419
|
+
},
|
|
420
|
+
{
|
|
421
|
+
"name": "Apache specific version with software name",
|
|
422
|
+
"versions": [
|
|
423
|
+
{"status": "affected", "version": "Apache HTTP Server 2.4 2.4.49"}
|
|
424
|
+
],
|
|
425
|
+
"tests": [
|
|
426
|
+
("Apache HTTP Server 2.4.49", True),
|
|
427
|
+
("Apache HTTP Server 2.4.48", False),
|
|
428
|
+
("2.4.49", True), # No software name
|
|
429
|
+
]
|
|
430
|
+
},
|
|
431
|
+
{
|
|
432
|
+
"name": "Apache specific version 2.4.37",
|
|
433
|
+
"versions": [
|
|
434
|
+
{"status": "affected", "version": "Apache HTTP Server 2.4.37"}
|
|
435
|
+
],
|
|
436
|
+
"tests": [
|
|
437
|
+
("Apache HTTP Server 2.4.37", True),
|
|
438
|
+
("Apache HTTP Server 2.4.36", False),
|
|
439
|
+
("Apache HTTP Server 2.4.38", False),
|
|
440
|
+
]
|
|
441
|
+
},
|
|
442
|
+
{
|
|
443
|
+
"name": "Apache version range with software name",
|
|
444
|
+
"versions": [
|
|
445
|
+
{"status": "affected", "version": "Apache HTTP Server 2.4.0 to 2.4.37"}
|
|
446
|
+
],
|
|
447
|
+
"tests": [
|
|
448
|
+
("Apache HTTP Server 2.4.0", True),
|
|
449
|
+
("Apache HTTP Server 2.4.37", True),
|
|
450
|
+
("Apache HTTP Server 2.4.20", True),
|
|
451
|
+
("Apache HTTP Server 2.3.9", False),
|
|
452
|
+
("Apache HTTP Server 2.4.38", False),
|
|
453
|
+
]
|
|
454
|
+
},
|
|
455
|
+
{
|
|
456
|
+
"name": "Fixed in format",
|
|
457
|
+
"versions": [
|
|
458
|
+
{"status": "affected", "version": "Fixed in Apache HTTP Server 2.4.34 (Affected 2.4.33)"}
|
|
459
|
+
],
|
|
460
|
+
"tests": [
|
|
461
|
+
("Apache HTTP Server 2.4.33", True),
|
|
462
|
+
("Apache HTTP Server 2.4.34", False),
|
|
463
|
+
("Apache HTTP Server 2.4.32", False),
|
|
464
|
+
]
|
|
465
|
+
},
|
|
466
|
+
{
|
|
467
|
+
"name": "Up to and including format",
|
|
468
|
+
"versions": [
|
|
469
|
+
{"status": "affected", "version": "up to and including 2.78"}
|
|
470
|
+
],
|
|
471
|
+
"tests": [
|
|
472
|
+
("2.78", True),
|
|
473
|
+
("2.77", False),
|
|
474
|
+
("2.70", False),
|
|
475
|
+
("2.79", False),
|
|
476
|
+
]
|
|
477
|
+
},
|
|
478
|
+
{
|
|
479
|
+
"name": "Semver with lessThanOrEqual",
|
|
480
|
+
"versions": [
|
|
481
|
+
{
|
|
482
|
+
"lessThanOrEqual": "2.4.54",
|
|
483
|
+
"status": "affected",
|
|
484
|
+
"version": "2.4",
|
|
485
|
+
"versionType": "semver"
|
|
486
|
+
}
|
|
487
|
+
],
|
|
488
|
+
"tests": [
|
|
489
|
+
("2.4.54", True),
|
|
490
|
+
("2.4.50", True),
|
|
491
|
+
("2.4.0", True),
|
|
492
|
+
("2.4.55", False),
|
|
493
|
+
]
|
|
494
|
+
},
|
|
495
|
+
{
|
|
496
|
+
"name": "Nginx mainline and stable branches",
|
|
497
|
+
"versions": [
|
|
498
|
+
{
|
|
499
|
+
"version": "Mainline",
|
|
500
|
+
"status": "affected",
|
|
501
|
+
"lessThan": "1.23.2",
|
|
502
|
+
"versionType": "custom"
|
|
503
|
+
},
|
|
504
|
+
{
|
|
505
|
+
"version": "Stable",
|
|
506
|
+
"status": "affected",
|
|
507
|
+
"lessThan": "1.22.1",
|
|
508
|
+
"versionType": "custom"
|
|
509
|
+
}
|
|
510
|
+
],
|
|
511
|
+
"tests": [
|
|
512
|
+
("nginx mainline 1.23.1", False), # Complex branch logic, hard to determine
|
|
513
|
+
("nginx stable 1.22.0", False), # Complex branch logic, hard to determine
|
|
514
|
+
("nginx 1.23.2", False),
|
|
515
|
+
("nginx 1.22.1", False),
|
|
516
|
+
]
|
|
517
|
+
},
|
|
518
|
+
{
|
|
519
|
+
"name": "Version with changes array - unaffected versions",
|
|
520
|
+
"versions": [
|
|
521
|
+
{
|
|
522
|
+
"status": "affected",
|
|
523
|
+
"version": "1.5.13",
|
|
524
|
+
"lessThan": "*",
|
|
525
|
+
"changes": [
|
|
526
|
+
{"at": "1.26.2", "status": "unaffected"},
|
|
527
|
+
{"at": "1.27.1", "status": "unaffected"}
|
|
528
|
+
],
|
|
529
|
+
"versionType": "semver"
|
|
530
|
+
}
|
|
531
|
+
],
|
|
532
|
+
"tests": [
|
|
533
|
+
("1.26.2", False), # Unaffected version
|
|
534
|
+
("1.27.1", False), # Unaffected version
|
|
535
|
+
("1.20.0", True), # Between 1.5.13 and 1.26.2
|
|
536
|
+
("1.5.12", False), # Below affected version
|
|
537
|
+
("1.5.13", True), # Exact affected version
|
|
538
|
+
]
|
|
539
|
+
},
|
|
540
|
+
{
|
|
541
|
+
"name": "Another version with changes array",
|
|
542
|
+
"versions": [
|
|
543
|
+
{
|
|
544
|
+
"changes": [
|
|
545
|
+
{"at": "1.27.4", "status": "unaffected"},
|
|
546
|
+
{"at": "1.26.3", "status": "unaffected"}
|
|
547
|
+
],
|
|
548
|
+
"lessThan": "*",
|
|
549
|
+
"status": "affected",
|
|
550
|
+
"version": "1.11.4",
|
|
551
|
+
"versionType": "semver"
|
|
552
|
+
}
|
|
553
|
+
],
|
|
554
|
+
"tests": [
|
|
555
|
+
("1.27.4", False), # Unaffected version
|
|
556
|
+
("1.26.3", False), # Unaffected version
|
|
557
|
+
("1.20.0", True), # Between 1.11.4 and unaffected versions
|
|
558
|
+
("1.11.4", True), # Exact affected version
|
|
559
|
+
("1.11.3", False), # Below affected version
|
|
560
|
+
]
|
|
561
|
+
}
|
|
562
|
+
]
|
|
563
|
+
|
|
564
|
+
return test_cases
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
def run_all_tests():
|
|
568
|
+
"""Run all test cases and display results."""
|
|
569
|
+
test_cases = create_test_cases()
|
|
570
|
+
|
|
571
|
+
total_tests = 0
|
|
572
|
+
passed_tests = 0
|
|
573
|
+
failed_tests = []
|
|
574
|
+
|
|
575
|
+
print("๐งช Running comprehensive CVE version parsing tests...\n")
|
|
576
|
+
print("=" * 80)
|
|
577
|
+
|
|
578
|
+
for i, test_case in enumerate(test_cases, 1):
|
|
579
|
+
print(f"\n๐ Test Case {i}: {test_case['name']}")
|
|
580
|
+
print("-" * 60)
|
|
581
|
+
|
|
582
|
+
versions_data = test_case['versions']
|
|
583
|
+
print(f"๐ CVE Data: {json.dumps(versions_data, indent=2)}")
|
|
584
|
+
print("\n๐ Test Results:")
|
|
585
|
+
|
|
586
|
+
case_passed = 0
|
|
587
|
+
case_total = 0
|
|
588
|
+
|
|
589
|
+
for current_version, expected_result in test_case['tests']:
|
|
590
|
+
total_tests += 1
|
|
591
|
+
case_total += 1
|
|
592
|
+
|
|
593
|
+
try:
|
|
594
|
+
actual_result = is_version_affected(current_version, versions_data)
|
|
595
|
+
|
|
596
|
+
if actual_result == expected_result:
|
|
597
|
+
passed_tests += 1
|
|
598
|
+
case_passed += 1
|
|
599
|
+
status = "โ
PASS"
|
|
600
|
+
else:
|
|
601
|
+
status = "โ FAIL"
|
|
602
|
+
failed_tests.append({
|
|
603
|
+
'case': test_case['name'],
|
|
604
|
+
'version': current_version,
|
|
605
|
+
'expected': expected_result,
|
|
606
|
+
'actual': actual_result
|
|
607
|
+
})
|
|
608
|
+
|
|
609
|
+
print(f" {status} | Version: {current_version:<30} | Expected: {str(expected_result):<5} | Got: {str(actual_result):<5}") # noqa: E501
|
|
610
|
+
|
|
611
|
+
except Exception as e:
|
|
612
|
+
status = "๐ฅ ERROR"
|
|
613
|
+
failed_tests.append({
|
|
614
|
+
'case': test_case['name'],
|
|
615
|
+
'version': current_version,
|
|
616
|
+
'expected': expected_result,
|
|
617
|
+
'error': str(e)
|
|
618
|
+
})
|
|
619
|
+
print(f" {status} | Version: {current_version:<30} | Error: {str(e)}")
|
|
620
|
+
|
|
621
|
+
print(f"\n๐ Case Summary: {case_passed}/{case_total} tests passed")
|
|
622
|
+
|
|
623
|
+
# Final summary
|
|
624
|
+
print("\n" + "=" * 80)
|
|
625
|
+
print("๐ FINAL TEST SUMMARY")
|
|
626
|
+
print("=" * 80)
|
|
627
|
+
print(f"โ
Total Tests Passed: {passed_tests}")
|
|
628
|
+
print(f"โ Total Tests Failed: {len(failed_tests)}")
|
|
629
|
+
print(f"๐ Success Rate: {(passed_tests/total_tests)*100:.1f}%")
|
|
630
|
+
|
|
631
|
+
if failed_tests:
|
|
632
|
+
print("\n๐ FAILED TEST DETAILS:")
|
|
633
|
+
print("-" * 40)
|
|
634
|
+
for i, failure in enumerate(failed_tests, 1):
|
|
635
|
+
print(f"{i}. Test Case: {failure['case']}")
|
|
636
|
+
print(f" Version: {failure['version']}")
|
|
637
|
+
if 'error' in failure:
|
|
638
|
+
print(f" Error: {failure['error']}")
|
|
639
|
+
else:
|
|
640
|
+
print(f" Expected: {failure['expected']}, Got: {failure['actual']}")
|
|
641
|
+
print()
|
|
642
|
+
|
|
643
|
+
return passed_tests == total_tests
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
def run_specific_test(test_name: str):
|
|
647
|
+
"""Run a specific test case by name."""
|
|
648
|
+
test_cases = create_test_cases()
|
|
649
|
+
|
|
650
|
+
for test_case in test_cases:
|
|
651
|
+
if test_name.lower() in test_case['name'].lower():
|
|
652
|
+
print(f"๐งช Running Test: {test_case['name']}")
|
|
653
|
+
print("=" * 60)
|
|
654
|
+
|
|
655
|
+
versions_data = test_case['versions']
|
|
656
|
+
print(f"CVE Data: {json.dumps(versions_data, indent=2)}\n")
|
|
657
|
+
|
|
658
|
+
for current_version, expected_result in test_case['tests']:
|
|
659
|
+
actual_result = is_version_affected(current_version, versions_data)
|
|
660
|
+
status = "โ
PASS" if actual_result == expected_result else "โ FAIL"
|
|
661
|
+
print(f"{status} | {current_version} -> Expected: {expected_result}, Got: {actual_result}")
|
|
662
|
+
|
|
663
|
+
return
|
|
664
|
+
|
|
665
|
+
print(f"โ Test case '{test_name}' not found!")
|
|
666
|
+
|
|
667
|
+
|
|
668
|
+
def interactive_test():
|
|
669
|
+
"""Interactive testing function for manual testing."""
|
|
670
|
+
print("๐งช Interactive CVE Version Tester")
|
|
671
|
+
print("=" * 40)
|
|
672
|
+
print("Enter 'quit' to exit\n")
|
|
673
|
+
|
|
674
|
+
while True:
|
|
675
|
+
try:
|
|
676
|
+
print("Enter current version (or 'quit' to exit):")
|
|
677
|
+
current_version = input("> ").strip()
|
|
678
|
+
|
|
679
|
+
if current_version.lower() == 'quit':
|
|
680
|
+
break
|
|
681
|
+
|
|
682
|
+
print("\nEnter CVE versions JSON (paste the versions array):")
|
|
683
|
+
cve_input = input("> ").strip()
|
|
684
|
+
|
|
685
|
+
# Try to parse the JSON
|
|
686
|
+
try:
|
|
687
|
+
versions_data = json.loads(cve_input)
|
|
688
|
+
if isinstance(versions_data, dict):
|
|
689
|
+
versions_data = [versions_data] # Convert single object to array
|
|
690
|
+
|
|
691
|
+
result = is_version_affected(current_version, versions_data)
|
|
692
|
+
|
|
693
|
+
print(f"\n๐ฏ Result: Version {current_version} is {'AFFECTED' if result else 'NOT AFFECTED'}")
|
|
694
|
+
print("-" * 40)
|
|
695
|
+
|
|
696
|
+
except json.JSONDecodeError:
|
|
697
|
+
print("โ Invalid JSON format. Please try again.")
|
|
698
|
+
except Exception as e:
|
|
699
|
+
print(f"โ Error: {str(e)}")
|
|
700
|
+
|
|
701
|
+
except KeyboardInterrupt:
|
|
702
|
+
print("\n๐ Goodbye!")
|
|
703
|
+
break
|
|
704
|
+
except Exception as e:
|
|
705
|
+
print(f"โ Unexpected error: {str(e)}")
|
|
706
|
+
|
|
707
|
+
|
|
708
|
+
if __name__ == "__main__":
|
|
709
|
+
# Run all tests
|
|
710
|
+
success = run_all_tests()
|
|
711
|
+
|
|
712
|
+
# Optionally run specific tests
|
|
713
|
+
# run_specific_test("nginx")
|
|
714
|
+
|
|
715
|
+
# Optionally run interactive tester
|
|
716
|
+
# interactive_test()
|
|
717
|
+
|
|
718
|
+
print(f"\n๐ All tests {'PASSED' if success else 'COMPLETED with failures'}!")
|