defmodule GitGud.AdvisoriesTest do
@moduledoc """
Dependency alerts: who they're on for and who sees them, a scan
exporting a repository's lockfiles and recording what the scanner
reports — opening, fixing and reopening alerts — and skipping a push
that left the lockfiles alone. The scanner is a stand-in script that
prints the report a test asks for.
"""
use GitGud.DataCase, async: false
use Oban.Testing, repo: GitGud.Repo
import GitGud.AccountsFixtures
import GitGud.ForgeFixtures
import GitGud.WorkflowGitHelpers
alias GitGud.Advisories
alias GitGud.Advisories.Alert
alias GitGud.Advisories.Workers.Scan
alias GitGud.Organizations
setup do
dir = Path.join(System.tmp_dir!(), "fake-osv-#{System.unique_integer([:positive])}")
File.mkdir_p!(dir)
report = Path.join(dir, "report.json")
seen = Path.join(dir, "seen")
scanner = Path.join(dir, "osv-scanner")
# Prints the report, and records the lockfiles it was asked to read
# and that they were really there.
File.write!(scanner, """
#!/bin/sh
: > #{seen}
while [ $# -gt 0 ]; do
if [ "$1" = "--lockfile" ]; then
[ -f "$2" ] && echo "$2" >> #{seen}
shift
fi
shift
done
cat #{report}
exit 1
""")
File.chmod!(scanner, 0o755)
before = Application.get_env(:git_gud, Advisories, [])
Application.put_env(
:git_gud,
Advisories,
Keyword.merge(before, scanner: scanner, source: :api)
)
on_exit(fn ->
Application.put_env(:git_gud, Advisories, before)
File.rm_rf(dir)
end)
{owner, repo} = repository_fixture()
lock = ~s(%{"plug": {:hex, :plug, "1.19.1", "x", [:mix], [], "hexpm", "y"}}\n)
head =
commit!(repo, %{"mix.lock" => lock, "lib/a.ex" => "a\n", "deps/x/mix.lock" => lock}, [])
git!(repo.disk_path, ["update-ref", "refs/heads/#{repo.default_branch}", head])
{:ok, owner: owner, repo: repo, report: report, seen: seen, head: head}
end
defp report(path, findings) do
packages =
for {name, version, ids} <- findings do
%{
"package" => %{"name" => name, "version" => version, "ecosystem" => "Hex"},
"vulnerabilities" =>
for id <- ids do
%{
"id" => id,
"aliases" => ["CVE-#{id}"],
"summary" => "#{name} is bad",
"affected" => [
%{
"package" => %{"name" => name},
"ranges" => [
%{
"type" => "SEMVER",
"events" => [%{"introduced" => "0"}, %{"fixed" => "9.9.9"}]
}
]
}
]
}
end,
"groups" => [%{"ids" => ids, "max_severity" => "8.7"}]
}
end
File.write!(
path,
Jason.encode!(%{
"results" => [%{"source" => %{"path" => "mix.lock"}, "packages" => packages}]
})
)
end
defp alerts(repo),
do: GitGud.Repo.all(from a in Alert, where: a.repository_id == ^repo.id, order_by: a.id)
describe "a scan" do
setup %{repo: repo} do
{:ok, repo} = Advisories.set_repo(repo, true)
{:ok, repo: repo}
end
test "opens an alert per vulnerability, reading only the repository's own lockfiles", %{
repo: repo,
report: report,
seen: seen
} do
# One vulnerability known by two ids, as osv-scanner groups them.
report(report, [{"plug", "1.19.1", ["GHSA-2", "EEF-1"]}])
assert {:ok, %{opened: 1, fixed: 0}} = Advisories.scan(repo)
# deps/ is somebody else's; lib/a.ex isn't a lockfile.
assert File.read!(seen) == "mix.lock\n"
[a] = alerts(repo)
assert {a.advisory_id, a.package, a.version, a.state} == {"EEF-1", "plug", "1.19.1", "open"}
assert {a.severity, a.fixed_versions} == {8.7, ["9.9.9"]}
assert {a.manifest_path, a.aliases} == {"mix.lock", ["CVE-EEF-1", "CVE-GHSA-2", "GHSA-2"]}
end
test "the same lockfiles again are skipped; forced, what's gone is fixed and comes back open",
%{
repo: repo,
report: report
} do
report(report, [{"plug", "1.19.1", ["EEF-1"]}])
{:ok, _} = Advisories.scan(repo)
repo = GitGud.Repo.reload!(repo)
assert {:ok, :unchanged} = Advisories.scan(repo)
report(report, [])
assert {:ok, %{opened: 0, fixed: 1}} = Advisories.scan(repo, force: true)
assert [%Alert{state: "fixed", fixed_at: %DateTime{}}] = alerts(repo)
report(report, [{"plug", "1.19.1", ["EEF-1"]}])
assert {:ok, %{opened: 0}} = Advisories.scan(repo, force: true)
assert [%Alert{state: "open", fixed_at: nil}] = alerts(repo)
end
test "a dismissed alert stays dismissed", %{repo: repo, report: report} do
report(report, [{"plug", "1.19.1", ["EEF-1"]}])
{:ok, _} = Advisories.scan(repo)
[alert] = alerts(repo)
GitGud.Repo.update!(Ecto.Changeset.change(alert, state: "dismissed"))
{:ok, _} = Advisories.scan(repo, force: true)
assert [%Alert{state: "dismissed"}] = alerts(repo)
end
test "without a scanner, nothing runs", %{repo: repo} do
Application.put_env(:git_gud, Advisories, scanner: "/nonexistent/osv-scanner", source: :api)
assert {:error, :no_scanner} = Advisories.scan(repo)
assert alerts(repo) == []
end
end
describe "who it's on for" do
test "a personal repository by its own switch; an org's by the org's", %{
owner: owner,
repo: repo
} do
refute Advisories.enabled?(repo)
{:ok, repo} = Advisories.set_repo(repo, true)
assert Advisories.enabled?(repo)
handle = "adv#{System.unique_integer([:positive])}"
{:ok, org} =
Organizations.create_organization(owner, %{"handle" => handle, "name" => handle})
{:ok, org_repo} =
GitGud.Repositories.create_repository_for_org(org, owner, %{"name" => "svc"})
refute Advisories.enabled?(org_repo)
assert {:error, :org_repository} = Advisories.set_repo(org_repo, true)
{:ok, _} = Advisories.set_org(org, true)
assert Advisories.enabled?(GitGud.Repo.reload!(org_repo))
end
test "turning it on queues a scan; the instance switch beats everyone's", %{repo: repo} do
{:ok, repo} = Advisories.set_repo(repo, true)
assert_enqueued(worker: Scan, args: %{"repository_id" => repo.id, "force" => true})
Application.put_env(:git_gud, Advisories, source: :off)
refute Advisories.enabled?(repo)
end
end
describe "who sees alerts" do
test "a personal repository's owner and writers, not its readers", %{owner: owner, repo: repo} do
assert Advisories.readable?(owner, repo)
refute Advisories.readable?(user_fixture(), %{repo | visibility: "public"})
refute Advisories.readable?(nil, repo)
end
test "an org repository's members, not outsiders", %{owner: owner} do
handle = "advr#{System.unique_integer([:positive])}"
{:ok, org} =
Organizations.create_organization(owner, %{"handle" => handle, "name" => handle})
{:ok, repo} =
GitGud.Repositories.create_repository_for_org(org, owner, %{
"name" => "web",
"visibility" => "public"
})
member = user_fixture()
{:ok, _} = Organizations.add_member(org, member)
assert Advisories.readable?(member, repo)
refute Advisories.readable?(user_fixture(), repo)
end
end
end
neiam
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
7.4 KiB · text
History
8389210