629 lines
22 KiB
Python
629 lines
22 KiB
Python
|
# -*- coding: utf-8 -*-
|
||
|
import errno
|
||
|
import itertools
|
||
|
import json
|
||
|
import logging
|
||
|
import os
|
||
|
import sys
|
||
|
import time
|
||
|
from datetime import datetime
|
||
|
from typing import List
|
||
|
|
||
|
import pipenv.patched.pip._vendor.requests as requests
|
||
|
from pipenv.patched.pip._vendor.packaging.specifiers import SpecifierSet
|
||
|
from pipenv.patched.pip._vendor.packaging.utils import canonicalize_name
|
||
|
from pipenv.patched.pip._vendor.packaging.version import parse as parse_version, parse
|
||
|
|
||
|
from .constants import (API_MIRRORS, CACHE_FILE, OPEN_MIRRORS, REQUEST_TIMEOUT, API_BASE_URL)
|
||
|
from .errors import (DatabaseFetchError, DatabaseFileNotFoundError,
|
||
|
InvalidKeyError, TooManyRequestsError, NetworkConnectionError,
|
||
|
RequestTimeoutError, ServerError, MalformedDatabase)
|
||
|
from .models import Vulnerability, CVE, Severity
|
||
|
from .util import RequirementFile, read_requirements, Package, build_telemetry_data, sync_safety_context, SafetyContext, \
|
||
|
validate_expiration_date, is_a_remote_mirror
|
||
|
|
||
|
|
||
|
if sys.version_info < (3, 10):
|
||
|
from pipenv.vendor import importlib_metadata
|
||
|
else:
|
||
|
import importlib.metadata as importlib_metadata
|
||
|
|
||
|
session = requests.session()
|
||
|
|
||
|
LOG = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
def get_from_cache(db_name, cache_valid_seconds=0):
|
||
|
LOG.debug('Trying to get from cache...')
|
||
|
if os.path.exists(CACHE_FILE):
|
||
|
LOG.info('Cache file path: %s', CACHE_FILE)
|
||
|
with open(CACHE_FILE) as f:
|
||
|
try:
|
||
|
data = json.loads(f.read())
|
||
|
LOG.debug('Trying to get the %s from the cache file', db_name)
|
||
|
LOG.debug('Databases in CACHE file: %s', ', '.join(data))
|
||
|
if db_name in data:
|
||
|
LOG.debug('db_name %s', db_name)
|
||
|
|
||
|
if "cached_at" in data[db_name]:
|
||
|
if data[db_name]["cached_at"] + cache_valid_seconds > time.time():
|
||
|
LOG.debug('Getting the database from cache at %s, cache setting: %s',
|
||
|
data[db_name]["cached_at"], cache_valid_seconds)
|
||
|
return data[db_name]["db"]
|
||
|
|
||
|
LOG.debug('Cached file is too old, it was cached at %s', data[db_name]["cached_at"])
|
||
|
else:
|
||
|
LOG.debug('There is not the cached_at key in %s database', data[db_name])
|
||
|
|
||
|
except json.JSONDecodeError:
|
||
|
LOG.debug('JSONDecodeError trying to get the cached database.')
|
||
|
else:
|
||
|
LOG.debug("Cache file doesn't exist...")
|
||
|
return False
|
||
|
|
||
|
|
||
|
def write_to_cache(db_name, data):
|
||
|
# cache is in: ~/safety/cache.json
|
||
|
# and has the following form:
|
||
|
# {
|
||
|
# "insecure.json": {
|
||
|
# "cached_at": 12345678
|
||
|
# "db": {}
|
||
|
# },
|
||
|
# "insecure_full.json": {
|
||
|
# "cached_at": 12345678
|
||
|
# "db": {}
|
||
|
# },
|
||
|
# }
|
||
|
if not os.path.exists(os.path.dirname(CACHE_FILE)):
|
||
|
try:
|
||
|
os.makedirs(os.path.dirname(CACHE_FILE))
|
||
|
with open(CACHE_FILE, "w") as _:
|
||
|
_.write(json.dumps({}))
|
||
|
LOG.debug('Cache file created')
|
||
|
except OSError as exc: # Guard against race condition
|
||
|
LOG.debug('Unable to create the cache file because: %s', exc.errno)
|
||
|
if exc.errno != errno.EEXIST:
|
||
|
raise
|
||
|
|
||
|
with open(CACHE_FILE, "r") as f:
|
||
|
try:
|
||
|
cache = json.loads(f.read())
|
||
|
except json.JSONDecodeError:
|
||
|
LOG.debug('JSONDecodeError in the local cache, dumping the full cache file.')
|
||
|
cache = {}
|
||
|
|
||
|
with open(CACHE_FILE, "w") as f:
|
||
|
cache[db_name] = {
|
||
|
"cached_at": time.time(),
|
||
|
"db": data
|
||
|
}
|
||
|
f.write(json.dumps(cache))
|
||
|
LOG.debug('Safety updated the cache file for %s database.', db_name)
|
||
|
|
||
|
|
||
|
def fetch_database_url(mirror, db_name, key, cached, proxy, telemetry=True):
|
||
|
headers = {}
|
||
|
if key:
|
||
|
headers["X-Api-Key"] = key
|
||
|
|
||
|
if not proxy:
|
||
|
proxy = {}
|
||
|
|
||
|
if cached:
|
||
|
cached_data = get_from_cache(db_name=db_name, cache_valid_seconds=cached)
|
||
|
if cached_data:
|
||
|
LOG.info('Database %s returned from cache.', db_name)
|
||
|
return cached_data
|
||
|
url = mirror + db_name
|
||
|
|
||
|
telemetry_data = {'telemetry': json.dumps(build_telemetry_data(telemetry=telemetry))}
|
||
|
|
||
|
try:
|
||
|
r = session.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy, params=telemetry_data)
|
||
|
except requests.exceptions.ConnectionError:
|
||
|
raise NetworkConnectionError()
|
||
|
except requests.exceptions.Timeout:
|
||
|
raise RequestTimeoutError()
|
||
|
except requests.exceptions.RequestException:
|
||
|
raise DatabaseFetchError()
|
||
|
|
||
|
if r.status_code == 403:
|
||
|
raise InvalidKeyError(key=key, reason=r.text)
|
||
|
|
||
|
if r.status_code == 429:
|
||
|
raise TooManyRequestsError(reason=r.text)
|
||
|
|
||
|
if r.status_code != 200:
|
||
|
raise ServerError(reason=r.reason)
|
||
|
|
||
|
try:
|
||
|
data = r.json()
|
||
|
except json.JSONDecodeError as e:
|
||
|
raise MalformedDatabase(reason=e)
|
||
|
|
||
|
if cached:
|
||
|
LOG.info('Writing %s to cache because cached value was %s', db_name, cached)
|
||
|
write_to_cache(db_name, data)
|
||
|
|
||
|
return data
|
||
|
|
||
|
|
||
|
def fetch_policy(key, proxy):
|
||
|
url = f"{API_BASE_URL}policy/"
|
||
|
headers = {"X-Api-Key": key}
|
||
|
|
||
|
if not proxy:
|
||
|
proxy = {}
|
||
|
|
||
|
try:
|
||
|
LOG.debug(f'Getting policy')
|
||
|
r = session.get(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy)
|
||
|
LOG.debug(r.text)
|
||
|
return r.json()
|
||
|
except:
|
||
|
import pipenv.vendor.click as click
|
||
|
|
||
|
LOG.exception("Error fetching policy")
|
||
|
click.secho(
|
||
|
"Warning: couldn't fetch policy from pyup.io.",
|
||
|
fg="yellow",
|
||
|
file=sys.stderr
|
||
|
)
|
||
|
|
||
|
return {"safety_policy": "", "audit_and_monitor": False}
|
||
|
|
||
|
|
||
|
def post_results(key, proxy, safety_json, policy_file):
|
||
|
url = f"{API_BASE_URL}result/"
|
||
|
headers = {"X-Api-Key": key}
|
||
|
|
||
|
if not proxy:
|
||
|
proxy = {}
|
||
|
|
||
|
# safety_json is in text form already. policy_file is a text YAML
|
||
|
audit_report = {
|
||
|
"safety_json": json.loads(safety_json),
|
||
|
"policy_file": policy_file
|
||
|
}
|
||
|
|
||
|
try:
|
||
|
LOG.debug(f'Posting results: {audit_report}')
|
||
|
r = session.post(url=url, timeout=REQUEST_TIMEOUT, headers=headers, proxies=proxy, json=audit_report)
|
||
|
LOG.debug(r.text)
|
||
|
|
||
|
return r.json()
|
||
|
except:
|
||
|
import pipenv.vendor.click as click
|
||
|
|
||
|
LOG.exception("Error posting results")
|
||
|
click.secho(
|
||
|
"Warning: couldn't upload results to pyup.io.",
|
||
|
fg="yellow",
|
||
|
file=sys.stderr
|
||
|
)
|
||
|
|
||
|
return {}
|
||
|
|
||
|
|
||
|
def fetch_database_file(path, db_name):
|
||
|
full_path = os.path.join(path, db_name)
|
||
|
if not os.path.exists(full_path):
|
||
|
raise DatabaseFileNotFoundError(db=path)
|
||
|
with open(full_path) as f:
|
||
|
return json.loads(f.read())
|
||
|
|
||
|
|
||
|
def fetch_database(full=False, key=False, db=False, cached=0, proxy=None, telemetry=True):
|
||
|
if key:
|
||
|
mirrors = API_MIRRORS
|
||
|
elif db:
|
||
|
mirrors = [db]
|
||
|
else:
|
||
|
mirrors = OPEN_MIRRORS
|
||
|
|
||
|
db_name = "insecure_full.json" if full else "insecure.json"
|
||
|
for mirror in mirrors:
|
||
|
# mirror can either be a local path or a URL
|
||
|
if is_a_remote_mirror(mirror):
|
||
|
data = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy, telemetry=telemetry)
|
||
|
else:
|
||
|
data = fetch_database_file(mirror, db_name=db_name)
|
||
|
if data:
|
||
|
return data
|
||
|
raise DatabaseFetchError()
|
||
|
|
||
|
|
||
|
def get_vulnerabilities(pkg, spec, db):
|
||
|
for entry in db[pkg]:
|
||
|
for entry_spec in entry["specs"]:
|
||
|
if entry_spec == spec:
|
||
|
yield entry
|
||
|
|
||
|
|
||
|
def get_vulnerability_from(vuln_id, cve, data, specifier, db, name, pkg, ignore_vulns):
|
||
|
base_domain = db.get('$meta', {}).get('base_domain')
|
||
|
pkg_meta = db.get('$meta', {}).get('packages', {}).get(name, {})
|
||
|
insecure_versions = pkg_meta.get("insecure_versions", [])
|
||
|
secure_versions = pkg_meta.get("secure_versions", [])
|
||
|
latest_version_without_known_vulnerabilities = pkg_meta.get("latest_secure_version", None)
|
||
|
latest_version = pkg_meta.get("latest_version", None)
|
||
|
pkg_refreshed = pkg._replace(insecure_versions=insecure_versions, secure_versions=secure_versions,
|
||
|
latest_version_without_known_vulnerabilities=latest_version_without_known_vulnerabilities,
|
||
|
latest_version=latest_version,
|
||
|
more_info_url=f"{base_domain}{pkg_meta.get('more_info_path', '')}")
|
||
|
|
||
|
ignored = (ignore_vulns and vuln_id in ignore_vulns and (
|
||
|
not ignore_vulns[vuln_id]['expires'] or ignore_vulns[vuln_id]['expires'] > datetime.utcnow()))
|
||
|
more_info_url = f"{base_domain}{data.get('more_info_path', '')}"
|
||
|
severity = None
|
||
|
|
||
|
if cve and (cve.cvssv2 or cve.cvssv3):
|
||
|
severity = Severity(source=cve.name, cvssv2=cve.cvssv2, cvssv3=cve.cvssv3)
|
||
|
|
||
|
return Vulnerability(
|
||
|
vulnerability_id=vuln_id,
|
||
|
package_name=name,
|
||
|
pkg=pkg_refreshed,
|
||
|
ignored=ignored,
|
||
|
ignored_reason=ignore_vulns.get(vuln_id, {}).get('reason', None) if ignore_vulns else None,
|
||
|
ignored_expires=ignore_vulns.get(vuln_id, {}).get('expires', None) if ignore_vulns else None,
|
||
|
vulnerable_spec=specifier,
|
||
|
all_vulnerable_specs=data.get("specs", []),
|
||
|
analyzed_version=pkg_refreshed.version,
|
||
|
advisory=data.get("advisory"),
|
||
|
is_transitive=data.get("transitive", False),
|
||
|
published_date=data.get("published_date"),
|
||
|
fixed_versions=[ver for ver in data.get("fixed_versions", []) if ver],
|
||
|
closest_versions_without_known_vulnerabilities=data.get("closest_secure_versions", []),
|
||
|
resources=data.get("vulnerability_resources"),
|
||
|
CVE=cve,
|
||
|
severity=severity,
|
||
|
affected_versions=data.get("affected_versions", []),
|
||
|
more_info_url=more_info_url
|
||
|
)
|
||
|
|
||
|
|
||
|
def get_cve_from(data, db_full):
|
||
|
cve_data = data.get("cve", '')
|
||
|
|
||
|
if not cve_data:
|
||
|
return None
|
||
|
|
||
|
cve_id = cve_data.split(",")[0].strip()
|
||
|
cve_meta = db_full.get("$meta", {}).get("cve", {}).get(cve_id, {})
|
||
|
return CVE(name=cve_id, cvssv2=cve_meta.get("cvssv2", None),
|
||
|
cvssv3=cve_meta.get("cvssv3", None))
|
||
|
|
||
|
|
||
|
def ignore_vuln_if_needed(vuln_id, cve, ignore_vulns, ignore_severity_rules):
|
||
|
|
||
|
if not ignore_severity_rules or not isinstance(ignore_vulns, dict):
|
||
|
return
|
||
|
|
||
|
severity = None
|
||
|
|
||
|
if cve:
|
||
|
if cve.cvssv2 and cve.cvssv2.get("base_score", None):
|
||
|
severity = cve.cvssv2.get("base_score", None)
|
||
|
|
||
|
if cve.cvssv3 and cve.cvssv3.get("base_score", None):
|
||
|
severity = cve.cvssv3.get("base_score", None)
|
||
|
|
||
|
ignore_severity_below = float(ignore_severity_rules.get('ignore-cvss-severity-below', 0.0))
|
||
|
ignore_unknown_severity = bool(ignore_severity_rules.get('ignore-cvss-unknown-severity', False))
|
||
|
|
||
|
if severity:
|
||
|
if float(severity) < ignore_severity_below:
|
||
|
reason = 'Ignored by severity rule in policy file, {0} < {1}'.format(float(severity),
|
||
|
ignore_severity_below)
|
||
|
ignore_vulns[vuln_id] = {'reason': reason, 'expires': None}
|
||
|
elif ignore_unknown_severity:
|
||
|
reason = 'Unknown CVSS severity, ignored by severity rule in policy file.'
|
||
|
ignore_vulns[vuln_id] = {'reason': reason, 'expires': None}
|
||
|
|
||
|
|
||
|
@sync_safety_context
|
||
|
def check(packages, key=False, db_mirror=False, cached=0, ignore_vulns=None, ignore_severity_rules=None, proxy=None,
|
||
|
include_ignored=False, is_env_scan=True, telemetry=True, params=None, project=None):
|
||
|
SafetyContext().command = 'check'
|
||
|
db = fetch_database(key=key, db=db_mirror, cached=cached, proxy=proxy, telemetry=telemetry)
|
||
|
db_full = None
|
||
|
vulnerable_packages = frozenset(db.keys())
|
||
|
vulnerabilities = []
|
||
|
|
||
|
for pkg in packages:
|
||
|
# Ignore recursive files not resolved
|
||
|
if isinstance(pkg, RequirementFile):
|
||
|
continue
|
||
|
|
||
|
# normalize the package name, the safety-db is converting underscores to dashes and uses
|
||
|
# lowercase
|
||
|
name = canonicalize_name(pkg.name)
|
||
|
|
||
|
if name in vulnerable_packages:
|
||
|
# we have a candidate here, build the spec set
|
||
|
for specifier in db[name]:
|
||
|
spec_set = SpecifierSet(specifiers=specifier)
|
||
|
if spec_set.contains(pkg.version):
|
||
|
if not db_full:
|
||
|
db_full = fetch_database(full=True, key=key, db=db_mirror, cached=cached, proxy=proxy,
|
||
|
telemetry=telemetry)
|
||
|
for data in get_vulnerabilities(pkg=name, spec=specifier, db=db_full):
|
||
|
vuln_id = data.get("id").replace("pyup.io-", "")
|
||
|
cve = get_cve_from(data, db_full)
|
||
|
|
||
|
ignore_vuln_if_needed(vuln_id, cve, ignore_vulns, ignore_severity_rules)
|
||
|
|
||
|
vulnerability = get_vulnerability_from(vuln_id, cve, data, specifier, db_full, name, pkg,
|
||
|
ignore_vulns)
|
||
|
|
||
|
should_add_vuln = not (vulnerability.is_transitive and is_env_scan)
|
||
|
|
||
|
if (include_ignored or vulnerability.vulnerability_id not in ignore_vulns) and should_add_vuln:
|
||
|
vulnerabilities.append(vulnerability)
|
||
|
|
||
|
return vulnerabilities, db_full
|
||
|
|
||
|
|
||
|
def precompute_remediations(remediations, package_metadata, vulns,
|
||
|
ignored_vulns):
|
||
|
for vuln in vulns:
|
||
|
if vuln.ignored:
|
||
|
ignored_vulns.add(vuln.vulnerability_id)
|
||
|
continue
|
||
|
|
||
|
if vuln.package_name in remediations.keys():
|
||
|
remediations[vuln.package_name]['vulns_found'] = remediations[vuln.package_name].get('vulns_found', 0) + 1
|
||
|
else:
|
||
|
vulns_count = 1
|
||
|
package_metadata[vuln.package_name] = {'insecure_versions': vuln.pkg.insecure_versions,
|
||
|
'secure_versions': vuln.pkg.secure_versions, 'version': vuln.pkg.version}
|
||
|
remediations[vuln.package_name] = {'vulns_found': vulns_count, 'version': vuln.pkg.version,
|
||
|
'more_info_url': vuln.pkg.more_info_url}
|
||
|
|
||
|
|
||
|
def get_closest_ver(versions, version):
|
||
|
results = {'minor': None, 'major': None}
|
||
|
if not version or not versions:
|
||
|
return results
|
||
|
|
||
|
sorted_versions = sorted(versions, key=lambda ver: parse_version(ver), reverse=True)
|
||
|
|
||
|
for v in sorted_versions:
|
||
|
index = parse_version(v)
|
||
|
current_v = parse_version(version)
|
||
|
|
||
|
if index > current_v:
|
||
|
results['major'] = index
|
||
|
|
||
|
if index < current_v:
|
||
|
results['minor'] = index
|
||
|
break
|
||
|
|
||
|
return results
|
||
|
|
||
|
|
||
|
def compute_sec_ver_for_user(package, ignored_vulns, db_full):
|
||
|
pkg_meta = db_full.get('$meta', {}).get('packages', {}).get(package, {})
|
||
|
versions = set(pkg_meta.get("insecure_versions", []) + pkg_meta.get("secure_versions", []))
|
||
|
affected_versions = []
|
||
|
|
||
|
for vuln in db_full.get(package, []):
|
||
|
vuln_id = vuln.get('id', None)
|
||
|
if vuln_id and vuln_id not in ignored_vulns:
|
||
|
affected_versions += vuln.get('affected_versions', [])
|
||
|
|
||
|
affected_v = set(affected_versions)
|
||
|
sec_ver_for_user = list(versions.difference(affected_v))
|
||
|
|
||
|
return sorted(sec_ver_for_user, key=lambda ver: parse_version(ver), reverse=True)
|
||
|
|
||
|
|
||
|
def compute_sec_ver(remediations, package_metadata, ignored_vulns, db_full):
|
||
|
"""
|
||
|
Compute the secure_versions and the closest_secure_version for each remediation using the affected_versions
|
||
|
of each no ignored vulnerability of the same package, there is only a remediation for each package.
|
||
|
"""
|
||
|
for pkg_name in remediations.keys():
|
||
|
pkg = package_metadata.get(pkg_name, {})
|
||
|
|
||
|
if not ignored_vulns:
|
||
|
secure_v = pkg.get('secure_versions', [])
|
||
|
else:
|
||
|
secure_v = compute_sec_ver_for_user(package=pkg_name, ignored_vulns=ignored_vulns, db_full=db_full)
|
||
|
|
||
|
remediations[pkg_name]['secure_versions'] = secure_v
|
||
|
remediations[pkg_name]['closest_secure_version'] = get_closest_ver(secure_v,
|
||
|
pkg.get('version', None))
|
||
|
|
||
|
|
||
|
def calculate_remediations(vulns, db_full):
|
||
|
remediations = {}
|
||
|
package_metadata = {}
|
||
|
ignored_vulns = set()
|
||
|
|
||
|
if not db_full:
|
||
|
return remediations
|
||
|
|
||
|
precompute_remediations(remediations, package_metadata, vulns, ignored_vulns)
|
||
|
compute_sec_ver(remediations, package_metadata, ignored_vulns, db_full)
|
||
|
|
||
|
return remediations
|
||
|
|
||
|
|
||
|
@sync_safety_context
|
||
|
def review(report=None, params=None):
|
||
|
SafetyContext().command = 'review'
|
||
|
vulnerable = []
|
||
|
vulnerabilities = report.get('vulnerabilities', []) + report.get('ignored_vulnerabilities', [])
|
||
|
remediations = {}
|
||
|
|
||
|
for key, value in report.get('remediations', {}).items():
|
||
|
recommended = value.get('recommended_version', None)
|
||
|
secure_v = value.get('other_recommended_versions', [])
|
||
|
major = None
|
||
|
if recommended:
|
||
|
secure_v.append(recommended)
|
||
|
major = parse(recommended)
|
||
|
|
||
|
remediations[key] = {'vulns_found': value.get('vulnerabilities_found', 0),
|
||
|
'version': value.get('current_version'),
|
||
|
'secure_versions': secure_v,
|
||
|
'closest_secure_version': {'major': major, 'minor': None},
|
||
|
# minor isn't supported in review
|
||
|
'more_info_url': value.get('more_info_url')}
|
||
|
|
||
|
packages = report.get('scanned_packages', [])
|
||
|
pkgs = {pkg_name: Package(**pkg_values) for pkg_name, pkg_values in packages.items()}
|
||
|
ctx = SafetyContext()
|
||
|
found_packages = list(pkgs.values())
|
||
|
ctx.packages = found_packages
|
||
|
ctx.review = report.get('report_meta', [])
|
||
|
ctx.key = ctx.review.get('api_key', False)
|
||
|
cvssv2 = None
|
||
|
cvssv3 = None
|
||
|
|
||
|
for vuln in vulnerabilities:
|
||
|
vuln['pkg'] = pkgs.get(vuln.get('package_name', None))
|
||
|
XVE_ID = vuln.get('CVE', None) # Trying to get first the CVE ID
|
||
|
|
||
|
severity = vuln.get('severity', None)
|
||
|
if severity and severity.get('source', False):
|
||
|
cvssv2 = severity.get('cvssv2', None)
|
||
|
cvssv3 = severity.get('cvssv3', None)
|
||
|
# Trying to get the PVE ID if it exists, otherwise it will be the same CVE ID of above
|
||
|
XVE_ID = severity.get('source', False)
|
||
|
vuln['severity'] = Severity(source=XVE_ID, cvssv2=cvssv2, cvssv3=cvssv3)
|
||
|
else:
|
||
|
vuln['severity'] = None
|
||
|
|
||
|
ignored_expires = vuln.get('ignored_expires', None)
|
||
|
|
||
|
if ignored_expires:
|
||
|
vuln['ignored_expires'] = validate_expiration_date(ignored_expires)
|
||
|
|
||
|
vuln['CVE'] = CVE(name=XVE_ID, cvssv2=cvssv2, cvssv3=cvssv3) if XVE_ID else None
|
||
|
|
||
|
vulnerable.append(Vulnerability(**vuln))
|
||
|
|
||
|
return vulnerable, remediations, found_packages
|
||
|
|
||
|
|
||
|
@sync_safety_context
|
||
|
def get_licenses(key=False, db_mirror=False, cached=0, proxy=None, telemetry=True):
|
||
|
key = key if key else os.environ.get("SAFETY_API_KEY", False)
|
||
|
|
||
|
if not key and not db_mirror:
|
||
|
raise InvalidKeyError(message="The API-KEY was not provided.")
|
||
|
if db_mirror:
|
||
|
mirrors = [db_mirror]
|
||
|
else:
|
||
|
mirrors = API_MIRRORS
|
||
|
|
||
|
db_name = "licenses.json"
|
||
|
|
||
|
for mirror in mirrors:
|
||
|
# mirror can either be a local path or a URL
|
||
|
if is_a_remote_mirror(mirror):
|
||
|
licenses = fetch_database_url(mirror, db_name=db_name, key=key, cached=cached, proxy=proxy,
|
||
|
telemetry=telemetry)
|
||
|
else:
|
||
|
licenses = fetch_database_file(mirror, db_name=db_name)
|
||
|
if licenses:
|
||
|
return licenses
|
||
|
raise DatabaseFetchError()
|
||
|
|
||
|
|
||
|
def get_announcements(key, proxy, telemetry=True):
|
||
|
LOG.info('Getting announcements')
|
||
|
|
||
|
announcements = []
|
||
|
headers = {}
|
||
|
|
||
|
if key:
|
||
|
headers["X-Api-Key"] = key
|
||
|
|
||
|
url = f"{API_BASE_URL}announcements/"
|
||
|
method = 'post'
|
||
|
data = build_telemetry_data(telemetry=telemetry)
|
||
|
request_kwargs = {'headers': headers, 'proxies': proxy, 'timeout': 3}
|
||
|
data_keyword = 'json'
|
||
|
|
||
|
source = os.environ.get('SAFETY_ANNOUNCEMENTS_URL', None)
|
||
|
|
||
|
if source:
|
||
|
LOG.debug(f'Getting the announcement from a different source: {source}')
|
||
|
url = source
|
||
|
method = 'get'
|
||
|
data = {
|
||
|
'telemetry': json.dumps(data)}
|
||
|
data_keyword = 'params'
|
||
|
|
||
|
request_kwargs[data_keyword] = data
|
||
|
request_kwargs['url'] = url
|
||
|
|
||
|
LOG.debug(f'Telemetry data sent: {data}')
|
||
|
|
||
|
try:
|
||
|
request_func = getattr(session, method)
|
||
|
r = request_func(**request_kwargs)
|
||
|
LOG.debug(r.text)
|
||
|
except Exception as e:
|
||
|
LOG.info('Unexpected but HANDLED Exception happened getting the announcements: %s', e)
|
||
|
return announcements
|
||
|
|
||
|
if r.status_code == 200:
|
||
|
try:
|
||
|
announcements = r.json()
|
||
|
if 'announcements' in announcements.keys():
|
||
|
announcements = announcements.get('announcements', [])
|
||
|
else:
|
||
|
LOG.info('There is not announcements key in the JSON response, is this a wrong structure?')
|
||
|
announcements = []
|
||
|
|
||
|
except json.JSONDecodeError as e:
|
||
|
LOG.info('Unexpected but HANDLED Exception happened decoding the announcement response: %s', e)
|
||
|
|
||
|
LOG.info('Announcements fetched')
|
||
|
|
||
|
return announcements
|
||
|
|
||
|
|
||
|
def get_packages(files=False, stdin=False) -> List[Package]:
|
||
|
if files:
|
||
|
return list(itertools.chain.from_iterable(read_requirements(f, resolve=True) for f in files))
|
||
|
|
||
|
if stdin:
|
||
|
return list(read_requirements(sys.stdin))
|
||
|
|
||
|
return [
|
||
|
Package(
|
||
|
name=dist.metadata["Name"],
|
||
|
version=str(dist.version),
|
||
|
found=str(dist.locate_file("")),
|
||
|
insecure_versions=[],
|
||
|
secure_versions=[],
|
||
|
latest_version=None,
|
||
|
latest_version_without_known_vulnerabilities=None,
|
||
|
more_info_url=None,
|
||
|
)
|
||
|
for dist in importlib_metadata.distributions()
|
||
|
if dist.metadata["Name"] not in {"python", "wsgiref", "argparse"}
|
||
|
]
|
||
|
|
||
|
def read_vulnerabilities(fh):
|
||
|
try:
|
||
|
data = json.load(fh)
|
||
|
except json.JSONDecodeError as e:
|
||
|
raise MalformedDatabase(reason=e, fetched_from=fh.name)
|
||
|
except TypeError as e:
|
||
|
raise MalformedDatabase(reason=e, fetched_from=fh.name)
|
||
|
|
||
|
return data
|
||
|
|
||
|
|
||
|
def close_session():
|
||
|
LOG.debug('Closing requests session.')
|
||
|
session.close()
|