Repository: google/osv-scanner
Pull request #417 (2023-06-20 04:19:07)

Update the model to better match schema and add YAML tags.
- YAML tags added to support YAML serialization
- omitempty added to optional fields
- Package changed to a pointer in Affected
- Bitnami added as an ecosystem

This PR is primarily to support the replacement of the same structs in google/osv.dev/vulnfeeds/vulns.


Patch:
diff --git a/pkg/models/constants.go b/pkg/models/constants.go
index 9915c4a..246fa5e 100644
--- a/pkg/models/constants.go
+++ b/pkg/models/constants.go
@@ -22,6 +22,7 @@ const (
 	EcosystemConanCenter   Ecosystem = "ConanCenter"
 	EcosystemRockyLinux    Ecosystem = "Rocky Linux"
 	EcosystemAlmaLinux     Ecosystem = "AlmaLinux"
+	EcosystemBitnami       Ecosystem = "Bitnami"
 )
 
 type SeverityType string
diff --git a/pkg/models/vulnerability.go b/pkg/models/vulnerability.go
index 335a32c..56b30bf 100644
--- a/pkg/models/vulnerability.go
+++ b/pkg/models/vulnerability.go
@@ -1,15 +1,18 @@
 package models
 
-import "time"
+import (
+	"encoding/json"
+	"time"
+)
 
 // Package identifies the affected code library or command provided by the
 // package.
 //
 // See: https://ossf.github.io/osv-schema/#affectedpackage-field
 type Package struct {
-	Ecosystem Ecosystem `json:"ecosystem,omitempty"`
-	Name      string    `json:"name,omitempty"`
-	Purl      string    `json:"purl,omitempty"`
+	Ecosystem Ecosystem `json:"ecosystem" yaml:"ecosystem"`
+	Name      string    `json:"name" yaml:"name"`
+	Purl      string    `json:"purl,omitempty" yaml:"purl,omitempty"`
 }
 
 // Event describes a single version that either:
