tarakan/priv/repo/seed_infestations.exs
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

195 lines
7.2 KiB
Elixir

# Demo infestation data: multi-repo patterns for the infestation map.
#
# mix run priv/repo/seed_infestations.exs
#
# This script fabricates security findings. It therefore refuses to run outside
# :dev, and it only ever writes against repositories it creates itself, under
# the reserved `infestation-demo-*` owner prefix.
#
# It must never adopt repositories that already exist. An earlier version
# selected every listed repository, which meant a run against a database
# containing real projects published invented "verified critical" findings
# attributed to them by name.
#
# Idempotent enough for demo: uses stable run_ids; re-running may hit unique
# constraints on run_id and skip duplicates.
import Ecto.Query
alias Tarakan.Accounts.Account
alias Tarakan.Infestations
alias Tarakan.Repo
alias Tarakan.Repositories.Repository
alias Tarakan.Scans
if Mix.env() != :dev do
Mix.shell().error("""
Refusing to run: this seeder fabricates public security findings and is
dev-only. Current environment: #{Mix.env()}.
""")
System.halt(1)
end
demo_owner_prefix = "infestation-demo"
demo_repo_count = 8
account =
Repo.get_by(Account, email: "mod@example.com") ||
Repo.one(from a in Account, where: a.platform_role in ["moderator", "admin"], limit: 1) ||
Repo.one!(from a in Account, where: a.state == "active", limit: 1)
# Create our own targets. Never touch anything already in the registry: a real
# project must not appear in fabricated data under any circumstances.
repos =
for i <- 1..demo_repo_count do
owner = "#{demo_owner_prefix}-#{i}"
name = "service"
case Repo.get_by(Repository, host: "github.com", owner: owner, name: name) do
%Repository{} = existing ->
existing
nil ->
%Repository{}
|> Ecto.Changeset.change(%{
host: "github.com",
owner: owner,
name: name,
listing_status: "listed",
submitted_by_id: account.id
})
|> Repo.insert!()
end
end
# Belt and braces: if anything above ever returns a repository we did not make,
# stop rather than write findings against it.
Enum.each(repos, fn repo ->
unless String.starts_with?(repo.owner, demo_owner_prefix <> "-") do
Mix.shell().error("Refusing to seed against non-demo repository #{repo.owner}/#{repo.name}")
System.halt(1)
end
end)
# {title, severity, status cycle per instance} - length of status list = repo fan-out
patterns = [
{"SQL injection via string concatenation", "critical", ~w(open open verified open open)},
{"Hardcoded credentials in config loader", "high", ~w(open fixed open verified open)},
{"Command injection through shell interpolation", "critical", ~w(open open open open verified)},
{"Path traversal in archive extract", "high", ~w(open open fixed open)},
{"SSRF via user-controlled URL fetch", "high", ~w(open verified open open open)},
{"JWT accepts alg=none", "critical", ~w(open open disputed open)},
{"Missing authorization on admin API route", "critical", ~w(open open open verified open open)},
{"Insecure deserialization of user input", "high", ~w(open fixed open open)},
{"Open redirect after login", "medium", ~w(open open verified fixed open)},
{"Prototype pollution in deep merge", "high", ~w(open open open open)},
{"Debug endpoint left enabled", "medium", ~w(open fixed open open open)},
{"Weak random token generation", "medium", ~w(open open verified open)},
{"CORS reflects arbitrary Origin", "medium", ~w(open open open open open)},
{"Timing-unsafe secret comparison", "high", ~w(open verified open open)},
{"Unsanitized HTML rendered as markup", "high", ~w(open open open fixed verified open open)},
{"XML external entity in parser", "high", ~w(open open verified)},
{"LDAP injection in directory search", "high", ~w(open open open fixed)},
{"Mass assignment on privileged fields", "medium", ~w(open verified open open open)},
{"Broken access control on object ID", "critical", ~w(open open open open verified open)},
{"Insecure direct object reference", "high", ~w(open open fixed open open)}
]
sha = fn i, repo_id, title ->
:crypto.hash(:sha256, "seed-epi-v2-#{repo_id}-#{i}-#{title}")
|> Base.encode16(case: :lower)
|> String.slice(0, 40)
end
{ok_count, skip_count, err_count} =
Enum.reduce(patterns, {0, 0, 0}, fn {title, severity, status_cycle}, {ok, skip, err} ->
n = length(status_cycle)
# Deterministic rotation so re-runs hit the same repos
offset = :erlang.phash2(title, length(repos))
selected =
for i <- 0..(n - 1) do
Enum.at(repos, rem(offset + i, length(repos)))
end
Enum.reduce(Enum.with_index(selected), {ok, skip, err}, fn {repo, i}, {ok, skip, err} ->
status = Enum.at(status_cycle, i)
path = "lib/seed/infestation_#{:erlang.phash2(title, 10_000)}/mod_#{i}.ex"
run_id = "epi-seed-v2-#{:erlang.phash2({title, repo.id, i}, 1_000_000_000)}"
findings_json =
Jason.encode!(%{
"tarakan_scan_format" => 1,
"findings" => [
%{
"file" => path,
"line_start" => 10 + i * 3,
"line_end" => 14 + i * 3,
"severity" => severity,
"title" => title,
"description" =>
"Demo infestation seed: #{title} observed in #{repo.owner}/#{repo.name}."
}
]
})
attrs = %{
"repository_id" => repo.id,
"submitted_by_id" => account.id,
"commit_sha" => sha.(i, repo.id, title),
"model" => "seed-agent",
"prompt_version" => "infestation-demo/v2",
"run_id" => run_id,
"provenance" => "agent",
"review_kind" => "code_review",
"findings_json" => findings_json,
"visibility" => "public"
}
case Scans.stage_review_insert(attrs) do
{:ok, scan} ->
scan = Repo.preload(scan, findings: :canonical_finding)
Enum.each(scan.findings, fn f ->
if f.canonical_finding do
f.canonical_finding
|> Ecto.Changeset.change(%{status: status})
|> Repo.update!()
end
end)
Scans.recalculate_repository_metrics(repo.id)
{ok + 1, skip, err}
{:error, %Ecto.Changeset{} = cs} ->
if Keyword.has_key?(cs.errors, :run_id) do
{ok, skip + 1, err}
else
Mix.shell().error("FAIL #{title} @ #{repo.owner}/#{repo.name}: #{inspect(cs.errors)}")
{ok, skip, err + 1}
end
{:error, reason} ->
Mix.shell().error("FAIL #{title} @ #{repo.owner}/#{repo.name}: #{inspect(reason)}")
{ok, skip, err + 1}
end
end)
end)
# Project rollups so atlas reads (read_from_rollup) see seeded data immediately.
Mix.shell().info("Refreshing infestation rollups…")
Tarakan.Infestations.Backfill.run_sync!()
eps = Infestations.list_infestations(min_repos: 2, days: 365, limit: 50)
Mix.shell().info(
"Infestation seed: #{ok_count} inserted, #{skip_count} skipped, #{err_count} failed. " <>
"#{length(eps)} multi-repo patterns visible."
)
Enum.each(eps, fn e ->
Mix.shell().info(
" #{e.repo_count} repos · open=#{e.open_count} ver=#{e.verified_count} fix=#{e.fixed_count} · #{e.title}"
)
end)