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) 2024 Jeremy Long. All Rights Reserved.
17   */
18  package org.owasp.dependencycheck.data.update;
19  
20  import org.jspecify.annotations.NonNull;
21  import org.slf4j.Logger;
22  import org.slf4j.LoggerFactory;
23  
24  import java.io.File;
25  import java.io.FileInputStream;
26  import java.io.FileOutputStream;
27  import java.io.IOException;
28  import java.io.InputStream;
29  import java.io.OutputStream;
30  import java.time.Duration;
31  import java.time.Instant;
32  import java.util.Properties;
33  
34  import static org.owasp.dependencycheck.utils.FileUtils.existsWithContent;
35  
36  /**
37   *
38   * @author Jeremy Long
39   */
40  public abstract class LocalDataSource implements CachedWebDataSource {
41  
42      /**
43       * Static logger.
44       */
45      private static final Logger LOGGER = LoggerFactory.getLogger(LocalDataSource.class);
46  
47      /**
48       * Saves the timestamp in a properties file adjacent to the provided repo file
49       *
50       * @param repo the local file data source
51       */
52      protected void saveLastUpdated(@NonNull File repo) {
53          final File timestampFile = new File(repo + ".properties");
54          try (OutputStream out = new FileOutputStream(timestampFile)) {
55              final Properties prop = new Properties();
56              prop.setProperty("LAST_UPDATED", String.valueOf(System.currentTimeMillis()));
57              prop.store(out, null);
58          } catch (IOException ex) {
59              throw new RuntimeException(ex);
60          }
61      }
62  
63      /**
64       * Retrieves the last updated date from the local file system (in a file
65       * next to the repo file).
66       *
67       * @param repo the local file data source
68       * @return the instant of the last updated date/time
69       */
70      protected Instant getLastUpdated(@NonNull File repo) {
71          long lastUpdatedOn = 0;
72          final File timestampFile = new File(repo + ".properties");
73          if (timestampFile.isFile()) {
74              try (InputStream is = new FileInputStream(timestampFile)) {
75                  final Properties props = new Properties();
76                  props.load(is);
77                  lastUpdatedOn = Long.parseLong(props.getProperty("LAST_UPDATED", "0"));
78              } catch (IOException | NumberFormatException ex) {
79                  LOGGER.debug("error reading timestamp file", ex);
80              }
81              if (lastUpdatedOn <= 0) {
82                  //fall back on conversion from file last modified
83                  lastUpdatedOn = repo.lastModified();
84              }
85          }
86          return Instant.ofEpochMilli(lastUpdatedOn);
87      }
88  
89      /**
90       * Determines if we should update the local data source.
91       *
92       * @param repo the local file data source
93       * @param validFor the duration for which the local data source should be considered valid
94       * @return <code>true</code> if an update to the data source should be performed; otherwise <code>false</code>.
95       *         If the repo does not exist, or is an empty file, it is considered stale.
96       */
97      protected boolean isStale(@NonNull File repo, @NonNull Duration validFor) {
98          boolean stale = true;
99          if (existsWithContent(repo)) {
100             final Instant lastUpdatedOn = getLastUpdated(repo);
101             final Instant now = Instant.now();
102             LOGGER.debug("{} last updated: {}, now: {}", getClass().getSimpleName(), lastUpdatedOn, now);
103             stale = lastUpdatedOn.plus(validFor).isBefore(now);
104             if (!stale) {
105                 LOGGER.info("Should skip {} update since last update was within period {}.", getClass().getSimpleName(), validFor);
106             }
107         }
108         return stale;
109     }
110 }