#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
    @copyright: 2011 Mika Seppänen
    @license: MIT <http://www.opensource.org/licenses/mit-license.php>
"""
import os
import re
import sys
import time
import json
import socket
import optparse

from zipfile import ZipFile
from opencollab.meta import Metas
from opencollab.wiki import CLIWiki, WikiFailure
from opencollab.util.config import parseOptions
from opencollab.util.file import uploadFile, md5obj

# Regex for finding javascript variable definitions
JS_VARIABLES = "^var[ \t]+([a-zA-Z0-9_-]+)[ \t]+=[ \t]+(.+?);$"

# Regexes for finding broken parts in input JSON
JS_HEXADECIMAL = "([^'0-9]0x[0-9abcdefABCDEF]+[^'])"
JS_HEXESCAPE = "\\\\x([0-9ABCDEFabcdef]{2,2})"
JS_EXTRACOMMA = "(\},[ \t\n]*\])"

# Severities from Skipfish
SEVERITY = {0: "Note",
            1: "Warning",
            2: "Low",
            3: "Medium",
            4: "High"
          }

# Issue descriptions from Skipfish
ISSUES = {
  10101: "SSL certificate issuer information",
  10201: "New HTTP cookie added",
  10202: "New 'Server' header value seen",
  10203: "New 'Via' header value seen",
  10204: "New 'X-*' header value seen",
  10205: "New 404 signature seen",
  10401: "Resource not directly accessible",
  10402: "HTTP authentication required",
  10403: "Server error triggered",
  10501: "All external links",
  10502: "External URL redirector",
  10503: "All e-mail addresses",
  10504: "Links to unknown protocols",
  10505: "Unknown form field (can't autocomplete)",
  10601: "HTML form (not classified otherwise)",
  10602: "Password entry form - consider brute-force",
  10603: "File upload form",
  10701: "User-supplied link rendered on a page",
  10801: "Incorrect or missing MIME type (low risk)",
  10802: "Generic MIME used (low risk)",
  10803: "Incorrect or missing charset (low risk)",
  10804: "Conflicting MIME / charset info (low risk)",
  10901: "Numerical filename - consider enumerating",
  10902: "OGNL-like parameter behavior",
  20101: "Resource fetch failed",
  20102: "Limits exceeded, fetch suppressed",
  20201: "Directory behavior checks failed (no brute force)",
  20202: "Parent behavior checks failed (no brute force)",
  20203: "IPS filtering enabled",
  20204: "IPS filtering disabled again",
  20205: "Response varies randomly, skipping checks",
  20301: "Node should be a directory, detection error?",
  30101: "HTTP credentials seen in URLs",
  30201: "SSL certificate expired or not yet valid",
  30202: "Self-signed SSL certificate",
  30203: "SSL certificate host name mismatch",
  30204: "No SSL certificate data found",
  30301: "Directory listing restrictions bypassed",
  30401: "Redirection to attacker-supplied URLs",
  30402: "Attacker-supplied URLs in embedded content (lower risk)",
  30501: "External content embedded on a page (lower risk)",
  30502: "Mixed content embedded on a page (lower risk)",
  30601: "HTML form with no apparent XSRF protection",
  30602: "JSON response with no apparent XSSI protection",
  30701: "Incorrect caching directives (lower risk)",
  40101: "XSS vector in document body",
  40102: "XSS vector via arbitrary URLs",
  40103: "HTTP response header splitting",
  40104: "Attacker-supplied URLs in embedded content (higher risk)",
  40201: "External content embedded on a page (higher risk)",
  40202: "Mixed content embedded on a page (higher risk)",
  40301: "Incorrect or missing MIME type (higher risk)",
  40302: "Generic MIME type (higher risk)",
  40304: "Incorrect or missing charset (higher risk)",
  40305: "Conflicting MIME / charset info (higher risk)",
  40401: "Interesting file",
  40402: "Interesting server message",
  40501: "Directory traversal / file inclusion possible",
  40601: "Incorrect caching directives (higher risk)",
  40701: "Password form submits from or to non-HTTPS page",
  50101: "Server-side XML injection vector",
  50102: "Shell injection vector",
  50103: "Query injection vector",
  50104: "Format string vector",
  50105: "Integer overflow vector",
  50201: "SQL query or similar syntax in parameters",
  50301: "PUT request accepted"
  }


def quote(input):
    return "'%s'" % input.groups()


def escape(input):
    return r"\u00%s" % input.groups()


def parseJS(file):
    input = open(file).read()
    vars = re.findall(JS_VARIABLES, input, re.DOTALL | re.MULTILINE)
    output = dict()

    for (key, value) in vars:
        # Hexadecimal numbers are not allowed in JSON, so trying to
        # convert them to strings.
        value = re.sub(JS_HEXADECIMAL, quote, value)

        # Hex escape is not supported in JSON, so trying to convert
        # them to correct format.
        value = re.sub(JS_HEXESCAPE, escape, value)

        # Random fixing to produce JSON that can be parsed using
        # python's json module.
        value = value.replace("'", '"')
        value = re.sub(JS_EXTRACOMMA, lambda x: "}]", value)

        output[key] = json.loads(value)

    return output


def handleDir(dir, issueFilter, issues, url):
    newDirs = list()
    parsed = parseJS(os.path.join(dir, "child_index.js"))
    if "child" in parsed:
        for child in parsed["child"]:
            newDirs.append((os.path.join(dir, child["dir"]), child.get("url", "")))

    parsed = parseJS(os.path.join(dir, "issue_index.js"))
    if "issue" in parsed:
        for issue in parsed["issue"]:
            if issue["severity"] >= issueFilter:
                issue["request"] = os.path.join(os.path.join(dir, issue["dir"]), "request.dat")
                issue["response"] = os.path.join(os.path.join(dir, issue["dir"]), "response.dat")
                # NOTE: this is only the base of the url
                issue["url"] = url
                issues.append(issue)

    for (dir, url) in newDirs:
        handleDir(dir, issueFilter, issues, url)


def main():
    parser = optparse.OptionParser()
    parser.add_option("-A", "--audit", dest="audit", default=None,
                      metavar="AUDIT", help=("AUDIT name to tag notes with."))
    parser.add_option("-f", "--filter", dest="filter", default="Medium",
                      metavar="SEVERITY", help=("Required SEVERITY for issue to get uploaded. (Possible values: Note, Warning, Low, Medium (default), High)"))
    parser.set_usage("%prog [options] SKIPFISH-DIRS")
    section = "skipfish-uploader"
    options = parseOptions(parser, section)
    x509 = options[section]["x509"]
    x509_ca_file = options[section]["x509_ca_file"]

    filter = options[section]["filter"]
    issueFilter = -1
    for no, txt in SEVERITY.iteritems():
        if filter == txt:
            issueFilter = no
    if issueFilter < 0:
        parser.error("Bad filter, please use excatly one of following values: Note, Warning, Low, Medium, High.")

    audit = options[section]["audit"]
    args = options[section]["args"]
    if len(args) < 1:
        parser.error("At least one input directory needs to be specified.")

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

    for dir in args:
        print "Processing directory %s." % dir
        metas = Metas()

        summary = parseJS(os.path.join(dir, "summary.js"))
        pagename = md5obj(repr(summary)).hexdigest()

        print "Creating page %s for this scan." % pagename
        start = time.mktime(time.strptime(summary.get("scan_date", ""), "%a %b %d %H:%M:%S %Y"))
        end = start + int(summary.get("scan_ms", 0))
        metas[pagename]["version"] = [summary.get("sf_version", "")]
        metas[pagename]["start"] = ["<<DateTime(%f)>>" % start]
        metas[pagename]["end"] = ["<<DateTime(%f)>>" % end]
        metas[pagename]["gwikicategory"] = ["CategoryScan"]
        metas[pagename]["type"] = ["Skipfish Run"]
        metas[pagename]["report"] = ["[[attachment:%s.zip]]" % pagename]

        parsed = parseJS(os.path.join(dir, "child_index.js"))
        url = ""
        if "child" in parsed:
            for child in parsed["child"]:
                url = child.get("url", "")
                metas[pagename]["URL"].add(url)

        zipName = "%s.zip" % pagename
        zip = ZipFile(zipName, "w")
        for root, dirs, files in os.walk(dir):
            for file in files:
                zip.write(os.path.join(root, file))

        zip.close()
        uploadFile(collab, pagename, zipName, zipName, True)
        os.remove(zipName)

        issues = list()
        handleDir(dir, issueFilter, issues, url)
        print "Found %d issues matching the filter. (Severity >= %s)" % (len(issues), SEVERITY[issueFilter])
        print "Creating notes for these issues."
        for issue in issues:
            pagename = md5(repr(issue["request"])).hexdigest()
            metas[pagename]["type"] = ["Skipfish Run"]
            metas[pagename]["Priority"] = [SEVERITY[issue["severity"]]]
            metas[pagename]["Status"] = ["Open"]
            metas[pagename]["Description"] = ["%s: %s" % (ISSUES[issue["type"]], issue["extra"])]
            metas[pagename]["Recommendation"] = ["Verify if this is true issue."]
            metas[pagename]["URL"] = [issue["url"]]
            metas[pagename]["gwikicategory"] = ["CategoryNote"]
            if os.path.exists(issue["request"]):
                metas[pagename]["Request"] = ["{{attachment:request.txt}}"]
                collab.putAttachment(pagename, "request.txt", open(issue["request"]).read(), True)
            if os.path.exists(issue["response"]):
                metas[pagename]["Response"] = ["{{attachment:response.txt}}"]
                collab.putAttachment(pagename, "response.txt", open(issue["response"]).read(), True)

            if audit:
                metas[pagename]["Audit"].add(audit)

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

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