/**
   * Check file extension and return appropriate input options for SevenZip.openInArchive()
   *
   * @param archiveFile file to check file extension
   * @return input parameter for SevenZip.openInArchive()
   */
  private ArchiveFormat get7ZipOptions(AbstractFile archiveFile) {
    // try to get the file type from the BB
    String detectedFormat = null;
    try {
      ArrayList<BlackboardAttribute> attributes =
          archiveFile.getGenInfoAttributes(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG);
      for (BlackboardAttribute attribute : attributes) {
        detectedFormat = attribute.getValueString();
        break;
      }
    } catch (TskCoreException ex) {
      logger.log(
          Level.WARNING,
          "Couldn't obtain file attributes for file: " + archiveFile.toString(),
          ex); // NON-NLS
    }

    if (detectedFormat == null) {
      logger.log(Level.WARNING, "Could not detect format for file: " + archiveFile); // NON-NLS

      // if we don't have attribute info then use file extension
      String extension = archiveFile.getNameExtension();
      if ("rar".equals(extension)) // NON-NLS
      {
        // for RAR files we need to open them explicitly as RAR. Otherwise, if there is a ZIP
        // archive inside RAR archive
        // it will be opened incorrectly when using 7zip's built-in auto-detect functionality
        return RAR;
      }

      // Otherwise open the archive using 7zip's built-in auto-detect functionality
      return null;
    } else if (detectedFormat.contains("application/x-rar-compressed")) // NON-NLS
    {
      // for RAR files we need to open them explicitly as RAR. Otherwise, if there is a ZIP archive
      // inside RAR archive
      // it will be opened incorrectly when using 7zip's built-in auto-detect functionality
      return RAR;
    }

    // Otherwise open the archive using 7zip's built-in auto-detect functionality
    return null;
  }
Example #2
0
    private void initData() {
      try {
        // Get all file and artifact tags

        // init data
        tags =
            new EnumMap<BlackboardArtifact.ARTIFACT_TYPE, Map<String, List<BlackboardArtifact>>>(
                BlackboardArtifact.ARTIFACT_TYPE.class);
        tags.put(
            BlackboardArtifact.ARTIFACT_TYPE.TSK_TAG_FILE,
            new HashMap<String, List<BlackboardArtifact>>());
        tags.put(
            BlackboardArtifact.ARTIFACT_TYPE.TSK_TAG_ARTIFACT,
            new HashMap<String, List<BlackboardArtifact>>());

        // populate
        for (BlackboardArtifact.ARTIFACT_TYPE artType : tags.keySet()) {
          final Map<String, List<BlackboardArtifact>> artTags = tags.get(artType);
          for (BlackboardArtifact artifact : skCase.getBlackboardArtifacts(artType)) {
            for (BlackboardAttribute attribute : artifact.getAttributes()) {
              if (attribute.getAttributeTypeID() == ATTRIBUTE_TYPE.TSK_TAG_NAME.getTypeID()) {
                String tagName = attribute.getValueString();
                if (artTags.containsKey(tagName)) {
                  List<BlackboardArtifact> artifacts = artTags.get(tagName);
                  artifacts.add(artifact);
                } else {
                  List<BlackboardArtifact> artifacts = new ArrayList<BlackboardArtifact>();
                  artifacts.add(artifact);
                  artTags.put(tagName, artifacts);
                }
                break;
              }
            }
          }
        }

      } catch (TskCoreException ex) {
        logger.log(Level.WARNING, "Count not initialize tag nodes, ", ex);
      }
    }
