View Javadoc
1   /*
2    * This file is part of dependency-check-core.
3    *
4    * Licensed under the Apache License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    *     http://www.apache.org/licenses/LICENSE-2.0
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   *
16   * Copyright (c) 2023 Jeremy Long. All Rights Reserved.
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             // It's still Jan 1 somewhere...
132             ZonedDateTime janSecond2004AtEarliest = ZonedDateTime.of(2004, 1, 2, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_EARLIEST);
133             assertFalse(isMandatoryFeedYear(janSecond2004AtEarliest, 2004));
134 
135             // Until it's no longer Jan 1 anywhere
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 }