|
| 1 | +import json |
| 2 | +import os |
| 3 | + |
| 4 | +from conan.api.conan_api import ConanAPI |
| 5 | +from conan.api.input import UserInput |
| 6 | +from conan.api.model import MultiPackagesList |
| 7 | +from conan.api.output import cli_out_write, ConanOutput |
| 8 | +from conan.api.subapi.audit import CONAN_CENTER_AUDIT_PROVIDER_NAME |
| 9 | +from conan.cli import make_abs_path |
| 10 | +from conan.cli.args import common_graph_args, validate_common_graph_args |
| 11 | +from conan.cli.command import conan_command, conan_subcommand |
| 12 | +from conan.cli.formatters.audit.vulnerabilities import text_vuln_formatter, json_vuln_formatter, \ |
| 13 | + html_vuln_formatter |
| 14 | +from conan.cli.printers import print_profiles |
| 15 | +from conan.cli.printers.graph import print_graph_basic |
| 16 | +from conan.errors import ConanException |
| 17 | + |
| 18 | + |
| 19 | +def _add_provider_arg(subparser): |
| 20 | + subparser.add_argument("-p", "--provider", help="Provider to use for scanning") |
| 21 | + |
| 22 | + |
| 23 | +@conan_subcommand(formatters={"text": text_vuln_formatter, |
| 24 | + "json": json_vuln_formatter, |
| 25 | + "html": html_vuln_formatter}) |
| 26 | +def audit_scan(conan_api: ConanAPI, parser, subparser, *args): |
| 27 | + """ |
| 28 | + Scan a given recipe for vulnerabilities in its dependencies. |
| 29 | + """ |
| 30 | + common_graph_args(subparser) |
| 31 | + # Needed for the validation of args, but this should usually be left as False here |
| 32 | + # TODO: Do we then want to hide it in the --help? |
| 33 | + subparser.add_argument("--build-require", action='store_true', default=False, |
| 34 | + help='Whether the provided reference is a build-require') |
| 35 | + |
| 36 | + _add_provider_arg(subparser) |
| 37 | + args = parser.parse_args(*args) |
| 38 | + |
| 39 | + # This comes from install command |
| 40 | + |
| 41 | + validate_common_graph_args(args) |
| 42 | + # basic paths |
| 43 | + cwd = os.getcwd() |
| 44 | + path = conan_api.local.get_conanfile_path(args.path, cwd, py=None) if args.path else None |
| 45 | + |
| 46 | + # Basic collaborators: remotes, lockfile, profiles |
| 47 | + remotes = conan_api.remotes.list(args.remote) if not args.no_remote else [] |
| 48 | + overrides = eval(args.lockfile_overrides) if args.lockfile_overrides else None |
| 49 | + lockfile = conan_api.lockfile.get_lockfile(lockfile=args.lockfile, conanfile_path=path, cwd=cwd, |
| 50 | + partial=args.lockfile_partial, overrides=overrides) |
| 51 | + profile_host, profile_build = conan_api.profiles.get_profiles_from_args(args) |
| 52 | + print_profiles(profile_host, profile_build) |
| 53 | + |
| 54 | + # Graph computation (without installation of binaries) |
| 55 | + gapi = conan_api.graph |
| 56 | + if path: |
| 57 | + deps_graph = gapi.load_graph_consumer(path, args.name, args.version, args.user, args.channel, |
| 58 | + profile_host, profile_build, lockfile, remotes, |
| 59 | + # TO DISCUSS: defaulting to False the is_build_require |
| 60 | + args.update, is_build_require=args.build_require) |
| 61 | + else: |
| 62 | + deps_graph = gapi.load_graph_requires(args.requires, args.tool_requires, profile_host, |
| 63 | + profile_build, lockfile, remotes, args.update) |
| 64 | + print_graph_basic(deps_graph) |
| 65 | + deps_graph.report_graph_error() |
| 66 | + |
| 67 | + if deps_graph.error: |
| 68 | + return {"error": deps_graph.error} |
| 69 | + |
| 70 | + provider = conan_api.audit.get_provider(args.provider or CONAN_CENTER_AUDIT_PROVIDER_NAME) |
| 71 | + |
| 72 | + return conan_api.audit.scan(deps_graph, provider) |
| 73 | + |
| 74 | + |
| 75 | +@conan_subcommand(formatters={"text": text_vuln_formatter, |
| 76 | + "json": json_vuln_formatter, |
| 77 | + "html": html_vuln_formatter}) |
| 78 | +def audit_list(conan_api: ConanAPI, parser, subparser, *args): |
| 79 | + """ |
| 80 | + List the vulnerabilities of the given reference. |
| 81 | + """ |
| 82 | + subparser.add_argument("reference", help="Reference to list vulnerabilities for", nargs="?") |
| 83 | + subparser.add_argument("-l", "--list", help="pkglist file to list vulnerabilities for", default=None) |
| 84 | + subparser.add_argument("-r", "--remote", help="Remote to use for listing", default=None) |
| 85 | + _add_provider_arg(subparser) |
| 86 | + args = parser.parse_args(*args) |
| 87 | + |
| 88 | + if not args.reference and not args.list: |
| 89 | + raise ConanException("Please specify a reference or a pkglist file") |
| 90 | + |
| 91 | + if args.reference and args.list: |
| 92 | + raise ConanException("Please specify a reference or a pkglist file, not both") |
| 93 | + |
| 94 | + provider = conan_api.audit.get_provider(args.provider or CONAN_CENTER_AUDIT_PROVIDER_NAME) |
| 95 | + |
| 96 | + references = [] |
| 97 | + if args.list: |
| 98 | + listfile = make_abs_path(args.list) |
| 99 | + multi_package_list = MultiPackagesList.load(listfile) |
| 100 | + cache_name = "Local Cache" if not args.remote else args.remote |
| 101 | + package_list = multi_package_list[cache_name] |
| 102 | + refs_to_list = package_list.serialize() |
| 103 | + references = list(refs_to_list.keys()) |
| 104 | + if not references: # the package list might contain only refs, no revs |
| 105 | + ConanOutput().warning("Nothing to list, package list do not contain recipe revisions") |
| 106 | + else: |
| 107 | + references = [args.reference] |
| 108 | + return conan_api.audit.list(references, provider) |
| 109 | + |
| 110 | +def text_provider_formatter(providers_action): |
| 111 | + providers = providers_action[0] |
| 112 | + action = providers_action[1] |
| 113 | + |
| 114 | + if action == "remove": |
| 115 | + cli_out_write("Provider removed successfully.") |
| 116 | + elif action == "add": |
| 117 | + cli_out_write("Provider added successfully.") |
| 118 | + elif action == "auth": |
| 119 | + cli_out_write("Provider authentication added.") |
| 120 | + elif action == "list": |
| 121 | + if not providers: |
| 122 | + cli_out_write("No providers found.") |
| 123 | + else: |
| 124 | + for provider in providers: |
| 125 | + if provider: |
| 126 | + cli_out_write(f"{provider.name} (type: {provider.type}) - {provider.url}") |
| 127 | + |
| 128 | +def json_provider_formatter(providers_action): |
| 129 | + ret = [] |
| 130 | + for provider in providers_action[0]: |
| 131 | + if provider: |
| 132 | + ret.append({"name": provider.name, "url": provider.url, "type": provider.type}) |
| 133 | + cli_out_write(json.dumps(ret, indent=4)) |
| 134 | + |
| 135 | + |
| 136 | +@conan_subcommand(formatters={"text": text_provider_formatter, "json": json_provider_formatter}) |
| 137 | +def audit_provider(conan_api, parser, subparser, *args): |
| 138 | + """ |
| 139 | + Manage security providers for the 'conan audit' command. |
| 140 | + """ |
| 141 | + |
| 142 | + subparser.add_argument("action", choices=["add", "list", "auth", "remove"], help="Action to perform from 'add', 'list' , 'remove' or 'auth'") |
| 143 | + subparser.add_argument("name", help="Provider name", nargs="?") |
| 144 | + |
| 145 | + subparser.add_argument("--url", help="Provider URL") |
| 146 | + subparser.add_argument("--type", help="Provider type", choices=["conan-center-proxy", "private"]) |
| 147 | + subparser.add_argument("--token", help="Provider token") |
| 148 | + args = parser.parse_args(*args) |
| 149 | + |
| 150 | + if args.action == "add": |
| 151 | + if not args.name or not args.url or not args.type: |
| 152 | + raise ConanException("Name, URL and type are required to add a provider") |
| 153 | + if " " in args.name: |
| 154 | + raise ConanException("Name cannot contain spaces") |
| 155 | + conan_api.audit.add_provider(args.name, args.url, args.type) |
| 156 | + |
| 157 | + if not args.token: |
| 158 | + user_input = UserInput(conan_api.config.get("core:non_interactive")) |
| 159 | + ConanOutput().write(f"Please enter a token for {args.name} the provider: ") |
| 160 | + token = user_input.get_password() |
| 161 | + else: |
| 162 | + token = args.token |
| 163 | + |
| 164 | + provider = conan_api.audit.get_provider(args.name) |
| 165 | + if token: |
| 166 | + conan_api.audit.auth_provider(provider, token) |
| 167 | + |
| 168 | + return [provider], args.action |
| 169 | + elif args.action == "remove": |
| 170 | + if not args.name: |
| 171 | + raise ConanException("Name required to remove a provider") |
| 172 | + conan_api.audit.remove_provider(args.name) |
| 173 | + return [], args.action |
| 174 | + elif args.action == "list": |
| 175 | + providers = conan_api.audit.list_providers() |
| 176 | + return providers, args.action |
| 177 | + elif args.action == "auth": |
| 178 | + if not args.name: |
| 179 | + raise ConanException("Name is required to authenticate on a provider") |
| 180 | + if not args.token: |
| 181 | + user_input = UserInput(conan_api.config.get("core:non_interactive")) |
| 182 | + ConanOutput().write(f"Please enter a token for {args.name} the provider: ") |
| 183 | + token = user_input.get_password() |
| 184 | + else: |
| 185 | + token = args.token |
| 186 | + |
| 187 | + provider = conan_api.audit.get_provider(args.name) |
| 188 | + conan_api.audit.auth_provider(provider, token) |
| 189 | + return [provider], args.action |
| 190 | + |
| 191 | +@conan_command(group="Security") |
| 192 | +def audit(conan_api, parser, *args): |
| 193 | + """ |
| 194 | + Find vulnerabilities in your dependencies. |
| 195 | + """ |
0 commit comments