diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ace6911..6329ee8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,9 +13,13 @@ jobs: os: [macos-latest, ubuntu-latest] steps: - name: Checkout code - uses: actions/checkout@v1 + uses: actions/checkout@v4 with: fetch-depth: 1 + - name: Setup Go + uses: actions/setup-go@v5 + with: + go-version: '1.24' - name: Get dependencies run: go get -v -t -d ./... - name: Build project diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index ec9beb8..e16d3fc 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -8,13 +8,16 @@ jobs: name: Lint runs-on: ubuntu-latest steps: + - uses: actions/setup-go@v5 + with: + go-version: '1.24' - name: Checkout code - uses: actions/checkout@v2 - - name: Install golangci-lint + uses: actions/checkout@v4 + - name: Install golangci-lint run: | go version - curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v1.52.2 + curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(go env GOPATH)/bin v2.5.0 - name: Run required linters in .golangci.yml plus hard-coded ones here run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m - name: Run optional linters (not required to pass) - run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m --issues-exit-code=0 -E dupl -E gocritic -E gosimple -E lll -E prealloc + run: $(go env GOPATH)/bin/golangci-lint run --timeout=3m --issues-exit-code=0 -E dupl -E gocritic -E lll -E prealloc diff --git a/.golangci.yml b/.golangci.yml index 516118e..141f8ba 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -1,5 +1,10 @@ # Do not delete linter settings. Linters like gocritic can be enabled on the command line. +version: 2 + +run: + timeout: 3m + linters-settings: dupl: threshold: 100 @@ -26,38 +31,30 @@ linters-settings: gofmt: simplify: false goimports: - golint: + revive: min-confidence: 0 govet: - check-shadowing: true + enable: + - shadow lll: line-length: 140 - maligned: - suggest-new: true misspell: locale: US linters: disable-all: true enable: - - deadcode - errcheck - goconst - gocyclo - - gofmt - - goimports - - golint + - revive - gosec - govet - ineffassign - - maligned - misspell - staticcheck - - structcheck - - typecheck - unconvert - unused - - varcheck issues: diff --git a/Makefile b/Makefile index 8b76620..de03c07 100644 --- a/Makefile +++ b/Makefile @@ -12,7 +12,7 @@ ifeq ($(MAKECMDGOALS),lint) GOLINT_ARGS ?= run --timeout=3m else ifeq ($(MAKECMDGOALS),lint-extra) - GOLINT_ARGS ?= run --timeout=3m --issues-exit-code=0 -E dupl -E gocritic -E gosimple -E lll -E prealloc + GOLINT_ARGS ?= run --timeout=3m --issues-exit-code=0 -E dupl -E gocritic -E lll -E prealloc endif endif diff --git a/evidence.go b/evidence.go index 202feda..d968199 100644 --- a/evidence.go +++ b/evidence.go @@ -1,9 +1,13 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid -import "time" +import ( + "errors" + "fmt" + "time" +) // Evidence models a evidence-entry type Evidence struct { @@ -53,3 +57,34 @@ func (e *Evidence) AddProcess(p Process) error { return nil } + +// Valid validates the Evidence receiver to ensure it has valid required fields +func (e Evidence) Valid() error { + if e.DeviceID == "" { + return errors.New("evidence device-id is empty") + } + + if e.Date.IsZero() { + return errors.New("evidence date is zero") + } + + // Validate Files if present + if e.Files != nil { + for i, file := range *e.Files { + if err := file.Valid(); err != nil { + return fmt.Errorf("evidence file[%d] invalid: %w", i, err) + } + } + } + + // Validate Processes if present + if e.Processes != nil { + for i, process := range *e.Processes { + if process.ProcessName == "" { + return fmt.Errorf("evidence process[%d] process-name is empty", i) + } + } + } + + return nil +} diff --git a/evidence_test.go b/evidence_test.go index b71d0d0..cea0088 100644 --- a/evidence_test.go +++ b/evidence_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -47,3 +47,171 @@ func TestEvidence_Roundtrip(t *testing.T) { assert.Equal(t, tv.Date.UTC(), actual.Date.UTC()) // compare as UTC assert.Equal(t, tv.DeviceID, actual.DeviceID) } + +func TestEvidence_Valid_empty_device_id(t *testing.T) { + evidence := Evidence{ + DeviceID: "", + Date: time.Now(), + } + + err := evidence.Valid() + + assert.EqualError(t, err, "evidence device-id is empty") +} + +func TestEvidence_Valid_zero_date(t *testing.T) { + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Time{}, + } + + err := evidence.Valid() + + assert.EqualError(t, err, "evidence date is zero") +} + +func TestEvidence_Valid_simple_valid(t *testing.T) { + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Now(), + } + + err := evidence.Valid() + + assert.NoError(t, err) +} + +func TestEvidence_Valid_with_valid_files(t *testing.T) { + files := Files{ + File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + }, + File{ + FileSystemItem: FileSystemItem{ + FsName: "config.ini", + }, + }, + } + + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Now(), + ResourceCollection: ResourceCollection{ + PathElements: PathElements{ + Files: &files, + }, + }, + } + + err := evidence.Valid() + + assert.NoError(t, err) +} + +func TestEvidence_Valid_with_invalid_files(t *testing.T) { + files := Files{ + File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + }, + File{ + FileSystemItem: FileSystemItem{ + FsName: "", // empty fs-name + }, + }, + } + + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Now(), + ResourceCollection: ResourceCollection{ + PathElements: PathElements{ + Files: &files, + }, + }, + } + + err := evidence.Valid() + + assert.EqualError(t, err, "evidence file[1] invalid: file fs-name is empty") +} + +func TestEvidence_Valid_with_valid_processes(t *testing.T) { + processes := Processes{ + Process{ + ProcessName: "test.exe", + }, + Process{ + ProcessName: "service.exe", + }, + } + + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Now(), + ResourceCollection: ResourceCollection{ + Processes: &processes, + }, + } + + err := evidence.Valid() + + assert.NoError(t, err) +} + +func TestEvidence_Valid_with_invalid_processes(t *testing.T) { + processes := Processes{ + Process{ + ProcessName: "test.exe", + }, + Process{ + ProcessName: "", // empty process name + }, + } + + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Now(), + ResourceCollection: ResourceCollection{ + Processes: &processes, + }, + } + + err := evidence.Valid() + + assert.EqualError(t, err, "evidence process[1] process-name is empty") +} + +func TestEvidence_Valid_with_mixed_valid_resources(t *testing.T) { + files := Files{ + File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + }, + } + + processes := Processes{ + Process{ + ProcessName: "service.exe", + }, + } + + evidence := Evidence{ + DeviceID: "test-device", + Date: time.Now(), + ResourceCollection: ResourceCollection{ + PathElements: PathElements{ + Files: &files, + }, + Processes: &processes, + }, + } + + err := evidence.Valid() + + assert.NoError(t, err) +} diff --git a/example_validation_test.go b/example_validation_test.go new file mode 100644 index 0000000..ddc16b1 --- /dev/null +++ b/example_validation_test.go @@ -0,0 +1,83 @@ +// Copyright 2020-2025 Contributors to the Veraison project. +// SPDX-License-Identifier: Apache-2.0 + +package swid + +import ( + "fmt" + "time" + + "github.com/google/uuid" +) + +func ExampleTagID_Valid() { + // Valid string TagID + validStringTagID := TagID{val: "com.acme.product-v1.0.0"} + if err := validStringTagID.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Println("String TagID is valid") + } + + // Valid UUID TagID + validUUIDTagID := TagID{val: uuid.MustParse("550e8400-e29b-41d4-a716-446655440000")} + if err := validUUIDTagID.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Println("UUID TagID is valid") + } + + // Invalid empty string TagID + emptyStringTagID := TagID{val: ""} + if err := emptyStringTagID.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } + + // Invalid nil TagID + nilTagID := TagID{val: nil} + if err := nilTagID.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } + + // Output: + // String TagID is valid + // UUID TagID is valid + // Error: tag-id string value is empty + // Error: tag-id value is nil +} + +func ExampleEvidence_Valid() { + // Valid Evidence + validEvidence := Evidence{ + DeviceID: "device-001", + Date: time.Now(), + } + if err := validEvidence.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } else { + fmt.Println("Evidence is valid") + } + + // Invalid Evidence - empty DeviceID + invalidEvidence1 := Evidence{ + DeviceID: "", + Date: time.Now(), + } + if err := invalidEvidence1.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } + + // Invalid Evidence - zero Date + invalidEvidence2 := Evidence{ + DeviceID: "device-001", + Date: time.Time{}, + } + if err := invalidEvidence2.Valid(); err != nil { + fmt.Printf("Error: %v\n", err) + } + + // Output: + // Evidence is valid + // Error: evidence device-id is empty + // Error: evidence date is zero +} diff --git a/file.go b/file.go index 51de4ba..5e60b59 100644 --- a/file.go +++ b/file.go @@ -1,8 +1,10 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid +import "errors" + // File models CoSWID file-entry type File struct { GlobalAttributes @@ -23,3 +25,25 @@ type File struct { // match, the the file has not been modified in any fashion. Hash *HashEntry `cbor:"7,keyasint,omitempty" json:"hash,omitempty" xml:"hash,attr,omitempty"` } + +// Valid validates the File receiver to ensure it has valid required and optional fields +func (f File) Valid() error { + // Check mandatory fields + if f.FsName == "" { + return errors.New("file fs-name is empty") + } + + // Validate optional elements if present + if f.Hash != nil { + if err := ValidHashEntry(f.Hash.HashAlgID, f.Hash.HashValue); err != nil { + return err + } + } + + // Size validation - if present, should be non-negative + if f.Size != nil && *f.Size < 0 { + return errors.New("file size cannot be negative") + } + + return nil +} diff --git a/file_test.go b/file_test.go index ca04cf1..8790826 100644 --- a/file_test.go +++ b/file_test.go @@ -1,10 +1,12 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid import ( "testing" + + "github.com/stretchr/testify/assert" ) var ( @@ -92,3 +94,84 @@ func TestFile_RoundtripMinset(t *testing.T) { roundTripper(t, tv, expectedCBOR) } + +func TestFile_Valid_valid_file(t *testing.T) { + file := testFileMinSet + + err := file.Valid() + + assert.NoError(t, err) +} + +func TestFile_Valid_empty_fs_name(t *testing.T) { + file := File{ + FileSystemItem: FileSystemItem{ + Location: "bin/", + FsName: "", // empty fs-name + }, + } + + err := file.Valid() + + assert.EqualError(t, err, "file fs-name is empty") +} + +func TestFile_Valid_with_valid_hash(t *testing.T) { + file := File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + Hash: &HashEntry{ + HashAlgID: Sha256, + HashValue: make([]byte, 32), // valid 32-byte hash for SHA-256 + }, + } + + err := file.Valid() + + assert.NoError(t, err) +} + +func TestFile_Valid_with_invalid_hash(t *testing.T) { + file := File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + Hash: &HashEntry{ + HashAlgID: Sha256, + HashValue: make([]byte, 16), // invalid 16-byte hash for SHA-256 (should be 32) + }, + } + + err := file.Valid() + + assert.Contains(t, err.Error(), "length mismatch for hash algorithm") +} + +func TestFile_Valid_with_negative_size(t *testing.T) { + negativeSize := int64(-1) + file := File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + Size: &negativeSize, + } + + err := file.Valid() + + assert.EqualError(t, err, "file size cannot be negative") +} + +func TestFile_Valid_with_valid_size(t *testing.T) { + validSize := int64(1024) + file := File{ + FileSystemItem: FileSystemItem{ + FsName: "test.exe", + }, + Size: &validSize, + } + + err := file.Valid() + + assert.NoError(t, err) +} diff --git a/hashentry_test.go b/hashentry_test.go index 8d2cfa6..931f9e9 100644 --- a/hashentry_test.go +++ b/hashentry_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -377,7 +377,7 @@ func TestHashEntry_Set_mismatched_input(t *testing.T) { } func TestHashEntry_ValidHashEntry_unknown_algo(t *testing.T) { - var unknownAlgID uint64 = 0 + var unknownAlgID uint64 err := ValidHashEntry(unknownAlgID, []byte{}) assert.EqualError(t, err, "unknown hash algorithm 0") } diff --git a/payload.go b/payload.go index 7ff0a16..100aa9a 100644 --- a/payload.go +++ b/payload.go @@ -1,4 +1,4 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -27,6 +27,7 @@ func (p *Payload) AddDirectory(d Directory) error { return nil } +// AddFile adds a File to the Payload func (p *Payload) AddFile(f File) error { if p.Files == nil { p.Files = new(Files) diff --git a/tagid.go b/tagid.go index d699a94..4a259b3 100644 --- a/tagid.go +++ b/tagid.go @@ -1,4 +1,4 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -89,7 +89,7 @@ func (t TagID) String() string { } } -// Returns TagID in URI representation according to CoSWID Spec +// URI returns TagID in URI representation according to CoSWID Spec // useful for URI fields like link->href func (t TagID) URI() string { switch v := t.val.(type) { @@ -102,6 +102,32 @@ func (t TagID) URI() string { } } +// Valid validates the TagID receiver to ensure it has a valid value +func (t TagID) Valid() error { + if t.val == nil { + return errors.New("tag-id value is nil") + } + + switch v := t.val.(type) { + case string: + if v == "" { + return errors.New("tag-id string value is empty") + } + case uuid.UUID: + if v == uuid.Nil { + return errors.New("tag-id UUID value is nil UUID") + } + // Check UUID variant as per RFC4122 + if variant := v.Variant(); variant != uuid.RFC4122 { + return fmt.Errorf("tag-id UUID expecting RFC4122 variant, got %s instead", variant) + } + default: + return fmt.Errorf("tag-id value must be string or uuid.UUID, got %T", v) + } + + return nil +} + // MarshalXMLAttr encodes the TagID receiver as XML attribute func (t TagID) MarshalXMLAttr(name xml.Name) (xml.Attr, error) { return xml.Attr{Name: name, Value: t.String()}, nil diff --git a/tagid_test.go b/tagid_test.go index 721b7a3..e1a5b0b 100644 --- a/tagid_test.go +++ b/tagid_test.go @@ -1,4 +1,4 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -7,14 +7,20 @@ import ( "encoding/xml" "testing" + "github.com/google/uuid" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) +const ( + testUUIDString = "00010001-0001-0001-0001-000100010001" + testTagIDString = "example.acme.roadrunner-sw-v1-0-0" +) + func TestTagID_NewFromUUIDString(t *testing.T) { - tv := "00010001-0001-0001-0001-000100010001" + tv := testUUIDString - expected := "00010001-0001-0001-0001-000100010001" + expected := testUUIDString actual := NewTagID(tv) @@ -36,7 +42,7 @@ func TestTagID_16Bytes(t *testing.T) { 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, } - expected := "00010001-0001-0001-0001-000100010001" + expected := testUUIDString actual := NewTagID(tv) @@ -68,7 +74,7 @@ func TestTagID_17Bytes(t *testing.T) { } func TestTagID_String(t *testing.T) { - tv := "example.acme.roadrunner-sw-v1-0-0" + tv := testTagIDString actual := NewTagID(tv) @@ -108,7 +114,7 @@ func TestTagID_UnmarshalXMLAttrString_empty(t *testing.T) { } func TestTagID_UnmarshalXMLAttrString(t *testing.T) { - v := "example.acme.roadrunner-sw-v1-0-0" + v := testTagIDString tv := xml.Attr{ Name: xml.Name{Local: "tagId"}, @@ -127,7 +133,7 @@ func TestTagID_UnmarshalXMLAttrString(t *testing.T) { } func TestTagID_MarshalXMLAttrString(t *testing.T) { - v := "example.acme.roadrunner-sw-v1-0-0" + v := testTagIDString tv := NewTagID(v) require.NotNil(t, tv) @@ -152,7 +158,7 @@ func TestTagID_MarshalXMLAttrBytes(t *testing.T) { tv := NewTagID(v) require.NotNil(t, tv) - expected := "00010001-0001-0001-0001-000100010001" + expected := testUUIDString actual, err := tv.MarshalXMLAttr(xml.Name{Local: "tagId"}) @@ -169,7 +175,7 @@ func TestTagID_MarshalJSONBytes(t *testing.T) { tv := NewTagID(v) require.NotNil(t, tv) - expected := `"00010001-0001-0001-0001-000100010001"` + expected := `"` + testUUIDString + `"` actual, err := tv.MarshalJSON() @@ -236,3 +242,63 @@ func TestTagID_UnmarshalCBOR_empty_bytes(t *testing.T) { assert.EqualError(t, err, expectedErr) } + +func TestTagID_Valid_nil_value(t *testing.T) { + tagID := TagID{val: nil} + + err := tagID.Valid() + + assert.EqualError(t, err, "tag-id value is nil") +} + +func TestTagID_Valid_empty_string(t *testing.T) { + tagID := TagID{val: ""} + + err := tagID.Valid() + + assert.EqualError(t, err, "tag-id string value is empty") +} + +func TestTagID_Valid_valid_string(t *testing.T) { + tagID := TagID{val: "com.acme.rrd-2013"} + + err := tagID.Valid() + + assert.NoError(t, err) +} + +func TestTagID_Valid_nil_uuid(t *testing.T) { + tagID := TagID{val: uuid.Nil} + + err := tagID.Valid() + + assert.EqualError(t, err, "tag-id UUID value is nil UUID") +} + +func TestTagID_Valid_valid_uuid(t *testing.T) { + // Use a proper RFC4122 UUID (version 4) + validUUID := uuid.MustParse("550e8400-e29b-41d4-a716-446655440000") + tagID := TagID{val: validUUID} + + err := tagID.Valid() + + assert.NoError(t, err) +} + +func TestTagID_Valid_invalid_uuid_variant(t *testing.T) { + // Create a UUID with an invalid variant (non-RFC4122) + invalidVariantUUID := uuid.UUID{0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01, 0x00, 0x01} + tagID := TagID{val: invalidVariantUUID} + + err := tagID.Valid() + + assert.Contains(t, err.Error(), "tag-id UUID expecting RFC4122 variant") +} + +func TestTagID_Valid_invalid_type(t *testing.T) { + tagID := TagID{val: 123} + + err := tagID.Valid() + + assert.EqualError(t, err, "tag-id value must be string or uuid.UUID, got int") +} diff --git a/test_utils.go b/test_utils.go index c6d87b3..6e4f04e 100644 --- a/test_utils.go +++ b/test_utils.go @@ -1,4 +1,4 @@ -// Copyright 2021 Contributors to the Veraison project. +// Copyright 2021-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/require" ) +// MustHexDecode decodes a hex string or panics on error func MustHexDecode(t *testing.T, s string) []byte { data, err := hex.DecodeString(s) if t != nil { diff --git a/versionscheme.go b/versionscheme.go index a5ed2a9..0bcd97b 100644 --- a/versionscheme.go +++ b/versionscheme.go @@ -1,4 +1,4 @@ -// Copyright 2020 Contributors to the Veraison project. +// Copyright 2020-2025 Contributors to the Veraison project. // SPDX-License-Identifier: Apache-2.0 package swid @@ -100,6 +100,7 @@ func (vs *VersionScheme) UnmarshalXMLAttr(attr xml.Attr) error { return xmlAttrToCode(attr, stringToVersionScheme, &vs.val) } +// SetCode sets the version scheme code if it is a known value func (vs *VersionScheme) SetCode(v int64) error { if _, ok := versionSchemeToString[v]; ok { vs.val = v