Autopsy  4.20.0
Graphical digital forensics platform for The Sleuth Kit and other tools.
AdHocSearchChildFactory.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2012-2021 Basis Technology Corp.
5  * Contact: carrier <at> sleuthkit <dot> org
6  *
7  * Licensed under the Apache License, Version 2.0 (the "License");
8  * you may not use this content except in compliance with the License.
9  * You may obtain a copy of the License at
10  *
11  * http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing, software
14  * distributed under the License is distributed on an "AS IS" BASIS,
15  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16  * See the License for the specific language governing permissions and
17  * limitations under the License.
18  */
19 package org.sleuthkit.autopsy.keywordsearch;
20 
21 import java.awt.EventQueue;
22 import java.util.ArrayList;
23 import java.util.Arrays;
24 import java.util.Collection;
25 import java.util.HashMap;
26 import java.util.LinkedHashMap;
27 import java.util.List;
28 import java.util.Map;
29 import java.util.concurrent.CancellationException;
30 import java.util.concurrent.ExecutionException;
31 import java.util.logging.Level;
32 import java.util.stream.Collectors;
33 import java.util.stream.Stream;
34 import javax.swing.SwingUtilities;
35 import javax.swing.SwingWorker;
36 import org.netbeans.api.progress.ProgressHandle;
37 import org.openide.nodes.ChildFactory;
38 import org.openide.nodes.Children;
39 import org.openide.nodes.Node;
40 import org.openide.util.Cancellable;
41 import org.openide.util.NbBundle;
42 import org.openide.util.lookup.Lookups;
55 import org.sleuthkit.datamodel.AbstractFile;
56 import org.sleuthkit.datamodel.BlackboardArtifact;
57 import org.sleuthkit.datamodel.BlackboardAttribute;
58 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD;
59 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW;
60 import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP;
61 import org.sleuthkit.datamodel.Content;
62 import org.sleuthkit.datamodel.SleuthkitCase;
63 import org.sleuthkit.datamodel.TskCoreException;
64 
72 class AdHocSearchChildFactory extends ChildFactory<KeyValue> {
73 
74  private static final Logger logger = Logger.getLogger(AdHocSearchChildFactory.class.getName());
75 
76  //common properties (superset of all Node properties) to be displayed as columns
77  static final List<String> COMMON_PROPERTIES
78  = Stream.concat(
79  Stream.of(
80  TSK_KEYWORD,
81  TSK_KEYWORD_REGEXP,
82  TSK_KEYWORD_PREVIEW)
83  .map(BlackboardAttribute.ATTRIBUTE_TYPE::getDisplayName),
84  Arrays.stream(AbstractAbstractFileNode.AbstractFilePropertyType.values())
85  .map(Object::toString))
86  .collect(Collectors.toList());
87 
88  private final Collection<AdHocQueryRequest> queryRequests;
89  private final boolean saveResults;
90 
98  AdHocSearchChildFactory(Collection<AdHocQueryRequest> queryRequests, boolean saveResults) {
99  this.queryRequests = queryRequests;
100  this.saveResults = saveResults;
101  }
102 
110  @Override
111  protected boolean createKeys(List<KeyValue> toPopulate) {
112 
113  for (AdHocQueryRequest queryRequest : queryRequests) {
117  if (!queryRequest.getQuery().validate()) {
118  //TODO mark the particular query node RED
119  break;
120  }
121 
122  //JMTODO: It looks like this map is not actually used for anything...
123  Map<String, Object> map = queryRequest.getProperties();
124  /*
125  * make sure all common properties are displayed as columns (since
126  * we are doing lazy child Node load we need to preinitialize
127  * properties when sending parent Node)
128  */
129  COMMON_PROPERTIES.stream()
130  .forEach((propertyType) -> map.put(propertyType, ""));
131  map.put(TSK_KEYWORD.getDisplayName(), queryRequest.getQueryString());
132  map.put(TSK_KEYWORD_REGEXP.getDisplayName(), !queryRequest.getQuery().isLiteral());
133 
134  createFlatKeys(queryRequest.getQuery(), toPopulate);
135  }
136 
137  // If there were no hits, make a single Node that will display that
138  // no results were found.
139  if (toPopulate.isEmpty()) {
140  toPopulate.add(new KeyValue("This KeyValue Is Empty", 0));
141  }
142 
143  return true;
144  }
145 
153  @NbBundle.Messages({"KeywordSearchResultFactory.query.exception.msg=Could not perform the query "})
154  private boolean createFlatKeys(KeywordSearchQuery queryRequest, List<KeyValue> toPopulate) {
155 
159  QueryResults queryResults;
160  try {
161  queryResults = queryRequest.performQuery();
162  } catch (KeywordSearchModuleException | NoOpenCoreException ex) {
163  logger.log(Level.SEVERE, "Could not perform the query " + queryRequest.getQueryString(), ex); //NON-NLS
164  MessageNotifyUtil.Notify.error(Bundle.KeywordSearchResultFactory_query_exception_msg() + queryRequest.getQueryString(), ex.getCause().getMessage());
165  return false;
166  }
167  SleuthkitCase tskCase;
168  try {
169  tskCase = Case.getCurrentCaseThrows().getSleuthkitCase();
170  } catch (NoCurrentCaseException ex) {
171  logger.log(Level.SEVERE, "There was no case open.", ex); //NON-NLS
172  return false;
173  }
174 
175  int hitNumber = 0;
176  List<KeywordHitKey> tempList = new ArrayList<>();
177  for (KeywordHit hit : getOneHitPerObject(queryResults)) {
178 
182  Map<String, Object> properties = new LinkedHashMap<>();
183 
187  if (hit.hasSnippet()) {
188  properties.put(TSK_KEYWORD_PREVIEW.getDisplayName(), hit.getSnippet());
189  }
190 
191  Content content;
192  String contentName;
193  try {
194  content = tskCase.getContentById(hit.getContentID());
195  if (content == null) {
196  logger.log(Level.SEVERE, "There was a error getting content by id."); //NON-NLS
197  return false;
198  }
199  } catch (TskCoreException ex) {
200  logger.log(Level.SEVERE, "There was a error getting content by id.", ex); //NON-NLS
201  return false;
202  }
203 
204  contentName = content.getName();
205  if (content instanceof AbstractFile) {
206  AbstractFsContentNode.fillPropertyMap(properties, (AbstractFile) content);
207  } else {
208  properties.put(LOCATION.toString(), contentName);
209  }
210 
211  String hitName;
212  BlackboardArtifact artifact = null;
213  if (hit.isArtifactHit()) {
214  try {
215  artifact = tskCase.getBlackboardArtifact(hit.getArtifactID().get());
216  hitName = artifact.getDisplayName() + " Artifact"; //NON-NLS
217  } catch (TskCoreException ex) {
218  logger.log(Level.SEVERE, "Error getting blckboard artifact by id", ex);
219  return false;
220  }
221  } else {
222  hitName = contentName;
223  }
224  hitNumber++;
225  tempList.add(new KeywordHitKey(hitName, properties, hitNumber, hit.getSolrObjectId(), content, artifact, queryRequest, queryResults));
226 
227  }
228 
229  if (hitNumber != 0) {
230  // Add all the nodes to toPopulate at once. Minimizes node creation
231  // EDT threads, which can slow and/or hang the UI on large queries.
232  toPopulate.addAll(tempList);
233  }
234 
235  //write to bb
236  //cannot reuse snippet in BlackboardResultWriter
237  //because for regex searches in UI we compress results by showing a content per regex once (even if multiple term hits)
238  //whereas in bb we write every hit per content separately
239  new BlackboardResultWriter(queryResults, queryRequest.getKeywordList().getName(), saveResults).execute();
240 
241  return true;
242  }
243 
253  Collection<KeywordHit> getOneHitPerObject(QueryResults queryResults) {
254  HashMap<Long, KeywordHit> hits = new HashMap<>();
255  for (Keyword keyWord : queryResults.getKeywords()) {
256  for (KeywordHit hit : queryResults.getResults(keyWord)) {
257  // add hit with lowest SolrObjectID-Chunk-ID combination.
258  if (!hits.containsKey(hit.getSolrObjectId())) {
259  hits.put(hit.getSolrObjectId(), hit);
260  } else if (hit.getChunkId() < hits.get(hit.getSolrObjectId()).getChunkId()) {
261  hits.put(hit.getSolrObjectId(), hit);
262  }
263  }
264  }
265  return hits.values();
266  }
267 
268  @NbBundle.Messages({"KeywordSearchResultFactory.createNodeForKey.noResultsFound.text=No results found."})
269  @Override
270  protected Node createNodeForKey(KeyValue key) {
271  Node resultNode;
272 
273  if (key instanceof KeywordHitKey) {
274  AdHocQueryResult adHocQueryResult = new AdHocQueryResult((KeywordHitKey) key);
275 
280  ArrayList<Object> lookups = new ArrayList<>();
281  lookups.add(adHocQueryResult);
282  if (((KeywordHitKey) key).getContent() != null) {
283  lookups.add(((KeywordHitKey) key).getContent());
284  }
285  if (((KeywordHitKey) key).getArtifact() != null) {
286  lookups.add(((KeywordHitKey) key).getArtifact());
287  }
288 
289  Node kvNode = new KeyValueNode(key, Children.LEAF, Lookups.fixed(lookups.toArray()));
290 
291  //wrap in KeywordSearchFilterNode for the markup content, might need to override FilterNode for more customization
292  resultNode = new AdHocSearchFilterNode(kvNode);
293  } else {
294  resultNode = new EmptyNode("This Node Is Empty");
295  resultNode.setDisplayName(NbBundle.getMessage(this.getClass(), "KeywordSearchResultFactory.createNodeForKey.noResultsFound.text"));
296  }
297 
298  return resultNode;
299 
300  }
301 
306  final class AdHocQueryResult {
307 
308  private final long solrObjectId;
309  private final QueryResults results;
310 
318  AdHocQueryResult(KeywordHitKey key) {
319  this.solrObjectId = key.getSolrObjectId();
320  this.results = key.getHits();
321  }
322 
329  long getSolrObjectId() {
330  return solrObjectId;
331  }
332 
338  QueryResults getResults() {
339  return results;
340  }
341  }
342 
347  class KeywordHitKey extends KeyValue {
348 
349  private final long solrObjectId;
350 
351  private final Content content;
352  private final BlackboardArtifact artifact;
353  private final QueryResults hits;
354  private final KeywordSearchQuery query;
355 
370  KeywordHitKey(String name, Map<String, Object> map, int id, long solrObjectId, Content content, BlackboardArtifact artifact, KeywordSearchQuery query, QueryResults hits) {
371  super(name, map, id);
372  this.solrObjectId = solrObjectId;
373  this.content = content;
374  this.artifact = artifact;
375 
376  this.hits = hits;
377  this.query = query;
378  }
379 
380  Content getContent() {
381  return content;
382  }
383 
384  BlackboardArtifact getArtifact() {
385  return artifact;
386  }
387 
388  long getSolrObjectId() {
389  return solrObjectId;
390  }
391 
392  QueryResults getHits() {
393  return hits;
394  }
395 
396  KeywordSearchQuery getQuery() {
397  return query;
398  }
399  }
400 
405  static class BlackboardResultWriter extends SwingWorker<Void, Void> {
406 
407  private static final List<BlackboardResultWriter> WRITERS = new ArrayList<>();
408  private ProgressHandle progress;
409  private final KeywordSearchQuery query;
410  private final QueryResults hits;
411  private static final int QUERY_DISPLAY_LEN = 40;
412  private final boolean saveResults;
413 
414  BlackboardResultWriter(QueryResults hits, String listName, boolean saveResults) {
415  this.hits = hits;
416  this.query = hits.getQuery();
417  this.saveResults = saveResults;
418  }
419 
420  @Override
421  protected Void doInBackground() throws Exception {
422  try {
423  if (RuntimeProperties.runningWithGUI()) {
424  final String queryStr = query.getQueryString();
425  final String queryDisp = queryStr.length() > QUERY_DISPLAY_LEN ? queryStr.substring(0, QUERY_DISPLAY_LEN - 1) + " ..." : queryStr;
426  SwingUtilities.invokeLater(() -> {
427  progress = ProgressHandle.createHandle(
428  NbBundle.getMessage(this.getClass(), "KeywordSearchResultFactory.progress.saving", queryDisp),
429  new Cancellable() {
430  @Override
431  public boolean cancel() {
432  //progress.setDisplayName(displayName + " " + NbBundle.getMessage(this.getClass(), "SearchRunner.doInBackGround.cancelMsg"));
433  logger.log(Level.INFO, "Ad hoc search cancelled by user"); //NON-NLS
434  new Thread(() -> {
435  BlackboardResultWriter.this.cancel(true);
436  }).start();
437  return true;
438  }
439  });
440  });
441  }
442  registerWriter(this); //register (synchronized on class) outside of writerLock to prevent deadlock
443  hits.process(this, false, saveResults, null);
444  } finally {
445  deregisterWriter(this);
446  if (RuntimeProperties.runningWithGUI() && progress != null) {
447  EventQueue.invokeLater(progress::finish);
448  }
449  }
450  return null;
451  }
452 
453  @Override
454  protected void done() {
455  try {
456  get();
457  } catch (InterruptedException | CancellationException ex) {
458  logger.log(Level.WARNING, "User cancelled writing of ad hoc search query results for '{0}' to the blackboard", query.getQueryString()); //NON-NLS
459  } catch (ExecutionException ex) {
460  logger.log(Level.SEVERE, "Error writing of ad hoc search query results for " + query.getQueryString() + " to the blackboard", ex); //NON-NLS
461  }
462  }
463 
464  private static synchronized void registerWriter(BlackboardResultWriter writer) {
465  WRITERS.add(writer);
466  }
467 
468  private static synchronized void deregisterWriter(BlackboardResultWriter writer) {
469  WRITERS.remove(writer);
470  }
471 
472  static synchronized void stopAllWriters() {
473  for (BlackboardResultWriter w : WRITERS) {
474  w.cancel(true);
475  WRITERS.remove(w);
476  }
477  }
478  }
479 }

Copyright © 2012-2022 Basis Technology. Generated on: Tue Aug 1 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.