Skip to content

Commit d5261e4

Browse files
committed
fix(upload): a 200 that stored nothing is not a publish
Publishing a report under the shared community credential returns HTTP 200 with no IngestionSnapshot: the server enriches and deliberately persists nothing, because that credential is shared by every unauthenticated user. The CLI falls back to it whenever a real credential cannot be read — an unset VULNETIX_API_KEY, or a keyring it cannot unlock in a headless session — so the run printed "[OK] published", named a category, counted findings, and left no ScannerRun anywhere. That is the same green-job-no-data failure this publish path exists to remove, and it was invisible: the two cases are identical in the HTTP status, and only the absence of a snapshot URL distinguished them. Each result now carries `persisted`, `gha upload --json` reports `notRecorded`, and both commands say so in plain output. It is not counted as a failure — the request did succeed, and failing the build here would break every unauthenticated user's pipeline — but it is no longer silent.
1 parent 4766e57 commit d5261e4

4 files changed

Lines changed: 94 additions & 0 deletions

File tree

cmd/gha.go

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -270,6 +270,14 @@ func runGHAUpload(cmd *cobra.Command, args []string) error {
270270
}
271271
}
272272

273+
// Every file accepted and none recorded is the failure mode this command
274+
// was built to remove, wearing a green tick. Say it once, loudly, rather
275+
// than once per file in a log nobody reads to the end.
276+
if n := countNotRecorded(results); n > 0 {
277+
dctx.Logger.Warnf("%d of %d file(s) were accepted but no scan was recorded.", n, len(results))
278+
dctx.Logger.Warn(notPersistedReason)
279+
}
280+
273281
if err := emitGHAJSON(results, env); err != nil {
274282
return err
275283
}
@@ -377,6 +385,11 @@ func emitGHAJSON(results []ghaFileResult, env vdb.CliEnv) error {
377385
"success": uploaded,
378386
"failed": failed,
379387
"skipped": skipped,
388+
// Additive, and the number worth watching: a file the server accepted
389+
// and did not record. It is not a failure — the request succeeded — but
390+
// it means the run produced no scan, and nothing else in this summary
391+
// would tell you that.
392+
"notRecorded": countNotRecorded(results),
380393
}
381394
if env.CI != nil {
382395
output["ci"] = map[string]any{
@@ -573,3 +586,15 @@ func tallyGHAResults(results []ghaFileResult) (uploaded, failed, skipped int) {
573586
}
574587
return uploaded, failed, skipped
575588
}
589+
590+
// countNotRecorded counts files the server accepted without storing a scan.
591+
func countNotRecorded(results []ghaFileResult) int {
592+
n := 0
593+
for _, r := range results {
594+
if r.Status == "uploaded" && !r.Persisted && r.Reason == notPersistedReason {
595+
n++
596+
}
597+
}
598+
599+
return n
600+
}

cmd/gha_submit.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,18 @@ type ghaFileResult struct {
5353

5454
SnapshotUuid string `json:"snapshotUuid,omitempty"`
5555
SnapshotURL string `json:"snapshotUrl,omitempty"`
56+
57+
// Persisted reports whether the server recorded the report as a scan, as
58+
// opposed to accepting it, enriching it and returning nothing.
59+
//
60+
// The two are indistinguishable from the HTTP status: both are 200. The
61+
// server declines to persist under the shared community credential, which
62+
// the CLI falls back to whenever a real credential cannot be read — an
63+
// unset VULNETIX_API_KEY, or a keyring it cannot unlock in a headless
64+
// session. The result was a run that printed "published", listed a category
65+
// and a finding count, and left no ScannerRun anywhere: the same
66+
// green-job-no-data failure this whole path exists to remove.
67+
Persisted bool `json:"persisted"`
5668
}
5769

5870
// ghaSubmitter holds everything the per-file publishers need.
@@ -188,9 +200,20 @@ func (s *ghaSubmitter) publishSARIF(res ghaFileResult, artifactName string, data
188200
res.SnapshotUuid = snapshotUuid
189201
res.SnapshotURL = snapshotURL
190202
res.PipelineID = snapshotUuid
203+
res.Persisted = snapshotUuid != ""
204+
if !res.Persisted {
205+
res.Reason = notPersistedReason
206+
s.warnf(" %s/%s %s", artifactName, res.File, notPersistedReason)
207+
}
191208
return res
192209
}
193210

211+
// notPersistedReason explains a 200 that recorded nothing. Community-tier
212+
// credentials get enrichment without persistence by design, and the CLI falls
213+
// back to the shared community credential whenever a real one cannot be read.
214+
const notPersistedReason = "accepted but not recorded: the request authenticated as the shared community credential, " +
215+
"so no scan was stored. Set VULNETIX_ORG_ID and VULNETIX_API_KEY, or run 'vulnetix auth login'."
216+
194217
// postSARIFChunks sends the document, splitting large ones so no single request
195218
// exceeds the server's body limit. Chunk 0 creates the run and snapshot; the
196219
// rest append under the uuid it returned.
@@ -488,6 +511,11 @@ func (s *ghaSubmitter) postSCA(res ghaFileResult, packages []vdb.CliPackageEntry
488511
res.SnapshotUuid = snapshotUuid
489512
res.SnapshotURL = snapshotURL
490513
res.PipelineID = snapshotUuid
514+
res.Persisted = snapshotUuid != ""
515+
if !res.Persisted {
516+
res.Reason = notPersistedReason
517+
s.warnf(" %s %s", res.File, notPersistedReason)
518+
}
491519
return res
492520
}
493521

cmd/gha_submit_test.go

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -388,3 +388,37 @@ func TestWorkspacePrefersCIOverGit(t *testing.T) {
388388
t.Errorf("workspace = %q, want the git root as fallback", got)
389389
}
390390
}
391+
392+
// A 200 that stored nothing is not a publish. The server returns exactly that
393+
// under the shared community credential, which the CLI falls back to whenever a
394+
// real one cannot be read — an unset VULNETIX_API_KEY, or a keyring it cannot
395+
// unlock in a headless session. Before this, such a run printed "published",
396+
// a category and a finding count, and left no ScannerRun anywhere.
397+
func TestNotRecordedIsCountedSeparatelyFromSuccess(t *testing.T) {
398+
results := []ghaFileResult{
399+
{Name: "gosec", File: "gosec.sarif", Status: "uploaded", Persisted: true, SnapshotUuid: "abc"},
400+
{Name: "semgrep", File: "semgrep.sarif", Status: "uploaded", Persisted: false, Reason: notPersistedReason},
401+
{Name: "readme", File: "README.md", Status: "skipped"},
402+
{Name: "broken", File: "broken.sarif", Status: "error", Error: "invalid"},
403+
}
404+
405+
if got := countNotRecorded(results); got != 1 {
406+
t.Errorf("countNotRecorded = %d, want 1", got)
407+
}
408+
409+
// The not-recorded file still counts as a success for the exit code: the
410+
// request did succeed, and failing the build on a community credential
411+
// would break every unauthenticated user's pipeline.
412+
uploaded, failed, skipped := tallyGHAResults(results)
413+
if uploaded != 2 || failed != 1 || skipped != 1 {
414+
t.Errorf("tally = (%d, %d, %d), want (2, 1, 1)", uploaded, failed, skipped)
415+
}
416+
417+
// A dry run reports uploaded with no snapshot, and must not be mistaken for
418+
// the community-credential case.
419+
if got := countNotRecorded([]ghaFileResult{
420+
{Status: "uploaded", Persisted: false, Reason: "dry run; nothing was sent"},
421+
}); got != 0 {
422+
t.Errorf("a dry run must not count as not-recorded, got %d", got)
423+
}
424+
}

cmd/upload.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -199,6 +199,13 @@ func printPublishResult(t *display.Terminal, filePath string, res ghaFileResult,
199199
if res.Status == "duplicate" {
200200
mark, suffix = display.WarningMark(t), " already published (same scan)"
201201
}
202+
// A 200 that stored nothing is not a publish. The server returns exactly
203+
// that for the shared community credential, which the CLI falls back to
204+
// whenever a real one cannot be read — so this said "published", printed a
205+
// category and a finding count, and left no record anywhere.
206+
if res.Status == "uploaded" && !res.Persisted && res.Reason == notPersistedReason {
207+
mark, suffix = display.WarningMark(t), " accepted, but no scan was recorded"
208+
}
202209
b.WriteString(mark + " " + display.Bold(t, filepath.Base(filePath)) + suffix + "\n")
203210

204211
kv := []display.KVPair{{Key: "Category", Value: res.Category}}

0 commit comments

Comments
 (0)