Initial commit on Forgejo
Fresh repository history for elektrine/tarakan hosted at https://git.elektrine.com/elektrine/tarakan.
This commit is contained in:
commit
af6077b9c3
455 changed files with 78366 additions and 0 deletions
208
lib/tarakan_web/controllers/api/infestation_controller.ex
Normal file
208
lib/tarakan_web/controllers/api/infestation_controller.ex
Normal file
|
|
@ -0,0 +1,208 @@
|
|||
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
|
||||
Loading…
Add table
Add a link
Reference in a new issue