|
1 | 1 | package main |
2 | 2 |
|
3 | 3 | import ( |
| 4 | + "fmt" |
4 | 5 | "strings" |
5 | 6 | "testing" |
6 | 7 |
|
7 | 8 | "github.com/cwayne18/vexscan/internal/analyze" |
8 | 9 | "github.com/cwayne18/vexscan/internal/ecosystem" |
| 10 | + "github.com/cwayne18/vexscan/internal/target" |
9 | 11 | ) |
10 | 12 |
|
11 | 13 | // fixplan renders a fix plan from a set of findings, telling the renderer which |
@@ -82,7 +84,9 @@ func TestFixPlanSeparatesNoFix(t *testing.T) { |
82 | 84 | t.Errorf("NO FIX YET should hold zlib1g only:\n%s", out) |
83 | 85 | } |
84 | 86 | lineWith(t, out, "1 of 2 affected findings have a fix.") |
85 | | - lineWith(t, out, "upgrading 1 package clears 1 advisory; 1 with no fix yet.") |
| 87 | + // Every count names its unit: the 1 that is cleared is an advisory and the |
| 88 | + // 1 that is not is a finding, and the sentence has to say so. |
| 89 | + lineWith(t, out, "upgrading 1 package clears 1 advisory; 1 finding has no fix yet.") |
86 | 90 | } |
87 | 91 |
|
88 | 92 | // TestFixPlanDoesNotCollapseUnorderableEcosystem is the safety rule: where the |
@@ -120,6 +124,73 @@ func TestFixPlanNothingFixable(t *testing.T) { |
120 | 124 | } |
121 | 125 | } |
122 | 126 |
|
| 127 | +// The fix plan's footer has to be its own. writeFooter repeats the main |
| 128 | +// report's summary and its section index, which named AFFECTED and RULED OUT |
| 129 | +// under a document whose only headings are UPGRADE and NO FIX YET -- an index |
| 130 | +// of sections the reader cannot find anywhere above it. |
| 131 | +func TestTheFixPlanFooterDoesNotIndexSectionsItDoesNotHave(t *testing.T) { |
| 132 | + var findings []analyze.Finding |
| 133 | + for i := 0; i < 40; i++ { |
| 134 | + findings = append(findings, linked( |
| 135 | + fmt.Sprintf("CVE-2023-%04d", i), fmt.Sprintf("pkg%02d", i), |
| 136 | + "1.0-1", "1.0-2", "HIGH")) |
| 137 | + } |
| 138 | + out := fixplan(t, map[string][]string{"os": {"Debian:12"}}, findings...) |
| 139 | + if n := strings.Count(out, "\n"); n <= footerThreshold { |
| 140 | + t.Fatalf("this test needs a report past the footer threshold, got %d lines", n) |
| 141 | + } |
| 142 | + if strings.Contains(out, "section(s)") { |
| 143 | + t.Errorf("the fix plan indexed sections it does not contain:\n%s", out) |
| 144 | + } |
| 145 | + // What it repeats instead is its own count of itself, at both ends. |
| 146 | + if n := strings.Count(out, "affected findings have a fix."); n != 2 { |
| 147 | + t.Errorf("want the fix summary at both ends, got %d:\n%s", n, out) |
| 148 | + } |
| 149 | +} |
| 150 | + |
| 151 | +// The section index does not belong in a fix plan, but a caveat does. "Part of |
| 152 | +// the target could not be read" is a fact about the scan and not about the |
| 153 | +// view, and a long report hiding its own header is the entire reason the footer |
| 154 | +// exists -- so dropping writeFooter must not drop that with it. |
| 155 | +func TestTheFixPlanFooterRepeatsTheCaveats(t *testing.T) { |
| 156 | + var findings []analyze.Finding |
| 157 | + for i := 0; i < 40; i++ { |
| 158 | + findings = append(findings, linked( |
| 159 | + fmt.Sprintf("CVE-2023-%04d", i), fmt.Sprintf("pkg%02d", i), |
| 160 | + "1.0-1", "1.0-2", "HIGH")) |
| 161 | + } |
| 162 | + out := renderFixPlan(&analyze.Result{ |
| 163 | + SchemaVersion: analyze.SchemaVersion, |
| 164 | + Target: "debian:12", |
| 165 | + Mode: "image", |
| 166 | + Ecosystems: []ecosystem.EcosystemResult{{ID: "os", Ecosystems: []string{"Debian:12"}, Components: 1}}, |
| 167 | + Findings: findings, |
| 168 | + Unreadable: &target.Unreadable{Count: 3, Paths: []string{"/var/lib/private"}}, |
| 169 | + }) |
| 170 | + if n := strings.Count(out, "INCOMPLETE: 3 path(s) could not be read"); n != 2 { |
| 171 | + t.Errorf("want the INCOMPLETE banner at both ends, got %d:\n%s", n, out) |
| 172 | + } |
| 173 | +} |
| 174 | + |
| 175 | +// A remediation view that silently omits rows is the one kind of report whose |
| 176 | +// shortness reads as good news. Everything the plan declines to plan for gets |
| 177 | +// counted somewhere. |
| 178 | +func TestTheFixPlanAccountsForWhatItDoesNotPlan(t *testing.T) { |
| 179 | + vexed := linked("CVE-2023-0001", "libc6", "2.36-9", "2.36-9+deb12u1", "HIGH") |
| 180 | + vexed.VEX = &ecosystem.VEXStatement{Status: "not_affected", Author: "SUSE"} |
| 181 | + undetermined := linked("CVE-2023-0002", "zlib1g", "1.2.13", "", "UNKNOWN") |
| 182 | + undetermined.Status = analyze.StatusUndetermined |
| 183 | + |
| 184 | + out := fixplan(t, map[string][]string{"os": {"Debian:12"}}, |
| 185 | + linked("CVE-2023-0003", "perl-base", "5.36.0-7", "5.36.0-7+deb12u3", "HIGH"), |
| 186 | + vexed, undetermined) |
| 187 | + |
| 188 | + lineWith(t, out, "1 already answered by a vendor VEX statement") |
| 189 | + lineWith(t, out, "1 undetermined finding(s) not shown") |
| 190 | + // And neither is counted as work: the plan is the one remaining row. |
| 191 | + lineWith(t, out, "1 of 1 affected findings have a fix.") |
| 192 | +} |
| 193 | + |
123 | 194 | func containsRow(lines []string, want string) bool { |
124 | 195 | for _, l := range lines { |
125 | 196 | if strings.Contains(l, want) { |
|
0 commit comments