Files
Evan Jarrett 6758996300 add SBOM package diffing, verify hold-service captain records
- diff view gains a Packages tab with added/removed/changed/unchanged
  package tables and purl-derived type/license/upstream links
- captain records verified against the DID's atcr_hold service before
  caching (processor + batch backfill), preventing forged holds
- fix empty-handle updates clobbering cached handles and colliding on
  the UNIQUE constraint
- move fillPrevCIDs into repo.go; DirectRepoOperator is now canonical,
  repomgr kept as a test oracle
- surface read-only crew status in hold selector
- reconcile docs
2026-06-13 12:49:03 -05:00

613 lines
18 KiB
Go

package handlers
import (
"testing"
)
func TestComputeLayerDiff_IdenticalLayers(t *testing.T) {
layers := []LayerDetail{
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:bbb", Size: 200, Command: "RUN apt-get update"},
}
diff := computeLayerDiff(layers, layers)
if len(diff) != 2 {
t.Fatalf("expected 2 entries, got %d", len(diff))
}
for _, e := range diff {
if e.Status != "shared" {
t.Errorf("expected shared, got %s", e.Status)
}
}
}
func TestComputeLayerDiff_SharedPrefixThenDivergence(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100},
{Index: 2, Digest: "sha256:old", Size: 200},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100},
{Index: 2, Digest: "sha256:new1", Size: 300},
{Index: 3, Digest: "sha256:new2", Size: 150},
}
diff := computeLayerDiff(from, to)
// Lockstep: shared, then -/+ pair (no command match), then +1 added
if len(diff) != 4 {
t.Fatalf("expected 4 entries, got %d", len(diff))
}
expected := []struct {
status string
digest string
}{
{"shared", "sha256:base"},
{"removed", "sha256:old"}, // no command, different digest → -/+
{"added", "sha256:new1"},
{"added", "sha256:new2"}, // extra layer in to
}
for i, e := range expected {
if diff[i].Status != e.status {
t.Errorf("[%d] expected status %s, got %s", i, e.status, diff[i].Status)
}
if diff[i].Layer.Digest != e.digest {
t.Errorf("[%d] expected digest %s, got %s", i, e.digest, diff[i].Layer.Digest)
}
}
}
func TestComputeLayerDiff_SameCommandDifferentDigest(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:old", Size: 200, Command: "RUN apt-get update"},
{Index: 3, Digest: "sha256:old2", Size: 300, Command: "RUN pip install flask"},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:new", Size: 250, Command: "RUN apt-get update"},
{Index: 3, Digest: "sha256:new2", Size: 350, Command: "RUN pip install flask"},
}
diff := computeLayerDiff(from, to)
if len(diff) != 3 {
t.Fatalf("expected 3 entries, got %d", len(diff))
}
if diff[0].Status != "shared" {
t.Errorf("[0] expected shared, got %s", diff[0].Status)
}
if diff[1].Status != "rebuilt" {
t.Errorf("[1] expected rebuilt, got %s", diff[1].Status)
}
if diff[1].PrevLayer == nil || diff[1].PrevLayer.Size != 200 {
t.Error("[1] expected PrevLayer with size 200")
}
if diff[2].Status != "rebuilt" {
t.Errorf("[2] expected rebuilt, got %s", diff[2].Status)
}
}
func TestComputeLayerDiff_DifferentCommandDifferentDigest(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:old", Size: 200, Command: "RUN pip install requests==2.28"},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:new", Size: 250, Command: "RUN pip install requests==2.31"},
}
diff := computeLayerDiff(from, to)
if len(diff) != 3 {
t.Fatalf("expected 3 entries, got %d", len(diff))
}
if diff[0].Status != "shared" {
t.Errorf("[0] expected shared, got %s", diff[0].Status)
}
// Different command → -/+ pair
if diff[1].Status != "removed" {
t.Errorf("[1] expected removed, got %s", diff[1].Status)
}
if diff[2].Status != "added" {
t.Errorf("[2] expected added, got %s", diff[2].Status)
}
}
func TestComputeLayerDiff_InsertedLayer(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:bbb", Size: 200, Command: "RUN apt-get update"},
{Index: 3, Digest: "sha256:ccc", Size: 300, Command: "RUN pip install flask"},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:ddd", Size: 210, Command: "RUN apt-get update"},
{Index: 3, Digest: "sha256:eee", Size: 150, Command: "RUN apt-get install curl"},
{Index: 4, Digest: "sha256:fff", Size: 310, Command: "RUN pip install flask"},
}
diff := computeLayerDiff(from, to)
// Expected: shared, rebuilt, +added, rebuilt
expected := []string{"shared", "rebuilt", "added", "rebuilt"}
if len(diff) != len(expected) {
t.Fatalf("expected %d entries, got %d: %v", len(expected), len(diff), diffStatuses(diff))
}
for i, e := range expected {
if diff[i].Status != e {
t.Errorf("[%d] expected %s, got %s", i, e, diff[i].Status)
}
}
}
func TestComputeLayerDiff_RemovedLayer(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:bbb", Size: 200, Command: "RUN apt-get update"},
{Index: 3, Digest: "sha256:ccc", Size: 150, Command: "RUN apt-get install curl"},
{Index: 4, Digest: "sha256:ddd", Size: 300, Command: "RUN pip install flask"},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:aaa", Size: 100, Command: "ADD file in /"},
{Index: 2, Digest: "sha256:eee", Size: 210, Command: "RUN apt-get update"},
{Index: 3, Digest: "sha256:fff", Size: 310, Command: "RUN pip install flask"},
}
diff := computeLayerDiff(from, to)
// Expected: shared, rebuilt, -removed, rebuilt
expected := []string{"shared", "rebuilt", "removed", "rebuilt"}
if len(diff) != len(expected) {
t.Fatalf("expected %d entries, got %d: %v", len(expected), len(diff), diffStatuses(diff))
}
for i, e := range expected {
if diff[i].Status != e {
t.Errorf("[%d] expected %s, got %s", i, e, diff[i].Status)
}
}
}
// helper for test error messages
func diffStatuses(diff []LayerDiffEntry) []string {
var s []string
for _, d := range diff {
s = append(s, d.Status)
}
return s
}
func TestComputeLayerDiff_EmptyLayersMatchByCommand(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100},
{Index: 0, EmptyLayer: true, Command: "ENV FOO=bar"},
{Index: 2, Digest: "sha256:old", Size: 200},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:base", Size: 100},
{Index: 0, EmptyLayer: true, Command: "ENV FOO=bar"},
{Index: 2, Digest: "sha256:new", Size: 300},
}
diff := computeLayerDiff(from, to)
if len(diff) != 4 {
t.Fatalf("expected 4 entries, got %d", len(diff))
}
if diff[0].Status != "shared" || diff[1].Status != "shared" {
t.Error("first two entries should be shared (base layer + empty layer)")
}
// Different digests, no command → -/+ pair
if diff[2].Status != "removed" {
t.Errorf("[2] expected removed, got %s", diff[2].Status)
}
if diff[3].Status != "added" {
t.Errorf("[3] expected added, got %s", diff[3].Status)
}
}
func TestComputeLayerDiff_CompletelyDifferent(t *testing.T) {
from := []LayerDetail{
{Index: 1, Digest: "sha256:old1", Size: 100},
}
to := []LayerDetail{
{Index: 1, Digest: "sha256:new1", Size: 200},
{Index: 2, Digest: "sha256:new2", Size: 300},
}
diff := computeLayerDiff(from, to)
// Lockstep: -/+ pair for position 1, then +1 added
if len(diff) != 3 {
t.Fatalf("expected 3 entries, got %d", len(diff))
}
if diff[0].Status != "removed" {
t.Errorf("[0] expected removed, got %s", diff[0].Status)
}
if diff[1].Status != "added" {
t.Errorf("[1] expected added, got %s", diff[1].Status)
}
if diff[2].Status != "added" {
t.Errorf("[2] expected added, got %s", diff[2].Status)
}
}
func TestComputeLayerDiff_EmptyInputs(t *testing.T) {
diff := computeLayerDiff(nil, nil)
if len(diff) != 0 {
t.Fatalf("expected 0 entries, got %d", len(diff))
}
diff = computeLayerDiff(nil, []LayerDetail{{Index: 1, Digest: "sha256:a"}})
if len(diff) != 1 || diff[0].Status != "added" {
t.Error("expected 1 added entry")
}
diff = computeLayerDiff([]LayerDetail{{Index: 1, Digest: "sha256:a"}}, nil)
if len(diff) != 1 || diff[0].Status != "removed" {
t.Error("expected 1 removed entry")
}
}
func TestComputeVulnDiff_FixedAndNew(t *testing.T) {
from := []vulnMatch{
{CVEID: "CVE-2024-001", Severity: "Critical", Package: "openssl", Version: "1.1.0"},
{CVEID: "CVE-2024-002", Severity: "High", Package: "curl", Version: "7.85"},
{CVEID: "CVE-2024-003", Severity: "Medium", Package: "zlib", Version: "1.2.11"},
}
to := []vulnMatch{
{CVEID: "CVE-2024-002", Severity: "High", Package: "curl", Version: "7.85"},
{CVEID: "CVE-2025-001", Severity: "High", Package: "requests", Version: "2.31"},
}
diff := computeVulnDiff(from, to)
counts := map[string]int{}
for _, e := range diff {
counts[e.Status]++
}
if counts["fixed"] != 2 {
t.Errorf("expected 2 fixed, got %d", counts["fixed"])
}
if counts["new"] != 1 {
t.Errorf("expected 1 new, got %d", counts["new"])
}
if counts["unchanged"] != 1 {
t.Errorf("expected 1 unchanged, got %d", counts["unchanged"])
}
}
func TestComputeVulnDiff_AllFixed(t *testing.T) {
from := []vulnMatch{
{CVEID: "CVE-2024-001", Severity: "Critical"},
{CVEID: "CVE-2024-002", Severity: "High"},
}
diff := computeVulnDiff(from, nil)
for _, e := range diff {
if e.Status != "fixed" {
t.Errorf("expected fixed, got %s", e.Status)
}
}
if len(diff) != 2 {
t.Errorf("expected 2, got %d", len(diff))
}
}
func TestComputeVulnDiff_AllNew(t *testing.T) {
to := []vulnMatch{
{CVEID: "CVE-2025-001", Severity: "Critical"},
}
diff := computeVulnDiff(nil, to)
if len(diff) != 1 || diff[0].Status != "new" {
t.Error("expected 1 new entry")
}
}
func TestComputeVulnDiff_Empty(t *testing.T) {
diff := computeVulnDiff(nil, nil)
if len(diff) != 0 {
t.Errorf("expected 0, got %d", len(diff))
}
}
func TestComputeDiffSummary(t *testing.T) {
fromLayers := []LayerDetail{
{Index: 1, Size: 1000},
{Index: 2, Size: 2000},
}
toLayers := []LayerDetail{
{Index: 1, Size: 1000},
{Index: 2, Size: 2500},
{Index: 3, Size: 500},
}
vulnDiff := []VulnDiffEntry{
{Status: "fixed", Vuln: vulnMatch{Severity: "Critical"}},
{Status: "fixed", Vuln: vulnMatch{Severity: "High"}},
{Status: "fixed", Vuln: vulnMatch{Severity: "High"}},
{Status: "new", Vuln: vulnMatch{Severity: "Medium"}},
{Status: "unchanged", Vuln: vulnMatch{Severity: "Low"}},
}
summary := computeDiffSummary(fromLayers, toLayers, vulnDiff, true, nil, false)
if summary.SizeDelta != 1000 {
t.Errorf("expected size delta 1000, got %d", summary.SizeDelta)
}
if summary.LayerCountFrom != 2 {
t.Errorf("expected from count 2, got %d", summary.LayerCountFrom)
}
if summary.LayerCountTo != 3 {
t.Errorf("expected to count 3, got %d", summary.LayerCountTo)
}
if summary.VulnFixedCount != 3 {
t.Errorf("expected 3 fixed, got %d", summary.VulnFixedCount)
}
if summary.VulnNewCount != 1 {
t.Errorf("expected 1 new, got %d", summary.VulnNewCount)
}
if summary.VulnFixedBySev.Critical != 1 {
t.Errorf("expected 1 fixed critical, got %d", summary.VulnFixedBySev.Critical)
}
if summary.VulnFixedBySev.High != 2 {
t.Errorf("expected 2 fixed high, got %d", summary.VulnFixedBySev.High)
}
if summary.VulnNewBySev.Medium != 1 {
t.Errorf("expected 1 new medium, got %d", summary.VulnNewBySev.Medium)
}
if !summary.HasVulnData {
t.Error("expected HasVulnData to be true")
}
}
func TestComputeDiffSummary_NoVulnData(t *testing.T) {
summary := computeDiffSummary(
[]LayerDetail{{Size: 100}},
[]LayerDetail{{Size: 200}},
nil,
false,
nil,
false,
)
if summary.HasVulnData {
t.Error("expected HasVulnData to be false")
}
if summary.SizeDelta != 100 {
t.Errorf("expected size delta 100, got %d", summary.SizeDelta)
}
}
func TestComputeDiffSummary_SmallerImage(t *testing.T) {
summary := computeDiffSummary(
[]LayerDetail{{Size: 5000}, {Size: 3000}},
[]LayerDetail{{Size: 2000}},
nil,
false,
nil,
false,
)
if summary.SizeDelta != -6000 {
t.Errorf("expected size delta -6000, got %d", summary.SizeDelta)
}
if summary.LayerCountFrom != 2 || summary.LayerCountTo != 1 {
t.Error("unexpected layer counts")
}
}
// sbomStatusCounts tallies a diff result by status for compact assertions.
func sbomStatusCounts(diff []SbomDiffEntry) map[string]int {
counts := make(map[string]int)
for _, e := range diff {
counts[e.Status]++
}
return counts
}
func TestComputeSbomDiff_Identical(t *testing.T) {
pkgs := []sbomPackage{
{Name: "openssl", Version: "3.0.1", Type: "deb"},
{Name: "zlib", Version: "1.2.13", Type: "deb"},
}
diff := computeSbomDiff(pkgs, pkgs)
if len(diff) != 2 {
t.Fatalf("expected 2 entries, got %d", len(diff))
}
for _, e := range diff {
if e.Status != "unchanged" {
t.Errorf("expected unchanged, got %q for %s", e.Status, e.Package.Name)
}
}
}
func TestComputeSbomDiff_VersionBump(t *testing.T) {
from := []sbomPackage{{Name: "openssl", Version: "3.0.1", Type: "deb"}}
to := []sbomPackage{{Name: "openssl", Version: "3.0.2", Type: "deb"}}
diff := computeSbomDiff(from, to)
if len(diff) != 1 {
t.Fatalf("expected 1 entry, got %d", len(diff))
}
if diff[0].Status != "changed" {
t.Errorf("expected changed, got %q", diff[0].Status)
}
if diff[0].PrevVersion != "3.0.1" {
t.Errorf("expected PrevVersion 3.0.1, got %q", diff[0].PrevVersion)
}
if diff[0].Package.Version != "3.0.2" {
t.Errorf("expected Package.Version 3.0.2, got %q", diff[0].Package.Version)
}
}
func TestComputeSbomDiff_AddedRemovedEmpty(t *testing.T) {
diff := computeSbomDiff(nil, nil)
if len(diff) != 0 {
t.Errorf("expected 0 entries for empty inputs, got %d", len(diff))
}
diff = computeSbomDiff(nil, []sbomPackage{{Name: "curl", Version: "8.0", Type: "deb"}})
if len(diff) != 1 || diff[0].Status != "added" {
t.Errorf("expected single added entry, got %+v", diff)
}
diff = computeSbomDiff([]sbomPackage{{Name: "curl", Version: "8.0", Type: "deb"}}, nil)
if len(diff) != 1 || diff[0].Status != "removed" {
t.Errorf("expected single removed entry, got %+v", diff)
}
}
func TestComputeSbomDiff_SameNameDifferentType(t *testing.T) {
// A deb package and a binary share a name; the deb version bump must
// not pair against the binary entry.
from := []sbomPackage{
{Name: "curl", Version: "8.0", Type: "deb"},
{Name: "curl", Version: "8.0", Type: ""},
}
to := []sbomPackage{
{Name: "curl", Version: "8.1", Type: "deb"},
{Name: "curl", Version: "8.0", Type: ""},
}
diff := computeSbomDiff(from, to)
if len(diff) != 2 {
t.Fatalf("expected 2 entries, got %d", len(diff))
}
counts := sbomStatusCounts(diff)
if counts["changed"] != 1 || counts["unchanged"] != 1 {
t.Errorf("expected 1 changed + 1 unchanged, got %v", counts)
}
for _, e := range diff {
if e.Status == "changed" {
if e.Package.Type != "deb" || e.PrevVersion != "8.0" || e.Package.Version != "8.1" {
t.Errorf("changed entry paired wrong: %+v", e)
}
}
}
}
func TestComputeSbomDiff_Multiplicity(t *testing.T) {
// from {1.0, 1.1} vs to {1.1, 1.2}: 1.1 matches exactly, 1.0 pairs
// with 1.2 as a version change.
from := []sbomPackage{
{Name: "libssl", Version: "1.0", Type: "deb"},
{Name: "libssl", Version: "1.1", Type: "deb"},
}
to := []sbomPackage{
{Name: "libssl", Version: "1.1", Type: "deb"},
{Name: "libssl", Version: "1.2", Type: "deb"},
}
diff := computeSbomDiff(from, to)
if len(diff) != 2 {
t.Fatalf("expected 2 entries, got %d", len(diff))
}
counts := sbomStatusCounts(diff)
if counts["unchanged"] != 1 || counts["changed"] != 1 {
t.Errorf("expected 1 unchanged + 1 changed, got %v", counts)
}
for _, e := range diff {
if e.Status == "changed" && (e.PrevVersion != "1.0" || e.Package.Version != "1.2") {
t.Errorf("expected 1.0 to pair with 1.2, got %+v", e)
}
}
// Asymmetric counts: two from-side versions, one to-side version.
from = []sbomPackage{
{Name: "libssl", Version: "1.0", Type: "deb"},
{Name: "libssl", Version: "1.1", Type: "deb"},
}
to = []sbomPackage{{Name: "libssl", Version: "1.2", Type: "deb"}}
diff = computeSbomDiff(from, to)
counts = sbomStatusCounts(diff)
if counts["changed"] != 1 || counts["removed"] != 1 {
t.Errorf("expected 1 changed + 1 removed, got %v", counts)
}
}
func TestComputeSbomDiff_DuplicateEntriesOneSide(t *testing.T) {
// Two identical entries on the from side must not both match the
// single to-side entry (multiset, not set).
from := []sbomPackage{
{Name: "foo", Version: "1.0", Type: "npm"},
{Name: "foo", Version: "1.0", Type: "npm"},
}
to := []sbomPackage{{Name: "foo", Version: "1.0", Type: "npm"}}
diff := computeSbomDiff(from, to)
if len(diff) != 2 {
t.Fatalf("expected 2 entries, got %d", len(diff))
}
counts := sbomStatusCounts(diff)
if counts["unchanged"] != 1 || counts["removed"] != 1 {
t.Errorf("expected 1 unchanged + 1 removed, got %v", counts)
}
}
func TestComputeSbomDiff_DeterministicOrder(t *testing.T) {
from := []sbomPackage{
{Name: "zlib", Version: "1.2", Type: "deb"},
{Name: "curl", Version: "8.0", Type: "deb"},
{Name: "gone", Version: "0.1", Type: "npm"},
}
to := []sbomPackage{
{Name: "curl", Version: "8.1", Type: "deb"},
{Name: "zlib", Version: "1.2", Type: "deb"},
{Name: "fresh", Version: "2.0", Type: "npm"},
}
want := []SbomDiffEntry{
{Status: "changed", Package: sbomPackage{Name: "curl", Version: "8.1", Type: "deb"}, PrevVersion: "8.0"},
{Status: "added", Package: sbomPackage{Name: "fresh", Version: "2.0", Type: "npm"}},
{Status: "removed", Package: sbomPackage{Name: "gone", Version: "0.1", Type: "npm"}},
{Status: "unchanged", Package: sbomPackage{Name: "zlib", Version: "1.2", Type: "deb"}},
}
for run := range 5 {
diff := computeSbomDiff(from, to)
if len(diff) != len(want) {
t.Fatalf("run %d: expected %d entries, got %d", run, len(want), len(diff))
}
for i := range want {
if diff[i] != want[i] {
t.Errorf("run %d entry %d: expected %+v, got %+v", run, i, want[i], diff[i])
}
}
}
}
func TestComputeDiffSummary_PackageCounts(t *testing.T) {
sbomDiff := []SbomDiffEntry{
{Status: "added"},
{Status: "added"},
{Status: "removed"},
{Status: "changed"},
{Status: "unchanged"},
}
summary := computeDiffSummary(nil, nil, nil, false, sbomDiff, true)
if summary.PkgAddedCount != 2 {
t.Errorf("expected 2 added, got %d", summary.PkgAddedCount)
}
if summary.PkgRemovedCount != 1 {
t.Errorf("expected 1 removed, got %d", summary.PkgRemovedCount)
}
if summary.PkgChangedCount != 1 {
t.Errorf("expected 1 changed, got %d", summary.PkgChangedCount)
}
if !summary.HasSbomData {
t.Error("expected HasSbomData to be true")
}
}