#!/usr/bin/python3 -O

# freewvs - a free web vulnerability scanner
#
# https://freewvs.schokokeks.org/
#
# 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/


import os
import glob
import re
import argparse
import sys
import json
import pathlib
from xml.sax.saxutils import escape  # noqa: DUO107


def versioncompare(safe_version, find_version):
    if safe_version == "":
        return True
    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 checkoldsafe(old_safe, find_version):
    find_version_tup = [int(x) for x in find_version.split(".")]
    for oldver in old_safe.split(","):
        oldver_tup = [int(x) for x in oldver.split(".")]

        if find_version_tup == oldver_tup:
            return True
        # handle special case where minor version is larger
        if (
            len(find_version_tup) >= 2
            and find_version_tup[:-1] == oldver_tup[:-1]
            and find_version_tup[-1] > oldver_tup[-1]
        ):
            return True
    return False


def vulnprint(appname, version, safeversion, vuln, vfilename, subdir, xml):
    appdir = "/".join(os.path.abspath(vfilename).split("/")[: -1 - subdir])
    if not xml:
        print(f"{appname} {version} ({safeversion}) {vuln} {appdir}")
    else:
        state = "vulnerable"
        if safeversion == "ok":
            state = "ok"
        print(f'  <app state="{state}">')
        print(f"    <appname>{escape(appname)}</appname>")
        print(f"    <version>{escape(version)}</version>")
        print(f"    <directory>{escape(appdir)}</directory>")
        if state == "vulnerable":
            print(f"    <safeversion>{escape(safeversion)}</safeversion>")
            print(f"    <vulninfo>{escape(vuln)}</vulninfo>")
        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()

jdir = False
for p in [
    os.path.dirname(sys.argv[0]) + "/freewvsdb",
    "/var/lib/freewvs",
    str(pathlib.Path.home()) + "/.cache/freewvs/",
]:
    if os.path.isdir(p):
        jdir = p
        break
if not jdir:
    print("Can't find freewvs json db")
    sys.exit(1)

jconfig = []
for cfile in glob.glob(jdir + "/*.json"):
    with open(cfile, encoding="ascii") as json_file:
        data = json.load(json_file)
        jconfig += data

scanfiles = set()
for app in jconfig:
    for det in app["detection"]:
        scanfiles.add(det["file"])


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

# start the search

for fdir in opts.dirs:
    for root, dirs, files in os.walk(fdir):
        # this protects us against nested directories causing
        # an exception
        if root.count(os.sep) > 500:
            del dirs[:]
        for filename in scanfiles.intersection(files):
            for item in jconfig:
                if not opts.thirdparty and "thirdparty" in item:
                    continue
                for det in item["detection"]:
                    if filename == det["file"]:
                        mfile = os.path.join(root, filename)
                        try:
                            file = open(mfile, encoding="ascii", errors="replace")
                        except OSError:
                            continue
                        filestr = file.read(200000)
                        file.close()

                        if (
                            "extra_match" in det and det["extra_match"] not in filestr
                        ) or (
                            "extra_nomatch" in det and det["extra_nomatch"] in filestr
                        ):
                            continue

                        if "path_match" in det and (
                            not root.endswith(det["path_match"])
                        ):
                            continue

                        findversion = re.search(
                            re.escape(det["variable"]) + r"[^0-9\n\r]*[.]*"
                            "([0-9.]*[0-9])[^0-9.]",
                            filestr,
                        )
                        if not findversion:
                            continue
                        findversion = findversion.group(1)

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

                        if not versioncompare(item["safe"], findversion) or (
                            "old_safe" in item
                            and checkoldsafe(item["old_safe"], findversion)
                        ):
                            if opts.all:
                                vulnprint(
                                    item["name"],
                                    findversion,
                                    "ok",
                                    "",
                                    mfile,
                                    det["subdir"],
                                    opts.xml,
                                )
                            continue

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

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

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