@@ -24,19 +27,20 @@ type Package struct {
 //
 // See: https://ossf.github.io/osv-schema/#affectedrangesevents-fields
 type Event struct {
-	Introduced   string `json:"introduced,omitempty"`
-	Fixed        string `json:"fixed,omitempty"`
-	LastAffected string `json:"last_affected,omitempty"`
-	Limit        string `json:"limit,omitempty"`
+	Introduced   string `json:"introduced,omitempty" yaml:"introduced,omitempty"`
+	Fixed        string `json:"fixed,omitempty" yaml:"fixed,omitempty"`
+	LastAffected string `json:"last_affected,omitempty" yaml:"last_affected,omitempty"`
+	Limit        string `json:"limit,omitempty" yaml:"limit,omitempty"`
 }
 
 // Range describes the affected range of given version for a specific package.
 //
 // See: https://ossf.github.io/osv-schema/#affectedranges-field
 type Range struct {
-	Type             RangeType              `json:"type"`
-	Events           []Event                `json:"events"`
-	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty"`
+	Type             RangeType              `json:"type" yaml:"type"`
+	Events           []Event                `json:"events" yaml:"events"`
+	Repo             string                 `json:"repo,omitempty" yaml:"repo,omitempty"`
+	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty" yaml:"database_specific,omitempty"`
 }
 
 // Severity is used to describe the severity of a vulnerability for an affected
@@ -44,8 +48,8 @@ type Range struct {
 //
 // See: https://ossf.github.io/osv-schema/#severity-field
 type Severity struct {
-	Type  SeverityType `json:"type"`
-	Score string       `json:"score"`
+	Type  SeverityType `json:"type" yaml:"type"`
+	Score string       `json:"score" yaml:"score"`
 }
 
 // Affected describes an affected package version, meaning one instance that
@@ -53,12 +57,33 @@ type Severity struct {
 //
 // See: https://ossf.github.io/osv-schema/#affected-fields
 type Affected struct {
-	Package           Package                `json:"package"`
-	Severity          []Severity             `json:"severity,omitempty"`
-	Ranges            []Range                `json:"ranges,omitempty"`
-	Versions          []string               `json:"versions,omitempty"`
-	DatabaseSpecific  map[string]interface{} `json:"database_specific,omitempty"`
-	EcosystemSpecific map[string]interface{} `json:"ecosystem_specific,omitempty"`
+	Package           Package                `json:"package,omitempty" yaml:"package,omitempty"`
+	Severity          []Severity             `json:"severity,omitempty" yaml:"severity,omitempty"`
+	Ranges            []Range                `json:"ranges,omitempty" yaml:"ranges,omitempty"`
+	Versions          []string               `json:"versions,omitempty" yaml:"versions,omitempty"`
+	DatabaseSpecific  map[string]interface{} `json:"database_specific,omitempty" yaml:"database_specific,omitempty"`
+	EcosystemSpecific map[string]interface{} `json:"ecosystem_specific,omitempty" yaml:"ecosystem_specific,omitempty"`
+}
+
+// MarshalJSON implements the json.Marshaler interface.
+//
+// This method ensures Package is only present if it is not equal to the zero value.
+// This is achieved by embedding the Affected struct with a pointer to Package used
+// to populate the "package" key in the JSON object.
+func (a Affected) MarshalJSON() ([]byte, error) {
+	type rawAffected Affected // alias Affected to avoid recursion during Marshal
+	type wrapper struct {
+		Package *Package `json:"package,omitempty" yaml:"package,omitempty"`
+		rawAffected
+	}
+	raw := wrapper{rawAffected: rawAffected(a)}
+	if a.Package == (Package{}) {
+		raw.Package = nil
+	} else {
+		raw.Package = &(a.Package)
+	}
+
+	return json.Marshal(raw)
 }
 
 // Reference links to additional information, advisories, issue tracker entries,
@@ -66,8 +91,8 @@ type Affected struct {
 //
 // See: https://ossf.github.io/osv-schema/#references-field
 type Reference struct {
-	Type ReferenceType `json:"type"`
-	URL  string        `json:"url"`
+	Type ReferenceType `json:"type" yaml:"type"`
+	URL  string        `json:"url" yaml:"url"`
 }
 
 // Credit gives credit for the discovery, confirmation, patch, or other events
@@ -75,9 +100,9 @@ type Reference struct {
 //
 // See: https://ossf.github.io/osv-schema/#credits-fields
 type Credit struct {
-	Name    string     `json:"name"`
-	Contact []string   `json:"contact,omitempty"`
-	Type    CreditType `json:"type"`
+	Name    string     `json:"name" yaml:"name"`
+	Type    CreditType `json:"type,omitempty" yaml:"type,omitempty"`
+	Contact []string   `json:"contact,omitempty" yaml:"contact,omitempty"`
 }
 
 // Vulnerability is the core Open Source Vulnerability (OSV) data type.
@@ -85,17 +110,17 @@ type Credit struct {
 // The full documentation for the schema is available at
 // https://ossf.github.io/osv-schema.
 type Vulnerability struct {
-	SchemaVersion    string                 `json:"schema_version"`
-	ID               string                 `json:"id"`
-	Modified         time.Time              `json:"modified"`
-	Published        time.Time              `json:"published"`
-	Aliases          []string               `json:"aliases,omitempty"`
-	Related          []string               `json:"related,omitempty"`
-	Summary          string                 `json:"summary"`
-	Details          string                 `json:"details"`
-	Affected         []Affected             `json:"affected"`
-	Severity         []Severity             `json:"severity,omitempty"`
-	References       []Reference            `json:"references,omitempty"`
-	Credits          []Credit               `json:"credits,omitempty"`
-	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty"`
+	SchemaVersion    string                 `json:"schema_version,omitempty" yaml:"schema_version,omitempty"`
+	ID               string                 `json:"id" yaml:"id"`
+	Modified         time.Time              `json:"modified" yaml:"modified"`
+	Published        time.Time              `json:"published,omitempty" yaml:"published,omitempty"`
+	Aliases          []string               `json:"aliases,omitempty" yaml:"aliases,omitempty"`
+	Related          []string               `json:"related,omitempty" yaml:"related,omitempty"`
+	Summary          string                 `json:"summary,omitempty" yaml:"summary,omitempty"`
+	Details          string                 `json:"details,omitempty" yaml:"details,omitempty"`
+	Affected         []Affected             `json:"affected,omitempty" yaml:"affected,omitempty"`
+	Severity         []Severity             `json:"severity,omitempty" yaml:"severity,omitempty"`
+	References       []Reference            `json:"references,omitempty" yaml:"references,omitempty"`
+	Credits          []Credit               `json:"credits,omitempty" yaml:"credits,omitempty"`
+	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty" yaml:"database_specific,omitempty"`
 }


Test patch:
diff --git a/pkg/models/vulnerability_test.go b/pkg/models/vulnerability_test.go
new file mode 100644
index 0000000..b6cb7d8
--- /dev/null
+++ b/pkg/models/vulnerability_test.go
@@ -0,0 +1,103 @@
+package models
+
+import (
+	"encoding/json"
+	"testing"
+
+	"gopkg.in/yaml.v3"
+)
+
+func TestAffected_MarshalJSONWithPackage(t *testing.T) {
+	t.Parallel()
+	v := Vulnerability{
+		ID: "TEST-0000",
+		Affected: []Affected{
+			{
+				Package:  Package{Ecosystem: EcosystemPyPI, Name: "requests"},
+				Versions: []string{"1.0.0"},
+			},
+		},
+	}
+	got, err := json.Marshal(v)
+	if err != nil {
+		t.Fatalf("Marshal() = %v; want no error", err)
+	}
+	want := `{"id":"TEST-0000","modified":"0001-01-01T00:00:00Z","published":"0001-01-01T00:00:00Z","affected":[{"package":{"ecosystem":"PyPI","name":"requests"},"versions":["1.0.0"]}]}`
+	if string(got) != want {
+		t.Errorf("Marshal() = %v; want %v", string(got), want)
+	}
+}
+
+func TestAffected_MarshalJSONWithoutPackage(t *testing.T) {
+	t.Parallel()
+	v := Vulnerability{
+		ID: "TEST-0000",
+		Affected: []Affected{
+			{
+				Versions: []string{"1.0.0"},
+			},
+		},
+	}
+	got, err := json.Marshal(v)
+	if err != nil {
+		t.Fatalf("Marshal() = %v; want no error", err)
+	}
+	want := `{"id":"TEST-0000","modified":"0001-01-01T00:00:00Z","published":"0001-01-01T00:00:00Z","affected":[{"versions":["1.0.0"]}]}`
+	if string(got) != want {
+		t.Errorf("Marshal() = %v; want %v", string(got), want)
+	}
+}
+
+func TestAffected_MarshalYAMLWithPackage(t *testing.T) {
+	t.Parallel()
+	v := Vulnerability{
+		ID: "TEST-0000",
+		Affected: []Affected{
+			{
+				Package:  Package{Ecosystem: EcosystemPyPI, Name: "requests"},
+				Versions: []string{"1.0.0"},
+			},
+		},
+	}
+	got, err := yaml.Marshal(v)
+	if err != nil {
+		t.Fatalf("Marshal() = %v; want no error", err)
+	}
+	want := `id: TEST-0000
+modified: 0001-01-01T00:00:00Z
+affected:
+    - package:
+        ecosystem: PyPI
+        name: requests
+      versions:
+        - 1.0.0
+`
+	if string(got) != want {
+		t.Errorf("Marshal() = %v; want %v", string(got), want)
+	}
+}
+
+func TestAffected_MarshalYAMLWithoutPackage(t *testing.T) {
+	t.Parallel()
+	v := Vulnerability{
+		ID: "TEST-0000",
+		Affected: []Affected{
+			{
+				Versions: []string{"1.0.0"},
+			},
+		},
+	}
+	got, err := yaml.Marshal(v)
+	if err != nil {
+		t.Fatalf("Marshal() = %v; want no error", err)
+	}
+	want := `id: TEST-0000
+modified: 0001-01-01T00:00:00Z
+affected:
+    - versions:
+        - 1.0.0
+`
+	if string(got) != want {
+		t.Errorf("Marshal() = %v; want %v", string(got), want)
+	}
+}