defmodule NPM.Security.CVE do @moduledoc """ CVE (Common Vulnerabilities and Exposures) cross-referencing. Maps npm advisory data to CVE identifiers and provides vulnerability severity analysis. """ @severity_order %{"critical" => 4, "high" => 3, "moderate" => 2, "low" => 1, "info" => 0} @doc """ Extracts CVE identifiers from advisory data. """ @spec extract_cves(map()) :: [String.t()] def extract_cves(%{"cves" => cves}) when is_list(cves), do: cves def extract_cves(%{"cve" => cve}) when is_binary(cve), do: [cve] def extract_cves(%{"references" => refs}) when is_binary(refs) do Regex.scan(~r/CVE-\d{4}-\d+/, refs) |> List.flatten() |> Enum.uniq() end def extract_cves(_), do: [] @doc """ Compares two severity levels. Returns :gt, :lt, or :eq. """ @spec compare_severity(String.t(), String.t()) :: :gt | :lt | :eq def compare_severity(a, b) do va = Map.get(@severity_order, a, 0) vb = Map.get(@severity_order, b, 0) cond do va > vb -> :gt va < vb -> :lt true -> :eq end end @doc """ Returns the highest severity from a list of advisories. """ @spec max_severity([map()]) :: String.t() def max_severity([]), do: "none" def max_severity(advisories) do advisories |> Enum.map(&(&1["severity"] || "info")) |> Enum.max_by(&Map.get(@severity_order, &1, 0)) end @doc """ Groups advisories by affected package. """ @spec group_by_package([map()]) :: %{String.t() => [map()]} def group_by_package(advisories) do Enum.group_by(advisories, &(&1["module_name"] || &1["name"] || "unknown")) end @doc """ Counts advisories by severity. """ @spec severity_counts([map()]) :: %{String.t() => non_neg_integer()} def severity_counts(advisories) do advisories |> Enum.group_by(&(&1["severity"] || "info")) |> Map.new(fn {severity, items} -> {severity, length(items)} end) end @doc """ Checks if any advisory is above a given threshold. """ @spec above_threshold?([map()], String.t()) :: boolean() def above_threshold?(advisories, threshold) do Enum.any?(advisories, fn adv -> compare_severity(adv["severity"] || "info", threshold) in [:gt, :eq] end) end @doc """ Formats a vulnerability summary. """ @spec format_summary([map()]) :: String.t() def format_summary([]), do: "No known vulnerabilities." def format_summary(advisories) do counts = severity_counts(advisories) parts = ~w(critical high moderate low info) |> Enum.flat_map(fn sev -> case Map.get(counts, sev) do nil -> [] 0 -> [] n -> ["#{n} #{sev}"] end end) "#{length(advisories)} vulnerabilities: #{Enum.join(parts, ", ")}" end end