lib/openagents/audit.ex

main at 58e6347eeb72 · 1 KB

defmodule OpenAgents.Audit do
  @moduledoc "Records bounded security events without repository content or credentials."

  alias OpenAgents.{AuditEvent, Repo}

  @maximum_metadata_bytes 8_192

  # The actor kinds this application writes. `machine` is one of them: a paired
  # computer that authenticates to the Git plane pushes and fetches under
  # `{:machine, id}` (`OpenAgentsWeb.Plugs.ForgeGitAuth.principal_for/1` into
  # `OpenAgents.Forge.GitHTTP.audit_actor/1`), so the stored value is real and
  # keeps its spelling under CANON-002. `OpenAgents.Forge.GitHTTP.audit_actor_kinds/0`
  # is the list that must stay inside this one, and `OpenAgents.AuditTest`
  # asserts the containment rather than trusting a reading of the call sites.
  @actor_kinds [:user, :agent, :machine, :operator, :system]

  @doc "The actor kinds `record!/5` accepts, as strings."
  @spec actor_kinds() :: [String.t()]
  def actor_kinds, do: Enum.map(@actor_kinds, &Atom.to_string/1)

  def record!(event_type, actor, subject_type, subject_id, options \\ []) do
    metadata = Keyword.get(options, :metadata, %{})

    if is_map(metadata) and byte_size(Jason.encode!(metadata)) <= @maximum_metadata_bytes do
      %AuditEvent{}
      |> AuditEvent.changeset(%{
        event_type: event_type,
        actor_type: actor_type(actor),
        actor_id: actor_id(actor),
        subject_type: subject_type,
        subject_id: to_string(subject_id),
        repository_id: Keyword.get(options, :repository_id),
        metadata: metadata
      })
      |> Repo.insert!()
    else
      raise ArgumentError, "audit metadata is invalid"
    end
  end

  defp actor_type({type, _id}) when type in @actor_kinds, do: Atom.to_string(type)

  defp actor_type(:system), do: "system"

  defp actor_id({_type, nil}), do: nil
  defp actor_id({_type, id}), do: to_string(id)
  defp actor_id(:system), do: nil
end