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