Repository: google/osv-scanner
Pull request #424 (2023-06-27 04:41:46)

Add withdrawn and fix time serialization to conform to the schema.
- withdrawn timestamp does not exist in the model - add it to the model.
- fix the timestamp serialization to ensure published and withdrawn are *only* present when the value is not zero
- fix timestamp serialization to use RFC3339 for JSON (it uses RFC3339Nano by default)
- fix the timestamps to be normalized to UTC as per the schema

G-Rath: Note this will conflict with #423
calebbrown: > Note this will conflict with #423

Thanks! Fixed :)

Patch:
diff --git a/pkg/models/vulnerability.go b/pkg/models/vulnerability.go
index 56b30bf..7b1b128 100644
--- a/pkg/models/vulnerability.go
+++ b/pkg/models/vulnerability.go
@@ -73,7 +73,7 @@ type Affected struct {
 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"`
+		Package *Package `json:"package,omitempty"`
 		rawAffected
 	}
 	raw := wrapper{rawAffected: rawAffected(a)}
@@ -114,6 +114,7 @@ type Vulnerability struct {
 	ID               string                 `json:"id" yaml:"id"`
 	Modified         time.Time              `json:"modified" yaml:"modified"`
 	Published        time.Time              `json:"published,omitempty" yaml:"published,omitempty"`
+	Withdrawn        time.Time              `json:"withdrawn,omitempty" yaml:"withdrawn,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"`
@@ -124,3 +125,45 @@ type Vulnerability struct {
 	Credits          []Credit               `json:"credits,omitempty" yaml:"credits,omitempty"`
 	DatabaseSpecific map[string]interface{} `json:"database_specific,omitempty" yaml:"database_specific,omitempty"`
 }
+
+// MarshalJSON implements the json.Marshaler interface.
+//
+// This method ensures times all times are formated correctly according to the schema.
+func (v Vulnerability) MarshalJSON() ([]byte, error) {
+	type rawVulnerability Vulnerability // alias Vulnerability to avoid recursion during Marshal
+	type wrapper struct {
+		Modified  string `json:"modified"`
+		Published string `json:"published,omitempty"`
+		Withdrawn string `json:"withdrawn,omitempty"`
+		rawVulnerability
+	}
+	raw := wrapper{rawVulnerability: rawVulnerability(v)}
+	raw.Modified = v.Modified.UTC().Format(time.RFC3339)
+	if !v.Published.IsZero() {
+		raw.Published = v.Published.UTC().Format(time.RFC3339)
+	}
+	if !v.Withdrawn.IsZero() {
+		raw.Withdrawn = v.Withdrawn.UTC().Format(time.RFC3339)
+	}
+
+	return json.Marshal(raw)
+}
+
+// MarshalYAML implements the yaml.Marshaler interface.
+//
+// This method ensures times all times are formated correctly.
+func (v Vulnerability) MarshalYAML() (interface{}, error) {
+	type rawVulnerability Vulnerability // alias Vulnerability to avoid recursion during Marshal
+	raw := rawVulnerability(v)
+	if !v.Modified.IsZero() {
+		raw.Modified = v.Modified.UTC()
+	}
+	if !v.Published.IsZero() {
+		raw.Published = v.Published.UTC()
+	}
+	if !v.Withdrawn.IsZero() {
+		raw.Withdrawn = v.Withdrawn.UTC()
+	}
+
+	return raw, nil
+}


Test patch:
diff --git a/pkg/models/vulnerability_test.go b/pkg/models/vulnerability_test.go
index 282e5b3..11171ca 100644
--- a/pkg/models/vulnerability_test.go
+++ b/pkg/models/vulnerability_test.go
@@ -3,6 +3,7 @@ package models_test
 import (
 	"encoding/json"
 	"testing"
+	"time"
 
 	"github.com/google/osv-scanner/pkg/models"
 	"gopkg.in/yaml.v3"
@@ -23,7 +24,7 @@ func TestAffected_MarshalJSONWithPackage(t *testing.T) {
 	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"]}]}`
+	want := `{"modified":"0001-01-01T00:00:00Z","id":"TEST-0000","affected":[{"package":{"ecosystem":"PyPI","name":"requests"},"versions":["1.0.0"]}]}`
 	if string(got) != want {
 		t.Errorf("Marshal() = %v; want %v", string(got), want)
 	}
@@ -43,7 +44,7 @@ func TestAffected_MarshalJSONWithoutPackage(t *testing.T) {
 	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"]}]}`
+	want := `{"modified":"0001-01-01T00:00:00Z","id":"TEST-0000","affected":[{"versions":["1.0.0"]}]}`
 	if string(got) != want {
 		t.Errorf("Marshal() = %v; want %v", string(got), want)
 	}
@@ -102,3 +103,131 @@ affected:
 		t.Errorf("Marshal() = %v; want %v", string(got), want)
 	}
 }
+
+func TestVulnerability_MarshalJSONTimes(t *testing.T) {
+	t.Parallel()
+	losAngeles, err := time.LoadLocation("America/Los_Angeles")
+	if err != nil {
+		panic(err)
+	}
+
+	tests := []struct {
+		name string
+		vuln models.Vulnerability
+		want string
+	}{
+		{
+			name: "empty",
+			vuln: models.Vulnerability{
+				ID: "TEST-0000",
+			},
+			want: `{"modified":"0001-01-01T00:00:00Z","id":"TEST-0000"}`,
+		},
+		{
+			name: "no withdraw",
+			vuln: models.Vulnerability{
+				ID:        "TEST-0000",
+				Modified:  time.Date(2023, 12, 1, 12, 30, 30, 0, time.UTC),
+				Published: time.Date(2021, 6, 30, 1, 0, 0, 0, time.UTC),
+			},
+			want: `{"modified":"2023-12-01T12:30:30Z","published":"2021-06-30T01:00:00Z","id":"TEST-0000"}`,
+		},
+		{
+			name: "all UTC",
+			vuln: models.Vulnerability{
+				ID:        "TEST-0000",
+				Modified:  time.Date(2023, 12, 1, 12, 30, 30, 0, time.UTC),
+				Published: time.Date(2021, 6, 30, 1, 0, 0, 0, time.UTC),
+				Withdrawn: time.Date(2022, 1, 15, 23, 59, 59, 0, time.UTC),
+			},
+			want: `{"modified":"2023-12-01T12:30:30Z","published":"2021-06-30T01:00:00Z","withdrawn":"2022-01-15T23:59:59Z","id":"TEST-0000"}`,
+		},
+		{
+			name: "all Los Angeles",
+			vuln: models.Vulnerability{
+				ID:        "TEST-0000",
+				Modified:  time.Date(2023, 12, 1, 12, 30, 30, 0, losAngeles),
+				Published: time.Date(2021, 6, 30, 1, 0, 0, 0, losAngeles),
+				Withdrawn: time.Date(2022, 1, 15, 23, 59, 59, 0, losAngeles),
+			},
+			want: `{"modified":"2023-12-01T20:30:30Z","published":"2021-06-30T08:00:00Z","withdrawn":"2022-01-16T07:59:59Z","id":"TEST-0000"}`,
+		},
+	}
+	for _, test := range tests {
+		innerTest := test
+		t.Run(innerTest.name, func(t *testing.T) {
+			t.Parallel()
+			got, err := json.Marshal(innerTest.vuln)
+			if err != nil {
+				t.Fatalf("Marshal() = %v; want no error", err)
+			}
+			if string(got) != innerTest.want {
+				t.Errorf("Marshal() = %v; want %v", string(got), innerTest.want)
+			}
+		})
+	}
+}
+
+func TestVulnerability_MarshalYAMLTimes(t *testing.T) {
+	t.Parallel()
+	losAngeles, err := time.LoadLocation("America/Los_Angeles")
+	if err != nil {
+		panic(err)
+	}
+
+	tests := []struct {
+		name string
+		vuln models.Vulnerability
+		want string
+	}{
+		{
+			name: "empty",
+			vuln: models.Vulnerability{
+				ID: "TEST-0000",
+			},
+			want: "id: TEST-0000\nmodified: 0001-01-01T00:00:00Z\n",
+		},
+		{
+			name: "no withdraw",
+			vuln: models.Vulnerability{
+				ID:        "TEST-0000",
+				Modified:  time.Date(2023, 12, 1, 12, 30, 30, 0, time.UTC),
+				Published: time.Date(2021, 6, 30, 1, 0, 0, 0, time.UTC),
+			},
+			want: "id: TEST-0000\nmodified: 2023-12-01T12:30:30Z\npublished: 2021-06-30T01:00:00Z\n",
+		},
+		{
+			name: "all UTC",
+			vuln: models.Vulnerability{
+				ID:        "TEST-0000",
+				Modified:  time.Date(2023, 12, 1, 12, 30, 30, 0, time.UTC),
+				Published: time.Date(2021, 6, 30, 1, 0, 0, 0, time.UTC),
+				Withdrawn: time.Date(2022, 1, 15, 23, 59, 59, 0, time.UTC),
+			},
+			want: "id: TEST-0000\nmodified: 2023-12-01T12:30:30Z\npublished: 2021-06-30T01:00:00Z\nwithdrawn: 2022-01-15T23:59:59Z\n",
+		},
+		{
+			name: "all Los Angeles",
+			vuln: models.Vulnerability{
+				ID:        "TEST-0000",
+				Modified:  time.Date(2023, 12, 1, 12, 30, 30, 0, losAngeles),
+				Published: time.Date(2021, 6, 30, 1, 0, 0, 0, losAngeles),
+				Withdrawn: time.Date(2022, 1, 15, 23, 59, 59, 0, losAngeles),
+			},
+			want: "id: TEST-0000\nmodified: 2023-12-01T20:30:30Z\npublished: 2021-06-30T08:00:00Z\nwithdrawn: 2022-01-16T07:59:59Z\n",
+		},
+	}
+	for _, test := range tests {
+		innerTest := test
+		t.Run(innerTest.name, func(t *testing.T) {
+			t.Parallel()
+			got, err := yaml.Marshal(innerTest.vuln)
+			if err != nil {
+				t.Fatalf("Marshal() = %v; want no error", err)
+			}
+			if string(got) != innerTest.want {
+				t.Errorf("Marshal() = %v; want %v", string(got), innerTest.want)
+			}
+		})
+	}
+}