Files
Hotel-Booking/Backend/venv/lib/python3.12/site-packages/safety/cli.py
Iliyan Angelov 62c1fe5951 updates
2025-12-01 06:50:10 +02:00

1354 lines
41 KiB
Python

# -*- coding: utf-8 -*-
# type: ignore
from __future__ import absolute_import
import configparser
import json
import logging
import os
import platform
import sys
from dataclasses import asdict
from datetime import date, datetime, timedelta
from enum import Enum
from functools import wraps
from pathlib import Path
import click
import typer
import typer.rich_utils
from packaging import version as packaging_version
from packaging.version import InvalidVersion
from safety_schemas.config.schemas.v3_0 import main as v3_0
from safety_schemas.models import (
ConfigModel,
Ecosystem,
Stage,
VulnerabilitySeverityLabels,
)
from safety.alerts import alert
from safety.auth import auth_options, proxy_options
from safety.auth.cli import auth_app
from safety.auth.models import Organization
from safety.decorators import notify
from safety.codebase.command import codebase_app
from safety.console import main_console as console
from safety.constants import (
BAR_LINE,
CONFIG_FILE_SYSTEM,
CONFIG_FILE_USER,
CONTEXT_COMMAND_TYPE,
EXIT_CODE_FAILURE,
EXIT_CODE_OK,
EXIT_CODE_VULNERABILITIES_FOUND,
CLI_MAIN_INTRODUCTION,
DEFAULT_EPILOG,
)
from safety.error_handlers import handle_cmd_exception, output_exception
from safety.errors import InvalidCredentialError, SafetyError, SafetyException
from safety.firewall.command import firewall_app
from safety.formatter import SafetyFormatter
from safety.init.command import init_app
from safety.meta import get_version
from safety.output_utils import should_add_nl
from safety.tool import tool_commands
from safety.scan.command import scan_project_app, scan_system_app
from safety.scan.constants import (
CLI_CHECK_COMMAND_HELP,
CLI_CHECK_UPDATES_HELP,
CLI_CONFIGURE_HELP,
CLI_CONFIGURE_ORGANIZATION_ID,
CLI_CONFIGURE_ORGANIZATION_NAME,
CLI_CONFIGURE_PROXY_HOST_HELP,
CLI_CONFIGURE_PROXY_PORT_HELP,
CLI_CONFIGURE_PROXY_PROTOCOL_HELP,
CLI_CONFIGURE_PROXY_REQUIRED,
CLI_CONFIGURE_PROXY_TIMEOUT,
CLI_CONFIGURE_SAVE_TO_SYSTEM,
CLI_DEBUG_HELP,
CLI_DISABLE_OPTIONAL_TELEMETRY_DATA_HELP,
CLI_GENERATE_HELP,
CLI_GENERATE_MINIMUM_CVSS_SEVERITY,
CLI_GENERATE_PATH,
CLI_LICENSES_COMMAND_HELP,
DEFAULT_SPINNER,
)
from safety.scan.finder import FileFinder
from safety.scan.main import process_files
from safety.util import (
DependentOption,
MutuallyExclusiveOption,
SafetyContext,
SafetyPolicyFile,
active_color_if_needed,
bare_alias,
filter_announcements,
get_fix_options,
get_packages_licenses,
get_processed_options,
html_alias,
initialize_config_dirs,
initialize_event_bus,
is_a_remote_mirror,
json_alias,
transform_ignore,
)
from .cli_util import (
CommandType,
SafetyCLICommand,
SafetyCLILegacyCommand,
SafetyCLILegacyGroup,
SafetyCLISubGroup,
)
from safety import safety as safety_core
try:
from typing import Annotated, Optional
except ImportError:
from typing_extensions import Annotated, Optional
import safety.asyncio_patch # noqa: F401
LOG = logging.getLogger(__name__)
def preprocess_args(f):
if "--debug" in sys.argv:
index = sys.argv.index("--debug")
if len(sys.argv) > index + 1:
next_arg = sys.argv[index + 1]
if next_arg in ("1", "true"):
sys.argv.pop(index + 1) # Remove the next argument (1 or true)
return f
def configure_logger(ctx, param, debug):
level = logging.CRITICAL
if debug:
level = logging.DEBUG
logging.basicConfig(format="%(asctime)s %(name)s => %(message)s", level=level)
if debug:
# Log the contents of the config.ini file
config = configparser.ConfigParser()
config.read(CONFIG_FILE_USER)
LOG.debug("Config file contents:")
for section in config.sections():
LOG.debug("[%s]", section)
for key, value in config.items(section):
LOG.debug("%s = %s", key, value)
# Log the proxy settings if they were attempted
if "proxy" in config:
LOG.debug(
"Proxy configuration attempted with settings: %s", dict(config["proxy"])
)
@click.group(
cls=SafetyCLILegacyGroup, help=CLI_MAIN_INTRODUCTION, epilog=DEFAULT_EPILOG
)
@auth_options()
@proxy_options
@click.option(
"--disable-optional-telemetry",
default=False,
is_flag=True,
show_default=True,
help=CLI_DISABLE_OPTIONAL_TELEMETRY_DATA_HELP,
)
@click.option("--debug", is_flag=True, help=CLI_DEBUG_HELP, callback=configure_logger)
@click.version_option(version=get_version())
@click.pass_context
@preprocess_args
def cli(ctx, debug, disable_optional_telemetry):
"""
Scan and secure Python projects against package vulnerabilities. To get started navigate to a Python project and run `safety scan`.
"""
SafetyContext().safety_source = "cli"
telemetry = not disable_optional_telemetry
ctx.obj.config = ConfigModel(telemetry_enabled=telemetry)
level = logging.CRITICAL
if debug:
level = logging.DEBUG
logging.basicConfig(format="%(asctime)s %(name)s => %(message)s", level=level)
LOG.info(f"Telemetry enabled: {ctx.obj.config.telemetry_enabled}")
# Before any command make sure that the parent dirs for Safety config are present.
initialize_config_dirs()
initialize_event_bus(ctx=ctx)
def clean_check_command(f):
"""
Main entry point for validation.
"""
@wraps(f)
def inner(ctx, *args, **kwargs):
save_json = kwargs["save_json"]
output = kwargs["output"]
authenticated: bool = ctx.obj.auth.client.is_using_auth_credentials()
files = kwargs["files"]
policy_file = kwargs["policy_file"]
auto_remediation_limit = kwargs["auto_remediation_limit"]
audit_and_monitor = kwargs["audit_and_monitor"]
exit_code = kwargs["exit_code"]
# This is handled in the custom subgroup Click class
# TODO: Remove this soon, for now it keeps a legacy behavior
kwargs.pop("key", None)
kwargs.pop("proxy_protocol", None)
kwargs.pop("proxy_host", None)
kwargs.pop("proxy_port", None)
if ctx.get_parameter_source(
"json_version"
) != click.core.ParameterSource.DEFAULT and not (
save_json or json or output == "json"
):
raise click.UsageError(
"Illegal usage: `--json-version` only works with JSON related outputs."
)
try:
if (
ctx.get_parameter_source("apply_remediations")
!= click.core.ParameterSource.DEFAULT
):
if not authenticated:
raise InvalidCredentialError(
message="The --apply-security-updates option needs authentication. See {link}."
)
if not files:
raise SafetyError(
message='--apply-security-updates only works with files; use the "-r" option to '
"specify files to remediate."
)
auto_remediation_limit = get_fix_options(
policy_file, auto_remediation_limit
)
policy_file, server_audit_and_monitor = safety_core.get_server_policies(
ctx.obj.auth.client, policy_file=policy_file, proxy_dictionary=None
)
audit_and_monitor = audit_and_monitor and server_audit_and_monitor
kwargs.update(
{
"auto_remediation_limit": auto_remediation_limit,
"policy_file": policy_file,
"audit_and_monitor": audit_and_monitor,
}
)
except SafetyError as e:
LOG.exception("Expected SafetyError happened: %s", e)
output_exception(e, exit_code_output=exit_code)
except Exception as e:
LOG.exception("Unexpected Exception happened: %s", e)
exception = e if isinstance(e, SafetyException) else SafetyException(info=e)
output_exception(exception, exit_code_output=exit_code)
return f(ctx, *args, **kwargs)
return inner
def print_deprecation_message(
old_command: str, deprecation_date: datetime, new_command: Optional[str] = None
) -> None:
"""
Print a formatted deprecation message for a command.
This function uses the click library to output a visually distinct
message in the console, warning users about the deprecation of a
specified command. It includes information about the deprecation date
and suggests an alternative command to use, if provided.
The message is formatted with colors and styles for emphasis:
- Yellow for the border and general information
- Red for the 'DEPRECATED' label
- Green for the suggestion of the new command (if provided)
Parameters:
- old_command (str): The name of the deprecated command.
- deprecation_date (datetime): The date when the command will no longer be supported.
- new_command (str, optional): The name of the alternative command to suggest. Default is None.
"""
click.echo("\n")
click.echo(click.style(BAR_LINE, fg="yellow", bold=True))
click.echo("\n")
click.echo(
click.style("DEPRECATED: ", fg="red", bold=True)
+ click.style(
f"this command (`{old_command}`) has been DEPRECATED, and will be unsupported beyond {deprecation_date.strftime('%d %B %Y')}.",
fg="yellow",
bold=True,
)
)
if new_command:
click.echo("\n")
click.echo(
click.style("We highly encourage switching to the new ", fg="green")
+ click.style(f"`{new_command}`", fg="green", bold=True)
+ click.style(
" command which is easier to use, more powerful, and can be set up to mimic the deprecated command if required.",
fg="green",
)
)
click.echo("\n")
click.echo(click.style(BAR_LINE, fg="yellow", bold=True))
click.echo("\n")
@cli.command(
cls=SafetyCLILegacyCommand,
context_settings={CONTEXT_COMMAND_TYPE: CommandType.UTILITY},
help=CLI_CHECK_COMMAND_HELP,
)
@proxy_options
@auth_options(stage=False)
@click.option(
"--db",
default="",
help="Path to a local or remote vulnerability database. Default: empty",
)
@click.option(
"--full-report/--short-report",
default=False,
cls=MutuallyExclusiveOption,
mutually_exclusive=["output", "json", "bare"],
with_values={
"output": ["json", "bare"],
"json": [True, False],
"html": [True, False],
"bare": [True, False],
},
help="Full reports include a security advisory (if available). Default: --short-report",
)
@click.option(
"--cache",
is_flag=False,
flag_value=60,
default=0,
help="Cache requests to the vulnerability database locally. Default: 0 seconds",
hidden=True,
)
@click.option(
"--stdin",
default=False,
cls=MutuallyExclusiveOption,
mutually_exclusive=["files"],
help="Read input from stdin.",
is_flag=True,
show_default=True,
)
@click.option(
"files",
"--file",
"-r",
multiple=True,
type=click.File(),
cls=MutuallyExclusiveOption,
mutually_exclusive=["stdin"],
help="Read input from one (or multiple) requirement files. Default: empty",
)
@click.option(
"--ignore",
"-i",
multiple=True,
type=str,
default=[],
callback=transform_ignore,
help="Ignore one (or multiple) vulnerabilities by ID (coma separated). Default: empty",
)
@click.option(
"ignore_unpinned_requirements",
"--ignore-unpinned-requirements/--check-unpinned-requirements",
"-iur",
default=None,
help="Check or ignore unpinned requirements found.",
)
@click.option(
"--json",
default=False,
cls=MutuallyExclusiveOption,
mutually_exclusive=["output", "bare"],
with_values={
"output": ["screen", "text", "bare", "json", "html"],
"bare": [True, False],
},
callback=json_alias,
hidden=True,
is_flag=True,
show_default=True,
)
@click.option(
"--html",
default=False,
cls=MutuallyExclusiveOption,
mutually_exclusive=["output", "bare"],
with_values={
"output": ["screen", "text", "bare", "json", "html"],
"bare": [True, False],
},
callback=html_alias,
hidden=True,
is_flag=True,
show_default=True,
)
@click.option(
"--bare",
default=False,
cls=MutuallyExclusiveOption,
mutually_exclusive=["output", "json"],
with_values={"output": ["screen", "text", "bare", "json"], "json": [True, False]},
callback=bare_alias,
hidden=True,
is_flag=True,
show_default=True,
)
@click.option(
"--output",
"-o",
type=click.Choice(["screen", "text", "json", "bare", "html"], case_sensitive=False),
default="screen",
callback=active_color_if_needed,
envvar="SAFETY_OUTPUT",
)
@click.option(
"--exit-code/--continue-on-error",
default=True,
help="Output standard exit codes. Default: --exit-code",
)
@click.option(
"--policy-file",
type=SafetyPolicyFile(),
default=".safety-policy.yml",
help="Define the policy file to be used",
)
@click.option(
"--audit-and-monitor/--disable-audit-and-monitor",
default=True,
help="Send results back to safetycli.com for viewing on your dashboard. Requires an API key.",
)
@click.option(
"project",
"--project-id",
"--project",
default=None,
help="Project to associate this scan with on safetycli.com. "
"Defaults to a canonicalized github style name if available, otherwise unknown",
)
@click.option(
"--save-json",
default="",
help="Path to where the output file will be placed; if the path is a"
" directory, Safety will use safety-report.json as filename."
" Default: empty",
)
@click.option(
"--save-html",
default="",
help="Path to where the output file will be placed; if the path is a"
" directory, Safety will use safety-report.html as the main file. "
"Default: empty",
)
@click.option(
"apply_remediations",
"--apply-security-updates",
"-asu",
default=False,
is_flag=True,
help="Apply security updates in your requirement files.",
)
@click.option(
"auto_remediation_limit",
"--auto-security-updates-limit",
"-asul",
multiple=True,
type=click.Choice(["patch", "minor", "major"]),
default=["patch"],
help="Define the limit to be used for automatic security updates in your requirement files."
" Default: patch",
)
@click.option(
"no_prompt",
"--no-prompt",
"-np",
default=False,
help="Safety won't ask for remediations outside of the remediation limit.",
is_flag=True,
show_default=True,
)
@click.option(
"json_version",
"--json-output-format",
type=click.Choice(["0.5", "1.1"]),
default="1.1",
help="Select the JSON version to be used in the output",
show_default=True,
)
@click.pass_context
@clean_check_command
@handle_cmd_exception
@notify
def check(
ctx,
db,
full_report,
stdin,
files,
cache,
ignore,
ignore_unpinned_requirements,
output,
json,
html,
bare,
exit_code,
policy_file,
audit_and_monitor,
project,
save_json,
save_html,
apply_remediations,
auto_remediation_limit,
no_prompt,
json_version,
):
"""
[underline][DEPRECATED][/underline] `check` has been replaced by the `scan` command, and will be unsupported beyond 1 June 2024.Find vulnerabilities at a target file or enviroment.
"""
LOG.info("Running check command")
non_interactive = (
not sys.stdout.isatty()
and os.environ.get("SAFETY_OS_DESCRIPTION", None) != "run"
)
silent_outputs = ["json", "bare", "html"]
is_silent_output = output in silent_outputs
prompt_mode = (
bool(not non_interactive and not stdin and not is_silent_output)
and not no_prompt
)
kwargs = {"version": json_version} if output == "json" else {}
print_deprecation_message("check", date(2024, 6, 1), new_command="scan")
# try:
packages = safety_core.get_packages(files, stdin)
ignore_severity_rules = None
ignore, ignore_severity_rules, exit_code, ignore_unpinned_requirements, project = (
get_processed_options(
policy_file,
ignore,
ignore_severity_rules,
exit_code,
ignore_unpinned_requirements,
project,
)
)
is_env_scan = not stdin and not files
params = {
"stdin": stdin,
"files": files,
"policy_file": policy_file,
"continue_on_error": not exit_code,
"ignore_severity_rules": ignore_severity_rules,
"project": project,
"audit_and_monitor": audit_and_monitor,
"prompt_mode": prompt_mode,
"auto_remediation_limit": auto_remediation_limit,
"apply_remediations": apply_remediations,
"ignore_unpinned_requirements": ignore_unpinned_requirements,
}
LOG.info("Calling the check function")
vulns, db_full = safety_core.check(
session=ctx.obj.auth.client,
packages=packages,
db_mirror=db,
cached=cache,
ignore_vulns=ignore,
ignore_severity_rules=ignore_severity_rules,
proxy=None,
include_ignored=True,
is_env_scan=is_env_scan,
telemetry=ctx.obj.config.telemetry_enabled,
params=params,
)
LOG.debug("Vulnerabilities returned: %s", vulns)
LOG.debug("full database returned is None: %s", db_full is None)
LOG.info("Safety is going to calculate remediations")
remediations = safety_core.calculate_remediations(vulns, db_full)
announcements = []
if not db or is_a_remote_mirror(db):
LOG.info("Not local DB used, Getting announcements")
announcements = safety_core.get_announcements(
ctx.obj.auth.client, telemetry=ctx.obj.config.telemetry_enabled
)
announcements.extend(
safety_core.add_local_notifications(packages, ignore_unpinned_requirements)
)
LOG.info(
"Safety is going to render the vulnerabilities report using %s output", output
)
fixes = []
if apply_remediations and is_silent_output:
# it runs and apply only automatic fixes.
fixes = safety_core.process_fixes(
files,
remediations,
auto_remediation_limit,
output,
no_output=True,
prompt=False,
)
output_report = SafetyFormatter(output, **kwargs).render_vulnerabilities(
announcements, vulns, remediations, full_report, packages, fixes
)
# Announcements are send to stderr if not terminal, it doesn't depend on "exit_code" value
stderr_announcements = filter_announcements(
announcements=announcements, by_type="error"
)
if stderr_announcements and non_interactive:
LOG.info(
"sys.stdout is not a tty, error announcements are going to be send to stderr"
)
click.secho(
SafetyFormatter(output="text").render_announcements(stderr_announcements),
fg="red",
file=sys.stderr,
)
found_vulns = list(filter(lambda v: not v.ignored, vulns))
LOG.info("Vulnerabilities found (Not ignored): %s", len(found_vulns))
LOG.info("All vulnerabilities found (ignored and Not ignored): %s", len(vulns))
click.secho(output_report, nl=should_add_nl(output, found_vulns), file=sys.stdout)
post_processing_report = save_json or audit_and_monitor or apply_remediations
if post_processing_report:
if apply_remediations and not is_silent_output:
# prompt_mode fixing after main check output if prompt is enabled.
fixes = safety_core.process_fixes(
files,
remediations,
auto_remediation_limit,
output,
no_output=False,
prompt=prompt_mode,
)
# Render fixes
json_report = (
output_report
if output == "json"
else SafetyFormatter(
output="json", version=json_version
).render_vulnerabilities(
announcements, vulns, remediations, full_report, packages, fixes
)
)
safety_core.save_report(save_json, "safety-report.json", json_report)
if save_html:
html_report = (
output_report
if output == "html"
else SafetyFormatter(output="html").render_vulnerabilities(
announcements, vulns, remediations, full_report, packages, fixes
)
)
safety_core.save_report(save_html, "safety-report.html", html_report)
print_deprecation_message("check", date(2024, 6, 1), new_command="scan")
if exit_code and found_vulns:
LOG.info("Exiting with default code for vulnerabilities found")
sys.exit(EXIT_CODE_VULNERABILITIES_FOUND)
sys.exit(EXIT_CODE_OK)
def clean_license_command(f):
"""
Main entry point for validation.
"""
@wraps(f)
def inner(ctx, *args, **kwargs):
# TODO: Remove this soon, for now it keeps a legacy behavior
kwargs.pop("key", None)
kwargs.pop("proxy_protocol", None)
kwargs.pop("proxy_host", None)
kwargs.pop("proxy_port", None)
return f(ctx, *args, **kwargs)
return inner
@cli.command(
cls=SafetyCLILegacyCommand,
context_settings={CONTEXT_COMMAND_TYPE: CommandType.UTILITY},
help=CLI_LICENSES_COMMAND_HELP,
)
@proxy_options
@auth_options(stage=False)
@click.option(
"--db", default="", help="Path to a local license database. Default: empty"
)
@click.option(
"--output",
"-o",
type=click.Choice(["screen", "text", "json", "bare"], case_sensitive=False),
default="screen",
)
@click.option(
"--cache",
default=0,
help="Whether license database file should be cached.Default: 0 seconds",
)
@click.option(
"files",
"--file",
"-r",
multiple=True,
type=click.File(),
help="Read input from one (or multiple) requirement files. Default: empty",
)
@click.pass_context
@clean_license_command
@handle_cmd_exception
@notify
def license(ctx, db, output, cache, files):
"""
Find the open source licenses used by your Python dependencies.
"""
print_deprecation_message("license", date(2024, 6, 1), new_command=None)
LOG.info("Running license command")
packages = safety_core.get_packages(files, False)
licenses_db = {}
SafetyContext().params = ctx.params
licenses_db = safety_core.get_licenses(
session=ctx.obj.auth.client,
db_mirror=db,
cached=cache,
telemetry=ctx.obj.config.telemetry_enabled,
)
filtered_packages_licenses = get_packages_licenses(
packages=packages, licenses_db=licenses_db
)
announcements = []
if not db:
announcements = safety_core.get_announcements(
session=ctx.obj.auth.client, telemetry=ctx.obj.config.telemetry_enabled
)
output_report = SafetyFormatter(output=output).render_licenses(
announcements, filtered_packages_licenses
)
click.secho(output_report, nl=True)
print_deprecation_message("license", date(2024, 6, 1), new_command=None)
@cli.command(
cls=SafetyCLILegacyCommand,
context_settings={CONTEXT_COMMAND_TYPE: CommandType.UTILITY},
help=CLI_GENERATE_HELP,
)
@click.option("--path", default=".", help=CLI_GENERATE_PATH)
@click.option(
"--minimum-cvss-severity",
default="critical",
help=CLI_GENERATE_MINIMUM_CVSS_SEVERITY,
)
@click.argument("name", required=True)
@click.pass_context
@handle_cmd_exception
@notify
def generate(ctx, name, path, minimum_cvss_severity):
"""Create a boilerplate Safety CLI policy file
NAME is the name of the file type to generate. Valid values are: policy_file
"""
if name != "policy_file" and name != "installation_policy":
click.secho(
f'This Safety version only supports "policy_file" generation. "{name}" is not supported.',
fg="red",
file=sys.stderr,
)
sys.exit(EXIT_CODE_FAILURE)
LOG.info("Running generate %s", name)
if name == "policy_file":
generate_policy_file(name, path)
elif name == "installation_policy":
generate_installation_policy(ctx, name, path, minimum_cvss_severity)
def generate_installation_policy(ctx, name, path, minimum_cvss_severity):
all_severities = [severity.name.lower() for severity in VulnerabilitySeverityLabels]
policy_severities = all_severities[
all_severities.index(minimum_cvss_severity.lower()) :
]
policy_severities_set = set(policy_severities[:])
target = path
ecosystems = [Ecosystem.PYTHON]
to_include = {
file_type: paths
for file_type, paths in ctx.obj.config.scan.include_files.items()
if file_type.ecosystem in ecosystems
}
# Initialize file finder
file_finder = FileFinder(
target=target,
ecosystems=ecosystems,
max_level=ctx.obj.config.scan.max_depth,
exclude=ctx.obj.config.scan.ignore,
include_files=to_include,
)
for handler in file_finder.handlers:
if handler.ecosystem:
wait_msg = "Fetching Safety's vulnerability database..."
with console.status(wait_msg, spinner=DEFAULT_SPINNER):
handler.download_required_assets(ctx.obj.auth.client)
wait_msg = "Scanning project directory"
with console.status(wait_msg, spinner=DEFAULT_SPINNER):
path, file_paths = file_finder.search()
target_ecosystems = ", ".join([member.value for member in ecosystems])
wait_msg = (
f"Analyzing {target_ecosystems} files and environments for security findings"
)
config = ctx.obj.config
vulnerabilities = []
with console.status(wait_msg, spinner=DEFAULT_SPINNER):
for path, analyzed_file in process_files(paths=file_paths, config=config):
affected_specifications = (
analyzed_file.dependency_results.get_affected_specifications()
)
if any(affected_specifications):
for spec in affected_specifications:
for vuln in spec.vulnerabilities:
if (
vuln.severity
and vuln.severity.cvssv3
and vuln.severity.cvssv3.get(
"base_severity", "none"
).lower()
in policy_severities_set
):
vulnerabilities.append(vuln)
policy = v3_0.Config(
installation=v3_0.Installation(
default_action=v3_0.InstallationAction.ALLOW,
allow=v3_0.AllowedInstallation(
packages=None,
vulnerabilities={
vuln.vulnerability_id: v3_0.IgnoredVulnerability(
reason=f"Autogenerated policy for {vuln.package_name} package.",
expires=date.today() + timedelta(days=90),
)
for vuln in vulnerabilities
},
),
deny=v3_0.DeniedInstallation(
packages=None,
vulnerabilities=v3_0.DeniedVulnerability(
block_on_any_of=v3_0.DeniedVulnerabilityCriteria(
cvss_severity=policy_severities
)
),
),
)
)
click.secho(policy.json(by_alias=True, exclude_none=True, indent=4))
def generate_policy_file(name, path):
path = Path(path)
if not path.exists():
click.secho(f'The path "{path}" does not exist.', fg="red", file=sys.stderr)
sys.exit(EXIT_CODE_FAILURE)
policy = path / ".safety-policy.yml"
default_config = ConfigModel()
try:
default_config.save_policy_file(policy)
LOG.debug("Safety created the policy file.")
msg = (
f"A default Safety policy file has been generated! Review the file contents in the path {path} in the "
"file: .safety-policy.yml"
)
click.secho(msg, fg="green")
except Exception as exc:
if isinstance(exc, OSError):
LOG.debug("Unable to generate %s because: %s", name, exc.errno)
click.secho(f"{str(exc)} error.", fg="red", file=sys.stderr)
sys.exit(EXIT_CODE_FAILURE)
@cli.command(
cls=SafetyCLILegacyCommand,
context_settings={CONTEXT_COMMAND_TYPE: CommandType.UTILITY},
)
@click.option(
"--path",
default=".safety-policy.yml",
help="Path where the generated file will be saved. Default: current directory",
)
@click.argument("name")
@click.argument("version", required=False)
@click.pass_context
@handle_cmd_exception
@notify
def validate(ctx, name, version, path):
"""Verify that a local policy file is valid. NAME is the name of the file type to validate. Valid values are: policy_file"""
if name != "policy_file":
click.secho(
f'This Safety version only supports "policy_file" validation. "{name}" is not supported.',
fg="red",
file=sys.stderr,
)
sys.exit(EXIT_CODE_FAILURE)
LOG.info("Running validate %s", name)
if not os.path.exists(path):
click.secho(f'The path "{path}" does not exist.', fg="red", file=sys.stderr)
sys.exit(EXIT_CODE_FAILURE)
if version not in ["3.0", "2.0", None]:
click.secho(
f'Version "{version}" is not a valid value, allowed values are 3.0 and 2.0. Use --path to specify the target file.',
fg="red",
file=sys.stderr,
)
sys.exit(EXIT_CODE_FAILURE)
def fail_validation(e):
click.secho(str(e).lstrip(), fg="red", file=sys.stderr)
sys.exit(EXIT_CODE_FAILURE)
if not version:
version = "3.0"
result = ""
if version == "3.0":
policy = None
try:
from .scan.main import load_policy_file
policy = load_policy_file(Path(path))
except Exception as e:
fail_validation(e)
click.secho(
f"The Safety policy ({version}) file "
"(Used for scan and system-scan commands) "
"was successfully parsed "
"with the following values:",
fg="green",
)
if policy and policy.config:
result = policy.config.as_v30().json()
else:
try:
values = SafetyPolicyFile().convert(path, None, None)
except Exception as e:
click.secho(str(e).lstrip(), fg="red", file=sys.stderr)
sys.exit(EXIT_CODE_FAILURE)
del values["raw"]
result = json.dumps(values, indent=4, default=str)
click.secho(
"The Safety policy file "
"(Valid only for the check command) "
"was successfully parsed with the "
"following values:",
fg="green",
)
console.print_json(result)
@cli.command(
cls=SafetyCLILegacyCommand,
help=CLI_CONFIGURE_HELP,
context_settings={CONTEXT_COMMAND_TYPE: CommandType.UTILITY},
)
@click.option(
"--proxy-protocol",
"-pr",
type=click.Choice(["http", "https"]),
default="https",
cls=DependentOption,
required_options=["proxy_host"],
help=CLI_CONFIGURE_PROXY_PROTOCOL_HELP,
)
@click.option(
"--proxy-host",
"-ph",
multiple=False,
type=str,
default=None,
help=CLI_CONFIGURE_PROXY_HOST_HELP,
)
@click.option(
"--proxy-port",
"-pp",
multiple=False,
type=int,
default=80,
cls=DependentOption,
required_options=["proxy_host"],
help=CLI_CONFIGURE_PROXY_PORT_HELP,
)
@click.option(
"--proxy-timeout",
"-pt",
multiple=False,
type=int,
default=None,
help=CLI_CONFIGURE_PROXY_TIMEOUT,
)
@click.option("--proxy-required", default=False, help=CLI_CONFIGURE_PROXY_REQUIRED)
@click.option(
"--organization-id",
"-org-id",
multiple=False,
default=None,
cls=DependentOption,
required_options=["organization_name"],
help=CLI_CONFIGURE_ORGANIZATION_ID,
)
@click.option(
"--organization-name",
"-org-name",
multiple=False,
default=None,
cls=DependentOption,
required_options=["organization_id"],
help=CLI_CONFIGURE_ORGANIZATION_NAME,
)
@click.option(
"--stage",
"-stg",
multiple=False,
default=Stage.development.value,
type=click.Choice([stage.value for stage in Stage]),
help="The project development stage to be tied to the current device.",
)
@click.option(
"--save-to-system/--save-to-user",
default=False,
is_flag=True,
help=CLI_CONFIGURE_SAVE_TO_SYSTEM,
)
@click.pass_context
@handle_cmd_exception
@notify
def configure(
ctx,
proxy_protocol,
proxy_host,
proxy_port,
proxy_timeout,
proxy_required,
organization_id,
organization_name,
stage,
save_to_system,
):
"""
Configure global settings, like proxy settings and organization details
"""
config = configparser.ConfigParser()
if save_to_system:
if not CONFIG_FILE_SYSTEM:
click.secho(
"Unable to determine the system wide config path. You can set the SAFETY_SYSTEM_CONFIG_PATH env var"
)
sys.exit(1)
CONFIG_FILE = CONFIG_FILE_SYSTEM
else:
CONFIG_FILE = CONFIG_FILE_USER
config.read(CONFIG_FILE)
PROXY_SECTION_NAME: str = "proxy"
PROXY_TIMEOUT_KEY: str = "timeout"
PROXY_REQUIRED_KEY: str = "required"
if organization_id:
config["organization"] = asdict(
Organization(id=organization_id, name=organization_name)
)
DEFAULT_PROXY_TIMEOUT: int = 500
if not proxy_timeout:
try:
proxy_timeout = int(config["proxy"]["timeout"])
except Exception:
proxy_timeout = DEFAULT_PROXY_TIMEOUT
new_proxy_config = {}
new_proxy_config.setdefault(PROXY_TIMEOUT_KEY, str(proxy_timeout))
new_proxy_config.setdefault(PROXY_REQUIRED_KEY, str(proxy_required))
if proxy_host:
new_proxy_config.update(
{"protocol": proxy_protocol, "host": proxy_host, "port": str(proxy_port)}
)
if not config.has_section(PROXY_SECTION_NAME):
config.add_section(PROXY_SECTION_NAME)
proxy_config = dict(config.items(PROXY_SECTION_NAME))
proxy_config.update(new_proxy_config)
for key, value in proxy_config.items():
config.set(PROXY_SECTION_NAME, key, value)
if stage:
config["host"] = {"stage": "development" if stage == "dev" else stage}
try:
with open(CONFIG_FILE, "w") as configfile:
config.write(configfile)
except Exception as e:
if (
isinstance(e, OSError) and e.errno == 2 or e is PermissionError
) and save_to_system:
click.secho(
"Unable to save the configuration: writing to system-wide Safety configuration file requires admin privileges"
)
else:
click.secho(f"Unable to save the configuration, error: {e}")
sys.exit(1)
cli_app = typer.Typer(rich_markup_mode="rich", cls=SafetyCLISubGroup)
typer.rich_utils.STYLE_HELPTEXT = ""
def print_check_updates_header(console):
VERSION = get_version()
console.print(
f"Safety {VERSION} checking for Safety version and configuration updates:"
)
class Output(str, Enum):
SCREEN = "screen"
JSON = "json"
@cli_app.command(
cls=SafetyCLICommand,
help=CLI_CHECK_UPDATES_HELP,
name="check-updates",
epilog=DEFAULT_EPILOG,
context_settings={
"allow_extra_args": True,
"ignore_unknown_options": True,
CONTEXT_COMMAND_TYPE: CommandType.UTILITY,
},
)
@handle_cmd_exception
@notify
def check_updates(
ctx: typer.Context,
version: Annotated[
int,
typer.Option(min=1),
] = 1,
output: Annotated[
Output, typer.Option(help="The main output generated by Safety CLI.")
] = Output.SCREEN,
):
"""
Check for Safety CLI version updates
"""
if output is Output.JSON:
console.quiet = True
print_check_updates_header(console)
wait_msg = "Authenticating and checking for Safety CLI updates"
VERSION = get_version()
PYTHON_VERSION = platform.python_version()
OS_TYPE = platform.system()
authenticated = ctx.obj.auth.client.is_using_auth_credentials()
data = None
console.print()
with console.status(wait_msg, spinner=DEFAULT_SPINNER):
try:
data = ctx.obj.auth.client.check_updates(
version=1,
safety_version=VERSION,
python_version=PYTHON_VERSION,
os_type=OS_TYPE,
os_release=platform.release(),
os_description=platform.platform(),
)
except InvalidCredentialError:
authenticated = False
except Exception as e:
LOG.exception(f"Failed to check updates, reason: {e}")
raise e
if not authenticated:
if console.quiet:
console.quiet = False
response = {
"status": 401,
"message": "Authenticated failed, please authenticate Safety and try again",
"data": {},
}
console.print_json(json.dumps(response))
else:
console.print()
console.print(
"[red]Safety is not authenticated, please first authenticate and try again.[/red]"
)
console.print()
console.print(
"To authenticate, use the `auth` command: `safety auth login` Or for more help: `safety auth —help`"
)
sys.exit(1)
if not data:
raise SafetyException("No data found.")
console.print("[green]Safety CLI is authenticated:[/green]")
from rich.padding import Padding
organization = data.get("organization", "-")
account = data.get("user_email", "-")
current_version = (
f"Current version: {VERSION} (Python {PYTHON_VERSION} on {OS_TYPE})"
)
latest_available_version = data.get("safety_updates", {}).get("stable_version", "-")
details = [
f"Organization: {organization}",
f"Account: {account}",
current_version,
f"Latest stable available version: {latest_available_version}",
]
for msg in details:
console.print(Padding(msg, (0, 0, 0, 1)), emoji=True)
console.print()
if latest_available_version:
try:
# Compare the current version and the latest available version using packaging.version
if packaging_version.parse(
latest_available_version
) > packaging_version.parse(VERSION):
console.print(
f"Update available: Safety version {latest_available_version}"
)
console.print()
console.print(
f"If Safety was installed from a requirements file, update Safety to version {latest_available_version} in that requirements file."
)
console.print()
console.print(
f"Pip: To install the updated version of Safety directly via pip, run: pip install safety=={latest_available_version}"
)
elif packaging_version.parse(
latest_available_version
) < packaging_version.parse(VERSION):
# Notify user about downgrading
console.print(
f"Latest stable version is {latest_available_version}. If you want to downgrade to this version, you can run: pip install safety=={latest_available_version}"
)
else:
console.print(
"You are already using the latest stable version of Safety."
)
except InvalidVersion as invalid_version:
LOG.exception(f"Invalid version format encountered: {invalid_version}")
console.print(
f"Error: Invalid version format encountered for the latest available version: {latest_available_version}"
)
console.print("Please report this issue or try again later.")
if console.quiet:
console.quiet = False
response = {"status": 200, "message": "", "data": data}
console.print_json(json.dumps(response))
cli.add_command(typer.main.get_command(cli_app), name="check-updates")
cli.add_command(typer.main.get_command(init_app), name="init")
cli.add_command(typer.main.get_command(scan_project_app), name="scan")
cli.add_command(typer.main.get_command(scan_system_app), name="system-scan")
cli.add_command(typer.main.get_command(codebase_app), name="codebase")
tool_commands.auto_register_tools(group=cli)
cli.add_command(typer.main.get_command(auth_app), name="auth")
cli.add_command(typer.main.get_command(firewall_app), name="firewall")
cli.add_command(alert)
if __name__ == "__main__":
cli()