defmodule Mix.Tasks.Paraxial.Scan do use Mix.Task require Logger alias Paraxial.Scan alias Paraxial.Helpers @gh_app_args ["--github_app", "--install_id", "--repo_owner", "--repo_name", "--pr_number"] @flag_args ["--paraxial_url", "--paraxial_api_key"] @impl Mix.Task def run(args) do if "--paraxial_url" in args and "--paraxial_api_key" in args do Logger.info("[Paraxial] URL and API key cli flags set correctly, these values will be used") cli_map = args_to_map(args, @flag_args) cli_url = Map.get(cli_map, "--paraxial_url") cli_api_key = Map.get(cli_map, "--paraxial_api_key") Application.put_env(:paraxial, :paraxial_url, cli_url) Application.put_env(:paraxial, :paraxial_api_key, cli_api_key) end HTTPoison.start() api_key = Helpers.get_api_key() if api_key == nil do Logger.error("[Paraxial] API key NOT found, scan results cannot be uploaded") else Logger.info("[Paraxial] API key found, scan results will be uploaded") end sobelow = Task.async(fn -> System.cmd("mix", ["sobelow", "--private", "--skip", "--config", "--format", "json"]) end) deps_audit = Task.async(fn -> System.cmd("mix", ["deps.audit"]) end) hex_audit = Task.async(fn -> System.cmd("mix", ["hex.audit"]) end) sarif_raw = if "--sarif" in args do Task.async(fn -> System.cmd("mix", ["sobelow", "--private", "--skip", "--config", "--format", "sarif"]) end) else Task.async(fn -> {nil, 0} end) end {sobelow, _exit_code} = Task.await(sobelow, :infinity) {deps_audit, _exit_code} = Task.await(deps_audit, :infinity) {hex_audit, _exit_code} = Task.await(hex_audit, :infinity) {sarif_raw, _exit_code} = Task.await(sarif_raw, :infinity) sl = Scan.make_sobelow(sobelow) dl = Scan.make_deps(deps_audit) hl = Scan.make_hex(hex_audit) findings = List.flatten([sl, dl, hl]) scan = %Scan{ timestamp: Scan.get_timestamp(), findings: findings, api_key: "REDACTED" } IO.inspect(scan, label: "[Paraxial] Scan findings") scan = Map.put(scan, :api_key, api_key) json = Jason.encode!(scan) url = Helpers.get_scan_ingest_url() scan_info = case HTTPoison.post(url, json, [{"Content-Type", "application/json"}]) do {:ok, %{body: body}} -> if String.contains?(body, "Scan written successfully") do %{"ok" => scan_info} = Jason.decode!(body) Logger.info("[Paraxial] #{scan_info}") scan_info else Logger.error("[Paraxial] Scan upload failed, check configuration.") :error end _ -> Logger.error("[Paraxial] Scan upload failed, check configuration.") :error end cond do Enum.all?(@gh_app_args, fn a -> a in args end) and scan_info == :error -> Logger.error("[Paraxial] Github upload did not run due to original scan upload failure.") :ok Enum.all?(@gh_app_args, fn a -> a in args end) -> Logger.info("[Paraxial] Github App Correct Arguments") github_app_upload(args, scan_info) "--github_app" in args -> Logger.error( "[Paraxial] --github_app is missing arguments. Required: --install_id, --repo_owner, --repo_name, --pr_number" ) true -> :ok end if "--sarif" in args do url = Paraxial.Helpers.get_sarif_url() # get the enriched version case HTTPoison.post(url, sarif_raw, [{"Content-Type", "application/json"}]) do {:ok, %{body: body}} -> File.write!("sarif.txt", body) Logger.info("[Paraxial] SARIF file written successfully.") _ -> Logger.error("[Paraxial] SARIF upload failed.") end end if "--add-exit-code" in args and length(scan.findings) > 0 do exit({:shutdown, 1}) end end def github_app_upload(args, scan_info) do regex = ~r/UUID (.+)/ captures = Regex.run(regex, scan_info, capture: :all_but_first) scan_uuid = Enum.at(captures, 0) cli_map = args_to_map(args, @gh_app_args) censored_backend_map = %{ "installation_id" => Map.get(cli_map, "--install_id"), "repository_owner" => Map.get(cli_map, "--repo_owner"), "repository_name" => Map.get(cli_map, "--repo_name"), "pull_request_number" => Map.get(cli_map, "--pr_number"), "scan_uuid" => scan_uuid, "api_key" => "REDACTED" } IO.inspect(censored_backend_map, label: "[Paraxial] Github Upload info") backend_map = Map.put(censored_backend_map, "api_key", Helpers.get_api_key()) url = Helpers.get_github_app_url() json = Jason.encode!(backend_map) debug_url = "https://github.com/#{cli_map["--repo_owner"]}/#{cli_map["--repo_name"]}/pull/#{cli_map["--pr_number"]}" case HTTPoison.post(url, json, [{"Content-Type", "application/json"}]) do {:ok, %{body: body}} -> if String.contains?(body, "Comment created successfully") do Logger.info("[Paraxial] Github PR Comment Created successfully") Logger.info("[Paraxial] URL: #{debug_url}") else Logger.error("[Paraxial] Github PR Comment failed") end _ -> Logger.error("[Paraxial] Github PR Comment failed") end end def args_to_map(args, all_args) do Enum.reduce(args, %{prev_val: false}, fn arg, acc -> cond do arg in all_args -> # Create a new key acc |> Map.put(:prev_val, arg) acc[:prev_val] != false -> # The previous flag in the list was valid, the current arg is the value acc |> Map.put(acc[:prev_val], arg) |> Map.put(:prev_val, false) true -> # No valid flag or value for the flag, do nothing acc end end) |> Map.delete(:prev_val) end end