Example #3
0
  /**
   * Get the artifact for a result tag.
   *
   * @param tagArtifactId artifact id of the tag
   * @return the tag's artifact
   */
  static BlackboardArtifact getArtifactFromTag(long tagArtifactId) {
    try {
      Case currentCase = Case.getCurrentCase();
      SleuthkitCase skCase = currentCase.getSleuthkitCase();

      BlackboardArtifact artifact = skCase.getBlackboardArtifact(tagArtifactId);
      if (artifact.getArtifactTypeID() == BlackboardArtifact.ARTIFACT_TYPE.TSK_TAG_FILE.getTypeID()
          || artifact.getArtifactTypeID()
              == BlackboardArtifact.ARTIFACT_TYPE.TSK_TAG_ARTIFACT.getTypeID()) {
        List<BlackboardAttribute> attributes = artifact.getAttributes();
        for (BlackboardAttribute att : attributes) {
          if (att.getAttributeTypeID()
              == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TAGGED_ARTIFACT.getTypeID()) {
            return skCase.getBlackboardArtifact(att.getValueLong());
          }
        }
      }
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Failed to get artifact " + tagArtifactId + " from case.");
    }

    return null;
  }
Example #4
0
  /**
   * Can a thumbnail be generated for the content?
   *
   * @param content
   * @return
   */
  public static boolean thumbnailSupported(Content content) {
    if (content instanceof AbstractFile == false) {
      return false;
    }

    AbstractFile f = (AbstractFile) content;
    if (f.getSize() == 0) {
      return false;
    }

    // check the blackboard for a file type attribute
    try {
      ArrayList<BlackboardAttribute> attributes =
          f.getGenInfoAttributes(ATTRIBUTE_TYPE.TSK_FILE_TYPE_SIG);
      for (BlackboardAttribute attribute : attributes) {
        if (SUPP_MIME_TYPES.contains(attribute.getValueString())) {
          return true;
        }
      }
    } catch (TskCoreException ex) {
      logger.log(
          Level.WARNING, "Error while getting file signature from blackboard.", ex); // NON-NLS
    }

    final String extension = f.getNameExtension();

    // if we have an extension, check it
    if (extension.equals("") == false) {
      // Note: thumbnail generator only supports JPG, GIF, and PNG for now
      if (SUPP_EXTENSIONS.contains(extension)) {
        return true;
      }
    }

    // if no extension or one that is not for an image, then read the content
    return isJpegFileHeader(f);
  }
