Autopsy 4.22.1
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 */
19package org.sleuthkit.autopsy.keywordsearch;
20
21import java.awt.EventQueue;
22import java.util.ArrayList;
23import java.util.Arrays;
24import java.util.Collection;
25import java.util.HashMap;
26import java.util.LinkedHashMap;
27import java.util.List;
28import java.util.Map;
29import java.util.concurrent.CancellationException;
30import java.util.concurrent.ExecutionException;
31import java.util.logging.Level;
32import java.util.stream.Collectors;
33import java.util.stream.Stream;
34import javax.swing.SwingUtilities;
35import javax.swing.SwingWorker;
36import org.netbeans.api.progress.ProgressHandle;
37import org.openide.nodes.ChildFactory;
38import org.openide.nodes.Children;
39import org.openide.nodes.Node;
40import org.openide.util.Cancellable;
41import org.openide.util.NbBundle;
42import org.openide.util.lookup.Lookups;
43import org.sleuthkit.autopsy.casemodule.Case;
44import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
45import org.sleuthkit.autopsy.core.RuntimeProperties;
46import org.sleuthkit.autopsy.coreutils.Logger;
47import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
48import org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode;
49import static org.sleuthkit.autopsy.datamodel.AbstractAbstractFileNode.AbstractFilePropertyType.LOCATION;
50import org.sleuthkit.autopsy.datamodel.AbstractFsContentNode;
51import org.sleuthkit.autopsy.datamodel.EmptyNode;
52import org.sleuthkit.autopsy.datamodel.KeyValue;
53import org.sleuthkit.autopsy.datamodel.KeyValueNode;
54import org.sleuthkit.autopsy.keywordsearch.AdHocSearchChildFactory.KeywordHitKey;
55import org.sleuthkit.datamodel.AbstractFile;
56import org.sleuthkit.datamodel.BlackboardArtifact;
57import org.sleuthkit.datamodel.BlackboardAttribute;
58import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD;
59import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW;
60import static org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP;
61import org.sleuthkit.datamodel.Content;
62import org.sleuthkit.datamodel.SleuthkitCase;
63import org.sleuthkit.datamodel.TskCoreException;
64
72class 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-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.