portune 0.1.1__tar.gz → 0.1.3__tar.gz
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.
Potentially problematic release.
This version of portune might be problematic. Click here for more details.
- {portune-0.1.1/portune.egg-info → portune-0.1.3}/PKG-INFO +1 -1
- {portune-0.1.1 → portune-0.1.3}/portune/portune.py +76 -48
- {portune-0.1.1 → portune-0.1.3}/portune/version.py +2 -2
- {portune-0.1.1 → portune-0.1.3/portune.egg-info}/PKG-INFO +1 -1
- {portune-0.1.1 → portune-0.1.3}/.github/workflows/python-publish.yml +0 -0
- {portune-0.1.1 → portune-0.1.3}/.gitignore +0 -0
- {portune-0.1.1 → portune-0.1.3}/LICENSE +0 -0
- {portune-0.1.1 → portune-0.1.3}/README.md +0 -0
- {portune-0.1.1 → portune-0.1.3}/portune/__init__.py +0 -0
- {portune-0.1.1 → portune-0.1.3}/portune.egg-info/SOURCES.txt +0 -0
- {portune-0.1.1 → portune-0.1.3}/portune.egg-info/dependency_links.txt +0 -0
- {portune-0.1.1 → portune-0.1.3}/portune.egg-info/entry_points.txt +0 -0
- {portune-0.1.1 → portune-0.1.3}/portune.egg-info/top_level.txt +0 -0
- {portune-0.1.1 → portune-0.1.3}/pyproject.toml +0 -0
- {portune-0.1.1 → portune-0.1.3}/setup.cfg +0 -0
|
@@ -78,7 +78,16 @@ div {
|
|
|
78
78
|
text-align: right;
|
|
79
79
|
padding-right: 8px;
|
|
80
80
|
}
|
|
81
|
-
|
|
81
|
+
.desc {
|
|
82
|
+
text-overflow: ellipsis;
|
|
83
|
+
max-width: 300px;
|
|
84
|
+
white-space: nowrap;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
}
|
|
87
|
+
.desc:hover {
|
|
88
|
+
overflow: visible;
|
|
89
|
+
white-space: normal;
|
|
90
|
+
}
|
|
82
91
|
.table-container {
|
|
83
92
|
height: 90%;
|
|
84
93
|
overflow-y: auto;
|
|
@@ -600,6 +609,7 @@ def parse_input_file(filename: str) -> List[Tuple[str, List[int]]]:
|
|
|
600
609
|
A list of tuples, each containing:
|
|
601
610
|
- hostname (str): The target hostname or IP address
|
|
602
611
|
- ports (List[int]): List of ports to scan for that host
|
|
612
|
+
- desc (str): Optional description for the host
|
|
603
613
|
|
|
604
614
|
Examples:
|
|
605
615
|
Input file format:
|
|
@@ -615,19 +625,26 @@ def parse_input_file(filename: str) -> List[Tuple[str, List[int]]]:
|
|
|
615
625
|
('host3', [8080])
|
|
616
626
|
]
|
|
617
627
|
"""
|
|
618
|
-
|
|
628
|
+
host_dict = {}
|
|
619
629
|
with open(filename, 'r') as f:
|
|
620
630
|
for line in f:
|
|
621
631
|
line = line.strip().lower()
|
|
622
632
|
if not line or line.startswith('#'):
|
|
623
633
|
continue
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
634
|
+
words = line.split()
|
|
635
|
+
fqdn = words[0]
|
|
636
|
+
ports = words[1] if len(words) > 1 else '22'
|
|
637
|
+
port_list = [int(p) for p in ports.split(',')]
|
|
638
|
+
desc = ' '.join(words[2:]).strip() if len(words) > 2 else ''
|
|
639
|
+
if fqdn in host_dict:
|
|
640
|
+
existing_ports, existing_desc = host_dict[fqdn]
|
|
641
|
+
host_dict[fqdn] = (list(set(existing_ports + port_list)), existing_desc or desc)
|
|
642
|
+
else:
|
|
643
|
+
host_dict[fqdn] = (port_list, desc)
|
|
644
|
+
hosts = []
|
|
645
|
+
for fqdn in host_dict:
|
|
646
|
+
ports, desc = host_dict[fqdn]
|
|
647
|
+
hosts.append((fqdn, sorted(ports), desc))
|
|
631
648
|
return hosts
|
|
632
649
|
|
|
633
650
|
def ping_host(ip: str, timeout: float = 2.0) -> bool:
|
|
@@ -698,7 +715,7 @@ def resolve_and_ping_host(hostname: str, timeout: float = 2.0, noping: bool = Fa
|
|
|
698
715
|
except Exception:
|
|
699
716
|
return hostname, {'ip': 'N/A', 'ping': False}
|
|
700
717
|
|
|
701
|
-
def ping_hosts(hosts: List[Tuple[str, List[int]]],
|
|
718
|
+
def ping_hosts(hosts: List[Tuple[str, List[int], str]],
|
|
702
719
|
timeout: float = 2.0,
|
|
703
720
|
parallelism: int = 10,
|
|
704
721
|
noping: bool = False) -> Dict[str, Dict[str, Union[str, bool]]]:
|
|
@@ -739,7 +756,7 @@ def ping_hosts(hosts: List[Tuple[str, List[int]]],
|
|
|
739
756
|
with ThreadPoolExecutor(max_workers=parallelism) as executor:
|
|
740
757
|
future_to_host = {
|
|
741
758
|
executor.submit(resolve_and_ping_host, hostname, timeout, noping): hostname
|
|
742
|
-
for hostname, _ in hosts
|
|
759
|
+
for hostname, _, _ in hosts
|
|
743
760
|
}
|
|
744
761
|
|
|
745
762
|
# Add callback to update progress bar when each future completes
|
|
@@ -760,7 +777,8 @@ def ping_hosts(hosts: List[Tuple[str, List[int]]],
|
|
|
760
777
|
|
|
761
778
|
def check_port(hostname: str,
|
|
762
779
|
port: int,
|
|
763
|
-
host_info: Dict[str, Union[str, bool]],
|
|
780
|
+
host_info: Dict[str, Union[str, bool]],
|
|
781
|
+
desc: str,
|
|
764
782
|
timeout: float = 2.0) -> Tuple[str, str, int, str, bool]:
|
|
765
783
|
"""Check if a specific TCP port is accessible on a host.
|
|
766
784
|
|
|
@@ -774,6 +792,7 @@ def check_port(hostname: str,
|
|
|
774
792
|
- 'ip': IP address or 'N/A' if resolution failed
|
|
775
793
|
- 'ping': Boolean indicating ping status
|
|
776
794
|
- 'hostname': Optional resolved hostname from reverse DNS
|
|
795
|
+
desc: Optional description for the host
|
|
777
796
|
timeout: Maximum time to wait for connection in seconds
|
|
778
797
|
|
|
779
798
|
Returns:
|
|
@@ -783,6 +802,7 @@ def check_port(hostname: str,
|
|
|
783
802
|
- port: The port number that was checked
|
|
784
803
|
- status: Connection status (CONNECTED/TIMEOUT/REFUSED/UNREACHABLE)
|
|
785
804
|
- ping: Boolean indicating if the host responded to ping
|
|
805
|
+
- desc: Optional description for the host
|
|
786
806
|
|
|
787
807
|
Status meanings:
|
|
788
808
|
CONNECTED: Successfully established TCP connection
|
|
@@ -792,7 +812,7 @@ def check_port(hostname: str,
|
|
|
792
812
|
RESOLVE_FAIL: Could not resolve hostname to IP
|
|
793
813
|
"""
|
|
794
814
|
if host_info['ip'] == 'N/A':
|
|
795
|
-
return (hostname, host_info['ip'], port, 'RESOLVE_FAIL', host_info['ping'])
|
|
815
|
+
return (hostname, host_info['ip'], port, 'RESOLVE_FAIL', host_info['ping'], desc)
|
|
796
816
|
|
|
797
817
|
# Use resolved hostname if available
|
|
798
818
|
display_hostname = host_info.get('hostname', hostname)
|
|
@@ -802,16 +822,16 @@ def check_port(hostname: str,
|
|
|
802
822
|
try:
|
|
803
823
|
s.connect((host_info['ip'], port))
|
|
804
824
|
s.close()
|
|
805
|
-
return (display_hostname, host_info['ip'], port, 'CONNECTED', host_info['ping'])
|
|
825
|
+
return (display_hostname, host_info['ip'], port, 'CONNECTED', host_info['ping'], desc)
|
|
806
826
|
except ConnectionAbortedError:
|
|
807
|
-
return (display_hostname, host_info['ip'], port, 'CONNECTED', host_info['ping'])
|
|
827
|
+
return (display_hostname, host_info['ip'], port, 'CONNECTED', host_info['ping'], desc)
|
|
808
828
|
except (TimeoutError, socket.timeout):
|
|
809
|
-
return (display_hostname, host_info['ip'], port, 'TIMEOUT', host_info['ping'])
|
|
829
|
+
return (display_hostname, host_info['ip'], port, 'TIMEOUT', host_info['ping'], desc)
|
|
810
830
|
except ConnectionRefusedError:
|
|
811
|
-
return (display_hostname, host_info['ip'], port, 'REFUSED', host_info['ping'])
|
|
831
|
+
return (display_hostname, host_info['ip'], port, 'REFUSED', host_info['ping'], desc)
|
|
812
832
|
except Exception:
|
|
813
833
|
# Handle other network errors (filtered, network unreachable, etc)
|
|
814
|
-
return (display_hostname, host_info['ip'], port, 'UNREACHABLE', host_info['ping'])
|
|
834
|
+
return (display_hostname, host_info['ip'], port, 'UNREACHABLE', host_info['ping'], desc)
|
|
815
835
|
|
|
816
836
|
def send_email_report(
|
|
817
837
|
output_file: str,
|
|
@@ -906,6 +926,29 @@ def format_percent(value: int, total: int) -> Tuple[str, str]:
|
|
|
906
926
|
return "0/0", "0.0%"
|
|
907
927
|
return f"{value}/{total}", f"{(value/total*100):.1f}%"
|
|
908
928
|
|
|
929
|
+
def get_vlan_base(ip: str, bits: int) -> str:
|
|
930
|
+
"""Calculate VLAN base address with end padding 0."""
|
|
931
|
+
if ip == 'N/A':
|
|
932
|
+
return 'N/A'
|
|
933
|
+
try:
|
|
934
|
+
octets = ip.split('.')
|
|
935
|
+
if len(octets) != 4:
|
|
936
|
+
return 'invalid'
|
|
937
|
+
|
|
938
|
+
# Convert IP to 32-bit integer
|
|
939
|
+
ip_int = sum(int(octet) << (24 - 8 * i) for i, octet in enumerate(octets))
|
|
940
|
+
|
|
941
|
+
# Apply mask
|
|
942
|
+
mask = ((1 << bits) - 1) << (32 - bits)
|
|
943
|
+
masked_ip = ip_int & mask
|
|
944
|
+
|
|
945
|
+
# Convert back to dotted notation
|
|
946
|
+
result_octets = [(masked_ip >> (24 - 8 * i)) & 255 for i in range(4)]
|
|
947
|
+
return '.'.join(map(str, result_octets))
|
|
948
|
+
except:
|
|
949
|
+
return 'N/A'
|
|
950
|
+
|
|
951
|
+
|
|
909
952
|
def compute_stats(
|
|
910
953
|
results: List[Tuple[str, str, int, str, bool]],
|
|
911
954
|
start_time: float,
|
|
@@ -954,28 +997,9 @@ def compute_stats(
|
|
|
954
997
|
}
|
|
955
998
|
|
|
956
999
|
|
|
957
|
-
def get_vlan_base(ip: str, bits: int) -> str:
|
|
958
|
-
"""Calculate VLAN base address with end padding 0."""
|
|
959
|
-
try:
|
|
960
|
-
octets = ip.split('.')
|
|
961
|
-
if len(octets) != 4:
|
|
962
|
-
return 'invalid'
|
|
963
|
-
|
|
964
|
-
# Convert IP to 32-bit integer
|
|
965
|
-
ip_int = sum(int(octet) << (24 - 8 * i) for i, octet in enumerate(octets))
|
|
966
|
-
|
|
967
|
-
# Apply mask
|
|
968
|
-
mask = ((1 << bits) - 1) << (32 - bits)
|
|
969
|
-
masked_ip = ip_int & mask
|
|
970
|
-
|
|
971
|
-
# Convert back to dotted notation
|
|
972
|
-
result_octets = [(masked_ip >> (24 - 8 * i)) & 255 for i in range(4)]
|
|
973
|
-
return '.'.join(map(str, result_octets))
|
|
974
|
-
except:
|
|
975
|
-
return 'invalid'
|
|
976
1000
|
|
|
977
1001
|
# Collect VLAN statistics for timeouts
|
|
978
|
-
for hostname, ip, port, status, _ in results:
|
|
1002
|
+
for hostname, ip, port, status, _, _ in results:
|
|
979
1003
|
if ip != 'N/A':
|
|
980
1004
|
try:
|
|
981
1005
|
vlan = get_vlan_base(ip, bits)
|
|
@@ -989,7 +1013,7 @@ def compute_stats(
|
|
|
989
1013
|
# Group results by hostname for host statistics
|
|
990
1014
|
host_stats = defaultdict(lambda: {'statuses': [], 'ping': False})
|
|
991
1015
|
for result in results:
|
|
992
|
-
hostname, _, _, status, ping = result
|
|
1016
|
+
hostname, _, _, status, ping, _ = result
|
|
993
1017
|
host_stats[hostname]['statuses'].append(status)
|
|
994
1018
|
host_stats[hostname]['ping'] |= ping
|
|
995
1019
|
|
|
@@ -1016,7 +1040,7 @@ def compute_stats(
|
|
|
1016
1040
|
})
|
|
1017
1041
|
})
|
|
1018
1042
|
|
|
1019
|
-
for hostname, ip, port, status, _ in results:
|
|
1043
|
+
for hostname, ip, port, status, _, _ in results:
|
|
1020
1044
|
# For IP addresses, use VLAN/16 as domain
|
|
1021
1045
|
try:
|
|
1022
1046
|
socket.inet_aton(hostname)
|
|
@@ -1179,23 +1203,25 @@ def generate_html_report(
|
|
|
1179
1203
|
f.write(f'<h3 class="icon">Port Accessibility Report from {HOSTNAME} ({MY_IP}) to {os.path.basename(input_file)} - {time.strftime("%Y-%m-%d %H:%M:%S", scan_time)}</h3>\n')
|
|
1180
1204
|
|
|
1181
1205
|
# Write detailed results table
|
|
1182
|
-
f.write('''
|
|
1206
|
+
f.write(f'''
|
|
1183
1207
|
<div class="table-container" id="result-container">
|
|
1184
1208
|
<table id="commandTable">
|
|
1185
1209
|
<thead>
|
|
1186
1210
|
<tr>
|
|
1187
1211
|
<th>Hostname</th>
|
|
1188
1212
|
<th>IP</th>
|
|
1213
|
+
<th>VLAN/{stats['vlan_bits']}</th>
|
|
1189
1214
|
<th>Port</th>
|
|
1190
1215
|
<th>Status</th>
|
|
1191
1216
|
<th>Ping</th>
|
|
1217
|
+
<th>Description</th>
|
|
1192
1218
|
</tr>
|
|
1193
1219
|
</thead>
|
|
1194
1220
|
<tbody>
|
|
1195
1221
|
''')
|
|
1196
1222
|
|
|
1197
1223
|
# Add result rows
|
|
1198
|
-
for hostname, ip, port, status, ping in results:
|
|
1224
|
+
for hostname, ip, port, status, ping, desc in results:
|
|
1199
1225
|
ping_status = 'UP' if ping else 'N/A' if noping else 'DOWN'
|
|
1200
1226
|
ping_class = 'green' if ping else 'blue' if noping else 'red'
|
|
1201
1227
|
status_class = 'green' if status == 'CONNECTED' else 'blue' if status == 'REFUSED' else 'red'
|
|
@@ -1203,9 +1229,11 @@ def generate_html_report(
|
|
|
1203
1229
|
<tr>
|
|
1204
1230
|
<td>{escape(str(hostname))}</td>
|
|
1205
1231
|
<td>{escape(str(ip))}</td>
|
|
1232
|
+
<td>{str(get_vlan_base(ip, stats['vlan_bits']))}</td>
|
|
1206
1233
|
<td style="text-align: right;">{port}</td>
|
|
1207
1234
|
<td style="text-align: center;"><span class="{status_class} status">{escape(status)}</span></td>
|
|
1208
1235
|
<td style="text-align: center;"><span class="{ping_class} ping">{ping_status}</span></td>
|
|
1236
|
+
<td class="desc">{escape(str(desc))}</td>
|
|
1209
1237
|
</tr>
|
|
1210
1238
|
''')
|
|
1211
1239
|
|
|
@@ -1368,7 +1396,7 @@ def format_table_output(
|
|
|
1368
1396
|
table.append(separator)
|
|
1369
1397
|
|
|
1370
1398
|
# Add data rows
|
|
1371
|
-
for hostname, ip, port, status, ping in results:
|
|
1399
|
+
for hostname, ip, port, status, ping, desc in results:
|
|
1372
1400
|
ping_status = 'UP' if ping else 'N/A' if noping else 'DOWN'
|
|
1373
1401
|
table.append(row_format.format(
|
|
1374
1402
|
str(hostname), widths['Hostname'],
|
|
@@ -1414,14 +1442,14 @@ def main():
|
|
|
1414
1442
|
host_info = ping_hosts(hosts, args.timeout, args.parallelism, args.noping)
|
|
1415
1443
|
|
|
1416
1444
|
# Calculate total tasks and initialize progress bar
|
|
1417
|
-
total_tasks = sum(len(ports) for _, ports in hosts)
|
|
1445
|
+
total_tasks = sum(len(ports) for _, ports, _ in hosts)
|
|
1418
1446
|
print(f"Preparing to scan {len(hosts)} hosts with {total_tasks} total ports...", file=sys.stderr)
|
|
1419
1447
|
|
|
1420
1448
|
# Prepare tasks with pre-resolved data
|
|
1421
1449
|
tasks = []
|
|
1422
|
-
for hostname, ports in hosts:
|
|
1450
|
+
for hostname, ports, desc in hosts:
|
|
1423
1451
|
for port in ports:
|
|
1424
|
-
tasks.append((hostname, port, host_info[hostname]))
|
|
1452
|
+
tasks.append((hostname, port, host_info[hostname], desc))
|
|
1425
1453
|
|
|
1426
1454
|
results = []
|
|
1427
1455
|
lock = threading.Lock()
|
|
@@ -1430,8 +1458,8 @@ def main():
|
|
|
1430
1458
|
progress_bar = ProgressBar(total_tasks, prefix='Scanning')
|
|
1431
1459
|
|
|
1432
1460
|
with ThreadPoolExecutor(max_workers=args.parallelism) as executor:
|
|
1433
|
-
future_to_task = {executor.submit(check_port, hostname, port, info, args.timeout): (hostname, port, info)
|
|
1434
|
-
for hostname, port, info in tasks}
|
|
1461
|
+
future_to_task = {executor.submit(check_port, hostname, port, info, desc, args.timeout): (hostname, port, info)
|
|
1462
|
+
for hostname, port, info, desc in tasks}
|
|
1435
1463
|
|
|
1436
1464
|
for future in as_completed(future_to_task):
|
|
1437
1465
|
res = future.result()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|