19 package org.sleuthkit.autopsy.datasourceprocessors.xry;
 
   21 import java.io.IOException;
 
   22 import java.nio.file.Path;
 
   23 import java.time.format.DateTimeParseException;
 
   24 import java.util.ArrayList;
 
   25 import java.util.Collection;
 
   26 import java.util.HashSet;
 
   27 import java.util.List;
 
   28 import java.util.Objects;
 
   29 import java.util.Optional;
 
   31 import java.util.logging.Level;
 
   34 import org.
sleuthkit.datamodel.Blackboard.BlackboardException;
 
   37 import org.
sleuthkit.datamodel.InvalidAccountIDException;
 
   40 import org.
sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper;
 
   41 import org.
sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper.CommunicationDirection;
 
   42 import org.
sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper.MessageReadStatus;
 
   47 final class XRYMessagesFileParser 
implements XRYFileParser {
 
   49     private static final Logger logger = Logger.getLogger(
 
   50             XRYMessagesFileParser.class.getName());
 
   52     private static final String PARSER_NAME = 
"XRY DSP";
 
   59         DELETED(
"deleted", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED),
 
   62         NAME_MATCHED(
"name (matched)", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON),
 
   79         private final BlackboardAttribute.ATTRIBUTE_TYPE 
type;
 
   81         XryKey(String name, BlackboardAttribute.ATTRIBUTE_TYPE type) {
 
   86         public BlackboardAttribute.ATTRIBUTE_TYPE 
getType() {
 
  104             } 
catch (IllegalArgumentException ex) {
 
  120             String normalizedName = name.trim().toLowerCase();
 
  122                 if (normalizedName.equals(keyChoice.name)) {
 
  127             throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found." 
  128                     + 
" All keys should be tested with contains.", name));
 
  154         public static boolean contains(String xryNamespace) {
 
  158             } 
catch (IllegalArgumentException ex) {
 
  175             String normalizedNamespace = xryNamespace.trim().toLowerCase();
 
  177                 if (normalizedNamespace.equals(keyChoice.name)) {
 
  182             throw new IllegalArgumentException(String.format(
"Namespace [%s] was not found." 
  183                     + 
" All namespaces should be tested with contains.", xryNamespace));
 
  215             } 
catch (IllegalArgumentException ex) {
 
  231             String normalizedName = name.trim().toLowerCase();
 
  233                 if (normalizedName.equals(keyChoice.name)) {
 
  238             throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found." 
  239                     + 
" All keys should be tested with contains.", name));
 
  263     public void parse(XRYFileReader reader, Content parent, SleuthkitCase currentCase) 
throws IOException, TskCoreException, BlackboardException {
 
  264         Path reportPath = reader.getReportPath();
 
  265         logger.log(Level.INFO, String.format(
"[XRY DSP] Processing report at" 
  266                 + 
" [ %s ]", reportPath.toString()));
 
  269         Set<Integer> referenceNumbersSeen = 
new HashSet<>();
 
  271         while (reader.hasNextEntity()) {
 
  272             String xryEntity = reader.nextEntity();
 
  275             List<XRYKeyValuePair> pairs = getXRYKeyValuePairs(xryEntity, reader, referenceNumbersSeen);
 
  279             final String messageType = PARSER_NAME;
 
  280             CommunicationDirection direction = CommunicationDirection.UNKNOWN;
 
  281             String senderId = null;
 
  282             final List<String> recipientIdsList = 
new ArrayList<>();
 
  284             MessageReadStatus readStatus = MessageReadStatus.UNKNOWN;
 
  285             final String subject = null;
 
  287             final String threadId = null;
 
  288             final Collection<BlackboardAttribute> otherAttributes = 
new ArrayList<>();
 
  290             for(XRYKeyValuePair pair : pairs) {
 
  291                 XryNamespace 
namespace = XryNamespace.NONE;
 
  292                 if (XryNamespace.contains(pair.getNamespace())) {
 
  293                     namespace = XryNamespace.fromDisplayName(pair.getNamespace());
 
  295                 XryKey key = XryKey.fromDisplayName(pair.getKey());
 
  296                 String normalizedValue = pair.getValue().toLowerCase().trim();
 
  301                         if(!XRYUtils.isPhoneValid(pair.getValue())) {
 
  306                         if(
namespace == XryNamespace.FROM || direction == CommunicationDirection.INCOMING) {
 
  307                             senderId = pair.getValue();
 
  308                         } 
else if(
namespace == XryNamespace.TO || direction == CommunicationDirection.OUTGOING) {
 
  309                             recipientIdsList.add(pair.getValue());
 
  312                                 currentCase.getCommunicationsManager().createAccountFileInstance(
 
  313                                         Account.Type.PHONE, pair.getValue(), PARSER_NAME, parent);
 
  314                             } 
catch (InvalidAccountIDException ex) {
 
  315                                 logger.log(Level.WARNING, String.format(
"Invalid account identifier %s", pair.getValue()), ex);
 
  318                             otherAttributes.add(
new BlackboardAttribute(
 
  319                                         BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
 
  320                                         PARSER_NAME, pair.getValue()));
 
  326                         if(!XRYUtils.isPhoneValid(pair.getValue())) {
 
  330                         senderId = pair.getValue();
 
  333                         if(!XRYUtils.isPhoneValid(pair.getValue())) {
 
  337                         recipientIdsList.add(pair.getValue());
 
  342                             long dateTimeSinceInEpoch = XRYUtils.calculateSecondsSinceEpoch(pair.getValue());
 
  343                             dateTime = dateTimeSinceInEpoch;
 
  344                         } 
catch (DateTimeParseException ex) {
 
  345                             logger.log(Level.WARNING, String.format(
"[%s] Assumption" 
  346                                     + 
" about the date time formatting of messages is " 
  347                                     + 
"not right. Here is the pair [ %s ]", PARSER_NAME, pair), ex);
 
  351                         switch (normalizedValue) {
 
  353                                 direction = CommunicationDirection.INCOMING;
 
  356                                 direction = CommunicationDirection.OUTGOING;
 
  360                             case "status report":
 
  364                                 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized " 
  365                                         + 
" value for key pair [ %s ].", PARSER_NAME, pair));
 
  369                         switch (normalizedValue) {
 
  371                                 readStatus = MessageReadStatus.READ;
 
  374                                 readStatus = MessageReadStatus.UNREAD;
 
  377                                 otherAttributes.add(
new BlackboardAttribute(
 
  378                                         BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED,
 
  379                                         PARSER_NAME, pair.getValue()));
 
  381                             case "sending failed":
 
  387                                 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized " 
  388                                         + 
" value for key pair [ %s ].", PARSER_NAME, pair));
 
  393                         text = pair.getValue();
 
  396                         switch (normalizedValue) {
 
  398                                 direction = CommunicationDirection.INCOMING;
 
  401                                 direction = CommunicationDirection.OUTGOING;
 
  404                                 direction = CommunicationDirection.UNKNOWN;
 
  409                         if(!XRYUtils.isPhoneValid(pair.getValue())) {
 
  413                         otherAttributes.add(
new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, 
 
  414                                 PARSER_NAME, pair.getValue()));
 
  419                         if (key.getType() != null) {
 
  420                             otherAttributes.add(
new BlackboardAttribute(key.getType(),
 
  421                                     PARSER_NAME, pair.getValue()));
 
  423                             logger.log(Level.INFO, String.format(
"[%s] Key value pair " 
  424                                     + 
"(in brackets) [ %s ] was recognized but " 
  425                                     + 
"more data or time is needed to finish implementation. Discarding... ",
 
  431             CommunicationArtifactsHelper helper = 
new CommunicationArtifactsHelper(
 
  432                 currentCase, PARSER_NAME, parent, Account.Type.PHONE);
 
  434             helper.addMessage(messageType, direction, senderId, recipientIdsList, 
 
  435                 dateTime, readStatus, subject, text, threadId, otherAttributes);
 
  443     private List<XRYKeyValuePair> getXRYKeyValuePairs(String xryEntity,
 
  444             XRYFileReader reader, Set<Integer> referenceValues) 
throws IOException {
 
  445         String[] xryLines = xryEntity.split(
"\n");
 
  447         logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ]", xryLines[0]));
 
  449         List<XRYKeyValuePair> pairs = 
new ArrayList<>();
 
  452         int keyCount = getCountOfKeyValuePairs(xryLines);
 
  453         for (
int i = 1; i <= keyCount; i++) {
 
  456             XRYKeyValuePair pair = getKeyValuePairByIndex(xryLines, i).get();
 
  457             if (XryMetaKey.contains(pair.getKey())) {
 
  462             if (!XryKey.contains(pair.getKey())) {
 
  463                 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key, " 
  464                         + 
"value pair (in brackets) [ %s ], " 
  465                         + 
"was not recognized. Discarding...", pair));
 
  469             if (pair.getValue().isEmpty()) {
 
  470                 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key " 
  471                         + 
"(in brackets) [ %s ] was recognized, but the value " 
  472                         + 
"was empty. Discarding...", pair.getKey()));
 
  478             if (pair.hasKey(XryKey.TEXT.getDisplayName())
 
  479                     || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
 
  480                 String segmentedText = getSegmentedText(xryLines, reader, referenceValues);
 
  481                 pair = 
new XRYKeyValuePair(pair.getKey(),
 
  483                         pair.getValue() + 
" " + segmentedText,
 
  484                         pair.getNamespace());
 
  497     private Integer getCountOfKeyValuePairs(String[] xryEntity) {
 
  499         for (
int i = 1; i < xryEntity.length; i++) {
 
  500             if (XRYKeyValuePair.isPair(xryEntity[i])) {
 
  517     private String getSegmentedText(String[] xryEntity, XRYFileReader reader,
 
  518             Set<Integer> referenceNumbersSeen) 
throws IOException {
 
  519         Optional<Integer> referenceNumber = getMetaKeyValue(xryEntity, XryMetaKey.REFERENCE_NUMBER);
 
  521         if (!referenceNumber.isPresent()) {
 
  525         logger.log(Level.INFO, String.format(
"[XRY DSP] Message entity " 
  526                 + 
"appears to be segmented with reference number [ %d ]", referenceNumber.get()));
 
  528         if (referenceNumbersSeen.contains(referenceNumber.get())) {
 
  529             logger.log(Level.SEVERE, String.format(
"[XRY DSP] This reference [ %d ] has already " 
  530                     + 
"been seen. This means that the segments are not " 
  531                     + 
"contiguous. Any segments contiguous with this " 
  532                     + 
"one will be aggregated and another " 
  533                     + 
"(otherwise duplicate) artifact will be created.", referenceNumber.get()));
 
  536         referenceNumbersSeen.add(referenceNumber.get());
 
  538         Optional<Integer> segmentNumber = getMetaKeyValue(xryEntity, XryMetaKey.SEGMENT_NUMBER);
 
  539         if (!segmentNumber.isPresent()) {
 
  540             logger.log(Level.SEVERE, String.format(
"No segment " 
  541                     + 
"number was found on the message entity" 
  542                     + 
"with reference number [%d]", referenceNumber.get()));
 
  546         StringBuilder segmentedText = 
new StringBuilder();
 
  548         int currentSegmentNumber = segmentNumber.get();
 
  549         while (reader.hasNextEntity()) {
 
  551             String nextEntity = reader.peek();
 
  552             String[] nextEntityLines = nextEntity.split(
"\n");
 
  553             Optional<Integer> nextReferenceNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.REFERENCE_NUMBER);
 
  555             if (!nextReferenceNumber.isPresent()
 
  556                     || !Objects.equals(nextReferenceNumber, referenceNumber)) {
 
  565             Optional<Integer> nextSegmentNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.SEGMENT_NUMBER);
 
  567             logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ] " 
  568                     + 
"segment with reference number [ %d ]", nextEntityLines[0], referenceNumber.get()));
 
  570             if (!nextSegmentNumber.isPresent()) {
 
  571                 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Segment with reference" 
  572                         + 
" number [ %d ] did not have a segment number associated with it." 
  573                         + 
" It cannot be determined if the reconstructed text will be in order.", referenceNumber.get()));
 
  574             } 
else if (nextSegmentNumber.get() != currentSegmentNumber + 1) {
 
  575                 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Contiguous " 
  576                         + 
"segments are not ascending incrementally. Encountered " 
  577                         + 
"segment [ %d ] after segment [ %d ]. This means the reconstructed " 
  578                         + 
"text will be out of order.", nextSegmentNumber.get(), currentSegmentNumber));
 
  581             int keyCount = getCountOfKeyValuePairs(nextEntityLines);
 
  582             for (
int i = 1; i <= keyCount; i++) {
 
  583                 XRYKeyValuePair pair = getKeyValuePairByIndex(nextEntityLines, i).get();
 
  584                 if (pair.hasKey(XryKey.TEXT.getDisplayName())
 
  585                         || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
 
  586                     segmentedText.append(pair.getValue()).append(
' ');
 
  590             if (nextSegmentNumber.isPresent()) {
 
  591                 currentSegmentNumber = nextSegmentNumber.get();
 
  596         if (segmentedText.length() > 0) {
 
  597             segmentedText.setLength(segmentedText.length() - 1);
 
  600         return segmentedText.toString();
 
  610     private Optional<Integer> getMetaKeyValue(String[] xryLines, XryMetaKey metaKey) {
 
  611         for (String xryLine : xryLines) {
 
  612             if (!XRYKeyValuePair.isPair(xryLine)) {
 
  616             XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
 
  617             if (pair.hasKey(metaKey.getDisplayName())) {
 
  619                     return Optional.of(Integer.parseInt(pair.getValue()));
 
  620                 } 
catch (NumberFormatException ex) {
 
  621                     logger.log(Level.SEVERE, String.format(
"[XRY DSP] Value [ %s ] for " 
  622                             + 
"meta key [ %s ] was not an integer.", pair.getValue(), metaKey), ex);
 
  626         return Optional.empty();
 
  638     private Optional<XRYKeyValuePair> getKeyValuePairByIndex(String[] xryLines, 
int index) {
 
  640         String 
namespace = "";
 
  641         for (
int i = 1; i < xryLines.length; i++) {
 
  642             String xryLine = xryLines[i];
 
  643             if (XryNamespace.contains(xryLine)) {
 
  644                 namespace = xryLine.trim();
 
  648             if (!XRYKeyValuePair.isPair(xryLine)) {
 
  649                 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Expected a key value " 
  650                         + 
"pair on this line (in brackets) [ %s ], but one was not detected." 
  651                         + 
" Discarding...", xryLine));
 
  655             XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
 
  656             String value = pair.getValue();
 
  658             for (; (i + 1) < xryLines.length
 
  659                     && !XRYKeyValuePair.isPair(xryLines[i + 1])
 
  660                     && !XryNamespace.contains(xryLines[i + 1]); i++) {
 
  661                 String continuedValue = xryLines[i + 1].trim();
 
  663                 value = value + 
" " + continuedValue;
 
  666             pair = 
new XRYKeyValuePair(pair.getKey(), value, 
namespace);
 
  668             if (pairsParsed == index) {
 
  669                 return Optional.of(pair);
 
  673         return Optional.empty();
 
final BlackboardAttribute.ATTRIBUTE_TYPE type
 
XryNamespace(String name)
 
XryKey(String name, BlackboardAttribute.ATTRIBUTE_TYPE type)
 
static boolean contains(String xryNamespace)
 
BlackboardAttribute.ATTRIBUTE_TYPE getType()
 
static XryNamespace fromDisplayName(String xryNamespace)
 
static boolean contains(String name)
 
static XryKey fromDisplayName(String name)