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.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             // It's still Jan 1 somewhere...
133             ZonedDateTime janSecond2004AtEarliest = ZonedDateTime.of(2004, 1, 2, 0, 0, 0, 0, NvdApiDataSource.FeedUrl.ZONE_GLOBAL_EARLIEST);
134             assertFalse(isMandatoryFeedYear(janSecond2004AtEarliest, 2004));
135 
136             // Until it's no longer Jan 1 anywhere
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 }