#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
    @copyright: 2008 Lari Huttunen
    @license: MIT <http://www.opensource.org/licenses/mit-license.php>
"""
import re
import os
import sys
import socket
import optparse
import xml.dom.minidom
from opencollab.meta import Meta, Metas
from opencollab.wiki import CLIWiki, WikiFailure
from opencollab.util.config import parseOptions
from opencollab.util.file import hashFile, uploadFile
from xml.dom.minidom import Node, ProcessingInstruction

NMAP_KEYS = ["Hosts Down", "Hosts Total", "Hosts Up",
             "NMAP Run", "NMAP Version", "NMAP traceroute",
             "OS Accuracy", "OS Family", "OS Generation",
             "OS Type", "OS Vendor", "PTR", "Run Arguments",
             "end", "start", "type", "Vendor", "TYPE",
             "XML Output Version", "listens to", "serves",
             "Audit"]

def handle_script(host_ipv4, script, metas):
    """
    This function is meant for implementing any sort of custom
    postprocessing of specific Nmap script results
    """
    scriptid = script.getAttribute('id')
    scriptcontent = script.getAttribute('output')
    scriptcontent = scriptcontent.replace('&#xa;', ' ').replace('\n', ' ').strip()
    metas[host_ipv4]['NMAP %s' % (scriptid)].add(scriptcontent)
    return metas


def parse_xml(metas, file, nmaprun_page):
    """
    xml.dom.minidom usage from
    http://trac.ict-ok.org/browser/trunk/org/ict_ok/admin_utils/netscan/nmap/nmap.py?rev=145
    """
    try:
        doc = xml.dom.minidom.parse(file)
    except xml.parsers.expat.ExpatError, e:
        error = file + ": " + str(e)
        sys.exit(error)
    hosts = doc.getElementsByTagName('host')
    for host in hosts:
        if host.nodeType == Node.ELEMENT_NODE:
            hostStatusElements = host.getElementsByTagName('status')
            if hostStatusElements[0].hasAttribute('state'):
                hostState = hostStatusElements[0].getAttribute('state')
                if hostState == 'up':
                    hostAddressElements = host.getElementsByTagName('address')

                    for addressElement in hostAddressElements:
                        addressType = addressElement.getAttribute('addrtype')
                        address = addressElement.getAttribute('addr')
                        if addressType == 'ipv4':
                            metas[address]['TYPE'].add('IPv4')
                            host_ipv4 = address
                            metas[address]['NMAP Run'].add('[[%s]]' % nmaprun_page)
                        if addressType == 'mac':
                            metas[address]['TYPE'].add('MAC')
                            metas[address]['Vendor'].add(addressElement.getAttribute('vendor'))
                            metas[address]['NMAP Run'].add('[[%s]]' % nmaprun_page)

                    hostNameElements = host.getElementsByTagName('hostname')
                    if host_ipv4 and len(hostNameElements) > 0:
                        hostname = hostNameElements[0].getAttribute('name')
                        metas[host_ipv4]['PTR'].add('[[%s]]' % hostname)
                        metas[hostname]['TYPE'].add('NAME')
                    hostPorts = host.getElementsByTagName('port')

                    if host_ipv4:
                        for hostPort in hostPorts:
                            portStatusElements = hostPort.getElementsByTagName('state')
                            if portStatusElements[0].getAttribute('state').find("open") >= 0:
                                portServiceElements = hostPort.getElementsByTagName('service')
                                state = portStatusElements[0].getAttribute('state')
                                port = hostPort.getAttribute('protocol').upper() + ":" + hostPort.getAttribute('portid').upper()
                                metas[host_ipv4]['listens to'].add('[[%s]]' % port)
                                if len(portServiceElements) > 0:
                                    service = portServiceElements[0].getAttribute('name')
                                    product = portServiceElements[0].getAttribute('product')
                                    if product:
                                        service += ' with ' + product
                                        service += ' ' + portServiceElements[0].getAttribute('version')
                                        service += ' ' + portServiceElements[0].getAttribute('extrainfo')
                                    service += ' on port ' + port
                                    metas[host_ipv4]['serves'].add(service)

                            # Grab any script results
                            portScripts = hostPort.getElementsByTagName('script')
                            for script in portScripts:
                                metas = handle_script(host_ipv4, script, metas)

                        hostOss = host.getElementsByTagName('osclass')
                        for hostOs in hostOss:
                            metas[host_ipv4]['OS Type'].add(hostOs.getAttribute('type'))
                            metas[host_ipv4]['OS Vendor'].add(hostOs.getAttribute('vendor'))
                            metas[host_ipv4]['OS Family'].add(hostOs.getAttribute('osfamily'))
                            metas[host_ipv4]['OS Generation'].add(hostOs.getAttribute('osgen'))
                            metas[host_ipv4]['OS Accuracy'].add(hostOs.getAttribute('accuracy'))

                        # Grab any script results
                        hostScript = host.getElementsByTagName('hostscript')
                        for scriptportion in hostScript:
                            scripts = scriptportion.getElementsByTagName('script')
                            for script in scripts:
                                metas = handle_script(host_ipv4, script, metas)

                        # Grab traceroute results
                        traces = host.getElementsByTagName('trace')
                        for trace in traces:
                            port = trace.getAttribute('port')
                            hops = trace.getElementsByTagName('hop')
                            for hop in hops:
                                dst_ipv4 = hop.getAttribute('ipaddr')
                                # Lack of reply should be omitted
                                if dst_ipv4.startswith('0.'):
                                    continue
                                ttl = "%03d" % int(hop.getAttribute('ttl'))
                                trace_string = "TTL %s [[%s]] (port [[TCP:%s]])" % (ttl, dst_ipv4, port)
                                metas[host_ipv4]['NMAP traceroute'].add(trace_string)

    nmaprun = doc.getElementsByTagName('nmaprun')
    metas[nmaprun_page]['Run Arguments'].add(nmaprun[0].getAttribute('args'))
    metas[nmaprun_page]['NMAP Version'].add(nmaprun[0].getAttribute('version'))
    metas[nmaprun_page]['start'].add('<<DateTime(%f)>>' % float(nmaprun[0].getAttribute('start')))
    metas[nmaprun_page]['XML Output Version'].add(nmaprun[0].getAttribute('xmloutputversion'))
    run_stats = doc.getElementsByTagName('finished')
    metas[nmaprun_page]['end'].add('<<DateTime(%f)>>' % float(run_stats[0].getAttribute('time')))
    hosts = doc.getElementsByTagName('hosts')
    metas[nmaprun_page]['Hosts Up'].add(hosts[0].getAttribute('up'))
    metas[nmaprun_page]['Hosts Down'].add(hosts[0].getAttribute('down'))
    metas[nmaprun_page]['Hosts Total'].add(hosts[0].getAttribute('total'))
    return metas


def main():
    parser = optparse.OptionParser()
    parser.add_option("-A", "--audit", dest="audit", default=None,
        metavar="AUDIT", help=("AUDIT name to tag identities with."))
    parser.add_option("-l", "--last-edit-wins",
        action="store_true", dest="lasteditwins", default=False,
        metavar="LAST-EDIT-WINS", help="Replace meta keys and values with LAST-EDIT-WINS semantics.")
    parser.add_option("-P", "--progress", action="store_true",
        dest="progress", default=False, help="Display file upload progress information.")
    parser.set_usage("%prog [options] NMAP-XML-FILES")
    ops = {}
    sect = "nmap-uploader"
    nmap_files = []
    ops = parseOptions(parser, sect, template=True, category=True)
    url = ops["creds"]["url"]
    x509 = ops[sect]["x509"]
    x509_ca_file = ops[sect]["x509_ca_file"]
    progress = ops[sect]["progress"]
    verbose = ops[sect]["verbose"]
    template = ops[sect]["template"]
    audit = ops[sect]["audit"]
    if template is None:
        template = "IdentityTemplate"
    category = ops[sect]["category"]
    if category is None:
        category = "CategoryIdentity"
    replace = ops[sect]["lasteditwins"]
    args = ops[sect]["args"]
    if len(args) < 1:
        parser.error("At least one XML input file path needs to be specified.")

    while True:
        try:
            collab = CLIWiki(ssl_verify_cert=x509, ssl_ca_certs=x509_ca_file, **ops['creds'])
        except WikiFailure:
            print "ERROR: Authentication failed."
        except (UnicodeError, socket.gaierror):
            sys.exit("ERROR: Not a valid URL.")
        else:
            break
    if verbose:
        print "NOTE: Processing input files."

    for arg in args:
        nmap_files.append(arg)

    metas = Metas()
    for file in nmap_files:
        xml_page = hashFile(file)
        fname = os.path.basename(file)
        if verbose:
            print "NOTE: Parsing", file

        metas = parse_xml(metas, file, xml_page)
        metas[xml_page]['type'].add('NMAP Run')
        metas[xml_page]['gwikicategory'] = ['CategoryNmap', 'CategoryScan']
        metas[xml_page]['gwikitemplate'] = ['ScanTemplate']

        if category is not None:
            for page in metas:
                if not 'gwikicategory' in metas[page]:
                    metas[page]["gwikicategory"].add(category)
        if template is not None:
            for page in metas:
                if not 'gwikitemplate' in metas[page]:
                    metas[page]["gwikitemplate"].add(template)

        if audit is not None:
            for page in metas:
                metas[page]["Audit"].add(audit)

        if verbose:
            print "NOTE: Uploading", file, "to", xml_page
        try:
            status = uploadFile(collab, xml_page, file, fname, progress)
        except (IOError, TypeError, RuntimeError), msg:
            sys.exit(msg)

    if verbose:
        print "NOTE: Importing metas to", url
    cleared = Meta()
    if replace:
        for page, meta in metas.iteritems():
            cleared[page] = NMAP_KEYS

    collab.incSetMeta(cleared, Metas(), metas)


if __name__ == "__main__":
    try:
        main()
    except KeyboardInterrupt:
        print "Script interrupted via CTRL-C."
