-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathpentest_visualizer.py
More file actions
393 lines (327 loc) · 15.3 KB
/
pentest_visualizer.py
File metadata and controls
393 lines (327 loc) · 15.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
#!/usr/bin/env python3
"""
Pentest Kickoff Script
Processes nmap XML output and generates XLSX spreadsheet for pentest kickoff.
"""
import argparse
import xml.etree.ElementTree as ET
import pandas as pd
from collections import defaultdict
import sys
import os
from datetime import datetime
class NmapProcessor:
def __init__(self, xml_file):
self.xml_file = xml_file
self.hosts = []
self.unique_ports = set()
self.unique_services = set()
self.port_service_map = {} # port -> set of services
def parse_nmap_xml(self):
"""Parse nmap XML file and extract host information."""
try:
tree = ET.parse(self.xml_file)
root = tree.getroot()
for host in root.findall('host'):
host_data = self._parse_host(host)
if host_data:
self.hosts.append(host_data)
except ET.ParseError as e:
print(f"Error parsing XML file: {e}")
sys.exit(1)
except FileNotFoundError:
print(f"XML file not found: {self.xml_file}")
sys.exit(1)
def _parse_host(self, host):
"""Parse individual host element from nmap XML."""
host_data = {
'ip': None,
'status': 'down',
'ports': [],
'open_ports_count': 0
}
# Get IP address
address = host.find('address')
if address is not None:
host_data['ip'] = address.get('addr')
# Get host status
status = host.find('status')
if status is not None:
host_data['status'] = status.get('state', 'down')
# Parse ports
ports_element = host.find('ports')
if ports_element is not None:
for port in ports_element.findall('port'):
port_data = self._parse_port(port)
if port_data:
host_data['ports'].append(port_data)
if port_data['status'] == 'open':
host_data['open_ports_count'] += 1
self.unique_ports.add(port_data['port'])
if port_data['service']:
self.unique_services.add(port_data['service'])
# Handle multiple services on same port
if port_data['port'] not in self.port_service_map:
self.port_service_map[port_data['port']] = set()
self.port_service_map[port_data['port']].add(port_data['service'])
return host_data if host_data['ip'] else None
def _parse_port(self, port):
"""Parse individual port element."""
port_data = {
'port': port.get('portid'),
'protocol': port.get('protocol'),
'status': 'closed',
'service': None,
'version': None,
'notes': None
}
# Get port status
state = port.find('state')
if state is not None:
port_data['status'] = state.get('state', 'closed')
# Get service information
service = port.find('service')
if service is not None:
port_data['service'] = service.get('name')
version = service.get('version')
product = service.get('product')
if version or product:
port_data['version'] = f"{product or ''} {version or ''}".strip()
return port_data
def create_summary_sheet(self):
"""Create summary sheet with all hosts and unique ports/services."""
# Prepare host summary data
host_summary = []
for host in self.hosts:
host_summary.append({
'IP Address': host['ip'],
'Status': host['status'],
'Ports Open': host['open_ports_count']
})
# Prepare unique ports/services data
ports_services = []
for port in sorted(self.unique_ports, key=int):
services = self.port_service_map.get(port, set())
if services:
service_str = ', '.join(sorted(services))
else:
service_str = 'Unknown'
# Get protocol for this port (assuming all instances have same protocol)
protocol = 'tcp' # Default to tcp
for host in self.hosts:
for port_data in host['ports']:
if port_data['port'] == port and port_data['status'] == 'open':
protocol = port_data['protocol'].upper()
break
if protocol != 'tcp':
break
ports_services.append({
'Unique Ports': port,
'Protocol': protocol,
'Service': service_str
})
return host_summary, ports_services
def create_host_sheets(self):
"""Create individual sheets for each host."""
host_sheets = {}
for host in self.hosts:
if host['ports']:
port_data = []
for port in host['ports']:
port_data.append({
'IP': host['ip'],
'Port': port['port'],
'Protocol': port['protocol'].upper(),
'Status': port['status'],
'Service': port['service'] or 'Unknown',
'Version/Notes': port['version'] or ''
})
host_sheets[host['ip']] = port_data
return host_sheets
def print_cli_summary(self):
"""Print unique ports and services to CLI."""
print("\n" + "="*60)
print("UNIQUE PORTS AND SERVICES IDENTIFIED")
print("="*60)
print(f"{'Port':<8} {'Service':<30}")
print("-" * 40)
for port in sorted(self.unique_ports, key=int):
services = self.port_service_map.get(port, set())
if services:
service_str = ', '.join(sorted(services))
else:
service_str = 'Unknown'
print(f"{port:<8} {service_str:<30}")
print(f"\nTotal unique ports found: {len(self.unique_ports)}")
print(f"Total unique services found: {len(self.unique_services)}")
print("="*60)
def format_excel_sheet(self, worksheet, sheet_type="summary", ip_address=None):
"""Format Excel sheet with proper column sizing."""
from openpyxl.styles import Font, Alignment, PatternFill
from openpyxl.utils import get_column_letter
# Make headers bold and blue for individual host sheets
if sheet_type == "host" and ip_address:
blue_fill = PatternFill(start_color="A4C2F4", end_color="A4C2F4", fill_type="solid")
bold_font = Font(bold=True, color="000000") # Black text on blue background
title_font = Font(size=14, bold=True, color="000000") # Black text for title
else:
blue_fill = None
bold_font = Font(bold=True)
title_font = None
# Format title row (row 1) for host sheets
if sheet_type == "host" and ip_address:
for cell in worksheet[1]:
cell.font = title_font
cell.alignment = Alignment(horizontal='center')
cell.fill = blue_fill
# Format header row (row 2) for host sheets
if sheet_type == "host" and ip_address:
for cell in worksheet[2]:
cell.font = bold_font
cell.alignment = Alignment(horizontal='center')
cell.fill = blue_fill
else:
# Format header row for summary sheet
for cell in worksheet[1]:
cell.font = bold_font
cell.alignment = Alignment(horizontal='center')
# Auto-adjust column widths based on content
for column in worksheet.columns:
max_length = 0
column_letter = get_column_letter(column[0].column)
for cell in column:
try:
if len(str(cell.value)) > max_length:
max_length = len(str(cell.value))
except:
pass
# Set minimum and maximum column widths
if sheet_type == "summary":
if column_letter == 'A': # IP Address
adjusted_width = max(max_length + 2, 15)
elif column_letter == 'B': # Status
adjusted_width = max(max_length + 2, 12)
elif column_letter == 'C': # Ports Open
adjusted_width = max(max_length + 2, 12)
elif column_letter == 'E': # Service (for ports/services section)
adjusted_width = max(max_length + 2, 25)
else:
adjusted_width = max(max_length + 2, 10)
else: # Individual host sheets
if column_letter == 'A': # IP
adjusted_width = max(max_length + 2, 15)
elif column_letter == 'B': # Port
adjusted_width = max(max_length + 2, 8)
elif column_letter == 'C': # Protocol
adjusted_width = max(max_length + 2, 10)
elif column_letter == 'D': # Status
adjusted_width = max(max_length + 2, 10)
elif column_letter == 'E': # Service
adjusted_width = max(max_length + 2, 15)
elif column_letter == 'F': # Version/Notes
adjusted_width = max(max_length + 2, 30)
else:
adjusted_width = max(max_length + 2, 10)
worksheet.column_dimensions[column_letter].width = min(adjusted_width, 50) # Cap at 50
def add_ip_title_to_sheet(self, worksheet, ip_address):
"""Add IP address as title at the top of the sheet."""
from openpyxl.styles import Font, Alignment
# Insert a new row at the top
worksheet.insert_rows(1)
# Add IP address as title
title_cell = worksheet.cell(row=1, column=1, value=f"Host: {ip_address}")
# Merge cells across all columns for the title
from openpyxl.utils import get_column_letter
last_column = get_column_letter(worksheet.max_column)
worksheet.merge_cells(f'A1:{last_column}1')
def format_summary_headers(self, worksheet):
"""Format headers of both tables in summary sheet with blue background."""
from openpyxl.styles import PatternFill, Font, Alignment
blue_fill = PatternFill(start_color="A4C2F4", end_color="A4C2F4", fill_type="solid")
bold_font = Font(bold=True, color="000000")
# Format first table header (IP Address, Status, Ports Open)
for col in range(1, 4): # Columns A, B, C
cell = worksheet.cell(row=1, column=col)
cell.fill = blue_fill
cell.font = bold_font
cell.alignment = Alignment(horizontal='center')
# Find the start of the ports/services section
ports_start_row = None
for row in range(1, worksheet.max_row + 1):
if worksheet.cell(row=row, column=1).value == "Unique Ports":
ports_start_row = row
break
# Format second table header (Unique Ports, Protocol, Service)
if ports_start_row:
for col in range(1, 4): # Columns A, B, C for Unique Ports, Protocol, Service
cell = worksheet.cell(row=ports_start_row, column=col)
cell.fill = blue_fill
cell.font = bold_font
cell.alignment = Alignment(horizontal='center')
def generate_xlsx(self, output_file):
"""Generate XLSX file with all sheets."""
with pd.ExcelWriter(output_file, engine='openpyxl') as writer:
# Create summary sheet
host_summary, ports_services = self.create_summary_sheet()
# Host summary
host_df = pd.DataFrame(host_summary)
host_df.to_excel(writer, sheet_name='Summary', index=False)
# Add ports/services to summary sheet (below host data)
if ports_services:
ports_df = pd.DataFrame(ports_services)
# Write to same sheet but starting from a different row
ports_df.to_excel(writer, sheet_name='Summary',
startrow=len(host_summary) + 3, index=False)
# Create individual host sheets
host_sheets = self.create_host_sheets()
for ip, port_data in host_sheets.items():
# Clean IP for sheet name (replace dots with underscores)
sheet_name = ip.replace('.', '_')
if len(sheet_name) > 31: # Excel sheet name limit
sheet_name = sheet_name[:31]
port_df = pd.DataFrame(port_data)
port_df.to_excel(writer, sheet_name=sheet_name, index=False)
# Apply formatting to all sheets
from openpyxl import load_workbook
wb = load_workbook(output_file)
# Format Summary sheet
if 'Summary' in wb.sheetnames:
self.format_excel_sheet(wb['Summary'], "summary")
self.format_summary_headers(wb['Summary'])
# Format individual host sheets with IP titles and blue headers
for sheet_name in wb.sheetnames:
if sheet_name != 'Summary':
# Extract IP address from sheet name (replace underscores with dots)
ip_address = sheet_name.replace('_', '.')
# Add IP title to the sheet
self.add_ip_title_to_sheet(wb[sheet_name], ip_address)
# Format the sheet with blue headers
self.format_excel_sheet(wb[sheet_name], "host", ip_address)
# Save the formatted workbook
wb.save(output_file)
def main():
parser = argparse.ArgumentParser(description='Process nmap XML output and generate pentest kickoff spreadsheet')
parser.add_argument('xml_file', help='Input nmap XML file')
parser.add_argument('-o', '--output', default='pentest_kickoff.xlsx',
help='Output XLSX file (default: pentest_kickoff.xlsx)')
args = parser.parse_args()
print(f"Processing nmap XML file: {args.xml_file}")
print(f"Output file: {args.output}")
# Initialize processor
processor = NmapProcessor(args.xml_file)
# Parse XML
print("Parsing nmap XML...")
processor.parse_nmap_xml()
if not processor.hosts:
print("No hosts found in XML file!")
sys.exit(1)
print(f"Found {len(processor.hosts)} hosts")
# Generate XLSX
print("Generating XLSX file...")
processor.generate_xlsx(args.output)
# Print CLI summary
processor.print_cli_summary()
print(f"\nPentest kickoff spreadsheet created: {args.output}")
print(f"Generated at: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
if __name__ == "__main__":
main()