package com.denimgroup.threadfix.service.report;
import java.text.DateFormatSymbols;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Calendar;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeSet;
import net.sf.jasperreports.engine.JRDataSource;
import net.sf.jasperreports.engine.JRField;
import com.denimgroup.threadfix.data.dao.ScanDao;
import com.denimgroup.threadfix.data.entities.Scan;
/**
* The current strategy for this report is to keep Sets of the vulnerability IDs
* representing the new / old / reopened vulns for each month.
* All the collected scans are iterated through, following this process:
* <br>
* <br>Vulns are added to the new vuln set if the scan contains the first finding for the vuln.
* <br>If a vuln is reopened by a scan, it is added to the reopened set.
* <br>At the end of every month, all vulns are moved to the old vulns set for future months.
* <br>If a vuln is closed by a scan, it is removed from all three sets.
* <br>
* <br>For each month, this method should yield counts for:
* <br>All the new vulns that weren't also closed in the same month,
* <br>The number of old vulnerabilities from previous months still open at the end of the month,
* <br>And the number of vulnerabilities that resurfaced and were still open at the end of the month.
*
* @author mcollins
*
*/
public class JasperXMonthSummaryReport implements JRDataSource {
private List<List<Scan>> normalizedScans = new ArrayList<>();
private List<String> dateList = new ArrayList<>();
private int index = 0, numMonths = 0;
private Map<String, Object> resultsHash = new HashMap<String, Object>();
private ScanDao scanDao = null;
private static final String[] months = new DateFormatSymbols().getMonths();
public JasperXMonthSummaryReport(List<List<Scan>> scanLists, ScanDao scanDao, int numMonths) {
this.scanDao = scanDao;
if (numMonths > 0 && numMonths <= 12) {
this.numMonths = numMonths;
} else {
numMonths = 6;
}
if (scanLists != null && scanLists.size() > 0) {
for (List<Scan> scanList : scanLists) {
Collections.sort(scanList, Scan.getTimeComparator());
normalizedScans.add(buildNormalizedScans(scanList));
}
}
index = -1;
}
/////////////////////////////////////////////////////////////
// These methods calculate the correct scan statistics //
/////////////////////////////////////////////////////////////
private List<Scan> buildNormalizedScans(List<Scan> startingScans) {
Map<Integer, Map<YearAndMonth, Scan>> channelScanMap = new HashMap<>();
for (Scan scan : startingScans) {
YearAndMonth yearAndMonth = new YearAndMonth(scan.getImportTime());
Integer applicationChannelId = scan.getApplicationChannel().getId();
if (!channelScanMap.containsKey(applicationChannelId)) {
channelScanMap.put(applicationChannelId, new HashMap<YearAndMonth, Scan>());
}
channelScanMap.get(applicationChannelId).put(yearAndMonth, scan);
}
YearAndMonth now = new YearAndMonth(Calendar.getInstance());
addIntermediateScans(channelScanMap, now);
Map<YearAndMonth, List<Integer>> results = collapseScans(channelScanMap, now.pastXMonths(numMonths));
return getFinalScans(results);
}
private void addIntermediateScans(Map<Integer, Map<YearAndMonth, Scan>> scansHash, YearAndMonth now) {
for (Integer key : scansHash.keySet()) {
Map<YearAndMonth, Scan> entry = scansHash.get(key);
TreeSet<YearAndMonth> times = new TreeSet<>(entry.keySet());
YearAndMonth currentTime = times.first();
Scan currentScan = entry.get(currentTime);
while (currentTime.compareTo(now) <= 0) {
if (entry.containsKey(currentTime)) {
currentScan = entry.get(currentTime);
} else {
entry.put(currentTime, currentScan);
}
currentTime = currentTime.next();
}
}
}
private Map<YearAndMonth, List<Integer>> collapseScans(Map<Integer, Map<YearAndMonth, Scan>> scansHash,
List<YearAndMonth> times) {
Map<YearAndMonth, Calendar> timeMap = new HashMap<>();
Map<YearAndMonth, List<Integer>> scanIds = new HashMap<>();
for (YearAndMonth time : times) {
scanIds.put(time, new ArrayList<Integer>());
if (scansHash != null) {
for (Integer key : scansHash.keySet()) {
if (scansHash.get(key) != null && scansHash.get(key).get(time) != null) {
Scan scan = scansHash.get(key).get(time);
scanIds.get(time).add(scan.getId());
if (!timeMap.containsKey(time)) {
timeMap.put(time, scan.getImportTime());
}
}
}
}
}
return scanIds;
}
private List<Scan> getFinalScans(Map<YearAndMonth, List<Integer>> results) {
List<Scan> scanList = new ArrayList<>();
for (YearAndMonth yearAndMonth : new TreeSet<YearAndMonth>(results.keySet())) {
List<Integer> result = results.get(yearAndMonth);
if (result != null && !result.isEmpty()) {
Map<String, Object> map = scanDao.getCountsForScans(result);
Scan scan = new Scan();
scan.setNumberCriticalVulnerabilities((Long) map.get("critical"));
scan.setNumberHighVulnerabilities((Long) map.get("high"));
scan.setNumberMediumVulnerabilities((Long) map.get("medium"));
scan.setNumberLowVulnerabilities((Long) map.get("low"));
dateList.add(yearAndMonth.getMonthName());
scanList.add(scan);
} else {
Scan scan = new Scan();
scan.setNumberCriticalVulnerabilities(0L);
scan.setNumberHighVulnerabilities(0L);
scan.setNumberMediumVulnerabilities(0L);
scan.setNumberLowVulnerabilities(0L);
dateList.add(yearAndMonth.getMonthName());
scanList.add(scan);
}
}
return scanList;
}
///////////////////////////////////////////////////////////////////
// This method makes it easier to use dates as keys in a map. //
///////////////////////////////////////////////////////////////////
class YearAndMonth implements Comparable<YearAndMonth> {
private int year, month;
YearAndMonth(int year, int month) { this.year = year; this.month = month; }
YearAndMonth(Calendar calendar) {
this.year = calendar.get(Calendar.YEAR);
this.month = calendar.get(Calendar.MONTH) + 1;
}
public YearAndMonth next() {
return addMonths(1);
}
public String toString() {
return "" + year + "-" + month;
}
public YearAndMonth addMonths(int num) {
if (num == 0) { return this; }
if (month + num > 12) {
return new YearAndMonth(year + ((month + num) / 12), ((month + num) % 12));
} else if (month + num < 1) {
return new YearAndMonth(year - 1 - ((month + num) / 12), ((month + num) % 12) + 12);
} else {
return new YearAndMonth(year, month + num);
}
}
public List<YearAndMonth> pastXMonths(int numMonths) {
YearAndMonth array[] = new YearAndMonth[numMonths];
for (int i = 0; i < numMonths; i ++) {
array[i] = this.addMonths(- i);
}
return Arrays.asList(array);
}
public String getMonthName() {
return months[month-1];
}
@Override
public int compareTo(YearAndMonth o) {
int retVal;
YearAndMonth other = ((YearAndMonth) o);
if (other.year > this.year) {
retVal = -1;
} else if (this.year > other.year) {
retVal = 1;
} else if (other.month > this.month) {
retVal = -1;
} else if (this.month > other.month) {
retVal = 1;
} else {
retVal = 0;
}
return(retVal);
}
public boolean equals(Object o) {
if (o != null && o instanceof YearAndMonth) {
YearAndMonth object = (YearAndMonth) o;
return object.year == this.year && object.month == this.month;
} else {
return false;
}
}
public int hashCode() {
return year * 100 + month;
}
}
////////////////////////////////////////////////////////////
// These methods implement the JRDataSource interface //
////////////////////////////////////////////////////////////
@Override
public Object getFieldValue(JRField field) {
if (field == null)
return null;
String name = field.getName();
if (name == null)
return null;
if (resultsHash.containsKey(name))
return resultsHash.get(name);
else
return null;
}
@Override
public boolean next() {
if (normalizedScans != null && normalizedScans.size() > 0 &&
index < normalizedScans.get(0).size() - 1) {
if (index == -1)
index = 0;
else
index++;
buildHash();
return true;
} else {
return false;
}
}
private void buildHash() {
resultsHash.clear();
long numCritical = 0,numHigh = 0,numMedium = 0,numLow = 0;
for (List<Scan> scanList: normalizedScans) {
Scan scan = scanList.get(index);
numCritical += scan.getNumberCriticalVulnerabilities();
numHigh += scan.getNumberHighVulnerabilities();
numMedium += scan.getNumberMediumVulnerabilities();
numLow += scan.getNumberLowVulnerabilities();
}
resultsHash.put("criticalVulns", numCritical);
resultsHash.put("highVulns", numHigh);
resultsHash.put("mediumVulns", numMedium);
resultsHash.put("lowVulns", numLow);
if (dateList.get(index) != null)
resultsHash.put("importTime", dateList.get(index));
}
}