Repository: aquasecurity/vuln-list-update

Issue:
bug(nvd): NVD API requires offset-from-UTC suffix
## Description
We got CI/CD errors for `nvd`.

despite what the docs say about `ISO-8601` format and offset-from-UTC optional fields (`The Z indicates an optional offset-from-UTC`)  (https://nvd.nist.gov/developers/vulnerabilities), it looks like `NVD` API requires time zone suffix now.
e.g.
```bash
➜  curl -I https://services.nvd.nist.gov/rest/json/cves/2.0/\?lastModEndDate\=2025-07-24T12:14:43\&lastModStartDate\=2025-07-24T00:37:00\&resultsPerPage\=2000\&startIndex\=0 | grep HTTP                 
HTTP/2 404 
➜  ~ curl -I https://services.nvd.nist.gov/rest/json/cves/2.0/\?lastModEndDate\=2025-07-24T12:14:43\&lastModStartDate\=2025-07-24T00:37:00\&resultsPerPage\=2000\&startIndex\=0 | grep message
message: Invalid ISO 8601 date/time format, see documentation.
➜  ~ curl -I https://services.nvd.nist.gov/rest/json/cves/2.0/\?lastModEndDate\=2025-07-24T12:14:43%2B00:00\&lastModStartDate\=2025-07-24T00:37:00%2B00:00\&resultsPerPage\=2000\&startIndex\=0 | grep HTTP   
```

Hints from issue comments:
@DmitriyLewen: RFC3339 format also works:
```bash
➜  curl -I https://services.nvd.nist.gov/rest/json/cves/2.0/\?lastModEndDate\=2025-07-24T12:14:43Z\&lastModStartDate\=2025-07-24T00:37:00Z\&resultsPerPage\=2000\&startIndex\=0 | grep HTTP
HTTP/2 200 

```

Solution patch:
diff --git a/nvd/nvd.go b/nvd/nvd.go
index 0629669..da1c9c7 100644
--- a/nvd/nvd.go
+++ b/nvd/nvd.go
@@ -20,7 +20,6 @@ const (
 	retry             = 50
 	url20             = "https://services.nvd.nist.gov/rest/json/cves/2.0/"
 	apiDir            = "api"
-	nvdTimeFormat     = "2006-01-02T15:04:05"
 	maxResultsPerPage = 2000
 	retryAfter        = 30 * time.Second
 	apiKeyEnvName     = "NVD_API_KEY"
@@ -201,16 +200,16 @@ func TimeIntervals(endTime time.Time) ([]TimeInterval, error) {
 	for endTime.Sub(lastUpdatedDate).Hours()/24 > 120 {
 		newLastUpdatedDate := lastUpdatedDate.Add(120 * 24 * time.Hour)
 		intervals = append(intervals, TimeInterval{
-			LastModStartDate: lastUpdatedDate.Format(nvdTimeFormat),
-			LastModEndDate:   newLastUpdatedDate.Format(nvdTimeFormat),
+			LastModStartDate: lastUpdatedDate.Format(time.RFC3339),
+			LastModEndDate:   newLastUpdatedDate.Format(time.RFC3339),
 		})
 		lastUpdatedDate = newLastUpdatedDate
 	}
 
 	// fill latest interval
 	intervals = append(intervals, TimeInterval{
-		LastModStartDate: lastUpdatedDate.Format(nvdTimeFormat),
-		LastModEndDate:   endTime.Format(nvdTimeFormat),
+		LastModStartDate: lastUpdatedDate.Format(time.RFC3339),
+		LastModEndDate:   endTime.Format(time.RFC3339),
 	})
 
 	return intervals, nil
diff --git a/ubuntu/ubuntu.go b/ubuntu/ubuntu.go
index 76e4597..8f7aa03 100644
--- a/ubuntu/ubuntu.go
+++ b/ubuntu/ubuntu.go
@@ -141,6 +141,44 @@ func walkDir(root string) error {
 	return nil
 }
 
+func parseMultiLineField(lines []string, currentIndex *int) []string {
+	var result []string
+	i := *currentIndex
+	for i+1 < len(lines) && (strings.HasPrefix(lines[i+1], " ") || lines[i+1] == "") {
+		i++
+		if line := strings.TrimSpace(lines[i]); line != "" {
+			result = append(result, line)
+		}
+	}
+	*currentIndex = i
+	return result
+}
+
+func parseNotesField(lines []string, currentIndex *int) []string {
+	var notes []string
+	i := *currentIndex
+	for i+1 < len(lines) && (strings.HasPrefix(lines[i+1], " ") || lines[i+1] == "") {
+		i++
+		line := strings.TrimSpace(lines[i])
+		if line == "" {
+			continue
+		}
+
+		// If line starts with single space, it's a main note line
+		note := []string{line}
+		// Look for continuation lines that start with double space
+		for i+1 < len(lines) && strings.HasPrefix(lines[i+1], "  ") {
+			i++
+			if continuationLine := strings.TrimSpace(lines[i]); continuationLine != "" {
+				note = append(note, continuationLine)
+			}
+		}
+		notes = append(notes, strings.Join(note, " "))
+	}
+	*currentIndex = i
+	return notes
+}
+
 func parse(r io.Reader) (vuln *Vulnerability, err error) {
 	vuln = &Vulnerability{}
 	vuln.Patches = map[Package]Statuses{}
@@ -193,61 +231,33 @@ func parse(r io.Reader) (vuln *Vulnerability, err error) {
 
 		// Parse References
 		if strings.HasPrefix(line, "References:") {
-			for strings.HasPrefix(lines[i+1], " ") {
-				i++
-				line = strings.TrimSpace(lines[i])
-				vuln.References = append(vuln.References, line)
-			}
+			vuln.References = parseMultiLineField(lines, &i)
 			continue
 		}
 
 		// Parse Description
 		if strings.HasPrefix(line, "Description:") {
-			var description []string
-			for strings.HasPrefix(lines[i+1], " ") {
-				i++
-				line = strings.TrimSpace(lines[i])
-				description = append(description, line)
-			}
+			description := parseMultiLineField(lines, &i)
 			vuln.Description = strings.Join(description, " ")
 			continue
 		}
 
 		// Parse Ubuntu Description
 		if strings.HasPrefix(line, "Ubuntu-Description:") {
-			var description []string
-			for strings.HasPrefix(lines[i+1], " ") {
-				i++
-				line = strings.TrimSpace(lines[i])
-				description = append(description, line)
-			}
+			description := parseMultiLineField(lines, &i)
 			vuln.UbuntuDescription = strings.Join(description, " ")
 			continue
 		}
 
 		// Parse Notes
 		if strings.HasPrefix(line, "Notes:") {
-			for strings.HasPrefix(lines[i+1], " ") {
-				i++
-				line = strings.TrimSpace(lines[i])
-				note := []string{line}
-				for strings.HasPrefix(lines[i+1], "  ") {
-					i++
-					l := strings.TrimSpace(lines[i])
-					note = append(note, l)
-				}
-				vuln.Notes = append(vuln.Notes, strings.Join(note, " "))
-			}
+			vuln.Notes = parseNotesField(lines, &i)
 			continue
 		}
 
 		// Parse Bugs
 		if strings.HasPrefix(line, "Bugs:") {
-			for strings.HasPrefix(lines[i+1], " ") {
-				i++
-				line = strings.TrimSpace(lines[i])
-				vuln.Bugs = append(vuln.Bugs, line)
-			}
+			vuln.Bugs = parseMultiLineField(lines, &i)
 			continue
 		}
 



Test patch:
diff --git a/nvd/nvd_test.go b/nvd/nvd_test.go
index 8dae331..d812ad9 100644
--- a/nvd/nvd_test.go
+++ b/nvd/nvd_test.go
@@ -223,8 +223,8 @@ func TestTimeIntervals(t *testing.T) {
 			fakeTimeNow:     time.Date(2023, 11, 28, 0, 0, 0, 0, time.UTC),
 			wantIntervals: []nvd.TimeInterval{
 				{
-					LastModStartDate: "2023-11-26T00:00:00",
-					LastModEndDate:   "2023-11-28T00:00:00",
+					LastModStartDate: "2023-11-26T00:00:00Z",
+					LastModEndDate:   "2023-11-28T00:00:00Z",
 				},
 			},
 		},
@@ -234,12 +234,12 @@ func TestTimeIntervals(t *testing.T) {
 			fakeTimeNow:     time.Date(2023, 11, 28, 0, 0, 0, 0, time.UTC),
 			wantIntervals: []nvd.TimeInterval{
 				{
-					LastModStartDate: "2023-05-28T00:00:00",
-					LastModEndDate:   "2023-09-25T00:00:00",
+					LastModStartDate: "2023-05-28T00:00:00Z",
+					LastModEndDate:   "2023-09-25T00:00:00Z",
 				},
 				{
-					LastModStartDate: "2023-09-25T00:00:00",
-					LastModEndDate:   "2023-11-28T00:00:00",
+					LastModStartDate: "2023-09-25T00:00:00Z",
+					LastModEndDate:   "2023-11-28T00:00:00Z",
 				},
 			},
 		},
@@ -249,8 +249,8 @@ func TestTimeIntervals(t *testing.T) {
 			fakeTimeNow:     time.Date(1970, 03, 01, 0, 0, 0, 0, time.UTC),
 			wantIntervals: []nvd.TimeInterval{
 				{
-					LastModStartDate: "1970-01-01T00:00:00",
-					LastModEndDate:   "1970-03-01T00:00:00",
+					LastModStartDate: "1970-01-01T00:00:00Z",
+					LastModEndDate:   "1970-03-01T00:00:00Z",
 				},
 			},
 		},
diff --git a/ubuntu/testdata/blank_lines_in_description b/ubuntu/testdata/blank_lines_in_description
new file mode 100644
index 0000000..2ca25fc
--- /dev/null
+++ b/ubuntu/testdata/blank_lines_in_description
@@ -0,0 +1,25 @@
+Candidate: CVE-2017-5192
+PublicDate: 2017-09-26 14:29:00 UTC
+References:
+ https://docs.saltstack.com/en/2016.3/topics/releases/2015.8.13.html
+ https://www.cve.org/CVERecord?id=CVE-2017-5192
+Description:
+ When using the local_batch client from salt-api in SaltStack Salt before
+ 2015.8.13, 2016.3.x before 2016.3.5, and 2016.11.x before 2016.11.2,
+ external authentication is not respected, enabling all authentication to be
+ bypassed.
+
+ The LocalClient.cmd_batch() method client does not accept external_auth
+ credentials and so access to it from salt-api has been removed for
+ now. This vulnerability allows code execution for already-authenticated
+ users and is only in effect when running salt-api as the root user.
+Ubuntu-Description:
+Notes:
+Bugs:
+Priority: medium
+Discovered-by:
+Assigned-to:
+
+upstream_salt: released (2016.11.2+ds-1)
+precise_salt: DNE
+trusty_salt: ignored (end of standard support)
\ No newline at end of file
diff --git a/ubuntu/testdata/notes_with_continuation_lines b/ubuntu/testdata/notes_with_continuation_lines
new file mode 100644
index 0000000..eb8eaec
--- /dev/null
+++ b/ubuntu/testdata/notes_with_continuation_lines
@@ -0,0 +1,25 @@
+Candidate: CVE-2017-0537
+PublicDate: 2017-03-08 01:59:00 UTC
+References:
+ https://source.android.com/security/bulletin/2017-01-01.html
+ https://android.googlesource.com/kernel/tegra.git/+/389b185cb2f17fff994dbdf8d4bac003d4b2b6b3%5E%21/#F0
+ https://lore.kernel.org/lkml/1484647168-30135-1-git-send-email-jilin@nvidia.com/#t
+ https://www.cve.org/CVERecord?id=CVE-2017-0537
+Description:
+ An information disclosure vulnerability in the kernel USB gadget driver
+ could enable a local malicious application to access data outside of its
+ permission levels. This issue is rated as Moderate because it first
+ requires compromising a privileged process. Product: Android. Versions:
+ Kernel-3.18. Android ID: A-31614969.
+Ubuntu-Description:
+Notes:
+ sbeattie> see android patch above
+ sbeattie> drivers/usb/gadget/configfs.c::usb_string_copy()
+ tyhicks> Patch submitter never verified that this was an issue on pure Linux
+  and upstream thinks that it could potentially be an issue in Android-specific
+  kernel changes
+ mdeslaur> The android package is in multiverse and not covered by ESM.
+ mdeslaur> Marking as ignored.
+Priority: medium
+Patches_test-package: needs-triage
+upstream_test-package: needs-triage
\ No newline at end of file
diff --git a/ubuntu/ubuntu_test.go b/ubuntu/ubuntu_test.go
index 9586a24..d918b62 100644
--- a/ubuntu/ubuntu_test.go
+++ b/ubuntu/ubuntu_test.go
@@ -426,6 +426,71 @@ func Test_parse(t *testing.T) {
 				},
 			},
 		},
+		{
+			name: "blank lines in description",
+			args: args{
+				filePath: "./testdata/blank_lines_in_description",
+			},
+			want: &Vulnerability{
+				Candidate: "CVE-2017-5192",
+				References: []string{
+					"https://docs.saltstack.com/en/2016.3/topics/releases/2015.8.13.html",
+					"https://www.cve.org/CVERecord?id=CVE-2017-5192",
+				},
+				Description: "When using the local_batch client from salt-api in SaltStack Salt before 2015.8.13, 2016.3.x before 2016.3.5, and 2016.11.x before 2016.11.2, external authentication is not respected, enabling all authentication to be bypassed. The LocalClient.cmd_batch() method client does not accept external_auth credentials and so access to it from salt-api has been removed for now. This vulnerability allows code execution for already-authenticated users and is only in effect when running salt-api as the root user.",
+				Priority:    "medium",
+				PublicDate:  time.Date(2017, 9, 26, 14, 29, 0, 0, time.UTC),
+				Patches: map[Package]Statuses{
+					Package("salt"): {
+						"upstream": Status{
+							Status: "released",
+							Note:   "2016.11.2+ds-1",
+						},
+						"precise": Status{
+							Status: "DNE",
+						},
+						"trusty": Status{
+							Status: "ignored",
+							Note:   "end of standard support",
+						},
+					},
+				},
+				UpstreamLinks: map[Package][]string{},
+			},
+		},
+		{
+			name: "notes with continuation lines",
+			args: args{
+				filePath: "./testdata/notes_with_continuation_lines",
+			},
+			want: &Vulnerability{
+				Candidate: "CVE-2017-0537",
+				References: []string{
+					"https://source.android.com/security/bulletin/2017-01-01.html",
+					"https://android.googlesource.com/kernel/tegra.git/+/389b185cb2f17fff994dbdf8d4bac003d4b2b6b3%5E%21/#F0",
+					"https://lore.kernel.org/lkml/1484647168-30135-1-git-send-email-jilin@nvidia.com/#t",
+					"https://www.cve.org/CVERecord?id=CVE-2017-0537",
+				},
+				Description: "An information disclosure vulnerability in the kernel USB gadget driver could enable a local malicious application to access data outside of its permission levels. This issue is rated as Moderate because it first requires compromising a privileged process. Product: Android. Versions: Kernel-3.18. Android ID: A-31614969.",
+				Priority:    "medium",
+				PublicDate:  time.Date(2017, 3, 8, 1, 59, 0, 0, time.UTC),
+				Notes: []string{
+					"sbeattie> see android patch above",
+					"sbeattie> drivers/usb/gadget/configfs.c::usb_string_copy()",
+					"tyhicks> Patch submitter never verified that this was an issue on pure Linux and upstream thinks that it could potentially be an issue in Android-specific kernel changes",
+					"mdeslaur> The android package is in multiverse and not covered by ESM.",
+					"mdeslaur> Marking as ignored.",
+				},
+				Patches: map[Package]Statuses{
+					Package("test-package"): {
+						"upstream": Status{
+							Status: "needs-triage",
+						},
+					},
+				},
+				UpstreamLinks: map[Package][]string{},
+			},
+		},
 	}
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {