Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add v6 distribution client #2150

Merged
merged 3 commits into from
Nov 12, 2024
Merged
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
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
Loading