Example #5
0
  /**
   * Looks up the tag names associated with either a tagged artifact or a tag artifact.
   *
   * @param artifactID The ID of the artifact
   * @param artifactTypeID The ID of the artifact type
   * @return A set of unique tag names
   */
  public static HashSet<String> getUniqueTagNames(long artifactID, int artifactTypeID) {
    HashSet<String> tagNames = new HashSet<>();

    try {
      ArrayList<Long> tagArtifactIDs = new ArrayList<>();
      if (artifactTypeID == ARTIFACT_TYPE.TSK_TAG_FILE.getTypeID()
          || artifactTypeID == ARTIFACT_TYPE.TSK_TAG_ARTIFACT.getTypeID()) {
        tagArtifactIDs.add(artifactID);
      } else {
        List<BlackboardArtifact> tags =
            Case.getCurrentCase()
                .getSleuthkitCase()
                .getBlackboardArtifacts(ATTRIBUTE_TYPE.TSK_TAGGED_ARTIFACT, artifactID);
        for (BlackboardArtifact tag : tags) {
          tagArtifactIDs.add(tag.getArtifactID());
        }
      }

      for (Long tagArtifactID : tagArtifactIDs) {
        String whereClause =
            "WHERE artifact_id = "
                + tagArtifactID
                + " AND attribute_type_id = "
                + ATTRIBUTE_TYPE.TSK_TAG_NAME.getTypeID();
        List<BlackboardAttribute> attributes =
            Case.getCurrentCase().getSleuthkitCase().getMatchingAttributes(whereClause);
        for (BlackboardAttribute attr : attributes) {
          tagNames.add(attr.getValueString());
        }
      }
    } catch (TskCoreException ex) {
      logger.log(Level.SEVERE, "Failed to get tags for artifact " + artifactID, ex);
    }

    return tagNames;
  }
  @Override
  public void viewArtifact(final BlackboardArtifact art) {
    BlackboardArtifact.ARTIFACT_TYPE type =
        BlackboardArtifact.ARTIFACT_TYPE.fromID(art.getArtifactTypeID());
    Children rootChilds = em.getRootContext().getChildren();
    Node treeNode = null;
    Node resultsNode = rootChilds.findChild(ResultsNode.NAME);
    Children resultsChilds = resultsNode.getChildren();
    if (type.equals(BlackboardArtifact.ARTIFACT_TYPE.TSK_HASHSET_HIT)) {
      Node hashsetRootNode = resultsChilds.findChild(type.getLabel());
      Children hashsetRootChilds = hashsetRootNode.getChildren();
      try {
        String setName = null;
        List<BlackboardAttribute> attributes = art.getAttributes();
        for (BlackboardAttribute att : attributes) {
          int typeId = att.getAttributeTypeID();
          if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
            setName = att.getValueString();
          }
        }
        treeNode = hashsetRootChilds.findChild(setName);
      } catch (TskException ex) {
        logger.log(Level.WARNING, "Error retrieving attributes", ex); // NON-NLS
      }
    } else if (type.equals(BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT)) {
      Node keywordRootNode = resultsChilds.findChild(type.getLabel());
      Children keywordRootChilds = keywordRootNode.getChildren();
      try {
        String listName = null;
        String keywordName = null;
        List<BlackboardAttribute> attributes = art.getAttributes();
        for (BlackboardAttribute att : attributes) {
          int typeId = att.getAttributeTypeID();
          if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
            listName = att.getValueString();
          } else if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID()) {
            keywordName = att.getValueString();
          }
        }
        Node listNode = keywordRootChilds.findChild(listName);
        Children listChildren = listNode.getChildren();
        treeNode = listChildren.findChild(keywordName);
      } catch (TskException ex) {
        logger.log(Level.WARNING, "Error retrieving attributes", ex); // NON-NLS
      }
    } else if (type.equals(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_FILE_HIT)
        || type.equals(BlackboardArtifact.ARTIFACT_TYPE.TSK_INTERESTING_ARTIFACT_HIT)) {
      Node interestingItemsRootNode = resultsChilds.findChild(type.getLabel());
      Children interestingItemsRootChildren = interestingItemsRootNode.getChildren();
      try {
        String setName = null;
        List<BlackboardAttribute> attributes = art.getAttributes();
        for (BlackboardAttribute att : attributes) {
          int typeId = att.getAttributeTypeID();
          if (typeId == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID()) {
            setName = att.getValueString();
          }
        }
        treeNode = interestingItemsRootChildren.findChild(setName);
      } catch (TskException ex) {
        logger.log(Level.WARNING, "Error retrieving attributes", ex); // NON-NLS
      }
    } else {
      Node extractedContent = resultsChilds.findChild(ExtractedContent.NAME);
      Children extractedChilds = extractedContent.getChildren();
      treeNode = extractedChilds.findChild(type.getLabel());
    }
    try {
      em.setExploredContextAndSelection(treeNode, new Node[] {treeNode});
    } catch (PropertyVetoException ex) {
      logger.log(Level.WARNING, "Property Veto: ", ex); // NON-NLS
    }

    // Another thread is needed because we have to wait for dataResult to populate
    EventQueue.invokeLater(
        new Runnable() {
          @Override
          public void run() {
            Children resultChilds = dataResult.getRootNode().getChildren();
            Node select = resultChilds.findChild(Long.toString(art.getArtifactID()));
            if (select != null) {
              dataResult.requestActive();
              dataResult.setSelectedNodes(new Node[] {select});
              fireViewerComplete();
            }
          }
        });
  }
