All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.metaeffekt.mirror.download.advisor.MsrcSecurityGuideDownload Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2021-2024 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.metaeffekt.mirror.download.advisor;

import com.metaeffekt.artifact.analysis.utils.FileUtils;
import com.metaeffekt.artifact.analysis.utils.StringUtils;
import com.metaeffekt.artifact.analysis.utils.TimeUtils;
import com.metaeffekt.mirror.download.documentation.MirrorMetadata;
import com.metaeffekt.mirror.Retry;
import com.metaeffekt.mirror.download.Download;
import com.metaeffekt.mirror.download.ResourceLocation;
import com.metaeffekt.mirror.download.documentation.DocRelevantMethods;
import org.json.JSONArray;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import static com.metaeffekt.mirror.download.advisor.MsrcSecurityGuideDownload.ResourceLocationMsrcSecurityUpdateGuide.AFFECTED_PRODUCTS_BASE_URL;

/**
 * 

References:

* *

As mentioned in the MSRC Download, the MSRC splits its data into three separate data sources. * This particular data source used to only be available via manual CSV file downloads, but since then, a way was found to perform these requests automatically.

*

Another thing to investigate is this new API (as of 2024-09-07) they made available for use: * https://api.msrc.microsoft.com/cvrf/v3.0/swagger/v3/swagger.json * and whether it can replace this existing one once more.

*/ @MirrorMetadata(directoryName = "msrc-security-guide", mavenPropertyName = "msrcSecurityGuideDownload") public class MsrcSecurityGuideDownload extends Download { private final static Logger LOG = LoggerFactory.getLogger(MsrcSecurityGuideDownload.class); private final static String START_DATE = "2016-01-01T00:00:00+01:00"; private final static int WRITE_TO_FILE_THRESHOLD = 20; public MsrcSecurityGuideDownload(File baseMirrorDirectory) { super(baseMirrorDirectory, MsrcSecurityGuideDownload.class); } @Override @DocRelevantMethods({"MsrcSecurityGuideDownload#downloadAllFromTo", "MsrcSecurityGuideDownload#downloadSecurityGuideByDateRangeAndOffset"}) public void performDownload() { final String startDate = determineStartDate(); final String endDate = getCurrentTimestampForRequest(); downloadAllFromTo(startDate, endDate); } private void downloadAllFromTo(String startDate, String endDate) { int offset = 0; int totalEntriesCount = 0; int currentEntriesCount = 0; int currentRequestsSinceLastWriteToFile = 0; long startTime = System.currentTimeMillis(); final Map> entriesToBeWrittenToFile = new ConcurrentHashMap<>(); super.executor.setDelay(0); do { try { final JSONObject response = downloadSecurityGuideByDateRangeAndOffset(startDate, endDate, offset); if (isResponseEmpty(response)) { LOG.info("No more entries found. Stopping download."); break; } totalEntriesCount = response.getInt("@odata.count"); currentEntriesCount = response.getJSONArray("value").length(); offset += currentEntriesCount; currentRequestsSinceLastWriteToFile++; sortEntriesIntoYears(response, entriesToBeWrittenToFile); if (currentRequestsSinceLastWriteToFile >= WRITE_TO_FILE_THRESHOLD) { final long currentTime = System.currentTimeMillis(); final long elapsedTime = currentTime - startTime; final long expectedRemainingTime = (elapsedTime / offset) * (totalEntriesCount - offset); LOG.info("Downloaded [{} / {}] entries, expected time remaining [{}]", offset, totalEntriesCount, TimeUtils.formatTimeDiff(expectedRemainingTime)); super.executor.submit(() -> { try { writeEntriesToFile(entriesToBeWrittenToFile); } catch (IOException e) { throw new RuntimeException("Failed to write entries to file", e); } }); super.executor.start(); currentRequestsSinceLastWriteToFile = 0; } } catch (IOException e) { LOG.error("Error while downloading security guide.", e); break; } } while (offset < totalEntriesCount); try { super.executor.join(); } catch (InterruptedException e) { throw new RuntimeException("Failed to join executor.", e); } try { writeEntriesToFile(entriesToBeWrittenToFile); } catch (IOException e) { LOG.error("Error while writing entries to file.", e); } try { setPreviousTotalEntriesCount(getTotalAvailableEntriesCount()); } catch (IOException e) { setPreviousTotalEntriesCount(totalEntriesCount); } setLastMirrorDate(endDate); } private void writeEntriesToFile(Map> entriesToBeWrittenToFile) throws IOException { // parse entries from file, merge with new entries, write back to file // check for duplicate unique ids final Map> entriesToBeWrittenToFileCopy; synchronized (entriesToBeWrittenToFile) { if (entriesToBeWrittenToFile.isEmpty()) { return; } entriesToBeWrittenToFileCopy = new HashMap<>(entriesToBeWrittenToFile); entriesToBeWrittenToFile.clear(); } for (Map.Entry> entry : entriesToBeWrittenToFileCopy.entrySet()) { final String year = entry.getKey(); final List entries = entry.getValue(); final File yearFile = new File(super.downloadIntoDirectory, year + ".json"); final JSONArray yearFileContent; if (yearFile.exists()) { yearFileContent = new JSONArray(FileUtils.readFileToString(yearFile, StandardCharsets.UTF_8)); } else { yearFileContent = new JSONArray(); } for (JSONObject newEntry : entries) { final String newEntryId = newEntry.getString("id"); boolean entryAlreadyExists = false; for (int i = 0; i < yearFileContent.length(); i++) { final JSONObject existingEntry = yearFileContent.getJSONObject(i); final String existingEntryId = existingEntry.getString("id"); if (newEntryId.equals(existingEntryId)) { entryAlreadyExists = true; break; } } if (!entryAlreadyExists) { yearFileContent.put(newEntry); } } FileUtils.writeStringToFile(yearFile, yearFileContent.toString(), StandardCharsets.UTF_8); } } private void sortEntriesIntoYears(JSONObject response, Map> entriesByYear) { final JSONArray entries = response.getJSONArray("value"); synchronized (entriesByYear) { for (int i = 0; i < entries.length(); i++) { final JSONObject entry = entries.getJSONObject(i); final String releaseDate = entry.getString("releaseDate"); // 2023-06-29T22:02:55Z final String year = releaseDate.substring(0, 4); entriesByYear.computeIfAbsent(year, k -> new ArrayList<>()).add(entry); } } } private String determineStartDate() { final String lastMirrorDate = getLastMirrorDate(); final String startDate; if (StringUtils.hasText(lastMirrorDate)) { startDate = lastMirrorDate; } else { startDate = START_DATE; } return startDate; } private JSONObject downloadSecurityGuideByDateRangeAndOffset(String startDate, String endDate, int offset) throws IOException { return new Retry<>(() -> { final Map getParameters = new HashMap<>(); getParameters.put("$orderBy", "releaseDate asc"); getParameters.put("$filter", "(releaseDate gt " + startDate + ") and (releaseDate lt " + endDate + ")"); getParameters.put("$skip", String.valueOf(offset)); final String getRequestUrl = super.downloader.buildGetRequest(getRemoteResourceLocation(AFFECTED_PRODUCTS_BASE_URL), getParameters); final List response = super.downloader.fetchResponseBodyFromUrlAsList(new URL(getRequestUrl)); return new JSONObject(String.join("", response)); }) .onException(Throwable.class) .withValidator(json -> { // must not be {"error":{"code":"400","message":"..."}} if (json.has("error")) { final JSONObject error = json.getJSONObject("error"); final String code = error.getString("code"); final String message = error.getString("message"); LOG.error("Error while downloading security guide: [{}] {}", code, message); return false; } return true; }) .retryCount(5) .withDelay(1000 * 5) .run(); } private boolean isResponseEmpty(JSONObject response) { return response.getJSONArray("value").isEmpty(); } private String getCurrentTimestampForRequest() { final OffsetDateTime now = OffsetDateTime.now(ZoneId.of("Europe/Paris")); // or: ZoneOffset.of("+01:00") final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ssXXX"); return now.format(formatter); } private void setPreviousTotalEntriesCount(int count) { super.propertyFiles.set(this.downloadIntoDirectory, "info", InfoFileAttributes.MSRC_PREFIX.getKey() + "entries-count", String.valueOf(count)); } private int getPreviousTotalEntriesCount() { return Integer.parseInt(super.propertyFiles.getString(this.downloadIntoDirectory, "info", InfoFileAttributes.MSRC_PREFIX.getKey() + "entries-count").orElse("0")); } private void setLastMirrorDate(String date) { super.propertyFiles.set(this.downloadIntoDirectory, "info", InfoFileAttributes.MSRC_PREFIX.getKey() + "last-mirror-date", date); } private String getLastMirrorDate() { return super.propertyFiles.getString(this.downloadIntoDirectory, "info", InfoFileAttributes.MSRC_PREFIX.getKey() + "last-mirror-date").orElse(""); } private int getTotalAvailableEntriesCount() throws IOException { final JSONObject response = downloadSecurityGuideByDateRangeAndOffset(START_DATE, getCurrentTimestampForRequest(), 999999999); return response.getInt("@odata.count"); } @Override protected boolean isResetRequired() { return false; } @Override protected boolean additionalIsDownloadRequired() { try { final int totalEntriesCount = getTotalAvailableEntriesCount(); final int previousTotalEntriesCount = getPreviousTotalEntriesCount(); if (previousTotalEntriesCount != totalEntriesCount) { LOG.info("Total entries count changed from [{}] to [{}], download is required", previousTotalEntriesCount, totalEntriesCount); return true; } else { return false; } } catch (IOException e) { return true; } } @Override public void setRemoteResourceLocation(String location, String url) { super.setRemoteResourceLocation(ResourceLocationMsrcSecurityUpdateGuide.valueOf(location), url); } public enum ResourceLocationMsrcSecurityUpdateGuide implements ResourceLocation { /** * Endpoint that https://msrc.microsoft.com/update-guide/ * uses to construct the xlsx and csv files locally.
* Has parameters that are filled by the code, without format strings here. */ AFFECTED_PRODUCTS_BASE_URL("https://api.msrc.microsoft.com/sug/v2.0/en-US/affectedProduct"); private final String defaultValue; ResourceLocationMsrcSecurityUpdateGuide(String defaultValue) { this.defaultValue = defaultValue; } @Override public String getDefault() { return this.defaultValue; } } }




© 2015 - 2025 Weber Informatics LLC | Privacy Policy