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.
Files changed (150) hide show
  1. secator/.gitignore +162 -0
  2. secator/__init__.py +0 -0
  3. secator/celery.py +453 -0
  4. secator/celery_signals.py +138 -0
  5. secator/celery_utils.py +320 -0
  6. secator/cli.py +2035 -0
  7. secator/cli_helper.py +395 -0
  8. secator/click.py +87 -0
  9. secator/config.py +670 -0
  10. secator/configs/__init__.py +0 -0
  11. secator/configs/profiles/__init__.py +0 -0
  12. secator/configs/profiles/aggressive.yaml +8 -0
  13. secator/configs/profiles/all_ports.yaml +7 -0
  14. secator/configs/profiles/full.yaml +31 -0
  15. secator/configs/profiles/http_headless.yaml +7 -0
  16. secator/configs/profiles/http_record.yaml +8 -0
  17. secator/configs/profiles/insane.yaml +8 -0
  18. secator/configs/profiles/paranoid.yaml +8 -0
  19. secator/configs/profiles/passive.yaml +11 -0
  20. secator/configs/profiles/polite.yaml +8 -0
  21. secator/configs/profiles/sneaky.yaml +8 -0
  22. secator/configs/profiles/tor.yaml +5 -0
  23. secator/configs/scans/__init__.py +0 -0
  24. secator/configs/scans/domain.yaml +31 -0
  25. secator/configs/scans/host.yaml +23 -0
  26. secator/configs/scans/network.yaml +30 -0
  27. secator/configs/scans/subdomain.yaml +27 -0
  28. secator/configs/scans/url.yaml +19 -0
  29. secator/configs/workflows/__init__.py +0 -0
  30. secator/configs/workflows/cidr_recon.yaml +48 -0
  31. secator/configs/workflows/code_scan.yaml +29 -0
  32. secator/configs/workflows/domain_recon.yaml +46 -0
  33. secator/configs/workflows/host_recon.yaml +95 -0
  34. secator/configs/workflows/subdomain_recon.yaml +120 -0
  35. secator/configs/workflows/url_bypass.yaml +15 -0
  36. secator/configs/workflows/url_crawl.yaml +98 -0
  37. secator/configs/workflows/url_dirsearch.yaml +62 -0
  38. secator/configs/workflows/url_fuzz.yaml +68 -0
  39. secator/configs/workflows/url_params_fuzz.yaml +66 -0
  40. secator/configs/workflows/url_secrets_hunt.yaml +23 -0
  41. secator/configs/workflows/url_vuln.yaml +91 -0
  42. secator/configs/workflows/user_hunt.yaml +29 -0
  43. secator/configs/workflows/wordpress.yaml +38 -0
  44. secator/cve.py +718 -0
  45. secator/decorators.py +7 -0
  46. secator/definitions.py +168 -0
  47. secator/exporters/__init__.py +14 -0
  48. secator/exporters/_base.py +3 -0
  49. secator/exporters/console.py +10 -0
  50. secator/exporters/csv.py +37 -0
  51. secator/exporters/gdrive.py +123 -0
  52. secator/exporters/json.py +16 -0
  53. secator/exporters/table.py +36 -0
  54. secator/exporters/txt.py +28 -0
  55. secator/hooks/__init__.py +0 -0
  56. secator/hooks/gcs.py +80 -0
  57. secator/hooks/mongodb.py +281 -0
  58. secator/installer.py +694 -0
  59. secator/loader.py +128 -0
  60. secator/output_types/__init__.py +49 -0
  61. secator/output_types/_base.py +108 -0
  62. secator/output_types/certificate.py +78 -0
  63. secator/output_types/domain.py +50 -0
  64. secator/output_types/error.py +42 -0
  65. secator/output_types/exploit.py +58 -0
  66. secator/output_types/info.py +24 -0
  67. secator/output_types/ip.py +47 -0
  68. secator/output_types/port.py +55 -0
  69. secator/output_types/progress.py +36 -0
  70. secator/output_types/record.py +36 -0
  71. secator/output_types/stat.py +41 -0
  72. secator/output_types/state.py +29 -0
  73. secator/output_types/subdomain.py +45 -0
  74. secator/output_types/tag.py +69 -0
  75. secator/output_types/target.py +38 -0
  76. secator/output_types/url.py +112 -0
  77. secator/output_types/user_account.py +41 -0
  78. secator/output_types/vulnerability.py +101 -0
  79. secator/output_types/warning.py +30 -0
  80. secator/report.py +140 -0
  81. secator/rich.py +130 -0
  82. secator/runners/__init__.py +14 -0
  83. secator/runners/_base.py +1240 -0
  84. secator/runners/_helpers.py +218 -0
  85. secator/runners/celery.py +18 -0
  86. secator/runners/command.py +1178 -0
  87. secator/runners/python.py +126 -0
  88. secator/runners/scan.py +87 -0
  89. secator/runners/task.py +81 -0
  90. secator/runners/workflow.py +168 -0
  91. secator/scans/__init__.py +29 -0
  92. secator/serializers/__init__.py +8 -0
  93. secator/serializers/dataclass.py +39 -0
  94. secator/serializers/json.py +45 -0
  95. secator/serializers/regex.py +25 -0
  96. secator/tasks/__init__.py +8 -0
  97. secator/tasks/_categories.py +487 -0
  98. secator/tasks/arjun.py +113 -0
  99. secator/tasks/arp.py +53 -0
  100. secator/tasks/arpscan.py +70 -0
  101. secator/tasks/bbot.py +372 -0
  102. secator/tasks/bup.py +118 -0
  103. secator/tasks/cariddi.py +193 -0
  104. secator/tasks/dalfox.py +87 -0
  105. secator/tasks/dirsearch.py +84 -0
  106. secator/tasks/dnsx.py +186 -0
  107. secator/tasks/feroxbuster.py +93 -0
  108. secator/tasks/ffuf.py +135 -0
  109. secator/tasks/fping.py +85 -0
  110. secator/tasks/gau.py +102 -0
  111. secator/tasks/getasn.py +60 -0
  112. secator/tasks/gf.py +36 -0
  113. secator/tasks/gitleaks.py +96 -0
  114. secator/tasks/gospider.py +84 -0
  115. secator/tasks/grype.py +109 -0
  116. secator/tasks/h8mail.py +75 -0
  117. secator/tasks/httpx.py +167 -0
  118. secator/tasks/jswhois.py +36 -0
  119. secator/tasks/katana.py +203 -0
  120. secator/tasks/maigret.py +87 -0
  121. secator/tasks/mapcidr.py +42 -0
  122. secator/tasks/msfconsole.py +179 -0
  123. secator/tasks/naabu.py +85 -0
  124. secator/tasks/nmap.py +487 -0
  125. secator/tasks/nuclei.py +151 -0
  126. secator/tasks/search_vulns.py +225 -0
  127. secator/tasks/searchsploit.py +109 -0
  128. secator/tasks/sshaudit.py +299 -0
  129. secator/tasks/subfinder.py +48 -0
  130. secator/tasks/testssl.py +283 -0
  131. secator/tasks/trivy.py +130 -0
  132. secator/tasks/trufflehog.py +240 -0
  133. secator/tasks/urlfinder.py +100 -0
  134. secator/tasks/wafw00f.py +106 -0
  135. secator/tasks/whois.py +34 -0
  136. secator/tasks/wpprobe.py +116 -0
  137. secator/tasks/wpscan.py +202 -0
  138. secator/tasks/x8.py +94 -0
  139. secator/tasks/xurlfind3r.py +83 -0
  140. secator/template.py +294 -0
  141. secator/thread.py +24 -0
  142. secator/tree.py +196 -0
  143. secator/utils.py +922 -0
  144. secator/utils_test.py +297 -0
  145. secator/workflows/__init__.py +29 -0
  146. secator-0.22.0.dist-info/METADATA +447 -0
  147. secator-0.22.0.dist-info/RECORD +150 -0
  148. secator-0.22.0.dist-info/WHEEL +4 -0
  149. secator-0.22.0.dist-info/entry_points.txt +2 -0
  150. 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'}!")