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)