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.
@@ -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"
@@ -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
+ ![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)
34
+ ![License](https://img.shields.io/badge/license-MIT-green.svg)
35
+ ![Platform](https://img.shields.io/badge/platform-Windows%20%7C%20Linux%20%7C%20macOS-lightgrey.svg)
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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (84.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,6 @@
1
+ [console_scripts]
2
+ nmap-parser = nmap_parser.cli:main
3
+ nmap-parser-gui = nmap_parser.gui:main
4
+
5
+ [gui_scripts]
6
+ nmap-parser-gui = nmap_parser.gui:main
@@ -0,0 +1 @@
1
+ nmap_parser