defmodule TarakanWeb.API.FindingSignalsApiTest do @moduledoc """ The three worker submission endpoints that had no HTTP surface until now: calibrated severity, code embeddings, and cluster detectors. """ use TarakanWeb.ConnCase, async: true import Ecto.Query, only: [from: 2] alias Tarakan.Accounts.ApiCredentials alias Tarakan.Repo alias Tarakan.Scans.CanonicalFinding setup %{conn: conn} do submitter = github_account_fixture() repository = listed_github_repository_fixture(submitter) findings = Jason.encode!(%{ "tarakan_scan_format" => 1, "findings" => [ %{ "file" => "lib/vulnerable.ex", "severity" => "critical", "title" => "Command injection #{System.unique_integer([:positive])}", "description" => "User input reaches System.cmd without escaping." } ] }) scan_fixture(repository, submitter, %{"findings_json" => findings}) finding = Repo.one!(from f in CanonicalFinding, where: f.repository_id == ^repository.id) reviewer = reviewer_account_fixture() {:ok, token, _cred} = ApiCredentials.create(reviewer, %{ name: "signals", scopes: ["reviews:read", "reviews:verify"] }) %{ conn: put_req_header(conn, "authorization", "Bearer #{token}"), anon: build_conn(), repository: repository, finding: finding } end defp signal_path(repository, finding, suffix) do "/api/github.com/#{repository.owner}/#{repository.name}/findings/#{finding.public_id}/#{suffix}" end describe "POST .../severity" do test "stores a rescore beside the submitter's claim", ctx do conn = post(ctx.conn, signal_path(ctx.repository, ctx.finding, "severity"), %{ "severity" => "low", "rubric" => "rubric-v1" }) assert %{"calibrated_severity" => "low"} = json_response(conn, 200) assert Repo.get!(CanonicalFinding, ctx.finding.id).severity == "critical" end test "rejects a severity outside the scale", ctx do conn = post(ctx.conn, signal_path(ctx.repository, ctx.finding, "severity"), %{ "severity" => "catastrophic", "rubric" => "rubric-v1" }) assert %{"errors" => %{"calibrated_severity" => _}} = json_response(conn, 422) end end describe "POST .../embedding" do test "stores the vector and assigns a cluster", ctx do conn = post(ctx.conn, signal_path(ctx.repository, ctx.finding, "embedding"), %{ "embedding" => [0.1, 0.9, 0.2], "embedding_model" => "test-embed-v1" }) assert %{"code_pattern_key" => key} = json_response(conn, 200) assert key =~ ~r/^code:/ end test "rejects an all-zero vector, which would match everything", ctx do conn = post(ctx.conn, signal_path(ctx.repository, ctx.finding, "embedding"), %{ "embedding" => [0.0, 0.0, 0.0], "embedding_model" => "test-embed-v1" }) assert %{"errors" => %{"embedding" => _}} = json_response(conn, 422) end end describe "POST /api/patterns/:key/rule" do @rule """ rules: - id: tarakan-cmd-injection message: user input reaches System.cmd severity: ERROR languages: [elixir] patterns: - pattern: System.cmd($C, $A) """ test "accepts a validated detector and marks it servable", ctx do conn = post(ctx.conn, "/api/patterns/code:abc/rule", %{ "rule_yaml" => @rule, "language" => "elixir", "checked_count" => 3, "matched_count" => 2, "matched_finding_ids" => [Ecto.UUID.generate(), Ecto.UUID.generate()] }) assert %{"servable" => true, "matched_count" => 2} = json_response(conn, 201) end test "a detector that matched nothing is stored but not servable", ctx do conn = post(ctx.conn, "/api/patterns/code:abc/rule", %{ "rule_yaml" => @rule, "checked_count" => 3, "matched_count" => 0, "matched_finding_ids" => [] }) assert %{"servable" => false, "validated_at" => nil} = json_response(conn, 201) end test "rejects a match count with no receipts", ctx do conn = post(ctx.conn, "/api/patterns/code:abc/rule", %{ "rule_yaml" => @rule, "checked_count" => 3, "matched_count" => 2, "matched_finding_ids" => [] }) assert %{"errors" => %{"matched_finding_ids" => _}} = json_response(conn, 422) end test "anonymous submissions are refused", ctx do conn = post(ctx.anon, "/api/patterns/code:abc/rule", %{"rule_yaml" => @rule}) assert json_response(conn, 401) end end end