#!/usr/bin/env python
# $FreeBSD: tags/RELEASE_11_3_0/security/vuxml/files/extra-validation.py 491045 2019-01-23 16:03:33Z zi $

import datetime
import xml.etree.ElementTree as ET
import sys
import re

if len(sys.argv) != 2:
    print("Usage: %s vuln.xml" % (sys.argv[0]))
    sys.exit(1)

re_date = re.compile(r'^(19|20)[0-9]{2}-[0-9]{2}-[0-9]{2}$')
re_invalid_package_name = re.compile('[@!#$%^&*()<>?/\|}{~:]')

# warn if description has more than X characters
DESCRIPTION_LENGTH = 5000

tree = ET.parse(sys.argv[1])
root = tree.getroot()

namespace = "{http://www.vuxml.org/apps/vuxml-1}"

ret = 0


def dateof(string):
    return datetime.datetime.strptime(string, "%Y-%m-%d")

all_vids = set()


for vuln in root:
    vid = vuln.get("vid")

    cancelled = False if vuln.find(namespace+"cancelled") is None else True
    if cancelled:
        continue

    # Validate Vids
    if vid in all_vids:
        print("Error: duplicate vid : {0}".format(vid))
    all_vids.add(vid)

    # Validate References
    references = vuln.find(namespace+"references")
    if references is None:
        print("Error: references is None : {0}".format(vid))
        ret = 1
    else:
        prev = references[0]
        for reference in references:
            if reference.tag < prev.tag:
                #print("Warn: tags out of order ({1} and {2}): {0}".format(vid, prev.tag[len(namespace):], reference.tag[len(namespace):]))
                pass
            prev = reference

    # Validate Dates
    dates = vuln.find(namespace+"dates")
    if dates is None:
        print("Error: no date : {0}".format(vid))
        ret = 1
    else:
        discovery = dates.find(namespace+"discovery")
        entry = dates.find(namespace+"entry")
        modified = dates.find(namespace+"modified")
        if discovery is None:
            print("Error: discovery is None : {0}".format(vid))
            ret = 1
        elif entry is None:
            print("Error: entry is None : {0}".format(vid))
            ret = 1
        else:
            if modified is None:
                modified = entry
            if not (dateof(discovery.text) <= dateof(entry.text) <= dateof(modified.text)):
                print("Error: dates are insane : {0}".format(vid))
                ret = 1

        # Make sure the dates are in YYYY-MM-DD format
        datelist = [discovery.text, entry.text] + ([modified.text] if modified is not None else [])
        for d in datelist:
            if not re_date.match(d):
                print("Warning: dates must be in YYYY-MM-DD format: {0}".format(d))

        # Check description lengths
        description = vuln.find(namespace + "description")
        description_len = len(ET.tostring(description))
        if description_len > DESCRIPTION_LENGTH:
            print("Warning: description too long ({0} chars, {1} is warning threshold): {2})" \
                  .format(description_len, DESCRIPTION_LENGTH, vid))

        # Walk and validate package names
        affects = vuln.find(namespace + "affects")
        packages = affects.findall(namespace + "package")
        for package in packages:
            names = package.findall(namespace + "name")

            for name in names:
                if (re_invalid_package_name.search(name.text) is not None):
                    print("Error: invalid package name: " + name.text + " for VID " + format(vid))
                    ret = 1

sys.exit(ret)
