#!/usr/bin/env python3
# -*- coding: utf-8 -*-
#
#  Copyright 2015 Igalia S.L.
#   -- Author: clopez@igalia.com
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 2 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.
#

# This is the data structure used for storing the CVE info:
# {'CVE-2014-1341': {'Author': 'Author name',
#  'Description': 'CVE Description',
#  'Versions': ['2.4.3', '2.6.4']}}

# Python3 required!

import gzip
import csv
import textwrap
import sys, os, re
import time
from urllib.request import urlretrieve
from collections import OrderedDict

postsdir = "_posts"

headertemplate_mail = """\
Subject: %(title)s %(WSA)s
To: webkit-gtk@lists.webkit.org, webkit-wpe@lists.webkit.org
Cc: security@webkit.org, oss-security@lists.openwall.com

------------------------------------------------------------------------
%(title)s%(titlespaces)s%(WSA)s
------------------------------------------------------------------------

Date reported           : %(reportdate)s
Advisory ID             : %(WSA)s
WebKitGTK Advisory URL  : https://webkitgtk.org/security/%(WSA)s.html
WPE WebKit Advisory URL : https://wpewebkit.org/security/%(WSA)s.html
CVE identifiers         : %(cvelist)s

%(affectedtext)s

"""

headertemplate_md = """\
---
layout: post
title: %(title)s%(titlespaces)s%(WSA)s
permalink: /security/%(WSA)s.html
tags: WSA
---

* Date Reported: **%(reportdate)s**\n
* Advisory ID: **%(WSA)s**\n
* CVE identifiers: %(cvelist)s\n

%(affectedtext)s

"""

cvetemplate_mail  = """\
%(CVE)s
    %(affected)s
    %(credits)s
    %(description)s

"""

cvetemplate_md  = """\
* %(CVE)s
  * %(affected)s
  * %(credits)s
  * %(description)s

"""


footertemplate_mail = """\

%(fixedvers)s

%(moreinfo)s

%(signature)s
"""

footertemplate_md = """\

%(fixedvers)s

%(moreinfo)s
"""

def _fill_cve_descriptions(cvedb, cveinfo):
    cvefile = gzip.open(cvedb,'rt',errors='replace')
    cvecsv = csv.reader(cvefile)
    for entry in cvecsv:
        if entry[0] in cveinfo:
            if 'Description' in cveinfo[entry[0]]:
                print ("\n%s %s MANUALDESCRIPTION notification %s" %("-"*17,entry[0],"-"*17))
                print ("Ignoring MITRE description: %s" %entry[2])
                print ("Using MANUALDESCRIPTION: %s" %cveinfo[entry[0]]['Description'])
                print ("%s\n" %("-"*80))
                continue
            cveinfo[entry[0]]['Description'] =  entry[2]
    return cveinfo

def _parse_cve_report(textfile):
    cve_report = open(textfile, 'r')
    cve_info = OrderedDict()
    for line in cve_report:
        try:
            if ":" not in line:
                continue
            if line.startswith("MANUALDESCRIPTION"):
                _, cvenumbers, cvedescription = line.split(':',2)
                for cvenumber in cvenumbers.split(','):
                    cvenumber = cvenumber.strip()
                    if cvenumber not in cve_info:
                        raise ValueError("MANUALDESCRIPTION entry for CVE %s don't has a matching entry above\n", cvenumber)
                    cve_info[cvenumber]['Description'] =  cvedescription.strip()
                continue
            cvenumber,cvedata = line.split(':',1)
            # FIXME: Add some check that is a valid CVE
            cvenumber = cvenumber.strip()
            cveauthor,cveversions = cvedata.rsplit('(',1)
            cveauthor = cveauthor.strip()
            cveversions = cveversions.split(')', 1)[0]
            if cvenumber in cve_info:
                raise ValueError("Duplicated CVE!")
            cve_info[cvenumber] = {}
            cve_info[cvenumber]['Author'] = cveauthor
            cve_info[cvenumber]['Versions'] =  cveversions
        except:
            raise ValueError("Error parsing line %s" %line)
            raise
    return cve_info

def _beautify_line_for_print(line,ident, width=72, dot=True):
        if dot and not line.endswith('.'):
            line += '.'
        line = textwrap.wrap(line,width=(width-len(ident)))
        line = "\n".join(line)
        line = textwrap.indent(line,ident)
        return line

def _get_list_cves(cve_info):
    cves = []
    for cvenumber in cve_info:
        cves.append(cvenumber)
    return cves

def _cve_mitre_links(cvetext, anchor=False):
    CVE_PATTERN = "(CVE-[0-9][0-9][0-9][0-9]-[0-9]+)"
    if anchor:
        return re.sub(CVE_PATTERN,"[\\1](#\\1)",cvetext)
    else:
        return re.sub(CVE_PATTERN,"https://cve.mitre.org/cgi-bin/cvename.cgi?name=\\1",cvetext)


def _getnext_WSA_number(postsdir):
    posts = os.listdir(postsdir)
    advisories = [ 0 ]
    year = "%s" % time.strftime("%Y")
    pattern = "%s-[0-9]{2}-[0-9]{2}-security-advisory-%s-" %(year,year)
    for post in posts:
        match = re.match(pattern,post)
        if match:
            number = int(post[len(match.group()):len(post)-3])
            advisories.append(number)

    nextadvisory = sorted(advisories)[-1] + 1
    return nextadvisory

def _getWSAid(postsdir):
    year = "%s" % time.strftime("%Y")
    return "WSA-%s-%04d" %(year,_getnext_WSA_number(postsdir))

