#!/usr/bin/env python
import argparse
from termcolor import colored
from tabulate import tabulate

"""
Script to extract the information, about the Attack Surface identified,
from the grepable (-oG) output of a NMAP scan.

Output flags: -oG scan.txt

Sources:
    https://github.com/nmap/nmap
    https://nmap.org/book/man-output.html
    https://github.com/nmap/nmap/blob/master/nmap-services

Dependencies:
    pip install tabulate colorama termcolor  
"""
# See https://github.com/nmap/nmap/blob/master/nmap-services
SERVICES_MAPPING = {"Web App and API": [80, 443, 8080, 8443, 9080, 9090, 9043, 8000, 8005, 8008, 9000, 9443],
                    "Remote Access": [22, 23, 107, 3389, 992, 513],
                    "File Transfer": [20, 21, 69, 989, 990],
                    "File Server and RPC": [111, 2049, 445],
                    "Mail": [24, 25, 109, 110, 143, 993, 995, 2525, 465],
                    "DNS": [42, 53, 63],
                    "DB Oracle": [1521, 1522, 1523],
                    "DB MSSQL": [1433],
                    "DB MySQL and Maria": [3306],
                    "DB PostgreSQL": [5432],
                    "DB Mongo": [27017, 27018, 27019],
                    "DB Redis": [6379],
                    "DB Cassandra": [7000, 7001, 7199, 9042],
                    "DB Neo4J": [7473, 7474],
                    "Monitoring SNMP": [161, 162, 10161, 10162],
                    "Monitoring SYSLOG": [514],
                    "MOM RabbitMQ": [5672, 15675],
                    "MOM ActiveMQ": [61616],
                    "MOM IBM MQSeries": [1414],
                    "MOM MQTT": [1883, 8883],
                    "Network time": [123],                    
                    "LDAP": [389, 636],
                    "Real-Time Streaming Protocol": [554]
                    }
AS_CATEGORY_OTHER_KEY = "Other"


def get_service_as_category_for_port(port):
    port_as_int = int(port)
    as_category = AS_CATEGORY_OTHER_KEY
    for srv in SERVICES_MAPPING:
        if port_as_int in SERVICES_MAPPING[srv]:
            as_category = srv
            break
    return as_category


def load_hosts(nmap_file):
    hosts = {}
    with open(nmap_file, mode="r", encoding="utf-8") as f:
        lines = f.read().splitlines()
    if " -oG " not in lines[0] and " -oA " not in lines[0]:
        raise Exception("Input file is not using the NMAP grepable format!")
    for line in lines:
        if "Ports:" in line and "/open/" in line:
            parts = line.split("(")
            # Extract the host IP part
            host = parts[0].split(":")[1].strip(" ()")
            if host not in hosts:
                hosts[host] = []
            # Extract the ports part
            ports = parts[1].split(":")[1].split(",")
            for port in ports:
                port_value = port.split("/")[0].strip(" ")
                if port_value not in hosts[host]:
                    hosts[host].append(port_value)
    return hosts


def compute_ports_distribution(hosts):
    stats = []
    metrics = {}
    for host in hosts:
        for port in hosts[host]:
            port_str = str(port)
            if port_str not in metrics:
                metrics[port_str] = 0
            metrics[port_str] += 1

    ports_with_single_occurence = []
    for key in sorted(metrics, key=metrics.get, reverse=True):
        # Gather, for the rendering, ports with a single occurence
        if metrics[key] == 1:
            ports_with_single_occurence.append(key)
        else:
            stats.append([key, metrics[key]])

    if len(ports_with_single_occurence) > 0:
        stats.append([",".join(ports_with_single_occurence), 1])

    return stats


def summarize_attack_surface(hosts):
    as_categories = []
    metrics = {}
    for host in hosts:
        for port in hosts[host]:
            as_category = get_service_as_category_for_port(port)
            if as_category not in metrics:
                metrics[as_category] = {"occurence": 0, "ports": []}
            metrics[as_category]["occurence"] = metrics[as_category]["occurence"] + 1
            if port not in metrics[as_category]["ports"]:
                metrics[as_category]["ports"].append(port)

    for key in metrics:
        if key != AS_CATEGORY_OTHER_KEY:
            metrics[key]["ports"].sort(reverse=True)
            as_categories.append([key.capitalize(), metrics[key]["occurence"], ",".join(metrics[key]["ports"])])

    if AS_CATEGORY_OTHER_KEY in metrics:
        metrics[AS_CATEGORY_OTHER_KEY]["ports"].sort(reverse=True)
        as_categories.append([AS_CATEGORY_OTHER_KEY,  metrics[AS_CATEGORY_OTHER_KEY]["occurence"], ",".join(metrics[AS_CATEGORY_OTHER_KEY]["ports"])])

    return as_categories


if __name__ == "__main__":
    parser = argparse.ArgumentParser(description="Script to extract the information, about the Attack Surface identified, from the grepable output of a NMAP scan.")
    parser.add_argument("-f", action="store", dest="nmap_file", help="Text file coming from the NMAP scan", required=True)
    args = parser.parse_args()
    hosts = load_hosts(args.nmap_file)
    print(colored(f"[+] Total of hosts:", "yellow"))
    print(len(hosts))
    print(colored(f"[+] Ports distribution:", "yellow"))
    affected = compute_ports_distribution(hosts)
    print(tabulate(affected, headers=["Port", "Occurence"]))
    print(colored(f"[+] Attack surface identified:", "yellow"))
    affected = summarize_attack_surface(hosts)
    print(tabulate(affected, headers=["Vector type", "Occurence", "Port(s)"]))
