1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 package org.owasp.dependencycheck.data.update;
19
20 import org.hamcrest.Matchers;
21 import org.jspecify.annotations.NonNull;
22 import org.junit.jupiter.api.Nested;
23 import org.junit.jupiter.api.Test;
24 import org.mockito.MockedStatic;
25 import org.owasp.dependencycheck.data.update.exception.UpdateException;
26 import org.owasp.dependencycheck.utils.DownloadFailedException;
27 import org.owasp.dependencycheck.utils.Downloader;
28 import org.owasp.dependencycheck.utils.Settings;
29
30 import java.net.URI;
31 import java.time.ZoneOffset;
32 import java.time.ZonedDateTime;
33 import java.util.List;
34 import java.util.Map;
35 import java.util.NoSuchElementException;
36
37 import static org.hamcrest.MatcherAssert.assertThat;
38 import static org.hamcrest.Matchers.contains;
39 import static org.hamcrest.Matchers.everyItem;
40 import static org.junit.jupiter.api.Assertions.assertEquals;
41 import static org.junit.jupiter.api.Assertions.assertFalse;
42 import static org.junit.jupiter.api.Assertions.assertThrows;
43 import static org.junit.jupiter.api.Assertions.assertTrue;
44 import static org.mockito.ArgumentMatchers.any;
45 import static org.mockito.Mockito.mock;
46 import static org.mockito.Mockito.mockStatic;
47 import static org.mockito.Mockito.when;
48 import static org.owasp.dependencycheck.data.update.NvdApiDataSource.FeedUrl.DEFAULT_FILE_PATTERN;
49 import static org.owasp.dependencycheck.data.update.NvdApiDataSource.FeedUrl.extractFromUrlOptionalPattern;
50 import static org.owasp.dependencycheck.data.update.NvdApiDataSource.FeedUrl.isMandatoryFeedYear;
51
52 class NvdApiDataSourceTest {
53
54 @Nested
55 class FeedUrlParsing {
56
57 @Test
58 void shouldExtractUrlWithPattern() throws Exception {
59 String nvdDataFeedUrl = "https://internal.server/nist/nvdcve-{0}.json.gz";
60 String expectedUrl = "https://internal.server/nist/nvdcve-2045.json.gz";
61 NvdApiDataSource.FeedUrl result = extractFromUrlOptionalPattern(nvdDataFeedUrl);
62
63 assertEquals(expectedUrl, result.toFormattedUrlString("2045"));
64 assertEquals(URI.create(expectedUrl).toURL(), result.toFormattedUrl("2045"));
65 assertEquals(URI.create("https://internal.server/nist/some-file.txt").toURL(), result.toSuffixedUrl("some-file.txt"));
66
67 assertEquals(expectedUrl, result.toFormattedUrlString("2045"));
68 assertEquals(URI.create(expectedUrl).toURL(), result.toFormattedUrl("2045"));
69 }
70
71 @Test
72 void shouldAllowTransformingFilePattern() {
73 NvdApiDataSource.FeedUrl result = extractFromUrlOptionalPattern("https://internal.server/nist/nvdcve-{0}.json.gz")
74 .withPattern(p -> p.orElseThrow().replace(".json.gz", ".something"));
75 assertEquals("https://internal.server/nist/nvdcve-ok.something", result.toFormattedUrlString("ok"));
76
77 NvdApiDataSource.FeedUrl resultNoPattern = extractFromUrlOptionalPattern("https://internal.server/nist/")
78 .withPattern(p -> p.orElse("my-suffix-{0}.json.gz"));
79 assertEquals("https://internal.server/nist/my-suffix-ok.json.gz", resultNoPattern.toFormattedUrlString("ok"));
80 }
81
82 @Test
83 void shouldExtractUrlWithoutPattern() throws Exception {
84 String nvdDataFeedUrl = "https://internal.server/nist/";
85 NvdApiDataSource.FeedUrl result = extractFromUrlOptionalPattern(nvdDataFeedUrl);
86
87 assertThrows(NoSuchElementException.class, () -> result.toFormattedUrlString("2045"));
88 assertThrows(NoSuchElementException.class, () -> result.toFormattedUrl("2045"));
89 assertEquals(URI.create("https://internal.server/nist/some-file.txt").toURL(), result.toSuffixedUrl("some-file.txt"));
90
91 String expectedUrl = "https://internal.server/nist/nvdcve-2045.json.gz";
92 NvdApiDataSource.FeedUrl resultWithPattern = extractFromUrlOptionalPattern(nvdDataFeedUrl)
93 .withPattern(p -> p.orElse(DEFAULT_FILE_PATTERN));
94
95 assertEquals(expectedUrl, resultWithPattern.toFormattedUrlString("2045"));
96 assertEquals(URI.create(expectedUrl).toURL(), resultWithPattern.toFormattedUrl("2045"));
97 }
98
99 @Test
100 void extractUrlWithoutPatternShouldAddTrailingSlashes() {
101 String nvdDataFeedUrl = "https://internal.server/nist";
102 String expectedUrl = "https://internal.server/nist/nvdcve-2045.json.gz";
103
104 NvdApiDataSource.FeedUrl result = extractFromUrlOptionalPattern(nvdDataFeedUrl)
105 .withPattern(p -> p.orElse(DEFAULT_FILE_PATTERN));
106
107 assertEquals(expectedUrl, result.toFormattedUrlString("2045"));
108 }
109 }
110
111 @Nested
112 class FeedUrlMandatoryYears {
113
114 @Test
115 void shouldConsiderYearsMandatoryWhenNotCurrentYearAtEarliestTZ() {
116 ZonedDateTime janFirst2004AtEarliest = ZonedDateTime.of(2004, 1, 1, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_EARLIEST);
117 assertTrue(isMandatoryFeedYear(janFirst2004AtEarliest, 2002));
118 assertTrue(isMandatoryFeedYear(janFirst2004AtEarliest, 2003));
119 assertFalse(isMandatoryFeedYear(janFirst2004AtEarliest, 2004));
120 }
121
122 @Test
123 void shouldConsiderYearsMandatoryWhenNotCurrentYearAtLatestTZ() {
124 ZonedDateTime janFirst2004AtLatest = ZonedDateTime.of(2004, 1, 1, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_LATEST);
125 assertTrue(isMandatoryFeedYear(janFirst2004AtLatest, 2002));
126 assertTrue(isMandatoryFeedYear(janFirst2004AtLatest, 2003));
127 assertFalse(isMandatoryFeedYear(janFirst2004AtLatest, 2004));
128 }
129
130 @Test
131 void shouldConsiderYearsMandatoryWhenNoLongerJan1Anywhere() {
132
133 ZonedDateTime janSecond2004AtEarliest = ZonedDateTime.of(2004, 1, 2, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_EARLIEST);
134 assertFalse(isMandatoryFeedYear(janSecond2004AtEarliest, 2004));
135
136
137 ZonedDateTime janSecond2004AtLatest = ZonedDateTime.of(2004, 1, 2, 0, 0, 0, 1, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_LATEST);
138 assertTrue(isMandatoryFeedYear(janSecond2004AtLatest, 2004));
139 }
140 }
141
142 @Nested
143 class FeedUrlMetadataRetrieval {
144
145 @Test
146 void shouldRetrieveMetadataByYear() throws Exception {
147 try (MockedStatic<Downloader> downloaderClass = mockStatic(Downloader.class)) {
148 Downloader downloader = mock(Downloader.class);
149 when(downloader.fetchContent(any(), any())).thenReturn("lastModifiedDate=2013-01-01T12:00:00Z");
150 downloaderClass.when(Downloader::getInstance).thenReturn(downloader);
151
152 assertThat(retrieveUntil(ZonedDateTime.of(2003, 12, 1, 0, 0, 0, 0, ZoneOffset.UTC)).keySet(),
153 contains("lastModifiedDate.2002", "lastModifiedDate.2003"));
154 }
155 }
156
157 @Test
158 void shouldRetrieveMetadataForNextYearOnJan1AtEarliestTZ() throws Exception {
159 try (MockedStatic<Downloader> downloaderClass = mockStatic(Downloader.class)) {
160 Downloader downloader = mock(Downloader.class);
161 when(downloader.fetchContent(any(), any())).thenReturn("lastModifiedDate=2013-01-01T12:00:00Z");
162 downloaderClass.when(Downloader::getInstance).thenReturn(downloader);
163
164 ZonedDateTime jan1Earliest = ZonedDateTime.of(2004, 1, 1, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_EARLIEST);
165 assertThat(retrieveUntil(jan1Earliest.minusSeconds(1)).keySet(),
166 contains("lastModifiedDate.2002", "lastModifiedDate.2003"));
167
168 assertThat(retrieveUntil(jan1Earliest).keySet(),
169 contains("lastModifiedDate.2002", "lastModifiedDate.2003", "lastModifiedDate.2004"));
170
171 assertThat(retrieveUntil(ZonedDateTime.of(2004, 1, 1, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_LATEST)).keySet(),
172 contains("lastModifiedDate.2002", "lastModifiedDate.2003", "lastModifiedDate.2004"));
173 }
174 }
175
176 @Test
177 void shouldNormallyRethrowDownloadErrorsEvenIfJan1OnEndYear() throws Exception {
178 try (MockedStatic<Downloader> downloaderClass = mockStatic(Downloader.class)) {
179 Downloader downloader = mock(Downloader.class);
180 when(downloader.fetchContent(any(), any())).thenThrow(new DownloadFailedException("failed to download"));
181 downloaderClass.when(Downloader::getInstance).thenReturn(downloader);
182
183 assertThrows(UpdateException.class, () -> retrieveUntil(ZonedDateTime.of(2003, 1, 1, 0, 0, 0, 0, ZoneOffset.UTC)));
184 }
185 }
186
187 @Test
188 void shouldIgnoreDownloadFailureForFinalYearIfStillJan1() throws Exception {
189 List<ZonedDateTime> untilDates = List.of(
190 ZonedDateTime.of(2004, 1, 1, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_EARLIEST),
191 ZonedDateTime.of(2004, 1, 2, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_LATEST)
192 .minusSeconds(1)
193 );
194
195 for (ZonedDateTime until : untilDates) {
196 try (MockedStatic<Downloader> downloaderClass = mockStatic(Downloader.class)) {
197 Downloader downloader = mock(Downloader.class);
198 when(downloader.fetchContent(any(), any()))
199 .thenReturn("lastModifiedDate=2013-01-01T12:00:00Z")
200 .thenReturn("lastModifiedDate=2013-01-01T12:00:00Z")
201 .thenThrow(new DownloadFailedException("failed to download 3rd file"));
202
203 downloaderClass.when(Downloader::getInstance).thenReturn(downloader);
204
205 assertThat(retrieveUntil(until).keySet(),
206 contains("lastModifiedDate.2002", "lastModifiedDate.2003"));
207 }
208 }
209 }
210
211 private @NonNull Map<String, ZonedDateTime> retrieveUntil(ZonedDateTime until) throws UpdateException {
212 Map<String, ZonedDateTime> lastModifieds;
213 NvdApiDataSource.FeedUrl feedUrl = extractFromUrlOptionalPattern("https://internal.server/nist/nvdcve-{0}.json.gz");
214
215 lastModifieds = feedUrl.getLastModifiedDatePropertiesByYear(new Settings(), until);
216
217 assertThat(lastModifieds.values(), everyItem(Matchers.equalTo(ZonedDateTime.of(2013, 1, 1, 12, 0, 0, 0, ZoneOffset.UTC))));
218 return lastModifieds;
219 }
220 }
221 }