def _generate_name_security_post(postsdir):
    year = "%s" % time.strftime("%Y")
    day = "%s" % time.strftime("%d")
    month = "%s" % time.strftime("%m")
    mdname = "%s-%s-%s-security-advisory-%s-%04d.md" %(year,month,day,year,_getnext_WSA_number(postsdir))
    return mdname

def generate_advisory(cve_info,mail=True):

    advisorytext = ""
    WSA=_getWSAid(postsdir)
    reportdate=time.strftime("%B %d, %Y")
    title = "WebKitGTK and WPE WebKit Security Advisory"
    titlespaces=" "*(72-len(title)-len(WSA) if mail else 1)
    ident=" "*4

    headertemplate = headertemplate_mail if mail else headertemplate_md
    cvetemplate = cvetemplate_mail if mail else cvetemplate_md
    footertemplate = footertemplate_mail if mail else footertemplate_md

    cves = _get_list_cves(cve_info)
    cves = ", ".join(cves)
    if mail:
        cves = _beautify_line_for_print(cves, " "*26)
    else:
        cves = _cve_mitre_links(cves,anchor=True)
        cves = _beautify_line_for_print(cves, " "*2, width=90)

    affectedtext = ("Several vulnerabilities were discovered in WebKitGTK and WPE WebKit.")
    affectedtext = _beautify_line_for_print(affectedtext, "")

    advisorytext += (headertemplate %{ 'reportdate':reportdate,
                             'WSA':WSA,
                             'cvelist':cves.strip(),
                             'title':title,
                             'titlespaces':titlespaces,
                             'affectedtext':affectedtext
                            })
    # Generate text for CVEs
    for cve in cve_info:
        cvetext=cve
        affected = "Versions affected: " + cve_info[cve]['Versions']
        affected = _beautify_line_for_print(affected, ident)
        credits = "Credit to " + cve_info[cve]['Author']
        credits = _beautify_line_for_print(credits, ident)
        description = cve_info[cve]['Description']
        description = _beautify_line_for_print(description, ident)
        if not mail:
            cvetext= '<a name="' + cve + '" href="' + _cve_mitre_links(cve) + '">' + cve +'</a>'
        advisorytext += (cvetemplate %{ 'CVE':cvetext.strip(),
                              'affected':affected.strip(),
                              'credits':credits.strip(),
                              'description':description.strip()
                              })
    # Generate text for Footer
    fixedvers =  "We recommend updating to the latest stable versions of WebKitGTK and WPE WebKit. "
    fixedvers += "It is the best way to ensure that you are running safe versions of WebKit. "
    fixedvers += "Please check our websites for information about the latest stable releases."
    fixedvers = _beautify_line_for_print(fixedvers,"", dot=False)
    moreinfo="Further information about WebKitGTK and WPE WebKit security advisories can be found at: "
    if mail:
        moreinfo = _beautify_line_for_print(moreinfo + "https://webkitgtk.org/security.html or https://wpewebkit.org/security/", "")
        signature = "The WebKitGTK and WPE WebKit team," + "\n" + reportdate
    else:
        moreinfo = moreinfo + "\n" + "[https://webkitgtk.org/security.html](https://webkitgtk.org/security.html) or [https://wpewebkit.org/security/](https://wpewebkit.org/security/)."
        signature = ""

    advisorytext += ( footertemplate %{ 'fixedvers':fixedvers,
                             'moreinfo':moreinfo,
                             'signature':signature
                              })
    return advisorytext

def main():

    import argparse
    cvedatadir = "cvedata"
    parser = argparse.ArgumentParser(
        prog = 'generate-security-advisory',
        usage = '%(prog)s [options ...] path-to-cves-report-file\n\n' +
        '\tExample: %(prog)s '+ '%s\n\n.' %(os.path.join(cvedatadir,"report-something.txt")))

    parser.add_argument('--update', action='store_true', dest='update', default=False,
                        help='Force an update of the CVE database from mitre.')

    parser.add_argument('cvereportfile', type=str, help='Path to the cve-report text file.')
    args = parser.parse_args()

    if not os.path.isfile(args.cvereportfile):
        raise ValueError("Can't find file %s" %args.cvereportfile)

    cvedb = os.path.join (cvedatadir, "allitems.csv.gz" )
    if not os.path.isdir (cvedatadir):
        os.mkdir(cvedatadir)

    if not os.path.isfile(cvedb):
        args.update = True

    if args.update:
        cvedburl = "https://cve.mitre.org/data/downloads/allitems.csv.gz"
        print ("Downloading %s ..." %cvedburl)
        urlretrieve(cvedburl, cvedb)
    else:
        print ("Using already downloaded CVE DB file %s. Run me with --update to get a fresh copy" %cvedb)

    cveinfo = _parse_cve_report(args.cvereportfile)
    cveinfo = _fill_cve_descriptions(cvedb, cveinfo)

    # First generate mail, otherwise we would increment WSAID twice.
    mailadvisory = generate_advisory(cveinfo)

    mailfilename = os.path.join(cvedatadir, "mail.txt")
    mdadvisoryfile = open(mailfilename, "w")
    mdadvisoryfile.write(mailadvisory)
    mdadvisoryfile.close()

    print ("Mail to be sent wrote at %s" %mailfilename )

    # Generate markdown for the web.
    mdadvisory = generate_advisory(cveinfo, mail=False)

    mdadvisoryfilename = os.path.join(postsdir, _generate_name_security_post(postsdir))
    mdadvisoryfile = open(mdadvisoryfilename, "w")
    mdadvisoryfile.write(mdadvisory)
    mdadvisoryfile.close()

    print ("Post for the website wrote at %s" %mdadvisoryfilename)

    return 0

if __name__ == '__main__':
    main()

