diff --git a/Core/build.xml b/Core/build.xml index e8966c2508..40b8e966a3 100644 --- a/Core/build.xml +++ b/Core/build.xml @@ -89,6 +89,11 @@ + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/contentviewers/osaccount/OsAccountDataPanel.java b/Core/src/org/sleuthkit/autopsy/contentviewers/osaccount/OsAccountDataPanel.java index f5f07d658b..82c267abff 100755 --- a/Core/src/org/sleuthkit/autopsy/contentviewers/osaccount/OsAccountDataPanel.java +++ b/Core/src/org/sleuthkit/autopsy/contentviewers/osaccount/OsAccountDataPanel.java @@ -191,11 +191,10 @@ public class OsAccountDataPanel extends JPanel { private SectionData buildRealmProperties(OsAccountRealm realm) { SectionData data = new SectionData(Bundle.OsAccountDataPanel_realm_title()); - Optional optional = realm.getRealmName(); - data.addData(Bundle.OsAccountDataPanel_realm_name(), - optional.isPresent() ? optional.get() : Bundle.OsAccountDataPanel_realm_unknown()); + String realmName = realm.getRealmNames().isEmpty() ? Bundle.OsAccountDataPanel_realm_unknown() : realm.getRealmNames().get(0); + data.addData(Bundle.OsAccountDataPanel_realm_name(), realmName); - optional = realm.getRealmAddr(); + Optional optional = realm.getRealmAddr(); data.addData(Bundle.OsAccountDataPanel_realm_address(), optional.isPresent() ? optional.get() : ""); diff --git a/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java b/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java index 94f15589a0..29e36b182a 100755 --- a/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java +++ b/Core/src/org/sleuthkit/autopsy/datamodel/OsAccounts.java @@ -256,12 +256,12 @@ public final class OsAccounts implements AutopsyVisitableItem { Bundle.OsAccounts_loginNameProperty_desc(), optional.isPresent() ? optional.get() : "")); - optional = account.getRealm().getRealmName(); + String realmName = account.getRealm().getRealmNames().isEmpty() ? "" : account.getRealm().getRealmNames().get(0); propertiesSet.put(new NodeProperty<>( Bundle.OsAccounts_accountRealmNameProperty_name(), Bundle.OsAccounts_accountRealmNameProperty_displayName(), Bundle.OsAccounts_accountRealmNameProperty_desc(), - optional.isPresent() ? optional.get() : "")); + realmName)); Optional creationTimeValue = account.getCreationTime(); String timeDisplayStr diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED index 4a47bcb092..a7b3c8870c 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/Bundle.properties-MERGED @@ -76,6 +76,8 @@ ExcelExportAction_exportToXLSX_gatheringTabData=Fetching Data for {0} Tab... ExcelExportAction_exportToXLSX_writingToFile=Writing to File... ExcelExportAction_getXLSXPath_directory=DataSourceSummary ExcelExportAction_moduleName=Data Source Summary +ExcelExportAction_runXLSXExport_errorMessage=There was an error while exporting. +ExcelExportAction_runXLSXExport_errorTitle=Error While Exporting ExcelExportAction_runXLSXExport_progressCancelActionTitle=Cancelling... ExcelExportAction_runXLSXExport_progressCancelTitle=Cancel # {0} - dataSource @@ -114,6 +116,12 @@ SizeRepresentationUtil_units_megabytes=MB SizeRepresentationUtil_units_petabytes=PB SizeRepresentationUtil_units_terabytes=TB TimelinePanel_earliestLabel_title=Earliest +TimelinePanel_getExports_activityRange=Activity Range +TimelinePanel_getExports_chartName=Last 30 Days +TimelinePanel_getExports_dateColumnHeader=Date +TimelinePanel_getExports_earliest=Earliest: +TimelinePanel_getExports_latest=Latest: +TimelinePanel_getExports_sheetName=Timeline TimelinePanel_latestLabel_title=Latest TimlinePanel_last30DaysChart_artifactEvts_title=Result Events TimlinePanel_last30DaysChart_fileEvts_title=File Events @@ -128,6 +136,7 @@ TypesPanel_fileMimeTypesChart_notAnalyzed_title=Not Analyzed TypesPanel_fileMimeTypesChart_other_title=Other TypesPanel_fileMimeTypesChart_title=File Types TypesPanel_fileMimeTypesChart_unknown_title=Unknown +TypesPanel_fileMimeTypesChart_valueLabel=Count TypesPanel_fileMimeTypesChart_videos_title=Videos TypesPanel_filesByCategoryTable_allocatedRow_title=Allocated Files TypesPanel_filesByCategoryTable_directoryRow_title=Directories diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ExcelExportAction.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ExcelExportAction.java index 3f3ac09ec3..22e06c07ed 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ExcelExportAction.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/ExcelExportAction.java @@ -18,7 +18,6 @@ */ package org.sleuthkit.autopsy.datasourcesummary.ui; -import java.awt.Component; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.io.File; @@ -34,6 +33,7 @@ import java.util.concurrent.CancellationException; import java.util.concurrent.ExecutionException; import java.util.function.Consumer; import java.util.logging.Level; +import javax.swing.JOptionPane; import javax.swing.SwingUtilities; import javax.swing.SwingWorker; import org.openide.util.NbBundle; @@ -186,7 +186,9 @@ class ExcelExportAction implements Consumer { "# {0} - dataSource", "ExcelExportAction_runXLSXExport_progressTitle=Exporting {0} to XLSX", "ExcelExportAction_runXLSXExport_progressCancelTitle=Cancel", - "ExcelExportAction_runXLSXExport_progressCancelActionTitle=Cancelling..." + "ExcelExportAction_runXLSXExport_progressCancelActionTitle=Cancelling...", + "ExcelExportAction_runXLSXExport_errorTitle=Error While Exporting", + "ExcelExportAction_runXLSXExport_errorMessage=There was an error while exporting.", }) private void runXLSXExport(DataSource dataSource, File path) { @@ -213,6 +215,10 @@ class ExcelExportAction implements Consumer { get(); } catch (ExecutionException ex) { logger.log(Level.WARNING, "Error while trying to export data source summary to xlsx.", ex); + JOptionPane.showMessageDialog(WindowManager.getDefault().getMainWindow(), + Bundle.ExcelExportAction_runXLSXExport_errorMessage(), + Bundle.ExcelExportAction_runXLSXExport_errorTitle(), + JOptionPane.ERROR_MESSAGE); } catch (InterruptedException | CancellationException ex) { // no op on cancellation } finally { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java index ff30a72331..94af4b7545 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TimelinePanel.java @@ -39,14 +39,20 @@ import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineDataSourceUtils import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.DailyActivityAmount; import org.sleuthkit.autopsy.datasourcesummary.datamodel.TimelineSummary.TimelineSummaryData; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartExport; import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartItem; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.BarChartSeries; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartSeries; import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartPanel.OrderedKey; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartSeries.BarChartItem; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker.DataFetchComponents; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetcher; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.KeyValueItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.TitledExportable; import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel; @@ -70,7 +76,9 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { private static final Logger logger = Logger.getLogger(TimelinePanel.class.getName()); private static final long serialVersionUID = 1L; - private static final DateFormat EARLIEST_LATEST_FORMAT = getUtcFormat("MMM d, yyyy"); + + private static final String EARLIEST_LATEST_FORMAT_STR = "MMM d, yyyy"; + private static final DateFormat EARLIEST_LATEST_FORMAT = getUtcFormat(EARLIEST_LATEST_FORMAT_STR); private static final DateFormat CHART_FORMAT = getUtcFormat("MMM d, yyyy"); private static final int MOST_RECENT_DAYS_COUNT = 30; @@ -94,6 +102,8 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { // all loadable components on this tab private final List> loadableComponents = Arrays.asList(earliestLabel, latestLabel, last30DaysChart); + private final DataFetcher dataFetcher; + // actions to load data for this tab private final List> dataFetchComponents; @@ -107,12 +117,11 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { public TimelinePanel(TimelineSummary timelineData) { super(timelineData); + dataFetcher = (dataSource) -> timelineData.getData(dataSource, MOST_RECENT_DAYS_COUNT); + // set up data acquisition methods dataFetchComponents = Arrays.asList( - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> timelineData.getData(dataSource, MOST_RECENT_DAYS_COUNT), - (result) -> handleResult(result)) - ); + new DataFetchWorker.DataFetchComponents<>(dataFetcher, (result) -> handleResult(result))); initComponents(); } @@ -138,9 +147,11 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { * data to be displayed as a bar chart. * * @param recentDaysActivity The data retrieved from TimelineSummary. + * @param showIntermediateDates If true, shows all dates. If false, shows + * only first and last date. * @return The data to be displayed in the BarChart. */ - private List parseChartData(List recentDaysActivity) { + private List parseChartData(List recentDaysActivity, boolean showIntermediateDates) { // if no data, return null indicating no result. if (CollectionUtils.isEmpty(recentDaysActivity)) { return null; @@ -155,7 +166,7 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { long fileAmt = curItem.getFileActivityCount(); long artifactAmt = curItem.getArtifactActivityCount() * 100; - String formattedDate = (i == 0 || i == recentDaysActivity.size() - 1) + String formattedDate = (showIntermediateDates || i == 0 || i == recentDaysActivity.size() - 1) ? formatDate(curItem.getDay(), CHART_FORMAT) : ""; OrderedKey thisKey = new OrderedKey(formattedDate, i); @@ -182,7 +193,7 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { private void handleResult(DataFetchResult result) { earliestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> formatDate(r.getMinDate(), EARLIEST_LATEST_FORMAT))); latestLabel.showDataFetchResult(DataFetchResult.getSubResult(result, r -> formatDate(r.getMaxDate(), EARLIEST_LATEST_FORMAT))); - last30DaysChart.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseChartData(r.getMostRecentDaysActivity()))); + last30DaysChart.showDataFetchResult(DataFetchResult.getSubResult(result, r -> parseChartData(r.getMostRecentDaysActivity(), false))); if (result != null && result.getResultType() == DataFetchResult.ResultType.SUCCESS @@ -282,9 +293,41 @@ public class TimelinePanel extends BaseDataSourceSummaryPanel { super.close(); } + /** + * Create a default cell model to be use with excel export in the earliest / + * latest date format. + * + * @param date The date. + * @return The cell model. + */ + private static DefaultCellModel getEarliestLatestCell(Date date) { + return new DefaultCellModel<>(date, (dt) -> dt == null ? "" : EARLIEST_LATEST_FORMAT.format(dt), EARLIEST_LATEST_FORMAT_STR); + } + + @Messages({ + "TimelinePanel_getExports_sheetName=Timeline", + "TimelinePanel_getExports_activityRange=Activity Range", + "TimelinePanel_getExports_earliest=Earliest:", + "TimelinePanel_getExports_latest=Latest:", + "TimelinePanel_getExports_dateColumnHeader=Date", + "TimelinePanel_getExports_chartName=Last 30 Days",}) @Override List getExports(DataSource dataSource) { - return Collections.emptyList(); + TimelineSummaryData summaryData = getFetchResult(dataFetcher, "Timeline", dataSource); + if (summaryData == null) { + return Collections.emptyList(); + } + + return Arrays.asList( + new ExcelSpecialFormatExport(Bundle.TimelinePanel_getExports_sheetName(), + Arrays.asList( + new TitledExportable(Bundle.TimelinePanel_getExports_activityRange(), Collections.emptyList()), + new KeyValueItemExportable(Bundle.TimelinePanel_getExports_earliest(), getEarliestLatestCell(summaryData.getMinDate())), + new KeyValueItemExportable(Bundle.TimelinePanel_getExports_latest(), getEarliestLatestCell(summaryData.getMaxDate())), + new BarChartExport(Bundle.TimelinePanel_getExports_dateColumnHeader(), + "#,###", + Bundle.TimelinePanel_getExports_chartName(), + parseChartData(summaryData.getMostRecentDaysActivity(), true))))); } /** diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java index 97fc63fc51..e11e638459 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/ui/TypesPanel.java @@ -47,8 +47,9 @@ import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport. import org.sleuthkit.autopsy.datasourcesummary.uiutils.IngestRunningLabel; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableComponent; import org.sleuthkit.autopsy.datasourcesummary.uiutils.LoadableLabel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartExport; import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel; -import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartPanel.PieChartItem; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.PieChartItem; import org.sleuthkit.datamodel.DataSource; import org.sleuthkit.datamodel.TskCoreException; @@ -64,6 +65,7 @@ import org.sleuthkit.datamodel.TskCoreException; "TypesPanel_filesByCategoryTable_slackRow_title=Slack Files", "TypesPanel_filesByCategoryTable_directoryRow_title=Directories", "TypesPanel_fileMimeTypesChart_title=File Types", + "TypesPanel_fileMimeTypesChart_valueLabel=Count", "TypesPanel_fileMimeTypesChart_audio_title=Audio", "TypesPanel_fileMimeTypesChart_documents_title=Documents", "TypesPanel_fileMimeTypesChart_executables_title=Executables", @@ -197,6 +199,8 @@ class TypesPanel extends BaseDataSourceSummaryPanel { private final DataFetcher osFetcher; private final DataFetcher sizeFetcher; + private final DataFetcher typesFetcher; + private final DataFetcher allocatedFetcher; private final DataFetcher unallocatedFetcher; private final DataFetcher slackFetcher; @@ -261,6 +265,8 @@ class TypesPanel extends BaseDataSourceSummaryPanel { this.osFetcher = containerData::getOperatingSystems; this.sizeFetcher = (dataSource) -> dataSource == null ? null : dataSource.getSize(); + + this.typesFetcher = (dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource); this.allocatedFetcher = (dataSource) -> typeData.getCountOfAllocatedFiles(dataSource); this.unallocatedFetcher = (dataSource) -> typeData.getCountOfUnallocatedFiles(dataSource); @@ -274,9 +280,7 @@ class TypesPanel extends BaseDataSourceSummaryPanel { (sizeResult) -> sizeLabel.showDataFetchResult( DataFetchResult.getSubResult(sizeResult, size -> SizeRepresentationUtil.getSizeString(size, INTEGER_SIZE_FORMAT, false)))), - new DataFetchWorker.DataFetchComponents<>( - (dataSource) -> getMimeTypeCategoriesModel(mimeTypeData, dataSource), - this::showMimeTypeCategories), + new DataFetchWorker.DataFetchComponents<>(typesFetcher, this::showMimeTypeCategories), new DataFetchWorker.DataFetchComponents<>(allocatedFetcher, countRes -> allocatedLabel.showDataFetchResult(DataFetchResult.getSubResult(countRes, (count) -> getStringOrZero(count)))), new DataFetchWorker.DataFetchComponents<>(unallocatedFetcher, @@ -442,6 +446,16 @@ class TypesPanel extends BaseDataSourceSummaryPanel { if (dataSource == null) { return Collections.emptyList(); } + + // Retrieve data to create the types pie chart + TypesPieChartData typesData = TypesPanel.getFetchResult(typesFetcher, "Types", dataSource); + PieChartExport typesChart = (typesData == null || !typesData.isUsefulContent()) ? null : + new PieChartExport( + Bundle.TypesPanel_fileMimeTypesChart_title(), + Bundle.TypesPanel_fileMimeTypesChart_valueLabel(), + "#,###", + Bundle.TypesPanel_fileMimeTypesChart_title(), + typesData.getPieSlices()); return Arrays.asList(new ExcelSpecialFormatExport(Bundle.TypesPanel_excelTabName(), Stream.of( @@ -449,6 +463,7 @@ class TypesPanel extends BaseDataSourceSummaryPanel { getStrExportable(osFetcher, Bundle.TypesPanel_osLabel_title(), dataSource), new KeyValueItemExportable(Bundle.TypesPanel_sizeLabel_title(), SizeRepresentationUtil.getBytesCell(getFetchResult(sizeFetcher, "Types", dataSource))), + typesChart, getCountExportable(allocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_allocatedRow_title(), dataSource), getCountExportable(unallocatedFetcher, Bundle.TypesPanel_filesByCategoryTable_unallocatedRow_title(), dataSource), getCountExportable(slackFetcher, Bundle.TypesPanel_filesByCategoryTable_slackRow_title(), dataSource), diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartExport.java new file mode 100644 index 0000000000..8353549e75 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartExport.java @@ -0,0 +1,273 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.Color; +import java.nio.ByteBuffer; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.lang3.tuple.Pair; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xddf.usermodel.XDDFColor; +import org.apache.poi.xddf.usermodel.XDDFShapeProperties; +import org.apache.poi.xddf.usermodel.XDDFSolidFillProperties; +import org.apache.poi.xddf.usermodel.chart.AxisCrosses; +import org.apache.poi.xddf.usermodel.chart.AxisPosition; +import org.apache.poi.xddf.usermodel.chart.BarDirection; +import org.apache.poi.xddf.usermodel.chart.BarGrouping; +import org.apache.poi.xddf.usermodel.chart.ChartTypes; +import org.apache.poi.xddf.usermodel.chart.LegendPosition; +import org.apache.poi.xddf.usermodel.chart.XDDFBarChartData; +import org.apache.poi.xddf.usermodel.chart.XDDFCategoryAxis; +import org.apache.poi.xddf.usermodel.chart.XDDFChartData; +import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend; +import org.apache.poi.xddf.usermodel.chart.XDDFDataSource; +import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory; +import org.apache.poi.xddf.usermodel.chart.XDDFValueAxis; +import org.apache.poi.xssf.usermodel.XSSFChart; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ItemDimensions; + +/** + * Class that creates an excel stacked bar chart along with data table. + */ +public class BarChartExport implements ExcelItemExportable, ExcelSheetExport { + + /** + * Creates an excel table model to be written to an excel sheet and used as + * a datasource for the chart. + * + * @param categories The categories with their data. + * @param keyColumnHeader The header column name for the table descriptions + * (i.e. types: file types / artifact types). + * @param valueFormatString The excel format string to use for values. + * @return An excel table export to be used as the data source for the chart + * in the excel document. + */ + private static ExcelTableExport>, ? extends ExcelCellModel> getTableModel( + List categories, String keyColumnHeader, String chartTitle) { + + // get the row keys by finding the series with the largest set of bar items + // (they should all be equal, but just in case) + List rowKeys = categories.stream() + .filter(cat -> cat != null && cat.getItems() != null) + .map(cat -> cat.getItems()) + .max((items1, items2) -> Integer.compare(items1.size(), items2.size())) + .orElse(Collections.emptyList()) + .stream() + .map((barChartItem) -> barChartItem.getKey()) + .collect(Collectors.toList()); + + // map of (bar chart category index, bar chart item index) -> value + Map, Double> valueMap = IntStream.range(0, categories.size()) + .mapToObj(idx -> Pair.of(idx, categories.get(idx))) + .filter(pair -> pair.getValue() != null && pair.getValue().getItems() != null) + .flatMap(categoryPair -> { + return IntStream.range(0, categoryPair.getValue().getItems().size()) + .mapToObj(idx -> Pair.of(idx, categoryPair.getValue().getItems().get(idx))) + .map(itemPair -> Pair.of( + Pair.of(categoryPair.getKey(), itemPair.getKey()), + itemPair.getValue() == null ? null : itemPair.getValue().getValue())); + }) + .collect(Collectors.toMap(e -> e.getKey(), e -> e.getValue(), (v1, v2) -> v1)); + + // Create rows of data to be displayed where each row is a tuple of the bar chart item + // key and the list of values in category order. + List>> values = IntStream.range(0, rowKeys.size()) + .mapToObj(idx -> Pair.of(idx, rowKeys.get(idx))) + .map((rowPair) -> { + List items = IntStream.range(0, categories.size()) + .mapToObj(idx -> valueMap.get(Pair.of(idx, rowPair.getKey()))) + .collect(Collectors.toList()); + + return Pair.of(rowPair.getValue(), items); + }) + .collect(Collectors.toList()); + + // Create the model for the category column + ColumnModel>, DefaultCellModel> categoryColumn + = new ColumnModel<>(keyColumnHeader, (row) -> new DefaultCellModel<>(row.getKey())); + + // create the models for each category of data to be displayed + Stream>, DefaultCellModel>> dataColumns = IntStream.range(0, categories.size()) + .mapToObj(idx -> new ColumnModel<>( + categories.get(idx).getKey().toString(), + (row) -> new DefaultCellModel<>(row.getValue().get(idx)))); + + // create table + return new ExcelTableExport>, DefaultCellModel>( + chartTitle, + Stream.concat(Stream.of(categoryColumn), dataColumns) + .collect(Collectors.toList()), + values + ); + } + + private static final int DEFAULT_ROW_SIZE = 15; + private static final int DEFAULT_COL_SIZE = 10; + private static final int DEFAULT_ROW_PADDING = 1; + private static final int DEFAULT_COL_OFFSET = 1; + + private final ExcelTableExport>, ? extends ExcelCellModel> tableExport; + private final int colOffset; + private final int rowPadding; + private final int colSize; + private final int rowSize; + private final String chartTitle; + private final String sheetName; + private final List categories; + private final String keyColumnHeader; + + /** + * Main constructor that assumes some defaults (i.e. chart size follows + * defaults and sheet name is chart title). + * + * @param keyColumnHeader The header column name for the table descriptions + * (i.e. types: file types / artifact types). + * @param valueFormatString The excel format string to use for values. + * @param chartTitle The title for the chart. + * @param categories The categories along with data. + */ + public BarChartExport(String keyColumnHeader, + String valueFormatString, + String chartTitle, + List categories) { + this(keyColumnHeader, valueFormatString, chartTitle, chartTitle, categories, + DEFAULT_COL_OFFSET, DEFAULT_ROW_PADDING, DEFAULT_COL_SIZE, DEFAULT_ROW_SIZE); + } + + /** + * Main constructor. + * + * @param keyColumnHeader The header column name for the table descriptions + * (i.e. types: file types / artifact types). + * @param valueFormatString The excel format string to use for values. + * @param chartTitle The title for the chart. + * @param sheetName The sheet name if used as a sheet export. + * @param categories The categories along with data. + * @param colOffset The column spacing between the table and the chart. + * @param rowPadding The padding between this and data above or below (if + * used as an ExcelItemExportable). + * @param colSize The column size of the chart. + * @param rowSize The row size of the chart. + */ + public BarChartExport(String keyColumnHeader, String valueFormatString, + String chartTitle, String sheetName, + List categories, + int colOffset, int rowPadding, int colSize, int rowSize) { + + this.keyColumnHeader = keyColumnHeader; + this.tableExport = getTableModel(categories, keyColumnHeader, chartTitle); + this.colOffset = colOffset; + this.rowPadding = rowPadding; + this.colSize = colSize; + this.rowSize = rowSize; + this.chartTitle = chartTitle; + this.sheetName = sheetName; + this.categories = categories; + } + + @Override + public String getSheetName() { + return sheetName; + } + + @Override + public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv env) throws ExcelExport.ExcelExportException { + write(sheet, 0, 0, env); + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + if (!(sheet instanceof XSSFSheet)) { + throw new ExcelExportException("Sheet must be an XSSFSheet in order to write."); + } + + XSSFSheet xssfSheet = (XSSFSheet) sheet; + + // write pie chart table data + ItemDimensions tableDimensions = tableExport.write(xssfSheet, rowStart + rowPadding, colStart, env); + + XSSFDrawing drawing = xssfSheet.createDrawingPatriarch(); + + int chartColStart = colStart + categories.size() + 1 + colOffset; + + //createAnchor has arguments of (int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2); + XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, chartColStart, rowStart + rowPadding, chartColStart + colSize + 1, rowStart + rowSize + 1); + + XSSFChart chart = drawing.createChart(anchor); + chart.setTitleText(chartTitle); + chart.setTitleOverlay(false); + XDDFChartLegend legend = chart.getOrAddLegend(); + legend.setPosition(LegendPosition.BOTTOM); + + // Use a category axis for the bottom axis. + XDDFCategoryAxis bottomAxis = chart.createCategoryAxis(AxisPosition.BOTTOM); + bottomAxis.setTitle(keyColumnHeader); + XDDFValueAxis leftAxis = chart.createValueAxis(AxisPosition.LEFT); + leftAxis.setCrosses(AxisCrosses.AUTO_ZERO); + leftAxis.setVisible(false); + + XDDFBarChartData data = (XDDFBarChartData) chart.createData(ChartTypes.BAR, bottomAxis, leftAxis); + data.setBarGrouping(BarGrouping.STACKED); + + XDDFDataSource headerSource = XDDFDataSourcesFactory.fromStringCellRange(xssfSheet, + new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(), + tableDimensions.getColStart(), tableDimensions.getColStart())); + + data.setBarDirection(BarDirection.COL); + + // set data for each series and set color if applicable + for (int i = 0; i < categories.size(); i++) { + XDDFChartData.Series series = data.addSeries(headerSource, + XDDFDataSourcesFactory.fromNumericCellRange(xssfSheet, + new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(), + tableDimensions.getColStart() + 1 + i, tableDimensions.getColStart() + 1 + i))); + + series.setTitle(categories.size() > i && categories.get(i).getKey() != null ? categories.get(i).getKey().toString() : "", null); + if (categories.get(i).getColor() != null) { + Color color = categories.get(i).getColor(); + byte[] colorArrARGB = ByteBuffer.allocate(4).putInt(color.getRGB()).array(); + byte[] colorArrRGB = new byte[]{colorArrARGB[1], colorArrARGB[2], colorArrARGB[3]}; + XDDFSolidFillProperties fill = new XDDFSolidFillProperties(XDDFColor.from(colorArrRGB)); + XDDFShapeProperties properties = series.getShapeProperties(); + if (properties == null) { + properties = new XDDFShapeProperties(); + } + properties.setFillProperties(fill); + series.setShapeProperties(properties); + } + } + + chart.plot(data); + + return new ItemDimensions(rowStart, colStart, Math.max(tableDimensions.getRowEnd(), rowStart + rowSize) + rowPadding, chartColStart + colSize); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java index 09f5ade505..21f3bca572 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartPanel.java @@ -19,9 +19,7 @@ package org.sleuthkit.autopsy.datasourcesummary.uiutils; import java.awt.BorderLayout; -import java.awt.Color; import java.awt.Font; -import java.util.Collections; import java.util.List; import javax.swing.JLabel; import org.apache.commons.collections4.CollectionUtils; @@ -34,90 +32,12 @@ import org.jfree.chart.plot.PlotOrientation; import org.jfree.chart.renderer.category.BarRenderer; import org.jfree.chart.renderer.category.StandardBarPainter; import org.jfree.data.category.DefaultCategoryDataset; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.BarChartSeries.BarChartItem; /** * A bar chart panel. */ -public class BarChartPanel extends AbstractLoadableComponent> { - - /** - * Represents a series in a bar chart where all items pertain to one - * category. - */ - public static class BarChartSeries { - - private final Comparable key; - private final Color color; - private final List items; - - /** - * Main constructor. - * - * @param key The key. - * @param color The color for this series. - * @param items The bars to be displayed for this series. - */ - public BarChartSeries(Comparable key, Color color, List items) { - this.key = key; - this.color = color; - this.items = (items == null) ? Collections.emptyList() : Collections.unmodifiableList(items); - } - - /** - * @return The color for this series. - */ - public Color getColor() { - return color; - } - - /** - * @return The bars to be displayed for this series. - */ - public List getItems() { - return items; - } - - /** - * @return The key for this item. - */ - public Comparable getKey() { - return key; - } - } - - /** - * An individual bar to be displayed in the bar chart. - */ - public static class BarChartItem { - - private final Comparable key; - private final double value; - - /** - * Main constructor. - * - * @param key The key. - * @param value The value for this item. - */ - public BarChartItem(Comparable key, double value) { - this.key = key; - this.value = value; - } - - /** - * @return The key for this item. - */ - public Comparable getKey() { - return key; - } - - /** - * @return The value for this item. - */ - public double getValue() { - return value; - } - } +public class BarChartPanel extends AbstractLoadableComponent> { /** * JFreeChart bar charts don't preserve the order of bars provided to the @@ -285,12 +205,12 @@ public class BarChartPanel extends AbstractLoadableComponent data) { + protected void setResults(List data) { this.dataset.clear(); if (CollectionUtils.isNotEmpty(data)) { for (int s = 0; s < data.size(); s++) { - BarChartPanel.BarChartSeries series = data.get(s); + BarChartSeries series = data.get(s); if (series != null && CollectionUtils.isNotEmpty(series.getItems())) { if (series.getColor() != null) { this.plot.getRenderer().setSeriesPaint(s, series.getColor()); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartSeries.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartSeries.java new file mode 100644 index 0000000000..c1626f34b2 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/BarChartSeries.java @@ -0,0 +1,101 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.Color; +import java.util.Collections; +import java.util.List; + +/** + * Represents a series in a bar chart where all items pertain to one category. + */ +public class BarChartSeries { + + /** + * An individual bar to be displayed in the bar chart. + */ + public static class BarChartItem { + + private final Comparable key; + private final double value; + + /** + * Main constructor. + * + * @param key The key. + * @param value The value for this item. + */ + public BarChartItem(Comparable key, double value) { + this.key = key; + this.value = value; + } + + /** + * @return The key for this item. + */ + public Comparable getKey() { + return key; + } + + /** + * @return The value for this item. + */ + public double getValue() { + return value; + } + } + private final Comparable key; + private final Color color; + private final List items; + + /** + * Main constructor. + * + * @param key The key. + * @param color The color for this series. + * @param items The bars to be displayed for this series. + */ + public BarChartSeries(Comparable key, Color color, List items) { + this.key = key; + this.color = color; + this.items = (items == null) ? Collections.emptyList() : Collections.unmodifiableList(items); + } + + /** + * @return The color for this series. + */ + public Color getColor() { + return color; + } + + /** + * @return The bars to be displayed for this series. + */ + public List getItems() { + return items; + } + + /** + * @return The key for this item. + */ + public Comparable getKey() { + return key; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java index 5592344e2c..c79cb381aa 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/ExcelTableExport.java @@ -81,6 +81,8 @@ public class ExcelTableExport implements ExcelSheet sheet.autoSizeColumn(i); } + // freeze header row + sheet.createFreezePane(0, 1); } @Override @@ -119,10 +121,8 @@ public class ExcelTableExport implements ExcelSheet cell.setCellValue(columns.get(i).getHeaderTitle()); cell.setCellStyle(worksheetEnv.getHeaderStyle()); } - // freeze header row - sheet.createFreezePane(0, 1); - // Create Cell Style for each column (if one is needed) + // Create Cell Style for each column (if one is needed) for (int rowNum = 0; rowNum < safeData.size(); rowNum++) { T rowData = safeData.get(rowNum); Row row = sheet.createRow(rowNum + rowStart + 1); diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java index c224567d9e..be814d23a9 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/JTablePanel.java @@ -351,6 +351,20 @@ public class JTablePanel extends AbstractLoadableComponent> { this.keyFunction = keyFunction; return this; } + + /** + * Returns the selected items or null if no item is selected. + * @return The selected items or null if no item is selected. + */ + public List getSelectedItems() { + int selectedRow = this.table.getSelectedRow(); + int count = this.table.getSelectedRowCount(); + if (selectedRow < 0 || this.tableModel == null || selectedRow + count > this.tableModel.getDataRows().size()) { + return null; + } else { + return this.tableModel.getDataRows().subList(selectedRow, selectedRow + count); + } + } @Override protected synchronized void setResults(List data) { diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartExport.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartExport.java new file mode 100644 index 0000000000..4005a34ebc --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartExport.java @@ -0,0 +1,195 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.util.Arrays; +import java.util.List; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.util.CellRangeAddress; +import org.apache.poi.xddf.usermodel.chart.LegendPosition; +import org.apache.poi.xddf.usermodel.chart.XDDFChartLegend; +import org.apache.poi.xddf.usermodel.chart.XDDFDataSource; +import org.apache.poi.xddf.usermodel.chart.XDDFDataSourcesFactory; +import org.apache.poi.xddf.usermodel.chart.XDDFNumericalDataSource; +import org.apache.poi.xddf.usermodel.chart.XDDFPieChartData; +import org.apache.poi.xssf.usermodel.XSSFChart; +import org.apache.poi.xssf.usermodel.XSSFClientAnchor; +import org.apache.poi.xssf.usermodel.XSSFDrawing; +import org.apache.poi.xssf.usermodel.XSSFSheet; +import org.openxmlformats.schemas.drawingml.x2006.chart.CTPieChart; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelExportException; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelExport.ExcelSheetExport; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ExcelItemExportable; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ExcelSpecialFormatExport.ItemDimensions; + +/** + * + * Class that creates an excel pie chart along with data table. + */ +public class PieChartExport implements ExcelItemExportable, ExcelSheetExport { + + private static final int DEFAULT_ROW_SIZE = 20; + private static final int DEFAULT_COL_SIZE = 10; + private static final int DEFAULT_ROW_PADDING = 1; + private static final int DEFAULT_COL_OFFSET = 1; + + private final ExcelTableExport tableExport; + private final int colOffset; + private final int rowPadding; + private final int colSize; + private final int rowSize; + private final String chartTitle; + private final String sheetName; + + /** + * Main constructor assuming defaults. + * + * @param keyColumnHeader The header column name for the table descriptions + * (i.e. file types). + * @param valueColumnHeader The header column name for the values. + * @param valueFormatString The excel format string to use for values. + * @param chartTitle The title for the chart. + * @param slices The values for the pie slices. + */ + public PieChartExport(String keyColumnHeader, + String valueColumnHeader, String valueFormatString, + String chartTitle, + List slices) { + this(keyColumnHeader, valueColumnHeader, valueFormatString, chartTitle, chartTitle, slices, + DEFAULT_COL_OFFSET, DEFAULT_ROW_PADDING, DEFAULT_COL_SIZE, DEFAULT_ROW_SIZE); + } + + /** + * Main constructor. + * + * @param keyColumnHeader The header column name for the table descriptions + * (i.e. file types). + * @param valueColumnHeader The header column name for the values. + * @param valueFormatString The excel format string to use for values. + * @param chartTitle The title for the chart. + * @param sheetName The sheet name if used as a sheet export. + * @param slices The values for the pie slices. + * @param colOffset The column spacing between the table and the chart. + * @param rowPadding The padding between this and data above or below (if + * used as an ExcelItemExportable). + * @param colSize The column size of the chart. + * @param rowSize The row size of the chart. + */ + public PieChartExport(String keyColumnHeader, + String valueColumnHeader, String valueFormatString, + String chartTitle, String sheetName, + List slices, + int colOffset, int rowPadding, int colSize, int rowSize) { + + this.tableExport = new ExcelTableExport<>(chartTitle, + Arrays.asList( + new ColumnModel<>(keyColumnHeader, (slice) -> new DefaultCellModel<>(slice.getLabel())), + new ColumnModel<>(valueColumnHeader, (slice) -> new DefaultCellModel<>(slice.getValue(), null, valueFormatString)) + ), + slices); + this.colOffset = colOffset; + this.rowPadding = rowPadding; + this.colSize = colSize; + this.rowSize = rowSize; + this.chartTitle = chartTitle; + this.sheetName = sheetName; + } + + @Override + public String getSheetName() { + return sheetName; + } + + @Override + public void renderSheet(Sheet sheet, ExcelExport.WorksheetEnv env) throws ExcelExport.ExcelExportException { + write(sheet, 0, 0, env); + } + + @Override + public ItemDimensions write(Sheet sheet, int rowStart, int colStart, ExcelExport.WorksheetEnv env) throws ExcelExportException { + if (!(sheet instanceof XSSFSheet)) { + throw new ExcelExportException("Sheet must be an XSSFSheet in order to write."); + } + + XSSFSheet xssfSheet = (XSSFSheet) sheet; + + // write pie chart table data + ItemDimensions tableDimensions = tableExport.write(xssfSheet, rowStart + rowPadding, colStart, env); + + XSSFDrawing drawing = xssfSheet.createDrawingPatriarch(); + + int chartColStart = colStart + 2 + colOffset; + + //createAnchor has arguments of (int dx1, int dy1, int dx2, int dy2, int col1, int row1, int col2, int row2); + XSSFClientAnchor anchor = drawing.createAnchor(0, 0, 0, 0, chartColStart, rowStart + rowPadding, chartColStart + colSize + 1, rowStart + rowSize + 1); + + XSSFChart chart = drawing.createChart(anchor); + chart.setTitleText(chartTitle); + chart.setTitleOverlay(false); + XDDFChartLegend legend = chart.getOrAddLegend(); + legend.setPosition(LegendPosition.RIGHT); + + // CellRangeAddress has arguments of (int firstRow, int lastRow, int firstCol, int lastCol) + XDDFDataSource cat = XDDFDataSourcesFactory.fromStringCellRange(xssfSheet, + new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(), + tableDimensions.getColStart(), tableDimensions.getColStart())); + + XDDFNumericalDataSource val = XDDFDataSourcesFactory.fromNumericCellRange(xssfSheet, + new CellRangeAddress(tableDimensions.getRowStart() + 1, tableDimensions.getRowEnd(), + tableDimensions.getColStart() + 1, tableDimensions.getColStart() + 1)); + + // NOTE: There appears to be a classpath issue with POI (a version of 4.0.1 and 4.1.1 simultaneously) + // that is causing conflicts for XDDFPieChartData creation (i.e. the compiler thinks its using 4.1.1 + // and the runtime thinks its using 4.0.1) Reflection is used below to use the 4.0.1 method while + // sidestepping compiler issues. + // XDDFPieChartData creation that can be used in poi >= 4.1.1: + // XDDFPieChartData data = (XDDFPieChartData) chart.createData(ChartTypes.PIE, bottomAxis, leftAxis); + // XDDFPieChartData creation that can be used in 4.0.1: + // XDDFPieChartData data = new XDDFPieChartData(chart.getCTChart().getPlotArea().addNewPieChart()); + XDDFPieChartData data; + try { + Constructor constructor = XDDFPieChartData.class.getConstructor(CTPieChart.class); + constructor.setAccessible(true); + data = constructor.newInstance(chart.getCTChart().getPlotArea().addNewPieChart()); + } catch (NoSuchMethodException | InvocationTargetException | InstantiationException | IllegalAccessException | IllegalArgumentException ex) { + throw new ExcelExportException("Error while instantiating chart data.", ex); + } + + data.setVaryColors(true); + data.addSeries(cat, val); + + // Add data labels + if (!chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).isSetDLbls()) { + chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).addNewDLbls(); + } + + chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowVal().setVal(true); + chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowSerName().setVal(false); + chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowCatName().setVal(true); + chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowPercent().setVal(true); + chart.getCTChart().getPlotArea().getPieChartArray(0).getSerArray(0).getDLbls().addNewShowLegendKey().setVal(false); + + chart.plot(data); + + return new ItemDimensions(rowStart, colStart, Math.max(tableDimensions.getRowEnd(), rowStart + rowSize) + rowPadding, chartColStart + colSize); + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartItem.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartItem.java new file mode 100644 index 0000000000..b00a97e996 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartItem.java @@ -0,0 +1,67 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.datasourcesummary.uiutils; + +import java.awt.Color; + +/** + * An individual pie chart slice in the pie chart. + */ +public class PieChartItem { + + private final String label; + private final double value; + private final Color color; + + /** + * Main constructor. + * + * @param label The label for this pie slice. + * @param value The value for this item. + * @param color The color for the pie slice. Can be null for + * auto-determined. + */ + public PieChartItem(String label, double value, Color color) { + this.label = label; + this.value = value; + this.color = color; + } + + /** + * @return The label for this item. + */ + public String getLabel() { + return label; + } + + /** + * @return The value for this item. + */ + public double getValue() { + return value; + } + + /** + * @return The color for the pie slice or null for auto-determined. + */ + public Color getColor() { + return color; + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java index fa0d00dab6..1e0ddde838 100644 --- a/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java +++ b/Core/src/org/sleuthkit/autopsy/datasourcesummary/uiutils/PieChartPanel.java @@ -1,7 +1,7 @@ /* * Autopsy Forensic Browser * - * Copyright 2019 Basis Technology Corp. + * Copyright 2020 Basis Technology Corp. * Contact: carrier sleuthkit org * * Licensed under the Apache License, Version 2.0 (the "License"); @@ -39,52 +39,7 @@ import org.openide.util.NbBundle.Messages; @Messages({ "PieChartPanel_noDataLabel=No Data" }) -public class PieChartPanel extends AbstractLoadableComponent> { - - /** - * An individual pie chart slice in the pie chart. - */ - public static class PieChartItem { - - private final String label; - private final double value; - private final Color color; - - /** - * Main constructor. - * - * @param label The label for this pie slice. - * @param value The value for this item. - * @param color The color for the pie slice. Can be null for - * auto-determined. - */ - public PieChartItem(String label, double value, Color color) { - this.label = label; - this.value = value; - this.color = color; - } - - /** - * @return The label for this item. - */ - public String getLabel() { - return label; - } - - /** - * @return The value for this item. - */ - public double getValue() { - return value; - } - - /** - * @return The color for the pie slice or null for auto-determined. - */ - public Color getColor() { - return color; - } - } +public class PieChartPanel extends AbstractLoadableComponent> { private static final long serialVersionUID = 1L; @@ -176,12 +131,12 @@ public class PieChartPanel extends AbstractLoadableComponent data) { + protected void setResults(List data) { this.dataset.clear(); this.plot.clearSectionPaints(false); if (data != null && !data.isEmpty()) { - for (PieChartPanel.PieChartItem slice : data) { + for (PieChartItem slice : data) { this.dataset.setValue(slice.getLabel(), slice.getValue()); if (slice.getColor() != null) { this.plot.setSectionPaint(slice.getLabel(), slice.getColor()); @@ -202,7 +157,7 @@ public class PieChartPanel extends AbstractLoadableComponent data, String message) { + public synchronized void showDataWithMessage(List data, String message) { setResults(data); setMessage(true, message); repaint(); diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java index c86f90ad4b..3b88aad79d 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotDialog.java @@ -85,9 +85,8 @@ public final class IngestProgressSnapshotDialog extends JDialog { this.getRootPane().registerKeyboardAction(e -> { this.dispose(); }, KeyStroke.getKeyStroke(KeyEvent.VK_ESCAPE, 0), JComponent.WHEN_IN_FOCUSED_WINDOW); - add(new IngestProgressSnapshotPanel(this, provider)); + add(new IngestProgressSnapshotPanel(this, provider), BorderLayout.CENTER); pack(); - setResizable(false); if (shouldBeModal) { // if called from a modal dialog, become modal, otherwise don't. setModal(true); } diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.form b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.form index 5c0c3dbc16..5795cd77f6 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.form +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.form @@ -1,6 +1,14 @@
+ + + + + + + + @@ -11,52 +19,20 @@ + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + @@ -79,6 +55,11 @@ + + + + + @@ -106,6 +87,11 @@ + + + + + @@ -116,11 +102,21 @@ + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.java b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.java index cf1fefb2d4..4270de8d66 100644 --- a/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.java +++ b/Core/src/org/sleuthkit/autopsy/ingest/IngestProgressSnapshotPanel.java @@ -361,6 +361,7 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel { @SuppressWarnings("unchecked") // //GEN-BEGIN:initComponents private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; snapshotsScrollPane = new javax.swing.JScrollPane(); threadActivitySnapshotsTable = new javax.swing.JTable(); @@ -371,6 +372,10 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel { moduleScrollPane = new javax.swing.JScrollPane(); moduleTable = new javax.swing.JTable(); + setMinimumSize(new java.awt.Dimension(500, 500)); + setPreferredSize(new java.awt.Dimension(600, 500)); + setLayout(new java.awt.GridBagLayout()); + threadActivitySnapshotsTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { @@ -381,6 +386,17 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel { )); snapshotsScrollPane.setViewportView(threadActivitySnapshotsTable); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 0; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(11, 10, 0, 10); + add(snapshotsScrollPane, gridBagConstraints); + jobTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { @@ -391,12 +407,29 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel { )); jobScrollPane.setViewportView(jobTable); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(6, 10, 0, 10); + add(jobScrollPane, gridBagConstraints); + org.openide.awt.Mnemonics.setLocalizedText(refreshButton, org.openide.util.NbBundle.getMessage(IngestProgressSnapshotPanel.class, "IngestProgressSnapshotPanel.refreshButton.text")); // NOI18N refreshButton.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { refreshButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.weightx = 1.0; + add(refreshButton, gridBagConstraints); org.openide.awt.Mnemonics.setLocalizedText(closeButton, org.openide.util.NbBundle.getMessage(IngestProgressSnapshotPanel.class, "IngestProgressSnapshotPanel.closeButton.text")); // NOI18N closeButton.addActionListener(new java.awt.event.ActionListener() { @@ -404,6 +437,12 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel { closeButtonActionPerformed(evt); } }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.EAST; + gridBagConstraints.insets = new java.awt.Insets(6, 6, 11, 10); + add(closeButton, gridBagConstraints); moduleTable.setModel(new javax.swing.table.DefaultTableModel( new Object [][] { @@ -415,44 +454,16 @@ class IngestProgressSnapshotPanel extends javax.swing.JPanel { )); moduleScrollPane.setViewportView(moduleTable); - javax.swing.GroupLayout layout = new javax.swing.GroupLayout(this); - this.setLayout(layout); - layout.setHorizontalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() - .addContainerGap() - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.TRAILING) - .addComponent(snapshotsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 881, Short.MAX_VALUE) - .addGroup(layout.createSequentialGroup() - .addGap(0, 0, Short.MAX_VALUE) - .addComponent(refreshButton) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(closeButton)) - .addComponent(jobScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 881, Short.MAX_VALUE) - .addComponent(moduleScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 881, Short.MAX_VALUE)) - .addContainerGap()) - ); - - layout.linkSize(javax.swing.SwingConstants.HORIZONTAL, new java.awt.Component[] {closeButton, refreshButton}); - - layout.setVerticalGroup( - layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) - .addGroup(layout.createSequentialGroup() - .addContainerGap() - .addComponent(snapshotsScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 102, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(jobScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 102, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) - .addComponent(moduleScrollPane, javax.swing.GroupLayout.DEFAULT_SIZE, 100, Short.MAX_VALUE) - .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) - .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) - .addComponent(refreshButton) - .addComponent(closeButton)) - .addContainerGap()) - ); - - layout.linkSize(javax.swing.SwingConstants.VERTICAL, new java.awt.Component[] {closeButton, refreshButton}); - + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 2; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.NORTHWEST; + gridBagConstraints.weightx = 1.0; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(6, 10, 0, 10); + add(moduleScrollPane, gridBagConstraints); }// //GEN-END:initComponents private void closeButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_closeButtonActionPerformed diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/AddEditCategoryDialog.form b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/AddEditCategoryDialog.form new file mode 100644 index 0000000000..a863b48212 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/AddEditCategoryDialog.form @@ -0,0 +1,138 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/AddEditCategoryDialog.java b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/AddEditCategoryDialog.java new file mode 100644 index 0000000000..26be388c8e --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/AddEditCategoryDialog.java @@ -0,0 +1,282 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.url.analytics.domaincategorization; + +import java.util.Set; +import javax.swing.event.DocumentEvent; +import javax.swing.event.DocumentListener; +import org.openide.util.NbBundle.Messages; +import org.sleuthkit.autopsy.url.analytics.DomainCategory; + +/** + * Dialog for adding or editing a custom domain suffix category. + */ +@Messages({ + "AddEditCategoryDialog_Edit=Edit Entry", + "AddEditCategoryDialog_Add=Add Entry" +}) +class AddEditCategoryDialog extends javax.swing.JDialog { + + private boolean changed = false; + private final Set currentSuffixes; + private final DomainCategory currentDomainCategory; + + // listens for document updates + private final DocumentListener updateListener = new DocumentListener() { + @Override + public void insertUpdate(DocumentEvent e) { + onValueUpdate(domainSuffixTextField.getText(), categoryTextField.getText()); + } + + @Override + public void removeUpdate(DocumentEvent e) { + onValueUpdate(domainSuffixTextField.getText(), categoryTextField.getText()); + } + + @Override + public void changedUpdate(DocumentEvent e) { + onValueUpdate(domainSuffixTextField.getText(), categoryTextField.getText()); + } + }; + + /** + * Main constructor if adding a new domain suffix. + * + * @param parent The parent frame for this dialog. + * @param currentSuffixes The current domain suffixes. + */ + AddEditCategoryDialog(java.awt.Frame parent, Set currentSuffixes) { + this(parent, currentSuffixes, null); + } + + /** + * Main constructor if editing a domain suffix. + * + * @param parentThe parent frame for this dialog. + * @param currentSuffixes The current domain suffixes. + * @param currentDomainCategory The domain category being edited. If null, + * it will be assumed that a new domain suffix is being added. + */ + AddEditCategoryDialog(java.awt.Frame parent, Set currentSuffixes, DomainCategory currentDomainCategory) { + super(parent, true); + initComponents(); + this.currentSuffixes = currentSuffixes; + this.currentDomainCategory = currentDomainCategory; + + // set title based on whether or not we are editing or adding + // also don't allow editing of domain suffix if editing + if (currentDomainCategory == null) { + setTitle(Bundle.AddEditCategoryDialog_Add()); + domainSuffixTextField.setEditable(true); + domainSuffixTextField.setEnabled(true); + onValueUpdate(null, null); + } else { + setTitle(Bundle.AddEditCategoryDialog_Edit()); + domainSuffixTextField.setEditable(false); + domainSuffixTextField.setEnabled(false); + onValueUpdate(currentDomainCategory.getHostSuffix(), currentDomainCategory.getCategory()); + } + + validationLabel.setText(""); + + categoryTextField.getDocument().addDocumentListener(updateListener); + domainSuffixTextField.getDocument().addDocumentListener(updateListener); + } + + /** + * Returns the string value for the name in the input field if Ok pressed or + * null if not. + * + * @return The string value for the name in the input field if Ok pressed or + * null if not. + */ + DomainCategory getValue() { + return new DomainCategory(domainSuffixTextField.getText(), categoryTextField.getText()); + } + + /** + * Returns whether or not the value has been changed and saved by the user. + * + * @return Whether or not the value has been changed and saved by the user. + */ + boolean isChanged() { + return changed; + } + + /** + * When the text field is updated, this method is called. + * + * @param suffixStr The current domain suffix string in the input. + * @param categoryStr The current category string in the input. + */ + @Messages({ + "# {0} - maxSuffixLen", + "AddEditCategoryDialog_onValueUpdate_badSuffix=Please provide a domain suffix that is no more than {0} characters that includes at least one period.", + "# {0} - maxCategoryLen", + "AddEditCategoryDialog_onValueUpdate_badCategory=Please provide a category that is no more than {0} characters.", + "AddEditCategoryDialog_onValueUpdate_suffixRepeat=Please provide a unique domain suffix.", + "AddEditCategoryDialog_onValueUpdate_sameCategory=Please provide a new category for this domain suffix.",}) + void onValueUpdate(String suffixStr, String categoryStr) { + + String safeSuffixStr = suffixStr == null ? "" : suffixStr; + String normalizedSuffix = WebCategoriesDataModel.getNormalizedSuffix(safeSuffixStr); + String safeCategoryStr = categoryStr == null ? "" : categoryStr; + String normalizedCategory = WebCategoriesDataModel.getNormalizedCategory(safeCategoryStr); + + // update input text field if it is not the same. + if (!safeCategoryStr.equals(categoryTextField.getText())) { + categoryTextField.setText(safeCategoryStr); + } + + if (!safeSuffixStr.equals(domainSuffixTextField.getText())) { + domainSuffixTextField.setText(safeSuffixStr); + } + + String validationMessage = null; + if (normalizedSuffix.length() == 0 + || normalizedSuffix.length() > WebCategoriesDataModel.getMaxDomainSuffixLength() + || normalizedSuffix.indexOf('.') < 0) { + + validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_badSuffix(WebCategoriesDataModel.getMaxCategoryLength()); + + } else if (normalizedCategory.length() == 0 || normalizedCategory.length() > WebCategoriesDataModel.getMaxCategoryLength()) { + validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_badCategory(WebCategoriesDataModel.getMaxCategoryLength()); + + } else if (currentSuffixes.contains(normalizedSuffix) + && (currentDomainCategory == null + || !normalizedSuffix.equals(currentDomainCategory.getHostSuffix()))) { + + validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_suffixRepeat(); + + } else if (currentDomainCategory != null + && currentDomainCategory.getCategory() != null + && normalizedCategory.equals(WebCategoriesDataModel.getNormalizedCategory(currentDomainCategory.getCategory()))) { + + validationMessage = Bundle.AddEditCategoryDialog_onValueUpdate_sameCategory(); + } + + saveButton.setEnabled(validationMessage == null); + validationLabel.setText(validationMessage == null ? "" : String.format("%s", validationMessage)); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + + categoryTextField = new javax.swing.JTextField(); + domainSuffixTextField = new javax.swing.JTextField(); + javax.swing.JLabel categoryLabel = new javax.swing.JLabel(); + javax.swing.JLabel domainSuffixLabel = new javax.swing.JLabel(); + validationLabel = new javax.swing.JLabel(); + javax.swing.JButton cancelButton = new javax.swing.JButton(); + saveButton = new javax.swing.JButton(); + + setDefaultCloseOperation(javax.swing.WindowConstants.DISPOSE_ON_CLOSE); + + categoryLabel.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.categoryLabel.text")); // NOI18N + + domainSuffixLabel.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.domainSuffixLabel.text")); // NOI18N + + validationLabel.setForeground(java.awt.Color.RED); + validationLabel.setText(" "); + validationLabel.setToolTipText(""); + + cancelButton.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.cancelButton.text")); // NOI18N + cancelButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + cancelButtonActionPerformed(evt); + } + }); + + saveButton.setText(org.openide.util.NbBundle.getMessage(AddEditCategoryDialog.class, "AddEditCategoryDialog.saveButton.text")); // NOI18N + saveButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + saveButtonActionPerformed(evt); + } + }); + + javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane()); + getContentPane().setLayout(layout); + layout.setHorizontalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(javax.swing.GroupLayout.Alignment.TRAILING, layout.createSequentialGroup() + .addGap(0, 0, Short.MAX_VALUE) + .addComponent(saveButton) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(cancelButton)) + .addComponent(validationLabel, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, Short.MAX_VALUE) + .addGroup(layout.createSequentialGroup() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(domainSuffixLabel) + .addComponent(categoryLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.UNRELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(categoryTextField, javax.swing.GroupLayout.DEFAULT_SIZE, 276, Short.MAX_VALUE) + .addComponent(domainSuffixTextField)))) + .addContainerGap()) + ); + layout.setVerticalGroup( + layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addGroup(layout.createSequentialGroup() + .addContainerGap() + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(domainSuffixTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(domainSuffixLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.BASELINE) + .addComponent(categoryTextField, javax.swing.GroupLayout.PREFERRED_SIZE, javax.swing.GroupLayout.DEFAULT_SIZE, javax.swing.GroupLayout.PREFERRED_SIZE) + .addComponent(categoryLabel)) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addComponent(validationLabel, javax.swing.GroupLayout.PREFERRED_SIZE, 46, javax.swing.GroupLayout.PREFERRED_SIZE) + .addPreferredGap(javax.swing.LayoutStyle.ComponentPlacement.RELATED) + .addGroup(layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING) + .addComponent(cancelButton) + .addComponent(saveButton)) + .addContainerGap(8, Short.MAX_VALUE)) + ); + + pack(); + }// //GEN-END:initComponents + + private void saveButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_saveButtonActionPerformed + this.changed = true; + dispose(); + }//GEN-LAST:event_saveButtonActionPerformed + + private void cancelButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_cancelButtonActionPerformed + this.changed = false; + dispose(); + }//GEN-LAST:event_cancelButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JTextField categoryTextField; + private javax.swing.JTextField domainSuffixTextField; + private javax.swing.JButton saveButton; + private javax.swing.JLabel validationLabel; + // End of variables declaration//GEN-END:variables +} diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties new file mode 100644 index 0000000000..fae4fa97ec --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties @@ -0,0 +1,14 @@ +WebCategoryOptionsController_title=Custom Web Categories +WebCategoryOptionsController_keywords=Custom Web Categories +AddEditCategoryDialog.categoryLabel.text=Category: +AddEditCategoryDialog.domainSuffixLabel.text=Domain Suffix: +AddEditCategoryDialog.saveButton.text=Save +AddEditCategoryDialog.cancelButton.text=Cancel +WebCategoriesOptionsPanel.panelDescription.text=This module allows you to classify web sites based on domain names. +WebCategoriesOptionsPanel.categoriesTitle.text=Categories: +WebCategoriesOptionsPanel.newEntryButton.text=New Entry +WebCategoriesOptionsPanel.editEntryButton.text=Edit Entry +WebCategoriesOptionsPanel.deleteEntryButton.text=Delete Entry +WebCategoriesOptionsPanel.importSetButton.text=Import Set +WebCategoriesOptionsPanel.exportSetButton.text=Export Set +WebCategoriesOptionsPanel.ingestRunningWarning.text=Ingest is currently running. No editing can take place at this time. diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties-MERGED b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties-MERGED new file mode 100644 index 0000000000..68993c6213 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/Bundle.properties-MERGED @@ -0,0 +1,37 @@ +AddEditCategoryDialog_Add=Add Entry +AddEditCategoryDialog_Edit=Edit Entry +# {0} - maxCategoryLen +AddEditCategoryDialog_onValueUpdate_badCategory=Please provide a category that is no more than {0} characters. +# {0} - maxSuffixLen +AddEditCategoryDialog_onValueUpdate_badSuffix=Please provide a domain suffix that is no more than {0} characters that includes at least one period. +AddEditCategoryDialog_onValueUpdate_sameCategory=Please provide a new category for this domain suffix. +AddEditCategoryDialog_onValueUpdate_suffixRepeat=Please provide a unique domain suffix. +WebCategoriesOptionsPanel_categoryTable_categoryColumnName=Category +WebCategoriesOptionsPanel_categoryTable_suffixColumnName=Domain Suffix +WebCategoriesOptionsPanel_exportSetButtonActionPerformed_defaultFileName=Custom Categories Export +WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateMessage=A file already exists at the selected path. The categories will not be exported. +WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateTitle=File Already Exists +WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorMessage=There was an error exporting. +WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorTitle=Export Error +WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage=There was an error importing this json file. +WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle=Import Error +WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictCancel=Cancel +# {0} - domainSuffix +WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictMessage=Domain suffix {0} already exists. What would you like to do? +WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictOverwrite=Overwrite +WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictSkip=Skip +WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictTitle=Domain Suffix Already Exists +WebCategoryOptionsController_title=Custom Web Categories +WebCategoryOptionsController_keywords=Custom Web Categories +AddEditCategoryDialog.categoryLabel.text=Category: +AddEditCategoryDialog.domainSuffixLabel.text=Domain Suffix: +AddEditCategoryDialog.saveButton.text=Save +AddEditCategoryDialog.cancelButton.text=Cancel +WebCategoriesOptionsPanel.panelDescription.text=This module allows you to classify web sites based on domain names. +WebCategoriesOptionsPanel.categoriesTitle.text=Categories: +WebCategoriesOptionsPanel.newEntryButton.text=New Entry +WebCategoriesOptionsPanel.editEntryButton.text=Edit Entry +WebCategoriesOptionsPanel.deleteEntryButton.text=Delete Entry +WebCategoriesOptionsPanel.importSetButton.text=Import Set +WebCategoriesOptionsPanel.exportSetButton.text=Export Set +WebCategoriesOptionsPanel.ingestRunningWarning.text=Ingest is currently running. No editing can take place at this time. diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/CustomWebCategorizer.java b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/CustomWebCategorizer.java new file mode 100644 index 0000000000..26acd97f3a --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/CustomWebCategorizer.java @@ -0,0 +1,93 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.url.analytics.domaincategorization; + +import java.sql.SQLException; +import java.util.logging.Level; +import org.apache.commons.lang3.StringUtils; +import org.openide.util.lookup.ServiceProvider; +import org.sleuthkit.autopsy.coreutils.Logger; +import org.sleuthkit.autopsy.url.analytics.DomainCategorizer; +import org.sleuthkit.autopsy.url.analytics.DomainCategorizerException; +import org.sleuthkit.autopsy.url.analytics.DomainCategory; + +/** + * A DomainCategoryProvider for custom web categories. NOTE: If this class + * package or name change, code in DomainCategoryRunner will also need to change + * to reflect the changing class name for ordering purposes. + */ +@ServiceProvider(service = DomainCategorizer.class) +public class CustomWebCategorizer implements DomainCategorizer { + + private static final Logger logger = Logger.getLogger(CustomWebCategorizer.class.getName()); + + private final WebCategoriesDataModel dataModel; + + /** + * Constructor accepting a custom WebCategoriesDataModel. + * + * @param dataModel The WebCategoriesDataModel to use as a data model. + */ + CustomWebCategorizer(WebCategoriesDataModel dataModel) { + this.dataModel = dataModel; + } + + /** + * No parameter constructor that uses the singleton instance of the + * WebCategoriesDataModel. + * + */ + public CustomWebCategorizer() { + this(WebCategoriesDataModel.getInstance()); + } + + @Override + public DomainCategory getCategory(String domain, String host) throws DomainCategorizerException { + if (!dataModel.isInitialized()) { + return null; + } + String hostToUse = (StringUtils.isBlank(host)) ? domain : host; + if (StringUtils.isBlank(hostToUse)) { + return null; + } + + hostToUse = hostToUse.toLowerCase(); + + try { + return dataModel.getMatchingRecord(hostToUse); + } catch (SQLException ex) { + logger.log(Level.WARNING, "There was an error while retrieving data for: " + hostToUse, ex); + return null; + } + } + + @Override + public void initialize() throws DomainCategorizerException { + try { + dataModel.initialize(); + } catch (SQLException ex) { + throw new DomainCategorizerException("Unable to initialize.", ex); + } + } + + @Override + public void close() throws SQLException { + dataModel.close(); + } +} diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesDataModel.java b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesDataModel.java new file mode 100644 index 0000000000..067a5fceea --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesDataModel.java @@ -0,0 +1,535 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.url.analytics.domaincategorization; + +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonGetter; +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.io.File; +import java.io.IOException; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.DriverManager; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Statement; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import java.util.stream.IntStream; +import java.util.stream.Stream; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; +import org.openide.modules.InstalledFileLocator; +import org.sleuthkit.autopsy.coreutils.NetworkUtils; +import org.sleuthkit.autopsy.url.analytics.DomainCategory; + +/** + * Provides the data model for exporting, importing and CRUD operations on + * custom web categories. + */ +class WebCategoriesDataModel implements AutoCloseable { + + /** + * DTO to be used with jackson when converting to and from exported content. + */ + @JsonIgnoreProperties(ignoreUnknown = true) + static class CustomCategorizationJsonDto { + + private final String category; + private final List domains; + + /** + * Main constructor. + * + * @param category The category. + * @param domains The list of host suffixes in this category. + */ + @JsonCreator + CustomCategorizationJsonDto( + @JsonProperty("category") String category, + @JsonProperty("domains") List domains) { + this.category = category; + this.domains = domains == null + ? Collections.emptyList() + : new ArrayList<>(domains); + } + + /** + * Returns the category. + * + * @return The category. + */ + @JsonGetter("category") + String getCategory() { + return category; + } + + /** + * Returns the list of domain suffixes in this category. + * + * @return The list of domain suffixes in this category. + */ + @JsonGetter("domains") + List getDomains() { + return domains; + } + } + + private static final int MAX_CAT_SIZE = 300; + private static final int MAX_DOMAIN_SIZE = 255; + + private static final String ROOT_FOLDER = "DomainCategorization"; + private static final String FILE_REL_PATH = "custom_list.db"; + private static final String JDBC_SQLITE_PREFIX = "jdbc:sqlite:"; + private static final String TABLE_NAME = "domain_suffix"; + private static final String SUFFIX_COLUMN = "suffix"; + private static final String CATEGORY_COLUMN = "category"; + + private static final Logger logger = Logger.getLogger(WebCategoriesDataModel.class.getName()); + private static WebCategoriesDataModel instance; + + /** + * Returns the maximum string length of a domain suffix. + * + * @return The maximum string length of a domain suffix. + */ + static int getMaxDomainSuffixLength() { + return MAX_DOMAIN_SIZE; + } + + /** + * Returns the maximum string length of a category. + * + * @return The maximum string length of a category. + */ + static int getMaxCategoryLength() { + return MAX_DOMAIN_SIZE; + } + + /** + * Retrieves the default path for where custom domain categorization exists. + * + * @return The path or null if the path cannot be reconciled. + */ + private static File getDefaultPath() { + File dir = InstalledFileLocator.getDefault().locate(ROOT_FOLDER, WebCategoriesDataModel.class.getPackage().getName(), false); + if (dir == null || !dir.exists()) { + logger.log(Level.WARNING, String.format("Unable to find file %s with InstalledFileLocator", ROOT_FOLDER)); + return null; + } + + return Paths.get(dir.getAbsolutePath(), FILE_REL_PATH).toFile(); + } + + /** + * Generates the normalized category string to be inserted into the + * database. + * + * @param category The category. + * @return The normalized string. + */ + static String getNormalizedCategory(String category) { + if (category == null) { + return ""; + } + + String trimmedCategory = category.trim(); + + return trimmedCategory.substring(0, Math.min(trimmedCategory.length(), MAX_CAT_SIZE)); + } + + /** + * Generates the normalized domain suffix string to be inserted into the + * database. + * + * @param domainSuffix The domain suffix. + * @return The normalized string. + */ + static String getNormalizedSuffix(String domainSuffix) { + if (domainSuffix == null) { + return ""; + } + + String sanitized = Stream.of(domainSuffix.split("\\.")) + .map(s -> { + return s + // alphanumeric and hyphen + .replaceAll("[^0-9a-zA-Z\\-]", "") + // no leading or trailing hyphen + .replaceAll("^\\-*(.+?)?\\-*$", "$1"); + }) + .filter(StringUtils::isNotEmpty) + .collect(Collectors.joining(".")); + + return sanitized.substring(0, Math.min(sanitized.length(), MAX_DOMAIN_SIZE)).toLowerCase(); + } + + /** + * Retrieves a singleton instance of this class. + * + * @return The singleton instance of this class. + */ + static WebCategoriesDataModel getInstance() { + if (instance == null) { + instance = new WebCategoriesDataModel(); + } + + return instance; + } + + private final File sqlitePath; + private Connection dbConn = null; + + /** + * Constructor used to create singleton instance. + */ + private WebCategoriesDataModel() { + this(getDefaultPath()); + } + + /** + * Constructor that accepts a variable path for the custom sqlite database + * for custom domain categories. + * + * @param sqlitePath The path. + */ + WebCategoriesDataModel(File sqlitePath) { + this.sqlitePath = sqlitePath; + } + + /** + * Creates a sqlite jdbc connection. + * + * @throws SQLException + */ + synchronized void initialize() throws SQLException { + String url = JDBC_SQLITE_PREFIX + sqlitePath.getAbsolutePath(); + if (this.dbConn != null) { + this.dbConn.close(); + this.dbConn = null; + } + + this.dbConn = DriverManager.getConnection(url); + + // speed up operations by turning off WAL + try (Statement turnOffWal = dbConn.createStatement()) { + turnOffWal.execute("PRAGMA journal_mode=OFF"); + } + + // create table if it doesn't exist + try (Statement createDomainsTable = dbConn.createStatement()) { + createDomainsTable.execute( + " CREATE TABLE IF NOT EXISTS " + TABLE_NAME + " (\n" + + " " + SUFFIX_COLUMN + " VARCHAR(" + MAX_DOMAIN_SIZE + ") PRIMARY KEY,\n" + + " " + CATEGORY_COLUMN + " VARCHAR(" + MAX_CAT_SIZE + ")\n" + + " ) WITHOUT ROWID"); + } + } + + /** + * Returns true if initialized. + * + * @return True if initialized. + */ + synchronized boolean isInitialized() { + return this.dbConn != null; + } + + /** + * Retrieves all domain categories present in json file. + * + * @param jsonInput The json file. + * @return The domain categories. + * @throws IOException + */ + List getJsonEntries(File jsonInput) throws IOException { + if (jsonInput == null) { + logger.log(Level.WARNING, "No valid file provided."); + return Collections.emptyList(); + } + + ObjectMapper mapper = new ObjectMapper(); + List customCategorizations = mapper.readValue(jsonInput, new TypeReference>() { + }); + + Stream categoryStream = (customCategorizations != null) ? customCategorizations.stream() : Stream.empty(); + + return categoryStream + .filter(c -> c != null && c.getCategory() != null && c.getDomains() != null) + .flatMap(c -> c.getDomains().stream() + .map(WebCategoriesDataModel::getNormalizedSuffix) + .filter(StringUtils::isNotBlank) + .map(d -> new DomainCategory(d, getNormalizedCategory(c.getCategory())))) + .collect(Collectors.toList()); + + } + + /** + * Exports current database to a json file. + * + * @param jsonOutput The output file. + * @throws SQLException + * @throws IOException + */ + synchronized void exportToJson(File jsonOutput) throws SQLException, IOException { + if (jsonOutput == null) { + logger.log(Level.WARNING, "Null file provided."); + return; + } + + if (!isInitialized()) { + initialize(); + } + + // retrieve items from the database + List> categoryDomains = new ArrayList<>(); + try (Statement domainSelect = dbConn.createStatement(); + ResultSet resultSet = domainSelect.executeQuery( + "SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + " FROM " + TABLE_NAME + " ORDER BY " + SUFFIX_COLUMN)) { + + while (resultSet.next()) { + categoryDomains.add(Pair.of(resultSet.getString(CATEGORY_COLUMN), resultSet.getString(SUFFIX_COLUMN))); + } + } + + // aggregate data appropriately into CustomCategorizationJsonDto + List categories + = categoryDomains.stream() + .collect(Collectors.toMap( + p -> p.getKey(), + p -> new ArrayList<>(Arrays.asList(p.getValue())), + (p1, p2) -> { + p1.addAll(p2); + return p1; + } + )) + .entrySet().stream() + .map(entry -> new CustomCategorizationJsonDto(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + + // write to disk + ObjectMapper mapper = new ObjectMapper(); + mapper.writerWithDefaultPrettyPrinter().writeValue(jsonOutput, categories); + } + + /** + * Delete a record from the database. + * + * @param domainSuffix The domain suffix of the item to delete. + * @return Whether or not the operation actually deleted something. + * @throws SQLException + * @throws IllegalArgumentException + */ + synchronized boolean deleteRecord(String domainSuffix) throws SQLException, IllegalArgumentException { + if (StringUtils.isBlank(domainSuffix)) { + throw new IllegalArgumentException("Expected non-empty domain suffix"); + } + + if (!isInitialized()) { + initialize(); + } + + try (PreparedStatement suffixDelete = dbConn.prepareStatement( + "DELETE FROM " + TABLE_NAME + " WHERE LOWER(" + SUFFIX_COLUMN + ") = LOWER(?)", Statement.RETURN_GENERATED_KEYS);) { + + suffixDelete.setString(1, getNormalizedSuffix(domainSuffix)); + return suffixDelete.executeUpdate() > 0; + } + } + + /** + * Inserts or updates the entry for the given domain suffix. + * + * @param entry The domain suffix and category. + * @return True if successfully inserted/updated. + * @throws SQLException + * @throws IllegalStateException + * @throws IllegalArgumentException + */ + synchronized boolean insertUpdateSuffix(DomainCategory entry) throws SQLException, IllegalStateException, IllegalArgumentException { + if (entry == null || StringUtils.isBlank(getNormalizedCategory(entry.getCategory())) || StringUtils.isBlank(getNormalizedSuffix(entry.getHostSuffix()))) { + throw new IllegalArgumentException("Expected non-empty, valid category and domain suffix."); + } + + if (!isInitialized()) { + initialize(); + } + + try (PreparedStatement insertUpdate = dbConn.prepareStatement( + "INSERT OR REPLACE INTO " + TABLE_NAME + "(" + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + ") VALUES (?, ?)", + Statement.RETURN_GENERATED_KEYS)) { + + insertUpdate.setString(1, getNormalizedSuffix(entry.getHostSuffix())); + insertUpdate.setString(2, getNormalizedCategory(entry.getCategory())); + return insertUpdate.executeUpdate() > 0; + } + } + + /** + * Return all records in the database. + * + * @return The list of domain suffixes and their categories. + * @throws SQLException + */ + List getRecords() throws SQLException { + if (!isInitialized()) { + initialize(); + } + + List entries = new ArrayList<>(); + + try (Statement domainSelect = dbConn.createStatement(); + ResultSet resultSet = domainSelect.executeQuery( + "SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + " FROM " + TABLE_NAME + " ORDER BY " + SUFFIX_COLUMN)) { + + while (resultSet.next()) { + entries.add(new DomainCategory( + resultSet.getString(SUFFIX_COLUMN), + resultSet.getString(CATEGORY_COLUMN))); + } + } + return entries; + + } + + private static final String GET_DOMAIN_SUFFIX_QUERY + = "SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + + " FROM " + TABLE_NAME + " WHERE " + SUFFIX_COLUMN + " = ?"; + + /** + * Return the matching domain suffix or null if none found. + * + * @param domainSuffix The domain suffix. + * @return The found entry or null. + * @throws SQLException + */ + DomainCategory getRecordBySuffix(String domainSuffix) throws SQLException { + if (!isInitialized()) { + initialize(); + } + + try (PreparedStatement domainSelect = dbConn.prepareStatement(GET_DOMAIN_SUFFIX_QUERY)) { + domainSelect.setString(1, domainSuffix); + + try (ResultSet resultSet = domainSelect.executeQuery()) { + if (resultSet.next()) { + return new DomainCategory( + resultSet.getString(SUFFIX_COLUMN), + resultSet.getString(CATEGORY_COLUMN)); + } else { + return null; + } + } + } + } + + // get the suffix and category from the main table and gets the longest matching suffix. + private static final String BASE_QUERY_FMT_STR + = "SELECT " + SUFFIX_COLUMN + ", " + CATEGORY_COLUMN + " FROM " + TABLE_NAME + + " WHERE suffix IN (%s) ORDER BY LENGTH(" + SUFFIX_COLUMN + ") DESC LIMIT 1"; + + /** + * Retrieves the longest matching domain suffix and category matching the + * list of suffixes or null if no item can be found. + * + * @param suffixes The list of suffixes. + * @return The longest matching entry or null if no entry found. + * @throws SQLException + */ + synchronized DomainCategory getLongestSuffixRecord(List suffixes) throws SQLException { + if (suffixes == null) { + return null; + } + + if (!isInitialized()) { + initialize(); + } + + String questionMarks = IntStream.range(0, suffixes.size()) + .mapToObj((num) -> "?") + .collect(Collectors.joining(",")); + + try (PreparedStatement stmt = dbConn.prepareStatement(String.format(BASE_QUERY_FMT_STR, questionMarks))) { + for (int i = 0; i < suffixes.size(); i++) { + stmt.setString(i + 1, suffixes.get(i)); + } + + try (ResultSet resultSet = stmt.executeQuery()) { + if (resultSet.next()) { + String suffix = resultSet.getString(SUFFIX_COLUMN); + String category = resultSet.getString(CATEGORY_COLUMN); + return new DomainCategory(suffix, category); + } + } + } + + return null; + } + + /** + * Retrieves the longest matching domain suffix and category matching the + * list of suffixes or null if no item can be found. + * + * @param host The host name. + * @return The longest matching entry or null if no entry found. + * @throws SQLException + */ + DomainCategory getMatchingRecord(String host) throws SQLException { + return getLongestSuffixRecord(getSuffixes(host)); + } + + /** + * Retrieves all the possible suffixes that could be tracked. For instance, + * if the host was "chatenabled.mail.google.com", the list should be + * ["chatenabled.mail.google.com", "mail.google.com", "google.com", "com"]. + * + * @param host The host. + * @return The possible suffixes. + */ + private List getSuffixes(String host) { + if (host == null) { + return null; + } + + List hostTokens = Arrays.asList(host.split("\\.")); + List hostSegmentations = new ArrayList<>(); + + for (int i = 0; i < hostTokens.size(); i++) { + String searchString = String.join(".", hostTokens.subList(i, hostTokens.size())); + hostSegmentations.add(searchString); + } + + return hostSegmentations; + } + + @Override + public synchronized void close() throws SQLException { + dbConn.close(); + dbConn = null; + } +} diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsController.java b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsController.java new file mode 100644 index 0000000000..aa1d6b0956 --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsController.java @@ -0,0 +1,86 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.url.analytics.domaincategorization; + +import java.beans.PropertyChangeListener; +import javax.swing.JComponent; +import org.netbeans.spi.options.OptionsPanelController; +import org.openide.util.HelpCtx; +import org.openide.util.Lookup; + +/** + * The options panel controller that registers and displays the option panel for + * custom web categories. + */ +@OptionsPanelController.TopLevelRegistration(categoryName = "#WebCategoryOptionsController_title", + iconBase = "org/sleuthkit/autopsy/images/domain-32.png", + position = 21, + keywords = "#WebCategoryOptionsController_keywords", + keywordsCategory = "Custom Web Categories") +public class WebCategoriesOptionsController extends OptionsPanelController { + + private final WebCategoriesDataModel dataModel = WebCategoriesDataModel.getInstance(); + private final WebCategoriesOptionsPanel panel = new WebCategoriesOptionsPanel(dataModel); + + @Override + public void update() { + panel.refresh(); + } + + @Override + public void applyChanges() { + // NO OP since saves happen whenever there is a change. + } + + @Override + public void cancel() { + // NO OP since saves happen whenever there is a change. + } + + @Override + public boolean isValid() { + return true; + } + + @Override + public boolean isChanged() { + return false; + } + + @Override + public JComponent getComponent(Lookup masterLookup) { + return panel; + } + + @Override + public HelpCtx getHelpCtx() { + return null; + } + + @Override + public void addPropertyChangeListener(PropertyChangeListener l) { + // NO OP since saves happen whenever there is a change. + } + + @Override + public void removePropertyChangeListener(PropertyChangeListener l) { + // NO OP since saves happen whenever there is a change. + } + +} diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsPanel.form b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsPanel.form new file mode 100644 index 0000000000..177919921c --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsPanel.form @@ -0,0 +1,214 @@ + + +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsPanel.java b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsPanel.java new file mode 100644 index 0000000000..3f7ca1297d --- /dev/null +++ b/Core/src/org/sleuthkit/autopsy/url/analytics/domaincategorization/WebCategoriesOptionsPanel.java @@ -0,0 +1,576 @@ +/* + * Autopsy Forensic Browser + * + * Copyright 2021 Basis Technology Corp. + * Contact: carrier sleuthkit org + * + * 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 org.sleuthkit.autopsy.url.analytics.domaincategorization; + +import java.awt.Cursor; +import java.beans.PropertyChangeListener; +import java.io.File; +import java.io.IOException; +import java.sql.SQLException; +import java.util.Arrays; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; +import java.util.stream.Collectors; +import javax.swing.JFileChooser; +import javax.swing.JFrame; +import javax.swing.JOptionPane; +import javax.swing.SwingUtilities; +import javax.swing.filechooser.FileNameExtensionFilter; +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.lang.StringUtils; +import org.openide.util.NbBundle.Messages; +import org.openide.util.WeakListeners; +import org.sleuthkit.autopsy.corecomponents.OptionsPanel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.ColumnModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchResult.ResultType; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DataFetchWorker; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.DefaultCellModel; +import org.sleuthkit.autopsy.datasourcesummary.uiutils.JTablePanel; +import org.sleuthkit.autopsy.ingest.IngestManager; +import org.sleuthkit.autopsy.ingest.IngestModuleGlobalSettingsPanel; +import org.sleuthkit.autopsy.url.analytics.DomainCategory; + +/** + * The options panel displayed for import, export, and CRUD operations on domain + * categories. + */ +@Messages({ + "WebCategoriesOptionsPanel_categoryTable_suffixColumnName=Domain Suffix", + "WebCategoriesOptionsPanel_categoryTable_categoryColumnName=Category",}) +public class WebCategoriesOptionsPanel extends IngestModuleGlobalSettingsPanel implements OptionsPanel, AutoCloseable { + + private static final Logger logger = Logger.getLogger(WebCategoriesOptionsPanel.class.getName()); + private static final String DEFAULT_EXTENSION = "json"; + private static final FileNameExtensionFilter DB_FILTER = new FileNameExtensionFilter("JSON", DEFAULT_EXTENSION); + + private final JFileChooser fileChooser = new JFileChooser(); + private final WebCategoriesDataModel dataModel; + + private final JTablePanel categoryTable + = JTablePanel.getJTablePanel(Arrays.asList( + new ColumnModel>( + Bundle.WebCategoriesOptionsPanel_categoryTable_suffixColumnName(), + (domCat) -> new DefaultCellModel<>(domCat.getHostSuffix()) + .setTooltip(domCat.getHostSuffix()), + 300 + ), + new ColumnModel<>( + Bundle.WebCategoriesOptionsPanel_categoryTable_categoryColumnName(), + (domCat) -> new DefaultCellModel<>(domCat.getCategory()) + .setTooltip(domCat.getCategory()), + 200 + ) + )).setKeyFunction((domCat) -> domCat.getHostSuffix()); + + private final PropertyChangeListener ingestListener = (evt) -> refreshComponentStates(); + private final PropertyChangeListener weakIngestListener = WeakListeners.propertyChange(ingestListener, this); + private Set domainSuffixes = new HashSet<>(); + private boolean isRefreshing = false; + + /** + * Main constructor. + * + * @param dataModel The data model that interacts with the database. + */ + public WebCategoriesOptionsPanel(WebCategoriesDataModel dataModel) { + initComponents(); + this.dataModel = dataModel; + + fileChooser.addChoosableFileFilter(DB_FILTER); + fileChooser.setFileFilter(DB_FILTER); + categoryTable.setCellListener((evt) -> refreshComponentStates()); + IngestManager.getInstance().addIngestJobEventListener(weakIngestListener); + setDefaultCursor(); + refresh(); + } + + /** + * Returns the items selected in the table or null if no selection. + * + * @return The items selected in the table or null if no selection. + */ + private List getSelected() { + return categoryTable.getSelectedItems(); + } + + /** + * Triggers swing worker to fetch data and show in table. + */ + void refresh() { + isRefreshing = true; + refreshComponentStates(); + categoryTable.showDefaultLoadingMessage(); + new DataFetchWorker>( + (noVal) -> this.dataModel.getRecords(), + (data) -> onRefreshedData(data), + null).execute(); + } + + /** + * When the result of loading the data is returned, this function handles + * updating the GUI. + * + * @param categoriesResult The result of attempting to fetch the data. + */ + private void onRefreshedData(DataFetchResult> categoriesResult) { + categoryTable.showDataFetchResult(categoriesResult); + if (categoriesResult.getResultType() == ResultType.SUCCESS && categoriesResult.getData() != null) { + domainSuffixes = categoriesResult.getData().stream() + .map((dc) -> dc.getHostSuffix()) + .collect(Collectors.toSet()); + } else { + domainSuffixes = new HashSet<>(); + } + isRefreshing = false; + refreshComponentStates(); + } + + /** + * Refreshes the state of the components based on whether or not an item is + * selected as well as whether or not data is loading or ingest is + * happening. + */ + private void refreshComponentStates() { + List selectedItems = getSelected(); + int selectedCount = CollectionUtils.isEmpty(selectedItems) ? 0 : selectedItems.size(); + boolean isIngestRunning = IngestManager.getInstance().isIngestRunning(); + boolean operationsPermitted = !isIngestRunning && !isRefreshing; + + deleteEntryButton.setEnabled(selectedCount > 0 && operationsPermitted); + editEntryButton.setEnabled(selectedCount == 1 && operationsPermitted); + + newEntryButton.setEnabled(operationsPermitted); + exportSetButton.setEnabled(operationsPermitted); + importSetButton.setEnabled(operationsPermitted); + + ingestRunningWarning.setVisible(isIngestRunning); + } + + /** + * Shows the AddEditCategoryDialog to the user and returns the user-inputted + * DomainCategory or null if nothing was saved. + * + * @param original If editing a value, this is the original value being + * edited. If adding a new value, this should be null. + * @return + */ + private DomainCategory getAddEditValue(DomainCategory original) { + JFrame parent = (this.getRootPane() != null && this.getRootPane().getParent() instanceof JFrame) + ? (JFrame) this.getRootPane().getParent() + : null; + + AddEditCategoryDialog addEditDialog = new AddEditCategoryDialog(parent, domainSuffixes, original); + addEditDialog.setResizable(false); + addEditDialog.setLocationRelativeTo(parent); + addEditDialog.setVisible(true); + addEditDialog.toFront(); + + if (addEditDialog.isChanged()) { + return addEditDialog.getValue(); + } else { + return null; + } + } + + /** + * Set cursor to waiting. + */ + private void setWaitingCursor() { + SwingUtilities.invokeLater(() -> this.setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR))); + } + + /** + * Set cursor to default. + */ + private void setDefaultCursor() { + SwingUtilities.invokeLater(() -> this.setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR))); + } + + /** + * An action for updating or altering data in the custom configuration. + */ + private interface UpdateAction { + + /** + * A runnable action to update custom configuration. + * + * @throws IllegalArgumentException + * @throws IOException + * @throws SQLException + */ + void run() throws IllegalArgumentException, IOException, SQLException; + } + + /** + * Runs an action to update the state of the configuration and runs refresh + * when complete. + * + * @param runnable The runnable action. + * @throws IllegalArgumentException + * @throws IOException + * @throws SQLException + */ + private void runUpdateAction(UpdateAction runnable) throws IllegalArgumentException, IOException, SQLException { + setWaitingCursor(); + runnable.run(); + setDefaultCursor(); + refresh(); + } + + /** + * This method is called from within the constructor to initialize the form. + * WARNING: Do NOT modify this code. The content of this method is always + * regenerated by the Form Editor. + */ + @SuppressWarnings("unchecked") + // //GEN-BEGIN:initComponents + private void initComponents() { + java.awt.GridBagConstraints gridBagConstraints; + + javax.swing.JLabel panelDescription = new javax.swing.JLabel(); + javax.swing.JLabel categoriesTitle = new javax.swing.JLabel(); + javax.swing.JPanel categoryTablePanel = categoryTable; + newEntryButton = new javax.swing.JButton(); + editEntryButton = new javax.swing.JButton(); + deleteEntryButton = new javax.swing.JButton(); + importSetButton = new javax.swing.JButton(); + exportSetButton = new javax.swing.JButton(); + javax.swing.JPanel bottomStrut = new javax.swing.JPanel(); + ingestRunningWarning = new javax.swing.JLabel(); + + setLayout(new java.awt.GridBagLayout()); + + panelDescription.setHorizontalAlignment(javax.swing.SwingConstants.CENTER); + panelDescription.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.panelDescription.text")); // NOI18N + panelDescription.setBorder(javax.swing.BorderFactory.createCompoundBorder(javax.swing.BorderFactory.createEtchedBorder(), javax.swing.BorderFactory.createEmptyBorder(5, 5, 5, 5))); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 0); + add(panelDescription, gridBagConstraints); + + categoriesTitle.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.categoriesTitle.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 1; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 0, 0); + add(categoriesTitle, gridBagConstraints); + + categoryTablePanel.setAutoscrolls(true); + categoryTablePanel.setMaximumSize(new java.awt.Dimension(400, 32767)); + categoryTablePanel.setMinimumSize(new java.awt.Dimension(400, 300)); + categoryTablePanel.setPreferredSize(new java.awt.Dimension(400, 600)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 2; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.weighty = 1.0; + gridBagConstraints.insets = new java.awt.Insets(2, 10, 10, 0); + add(categoryTablePanel, gridBagConstraints); + + newEntryButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/add16.png"))); // NOI18N + newEntryButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.newEntryButton.text")); // NOI18N + newEntryButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + newEntryButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 5, 5); + add(newEntryButton, gridBagConstraints); + + editEntryButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/edit16.png"))); // NOI18N + editEntryButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.editEntryButton.text")); // NOI18N + editEntryButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + editEntryButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 3; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5); + add(editEntryButton, gridBagConstraints); + + deleteEntryButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/delete16.png"))); // NOI18N + deleteEntryButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.deleteEntryButton.text")); // NOI18N + deleteEntryButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + deleteEntryButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 2; + gridBagConstraints.gridy = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5); + add(deleteEntryButton, gridBagConstraints); + + importSetButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/import16.png"))); // NOI18N + importSetButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.importSetButton.text")); // NOI18N + importSetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + importSetButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 4; + gridBagConstraints.fill = java.awt.GridBagConstraints.BOTH; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 10, 5, 5); + add(importSetButton, gridBagConstraints); + + exportSetButton.setIcon(new javax.swing.ImageIcon(getClass().getResource("/org/sleuthkit/autopsy/images/export16.png"))); // NOI18N + exportSetButton.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.exportSetButton.text")); // NOI18N + exportSetButton.addActionListener(new java.awt.event.ActionListener() { + public void actionPerformed(java.awt.event.ActionEvent evt) { + exportSetButtonActionPerformed(evt); + } + }); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 1; + gridBagConstraints.gridy = 4; + gridBagConstraints.anchor = java.awt.GridBagConstraints.LINE_START; + gridBagConstraints.insets = new java.awt.Insets(0, 0, 5, 5); + add(exportSetButton, gridBagConstraints); + + bottomStrut.setPreferredSize(new java.awt.Dimension(10, 0)); + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 3; + gridBagConstraints.gridy = 6; + gridBagConstraints.fill = java.awt.GridBagConstraints.HORIZONTAL; + gridBagConstraints.weightx = 1.0; + add(bottomStrut, gridBagConstraints); + + ingestRunningWarning.setForeground(java.awt.Color.RED); + ingestRunningWarning.setText(org.openide.util.NbBundle.getMessage(WebCategoriesOptionsPanel.class, "WebCategoriesOptionsPanel.ingestRunningWarning.text")); // NOI18N + gridBagConstraints = new java.awt.GridBagConstraints(); + gridBagConstraints.gridx = 0; + gridBagConstraints.gridy = 5; + gridBagConstraints.gridwidth = 3; + gridBagConstraints.anchor = java.awt.GridBagConstraints.WEST; + gridBagConstraints.insets = new java.awt.Insets(10, 10, 10, 10); + add(ingestRunningWarning, gridBagConstraints); + }// //GEN-END:initComponents + + private void deleteEntryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_deleteEntryButtonActionPerformed + List selectedItems = getSelected(); + if (!CollectionUtils.isEmpty(selectedItems)) { + setWaitingCursor(); + for (DomainCategory selected : selectedItems) { + if (selected != null && selected.getHostSuffix() != null) { + try { + dataModel.deleteRecord(selected.getHostSuffix()); + } catch (IllegalArgumentException | SQLException ex) { + logger.log(Level.WARNING, "There was an error while deleting: " + selected.getHostSuffix(), ex); + } + } + } + setDefaultCursor(); + refresh(); + } + }//GEN-LAST:event_deleteEntryButtonActionPerformed + + private void newEntryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_newEntryButtonActionPerformed + DomainCategory newCategory = getAddEditValue(null); + if (newCategory != null) { + try { + runUpdateAction(() -> dataModel.insertUpdateSuffix(newCategory)); + } catch (IllegalArgumentException | SQLException | IOException ex) { + setDefaultCursor(); + logger.log(Level.WARNING, "There was an error while adding new record: " + newCategory.getHostSuffix(), ex); + } + } + }//GEN-LAST:event_newEntryButtonActionPerformed + + private void editEntryButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_editEntryButtonActionPerformed + List selectedItems = getSelected(); + if (CollectionUtils.isNotEmpty(selectedItems)) { + DomainCategory selected = selectedItems.get(0); + if (selected != null && selected.getHostSuffix() != null) { + try { + DomainCategory newCategory = getAddEditValue(selected); + if (newCategory != null) { + runUpdateAction(() -> dataModel.insertUpdateSuffix(newCategory)); + } + } catch (IllegalArgumentException | SQLException | IOException ex) { + setDefaultCursor(); + logger.log(Level.WARNING, "There was an error while editing: " + selected.getHostSuffix(), ex); + } + } + } + }//GEN-LAST:event_editEntryButtonActionPerformed + + @Messages({ + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage=There was an error importing this json file.", + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle=Import Error", + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictTitle=Domain Suffix Already Exists", + "# {0} - domainSuffix", + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictMessage=Domain suffix {0} already exists. What would you like to do?", + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictOverwrite=Overwrite", + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictSkip=Skip", + "WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictCancel=Cancel"}) + private void importSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_importSetButtonActionPerformed + fileChooser.setSelectedFile(new File("")); + int result = fileChooser.showOpenDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + File selectedFile = fileChooser.getSelectedFile(); + if (selectedFile != null && selectedFile.exists()) { + try { + runUpdateAction(() -> { + List categories = dataModel.getJsonEntries(selectedFile); + + for (DomainCategory domcat : categories) { + String normalizedCategory = domcat == null ? "" : WebCategoriesDataModel.getNormalizedCategory(domcat.getCategory()); + String normalizedSuffix = domcat == null ? "" : WebCategoriesDataModel.getNormalizedSuffix(domcat.getHostSuffix()); + + if (StringUtils.isBlank(normalizedCategory) || StringUtils.isBlank(normalizedSuffix)) { + logger.log(Level.WARNING, String.format("Invalid entry [category: %s, domain suffix: %s]", normalizedCategory, normalizedSuffix)); + continue; + } + + DomainCategory currentCategory = dataModel.getRecordBySuffix(normalizedSuffix); + // if a mapping for the domain suffix already exists and the value will change, prompt the user on what to do. + if (currentCategory != null) { + if (normalizedCategory.equalsIgnoreCase(currentCategory.getCategory())) { + // do nothing if import item is same as already present + continue; + } else { + + String[] options = { + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictOverwrite(), + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictSkip(), + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictCancel() + }; + + int optionItem = JOptionPane.showOptionDialog(null, + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictMessage(normalizedSuffix), + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_onConflictTitle(), + JOptionPane.DEFAULT_OPTION, JOptionPane.WARNING_MESSAGE, null, options, options[0]); + + switch (optionItem) { + case 0: + break; + case 1: + continue; + case 2: + return; + } + } + } + + dataModel.insertUpdateSuffix(new DomainCategory(normalizedSuffix, normalizedCategory)); + } + }); + } catch (IllegalArgumentException | SQLException | IOException ex) { + setDefaultCursor(); + JOptionPane.showMessageDialog( + this, + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage(), + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle(), + JOptionPane.ERROR_MESSAGE); + logger.log(Level.WARNING, "There was an error on import.", ex); + } + } + } + }//GEN-LAST:event_importSetButtonActionPerformed + + @Messages({ + "WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateMessage=A file already exists at the selected path. The categories will not be exported.", + "WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateTitle=File Already Exists", + "WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorMessage=There was an error exporting.", + "WebCategoriesOptionsPanel_exportSetButtonActionPerformed_errorTitle=Export Error", + "WebCategoriesOptionsPanel_exportSetButtonActionPerformed_defaultFileName=Custom Categories Export" + }) + private void exportSetButtonActionPerformed(java.awt.event.ActionEvent evt) {//GEN-FIRST:event_exportSetButtonActionPerformed + fileChooser.setSelectedFile(new File(String.format("%s.json", Bundle.WebCategoriesOptionsPanel_exportSetButtonActionPerformed_defaultFileName()))); + int result = fileChooser.showSaveDialog(this); + if (result == JFileChooser.APPROVE_OPTION) { + File selectedFile = fileChooser.getSelectedFile(); + if (selectedFile != null) { + if (selectedFile.exists()) { + JOptionPane.showMessageDialog( + this, + Bundle.WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateMessage(), + Bundle.WebCategoriesOptionsPanel_exportSetButtonActionPerformed_duplicateTitle(), + JOptionPane.ERROR_MESSAGE); + return; + } + try { + setWaitingCursor(); + dataModel.exportToJson(selectedFile); + setDefaultCursor(); + } catch (SQLException | IOException ex) { + setDefaultCursor(); + JOptionPane.showMessageDialog( + this, + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorMessage(), + Bundle.WebCategoriesOptionsPanel_importSetButtonActionPerformed_errorTitle(), + JOptionPane.ERROR_MESSAGE); + logger.log(Level.WARNING, "There was an error on export.", ex); + } + } + } + }//GEN-LAST:event_exportSetButtonActionPerformed + + + // Variables declaration - do not modify//GEN-BEGIN:variables + private javax.swing.JButton deleteEntryButton; + private javax.swing.JButton editEntryButton; + private javax.swing.JButton exportSetButton; + private javax.swing.JButton importSetButton; + private javax.swing.JLabel ingestRunningWarning; + private javax.swing.JButton newEntryButton; + // End of variables declaration//GEN-END:variables + + @Override + public void saveSettings() { + // NO OP since saves happen whenever there is a change. + } + + @Override + public void store() { + // NO OP since saves happen whenever there is a change. + } + + @Override + public void load() { + refresh(); + } + + @Override + public void close() { + IngestManager.getInstance().removeIngestJobEventListener(weakIngestListener); + } +} diff --git a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DomainCategoryRunner.java b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DomainCategoryRunner.java index cb06a525c9..94108c5a00 100644 --- a/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DomainCategoryRunner.java +++ b/RecentActivity/src/org/sleuthkit/autopsy/recentactivity/DomainCategoryRunner.java @@ -83,6 +83,9 @@ class DomainCategoryRunner extends Extract { private static final Logger logger = Logger.getLogger(DomainCategoryRunner.class.getName()); + // NOTE: if CustomWebCategorizer ever changes name, this will need to be changed as well. + private static final String CUSTOM_CATEGORIZER_PATH = "org.sleuthkit.autopsy.url.analytics.domaincategorization.CustomWebCategorizer"; + /** * Get seconds from epoch from the mapping for the attribute type id. * @@ -444,7 +447,16 @@ class DomainCategoryRunner extends Extract { List foundProviders = lookupList.stream() .filter(provider -> provider != null) - .sorted((a, b) -> a.getClass().getName().compareToIgnoreCase(b.getClass().getName())) + .sorted((a, b) -> { + boolean aIsCustom = a.getClass().getName().contains(CUSTOM_CATEGORIZER_PATH); + boolean bIsCustom = b.getClass().getName().contains(CUSTOM_CATEGORIZER_PATH); + if (aIsCustom != bIsCustom) { + // push custom categorizer to top + return -Boolean.compare(aIsCustom, bIsCustom); + } + + return a.getClass().getName().compareToIgnoreCase(b.getClass().getName()); + }) .collect(Collectors.toList()); // add the default categorizer last as a last resort diff --git a/thirdparty/DomainCategorization/README.txt b/thirdparty/DomainCategorization/README.txt new file mode 100644 index 0000000000..4dfe31b7ac --- /dev/null +++ b/thirdparty/DomainCategorization/README.txt @@ -0,0 +1 @@ +This is a folder containing information for web domain categorization and custom categories. \ No newline at end of file