nmap-xml2xlsx 1.0.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.
- nmap_parser/__init__.py +16 -0
- nmap_parser/__main__.py +22 -0
- nmap_parser/cli.py +111 -0
- nmap_parser/core.py +441 -0
- nmap_parser/gui.py +256 -0
- nmap_xml2xlsx-1.0.0.dist-info/METADATA +442 -0
- nmap_xml2xlsx-1.0.0.dist-info/RECORD +10 -0
- nmap_xml2xlsx-1.0.0.dist-info/WHEEL +5 -0
- nmap_xml2xlsx-1.0.0.dist-info/entry_points.txt +6 -0
- nmap_xml2xlsx-1.0.0.dist-info/top_level.txt +1 -0
nmap_parser/__init__.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
@File : __init__.py
|
|
5
|
+
@Time : 2026/06/15
|
|
6
|
+
@Author : test233
|
|
7
|
+
@Version : 1.0.0
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
Nmap Parser
|
|
12
|
+
Convert Nmap XML scan results to Excel format with GUI and CLI support.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
__version__ = "1.0.0"
|
|
16
|
+
__author__ = "test233"
|
nmap_parser/__main__.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
@File : __main__.py
|
|
5
|
+
@Time : 2026/06/15
|
|
6
|
+
@Author : test233
|
|
7
|
+
@Version : 1.0.0
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
Entry point for python -m nmap_parser
|
|
12
|
+
Launches GUI by default, or CLI if arguments are provided.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import sys
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
if len(sys.argv) > 1:
|
|
19
|
+
from nmap_parser.cli import main
|
|
20
|
+
else:
|
|
21
|
+
from nmap_parser.gui import main
|
|
22
|
+
main()
|
nmap_parser/cli.py
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
@File : cli.py
|
|
5
|
+
@Time : 2026/06/15
|
|
6
|
+
@Author : test233
|
|
7
|
+
@Version : 1.0.0
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
Command Line Interface for Nmap Parser
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import sys
|
|
15
|
+
import argparse
|
|
16
|
+
|
|
17
|
+
try:
|
|
18
|
+
from .core import NmapConverter
|
|
19
|
+
except ImportError:
|
|
20
|
+
from core import NmapConverter # type: ignore
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def main():
|
|
24
|
+
"""Main entry point for CLI"""
|
|
25
|
+
converter = NmapConverter()
|
|
26
|
+
|
|
27
|
+
parser = argparse.ArgumentParser(
|
|
28
|
+
description='Nmap XML Parser - Generate Excel Report (nmap-parser)',
|
|
29
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
30
|
+
epilog="""
|
|
31
|
+
Examples:
|
|
32
|
+
1. Parse a single XML file:
|
|
33
|
+
nmap-parser scan.xml report.xlsx
|
|
34
|
+
|
|
35
|
+
2. Parse a directory (non-recursive):
|
|
36
|
+
nmap-parser ./nmap_scans report.xlsx
|
|
37
|
+
|
|
38
|
+
3. Parse a directory (recursive):
|
|
39
|
+
nmap-parser ./nmap_scans report.xlsx -r
|
|
40
|
+
|
|
41
|
+
4. Launch GUI mode:
|
|
42
|
+
nmap-parser-gui
|
|
43
|
+
"""
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
parser.add_argument('input', help='Input path (single XML file or directory)')
|
|
47
|
+
parser.add_argument('output', help='Output Excel file path (.xlsx)')
|
|
48
|
+
parser.add_argument('-r', '--recursive', action='store_true',
|
|
49
|
+
help='Recursively search for XML files in directory')
|
|
50
|
+
parser.add_argument('-q', '--quiet', action='store_true',
|
|
51
|
+
help='Quiet mode, suppress log output')
|
|
52
|
+
parser.add_argument('-v', '--version', action='version',
|
|
53
|
+
version='nmap-parser 1.0.0')
|
|
54
|
+
|
|
55
|
+
args = parser.parse_args()
|
|
56
|
+
|
|
57
|
+
if not args.output.endswith('.xlsx'):
|
|
58
|
+
args.output += '.xlsx'
|
|
59
|
+
|
|
60
|
+
try:
|
|
61
|
+
xml_files = converter.get_xml_files(args.input, args.recursive)
|
|
62
|
+
if not xml_files:
|
|
63
|
+
if not args.quiet:
|
|
64
|
+
print(f"{converter.emoji_error} No XML files found")
|
|
65
|
+
sys.exit(1)
|
|
66
|
+
|
|
67
|
+
if not args.quiet:
|
|
68
|
+
print(f"{converter.emoji_scan} Found {len(xml_files)} XML file(s)")
|
|
69
|
+
print(f"{converter.emoji_scan} Parsing...")
|
|
70
|
+
|
|
71
|
+
all_data = {
|
|
72
|
+
"task_info": [],
|
|
73
|
+
"hosts": [],
|
|
74
|
+
"ports": [],
|
|
75
|
+
"vulns": []
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
for xml_file in xml_files:
|
|
79
|
+
try:
|
|
80
|
+
file_data = converter.parse_nmap_xml(xml_file)
|
|
81
|
+
if file_data["task_info"]:
|
|
82
|
+
all_data["task_info"].append(file_data["task_info"])
|
|
83
|
+
all_data["hosts"].extend(file_data["hosts"])
|
|
84
|
+
all_data["ports"].extend(file_data["ports"])
|
|
85
|
+
all_data["vulns"].extend(file_data["vulns"])
|
|
86
|
+
|
|
87
|
+
if not args.quiet:
|
|
88
|
+
print(f" {converter.emoji_success} {xml_file}")
|
|
89
|
+
except Exception as e:
|
|
90
|
+
if not args.quiet:
|
|
91
|
+
print(f" {converter.emoji_error} {xml_file}: {str(e)}")
|
|
92
|
+
|
|
93
|
+
converter.write_to_xlsx(all_data, args.output)
|
|
94
|
+
|
|
95
|
+
if not args.quiet:
|
|
96
|
+
stats = converter.get_summary_stats(all_data, len(xml_files))
|
|
97
|
+
print(f"\n{converter.emoji_stats} Summary:")
|
|
98
|
+
print(f" Files: {stats['files_count']}")
|
|
99
|
+
print(f" Hosts: {stats['hosts_total']} (up: {stats['hosts_up']})")
|
|
100
|
+
print(f" Ports: {stats['ports_total']} (open: {stats['ports_open']})")
|
|
101
|
+
print(f" Scripts/Vulns: {stats['vulns_total']}")
|
|
102
|
+
print(f"\n{converter.emoji_success} Done! Output: {args.output}")
|
|
103
|
+
|
|
104
|
+
except Exception as e:
|
|
105
|
+
if not args.quiet:
|
|
106
|
+
print(f"{converter.emoji_error} Error: {str(e)}")
|
|
107
|
+
sys.exit(1)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
if __name__ == "__main__":
|
|
111
|
+
main()
|
nmap_parser/core.py
ADDED
|
@@ -0,0 +1,441 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
@File : core.py
|
|
5
|
+
@Time : 2026/06/15
|
|
6
|
+
@Author : test233
|
|
7
|
+
@Version : 1.0.0
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
Core conversion logic for Nmap XML to Excel
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
import os
|
|
15
|
+
import xml.etree.ElementTree as ET
|
|
16
|
+
from typing import List, Dict, Any
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
|
|
19
|
+
import xlsxwriter
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class NmapConverter:
|
|
23
|
+
"""Nmap XML to Excel converter"""
|
|
24
|
+
|
|
25
|
+
def __init__(self):
|
|
26
|
+
self.emoji_success = "[OK]"
|
|
27
|
+
self.emoji_info = "[*]"
|
|
28
|
+
self.emoji_warning = "[!]"
|
|
29
|
+
self.emoji_error = "[X]"
|
|
30
|
+
self.emoji_scan = "[>]"
|
|
31
|
+
self.emoji_file = "[F]"
|
|
32
|
+
self.emoji_folder = "[D]"
|
|
33
|
+
self.emoji_excel = "[E]"
|
|
34
|
+
self.emoji_stats = "[=]"
|
|
35
|
+
|
|
36
|
+
def get_xml_files(self, input_path: str, recursive: bool = False) -> List[str]:
|
|
37
|
+
"""Get all XML files under the given path"""
|
|
38
|
+
xml_files = []
|
|
39
|
+
input_path = os.path.abspath(input_path)
|
|
40
|
+
|
|
41
|
+
if os.path.isfile(input_path):
|
|
42
|
+
if input_path.endswith('.xml'):
|
|
43
|
+
xml_files.append(input_path)
|
|
44
|
+
return xml_files
|
|
45
|
+
|
|
46
|
+
if os.path.isdir(input_path):
|
|
47
|
+
search_pattern = "**/*.xml" if recursive else "*.xml"
|
|
48
|
+
for xml_file in Path(input_path).glob(search_pattern):
|
|
49
|
+
xml_files.append(str(xml_file))
|
|
50
|
+
|
|
51
|
+
xml_files.sort()
|
|
52
|
+
return xml_files
|
|
53
|
+
|
|
54
|
+
return []
|
|
55
|
+
|
|
56
|
+
def parse_nmap_xml(self, xml_path: str) -> Dict[str, Any]:
|
|
57
|
+
"""Parse a single Nmap XML file"""
|
|
58
|
+
result = {
|
|
59
|
+
"task_info": {},
|
|
60
|
+
"hosts": [],
|
|
61
|
+
"ports": [],
|
|
62
|
+
"vulns": []
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
tree = ET.parse(xml_path)
|
|
67
|
+
root = tree.getroot()
|
|
68
|
+
filename = os.path.basename(xml_path)
|
|
69
|
+
|
|
70
|
+
result["task_info"] = {
|
|
71
|
+
"xml_file": filename,
|
|
72
|
+
"scanner": root.get('scanner', ''),
|
|
73
|
+
"nmap_version": root.get('version', ''),
|
|
74
|
+
"scan_args": root.get('args', ''),
|
|
75
|
+
"start_time": root.get('start', ''),
|
|
76
|
+
"start_time_str": root.get('startstr', ''),
|
|
77
|
+
"xml_output_version": root.get('xmloutputversion', ''),
|
|
78
|
+
"total_hosts": "",
|
|
79
|
+
"up_hosts": "",
|
|
80
|
+
"down_hosts": "",
|
|
81
|
+
"finish_time": "",
|
|
82
|
+
"finish_time_str": "",
|
|
83
|
+
"elapsed_time": "",
|
|
84
|
+
"exit_status": ""
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
runstats = root.find('runstats/finished')
|
|
88
|
+
if runstats is not None:
|
|
89
|
+
result["task_info"]["finish_time"] = runstats.get('time', '')
|
|
90
|
+
result["task_info"]["finish_time_str"] = runstats.get('timestr', '')
|
|
91
|
+
result["task_info"]["elapsed_time"] = runstats.get('elapsed', '')
|
|
92
|
+
result["task_info"]["exit_status"] = runstats.get('exit', '')
|
|
93
|
+
|
|
94
|
+
hoststats = root.find('runstats/hosts')
|
|
95
|
+
if hoststats is not None:
|
|
96
|
+
result["task_info"]["total_hosts"] = hoststats.get('total', '')
|
|
97
|
+
result["task_info"]["up_hosts"] = hoststats.get('up', '')
|
|
98
|
+
result["task_info"]["down_hosts"] = hoststats.get('down', '')
|
|
99
|
+
|
|
100
|
+
for host in root.findall('host'):
|
|
101
|
+
host_info = {
|
|
102
|
+
"xml_file": filename,
|
|
103
|
+
"ip_address": "",
|
|
104
|
+
"hostname": "",
|
|
105
|
+
"status": "",
|
|
106
|
+
"status_reason": "",
|
|
107
|
+
"status_ttl": "",
|
|
108
|
+
"start_time": host.get('starttime', ''),
|
|
109
|
+
"end_time": host.get('endtime', '')
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
addr = host.find('address')
|
|
113
|
+
if addr is not None:
|
|
114
|
+
host_info["ip_address"] = addr.get('addr', '')
|
|
115
|
+
|
|
116
|
+
hostnames = host.find('hostnames')
|
|
117
|
+
if hostnames is not None:
|
|
118
|
+
hostname_elem = hostnames.find('hostname')
|
|
119
|
+
if hostname_elem is not None:
|
|
120
|
+
host_info["hostname"] = hostname_elem.get('name', '')
|
|
121
|
+
|
|
122
|
+
status = host.find('status')
|
|
123
|
+
if status is not None:
|
|
124
|
+
host_info["status"] = status.get('state', '')
|
|
125
|
+
host_info["status_reason"] = status.get('reason', '')
|
|
126
|
+
host_info["status_ttl"] = status.get('reason_ttl', '')
|
|
127
|
+
|
|
128
|
+
result["hosts"].append(host_info)
|
|
129
|
+
|
|
130
|
+
ports = host.find('ports')
|
|
131
|
+
if ports is not None:
|
|
132
|
+
for port in ports.findall('port'):
|
|
133
|
+
port_info = {
|
|
134
|
+
"xml_file": filename,
|
|
135
|
+
"ip_address": host_info["ip_address"],
|
|
136
|
+
"port_id": port.get('portid', ''),
|
|
137
|
+
"protocol": port.get('protocol', ''),
|
|
138
|
+
"state": "",
|
|
139
|
+
"state_reason": "",
|
|
140
|
+
"state_ttl": "",
|
|
141
|
+
"service_name": "",
|
|
142
|
+
"service_product": "",
|
|
143
|
+
"service_version": "",
|
|
144
|
+
"service_extrainfo": "",
|
|
145
|
+
"service_method": "",
|
|
146
|
+
"service_conf": "",
|
|
147
|
+
"cpe": ""
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
state = port.find('state')
|
|
151
|
+
if state is not None:
|
|
152
|
+
port_info["state"] = state.get('state', '')
|
|
153
|
+
port_info["state_reason"] = state.get('reason', '')
|
|
154
|
+
port_info["state_ttl"] = state.get('reason_ttl', '')
|
|
155
|
+
|
|
156
|
+
service = port.find('service')
|
|
157
|
+
if service is not None:
|
|
158
|
+
port_info["service_name"] = service.get('name', '')
|
|
159
|
+
port_info["service_product"] = service.get('product', '')
|
|
160
|
+
port_info["service_version"] = service.get('version', '')
|
|
161
|
+
port_info["service_extrainfo"] = service.get('extrainfo', '')
|
|
162
|
+
port_info["service_method"] = service.get('method', '')
|
|
163
|
+
port_info["service_conf"] = service.get('conf', '')
|
|
164
|
+
|
|
165
|
+
cpe = port.find('cpe')
|
|
166
|
+
if cpe is not None:
|
|
167
|
+
port_info["cpe"] = cpe.text if cpe.text else ''
|
|
168
|
+
|
|
169
|
+
result["ports"].append(port_info)
|
|
170
|
+
|
|
171
|
+
for port in ports.findall('port'):
|
|
172
|
+
for script in port.findall('script'):
|
|
173
|
+
vuln_info = {
|
|
174
|
+
"xml_file": filename,
|
|
175
|
+
"ip_address": host_info["ip_address"],
|
|
176
|
+
"port_id": port.get('portid', ''),
|
|
177
|
+
"protocol": port.get('protocol', ''),
|
|
178
|
+
"script_id": script.get('id', ''),
|
|
179
|
+
"output": script.get('output', ''),
|
|
180
|
+
"version": script.get('version', ''),
|
|
181
|
+
"result": "",
|
|
182
|
+
"severity": "",
|
|
183
|
+
"description": "",
|
|
184
|
+
"risk_factor": "",
|
|
185
|
+
"see_also": "",
|
|
186
|
+
"solution": "",
|
|
187
|
+
"cve": "",
|
|
188
|
+
"bid": "",
|
|
189
|
+
"cert": "",
|
|
190
|
+
"msf_module": "",
|
|
191
|
+
"osvdb": ""
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
for elem in script:
|
|
195
|
+
tag = elem.tag.lower()
|
|
196
|
+
if tag == 'elem':
|
|
197
|
+
elem_key = elem.get('key', '').lower()
|
|
198
|
+
elem_text = elem.text if elem.text else ''
|
|
199
|
+
|
|
200
|
+
if elem_key == 'result':
|
|
201
|
+
vuln_info["result"] = elem_text
|
|
202
|
+
elif elem_key == 'severity':
|
|
203
|
+
vuln_info["severity"] = elem_text
|
|
204
|
+
elif elem_key == 'description':
|
|
205
|
+
vuln_info["description"] = elem_text
|
|
206
|
+
elif elem_key == 'risk_factor':
|
|
207
|
+
vuln_info["risk_factor"] = elem_text
|
|
208
|
+
elif elem_key == 'see_also':
|
|
209
|
+
vuln_info["see_also"] = elem_text
|
|
210
|
+
elif elem_key == 'solution':
|
|
211
|
+
vuln_info["solution"] = elem_text
|
|
212
|
+
elif elem_key == 'cve':
|
|
213
|
+
vuln_info["cve"] = elem_text
|
|
214
|
+
elif elem_key == 'bid':
|
|
215
|
+
vuln_info["bid"] = elem_text
|
|
216
|
+
elif elem_key == 'cert':
|
|
217
|
+
vuln_info["cert"] = elem_text
|
|
218
|
+
elif elem_key == 'msf_module':
|
|
219
|
+
vuln_info["msf_module"] = elem_text
|
|
220
|
+
elif elem_key == 'osvdb':
|
|
221
|
+
vuln_info["osvdb"] = elem_text
|
|
222
|
+
|
|
223
|
+
result["vulns"].append(vuln_info)
|
|
224
|
+
|
|
225
|
+
for hosthint in root.findall('hosthint'):
|
|
226
|
+
host_info = {
|
|
227
|
+
"xml_file": filename,
|
|
228
|
+
"ip_address": "",
|
|
229
|
+
"hostname": "",
|
|
230
|
+
"status": "",
|
|
231
|
+
"status_reason": "",
|
|
232
|
+
"status_ttl": "",
|
|
233
|
+
"start_time": "",
|
|
234
|
+
"end_time": ""
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
addr = hosthint.find('address')
|
|
238
|
+
if addr is not None:
|
|
239
|
+
host_info["ip_address"] = addr.get('addr', '')
|
|
240
|
+
|
|
241
|
+
status = hosthint.find('status')
|
|
242
|
+
if status is not None:
|
|
243
|
+
host_info["status"] = status.get('state', '')
|
|
244
|
+
host_info["status_reason"] = status.get('reason', '')
|
|
245
|
+
host_info["status_ttl"] = status.get('reason_ttl', '')
|
|
246
|
+
|
|
247
|
+
result["hosts"].append(host_info)
|
|
248
|
+
|
|
249
|
+
except ET.ParseError:
|
|
250
|
+
raise
|
|
251
|
+
except Exception:
|
|
252
|
+
raise
|
|
253
|
+
|
|
254
|
+
return result
|
|
255
|
+
|
|
256
|
+
def write_to_xlsx(self, data: Dict[str, Any], output_file: str) -> None:
|
|
257
|
+
"""Write parsed data to XLSX file"""
|
|
258
|
+
workbook = xlsxwriter.Workbook(output_file)
|
|
259
|
+
|
|
260
|
+
header_format = workbook.add_format({
|
|
261
|
+
'bold': True,
|
|
262
|
+
'bg_color': '#4F81BD',
|
|
263
|
+
'font_color': 'white',
|
|
264
|
+
'align': 'center',
|
|
265
|
+
'valign': 'vcenter',
|
|
266
|
+
'border': 1
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
cell_format = workbook.add_format({
|
|
270
|
+
'border': 1,
|
|
271
|
+
'align': 'left',
|
|
272
|
+
'valign': 'vcenter'
|
|
273
|
+
})
|
|
274
|
+
|
|
275
|
+
# --- Task Info worksheet ---
|
|
276
|
+
task_worksheet = workbook.add_worksheet("Task Info")
|
|
277
|
+
task_fields = [
|
|
278
|
+
"XML File", "Scanner", "Nmap Version", "Scan Args",
|
|
279
|
+
"Start Time", "Start Time Str", "XML Version", "Total Hosts",
|
|
280
|
+
"Up Hosts", "Down Hosts", "Finish Time", "Finish Time Str",
|
|
281
|
+
"Elapsed (s)", "Exit Status"
|
|
282
|
+
]
|
|
283
|
+
|
|
284
|
+
for col, field in enumerate(task_fields):
|
|
285
|
+
task_worksheet.write(0, col, field, header_format)
|
|
286
|
+
|
|
287
|
+
task_worksheet.set_column(0, 0, 20)
|
|
288
|
+
task_worksheet.set_column(1, 2, 12)
|
|
289
|
+
task_worksheet.set_column(3, 3, 80)
|
|
290
|
+
task_worksheet.set_column(4, 4, 15)
|
|
291
|
+
task_worksheet.set_column(5, 5, 22)
|
|
292
|
+
task_worksheet.set_column(6, 6, 12)
|
|
293
|
+
task_worksheet.set_column(7, 9, 10)
|
|
294
|
+
task_worksheet.set_column(10, 10, 15)
|
|
295
|
+
task_worksheet.set_column(11, 11, 22)
|
|
296
|
+
task_worksheet.set_column(12, 13, 12)
|
|
297
|
+
|
|
298
|
+
for row, task in enumerate(data["task_info"], start=1):
|
|
299
|
+
task_worksheet.write(row, 0, task["xml_file"], cell_format)
|
|
300
|
+
task_worksheet.write(row, 1, task["scanner"], cell_format)
|
|
301
|
+
task_worksheet.write(row, 2, task["nmap_version"], cell_format)
|
|
302
|
+
task_worksheet.write(row, 3, task["scan_args"], cell_format)
|
|
303
|
+
task_worksheet.write(row, 4, task["start_time"], cell_format)
|
|
304
|
+
task_worksheet.write(row, 5, task["start_time_str"], cell_format)
|
|
305
|
+
task_worksheet.write(row, 6, task["xml_output_version"], cell_format)
|
|
306
|
+
task_worksheet.write(row, 7, task["total_hosts"], cell_format)
|
|
307
|
+
task_worksheet.write(row, 8, task["up_hosts"], cell_format)
|
|
308
|
+
task_worksheet.write(row, 9, task["down_hosts"], cell_format)
|
|
309
|
+
task_worksheet.write(row, 10, task["finish_time"], cell_format)
|
|
310
|
+
task_worksheet.write(row, 11, task["finish_time_str"], cell_format)
|
|
311
|
+
task_worksheet.write(row, 12, task["elapsed_time"], cell_format)
|
|
312
|
+
task_worksheet.write(row, 13, task["exit_status"], cell_format)
|
|
313
|
+
|
|
314
|
+
# --- Host Status worksheet ---
|
|
315
|
+
host_worksheet = workbook.add_worksheet("Host Status")
|
|
316
|
+
host_fields = [
|
|
317
|
+
"XML File", "IP Address", "Hostname", "Status", "Status Reason",
|
|
318
|
+
"Status TTL", "Start Time", "End Time"
|
|
319
|
+
]
|
|
320
|
+
|
|
321
|
+
for col, field in enumerate(host_fields):
|
|
322
|
+
host_worksheet.write(0, col, field, header_format)
|
|
323
|
+
|
|
324
|
+
host_worksheet.set_column(0, 0, 20)
|
|
325
|
+
host_worksheet.set_column(1, 1, 16)
|
|
326
|
+
host_worksheet.set_column(2, 2, 25)
|
|
327
|
+
host_worksheet.set_column(3, 3, 10)
|
|
328
|
+
host_worksheet.set_column(4, 4, 30)
|
|
329
|
+
host_worksheet.set_column(5, 5, 8)
|
|
330
|
+
host_worksheet.set_column(6, 7, 22)
|
|
331
|
+
|
|
332
|
+
for row, host in enumerate(data["hosts"], start=1):
|
|
333
|
+
host_worksheet.write(row, 0, host["xml_file"], cell_format)
|
|
334
|
+
host_worksheet.write(row, 1, host["ip_address"], cell_format)
|
|
335
|
+
host_worksheet.write(row, 2, host["hostname"], cell_format)
|
|
336
|
+
host_worksheet.write(row, 3, host["status"], cell_format)
|
|
337
|
+
host_worksheet.write(row, 4, host["status_reason"], cell_format)
|
|
338
|
+
host_worksheet.write(row, 5, host["status_ttl"], cell_format)
|
|
339
|
+
host_worksheet.write(row, 6, host["start_time"], cell_format)
|
|
340
|
+
host_worksheet.write(row, 7, host["end_time"], cell_format)
|
|
341
|
+
|
|
342
|
+
# --- Port & Service worksheet ---
|
|
343
|
+
port_worksheet = workbook.add_worksheet("Port & Service")
|
|
344
|
+
port_fields = [
|
|
345
|
+
"XML File", "IP Address", "Port", "Protocol", "State",
|
|
346
|
+
"State Reason", "State TTL", "Service Name", "Service Product",
|
|
347
|
+
"Service Version", "Service Extra", "Method", "Confidence", "CPE"
|
|
348
|
+
]
|
|
349
|
+
|
|
350
|
+
for col, field in enumerate(port_fields):
|
|
351
|
+
port_worksheet.write(0, col, field, header_format)
|
|
352
|
+
|
|
353
|
+
port_worksheet.set_column(0, 0, 20)
|
|
354
|
+
port_worksheet.set_column(1, 1, 16)
|
|
355
|
+
port_worksheet.set_column(2, 3, 8)
|
|
356
|
+
port_worksheet.set_column(4, 4, 10)
|
|
357
|
+
port_worksheet.set_column(5, 5, 30)
|
|
358
|
+
port_worksheet.set_column(6, 6, 8)
|
|
359
|
+
port_worksheet.set_column(7, 7, 15)
|
|
360
|
+
port_worksheet.set_column(8, 9, 20)
|
|
361
|
+
port_worksheet.set_column(10, 10, 30)
|
|
362
|
+
port_worksheet.set_column(11, 12, 10)
|
|
363
|
+
port_worksheet.set_column(13, 13, 50)
|
|
364
|
+
|
|
365
|
+
for row, port in enumerate(data["ports"], start=1):
|
|
366
|
+
port_worksheet.write(row, 0, port["xml_file"], cell_format)
|
|
367
|
+
port_worksheet.write(row, 1, port["ip_address"], cell_format)
|
|
368
|
+
port_worksheet.write(row, 2, port["port_id"], cell_format)
|
|
369
|
+
port_worksheet.write(row, 3, port["protocol"], cell_format)
|
|
370
|
+
port_worksheet.write(row, 4, port["state"], cell_format)
|
|
371
|
+
port_worksheet.write(row, 5, port["state_reason"], cell_format)
|
|
372
|
+
port_worksheet.write(row, 6, port["state_ttl"], cell_format)
|
|
373
|
+
port_worksheet.write(row, 7, port["service_name"], cell_format)
|
|
374
|
+
port_worksheet.write(row, 8, port["service_product"], cell_format)
|
|
375
|
+
port_worksheet.write(row, 9, port["service_version"], cell_format)
|
|
376
|
+
port_worksheet.write(row, 10, port["service_extrainfo"], cell_format)
|
|
377
|
+
port_worksheet.write(row, 11, port["service_method"], cell_format)
|
|
378
|
+
port_worksheet.write(row, 12, port["service_conf"], cell_format)
|
|
379
|
+
port_worksheet.write(row, 13, port["cpe"], cell_format)
|
|
380
|
+
|
|
381
|
+
# --- Vulnerabilities worksheet ---
|
|
382
|
+
vuln_worksheet = workbook.add_worksheet("Vulnerabilities")
|
|
383
|
+
vuln_fields = [
|
|
384
|
+
"XML File", "IP Address", "Port", "Protocol", "Script ID",
|
|
385
|
+
"Output", "Version", "Result", "Severity", "Description",
|
|
386
|
+
"Risk Factor", "See Also", "Solution", "CVE", "BID",
|
|
387
|
+
"CERT", "MSF Module", "OSVDB"
|
|
388
|
+
]
|
|
389
|
+
|
|
390
|
+
for col, field in enumerate(vuln_fields):
|
|
391
|
+
vuln_worksheet.write(0, col, field, header_format)
|
|
392
|
+
|
|
393
|
+
vuln_worksheet.set_column(0, 0, 20)
|
|
394
|
+
vuln_worksheet.set_column(1, 1, 16)
|
|
395
|
+
vuln_worksheet.set_column(2, 4, 10)
|
|
396
|
+
vuln_worksheet.set_column(5, 5, 80)
|
|
397
|
+
vuln_worksheet.set_column(6, 6, 12)
|
|
398
|
+
vuln_worksheet.set_column(7, 7, 20)
|
|
399
|
+
vuln_worksheet.set_column(8, 8, 12)
|
|
400
|
+
vuln_worksheet.set_column(9, 9, 60)
|
|
401
|
+
vuln_worksheet.set_column(10, 10, 15)
|
|
402
|
+
vuln_worksheet.set_column(11, 11, 60)
|
|
403
|
+
vuln_worksheet.set_column(12, 12, 40)
|
|
404
|
+
vuln_worksheet.set_column(13, 17, 15)
|
|
405
|
+
|
|
406
|
+
for row, vuln in enumerate(data["vulns"], start=1):
|
|
407
|
+
vuln_worksheet.write(row, 0, vuln["xml_file"], cell_format)
|
|
408
|
+
vuln_worksheet.write(row, 1, vuln["ip_address"], cell_format)
|
|
409
|
+
vuln_worksheet.write(row, 2, vuln["port_id"], cell_format)
|
|
410
|
+
vuln_worksheet.write(row, 3, vuln.get("protocol", ""), cell_format)
|
|
411
|
+
vuln_worksheet.write(row, 4, vuln["script_id"], cell_format)
|
|
412
|
+
vuln_worksheet.write(row, 5, vuln.get("output", ""), cell_format)
|
|
413
|
+
vuln_worksheet.write(row, 6, vuln.get("version", ""), cell_format)
|
|
414
|
+
vuln_worksheet.write(row, 7, vuln.get("result", ""), cell_format)
|
|
415
|
+
vuln_worksheet.write(row, 8, vuln.get("severity", ""), cell_format)
|
|
416
|
+
vuln_worksheet.write(row, 9, vuln.get("description", ""), cell_format)
|
|
417
|
+
vuln_worksheet.write(row, 10, vuln.get("risk_factor", ""), cell_format)
|
|
418
|
+
vuln_worksheet.write(row, 11, vuln.get("see_also", ""), cell_format)
|
|
419
|
+
vuln_worksheet.write(row, 12, vuln.get("solution", ""), cell_format)
|
|
420
|
+
vuln_worksheet.write(row, 13, vuln.get("cve", ""), cell_format)
|
|
421
|
+
vuln_worksheet.write(row, 14, vuln.get("bid", ""), cell_format)
|
|
422
|
+
vuln_worksheet.write(row, 15, vuln.get("cert", ""), cell_format)
|
|
423
|
+
vuln_worksheet.write(row, 16, vuln.get("msf_module", ""), cell_format)
|
|
424
|
+
vuln_worksheet.write(row, 17, vuln.get("osvdb", ""), cell_format)
|
|
425
|
+
|
|
426
|
+
workbook.close()
|
|
427
|
+
|
|
428
|
+
def get_summary_stats(self, all_data: Dict[str, Any], xml_files_count: int) -> Dict[str, Any]:
|
|
429
|
+
"""Get summary statistics"""
|
|
430
|
+
stats = {
|
|
431
|
+
"files_count": xml_files_count,
|
|
432
|
+
"hosts_total": len(all_data['hosts']),
|
|
433
|
+
"hosts_up": sum(1 for h in all_data['hosts'] if h['status'] == 'up'),
|
|
434
|
+
"hosts_down": len(all_data['hosts']) - sum(1 for h in all_data['hosts'] if h['status'] == 'up'),
|
|
435
|
+
"ports_total": len(all_data['ports']),
|
|
436
|
+
"ports_open": sum(1 for p in all_data['ports'] if p['state'] == 'open'),
|
|
437
|
+
"ports_closed": len(all_data['ports']) - sum(1 for p in all_data['ports'] if p['state'] == 'open'),
|
|
438
|
+
"vulns_total": len(all_data['vulns']),
|
|
439
|
+
"scripts_count": len(set(v['script_id'] for v in all_data['vulns'])) if all_data['vulns'] else 0,
|
|
440
|
+
}
|
|
441
|
+
return stats
|
nmap_parser/gui.py
ADDED
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
|
|
3
|
+
'''
|
|
4
|
+
@File : gui.py
|
|
5
|
+
@Time : 2026/06/15
|
|
6
|
+
@Author : test233
|
|
7
|
+
@Version : 1.0.0
|
|
8
|
+
'''
|
|
9
|
+
|
|
10
|
+
"""
|
|
11
|
+
Graphical User Interface for Nmap Parser
|
|
12
|
+
Uses CustomTkinter for a modern look
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
import os
|
|
16
|
+
import sys
|
|
17
|
+
import threading
|
|
18
|
+
import customtkinter as ctk
|
|
19
|
+
from tkinter import filedialog, messagebox
|
|
20
|
+
|
|
21
|
+
try:
|
|
22
|
+
from .core import NmapConverter
|
|
23
|
+
except ImportError:
|
|
24
|
+
from core import NmapConverter # type: ignore
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class ConverterApp(ctk.CTk):
|
|
28
|
+
"""Main application window"""
|
|
29
|
+
|
|
30
|
+
def __init__(self):
|
|
31
|
+
super().__init__()
|
|
32
|
+
|
|
33
|
+
self.converter = NmapConverter()
|
|
34
|
+
self.input_path = None
|
|
35
|
+
self.output_path = None
|
|
36
|
+
self.recursive = False
|
|
37
|
+
|
|
38
|
+
self.title("Nmap Parser - XML to Excel")
|
|
39
|
+
self.geometry("700x600")
|
|
40
|
+
self.minsize(600, 600)
|
|
41
|
+
|
|
42
|
+
ctk.set_appearance_mode("System")
|
|
43
|
+
ctk.set_default_color_theme("blue")
|
|
44
|
+
|
|
45
|
+
self._create_widgets()
|
|
46
|
+
|
|
47
|
+
def _create_widgets(self):
|
|
48
|
+
"""Create UI widgets"""
|
|
49
|
+
self.grid_columnconfigure(1, weight=1)
|
|
50
|
+
self.grid_rowconfigure(4, weight=1)
|
|
51
|
+
|
|
52
|
+
title_label = ctk.CTkLabel(
|
|
53
|
+
self,
|
|
54
|
+
text="Nmap Parser - XML to Excel",
|
|
55
|
+
font=ctk.CTkFont(size=20, weight="bold")
|
|
56
|
+
)
|
|
57
|
+
title_label.grid(row=0, column=0, columnspan=3, pady=(20, 10), padx=20)
|
|
58
|
+
|
|
59
|
+
input_label = ctk.CTkLabel(self, text="Input Path:")
|
|
60
|
+
input_label.grid(row=1, column=0, pady=10, padx=(20, 10), sticky="w")
|
|
61
|
+
|
|
62
|
+
self.input_entry = ctk.CTkEntry(self, placeholder_text="Select XML file or directory")
|
|
63
|
+
self.input_entry.grid(row=1, column=1, pady=10, padx=(0, 10), sticky="ew")
|
|
64
|
+
|
|
65
|
+
input_btn = ctk.CTkButton(self, text="Browse...", command=self._browse_input, width=80)
|
|
66
|
+
input_btn.grid(row=1, column=2, pady=10, padx=(0, 20))
|
|
67
|
+
|
|
68
|
+
output_label = ctk.CTkLabel(self, text="Output File:")
|
|
69
|
+
output_label.grid(row=2, column=0, pady=10, padx=(20, 10), sticky="w")
|
|
70
|
+
|
|
71
|
+
self.output_entry = ctk.CTkEntry(self, placeholder_text="Select output Excel file path")
|
|
72
|
+
self.output_entry.grid(row=2, column=1, pady=10, padx=(0, 10), sticky="ew")
|
|
73
|
+
|
|
74
|
+
output_btn = ctk.CTkButton(self, text="Browse...", command=self._browse_output, width=80)
|
|
75
|
+
output_btn.grid(row=2, column=2, pady=10, padx=(0, 20))
|
|
76
|
+
|
|
77
|
+
options_frame = ctk.CTkFrame(self)
|
|
78
|
+
options_frame.grid(row=3, column=0, columnspan=3, pady=10, padx=20, sticky="ew")
|
|
79
|
+
|
|
80
|
+
self.recursive_checkbox = ctk.CTkCheckBox(
|
|
81
|
+
options_frame, text="Recursively search subdirectories",
|
|
82
|
+
command=self._toggle_recursive
|
|
83
|
+
)
|
|
84
|
+
self.recursive_checkbox.grid(row=0, column=0, padx=20, pady=10, sticky="w")
|
|
85
|
+
|
|
86
|
+
self.convert_btn = ctk.CTkButton(
|
|
87
|
+
self,
|
|
88
|
+
text="Start Conversion",
|
|
89
|
+
command=self._start_conversion,
|
|
90
|
+
font=ctk.CTkFont(size=16, weight="bold"),
|
|
91
|
+
height=40
|
|
92
|
+
)
|
|
93
|
+
self.convert_btn.grid(row=4, column=0, columnspan=3, pady=20, padx=20, sticky="ew")
|
|
94
|
+
|
|
95
|
+
self.status_label = ctk.CTkLabel(self, text="", wraplength=600, justify="left")
|
|
96
|
+
self.status_label.grid(row=5, column=0, columnspan=3, pady=(0, 20), padx=20, sticky="ew")
|
|
97
|
+
|
|
98
|
+
self.progress_bar = ctk.CTkProgressBar(self)
|
|
99
|
+
self.progress_bar.grid(row=6, column=0, columnspan=3, pady=(0, 20), padx=20, sticky="ew")
|
|
100
|
+
self.progress_bar.set(0)
|
|
101
|
+
|
|
102
|
+
self.info_text = ctk.CTkTextbox(self, state="disabled", height=150)
|
|
103
|
+
self.info_text.grid(row=7, column=0, columnspan=3, pady=(0, 20), padx=20, sticky="ew")
|
|
104
|
+
|
|
105
|
+
def _browse_input(self):
|
|
106
|
+
"""Browse for input file or directory"""
|
|
107
|
+
path = filedialog.askopenfilename(
|
|
108
|
+
title="Select Nmap XML file",
|
|
109
|
+
filetypes=[("XML files", "*.xml"), ("All files", "*.*")]
|
|
110
|
+
)
|
|
111
|
+
if path:
|
|
112
|
+
self.input_path = path
|
|
113
|
+
self.input_entry.delete(0, 'end')
|
|
114
|
+
self.input_entry.insert(0, path)
|
|
115
|
+
|
|
116
|
+
def _browse_output(self):
|
|
117
|
+
"""Browse for output file"""
|
|
118
|
+
path = filedialog.asksaveasfilename(
|
|
119
|
+
title="Save Excel file",
|
|
120
|
+
defaultextension=".xlsx",
|
|
121
|
+
filetypes=[("Excel files", "*.xlsx"), ("All files", "*.*")]
|
|
122
|
+
)
|
|
123
|
+
if path:
|
|
124
|
+
self.output_path = path
|
|
125
|
+
self.output_entry.delete(0, 'end')
|
|
126
|
+
self.output_entry.insert(0, path)
|
|
127
|
+
|
|
128
|
+
def _toggle_recursive(self):
|
|
129
|
+
"""Toggle recursive option"""
|
|
130
|
+
self.recursive = not self.recursive
|
|
131
|
+
|
|
132
|
+
def _log_message(self, message):
|
|
133
|
+
"""Add message to log"""
|
|
134
|
+
self.info_text.configure(state="normal")
|
|
135
|
+
self.info_text.insert("end", message + "\n")
|
|
136
|
+
self.info_text.see("end")
|
|
137
|
+
self.info_text.configure(state="disabled")
|
|
138
|
+
|
|
139
|
+
def _update_status(self, message):
|
|
140
|
+
"""Update status label"""
|
|
141
|
+
self.status_label.configure(text=message)
|
|
142
|
+
|
|
143
|
+
def _start_conversion(self):
|
|
144
|
+
"""Start conversion in background thread"""
|
|
145
|
+
if not self.input_path:
|
|
146
|
+
messagebox.showerror("Error", "Please select an input XML file or directory")
|
|
147
|
+
return
|
|
148
|
+
|
|
149
|
+
if not self.output_path:
|
|
150
|
+
messagebox.showerror("Error", "Please select an output Excel file path")
|
|
151
|
+
return
|
|
152
|
+
|
|
153
|
+
self.convert_btn.configure(state="disabled")
|
|
154
|
+
self.progress_bar.set(0)
|
|
155
|
+
self.info_text.configure(state="normal")
|
|
156
|
+
self.info_text.delete("1.0", "end")
|
|
157
|
+
self.info_text.configure(state="disabled")
|
|
158
|
+
|
|
159
|
+
thread = threading.Thread(target=self._run_conversion)
|
|
160
|
+
thread.daemon = True
|
|
161
|
+
thread.start()
|
|
162
|
+
|
|
163
|
+
def _run_conversion(self):
|
|
164
|
+
"""Run conversion process"""
|
|
165
|
+
try:
|
|
166
|
+
self._update_status(f"{self.converter.emoji_scan} Scanning input path...")
|
|
167
|
+
self._log_message(f"Input: {self.input_path}")
|
|
168
|
+
self._log_message(f"Output: {self.output_path}")
|
|
169
|
+
self._log_message(f"Recursive: {'Yes' if self.recursive else 'No'}")
|
|
170
|
+
self._log_message("")
|
|
171
|
+
|
|
172
|
+
xml_files = self.converter.get_xml_files(self.input_path, self.recursive)
|
|
173
|
+
|
|
174
|
+
if not xml_files:
|
|
175
|
+
self._update_status(f"{self.converter.emoji_error} No XML files found")
|
|
176
|
+
messagebox.showerror("Error", "No XML files found")
|
|
177
|
+
self.convert_btn.configure(state="normal")
|
|
178
|
+
return
|
|
179
|
+
|
|
180
|
+
self._log_message(f"Found {len(xml_files)} XML file(s):")
|
|
181
|
+
for f in xml_files:
|
|
182
|
+
self._log_message(f" - {os.path.basename(f)}")
|
|
183
|
+
self._log_message("")
|
|
184
|
+
|
|
185
|
+
self._update_status(f"{self.converter.emoji_scan} Parsing {len(xml_files)} file(s)...")
|
|
186
|
+
|
|
187
|
+
all_data = {
|
|
188
|
+
"task_info": [],
|
|
189
|
+
"hosts": [],
|
|
190
|
+
"ports": [],
|
|
191
|
+
"vulns": []
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
success_count = 0
|
|
195
|
+
fail_count = 0
|
|
196
|
+
|
|
197
|
+
for i, xml_file in enumerate(xml_files):
|
|
198
|
+
try:
|
|
199
|
+
file_data = self.converter.parse_nmap_xml(xml_file)
|
|
200
|
+
if file_data["task_info"]:
|
|
201
|
+
all_data["task_info"].append(file_data["task_info"])
|
|
202
|
+
all_data["hosts"].extend(file_data["hosts"])
|
|
203
|
+
all_data["ports"].extend(file_data["ports"])
|
|
204
|
+
all_data["vulns"].extend(file_data["vulns"])
|
|
205
|
+
success_count += 1
|
|
206
|
+
self._log_message(
|
|
207
|
+
f"{self.converter.emoji_success} {os.path.basename(xml_file)}"
|
|
208
|
+
)
|
|
209
|
+
except Exception as e:
|
|
210
|
+
fail_count += 1
|
|
211
|
+
self._log_message(
|
|
212
|
+
f"{self.converter.emoji_error} {os.path.basename(xml_file)}: {str(e)}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
progress = (i + 1) / len(xml_files)
|
|
216
|
+
self.progress_bar.set(progress)
|
|
217
|
+
|
|
218
|
+
self._update_status(f"{self.converter.emoji_excel} Generating Excel file...")
|
|
219
|
+
self._log_message("")
|
|
220
|
+
|
|
221
|
+
self.converter.write_to_xlsx(all_data, self.output_path)
|
|
222
|
+
|
|
223
|
+
stats = self.converter.get_summary_stats(all_data, len(xml_files))
|
|
224
|
+
|
|
225
|
+
self._log_message("=" * 50)
|
|
226
|
+
self._log_message(f"{self.converter.emoji_stats} Summary:")
|
|
227
|
+
self._log_message(f" Successfully parsed: {success_count} file(s)")
|
|
228
|
+
if fail_count > 0:
|
|
229
|
+
self._log_message(f" Failed: {fail_count} file(s)")
|
|
230
|
+
self._log_message(f" Total hosts: {stats['hosts_total']} (up: {stats['hosts_up']})")
|
|
231
|
+
self._log_message(f" Total ports: {stats['ports_total']} (open: {stats['ports_open']})")
|
|
232
|
+
self._log_message(f" Scripts/Vulns: {stats['vulns_total']}")
|
|
233
|
+
self._log_message("=" * 50)
|
|
234
|
+
|
|
235
|
+
self._update_status(f"{self.converter.emoji_success} Conversion successful!")
|
|
236
|
+
self._log_message(f"\n{self.converter.emoji_success} Output file: {self.output_path}")
|
|
237
|
+
|
|
238
|
+
messagebox.showinfo("Success", f"Conversion complete!\n\nOutput: {self.output_path}")
|
|
239
|
+
|
|
240
|
+
except Exception as e:
|
|
241
|
+
self._update_status(f"{self.converter.emoji_error} Conversion failed")
|
|
242
|
+
self._log_message(f"{self.converter.emoji_error} Error: {str(e)}")
|
|
243
|
+
messagebox.showerror("Error", f"Conversion failed:\n{str(e)}")
|
|
244
|
+
|
|
245
|
+
finally:
|
|
246
|
+
self.convert_btn.configure(state="normal")
|
|
247
|
+
|
|
248
|
+
|
|
249
|
+
def main():
|
|
250
|
+
"""Main entry point for GUI"""
|
|
251
|
+
app = ConverterApp()
|
|
252
|
+
app.mainloop()
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
if __name__ == "__main__":
|
|
256
|
+
main()
|
|
@@ -0,0 +1,442 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: nmap-xml2xlsx
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Convert Nmap XML scan results to Excel format with GUI and CLI support
|
|
5
|
+
Author: test233
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/bright-angel/nmap-parser
|
|
8
|
+
Project-URL: Repository, https://github.com/bright-angel/nmap-parser.git
|
|
9
|
+
Project-URL: Issues, https://github.com/bright-angel/nmap-parser/issues
|
|
10
|
+
Keywords: nmap,xml,excel,xlsx,security,scan,parser
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Environment :: Console
|
|
13
|
+
Classifier: Environment :: Win32 (MS Windows)
|
|
14
|
+
Classifier: Environment :: X11 Applications :: GTK
|
|
15
|
+
Classifier: Environment :: MacOS X
|
|
16
|
+
Classifier: Intended Audience :: Information Technology
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Topic :: Security
|
|
20
|
+
Classifier: Topic :: System :: Networking
|
|
21
|
+
Classifier: Topic :: Utilities
|
|
22
|
+
Requires-Python: >=3.8
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
Requires-Dist: xlsxwriter>=3.0.0
|
|
25
|
+
Requires-Dist: customtkinter>=5.2.0
|
|
26
|
+
Provides-Extra: gui
|
|
27
|
+
Requires-Dist: customtkinter>=5.2.0; extra == "gui"
|
|
28
|
+
|
|
29
|
+
# Nmap XML to Excel Converter
|
|
30
|
+
|
|
31
|
+
将 Nmap 扫描的 XML 结果转换为美观的 Excel 报告工具,支持命令行和图形界面两种使用方式。
|
|
32
|
+
|
|
33
|
+

|
|
34
|
+

|
|
35
|
+

|
|
36
|
+
|
|
37
|
+
## 功能特点
|
|
38
|
+
|
|
39
|
+
- ✅ **多模式支持**:单个 XML 文件或整个目录批量转换
|
|
40
|
+
- ✅ **递归扫描**:支持递归查找子目录中的所有 XML 文件
|
|
41
|
+
- ✅ **完整报告**:自动生成 4 个工作表的 Excel 报告
|
|
42
|
+
- ✅ **双界面**:命令行 (CLI) 和图形界面 (GUI) 双版本
|
|
43
|
+
- ✅ **易于安装**:支持 pipx 隔离安装,无需配置虚拟环境
|
|
44
|
+
- ✅ **完整漏洞信息**:解析 Nmap NSE 脚本的详细漏洞数据(CVE、严重程度、解决方案等)
|
|
45
|
+
- ✅ **跨平台**:支持 Windows、Linux、macOS
|
|
46
|
+
- ✅ **独立 EXE**:可打包为独立 Windows 程序,无需 Python 环境
|
|
47
|
+
|
|
48
|
+
## 快速开始
|
|
49
|
+
|
|
50
|
+
### 1. 安装依赖
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -r requirements.txt
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
### 2. 安装工具
|
|
57
|
+
|
|
58
|
+
**方式 A:使用 pipx(推荐,隔离环境)**
|
|
59
|
+
|
|
60
|
+
```bash
|
|
61
|
+
pipx install .
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**方式 B:使用 pip**
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install .
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**方式 C:开发模式**
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
pip install -e .
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### 3. 使用
|
|
77
|
+
|
|
78
|
+
**命令行模式:**
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
nmap-parser scan.xml report.xlsx
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
**图形界面模式:**
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
nmap-parser-gui
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
## 安装方式对比
|
|
91
|
+
|
|
92
|
+
| 方式 | 命令 | 适用场景 |
|
|
93
|
+
|------|------|----------|
|
|
94
|
+
| **pipx** | `pipx install .` | 推荐使用,独立隔离环境 |
|
|
95
|
+
| **pip** | `pip install .` | 系统级安装 |
|
|
96
|
+
| **开发** | `pip install -e .` | 开发调试模式 |
|
|
97
|
+
| **手动** | `python nmap_parser/gui.py` | 无需安装,直接运行 |
|
|
98
|
+
|
|
99
|
+
## 使用方法
|
|
100
|
+
|
|
101
|
+
### 命令行模式 (CLI)
|
|
102
|
+
|
|
103
|
+
**基础用法:**
|
|
104
|
+
|
|
105
|
+
```bash
|
|
106
|
+
# 转换单个 XML 文件
|
|
107
|
+
nmap-parser scan.xml report.xlsx
|
|
108
|
+
|
|
109
|
+
# 转换目录下所有 XML 文件
|
|
110
|
+
nmap-parser ./nmap_scans report.xlsx
|
|
111
|
+
|
|
112
|
+
# 递归转换(包含子目录)
|
|
113
|
+
nmap-parser ./nmap_scans report.xlsx -r
|
|
114
|
+
|
|
115
|
+
# 静默模式(不输出日志)
|
|
116
|
+
nmap-parser scan.xml report.xlsx -q
|
|
117
|
+
|
|
118
|
+
# 查看版本
|
|
119
|
+
nmap-parser -v
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
**命令行参数:**
|
|
123
|
+
|
|
124
|
+
| 参数 | 简写 | 说明 |
|
|
125
|
+
|------|------|------|
|
|
126
|
+
| `input` | - | 输入路径(XML 文件或目录) |
|
|
127
|
+
| `output` | - | 输出 Excel 文件路径(.xlsx) |
|
|
128
|
+
| `--recursive` | `-r` | 递归查找子目录 |
|
|
129
|
+
| `--quiet` | `-q` | 静默模式,不输出日志 |
|
|
130
|
+
| `--version` | `-v` | 显示版本信息 |
|
|
131
|
+
| `--help` | `-h` | 显示帮助信息 |
|
|
132
|
+
|
|
133
|
+
**退出码:**
|
|
134
|
+
|
|
135
|
+
- `0` - 成功
|
|
136
|
+
- `1` - 错误(未找到文件、解析失败等)
|
|
137
|
+
|
|
138
|
+
### 图形界面模式 (GUI)
|
|
139
|
+
|
|
140
|
+
**启动方式:**
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
# 已安装
|
|
144
|
+
nmap-parser-gui
|
|
145
|
+
|
|
146
|
+
# 未安装,直接运行
|
|
147
|
+
python nmap_parser/gui.py
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
**界面功能:**
|
|
151
|
+
|
|
152
|
+
- 📁 **文件选择器**:点击浏览选择输入 XML 文件或目录
|
|
153
|
+
- 📂 **目录浏览**:可选择文件夹进行批量转换
|
|
154
|
+
- 🔄 **递归选项**:勾选后递归扫描子目录
|
|
155
|
+
- 📊 **实时进度**:显示转换进度条
|
|
156
|
+
- 📝 **详细日志**:显示每个文件的处理状态
|
|
157
|
+
- ✅ **完成提示**:转换完成后弹出对话框
|
|
158
|
+
|
|
159
|
+
## 输出 Excel 结构
|
|
160
|
+
|
|
161
|
+
生成的 Excel 包含 4 个工作表:
|
|
162
|
+
|
|
163
|
+
### 1️⃣ 任务信息 (Task Info)
|
|
164
|
+
|
|
165
|
+
| 列名 | 说明 |
|
|
166
|
+
|------|------|
|
|
167
|
+
| xml 文件名 | 扫描的 XML 文件名 |
|
|
168
|
+
| 扫描工具 | Nmap 或其他扫描器名称 |
|
|
169
|
+
| 工具版本 | 扫描器版本号 |
|
|
170
|
+
| 扫描参数 | Nmap 命令行参数 |
|
|
171
|
+
| 开始时间戳 | Unix 时间戳格式 |
|
|
172
|
+
| 开始时间 | 可读的时间格式 |
|
|
173
|
+
| XML 版本 | Nmap XML 输出格式版本 |
|
|
174
|
+
| 总主机数 | 扫描的主机总数 |
|
|
175
|
+
| 存活主机数 | 在线主机数量 |
|
|
176
|
+
| 下线主机数 | 离线主机数量 |
|
|
177
|
+
| 结束时间戳 | 扫描结束时间戳 |
|
|
178
|
+
| 结束时间 | 可读的结束时间 |
|
|
179
|
+
| 耗时 (秒) | 扫描持续时间 |
|
|
180
|
+
| 退出状态 | 扫描任务状态(success/fail) |
|
|
181
|
+
|
|
182
|
+
### 2️⃣ 主机状态 (Host Status)
|
|
183
|
+
|
|
184
|
+
| 列名 | 说明 |
|
|
185
|
+
|------|------|
|
|
186
|
+
| xml 文件名 | 来源 XML 文件 |
|
|
187
|
+
| IP 地址 | 主机 IP 地址 |
|
|
188
|
+
| 主机名 | 反向解析的主机名 |
|
|
189
|
+
| 状态 | up/down |
|
|
190
|
+
| 状态原因 | 判断状态的依据 |
|
|
191
|
+
| 状态 TTL | 响应 TTL 值 |
|
|
192
|
+
| 开始时间 | 主机扫描开始时间 |
|
|
193
|
+
| 结束时间 | 主机扫描结束时间 |
|
|
194
|
+
|
|
195
|
+
### 3️⃣ 端口服务 (Port & Service)
|
|
196
|
+
|
|
197
|
+
| 列名 | 说明 |
|
|
198
|
+
|------|------|
|
|
199
|
+
| xml 文件名 | 来源 XML 文件 |
|
|
200
|
+
| IP 地址 | 主机 IP |
|
|
201
|
+
| 端口号 | 端口号 (1-65535) |
|
|
202
|
+
| 协议 | TCP/UDP |
|
|
203
|
+
| 状态 | open/closed/filtered |
|
|
204
|
+
| 状态原因 | 状态判断原因 |
|
|
205
|
+
| 状态 TTL | TTL 值 |
|
|
206
|
+
| 服务名称 | 识别的服务名 |
|
|
207
|
+
| 服务产品 | 产品名(如 OpenSSH) |
|
|
208
|
+
| 服务版本 | 版本号 |
|
|
209
|
+
| 服务额外信息 | 系统、协议等额外信息 |
|
|
210
|
+
| 探测方法 | table/probed |
|
|
211
|
+
| 可信度 | 1-10 的置信度 |
|
|
212
|
+
| CPE 信息 | 通用平台枚举标识符 |
|
|
213
|
+
|
|
214
|
+
### 4️⃣ 漏洞情况 (Vulnerabilities)
|
|
215
|
+
|
|
216
|
+
基于 Nmap NSE 脚本的详细输出:
|
|
217
|
+
|
|
218
|
+
| 列名 | XML 路径 | 说明 |
|
|
219
|
+
|------|---------|------|
|
|
220
|
+
| 脚本 ID | `script@id` | NSE 脚本标识符 |
|
|
221
|
+
| 输出 | `script@output` | 脚本原始输出 |
|
|
222
|
+
| 版本 | `script@version` | 脚本版本 |
|
|
223
|
+
| 协议 | `port@protocol` | 端口协议 |
|
|
224
|
+
| 结果 | `elem[@key="result"]` | 检测结果 (如 VULNERABLE) |
|
|
225
|
+
| 严重程度 | `elem[@key="severity"]` | HIGH/MEDIUM/LOW |
|
|
226
|
+
| 描述 | `elem[@key="description"]` | 漏洞详细描述 |
|
|
227
|
+
| 风险因素 | `elem[@key="risk_factor"]` | Critical/High/Medium/Low |
|
|
228
|
+
| 参考链接 | `elem[@key="see_also"]` | 外部参考链接 |
|
|
229
|
+
| 解决方案 | `elem[@key="solution"]` | 修复建议 |
|
|
230
|
+
| CVE 编号 | `elem[@key="cve"]` | CVE 标识符 |
|
|
231
|
+
| BID 编号 | `elem[@key="bid"]` | BugTraq ID |
|
|
232
|
+
| CERT 公告 | `elem[@key="cert"]` | CERT 公告编号 |
|
|
233
|
+
| MSF 模块 | `elem[@key="msf_module"]` | Metasploit 模块名 |
|
|
234
|
+
| OSVDB 编号 | `elem[@key="osvdb"]` | 开源漏洞库 ID |
|
|
235
|
+
|
|
236
|
+
## 使用示例
|
|
237
|
+
|
|
238
|
+
### 示例 1:转换单个扫描文件
|
|
239
|
+
|
|
240
|
+
```bash
|
|
241
|
+
nmap-parser nmap_scan.xml report.xlsx
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
### 示例 2:批量转换目录
|
|
245
|
+
|
|
246
|
+
```bash
|
|
247
|
+
nmap-parser ./scans/ all_reports.xlsx -r
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
### 示例 3:生成带漏洞详情的报告
|
|
251
|
+
|
|
252
|
+
使用 Nmap 进行漏洞扫描(需要安装 NSE 脚本):
|
|
253
|
+
|
|
254
|
+
```bash
|
|
255
|
+
# Nmap 扫描并输出 XML
|
|
256
|
+
nmap -sV --script vuln -oX scan_result.xml 192.168.1.0/24
|
|
257
|
+
|
|
258
|
+
# 转换为 Excel
|
|
259
|
+
nmap-parser scan_result.xml vulnerability_report.xlsx
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
### 示例 4:在 CI/CD 中使用
|
|
263
|
+
|
|
264
|
+
```bash
|
|
265
|
+
# 扫描
|
|
266
|
+
nmap -oX results.xml target.host
|
|
267
|
+
|
|
268
|
+
# 转换
|
|
269
|
+
nmap-parser results.xml report.xlsx
|
|
270
|
+
|
|
271
|
+
# 检查生成结果
|
|
272
|
+
ls -la report.xlsx
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
## 系统要求
|
|
276
|
+
|
|
277
|
+
### 运行环境
|
|
278
|
+
|
|
279
|
+
- **Python**: 3.8 或更高版本
|
|
280
|
+
- **操作系统**:Windows 7/10/11、Linux、macOS
|
|
281
|
+
|
|
282
|
+
### 依赖库
|
|
283
|
+
|
|
284
|
+
```txt
|
|
285
|
+
xlsxwriter>=3.0.0 # Excel 文件生成
|
|
286
|
+
customtkinter>=5.2.0 # GUI 框架
|
|
287
|
+
darkdetect # 暗色模式检测
|
|
288
|
+
packaging # 工具库
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
## 打包为独立 EXE (Windows)
|
|
292
|
+
|
|
293
|
+
### 快速打包
|
|
294
|
+
|
|
295
|
+
```batch
|
|
296
|
+
build_exe_simple.bat
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### 完整打包(带版本信息)
|
|
300
|
+
|
|
301
|
+
```batch
|
|
302
|
+
build_exe.bat
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### 手动打包
|
|
306
|
+
|
|
307
|
+
```batch
|
|
308
|
+
pip install pyinstaller
|
|
309
|
+
|
|
310
|
+
pyinstaller --onefile --windowed --name "nmap-parser-gui" --hidden-import customtkinter --hidden-import tkinter --hidden-import xlsxwriter --collect-all customtkinter --noconfirm --clean nmap_parser\gui.py
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
打包成功后,在 `dist` 目录生成独立的 EXE 文件(约 30-50 MB),无需 Python 环境即可运行。
|
|
314
|
+
|
|
315
|
+
## 项目结构
|
|
316
|
+
|
|
317
|
+
```
|
|
318
|
+
workspace/
|
|
319
|
+
├── pyproject.toml # 项目配置文件
|
|
320
|
+
├── requirements.txt # Python 依赖
|
|
321
|
+
├── README.md # 使用文档
|
|
322
|
+
├── PACKAGING.md # 打包指南
|
|
323
|
+
├── build_exe.bat # Windows 打包脚本(完整版)
|
|
324
|
+
├── build_exe_simple.bat # Windows 打包脚本(简易版)
|
|
325
|
+
└── nmap_parser/
|
|
326
|
+
├── __init__.py # 包初始化
|
|
327
|
+
├── core.py # 核心转换逻辑
|
|
328
|
+
├── cli.py # 命令行界面
|
|
329
|
+
└── gui.py # 图形界面
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
## 开发指南
|
|
333
|
+
|
|
334
|
+
### 安装开发环境
|
|
335
|
+
|
|
336
|
+
```bash
|
|
337
|
+
pip install -e .
|
|
338
|
+
pip install -r requirements.txt
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
### 运行测试
|
|
342
|
+
|
|
343
|
+
```bash
|
|
344
|
+
# 命令行模式
|
|
345
|
+
python -m nmap_parser.cli test_scan.xml output.xlsx
|
|
346
|
+
|
|
347
|
+
# GUI 模式
|
|
348
|
+
python -m nmap_parser.gui
|
|
349
|
+
|
|
350
|
+
# 或直接运行(无需安装)
|
|
351
|
+
python nmap_parser/cli.py test_scan.xml output.xlsx
|
|
352
|
+
python nmap_parser/gui.py
|
|
353
|
+
```
|
|
354
|
+
|
|
355
|
+
### 代码结构
|
|
356
|
+
|
|
357
|
+
- **core.py**: NmapConverter 类,包含 XML 解析和 XLSX 生成逻辑
|
|
358
|
+
- **cli.py**: 命令行解析,调用核心功能
|
|
359
|
+
- **gui.py**: CustomTkinter 实现的图形界面
|
|
360
|
+
|
|
361
|
+
## 常见问题
|
|
362
|
+
|
|
363
|
+
### Q: 为什么 GUI 打不开?
|
|
364
|
+
|
|
365
|
+
确保已安装 customtkinter:
|
|
366
|
+
|
|
367
|
+
```bash
|
|
368
|
+
pip install customtkinter
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
### Q: 转换后 Excel 为空?
|
|
372
|
+
|
|
373
|
+
检查输入的 XML 文件是否是有效的 Nmap 输出格式。
|
|
374
|
+
|
|
375
|
+
### Q: 支持其他扫描器的 XML 吗?
|
|
376
|
+
|
|
377
|
+
当前仅支持 Nmap XML 格式,其他扫描器需要适配 XML 解析逻辑。
|
|
378
|
+
|
|
379
|
+
### Q: 如何提交 Bug 或建议?
|
|
380
|
+
|
|
381
|
+
请在 GitHub 仓库提交 Issue。
|
|
382
|
+
|
|
383
|
+
## 截图预览
|
|
384
|
+
|
|
385
|
+
### 命令行界面
|
|
386
|
+
|
|
387
|
+
```
|
|
388
|
+
🔍 找到 2 个 XML 文件
|
|
389
|
+
🔍 开始解析...
|
|
390
|
+
✅ /path/to/scan1.xml
|
|
391
|
+
✅ /path/to/scan2.xml
|
|
392
|
+
|
|
393
|
+
📈 统计信息:
|
|
394
|
+
文件数:2
|
|
395
|
+
主机数:10 (存活:8)
|
|
396
|
+
端口数:45 (开放:32)
|
|
397
|
+
脚本/漏洞:5
|
|
398
|
+
|
|
399
|
+
✅ 完成!输出:report.xlsx
|
|
400
|
+
```
|
|
401
|
+
|
|
402
|
+
### 图形界面
|
|
403
|
+
|
|
404
|
+
```
|
|
405
|
+
┌────────────────────────────────────────┐
|
|
406
|
+
│ 🔍 Nmap XML to Excel Converter │
|
|
407
|
+
├────────────────────────────────────────┤
|
|
408
|
+
│ 输入路径:[____________] [浏览...] │
|
|
409
|
+
│ 输出文件:[____________] [浏览...] │
|
|
410
|
+
├────────────────────────────────────────┤
|
|
411
|
+
│ ☑ 递归查找子目录 │
|
|
412
|
+
├────────────────────────────────────────┤
|
|
413
|
+
│ [ 开始转换 ] │
|
|
414
|
+
│ 进度:███████████░░░░░ 65% │
|
|
415
|
+
│ 📝 详细日志输出... │
|
|
416
|
+
└────────────────────────────────────────┘
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
## 更新日志
|
|
420
|
+
|
|
421
|
+
### v1.0.0
|
|
422
|
+
|
|
423
|
+
- ✅ 初始版本发布
|
|
424
|
+
- ✅ 支持 Nmap XML 完整解析
|
|
425
|
+
- ✅ 4 个工作表报告
|
|
426
|
+
- ✅ CLI 和 GUI 双界面
|
|
427
|
+
- ✅ Windows EXE 打包支持
|
|
428
|
+
- ✅ 完整漏洞信息提取
|
|
429
|
+
|
|
430
|
+
## 许可证
|
|
431
|
+
|
|
432
|
+
MIT License
|
|
433
|
+
|
|
434
|
+
## 作者
|
|
435
|
+
|
|
436
|
+
test233
|
|
437
|
+
|
|
438
|
+
## 致谢
|
|
439
|
+
|
|
440
|
+
- [Nmap](https://nmap.org/) - 强大的网络扫描器
|
|
441
|
+
- [CustomTkinter](https://github.com/TomSchimansky/CustomTkinter) - 现代化 Python GUI 框架
|
|
442
|
+
- [XlsxWriter](https://github.com/jmcnamara/XlsxWriter) - Excel 文件生成库
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
nmap_parser/__init__.py,sha256=7aT91XjS9koBik2g39Z8f5EvDaLkT7oMn6AkKghp6S0,279
|
|
2
|
+
nmap_parser/__main__.py,sha256=Uk6S9uE-wEj_Z8xskM63obKuFtqA7UJtD4Kw0OsD2M0,421
|
|
3
|
+
nmap_parser/cli.py,sha256=v1TZ62hOZIvcpeIXGJY1tA49oYgriC1kFfATC275IS8,3597
|
|
4
|
+
nmap_parser/core.py,sha256=Aie83O9WDzDr7Pfib_kkj4iEojZvTtSn6VPP2d8ct44,20429
|
|
5
|
+
nmap_parser/gui.py,sha256=NZG1zaXaU7A3H2ZKEGkXBssreSW819CA2dXkaGT5Zq8,9716
|
|
6
|
+
nmap_xml2xlsx-1.0.0.dist-info/METADATA,sha256=Ha3dRja13FyTanfXV7axYvoZ5wzY6ED8auI_CeweO_o,12368
|
|
7
|
+
nmap_xml2xlsx-1.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
|
|
8
|
+
nmap_xml2xlsx-1.0.0.dist-info/entry_points.txt,sha256=0q-D1ZDZafdQV3-DXlZRSdXrVADc-V53N-eBCAxN0Dk,146
|
|
9
|
+
nmap_xml2xlsx-1.0.0.dist-info/top_level.txt,sha256=8GOIK8t7dEL3UIq4pGctAZFbvj101calTO5MhnsPx2o,12
|
|
10
|
+
nmap_xml2xlsx-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
nmap_parser
|