694 lines
26 KiB
Python
694 lines
26 KiB
Python
|
import json
|
||
|
import logging
|
||
|
import os
|
||
|
import textwrap
|
||
|
from datetime import datetime
|
||
|
|
||
|
import pipenv.vendor.click as click
|
||
|
|
||
|
from pipenv.patched.safety.constants import RED, YELLOW
|
||
|
from pipenv.patched.safety.util import get_safety_version, Package, get_terminal_size, \
|
||
|
SafetyContext, build_telemetry_data, build_git_data, is_a_remote_mirror
|
||
|
|
||
|
LOG = logging.getLogger(__name__)
|
||
|
|
||
|
|
||
|
def build_announcements_section_content(announcements, columns=get_terminal_size().columns,
|
||
|
start_line_decorator=' ', end_line_decorator=' '):
|
||
|
section = ''
|
||
|
|
||
|
for i, announcement in enumerate(announcements):
|
||
|
|
||
|
color = ''
|
||
|
if announcement.get('type') == 'error':
|
||
|
color = RED
|
||
|
elif announcement.get('type') == 'warning':
|
||
|
color = YELLOW
|
||
|
|
||
|
item = '{message}'.format(
|
||
|
message=format_long_text('* ' + announcement.get('message'), color, columns,
|
||
|
start_line_decorator, end_line_decorator))
|
||
|
section += '{item}'.format(item=item)
|
||
|
|
||
|
if i + 1 < len(announcements):
|
||
|
section += '\n'
|
||
|
|
||
|
return section
|
||
|
|
||
|
|
||
|
def add_empty_line():
|
||
|
return format_long_text('')
|
||
|
|
||
|
|
||
|
def style_lines(lines, columns, pre_processed_text='', start_line=' ' * 4, end_line=' ' * 4):
|
||
|
styled_text = pre_processed_text
|
||
|
|
||
|
for line in lines:
|
||
|
styled_line = ''
|
||
|
left_padding = ' ' * line.get('left_padding', 0)
|
||
|
|
||
|
for i, word in enumerate(line.get('words', [])):
|
||
|
if word.get('style', {}):
|
||
|
text = ''
|
||
|
|
||
|
if i == 0:
|
||
|
text = left_padding # Include the line padding in the word to avoid Github issues
|
||
|
left_padding = '' # Clean left padding to avoid be added two times
|
||
|
|
||
|
text += word.get('value', '')
|
||
|
|
||
|
styled_line += click.style(text=text, **word.get('style', {}))
|
||
|
else:
|
||
|
styled_line += word.get('value', '')
|
||
|
|
||
|
styled_text += format_long_text(styled_line, columns=columns, start_line_decorator=start_line,
|
||
|
end_line_decorator=end_line,
|
||
|
left_padding=left_padding, **line.get('format', {})) + '\n'
|
||
|
|
||
|
return styled_text
|
||
|
|
||
|
|
||
|
def format_vulnerability(vulnerability, full_mode, only_text=False, columns=get_terminal_size().columns):
|
||
|
|
||
|
common_format = {'left_padding': 3, 'format': {'sub_indent': ' ' * 3, 'max_lines': None}}
|
||
|
|
||
|
styled_vulnerability = [
|
||
|
{'words': [{'style': {'bold': True}, 'value': 'Vulnerability ID: '}, {'value': vulnerability.vulnerability_id}]},
|
||
|
]
|
||
|
|
||
|
vulnerability_spec = [
|
||
|
{'words': [{'style': {'bold': True}, 'value': 'Affected spec: '}, {'value': vulnerability.vulnerable_spec}]}]
|
||
|
|
||
|
cve = vulnerability.CVE
|
||
|
|
||
|
cvssv2_line = None
|
||
|
cve_lines = []
|
||
|
|
||
|
if cve:
|
||
|
if full_mode and cve.cvssv2:
|
||
|
b = cve.cvssv2.get("base_score", "-")
|
||
|
s = cve.cvssv2.get("impact_score", "-")
|
||
|
v = cve.cvssv2.get("vector_string", "-")
|
||
|
|
||
|
# Reset sub_indent as the left_margin is going to be applied in this case
|
||
|
cvssv2_line = {'format': {'sub_indent': ''}, 'words': [
|
||
|
{'value': f'CVSS v2, BASE SCORE {b}, IMPACT SCORE {s}, VECTOR STRING {v}'},
|
||
|
]}
|
||
|
|
||
|
if cve.cvssv3 and "base_severity" in cve.cvssv3.keys():
|
||
|
cvss_base_severity_style = {'bold': True}
|
||
|
base_severity = cve.cvssv3.get("base_severity", "-")
|
||
|
|
||
|
if base_severity.upper() in ['HIGH', 'CRITICAL']:
|
||
|
cvss_base_severity_style['fg'] = 'red'
|
||
|
|
||
|
b = cve.cvssv3.get("base_score", "-")
|
||
|
|
||
|
if full_mode:
|
||
|
s = cve.cvssv3.get("impact_score", "-")
|
||
|
v = cve.cvssv3.get("vector_string", "-")
|
||
|
|
||
|
cvssv3_text = f'CVSS v3, BASE SCORE {b}, IMPACT SCORE {s}, VECTOR STRING {v}'
|
||
|
|
||
|
else:
|
||
|
cvssv3_text = f'CVSS v3, BASE SCORE {b} '
|
||
|
|
||
|
cve_lines = [
|
||
|
{'words': [{'style': {'bold': True}, 'value': '{0} is '.format(cve.name)},
|
||
|
{'style': cvss_base_severity_style,
|
||
|
'value': f'{base_severity} SEVERITY => '},
|
||
|
{'value': cvssv3_text},
|
||
|
]},
|
||
|
]
|
||
|
|
||
|
if cvssv2_line:
|
||
|
cve_lines.append(cvssv2_line)
|
||
|
|
||
|
elif cve.name:
|
||
|
cve_lines = [
|
||
|
{'words': [{'style': {'bold': True}, 'value': cve.name}]}
|
||
|
]
|
||
|
|
||
|
advisory_format = {'sub_indent': ' ' * 3, 'max_lines': None} if full_mode else {'sub_indent': ' ' * 3,
|
||
|
'max_lines': 2}
|
||
|
|
||
|
basic_vuln_data_lines = [
|
||
|
{'format': advisory_format, 'words': [
|
||
|
{'style': {'bold': True}, 'value': 'ADVISORY: '},
|
||
|
{'value': vulnerability.advisory.replace('\n', '')}]}
|
||
|
]
|
||
|
|
||
|
if SafetyContext().key:
|
||
|
fixed_version_line = {'words': [
|
||
|
{'style': {'bold': True}, 'value': 'Fixed versions: '},
|
||
|
{'value': ', '.join(vulnerability.fixed_versions) if vulnerability.fixed_versions else 'No known fix'}
|
||
|
]}
|
||
|
|
||
|
basic_vuln_data_lines.append(fixed_version_line)
|
||
|
|
||
|
more_info_line = [{'words': [{'style': {'bold': True}, 'value': 'For more information, please visit '},
|
||
|
{'value': click.style(vulnerability.more_info_url)}]}]
|
||
|
|
||
|
vuln_title = f'-> Vulnerability found in {vulnerability.package_name} version {vulnerability.analyzed_version}\n'
|
||
|
|
||
|
styled_text = click.style(vuln_title, fg='red')
|
||
|
|
||
|
to_print = styled_vulnerability
|
||
|
|
||
|
if not vulnerability.ignored:
|
||
|
to_print += vulnerability_spec + basic_vuln_data_lines + cve_lines
|
||
|
else:
|
||
|
generic_reason = 'This vulnerability is being ignored'
|
||
|
if vulnerability.ignored_expires:
|
||
|
generic_reason += f" until {vulnerability.ignored_expires.strftime('%Y-%m-%d %H:%M:%S UTC')}. " \
|
||
|
f"See your configurations"
|
||
|
|
||
|
specific_reason = None
|
||
|
if vulnerability.ignored_reason:
|
||
|
specific_reason = [
|
||
|
{'words': [{'style': {'bold': True}, 'value': 'Reason: '}, {'value': vulnerability.ignored_reason}]}]
|
||
|
|
||
|
expire_section = [{'words': [
|
||
|
{'style': {'bold': True, 'fg': 'green'}, 'value': f'{generic_reason}.'}, ]}]
|
||
|
|
||
|
if specific_reason:
|
||
|
expire_section += specific_reason
|
||
|
|
||
|
to_print += expire_section
|
||
|
|
||
|
if cve:
|
||
|
to_print += more_info_line
|
||
|
|
||
|
to_print = [{**common_format, **line} for line in to_print]
|
||
|
|
||
|
content = style_lines(to_print, columns, styled_text, start_line='', end_line='', )
|
||
|
|
||
|
return click.unstyle(content) if only_text else content
|
||
|
|
||
|
|
||
|
def format_license(license, only_text=False, columns=get_terminal_size().columns):
|
||
|
to_print = [
|
||
|
{'words': [{'style': {'bold': True}, 'value': license['package']},
|
||
|
{'value': ' version {0} found using license '.format(license['version'])},
|
||
|
{'style': {'bold': True}, 'value': license['license']}
|
||
|
]
|
||
|
},
|
||
|
]
|
||
|
|
||
|
content = style_lines(to_print, columns, '-> ', start_line='', end_line='')
|
||
|
|
||
|
return click.unstyle(content) if only_text else content
|
||
|
|
||
|
|
||
|
def build_remediation_section(remediations, only_text=False, columns=get_terminal_size().columns, kwargs=None):
|
||
|
columns -= 2
|
||
|
left_padding = ' ' * 3
|
||
|
|
||
|
if not kwargs:
|
||
|
# Reset default params in the format_long_text func
|
||
|
kwargs = {'left_padding': '', 'columns': columns, 'start_line_decorator': '', 'end_line_decorator': '',
|
||
|
'sub_indent': left_padding}
|
||
|
|
||
|
END_SECTION = '+' + '=' * columns + '+'
|
||
|
|
||
|
if not remediations:
|
||
|
return []
|
||
|
|
||
|
content = ''
|
||
|
total_vulns = 0
|
||
|
total_packages = len(remediations.keys())
|
||
|
|
||
|
for pkg in remediations.keys():
|
||
|
total_vulns += remediations[pkg]['vulns_found']
|
||
|
upgrade_to = remediations[pkg]['closest_secure_version']['major']
|
||
|
downgrade_to = remediations[pkg]['closest_secure_version']['minor']
|
||
|
fix_version = None
|
||
|
|
||
|
if upgrade_to:
|
||
|
fix_version = str(upgrade_to)
|
||
|
elif downgrade_to:
|
||
|
fix_version = str(downgrade_to)
|
||
|
|
||
|
new_line = '\n'
|
||
|
|
||
|
other_options = [str(fix) for fix in remediations[pkg].get('secure_versions', []) if str(fix) != fix_version]
|
||
|
raw_recommendation = f"We recommend upgrading to version {upgrade_to} of {pkg}."
|
||
|
|
||
|
if other_options:
|
||
|
raw_other_options = ', '.join(other_options)
|
||
|
raw_pre_other_options = 'Other versions without known vulnerabilities are:'
|
||
|
if len(other_options) == 1:
|
||
|
raw_pre_other_options = 'Other version without known vulnerabilities is'
|
||
|
raw_recommendation = f"{raw_recommendation} {raw_pre_other_options} " \
|
||
|
f"{raw_other_options}"
|
||
|
|
||
|
remediation_content = [
|
||
|
f'{left_padding}The closest version with no known vulnerabilities is ' + click.style(upgrade_to, bold=True),
|
||
|
new_line,
|
||
|
click.style(f'{left_padding}{raw_recommendation}', bold=True, fg='green')
|
||
|
]
|
||
|
|
||
|
if not fix_version:
|
||
|
remediation_content = [new_line,
|
||
|
click.style(f'{left_padding}There is no known fix for this vulnerability.', bold=True, fg='yellow')]
|
||
|
|
||
|
text = 'vulnerabilities' if remediations[pkg]['vulns_found'] > 1 else 'vulnerability'
|
||
|
|
||
|
raw_rem_title = f"-> {pkg} version {remediations[pkg]['version']} was found, " \
|
||
|
f"which has {remediations[pkg]['vulns_found']} {text}"
|
||
|
|
||
|
remediation_title = click.style(raw_rem_title, fg=RED, bold=True)
|
||
|
|
||
|
content += new_line + format_long_text(remediation_title, **kwargs) + new_line
|
||
|
|
||
|
pre_content = remediation_content + [
|
||
|
f"{left_padding}For more information, please visit {remediations[pkg]['more_info_url']}",
|
||
|
f'{left_padding}Always check for breaking changes when upgrading packages.',
|
||
|
new_line]
|
||
|
|
||
|
for i, element in enumerate(pre_content):
|
||
|
content += format_long_text(element, **kwargs)
|
||
|
|
||
|
if i + 1 < len(pre_content):
|
||
|
content += '\n'
|
||
|
|
||
|
title = format_long_text(click.style(f'{left_padding}REMEDIATIONS', fg='green', bold=True), **kwargs)
|
||
|
|
||
|
body = [content]
|
||
|
|
||
|
if not is_using_api_key():
|
||
|
vuln_text = 'vulnerabilities were' if total_vulns != 1 else 'vulnerability was'
|
||
|
pkg_text = 'packages' if total_packages > 1 else 'package'
|
||
|
msg = "{0} {1} found in {2} {3}. " \
|
||
|
"For detailed remediation & fix recommendations, upgrade to a commercial license."\
|
||
|
.format(total_vulns, vuln_text, total_packages, pkg_text)
|
||
|
content = '\n' + format_long_text(msg, left_padding=' ', columns=columns) + '\n'
|
||
|
body = [content]
|
||
|
|
||
|
body.append(END_SECTION)
|
||
|
|
||
|
content = [title] + body
|
||
|
|
||
|
if only_text:
|
||
|
content = [click.unstyle(item) for item in content]
|
||
|
|
||
|
return content
|
||
|
|
||
|
|
||
|
def get_final_brief(total_vulns_found, total_remediations, ignored, total_ignored, kwargs=None):
|
||
|
if not kwargs:
|
||
|
kwargs = {}
|
||
|
|
||
|
total_vulns = max(0, total_vulns_found - total_ignored)
|
||
|
|
||
|
vuln_text = 'vulnerabilities' if total_ignored > 1 else 'vulnerability'
|
||
|
pkg_text = 'packages were' if len(ignored.keys()) > 1 else 'package was'
|
||
|
|
||
|
policy_file_text = ' using a safety policy file' if is_using_a_safety_policy_file() else ''
|
||
|
|
||
|
vuln_brief = f" {total_vulns} vulnerabilit{'y was' if total_vulns == 1 else 'ies were'} found."
|
||
|
ignored_text = f' {total_ignored} {vuln_text} from {len(ignored.keys())} {pkg_text} ignored.' if ignored else ''
|
||
|
remediation_text = f" {total_remediations} remediation{' was' if total_remediations == 1 else 's were'} " \
|
||
|
f"recommended." if is_using_api_key() else ''
|
||
|
|
||
|
raw_brief = f"Scan was completed{policy_file_text}.{vuln_brief}{ignored_text}{remediation_text}"
|
||
|
|
||
|
return format_long_text(raw_brief, start_line_decorator=' ', **kwargs)
|
||
|
|
||
|
|
||
|
def get_final_brief_license(licenses, kwargs=None):
|
||
|
if not kwargs:
|
||
|
kwargs = {}
|
||
|
|
||
|
licenses_text = ' Scan was completed.'
|
||
|
|
||
|
if licenses:
|
||
|
licenses_text = 'The following software licenses were present in your system: {0}'.format(', '.join(licenses))
|
||
|
|
||
|
return format_long_text("{0}".format(licenses_text), start_line_decorator=' ', **kwargs)
|
||
|
|
||
|
|
||
|
def format_long_text(text, color='', columns=get_terminal_size().columns, start_line_decorator=' ', end_line_decorator=' ', left_padding='', max_lines=None, styling=None, indent='', sub_indent=''):
|
||
|
if not styling:
|
||
|
styling = {}
|
||
|
|
||
|
if color:
|
||
|
styling.update({'fg': color})
|
||
|
|
||
|
columns -= len(start_line_decorator) + len(end_line_decorator)
|
||
|
formatted_lines = []
|
||
|
lines = text.replace('\r', '').splitlines()
|
||
|
|
||
|
for line in lines:
|
||
|
base_format = "{:" + str(columns) + "}"
|
||
|
if line == '':
|
||
|
empty_line = base_format.format(" ")
|
||
|
formatted_lines.append("{0}{1}{2}".format(start_line_decorator, empty_line, end_line_decorator))
|
||
|
wrapped_lines = textwrap.wrap(line, width=columns, max_lines=max_lines, initial_indent=indent, subsequent_indent=sub_indent, placeholder='...')
|
||
|
for wrapped_line in wrapped_lines:
|
||
|
try:
|
||
|
new_line = left_padding + wrapped_line.encode('utf-8')
|
||
|
except TypeError:
|
||
|
new_line = left_padding + wrapped_line
|
||
|
|
||
|
if styling:
|
||
|
new_line = click.style(new_line, **styling)
|
||
|
|
||
|
formatted_lines.append(f"{start_line_decorator}{new_line}{end_line_decorator}")
|
||
|
|
||
|
return "\n".join(formatted_lines)
|
||
|
|
||
|
|
||
|
def get_printable_list_of_scanned_items(scanning_target):
|
||
|
context = SafetyContext()
|
||
|
|
||
|
result = []
|
||
|
scanned_items_data = []
|
||
|
|
||
|
if scanning_target == 'environment':
|
||
|
locations = set([pkg.found for pkg in context.packages if isinstance(pkg, Package)])
|
||
|
|
||
|
for path in locations:
|
||
|
result.append([{'styled': False, 'value': '-> ' + path}])
|
||
|
scanned_items_data.append(path)
|
||
|
|
||
|
if len(locations) <= 0:
|
||
|
msg = 'No locations found in the environment'
|
||
|
result.append([{'styled': False, 'value': msg}])
|
||
|
scanned_items_data.append(msg)
|
||
|
|
||
|
elif scanning_target == 'stdin':
|
||
|
scanned_stdin = [pkg.name for pkg in context.packages if isinstance(pkg, Package)]
|
||
|
value = 'No found packages in stdin'
|
||
|
scanned_items_data = [value]
|
||
|
|
||
|
if len(scanned_stdin) > 0:
|
||
|
value = ', '.join(scanned_stdin)
|
||
|
scanned_items_data = scanned_stdin
|
||
|
|
||
|
result.append(
|
||
|
[{'styled': False, 'value': value}])
|
||
|
|
||
|
elif scanning_target == 'files':
|
||
|
for file in context.params.get('files', []):
|
||
|
result.append([{'styled': False, 'value': f'-> {file.name}'}])
|
||
|
scanned_items_data.append(file.name)
|
||
|
elif scanning_target == 'file':
|
||
|
file = context.params.get('file', None)
|
||
|
name = file.name if file else ''
|
||
|
result.append([{'styled': False, 'value': f'-> {name}'}])
|
||
|
scanned_items_data.append(name)
|
||
|
|
||
|
return result, scanned_items_data
|
||
|
|
||
|
|
||
|
REPORT_HEADING = format_long_text(click.style('REPORT', bold=True))
|
||
|
|
||
|
|
||
|
def build_report_brief_section(columns=None, primary_announcement=None, report_type=1, **kwargs):
|
||
|
if not columns:
|
||
|
columns = get_terminal_size().columns
|
||
|
|
||
|
styled_brief_lines = []
|
||
|
|
||
|
if primary_announcement:
|
||
|
styled_brief_lines.append(
|
||
|
build_primary_announcement(columns=columns, primary_announcement=primary_announcement))
|
||
|
|
||
|
for line in get_report_brief_info(report_type=report_type, **kwargs):
|
||
|
ln = ''
|
||
|
padding = ' ' * 2
|
||
|
|
||
|
for i, words in enumerate(line):
|
||
|
processed_words = words.get('value', '')
|
||
|
if words.get('style', False):
|
||
|
text = ''
|
||
|
if i == 0:
|
||
|
text = padding
|
||
|
padding = ''
|
||
|
text += processed_words
|
||
|
|
||
|
processed_words = click.style(text, bold=True)
|
||
|
|
||
|
ln += processed_words
|
||
|
|
||
|
styled_brief_lines.append(format_long_text(ln, color='', columns=columns, start_line_decorator='',
|
||
|
left_padding=padding, end_line_decorator='', sub_indent=' ' * 2))
|
||
|
|
||
|
return "\n".join([add_empty_line(), REPORT_HEADING, add_empty_line(), '\n'.join(styled_brief_lines)])
|
||
|
|
||
|
|
||
|
def build_report_for_review_vuln_report(as_dict=False):
|
||
|
ctx = SafetyContext()
|
||
|
report_from_file = ctx.review
|
||
|
packages = ctx.packages
|
||
|
|
||
|
if as_dict:
|
||
|
return report_from_file
|
||
|
|
||
|
policy_f_name = report_from_file.get('policy_file', None)
|
||
|
safety_policy_used = []
|
||
|
if policy_f_name:
|
||
|
safety_policy_used = [
|
||
|
{'style': False, 'value': '\nScanning using a security policy file'},
|
||
|
{'style': True, 'value': ' {0}'.format(policy_f_name)},
|
||
|
]
|
||
|
|
||
|
action_executed = [
|
||
|
{'style': True, 'value': 'Scanning dependencies'},
|
||
|
{'style': False, 'value': ' in your '},
|
||
|
{'style': True, 'value': report_from_file.get('scan_target', '-') + ':'},
|
||
|
]
|
||
|
|
||
|
scanned_items = []
|
||
|
|
||
|
for name in report_from_file.get('scanned', []):
|
||
|
scanned_items.append([{'styled': False, 'value': '-> ' + name}])
|
||
|
|
||
|
nl = [{'style': False, 'value': ''}]
|
||
|
using_sentence = build_using_sentence(report_from_file.get('api_key', None),
|
||
|
report_from_file.get('local_database_path_used', None))
|
||
|
scanned_count_sentence = build_scanned_count_sentence(packages)
|
||
|
old_timestamp = report_from_file.get('timestamp', None)
|
||
|
|
||
|
old_timestamp = [{'style': False, 'value': 'Report generated '}, {'style': True, 'value': old_timestamp}]
|
||
|
now = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||
|
current_timestamp = [{'style': False, 'value': 'Timestamp '}, {'style': True, 'value': now}]
|
||
|
|
||
|
brief_info = [[{'style': False, 'value': 'Safety '},
|
||
|
{'style': True, 'value': 'v' + report_from_file.get('safety_version', '-')},
|
||
|
{'style': False, 'value': ' is scanning for '},
|
||
|
{'style': True, 'value': 'Vulnerabilities'},
|
||
|
{'style': True, 'value': '...'}] + safety_policy_used, action_executed
|
||
|
] + [nl] + scanned_items + [nl] + [using_sentence] + [scanned_count_sentence] + [old_timestamp] + \
|
||
|
[current_timestamp]
|
||
|
|
||
|
return brief_info
|
||
|
|
||
|
|
||
|
def build_using_sentence(key, db):
|
||
|
key_sentence = []
|
||
|
custom_integration = os.environ.get('SAFETY_CUSTOM_INTEGRATION',
|
||
|
'false').lower() == 'true'
|
||
|
|
||
|
if key:
|
||
|
key_sentence = [{'style': True, 'value': 'an API KEY'},
|
||
|
{'style': False, 'value': ' and the '}]
|
||
|
db_name = 'PyUp Commercial'
|
||
|
elif db:
|
||
|
if is_a_remote_mirror(db):
|
||
|
if custom_integration:
|
||
|
return []
|
||
|
db_name = f"remote URL {db}"
|
||
|
else:
|
||
|
db_name = f"local file {db}"
|
||
|
else:
|
||
|
db_name = 'non-commercial'
|
||
|
|
||
|
database_sentence = [{'style': True, 'value': db_name + ' database'}]
|
||
|
|
||
|
return [{'style': False, 'value': 'Using '}] + key_sentence + database_sentence
|
||
|
|
||
|
|
||
|
def build_scanned_count_sentence(packages):
|
||
|
scanned_count = 'No packages found'
|
||
|
if len(packages) >= 1:
|
||
|
scanned_count = 'Found and scanned {0} {1}'.format(len(packages),
|
||
|
'packages' if len(packages) > 1 else 'package')
|
||
|
|
||
|
return [{'style': True, 'value': scanned_count}]
|
||
|
|
||
|
|
||
|
def add_warnings_if_needed(brief_info):
|
||
|
ctx = SafetyContext()
|
||
|
warnings = []
|
||
|
|
||
|
if ctx.packages:
|
||
|
if ctx.params.get('continue_on_error', False):
|
||
|
warnings += [[{'style': True,
|
||
|
'value': '* Continue-on-error is enabled, so returning successful (0) exit code in all cases.'}]]
|
||
|
|
||
|
if ctx.params.get('ignore_severity_rules', False) and not is_using_api_key():
|
||
|
warnings += [[{'style': True,
|
||
|
'value': '* Could not filter by severity, please upgrade your account to include severity data.'}]]
|
||
|
|
||
|
if warnings:
|
||
|
brief_info += [[{'style': False, 'value': ''}]] + warnings
|
||
|
|
||
|
|
||
|
def get_report_brief_info(as_dict=False, report_type=1, **kwargs):
|
||
|
LOG.info('get_report_brief_info: %s, %s, %s', as_dict, report_type, kwargs)
|
||
|
|
||
|
context = SafetyContext()
|
||
|
|
||
|
packages = [pkg for pkg in context.packages if isinstance(pkg, Package)]
|
||
|
brief_data = {}
|
||
|
command = context.command
|
||
|
|
||
|
if command == 'review':
|
||
|
review = build_report_for_review_vuln_report(as_dict)
|
||
|
return review
|
||
|
|
||
|
key = context.key
|
||
|
db = context.db_mirror
|
||
|
|
||
|
scanning_types = {'check': {'name': 'Vulnerabilities', 'action': 'Scanning dependencies', 'scanning_target': 'environment'}, # Files, Env or Stdin
|
||
|
'license': {'name': 'Licenses', 'action': 'Scanning licenses', 'scanning_target': 'environment'}, # Files or Env
|
||
|
'review': {'name': 'Report', 'action': 'Reading the report',
|
||
|
'scanning_target': 'file'}} # From file
|
||
|
|
||
|
targets = ['stdin', 'environment', 'files', 'file']
|
||
|
for target in targets:
|
||
|
if context.params.get(target, False):
|
||
|
scanning_types[command]['scanning_target'] = target
|
||
|
break
|
||
|
|
||
|
scanning_target = scanning_types.get(context.command, {}).get('scanning_target', '')
|
||
|
brief_data['scan_target'] = scanning_target
|
||
|
scanned_items, data = get_printable_list_of_scanned_items(scanning_target)
|
||
|
brief_data['scanned'] = data
|
||
|
nl = [{'style': False, 'value': ''}]
|
||
|
|
||
|
action_executed = [
|
||
|
{'style': True, 'value': scanning_types.get(context.command, {}).get('action', '')},
|
||
|
{'style': False, 'value': ' in your '},
|
||
|
{'style': True, 'value': scanning_target + ':'},
|
||
|
]
|
||
|
|
||
|
policy_file = context.params.get('policy_file', None)
|
||
|
safety_policy_used = []
|
||
|
|
||
|
brief_data['policy_file'] = policy_file.get('filename', '-') if policy_file else None
|
||
|
brief_data['policy_file_source'] = 'server' if brief_data['policy_file'] and 'server-safety-policy' in brief_data['policy_file'] else 'local'
|
||
|
|
||
|
if policy_file and policy_file.get('filename', False):
|
||
|
safety_policy_used = [
|
||
|
{'style': False, 'value': '\nScanning using a security policy file'},
|
||
|
{'style': True, 'value': ' {0}'.format(policy_file.get('filename', '-'))},
|
||
|
]
|
||
|
|
||
|
audit_and_monitor = []
|
||
|
if context.params.get('audit_and_monitor'):
|
||
|
logged_url = context.params.get('audit_and_monitor_url') if context.params.get('audit_and_monitor_url') else "https://pyup.io"
|
||
|
audit_and_monitor = [
|
||
|
{'style': False, 'value': '\nLogging scan results to'},
|
||
|
{'style': True, 'value': ' {0}'.format(logged_url)},
|
||
|
]
|
||
|
|
||
|
current_time = str(datetime.now().strftime('%Y-%m-%d %H:%M:%S'))
|
||
|
|
||
|
brief_data['api_key'] = bool(key)
|
||
|
brief_data['local_database_path'] = db if db else None
|
||
|
brief_data['safety_version'] = get_safety_version()
|
||
|
brief_data['timestamp'] = current_time
|
||
|
brief_data['packages_found'] = len(packages)
|
||
|
# Vuln report
|
||
|
additional_data = []
|
||
|
if report_type == 1:
|
||
|
brief_data['vulnerabilities_found'] = kwargs.get('vulnerabilities_found', 0)
|
||
|
brief_data['vulnerabilities_ignored'] = kwargs.get('vulnerabilities_ignored', 0)
|
||
|
brief_data['remediations_recommended'] = 0
|
||
|
|
||
|
additional_data = [
|
||
|
[{'style': True, 'value': str(brief_data['vulnerabilities_found'])},
|
||
|
{'style': True, 'value': f' vulnerabilit{"y" if brief_data["vulnerabilities_found"] == 1 else "ies"} found'}],
|
||
|
[{'style': True, 'value': str(brief_data['vulnerabilities_ignored'])},
|
||
|
{'style': True, 'value': f' vulnerabilit{"y" if brief_data["vulnerabilities_ignored"] == 1 else "ies"} ignored'}],
|
||
|
]
|
||
|
|
||
|
if is_using_api_key():
|
||
|
brief_data['remediations_recommended'] = kwargs.get('remediations_recommended', 0)
|
||
|
additional_data.extend(
|
||
|
[[{'style': True, 'value': str(brief_data['remediations_recommended'])},
|
||
|
{'style': True, 'value':
|
||
|
f' remediation{"" if brief_data["remediations_recommended"] == 1 else "s"} recommended'}]])
|
||
|
|
||
|
elif report_type == 2:
|
||
|
brief_data['licenses_found'] = kwargs.get('licenses_found', 0)
|
||
|
additional_data = [
|
||
|
[{'style': True, 'value': str(brief_data['licenses_found'])},
|
||
|
{'style': True, 'value': f' license {"type" if brief_data["licenses_found"] == 1 else "types"} found'}],
|
||
|
]
|
||
|
|
||
|
brief_data['telemetry'] = build_telemetry_data()
|
||
|
|
||
|
brief_data['git'] = build_git_data()
|
||
|
brief_data['project'] = context.params.get('project', None)
|
||
|
|
||
|
brief_data['json_version'] = 1
|
||
|
|
||
|
using_sentence = build_using_sentence(key, db)
|
||
|
using_sentence_section = [nl] if not using_sentence else [nl] + [build_using_sentence(key, db)]
|
||
|
scanned_count_sentence = build_scanned_count_sentence(packages)
|
||
|
|
||
|
timestamp = [{'style': False, 'value': 'Timestamp '}, {'style': True, 'value': current_time}]
|
||
|
|
||
|
brief_info = [[{'style': False, 'value': 'Safety '},
|
||
|
{'style': True, 'value': 'v' + get_safety_version()},
|
||
|
{'style': False, 'value': ' is scanning for '},
|
||
|
{'style': True, 'value': scanning_types.get(context.command, {}).get('name', '')},
|
||
|
{'style': True, 'value': '...'}] + safety_policy_used + audit_and_monitor, action_executed
|
||
|
] + [nl] + scanned_items + using_sentence_section + [scanned_count_sentence] + [timestamp]
|
||
|
|
||
|
brief_info.extend(additional_data)
|
||
|
|
||
|
add_warnings_if_needed(brief_info)
|
||
|
|
||
|
LOG.info('Brief info data: %s', brief_data)
|
||
|
LOG.info('Brief info, styled output: %s', '\n\n LINE ---->\n ' + '\n\n LINE ---->\n '.join(map(str, brief_info)))
|
||
|
|
||
|
return brief_data if as_dict else brief_info
|
||
|
|
||
|
|
||
|
def build_primary_announcement(primary_announcement, columns=None, only_text=False):
|
||
|
lines = json.loads(primary_announcement.get('message'))
|
||
|
|
||
|
for line in lines:
|
||
|
if 'words' not in line:
|
||
|
raise ValueError('Missing words keyword')
|
||
|
if len(line['words']) <= 0:
|
||
|
raise ValueError('No words in this line')
|
||
|
for word in line['words']:
|
||
|
if 'value' not in word or not word['value']:
|
||
|
raise ValueError('Empty word or without value')
|
||
|
|
||
|
message = style_lines(lines, columns, start_line='', end_line='')
|
||
|
|
||
|
return click.unstyle(message) if only_text else message
|
||
|
|
||
|
|
||
|
def is_using_api_key():
|
||
|
return bool(SafetyContext().key)
|
||
|
|
||
|
|
||
|
def is_using_a_safety_policy_file():
|
||
|
return bool(SafetyContext().params.get('policy_file', None))
|
||
|
|
||
|
|
||
|
def should_add_nl(output, found_vulns):
|
||
|
if output == 'bare' and not found_vulns:
|
||
|
return False
|
||
|
|
||
|
return True
|
||
|
|