package com.tenable.io.api.workbenches;


import com.tenable.io.api.models.*;
import com.tenable.io.api.workbenches.models.nessus.*;
import com.tenable.io.core.exceptions.TenableIoException;
import com.tenable.io.core.utilities.*;
import com.tenable.io.core.utilities.models.DefaultXmlObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.net.Inet4Address;
import java.net.InetAddress;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.UUID;


/**
 * Copyright (c) 2017 Tenable Network Security, Inc.
 */
public class WorkbenchNessusFileParser {
    private static Logger logger = LoggerFactory.getLogger( WorkbenchNessusFileParser.class );

    private int pagesize;
    private File fileToParse;
    private NessusFileParser parser;
    private Map<String, NessusObject> nessusObjectByName;


    /**
     * Instantiates a new Workbench nessus file parser in non-paginated mode.
     *
     * @param fileToParse the file to parse
     */
    public WorkbenchNessusFileParser( File fileToParse ) {
        nessusObjectByName = new HashMap<>(4);
        nessusObjectByName.put( "report", new ReportObject() );
        nessusObjectByName.put( "reporthost", new AssetWithVulnerabilitiesObject() );
        nessusObjectByName.put( "hostproperties", new AssetObject() );
        nessusObjectByName.put( "reportitem", new VulnerabilityObject() );

        this.fileToParse = fileToParse;
        this.pagesize = 0;
    }


    /**
     * Instantiates a new Workbench nessus file parser in paginated mode.
     *
     * @param fileToParse the file to parse
     * @param pagesize    the page size
     */
    public WorkbenchNessusFileParser( File fileToParse, int pagesize ) {
        this( fileToParse );
        this.pagesize = pagesize;
    }


    /**
     * Parse the whole nessus file object, regardless of the pagination parameters.
     * Note: calling this method after calling getNextPage() one or more times will lead to unpredictable results
     *
     * @return A Report object containing the entire deserialized nessus file object
     * @throws TenableIoException thrown if a parameter is invalid or the nessus file cannot be found or when encountering deserialization error
     */
    public Report parseAll() throws TenableIoException {
        if( this.parser  == null )
            this.parser = new NessusFileParserBuilder()
                    .withNessusObjectByNameMap( nessusObjectByName )
                    .withNessusFile( fileToParse )
                    .build();

        return (Report)parser.parseAll( "Report" );
    }


    /**
     * Parses the next page's worth of AssetVulnerabilities object and returns it.
     * Returns an empty list if the last items has been reached.
     *
     * @return the next page's worth of AssetVulnerabilities object
     * @throws TenableIoException thrown if a parameter is invalid or the nessus file cannot be found or when encountering deserialization error
     */
    public List<AssetVulnerabilities> getNextAssetPage() throws TenableIoException {
        return (List)getNextPage( "reporthost" );
    }


    /**
     * Parses the next page's worth of Vulnerability object and returns it.
     * Returns an empty list if the last items has been reached.
     *
     * @return the next page's worth of Vulnerability object
     * @throws TenableIoException thrown if a parameter is invalid or the nessus file cannot be found or when encountering deserialization error
     */
    public List<Vulnerability> getNextVulnerabilitiesPage() throws TenableIoException {
        return (List)getNextPage( "reportitem" );
    }


    private List<Object> getNextPage( String paginatedObjectName ) throws TenableIoException {
        if( this.parser  == null )
            this.parser = new NessusFileParserBuilder()
                    .withNessusObjectByNameMap( nessusObjectByName )
                    .withNessusFile( fileToParse )
                    .withPagesize( pagesize )
                    .withPaginateObjectName( paginatedObjectName )
                    .build();

        return parser.getNextPage();
    }


    // ************** Objects specific parsers ****************
    private class ReportObject extends NessusObject<Report> {
        @Override
        public Report create( String nodeName, Map<String, String> attributes ) {
            return new Report().withName( attributes.get( "name" ) );
        }


        @Override
        public void gotChild( Report currentObject, Object child ) {
            if( child instanceof AssetVulnerabilities )
                currentObject.addAssetWithVulnerabilities( (AssetVulnerabilities)child );
        }
    }


    private class AssetWithVulnerabilitiesObject extends NessusObject<AssetVulnerabilities> {
        @Override
        public AssetVulnerabilities create( String nodeName, Map<String, String> attributes ) {
            return new AssetVulnerabilities().withName( attributes.get( "name" ) );
        }


