Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
208 lines
6.7 KiB
Elixir
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
|