tarakan/lib/tarakan_web/controllers/api/infestation_controller.ex
Maxfield Luke af6077b9c3
All checks were successful
CI and deploy / Test (push) Successful in 4m52s
CI and deploy / Deploy production (push) Successful in 23s
Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at
https://git.elektrine.com/elektrine/tarakan.
2026-07-29 04:43:40 -04:00

208 lines
6.7 KiB
Elixir

defmodule TarakanWeb.API.InfestationController do
@moduledoc """
Public read access to the cross-repository infestation map.
Patterns aggregate only publicly disclosed canonical findings on listed
repositories, so listing and detail responses need no further filtering.
Query params are whitelisted: `days` buckets to 7/30/90/365, `min_repos`
clamps to 2..50, `limit` to 100.
"""
use TarakanWeb, :controller
alias Tarakan.FindingSignals
alias Tarakan.Infestations
@days [7, 30, 90, 365]
@default_days 30
@default_limit 40
@max_limit 100
def index(conn, params) do
patterns =
Infestations.list_infestations(
days: parse_days(params["days"]),
min_repos: parse_min_repos(params["min_repos"]),
limit: parse_limit(params["limit"])
)
json(conn, %{patterns: Enum.map(patterns, &pattern_json/1)})
end
def show(conn, %{"pattern_key" => pattern_key}) do
case Infestations.get_infestation(pattern_key) do
nil ->
conn
|> put_status(:not_found)
|> json(%{error: "pattern was not found"})
infestation ->
page = Infestations.list_instances_page(pattern_key, limit: 50)
json(conn, %{
pattern: pattern_json(infestation),
instances: Enum.map(page.entries, &instance_json/1)
})
end
end
@doc """
Vaccine pack: the validated detector for this infestation's dominant code
cluster, as a rule file.
Free, and 404s when no worker has produced a rule that actually matches known
instances yet. The previous version of this endpoint always returned something,
and what it returned matched nothing.
"""
def vaccine(conn, %{"pattern_key" => pattern_key}) do
with %{} = infestation <- Infestations.get_infestation(pattern_key),
cluster when is_binary(cluster) <- FindingSignals.dominant_cluster(pattern_key),
rule when not is_nil(rule) <- FindingSignals.best_rule(cluster) do
conn
|> put_resp_content_type("application/x-yaml")
|> send_resp(200, vaccine_yaml(infestation, rule))
else
nil ->
conn
|> put_status(:not_found)
|> json(%{
error: "no validated detector for this pattern yet",
pattern_key: pattern_key
})
end
end
# The rule is served verbatim as the worker wrote and validated it. The header
# carries the provenance so a reader can tell how much the detector was tested
# rather than having to trust it.
defp vaccine_yaml(infestation, rule) do
"""
# Tarakan vaccine pack for #{infestation.pattern_key}
# #{infestation.title}
#
# Engine: #{rule.engine}
# Validated: matched #{rule.matched_count} of #{rule.checked_count} known instances
# at #{rule.validated_at}
# Source: #{TarakanWeb.Endpoint.url()}/infestations/#{infestation.pattern_key}
#
# This detector was written and validated against real instances by a
# contributor's agent. Review what it matches before gating CI on it.
#{rule.rule_yaml}
"""
end
@doc """
Submits a detector for one code cluster.
The worker validates before submitting: it has both the rule engine and the
code, so it runs the rule against known members and reports which ones it hit.
`matched_finding_ids` is the receipt for `matched_count` and must list exactly
the findings that matched, or the submission is rejected.
"""
def put_rule(conn, %{"code_pattern_key" => key} = params) do
scope = conn.assigns[:current_scope]
cond do
is_nil(scope) || is_nil(scope.account) ->
conn
|> put_status(:unauthorized)
|> json(%{error: "missing or invalid API token"})
true ->
attrs = %{
engine: params["engine"] || "semgrep",
language: params["language"],
rule_yaml: params["rule_yaml"],
checked_count: params["checked_count"] || 0,
matched_count: params["matched_count"] || 0,
matched_finding_ids: params["matched_finding_ids"] || []
}
case FindingSignals.put_rule(scope, key, attrs) do
{:ok, rule} ->
conn
|> put_status(:created)
|> json(%{
code_pattern_key: rule.code_pattern_key,
engine: rule.engine,
matched_count: rule.matched_count,
checked_count: rule.checked_count,
validated_at: rule.validated_at,
# An unvalidated rule is stored but never served, so say so rather
# than letting the worker assume it shipped.
servable: Tarakan.Scans.CodePatternRule.servable?(rule)
})
{:error, :unauthorized} ->
conn |> put_status(:forbidden) |> json(%{error: "not authorized"})
{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:unprocessable_entity)
|> json(%{errors: TarakanWeb.ChangesetErrors.to_map(changeset)})
end
end
end
defp parse_days(value) do
case Integer.parse(to_string(value)) do
{days, ""} when days in @days -> days
_other -> @default_days
end
end
defp parse_min_repos(nil), do: 2
defp parse_min_repos(value) do
case Integer.parse(to_string(value)) do
{min_repos, ""} -> min_repos |> max(2) |> min(50)
_other -> 2
end
end
defp parse_limit(nil), do: @default_limit
defp parse_limit(value) do
case Integer.parse(to_string(value)) do
{limit, ""} when limit > 0 -> min(limit, @max_limit)
_other -> @default_limit
end
end
defp pattern_json(pattern) do
%{
pattern_key: pattern.pattern_key,
title: pattern.title,
severity: pattern.severity,
repo_count: pattern.repo_count,
instance_count: pattern.instance_count,
open_count: pattern.open_count,
verified_count: pattern.verified_count,
fixed_count: pattern.fixed_count,
disputed_count: pattern.disputed_count,
first_seen_at: pattern.first_seen_at,
last_seen_at: pattern.last_seen_at,
sample_file_path: pattern.sample_file_path,
sample_occurrence_public_id: pattern.sample_occurrence_public_id,
record_url: TarakanWeb.Endpoint.url() <> ~p"/infestations/#{pattern.pattern_key}"
}
end
defp instance_json(instance) do
%{
public_id: instance.public_id,
occurrence_public_id: instance.occurrence_public_id,
status: instance.status,
severity: instance.severity,
title: instance.title,
file_path: instance.file_path,
detections_count: instance.detections_count,
confirmations_count: instance.confirmations_count,
repository: %{
host: instance.host,
owner: instance.owner,
name: instance.name
},
updated_at: instance.updated_at
}
end
end