        @Override
        public void gotChild( AssetVulnerabilities currentObject, Object child ) {
            if( child instanceof Asset )
                currentObject.setAsset( (Asset)child );
            else if( child instanceof Vulnerability )
                currentObject.addVulnerability( (Vulnerability)child );
        }
    }


    private class AssetObject extends NessusObject<Asset> {
        @Override
        public Asset create( String nodeName, Map<String, String> attributes ) {
            return new Asset();
        }

        @Override
        public void gotChild( Asset currentObject, Object child ) {
            if( child instanceof DefaultXmlObject ) {
                DefaultXmlObject c = (DefaultXmlObject)child;
                if( c.getName().toLowerCase() == "tag" ) {
                    String tagName = c.getAttributes().get( "name" ).toLowerCase();
                    String tagValue = c.getValue();
                    try {
                        switch( tagName ) {
                            case "bios-uuid":
                                currentObject.setBiosUuid( UUID.fromString( tagValue ) );
                                break;
                            case "host-fqdn":
                                currentObject.setHostFqdn( tagValue );
                                break;
                            case "hostname":
                                currentObject.setHostName( tagValue );
                                break;
                            case "host-ip":
                                String[] ipChunks = tagValue.split( "\\." );
                                if( ipChunks.length == 4 ) {
                                    byte[] hostIp = new byte[4];
                                    for( int i = 0; i < 4; i++ )
                                        hostIp[i] = (byte)Integer.parseInt( ipChunks[i] );

                                    InetAddress address = InetAddress.getByAddress( tagValue, hostIp );
                                    if( address instanceof Inet4Address )
                                        currentObject.setHostIpV4( (Inet4Address)address );
                                }
                                break;
                            case "host-uuid":
                                currentObject.setId( UUID.fromString( tagValue ) );
                                break;
                            case "host_start":
                                currentObject.setLastHostScanStart( DateHelper.parseIso8601Date( tagValue ) );
                                break;
                            case "host_end":
                                currentObject.setLastHostScanEnd( DateHelper.parseIso8601Date( tagValue ) );
                                break;
                            case "lastauthenticatedresults":
                                currentObject.setLastAuthenticatedResult( DateHelper.parseIso8601Date( tagValue ) );
                                break;
                            case "local-checks-proto":
                                currentObject.setLastAuthenticatedScanProto( tagValue );
                                break;
                            case "mac-macaddress":
                                String[] macs = tagValue.split( "\n" );
                                for( String mac : macs ) {
                                    mac = mac.trim();
                                    if( mac.length() > 0 ) // the last mac is typically a sequence of spaces
                                        currentObject.addMacAddress( MacAddressHelper.parse( mac ) );
                                }
                                break;
                            case "mcafee-epo-guid":
                                currentObject.setMcAfeeApoGuid( UUID.fromString( tagValue ) );
                                break;
                            case "netbios-name":
                                currentObject.setNetbiosName( tagValue );
                                break;
                            case "operating-system":
                                currentObject.setOperatingSystem( tagValue );
                                break;
                            case "system-type":
                                currentObject.setSystemType( tagValue );
                                break;
                        }
                    } catch( Exception e ) {
                        logger.warn( String.format( "Exception when parsing Host Properties tag \'%s\' with value \'%s\'", tagName, tagValue ), e );
                    }
                }
            }
        }
    }


    private class VulnerabilityObject extends NessusObject<Vulnerability> {
        @Override
        public Vulnerability create( String nodeName, Map<String, String> attributes ) {
            SeverityLevel severityLevel = SeverityLevel.INFO;
            try {
                severityLevel = SeverityLevel.forIntValue( Integer.parseInt( attributes.get( "severity" ) ) );
            } catch( TenableIoException e ) {
                // Should never happen, but log error just in case
                logger.error( String.format( "Exception when parsing a ReportItem (pluginID: %s) SeverityLevel of \'%s\'.", attributes.get( "pluginid" ), attributes.get( "severity" ) ), e );
            }

            return new Vulnerability()
                    .withSeverity( severityLevel )
                    .withProtocol( attributes.get( "protocol" ) )
                    .withPluginFamily( attributes.get( "pluginfaimily" ) )
                    .withPort( Integer.parseInt( attributes.get( "port" ) ) )
                    .withPluginID( Long.parseLong( attributes.get( "pluginid" ) ) )
                    .withPluginName( attributes.get( "pluginname" ) );
        }


