Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
110 changes: 110 additions & 0 deletions toolkit/fixtures/fixtures.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
// Package fixtures provides definitions and helpers implementing the [security
// reporting fixture spec].
//
// [security reporting fixture spec]: https://clairproject.org/TODO
package fixtures

import (
"bufio"
"context"
"encoding/csv"
"errors"
"fmt"
"io"
"iter"
"regexp"
"strings"
)

// These are the known media types.
const (
MediaTypeVEX = `application/csaf+json`
MediaTypeManifest1 = `application/vnd.com.redhat.container.acceptancetest.v1+csv`
MediaTypeBOM = `application/spdx+json`
)

// These are the compressed variants of the known media types.
const (
MediaTypeZstdVEX = `application/csaf+json+zstd`
MediaTypeZstdManifest1 = `application/vnd.com.redhat.container.acceptancetest.v1+csv+zstd`
MediaTypeZstdBOM = `application/spdx+json+zstd`
)

func ParseManifest(ctx context.Context, mt string, rd io.Reader) (iter.Seq2[ManifestRecord, error], error) {
mr := csv.NewReader(bufio.NewReader(rd))
mr.ReuseRecord = true
mr.Comment = '#'
switch mt {
case MediaTypeManifest1, MediaTypeZstdManifest1:
mr.FieldsPerRecord = 3
default:
return nil, fmt.Errorf("fixtures: unknown media type %q", mt)
}

seq := func(yield func(ManifestRecord, error) bool) {
for {
var m ManifestRecord
s, err := mr.Read()
switch {
case err == nil:
case errors.Is(err, io.EOF):
return
default:
err := fmt.Errorf("fixtures: error at position %d: %w", mr.InputOffset(), err)
yield(m, err)
return
}

if !trackingID.MatchString(s[0]) {
l, c := mr.FieldPos(0)
err := fmt.Errorf("fixtures: invalid tracking ID at line %d, column %d", l, c)
yield(m, err)
return
}
m.ID = s[0]

if len(s[1]) == 0 {
l, c := mr.FieldPos(1)
err := fmt.Errorf("fixtures: invalid product ID at line %d, column %d", l, c)
yield(m, err)
return
}
m.Product = s[1]

switch {
case strings.EqualFold(s[2], `affected`):
m.Status = StatusAffected
case strings.EqualFold(s[2], `unaffected`):
m.Status = StatusUnaffected
default:
m.Status = StatusUnknown
}

if !yield(m, nil) {
return
}
}
}

return seq, nil
}

// TrackingID is the regexp that a CSAF document's ID must conform to.
//
// See also: https://docs.oasis-open.org/csaf/csaf/v2.0/os/csaf-v2.0-os.html#321124-document-property---tracking---id
var trackingID = regexp.MustCompile(`^[\S](.*[\S])?$`)

type ManifestRecord struct {
ID string
Product string
Status VulnerabilityStatus
}

//go:generate go run golang.org/x/tools/cmd/stringer@latest -linecomment -type=VulnerabilityStatus
type VulnerabilityStatus uint

const (
StatusUnknown VulnerabilityStatus = iota // UNKNOWN
StatusAffected // AFFECTED
StatusUnaffected // UNAFFECTED
)
72 changes: 72 additions & 0 deletions toolkit/fixtures/manifest_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
package fixtures

import (
"bytes"
"encoding/json"
"path/filepath"
"strings"
"testing"

"github.com/google/go-cmp/cmp"
"golang.org/x/tools/txtar"
)

func TestManifest(t *testing.T) {
ms, _ := filepath.Glob("testdata/manifest/*.txtar")
for _, m := range ms {
name := strings.TrimSuffix(filepath.Base(m), ".txtar")
t.Run(name, func(t *testing.T) {
t.Parallel()
ar, err := txtar.ParseFile(m)
if err != nil {
t.Fatal(err)
}
var input, wantJSON *txtar.File
for i := range ar.Files {
f := &ar.Files[i]
switch f.Name {
case "input.csv":
input = f
case "want.json":
wantJSON = f
default: // Skip
}
}
if input == nil || wantJSON == nil {
t.Fatal(`malformed archive: missing "input.csv" or "want.json"`)
}

ctx := t.Context()
seq, err := ParseManifest(ctx, MediaTypeManifest1, bytes.NewReader(input.Data))
if err != nil {
t.Fatal(err)
}
wantBuf := bytes.NewBuffer(wantJSON.Data)

lineNo := 0
for r, err := range seq {
lineNo++
if err != nil {
t.Fatalf("input.csv: line %d: unexpected error: %v", lineNo, err)
}
wantLine, wantErr := wantBuf.ReadBytes('\n')
if wantErr != nil && len(wantLine) == 0 {
t.Fatalf("want.json: line %d: %v", lineNo, wantErr)
}
var want []string
if err := json.Unmarshal(wantLine, &want); err != nil {
t.Fatalf("want.json: line %d: %v", lineNo, err)
}

got := []string{r.ID, r.Product, r.Status.String()}
if !cmp.Equal(got, want) {
t.Error(cmp.Diff(got, want))
}
}

if l := wantBuf.Len(); l != 0 {
t.Errorf("want.json: %d unread bytes left", l)
}
})
}
}
5 changes: 5 additions & 0 deletions toolkit/fixtures/testdata/manifest/Simple.txtar
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
-- input.csv --
# Comments are ignored.
1,1,
-- want.json --
["1","1","UNKNOWN"]
26 changes: 26 additions & 0 deletions toolkit/fixtures/vulnerabilitystatus_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

7 changes: 5 additions & 2 deletions toolkit/go.mod
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
module github.com/quay/claircore/toolkit

go 1.24
go 1.24.0

require github.com/google/go-cmp v0.7.0
require (
github.com/google/go-cmp v0.7.0
golang.org/x/tools v0.41.0
)
2 changes: 2 additions & 0 deletions toolkit/go.sum
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
golang.org/x/tools v0.41.0 h1:a9b8iMweWG+S0OBnlU36rzLp20z1Rp10w+IY2czHTQc=
golang.org/x/tools v0.41.0/go.mod h1:XSY6eDqxVNiYgezAVqqCeihT4j1U2CCsqvH3WhQpnlg=