Example #7
0
  @Override
  public synchronized ObservableResult evaluate() {

    setWarnings("");

    if (obj.getAddressValue() == null) {
      return new ObservableResult(
          id,
          "AddressObject: No address value field found", // NON-NLS
          spacing,
          ObservableResult.ObservableState.INDETERMINATE,
          null);
    }

    String origAddressStr = obj.getAddressValue().getValue().toString();

    // For now, we don't support "NONE" because it honestly doesn't seem like it
    // would ever appear in practice.
    if (((obj.getAddressValue().getApplyCondition() != null)
        && (obj.getAddressValue().getApplyCondition() == ConditionApplicationEnum.NONE))) {
      return new ObservableResult(
          id,
          "AddressObject: Can not process apply condition "
              + obj.getAddressValue().getApplyCondition().toString() // NON-NLS
              + " on Address object",
          spacing,
          ObservableResult.ObservableState.INDETERMINATE,
          null); // NON-NLS
    }

    // Set warnings for any unsupported fields
    setUnsupportedFieldWarnings();

    Case case1 = Case.getCurrentCase();
    SleuthkitCase sleuthkitCase = case1.getSleuthkitCase();

    try {
      // Need to check that every part of the string had at least one match
      // in the AND case
      boolean everyPartMatched = true;
      List<BlackboardArtifact> combinedArts = new ArrayList<BlackboardArtifact>();
      String searchString = "";
      String[] parts = origAddressStr.split("##comma##"); // NON-NLS

      for (String addressStr : parts) {

        // Update the string to show in the results
        if (!searchString.isEmpty()) {

          if ((obj.getAddressValue().getApplyCondition() != null)
              && (obj.getAddressValue().getApplyCondition() == ConditionApplicationEnum.ALL)) {
            searchString += " AND "; // NON-NLS
          } else {
            searchString += " OR "; // NON-NLS
          }
        }
        searchString += addressStr;

        if ((obj.getAddressValue().getCondition() == null)
            || (obj.getAddressValue().getCondition() == ConditionTypeEnum.EQUALS)) {
          List<BlackboardArtifact> arts =
              sleuthkitCase.getBlackboardArtifacts(
                  BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT,
                  BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD,
                  addressStr);

          if (arts.isEmpty()) {
            everyPartMatched = false;
          } else {
            combinedArts.addAll(arts);
          }

        } else {
          // This is inefficient, but the easiest way to do it.

          List<BlackboardArtifact> finalHits = new ArrayList<BlackboardArtifact>();

          // Get all the URL artifacts
          List<BlackboardArtifact> artList =
              sleuthkitCase.getBlackboardArtifacts(
                  BlackboardArtifact.ARTIFACT_TYPE.TSK_KEYWORD_HIT);

          for (BlackboardArtifact art : artList) {

            for (BlackboardAttribute attr : art.getAttributes()) {
              if (attr.getAttributeTypeID()
                  == BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID()) {
                if (compareStringObject(
                    addressStr,
                    obj.getAddressValue().getCondition(),
                    obj.getAddressValue().getApplyCondition(),
                    attr.getValueString())) {
                  finalHits.add(art);
                }
              }
            }
          }

          if (finalHits.isEmpty()) {
            everyPartMatched = false;
          } else {
            combinedArts.addAll(finalHits);
          }
        }
      }

      // If we're in the ALL case, make sure every piece matched
      if ((obj.getAddressValue().getApplyCondition() != null)
          && (obj.getAddressValue().getApplyCondition() == ConditionApplicationEnum.ALL)
          && (!everyPartMatched)) {
        return new ObservableResult(
            id,
            "AddressObject: No matches for " + searchString, // NON-NLS
            spacing,
            ObservableResult.ObservableState.FALSE,
            null);
      }

      if (!combinedArts.isEmpty()) {
        List<StixArtifactData> artData = new ArrayList<StixArtifactData>();
        for (BlackboardArtifact a : combinedArts) {
          artData.add(new StixArtifactData(a.getObjectID(), id, "AddressObject")); // NON-NLS
        }
        return new ObservableResult(
            id,
            "AddressObject: Found a match for " + searchString, // NON-NLS
            spacing,
            ObservableResult.ObservableState.TRUE,
            artData);
      }

      return new ObservableResult(
          id,
          "AddressObject: Found no matches for " + searchString, // NON-NLS
          spacing,
          ObservableResult.ObservableState.FALSE,
          null);

    } catch (TskCoreException ex) {
      return new ObservableResult(
          id,
          "AddressObject: Exception during evaluation: " + ex.getLocalizedMessage(), // NON-NLS
          spacing,
          ObservableResult.ObservableState.INDETERMINATE,
          null);
    }
  }
    @Override
    protected Object doInBackground() throws Exception {
      logger.log(Level.INFO, "Pending start of new searcher");

      final String displayName = "Keyword Search" + (finalRun ? " - Finalizing" : "");
      progress =
          ProgressHandleFactory.createHandle(
              displayName + (" (Pending)"),
              new Cancellable() {

                @Override
                public boolean cancel() {
                  logger.log(Level.INFO, "Cancelling the searcher by user.");
                  if (progress != null) {
                    progress.setDisplayName(displayName + " (Cancelling...)");
                  }
                  return Searcher.this.cancel(true);
                }
              });

      progress.start();
      progress.switchToIndeterminate();

      // block to ensure previous searcher is completely done with doInBackground()
      // even after previous searcher cancellation, we need to check this
      searcherLock.lock();
      try {
        logger.log(Level.INFO, "Started a new searcher");
        progress.setDisplayName(displayName);
        // make sure other searchers are not spawned
        searcherDone = false;
        runSearcher = false;
        if (searchTimer.isRunning()) {
          searchTimer.stop();
        }

        int numSearched = 0;

        updateKeywords();
        progress.switchToDeterminate(keywords.size());

        for (Keyword keywordQuery : keywords) {
          if (this.isCancelled()) {
            logger.log(
                Level.INFO,
                "Cancel detected, bailing before new keyword processed: "
                    + keywordQuery.getQuery());
            return null;
          }
          final String queryStr = keywordQuery.getQuery();
          final KeywordSearchList list = keywordToList.get(queryStr);
          final String listName = list.getName();

          // DEBUG
          // logger.log(Level.INFO, "Searching: " + queryStr);

          progress.progress(queryStr, numSearched);

          KeywordSearchQuery del = null;

          boolean isRegex = !keywordQuery.isLiteral();
          if (!isRegex) {
            del = new LuceneQuery(keywordQuery);
            del.escape();
          } else {
            del = new TermComponentQuery(keywordQuery);
          }

          Map<String, List<ContentHit>> queryResult = null;

          try {
            queryResult = del.performQuery();
          } catch (NoOpenCoreException ex) {
            logger.log(Level.WARNING, "Error performing query: " + keywordQuery.getQuery(), ex);
            // no reason to continue with next query if recovery failed
            // or wait for recovery to kick in and run again later
            // likely case has closed and threads are being interrupted
            return null;
          } catch (CancellationException e) {
            logger.log(
                Level.INFO,
                "Cancel detected, bailing during keyword query: " + keywordQuery.getQuery());
            return null;
          } catch (Exception e) {
            logger.log(Level.WARNING, "Error performing query: " + keywordQuery.getQuery(), e);
            continue;
          }

          // calculate new results but substracting results already obtained in this run
          Map<Keyword, List<ContentHit>> newResults = new HashMap<Keyword, List<ContentHit>>();

          for (String termResult : queryResult.keySet()) {
            List<ContentHit> queryTermResults = queryResult.get(termResult);
            Keyword termResultK = new Keyword(termResult, !isRegex);
            List<ContentHit> curTermResults = currentResults.get(termResultK);
            if (curTermResults == null) {
              currentResults.put(termResultK, queryTermResults);
              newResults.put(termResultK, queryTermResults);
            } else {
              // some AbstractFile hits already exist for this keyword
              for (ContentHit res : queryTermResults) {
                if (!previouslyHit(curTermResults, res)) {
                  // add to new results
                  List<ContentHit> newResultsFs = newResults.get(termResultK);
                  if (newResultsFs == null) {
                    newResultsFs = new ArrayList<ContentHit>();
                    newResults.put(termResultK, newResultsFs);
                  }
                  newResultsFs.add(res);
                  curTermResults.add(res);
                }
              }
            }
          }

          if (!newResults.isEmpty()) {

            // write results to BB

            // new artifacts created, to report to listeners
            Collection<BlackboardArtifact> newArtifacts = new ArrayList<BlackboardArtifact>();

            for (final Keyword hitTerm : newResults.keySet()) {
              List<ContentHit> contentHitsAll = newResults.get(hitTerm);
              Map<AbstractFile, Integer> contentHitsFlattened =
                  ContentHit.flattenResults(contentHitsAll);
              for (final AbstractFile hitFile : contentHitsFlattened.keySet()) {
                String snippet = null;
                final String snippetQuery =
                    KeywordSearchUtil.escapeLuceneQuery(hitTerm.getQuery(), true, false);
                int chunkId = contentHitsFlattened.get(hitFile);
                try {
                  snippet =
                      LuceneQuery.querySnippet(
                          snippetQuery, hitFile.getId(), chunkId, isRegex, true);
                } catch (NoOpenCoreException e) {
                  logger.log(Level.WARNING, "Error querying snippet: " + snippetQuery, e);
                  // no reason to continue
                  return null;
                } catch (Exception e) {
                  logger.log(Level.WARNING, "Error querying snippet: " + snippetQuery, e);
                  continue;
                }

                KeywordWriteResult written =
                    del.writeToBlackBoard(hitTerm.getQuery(), hitFile, snippet, listName);

                if (written == null) {
                  logger.log(
                      Level.WARNING,
                      "BB artifact for keyword hit not written, file: "
                          + hitFile
                          + ", hit: "
                          + hitTerm.toString());
                  continue;
                }

                newArtifacts.add(written.getArtifact());

                // generate a data message for each artifact
                StringBuilder subjectSb = new StringBuilder();
                StringBuilder detailsSb = new StringBuilder();
                // final int hitFiles = newResults.size();

                if (!keywordQuery.isLiteral()) {
                  subjectSb.append("RegExp hit: ");
                } else {
                  subjectSb.append("Keyword hit: ");
                }
                // subjectSb.append("<");
                String uniqueKey = null;
                BlackboardAttribute attr =
                    written.getAttribute(
                        BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD.getTypeID());
                if (attr != null) {
                  final String keyword = attr.getValueString();
                  subjectSb.append(keyword);
                  uniqueKey = keyword.toLowerCase();
                }

                // subjectSb.append(">");
                // String uniqueKey = queryStr;

                // details
                detailsSb.append("<table border='0' cellpadding='4' width='280'>");
                // hit
                detailsSb.append("<tr>");
                detailsSb.append("<th>Keyword hit</th>");
                detailsSb
                    .append("<td>")
                    .append(StringEscapeUtils.escapeHtml(attr.getValueString()))
                    .append("</td>");
                detailsSb.append("</tr>");

                // preview
                attr =
                    written.getAttribute(
                        BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW.getTypeID());
                if (attr != null) {
                  detailsSb.append("<tr>");
                  detailsSb.append("<th>Preview</th>");
                  detailsSb
                      .append("<td>")
                      .append(StringEscapeUtils.escapeHtml(attr.getValueString()))
                      .append("</td>");
                  detailsSb.append("</tr>");
                }

                // file
                detailsSb.append("<tr>");
                detailsSb.append("<th>File</th>");
                if (hitFile.getType().equals(TskData.TSK_DB_FILES_TYPE_ENUM.FS)) {
                  detailsSb
                      .append("<td>")
                      .append(((FsContent) hitFile).getParentPath())
                      .append(hitFile.getName())
                      .append("</td>");
                } else {
                  detailsSb.append("<td>").append(hitFile.getName()).append("</td>");
                }
                detailsSb.append("</tr>");

                // list
                attr =
                    written.getAttribute(
                        BlackboardAttribute.ATTRIBUTE_TYPE.TSK_SET_NAME.getTypeID());
                detailsSb.append("<tr>");
                detailsSb.append("<th>List</th>");
                detailsSb.append("<td>").append(attr.getValueString()).append("</td>");
                detailsSb.append("</tr>");

                // regex
                if (!keywordQuery.isLiteral()) {
                  attr =
                      written.getAttribute(
                          BlackboardAttribute.ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP.getTypeID());
                  if (attr != null) {
                    detailsSb.append("<tr>");
                    detailsSb.append("<th>RegEx</th>");
                    detailsSb.append("<td>").append(attr.getValueString()).append("</td>");
                    detailsSb.append("</tr>");
                  }
                }
                detailsSb.append("</table>");

                // check if should send messages on hits on this list
                if (list.getIngestMessages()) // post ingest inbox msg
                {
                  managerProxy.postMessage(
                      IngestMessage.createDataMessage(
                          ++messageID,
                          instance,
                          subjectSb.toString(),
                          detailsSb.toString(),
                          uniqueKey,
                          written.getArtifact()));
                }
              } // for each term hit
            } // for each file hit

            // update artifact browser
            if (!newArtifacts.isEmpty()) {
              IngestManager.fireServiceDataEvent(
                  new ServiceDataEvent(MODULE_NAME, ARTIFACT_TYPE.TSK_KEYWORD_HIT, newArtifacts));
            }
          }
          progress.progress(queryStr, ++numSearched);
        }

      } // end try block
      catch (Exception ex) {
        logger.log(Level.WARNING, "searcher exception occurred", ex);
      } finally {
        finalizeSearcher();
        searcherLock.unlock();
      }

      return null;
    }
  @Override
  public KeywordCachedArtifact writeSingleFileHitsToBlackBoard(
      String termHit, KeywordHit hit, String snippet, String listName) {
    BlackboardArtifact newArtifact;

    Collection<BlackboardAttribute> attributes = new ArrayList<>();
    if (keyword.getType() == ATTRIBUTE_TYPE.TSK_CARD_NUMBER) {
      attributes.add(
          new BlackboardAttribute(
              ATTRIBUTE_TYPE.TSK_ACCOUNT_TYPE, MODULE_NAME, Account.Type.CREDIT_CARD.name()));

      Map<BlackboardAttribute.Type, BlackboardAttribute> parsedTrackAttributeMap = new HashMap<>();

      // try to match it against the track 1 regex
      Matcher matcher = TRACK1_PATTERN.matcher(hit.getSnippet());
      if (matcher.find()) {
        parseTrack1Data(parsedTrackAttributeMap, matcher);
      }

      // then try to match it against the track 2 regex
      matcher = TRACK2_PATTERN.matcher(hit.getSnippet());
      if (matcher.find()) {
        parseTrack2Data(parsedTrackAttributeMap, matcher);
      }

      // if we couldn't parse the CCN abort this artifact
      final BlackboardAttribute ccnAttribute =
          parsedTrackAttributeMap.get(new BlackboardAttribute.Type(ATTRIBUTE_TYPE.TSK_CARD_NUMBER));
      if (ccnAttribute == null || StringUtils.isBlank(ccnAttribute.getValueString())) {
        if (hit.isArtifactHit()) {
          LOGGER.log(
              Level.SEVERE,
              String.format(
                  "Failed to parse credit card account number for artifact keyword hit: term = %s, snippet = '%s', artifact id = %d",
                  termHit, hit.getSnippet(), hit.getArtifact().getArtifactID()));
        } else {
          LOGGER.log(
              Level.SEVERE,
              String.format(
                  "Failed to parse credit card account number for content keyword hit: term = %s, snippet = '%s', object id = %d",
                  termHit, hit.getSnippet(), hit.getContent().getId()));
        }
        return null;
      }

      attributes.addAll(parsedTrackAttributeMap.values());

      // look up the bank name, schem, etc from the BIN
      final int bin = Integer.parseInt(ccnAttribute.getValueString().substring(0, 8));
      CreditCards.BankIdentificationNumber binInfo = CreditCards.getBINInfo(bin);
      if (binInfo != null) {
        binInfo
            .getScheme()
            .ifPresent(
                scheme ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_CARD_SCHEME, MODULE_NAME, scheme)));
        binInfo
            .getCardType()
            .ifPresent(
                cardType ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_CARD_TYPE, MODULE_NAME, cardType)));
        binInfo
            .getBrand()
            .ifPresent(
                brand ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_BRAND_NAME, MODULE_NAME, brand)));
        binInfo
            .getBankName()
            .ifPresent(
                bankName ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_BANK_NAME, MODULE_NAME, bankName)));
        binInfo
            .getBankPhoneNumber()
            .ifPresent(
                phoneNumber ->
                    attributes.add(
                        new BlackboardAttribute(
                            ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, MODULE_NAME, phoneNumber)));
        binInfo
            .getBankURL()
            .ifPresent(
                url ->
                    attributes.add(
                        new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_URL, MODULE_NAME, url)));
        binInfo
            .getCountry()
            .ifPresent(
                country ->
                    attributes.add(
                        new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_COUNTRY, MODULE_NAME, country)));
        binInfo
            .getBankCity()
            .ifPresent(
                city ->
                    attributes.add(
                        new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_CITY, MODULE_NAME, city)));
      }

      /* if the hit is from unused or unalocated blocks, record the
       * KEYWORD_SEARCH_DOCUMENT_ID, so we can show just that chunk in the
       * UI
       */
      if (hit.getContent() instanceof AbstractFile) {
        AbstractFile file = (AbstractFile) hit.getContent();
        if (file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNUSED_BLOCKS
            || file.getType() == TskData.TSK_DB_FILES_TYPE_ENUM.UNALLOC_BLOCKS) {
          attributes.add(
              new BlackboardAttribute(
                  KEYWORD_SEARCH_DOCUMENT_ID, MODULE_NAME, hit.getSolrDocumentId()));
        }
      }

      // make account artifact
      try {
        newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_ACCOUNT);
      } catch (TskCoreException tskCoreException) {
        LOGGER.log(
            Level.SEVERE, "Error adding bb artifact for account", tskCoreException); // NON-NLS
        return null;
      }
    } else {

      // regex match
      attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD, MODULE_NAME, termHit));
      // regex keyword
      attributes.add(
          new BlackboardAttribute(
              ATTRIBUTE_TYPE.TSK_KEYWORD_REGEXP, MODULE_NAME, keyword.getQuery()));

      // make keyword hit artifact
      try {
        newArtifact = hit.getContent().newArtifact(ARTIFACT_TYPE.TSK_KEYWORD_HIT);

      } catch (TskCoreException tskCoreException) {
        LOGGER.log(
            Level.SEVERE, "Error adding bb artifact for keyword hit", tskCoreException); // NON-NLS
        return null;
      }
    }
    if (StringUtils.isNotBlank(listName)) {
      attributes.add(new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_SET_NAME, MODULE_NAME, listName));
    }
    // preview
    if (snippet != null) {
      attributes.add(
          new BlackboardAttribute(ATTRIBUTE_TYPE.TSK_KEYWORD_PREVIEW, MODULE_NAME, snippet));
    }

    if (hit.isArtifactHit()) {
      attributes.add(
          new BlackboardAttribute(
              ATTRIBUTE_TYPE.TSK_ASSOCIATED_ARTIFACT,
              MODULE_NAME,
              hit.getArtifact().getArtifactID()));
    }

    try {
      // TODO: do we still/really need this KeywordCachedArtifact class?
      newArtifact.addAttributes(attributes);
      KeywordCachedArtifact writeResult = new KeywordCachedArtifact(newArtifact);
      writeResult.add(attributes);
      return writeResult;
    } catch (TskCoreException e) {
      LOGGER.log(
          Level.SEVERE, "Error adding bb attributes for terms search artifact", e); // NON-NLS
      return null;
    }
  }