lib/mix/tasks/paraxial_scan.ex

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"]

  @impl Mix.Task
  def run(args) do
    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)

    {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)

    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 "--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)

    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) do
    Enum.reduce(args, %{prev_val: false}, fn arg, acc ->
      cond do
        arg in @gh_app_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