Skip to main content

lib/rulestead/webhooks/code_refs_plug.ex

# credo:disable-for-this-file
defmodule Rulestead.Webhooks.CodeRefsPlug do
  @moduledoc false
  # Ingress endpoint for receiving code references from CI.

  import Plug.Conn
  alias Rulestead.CodeRefs.CodeReference
  alias Rulestead.CodeRefs.ScanReceipt
  alias Rulestead.Repo

  def init(opts), do: opts

  def call(conn, opts) do
    expected_token = Keyword.get(opts, :secret) || System.get_env("RULESTEAD_CI_TOKEN")

    case get_req_header(conn, "authorization") do
      ["Bearer " <> token] when token == expected_token and not is_nil(expected_token) ->
        case read_body_json(conn) do
          {:ok, body, conn} ->
            handle_payload(conn, body)

          {:error, _reason, conn} ->
            send_json_resp(conn, 400, %{error: "invalid JSON"})
        end

      _ ->
        send_json_resp(conn, 401, %{error: "unauthorized"})
    end
  end

  defp handle_payload(conn, %{"references" => references}) when is_list(references) do
    valid_references = Enum.filter(references, &valid_reference?/1)

    if length(valid_references) == length(references) or valid_references != [] do
      now = DateTime.utc_now() |> DateTime.truncate(:microsecond)

      rows =
        Enum.map(valid_references, fn ref ->
          %{
            id: Ecto.UUID.generate(),
            flag_key: ref["flag_key"],
            file: ref["file"],
            line: ref["line"],
            inserted_at: now,
            updated_at: now
          }
        end)

      Ecto.Multi.new()
      |> Ecto.Multi.delete_all(:delete_old, CodeReference)
      |> Ecto.Multi.insert_all(:insert_new, CodeReference, rows)
      |> Ecto.Multi.insert(
        :scan_receipt,
        ScanReceipt.changeset(%ScanReceipt{}, %{
          received_at: now,
          reference_count: length(rows)
        })
      )
      |> Repo.transact()
      |> case do
        {:ok, _} ->
          send_json_resp(conn, 200, %{status: "ok", count: length(rows)})

        {:error, _, _, _} ->
          send_json_resp(conn, 500, %{error: "internal server error"})
      end
    else
      send_json_resp(conn, 400, %{error: "invalid reference entry"})
    end
  end

  defp handle_payload(conn, _body) do
    send_json_resp(conn, 400, %{error: "invalid payload shape"})
  end

  defp valid_reference?(%{"flag_key" => flag_key, "file" => file, "line" => line})
       when is_binary(flag_key) and is_binary(file) and is_integer(line),
       do: true

  defp valid_reference?(_), do: false

  defp read_body_json(conn) do
    case conn.body_params do
      %Plug.Conn.Unfetched{} ->
        case read_body(conn) do
          {:ok, body, conn} ->
            case Jason.decode(body) do
              {:ok, decoded} -> {:ok, decoded, conn}
              {:error, _} -> {:error, :invalid_json, conn}
            end

          {:more, _, conn} ->
            {:error, :too_large, conn}

          {:error, reason} ->
            {:error, reason, conn}
        end

      params ->
        {:ok, params, conn}
    end
  end

  defp send_json_resp(conn, status, payload) do
    conn
    |> put_resp_content_type("application/json")
    |> send_resp(status, Jason.encode!(payload))
    |> halt()
  end
end