#!/usr/bin/python3 -O

# freewvs 0.1 - the free web vulnerability scanner
#
# https://source.schokokeks.org/freewvs/
#
# Written 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 re
import argparse
import sys
from xml.sax.saxutils import escape


def versioncompare(safe_version, find_version):
    safe_version_tup = [int(x) for x in safe_version.split(".")]
    find_version_tup = [int(x) for x in find_version.split(".")]
    return find_version_tup < safe_version_tup


def vulnprint(appname, version, safeversion, vuln, vfilename, subdir,
              xml):
    appdir = '/'.join(os.path.abspath(vfilename).split('/')[:-1 - subdir])
    if not xml:
        print("%(appname)s %(version)s (%(safeversion)s) %(vuln)s "
              "%(appdir)s" % vars())
    else:
        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>')


# Command-line options
parser = argparse.ArgumentParser()
parser.add_argument("dirs", nargs="*",
                    help="Directories to scan")
parser.add_argument("-a", "--all", action="store_true",
                    help="Show all webapps found, not just vulnerable")
parser.add_argument("-x", "--xml", action="store_true",
                    help="Output results as XML")
parser.add_argument("-3", "--thirdparty", action="store_true",
                    help="Scan for third-party components like jquery")
opts = 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 = []
scanfiles = set()
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'))
    scanfiles.add(item['file'])

    # match magic
    item['variable'] = re.compile(re.escape(config.get(sect, 'variable'))
                                  + 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.xml:
    print('<?xml version="1.0" ?>')
    print('<freewvs>')

# start the search

for fdir in opts.dirs:
    for root, NULL, files in os.walk(fdir):
        for filename in scanfiles.intersection(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']
                       and item['extra_match'] not in filestr)
                       or (item['extra_nomatch']
                       and item['extra_nomatch'] in filestr)
                       or (item['path_match']
                       and not root.endswith(item['path_match']))):
                        continue

                    findversion = item['variable'].search(filestr)
                    if not findversion:
                        continue
                    findversion = findversion.group(1)

                    # 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'], findversion)
                       or findversion in item['old_safe']):
                        if opts.all:
                            vulnprint(item['name'], findversion, "ok", "",
                                      mfile, item['subdir'], opts.xml)
                        continue

                    safev = item['safe']
                    for ver in item['old_safe']:
                        if versioncompare(ver, findversion):
                            safev = ver

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

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