2.1 KiB · text History 8389210
defmodule GitGud.Advisories.Alert do
  @moduledoc """
  One known vulnerability in one dependency of one repository: an OSV
  advisory against a package at the version a lockfile pins.

  `open` until a scan no longer finds it (`fixed`), or someone decides
  it doesn't apply (`dismissed`, with a reason). The OSV id is the
  advisory's own; `aliases` are its GHSA / CVE / RUSTSEC names.
  """
  use Ecto.Schema
  import Ecto.Changeset

  @states ~w(open fixed dismissed)

  schema "security_alerts" do
    belongs_to :repository, GitGud.Repositories.Repository
    field :advisory_id, :string
    field :aliases, {:array, :string}, default: []
    field :summary, :string
    field :ecosystem, :string
    field :package, :string
    field :version, :string
    field :fixed_versions, {:array, :string}, default: []
    field :severity, :float
    field :manifest_path, :string
    field :state, :string, default: "open"
    field :dismissed_reason, :string
    belongs_to :dismissed_by, GitGud.Accounts.User
    field :first_seen_at, :utc_datetime
    field :fixed_at, :utc_datetime

    timestamps(type: :utc_datetime)
  end

  @fields ~w(advisory_id aliases summary ecosystem package version fixed_versions severity
             manifest_path)a

  @doc "A finding from a scan, for `repository_id` (set by the caller)."
  def changeset(alert, attrs) do
    alert
    |> cast(attrs, @fields)
    |> validate_required([:advisory_id, :ecosystem, :package, :version, :manifest_path])
    |> update_change(:summary, &(&1 && String.slice(&1, 0, 255)))
    |> validate_inclusion(:state, @states)
  end

  def states, do: @states

  @doc "High (7.0+) and critical (9.0+) — what notifies right away."
  def urgent?(%__MODULE__{severity: s}) when is_number(s), do: s >= 7.0
  def urgent?(_), do: false

  @doc "CVSS score to a word, as advisories put it."
  def severity_label(%__MODULE__{severity: nil}), do: "unknown"
  def severity_label(%__MODULE__{severity: s}) when s >= 9.0, do: "critical"
  def severity_label(%__MODULE__{severity: s}) when s >= 7.0, do: "high"
  def severity_label(%__MODULE__{severity: s}) when s >= 4.0, do: "medium"
  def severity_label(%__MODULE__{}), do: "low"
end