Skip to content

Commit

Permalink
Add v6 distribution client (#2150)
Browse files Browse the repository at this point in the history
* add distribution client

Signed-off-by: Alex Goodman <[email protected]>

* show message on deprecation or EOL

Signed-off-by: Alex Goodman <[email protected]>

* rename metadata.json to description.json

Signed-off-by: Alex Goodman <[email protected]>

---------

Signed-off-by: Alex Goodman <[email protected]>
  • Loading branch information
wagoodman authored Nov 12, 2024
1 parent fbc29c4 commit e641347
Show file tree
Hide file tree
Showing 10 changed files with 1,342 additions and 0 deletions.
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ go 1.23.0
require (
github.com/CycloneDX/cyclonedx-go v0.9.1
github.com/Masterminds/sprig/v3 v3.3.0
github.com/OneOfOne/xxhash v1.2.8
github.com/acarl005/stripansi v0.0.0-20180116102854-5a71ef0e047d
github.com/adrg/xdg v0.5.3
github.com/anchore/bubbly v0.0.0-20231115134915-def0aba654a9
Expand Down
45 changes: 45 additions & 0 deletions grype/db/internal/schemaver/schema_ver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package schemaver

import (
"fmt"
"strconv"
"strings"
)

type SchemaVer string

func New(model, revision, addition int) SchemaVer {
return SchemaVer(fmt.Sprintf("%d.%d.%d", model, revision, addition))
}

func (s SchemaVer) String() string {
return string(s)
}

func (s SchemaVer) ModelPart() (int, bool) {
v, ok := parseVersionPart(s, 0)
if v == 0 {
ok = false
}
return v, ok
}

func (s SchemaVer) RevisionPart() (int, bool) {
return parseVersionPart(s, 1)
}

func (s SchemaVer) AdditionPart() (int, bool) {
return parseVersionPart(s, 2)
}

func parseVersionPart(s SchemaVer, index int) (int, bool) {
parts := strings.Split(string(s), ".")
if len(parts) <= index {
return 0, false
}
value, err := strconv.Atoi(parts[index])
if err != nil {
return 0, false
}
return value, true
}
95 changes: 95 additions & 0 deletions grype/db/internal/schemaver/schema_ver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
package schemaver

import (
"fmt"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestSchemaVer_VersionComponents(t *testing.T) {
tests := []struct {
name string
version SchemaVer
expectedModel int
expectedRevision int
expectedAddition int
}{
{
name: "go case",
version: "1.2.3",
expectedModel: 1,
expectedRevision: 2,
expectedAddition: 3,
},
{
name: "model only",
version: "1.0.0",
expectedModel: 1,
expectedRevision: 0,
expectedAddition: 0,
},
{
name: "invalid model",
version: "0.2.3",
expectedModel: -1,
expectedRevision: 2,
expectedAddition: 3,
},
{
name: "invalid version format",
version: "invalid.version",
expectedModel: -1,
expectedRevision: -1,
expectedAddition: -1,
},
{
name: "zero version",
version: "0.0.0",
expectedModel: -1,
expectedRevision: 0,
expectedAddition: 0,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
type subject struct {
name string
exp int
fn func() (int, bool)
}

for _, sub := range []subject{
{
name: "model",
exp: tt.expectedModel,
fn: tt.version.ModelPart,
},
{
name: "revision",
exp: tt.expectedRevision,
fn: tt.version.RevisionPart,
},
{
name: "addition",
exp: tt.expectedAddition,
fn: tt.version.AdditionPart,
},
} {
t.Run(sub.name, func(t *testing.T) {
act, ok := sub.fn()

if sub.exp == -1 {
require.False(t, ok, fmt.Sprintf("Expected %s to be invalid", sub.name))
return
}
require.True(t, ok, fmt.Sprintf("Expected %s to be valid", sub.name))
assert.Equal(t, sub.exp, act, fmt.Sprintf("Expected %s to be %d, got %d", sub.name, sub.exp, act))
})
}

})
}
}
87 changes: 87 additions & 0 deletions grype/db/v6/description.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package v6

import (
"fmt"
"path"
"time"

"github.com/OneOfOne/xxhash"
"github.com/spf13/afero"

"github.com/anchore/grype/grype/db/internal/schemaver"
"github.com/anchore/grype/internal/file"
)

const DescriptionFileName = "description.json"

type Description struct {
// SchemaVersion is the version of the DB schema
SchemaVersion schemaver.SchemaVer `json:"schemaVersion,omitempty"`

// Built is the timestamp the database was built
Built Time `json:"built"`

// Checksum is the self-describing digest of the database file
Checksum string `json:"checksum"`
}

type Time struct {
time.Time
}

func (t Time) MarshalJSON() ([]byte, error) {
return []byte(fmt.Sprintf("%q", t.String())), nil
}

func (t *Time) UnmarshalJSON(data []byte) error {
str := string(data)
if len(str) < 2 || str[0] != '"' || str[len(str)-1] != '"' {
return fmt.Errorf("invalid time format")
}
str = str[1 : len(str)-1]

parsedTime, err := time.Parse(time.RFC3339, str)
if err != nil {
return err
}

t.Time = parsedTime.In(time.UTC)
return nil
}

func (t Time) String() string {
return t.Time.UTC().Round(time.Second).Format(time.RFC3339)
}

func NewDescriptionFromDir(fs afero.Fs, dir string) (*Description, error) {
// checksum the DB file
dbFilePath := path.Join(dir, VulnerabilityDBFileName)
digest, err := file.HashFile(fs, dbFilePath, xxhash.New64())
if err != nil {
return nil, fmt.Errorf("failed to calculate checksum for DB file (%s): %w", dbFilePath, err)
}
namedDigest := fmt.Sprintf("xxh64:%s", digest)

// access the DB to get the built time and schema version
r, err := NewReader(Config{
DBDirPath: dir,
})
if err != nil {
return nil, err
}

meta, err := r.GetDBMetadata()
if err != nil {
return nil, err
}

return &Description{
SchemaVersion: schemaver.New(meta.Model, meta.Revision, meta.Addition),
Built: Time{Time: *meta.BuildTimestamp},
Checksum: namedDigest,
}, nil
}

func (m Description) String() string {
return fmt.Sprintf("DB(version=%s built=%s checksum=%s)", m.SchemaVersion, m.Built, m.Checksum)
}
127 changes: 127 additions & 0 deletions grype/db/v6/description_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
package v6

import (
"encoding/json"
"fmt"
"io"
"os"
"path"
"testing"
"time"

"github.com/OneOfOne/xxhash"
"github.com/spf13/afero"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/anchore/grype/grype/db/internal/schemaver"
)

func TestNewDatabaseDescriptionFromDir(t *testing.T) {
tempDir := t.TempDir()

// make a test DB
s, err := NewWriter(Config{DBDirPath: tempDir})
require.NoError(t, err)
require.NoError(t, s.SetDBMetadata())
expected, err := s.GetDBMetadata()
require.NoError(t, err)
require.NoError(t, s.Close())

// get the xxhash of the db file
hasher := xxhash.New64()
f, err := os.Open(path.Join(tempDir, VulnerabilityDBFileName))
require.NoError(t, err)
_, err = io.Copy(hasher, f)
require.NoError(t, err)
require.NoError(t, f.Close())
expectedHash := fmt.Sprintf("xxh64:%x", hasher.Sum(nil))

// run the test subject
description, err := NewDescriptionFromDir(afero.NewOsFs(), tempDir)
require.NoError(t, err)
require.NotNil(t, description)

// did it work?
assert.Equal(t, Description{
SchemaVersion: schemaver.New(expected.Model, expected.Revision, expected.Addition),
Built: Time{*expected.BuildTimestamp},
Checksum: expectedHash,
}, *description)
}

func TestTime_JSONMarshalling(t *testing.T) {
tests := []struct {
name string
time Time
expected string
}{
{
name: "go case",
time: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
expected: `"2023-09-26T12:00:00Z"`,
},
{
name: "convert to utc",
time: Time{time.Date(2023, 9, 26, 13, 0, 0, 0, time.FixedZone("UTC+1", 3600))},
expected: `"2023-09-26T12:00:00Z"`,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
jsonData, err := json.Marshal(tt.time)
require.NoError(t, err)
require.Equal(t, tt.expected, string(jsonData))
})
}
}

func TestTime_JSONUnmarshalling(t *testing.T) {
tests := []struct {
name string
jsonData string
expectedTime Time
expectError require.ErrorAssertionFunc
}{
{
name: "use zulu offset",
jsonData: `"2023-09-26T12:00:00Z"`,
expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
},
{
name: "use tz offset in another timezone",
jsonData: `"2023-09-26T14:00:00+02:00"`,
expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
},
{
name: "use tz offset that is utc",
jsonData: `"2023-09-26T12:00:00+00:00"`,
expectedTime: Time{time.Date(2023, 9, 26, 12, 0, 0, 0, time.UTC)},
},
{
name: "invalid format",
jsonData: `"invalid-time-format"`,
expectError: require.Error,
},
{
name: "invalid json",
jsonData: `invalid`,
expectError: require.Error,
},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if tt.expectError == nil {
tt.expectError = require.NoError
}
var parsedTime Time
err := json.Unmarshal([]byte(tt.jsonData), &parsedTime)
tt.expectError(t, err)
if err == nil {
assert.Equal(t, tt.expectedTime.Time, parsedTime.Time)
}
})
}
}
Loading

0 comments on commit e641347

Please sign in to comment.