#!/usr/bin/python3 -O

# freewvs 0.1 - the free web vulnerability scanner
#
# https://source.schokokeks.org/freewvs/
#
# Written 2007-2012 by schokokeks.org Hosting, https://schokokeks.org
#
# Contributions by
# Hanno Boeck, https://hboeck.de/
# Fabian Fingerle, https://fabian-fingerle.de/
# Bernd Wurst, https://bwurst.org/
#
# To the extent possible under law, the author(s) have dedicated all copyright
# and related and neighboring rights to this software to the public domain
# worldwide. This software is distributed without any warranty.
#
# You should have received a copy of the CC0 Public Domain Dedication along
# with this software. If not, see
# https://creativecommons.org/publicdomain/zero/1.0/
# Nevertheless, in case you use a significant part of this code, we ask (but
# not require, see the license) that you keep the authors' names in place and
# return your changes to the public. We would be especially happy if you tell
# us what you're going to do with this code.

import configparser

import os
import glob
import pprint
import re
import optparse
import sys
import gettext
from xml.sax.saxutils import escape

gettext.textdomain('freewvs')
_ = gettext.gettext


def versioncompare(safe_version, find_version):
    if safe_version == [""]:
        return True
    for i in range(min(len(find_version), len(safe_version))):
        if int(find_version[i]) < int(safe_version[i]):
            return True
        if int(find_version[i]) > int(safe_version[i]):
            return False
    return len(find_version) < len(safe_version)


def vulnprint(appname, version, safeversion, vuln, vfilename, subdir,
              style=None):
    appdir = '/'.join(os.path.abspath(vfilename).split('/')[:-1 - subdir])
    if not style:
        print("%(appname)s %(version)s (%(safeversion)s) %(vuln)s "
              "%(appdir)s" % vars())
    elif style == 'fancy':
        print(_("Directory: %(appdir)s") % vars())
        if safeversion != "ok":
            if safeversion != "":
                print(_("Vulnerable %(appname)s %(version)s found, please "
                        "update to %(safeversion)s or above.") % vars())
            else:
                print(_("Vulnerable %(appname)s %(version)s found, no fixed "
                        "version available.") % vars())
            if vuln[:3] == "CVE":
                print(_("https://cve.mitre.org/cgi-bin/cvename.cgi?name="
                        "%(vuln)s") % vars())
            else:
                print(vuln)
        else:
            print(_("%(appname)s %(version)s found.") % vars())
        print("")
    elif style == 'xml':
        state = 'vulnerable'
        if safeversion == 'ok':
            state = 'ok'
        print('  <app state="%s">' % state)
        print('    <appname>%s</appname>' % escape(appname))
        print('    <version>%s</version>' % escape(version))
        print('    <directory>%s</directory>' % escape(appdir))
        if state == 'vulnerable':
            print('    <safeversion>%s</safeversion>' % escape(safeversion))
            print('    <vulninfo>%s</vulninfo>' % escape(vuln))
        print('  </app>')


pp = pprint.PrettyPrinter(indent=4)

# Command-line options
parser = optparse.OptionParser(usage="usage: %prog [options] <path>"
                               "[<path2> ...]")
parser.add_option("-a", "--all", action="store_true", dest="ALL",
                  help="Show all webapps found, not just vulnerable")
parser.add_option("-d", "--debug", action="store_true", dest="DEBUG",
                  help="Show lots of debugging output, mainly useful"
                  "for development")
parser.add_option("-f", "--fancy", action="store_const", dest="OUTPUT",
                  const="fancy", help="Show more fancy output")
parser.add_option("-x", "--xml", action="store_const", dest="OUTPUT",
                  const="xml", help="Output results as XML")
parser.add_option("-3", "--thirdparty", action="store_true", dest="THIRDPARTY",
                  help="Scan for third-party components like jquery")
opts, args = parser.parse_args()

# Parse vulnerability database
config = configparser.ConfigParser()
try:
    config.read(glob.glob('/usr/share/freewvs/*.freewvs'))
    config.read(glob.glob('/usr/local/share/freewvs/*.freewvs'))
    config.read(glob.glob(os.path.dirname(sys.argv[0])
                          + '/freewvsdb/*.freewvs'))
