133 lines
5.2 KiB
Python
133 lines
5.2 KiB
Python
|
import hashlib
|
||
|
import os
|
||
|
import sys
|
||
|
|
||
|
from functools import wraps
|
||
|
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version
|
||
|
from pathlib import Path
|
||
|
|
||
|
import pipenv.vendor.click as click
|
||
|
|
||
|
# Jinja2 will only be installed if the optional deps are installed.
|
||
|
# It's fine if our functions fail, but don't let this top level
|
||
|
# import error out.
|
||
|
try:
|
||
|
import jinja2
|
||
|
except ImportError:
|
||
|
jinja2 = None
|
||
|
|
||
|
import pipenv.patched.pip._vendor.requests as requests
|
||
|
|
||
|
|
||
|
def highest_base_score(vulns):
|
||
|
highest_base_score = 0
|
||
|
for vuln in vulns:
|
||
|
if vuln['severity'] is not None:
|
||
|
highest_base_score = max(highest_base_score, (vuln['severity'].get('cvssv3', {}) or {}).get('base_score', 10))
|
||
|
|
||
|
return highest_base_score
|
||
|
|
||
|
def generate_branch_name(pkg, remediation):
|
||
|
return pkg + "/" + remediation['recommended_version']
|
||
|
|
||
|
def generate_issue_title(pkg, remediation):
|
||
|
return f"Security Vulnerability in {pkg}"
|
||
|
|
||
|
def generate_title(pkg, remediation, vulns):
|
||
|
suffix = "y" if len(vulns) == 1 else "ies"
|
||
|
return f"Update {pkg} from {remediation['current_version']} to {remediation['recommended_version']} to fix {len(vulns)} vulnerabilit{suffix}"
|
||
|
|
||
|
def generate_body(pkg, remediation, vulns, *, api_key):
|
||
|
changelog = fetch_changelog(pkg, remediation['current_version'], remediation['recommended_version'], api_key=api_key)
|
||
|
|
||
|
p = Path(__file__).parent / 'templates'
|
||
|
env = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(p)))
|
||
|
template = env.get_template('pr.jinja2')
|
||
|
|
||
|
overall_impact = cvss3_score_to_label(highest_base_score(vulns))
|
||
|
result = template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": False })
|
||
|
|
||
|
# GitHub has a PR body length limit of 65536. If we're going over that, skip the changelog and just use a link.
|
||
|
if len(result) > 65500:
|
||
|
return template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": True })
|
||
|
|
||
|
return result
|
||
|
|
||
|
def generate_issue_body(pkg, remediation, vulns, *, api_key):
|
||
|
changelog = fetch_changelog(pkg, remediation['current_version'], remediation['recommended_version'], api_key=api_key)
|
||
|
|
||
|
p = Path(__file__).parent / 'templates'
|
||
|
env = jinja2.Environment(loader=jinja2.FileSystemLoader(Path(p)))
|
||
|
template = env.get_template('issue.jinja2')
|
||
|
|
||
|
overall_impact = cvss3_score_to_label(highest_base_score(vulns))
|
||
|
result = template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": False })
|
||
|
|
||
|
# GitHub has a PR body length limit of 65536. If we're going over that, skip the changelog and just use a link.
|
||
|
if len(result) > 65500:
|
||
|
return template.render({"pkg": pkg, "remediation": remediation, "vulns": vulns, "changelog": changelog, "overall_impact": overall_impact, "summary_changelog": True })
|
||
|
|
||
|
def generate_commit_message(pkg, remediation):
|
||
|
return f"Update {pkg} from {remediation['current_version']} to {remediation['recommended_version']}"
|
||
|
|
||
|
def git_sha1(raw_contents):
|
||
|
return hashlib.sha1(b"blob " + str(len(raw_contents)).encode('ascii') + b"\0" + raw_contents).hexdigest()
|
||
|
|
||
|
def fetch_changelog(package, from_version, to_version, *, api_key):
|
||
|
from_version = parse_version(from_version)
|
||
|
to_version = parse_version(to_version)
|
||
|
changelog = {}
|
||
|
|
||
|
r = requests.get(
|
||
|
"https://pyup.io/api/v1/changelogs/{}/".format(package),
|
||
|
headers={"X-Api-Key": api_key}
|
||
|
)
|
||
|
|
||
|
if r.status_code == 200:
|
||
|
data = r.json()
|
||
|
if data:
|
||
|
# sort the changelog by release
|
||
|
sorted_log = sorted(data.items(), key=lambda v: parse_version(v[0]), reverse=True)
|
||
|
|
||
|
# go over each release and add it to the log if it's within the "upgrade
|
||
|
# range" e.g. update from 1.2 to 1.3 includes a changelog for 1.2.1 but
|
||
|
# not for 0.4.
|
||
|
for version, log in sorted_log:
|
||
|
parsed_version = parse_version(version)
|
||
|
if parsed_version > from_version and parsed_version <= to_version:
|
||
|
changelog[version] = log
|
||
|
|
||
|
return changelog
|
||
|
|
||
|
def cvss3_score_to_label(score):
|
||
|
if score >= 0.1 and score <= 3.9:
|
||
|
return 'low'
|
||
|
elif score >= 4.0 and score <= 6.9:
|
||
|
return 'medium'
|
||
|
elif score >= 7.0 and score <= 8.9:
|
||
|
return 'high'
|
||
|
elif score >= 9.0:
|
||
|
return 'critical'
|
||
|
|
||
|
return None
|
||
|
|
||
|
def require_files_report(func):
|
||
|
@wraps(func)
|
||
|
def inner(obj, *args, **kwargs):
|
||
|
if obj.report['report_meta']['scan_target'] != "files":
|
||
|
click.secho("This report was generated against an environment, but this alerter requires a file.", fg='red')
|
||
|
sys.exit(1)
|
||
|
|
||
|
files = obj.report['report_meta']['scanned']
|
||
|
obj.requirements_files = {}
|
||
|
for f in files:
|
||
|
if not os.path.exists(f):
|
||
|
cwd = os.getcwd()
|
||
|
click.secho("A requirements file scanned in the report, {}, does not exist (looking in {}).".format(f, cwd), fg='red')
|
||
|
sys.exit(1)
|
||
|
|
||
|
obj.requirements_files[f] = open(f, "rb").read()
|
||
|
|
||
|
return func(obj, *args, **kwargs)
|
||
|
return inner
|