        @Override
        public void gotChild( Vulnerability currentObject, Object child ) {
            if( child instanceof DefaultXmlObject ) {
                DefaultXmlObject c = (DefaultXmlObject)child;
                String childName = c.getName().toLowerCase();
                String childValue = c.getValue();

                try {
                    switch( childName ) {
                        case "bid":
                            currentObject.addBid( childValue );
                            break;
                        case "canvas_package":
                            currentObject.setCanvasPackage( childValue );
                            break;
                        case "cve":
                            currentObject.addCve( childValue );
                            break;
                        case "cvss_base_score":
                            currentObject.setCvssBaseScore( Float.parseFloat( childValue ) );
                            break;
                        case "cvss_temporal_score":
                            currentObject.setCvssTemporalScore( Float.parseFloat( childValue ) );
                            break;
                        case "cvss_temporal_vector":
                            currentObject.setCvssTemporalVector( childValue );
                            break;
                        case "cvss_vector":
                            currentObject.setCvssVector( childValue );
                            break;
                        case "cvss3_base_score":
                            currentObject.setCvss3BaseScore( Float.parseFloat( childValue ) );
                            break;
                        case "cvss3_temporal_score":
                            currentObject.setCvss3TemporalScore( Float.parseFloat( childValue ) );
                            break;
                        case "cvss3_temporal_vector":
                            currentObject.setCvss3TemporalVector( childValue );
                            break;
                        case "cvss3_vector":
                            currentObject.setCvss3Vector( childValue );
                            break;
                        case "d2_elliot_name":
                            currentObject.setD2ElliotName( childValue );
                            break;
                        case "description":
                            currentObject.setDescription( childValue );
                            break;
                        case "exploit_available":
                            currentObject.setExploitAvailable( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploited_by_nessus":
                            currentObject.setExploitedByNessus( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploit_framework_canvas":
                            currentObject.setExploitFrameworkCanvas( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploit_framework_core":
                            currentObject.setExploitFrameworkCore( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploit_framework_exploithub":
                            currentObject.setExploitFrameworkExploithub( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploit_framework_metasploit":
                            currentObject.setExploitFrameworkMetasploit( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploit_framework_d2_elliot":
                            currentObject.setExploitFrameworkD2Elliot( Boolean.parseBoolean( childValue ) );
                            break;
                        case "exploited_by_malware":
                            currentObject.setExploitedByMalware( Boolean.parseBoolean( childValue ) );
                            break;
                        case "first_found":
                            currentObject.setFirstFound( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "has_patch":
                            currentObject.setHasPatch( Boolean.parseBoolean( childValue ) );
                            break;
                        case "in_the_news":
                            currentObject.setInTheNews( Boolean.parseBoolean( childValue ) );
                            break;
                        case "last_found":
                            currentObject.setLastFound( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "last_fixed":
                            currentObject.setLastFixed( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "malware":
                            currentObject.setMalware( Boolean.parseBoolean( childValue ) );
                            break;
                        case "metasploit_name":
                            currentObject.setMetasploitName( childValue );
                            break;
                        case "patch_publication_date":
                            currentObject.setPatchPublicationDate( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "plugin_modification_date":
                            currentObject.setPluginModificationDate( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "plugin_publication_date":
                            currentObject.setPluginPublicationDate( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "plugin_output":
                            currentObject.setPluginOutput( childValue );
                            break;
                        case "plugin_type":
                            currentObject.setPluginType( childValue );
                            break;
                        case "plugin_version":
                            currentObject.setPluginVersion( childValue );
                            break;
                        case "solution":
                            currentObject.setSolution( childValue );
                            break;
                        case "risk_factor":
                            currentObject.setRiskFactor( RiskFactor.forValue( childValue ) );
                            break;
                        case "synopsis":
                            currentObject.setSynopsis( childValue );
                            break;
                        case "unsupported_by_vendor":
                            currentObject.setUnsupportedByVendor( Boolean.parseBoolean( childValue ) );
                            break;
                        case "vulnerability_state":
                            currentObject.setVulnerabilityState( VulnerabilityState.forValue( childValue ) );
                            break;
                        case "vuln_publication_date":
                            currentObject.setVulnPublicationDate( DateHelper.parseIso8601Date( childValue ) );
                            break;
                        case "xref":
                            currentObject.addXref( childValue );
                            break;
                        case "see_also":
                            currentObject.addSeeAlso( childValue );
                            break;
                    }
                } catch( Exception e ) {
                    logger.warn( String.format( "Exception when parsing ReportItem child node \'%s\' with value \'%s\'", childName, childValue ), e );
                }
            }
        }
    }
}