except configparser.MissingSectionHeaderError as err:
    print("Error parsing config files: %s" % err)

vdb = []
for sect in config.sections():
    item = {}

    if (config.getboolean(sect, 'thirdparty', fallback=False)
       and not opts.THIRDPARTY):
        continue

    # base options
    item['name'] = sect
    item['safe'] = config.get(sect, 'safe')
    item['file'] = config.get(sect, 'file')
    item['vuln'] = config.get(sect, 'vuln')
    item['subdir'] = int(config.get(sect, 'subdir'))

    # match magic
    item['variable'] = []
    for var in config.get(sect, 'variable').split(","):
        item['variable'].append(re.compile(re.escape(var)
                                + r"[^0-9\n\r]*[.]*([0-9.]*[0-9])[^0-9.]"))

    # optional options
    if config.has_option(sect, 'extra_match'):
        item['extra_match'] = config.get(sect, 'extra_match')
    else:
        item['extra_match'] = False
    if config.has_option(sect, 'extra_nomatch'):
        item['extra_nomatch'] = config.get(sect, 'extra_nomatch')
    else:
        item['extra_nomatch'] = False
    if config.has_option(sect, 'path_match'):
        item['path_match'] = config.get(sect, 'path_match')
    else:
        item['path_match'] = False
    if config.has_option(sect, 'add_minor'):
        item['add_minor'] = config.get(sect, 'add_minor')
    else:
        item['add_minor'] = False
    if config.has_option(sect, 'old_safe'):
        item['old_safe'] = config.get(sect, 'old_safe').split(",")
    else:
        item['old_safe'] = []

    vdb.append(item)
if opts.DEBUG:
    pp.pprint(vdb)

if opts.OUTPUT == 'xml':
    print('<?xml version="1.0" ?>')
    print('<freewvs>')

# start the search

for fdir in args:
    for root, NULL, files in os.walk(fdir):
        for filename in files:
            for item in vdb:
                if filename == item['file']:
                    mfile = os.path.join(root, filename)
                    try:
                        file = open(mfile, errors='replace')
                    except Exception:
                        continue
                    filestr = file.read()
                    file.close()

                    if item['extra_match']:
                        ematch = filestr.find(item['extra_match']) != -1
                    elif item['extra_nomatch']:
                        ematch = not filestr.find(item['extra_nomatch']) != -1
                    else:
                        ematch = True

                    if (item['path_match']
                       and not root.endswith(item['path_match'])):
                        continue

                    findversion = []
                    for var in item['variable']:
                        var = var.search(filestr)
                        if not var:
                            findversion = False
                            break
                        else:
                            findversion.append(var.group(1))

                    if findversion and ematch:
                        findversion = '.'.join(findversion)

                        # Very ugly phpbb workaround
                        if item['add_minor']:
                            findversion = findversion.split('.')
                            findversion[-1] = str(int(findversion[-1])
                                                  + int(item['add_minor']))
                            findversion = '.'.join(findversion)

                        if not (versioncompare(item['safe'].split('.'),
                                findversion.split('.'))) or \
                                item['old_safe'].count(findversion) > 0:
                            if opts.ALL:
                                if opts.DEBUG:
                                    print("File " + mfile)
                                vulnprint(item['name'], findversion,
                                          "ok", "", mfile, item['subdir'],
                                          opts.OUTPUT)
                        else:
                            if opts.DEBUG:
                                print("File " + mfile)
                            safev = "9999"
                            for ver in item['old_safe']:
                                if(versioncompare(ver.split('.'),
                                   findversion.split('.'))
                                   and not versioncompare(ver.split('.'),
                                   safev.split('.'))):
                                    safev = ver
                            if safev == "9999":
                                safev = item['safe']

                            vulnprint(item['name'], findversion,
                                      safev, item['vuln'],
                                      mfile, item['subdir'], opts.OUTPUT)

                    else:
                        if opts.DEBUG:
                            print("regexp failed for "
                                  + item['name'] + " on " + mfile)

if opts.OUTPUT == 'xml':
    print('</freewvs>')