# 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)