Packages
LowEndInsight is a simple 'bus-factor' risk analysis library for Open Source Software which is managed within a Git repository. Provide the git URL and the library will respond with a basic Elixir Map structure report.
Current section
Files
Jump to
Current section
Files
lib/oci/annotations.ex
# Copyright (C) 2020 by the Georgia Tech Research Institute (GTRI)
# This software may be modified and distributed under the terms of
# the BSD 3-Clause license. See the LICENSE file for details.
defmodule Lei.OCI.Annotations do
@moduledoc """
Generates OCI image annotations from LowEndInsight analysis reports.
Annotations follow the `dev.lowendinsight.*` namespace using OCI annotation
conventions (reverse-DNS prefix, hyphenated keys, string values).
See `docs/OCI_ANNOTATION_SCHEMA.md` for the full schema specification.
"""
@prefix "dev.lowendinsight"
@risk_keys [
{:risk, "risk"},
{:contributor_risk, "contributor-risk"},
{:contributor_count, "contributor-count"},
{:commit_currency_risk, "commit-currency-risk"},
{:commit_currency_weeks, "commit-currency-weeks"},
{:functional_contributors_risk, "functional-contributors-risk"},
{:functional_contributors, "functional-contributors"},
{:large_recent_commit_risk, "large-recent-commit-risk"},
{:sbom_risk, "sbom-risk"}
]
@doc """
Generates a map of OCI annotation key-value pairs from a single-repo LEI report.
Returns `{:ok, map}` where all keys are `dev.lowendinsight.*` strings and all
values are strings (per OCI spec). Returns `{:error, reason}` for unsupported formats.
## Examples
iex> report = %{header: %{start_time: "2024-01-01T00:00:00Z"}, data: %{repo: "https://github.com/org/repo", results: %{risk: "critical", contributor_count: 1}}}
iex> {:ok, annotations} = Lei.OCI.Annotations.from_report(report)
iex> annotations["dev.lowendinsight.risk"]
"critical"
"""
@spec from_report(map()) :: {:ok, map()} | {:error, String.t()}
def from_report(%{header: header, data: data}) do
results = data[:results] || Map.get(data, :results, %{})
repo_url = data[:repo] || Map.get(data, :repo, "")
timestamp = header[:start_time] || Map.get(header, :start_time, "")
version = Map.get(header, :library_version, lowendinsight_version())
annotations =
build_risk_annotations(results)
|> Map.merge(%{
"#{@prefix}.analyzed-at" => to_string(timestamp),
"#{@prefix}.version" => to_string(version),
"#{@prefix}.source-repo" => to_string(repo_url)
})
{:ok, annotations}
end
def from_report(%{report: %{repos: repos}} = report) do
case repos do
[single_repo] ->
header = single_repo[:header] || single_repo.header
data = single_repo[:data] || single_repo.data
timestamp =
case report do
%{metadata: %{times: %{start_time: t}}} -> t
_ -> header[:start_time] || Map.get(header, :start_time, "")
end
results = data[:results] || Map.get(data, :results, %{})
repo_url = data[:repo] || Map.get(data, :repo, "")
annotations =
build_risk_annotations(results)
|> Map.merge(%{
"#{@prefix}.analyzed-at" => to_string(timestamp),
"#{@prefix}.version" => lowendinsight_version(),
"#{@prefix}.source-repo" => to_string(repo_url)
})
{:ok, annotations}
_multiple ->
{:error, "multi-repo reports must specify a single repo for OCI annotations"}
end
end
def from_report(_), do: {:error, "unsupported report format"}
@doc """
Generates OCI annotations directly from a results map, repo URL, and timestamp.
Useful when you already have extracted analysis results and don't need to
parse a full report structure.
"""
@spec from_results(map(), String.t(), String.t()) :: {:ok, map()}
def from_results(results, repo_url, analyzed_at) when is_map(results) do
annotations =
build_risk_annotations(results)
|> Map.merge(%{
"#{@prefix}.analyzed-at" => to_string(analyzed_at),
"#{@prefix}.version" => lowendinsight_version(),
"#{@prefix}.source-repo" => to_string(repo_url)
})
{:ok, annotations}
end
@doc """
Encodes annotations as a JSON string suitable for `--annotation-file` flags
in OCI tooling (crane, oras, docker buildx).
"""
@spec to_json(map()) :: {:ok, String.t()}
def to_json(annotations) when is_map(annotations) do
{:ok, Poison.encode!(annotations, pretty: true)}
end
@doc """
Returns annotations as a list of `--annotation key=value` CLI flag strings,
suitable for passing to `docker buildx build` or `oras push`.
"""
@spec to_cli_flags(map()) :: [String.t()]
def to_cli_flags(annotations) when is_map(annotations) do
annotations
|> Enum.sort_by(fn {k, _v} -> k end)
|> Enum.map(fn {k, v} -> "--annotation #{k}=#{v}" end)
end
defp build_risk_annotations(results) when is_map(results) do
@risk_keys
|> Enum.reduce(%{}, fn {atom_key, annotation_key}, acc ->
value = results[atom_key] || Map.get(results, atom_key)
if value != nil do
Map.put(acc, "#{@prefix}.#{annotation_key}", to_string(value))
else
acc
end
end)
end
defp build_risk_annotations(_), do: %{}
defp lowendinsight_version do
case :application.get_key(:lowendinsight, :vsn) do
{:ok, vsn} -> List.to_string(vsn)
_ -> "0.0.0"
end
end
end