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() {
105 }
catch (IllegalArgumentException ex) {
122 String normalizedName = name.trim().toLowerCase();
124 if (normalizedName.equals(keyChoice.name)) {
129 throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found."
130 +
" All keys should be tested with contains.", name));
157 public static boolean contains(String xryNamespace) {
161 }
catch (IllegalArgumentException ex) {
179 String normalizedNamespace = xryNamespace.trim().toLowerCase();
181 if (normalizedNamespace.equals(keyChoice.name)) {
186 throw new IllegalArgumentException(String.format(
"Namespace [%s] was not found."
187 +
" All namespaces should be tested with contains.", xryNamespace));
220 }
catch (IllegalArgumentException ex) {
237 String normalizedName = name.trim().toLowerCase();
239 if (normalizedName.equals(keyChoice.name)) {
244 throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found."
245 +
" All keys should be tested with contains.", name));
271 public void parse(XRYFileReader reader, Content parent, SleuthkitCase currentCase)
throws IOException, TskCoreException, BlackboardException {
272 Path reportPath = reader.getReportPath();
273 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing report at"
274 +
" [ %s ]", reportPath.toString()));
277 Set<Integer> referenceNumbersSeen =
new HashSet<>();
279 while (reader.hasNextEntity()) {
280 String xryEntity = reader.nextEntity();
283 List<XRYKeyValuePair> pairs = getXRYKeyValuePairs(xryEntity, reader, referenceNumbersSeen);
287 final String messageType = PARSER_NAME;
288 CommunicationDirection direction = CommunicationDirection.UNKNOWN;
289 String senderId = null;
290 final List<String> recipientIdsList =
new ArrayList<>();
292 MessageReadStatus readStatus = MessageReadStatus.UNKNOWN;
293 final String subject = null;
295 final String threadId = null;
296 final Collection<BlackboardAttribute> otherAttributes =
new ArrayList<>();
298 for (XRYKeyValuePair pair : pairs) {
299 XryNamespace
namespace = XryNamespace.NONE;
300 if (XryNamespace.contains(pair.getNamespace())) {
301 namespace = XryNamespace.fromDisplayName(pair.getNamespace());
303 XryKey key = XryKey.fromDisplayName(pair.getKey());
304 String normalizedValue = pair.getValue().toLowerCase().trim();
309 if (!XRYUtils.isPhoneValid(pair.getValue())) {
314 if (
namespace == XryNamespace.FROM || direction == CommunicationDirection.INCOMING) {
315 senderId = pair.getValue();
316 }
else if (
namespace == XryNamespace.TO || direction == CommunicationDirection.OUTGOING) {
317 recipientIdsList.add(pair.getValue());
320 currentCase.getCommunicationsManager().createAccountFileInstance(
321 Account.Type.PHONE, pair.getValue(), PARSER_NAME, parent, null, null);
322 }
catch (InvalidAccountIDException ex) {
323 logger.log(Level.WARNING, String.format(
"Invalid account identifier %s", pair.getValue()), ex);
326 otherAttributes.add(
new BlackboardAttribute(
327 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
328 PARSER_NAME, pair.getValue()));
334 if (!XRYUtils.isPhoneValid(pair.getValue())) {
338 senderId = pair.getValue();
341 if (!XRYUtils.isPhoneValid(pair.getValue())) {
345 recipientIdsList.add(pair.getValue());
350 long dateTimeSinceInEpoch = XRYUtils.calculateSecondsSinceEpoch(pair.getValue());
351 dateTime = dateTimeSinceInEpoch;
352 }
catch (DateTimeParseException ex) {
353 logger.log(Level.WARNING, String.format(
"[%s] Assumption"
354 +
" about the date time formatting of messages is "
355 +
"not right. Here is the pair [ %s ]", PARSER_NAME, pair), ex);
359 switch (normalizedValue) {
361 direction = CommunicationDirection.INCOMING;
364 direction = CommunicationDirection.OUTGOING;
368 case "status report":
372 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized "
373 +
" value for key pair [ %s ].", PARSER_NAME, pair));
377 switch (normalizedValue) {
379 readStatus = MessageReadStatus.READ;
382 readStatus = MessageReadStatus.UNREAD;
385 otherAttributes.add(
new BlackboardAttribute(
386 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED,
387 PARSER_NAME, pair.getValue()));
389 case "sending failed":
395 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized "
396 +
" value for key pair [ %s ].", PARSER_NAME, pair));
401 text = pair.getValue();
404 switch (normalizedValue) {
406 direction = CommunicationDirection.INCOMING;
409 direction = CommunicationDirection.OUTGOING;
412 direction = CommunicationDirection.UNKNOWN;
417 if (!XRYUtils.isPhoneValid(pair.getValue())) {
421 otherAttributes.add(
new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
422 PARSER_NAME, pair.getValue()));
427 if (key.getType() != null) {
428 otherAttributes.add(
new BlackboardAttribute(key.getType(),
429 PARSER_NAME, pair.getValue()));
431 logger.log(Level.INFO, String.format(
"[%s] Key value pair "
432 +
"(in brackets) [ %s ] was recognized but "
433 +
"more data or time is needed to finish implementation. Discarding... ",
439 CommunicationArtifactsHelper helper =
new CommunicationArtifactsHelper(
440 currentCase, PARSER_NAME, parent, Account.Type.PHONE, null);
442 helper.addMessage(messageType, direction, senderId, recipientIdsList,
443 dateTime, readStatus, subject, text, threadId, otherAttributes);
451 private List<XRYKeyValuePair> getXRYKeyValuePairs(String xryEntity,
452 XRYFileReader reader, Set<Integer> referenceValues)
throws IOException {
453 String[] xryLines = xryEntity.split(
"\n");
455 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ]", xryLines[0]));
457 List<XRYKeyValuePair> pairs =
new ArrayList<>();
460 int keyCount = getCountOfKeyValuePairs(xryLines);
461 for (
int i = 1; i <= keyCount; i++) {
464 XRYKeyValuePair pair = getKeyValuePairByIndex(xryLines, i).get();
465 if (XryMetaKey.contains(pair.getKey())) {
470 if (!XryKey.contains(pair.getKey())) {
471 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key, "
472 +
"value pair (in brackets) [ %s ], "
473 +
"was not recognized. Discarding...", pair));
477 if (pair.getValue().isEmpty()) {
478 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key "
479 +
"(in brackets) [ %s ] was recognized, but the value "
480 +
"was empty. Discarding...", pair.getKey()));
486 if (pair.hasKey(XryKey.TEXT.getDisplayName())
487 || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
488 String segmentedText = getSegmentedText(xryLines, reader, referenceValues);
489 pair =
new XRYKeyValuePair(pair.getKey(),
491 pair.getValue() +
" " + segmentedText,
492 pair.getNamespace());
505 private Integer getCountOfKeyValuePairs(String[] xryEntity) {
507 for (
int i = 1; i < xryEntity.length; i++) {
508 if (XRYKeyValuePair.isPair(xryEntity[i])) {
528 private String getSegmentedText(String[] xryEntity, XRYFileReader reader,
529 Set<Integer> referenceNumbersSeen)
throws IOException {
530 Optional<Integer> referenceNumber = getMetaKeyValue(xryEntity, XryMetaKey.REFERENCE_NUMBER);
532 if (!referenceNumber.isPresent()) {
536 logger.log(Level.INFO, String.format(
"[XRY DSP] Message entity "
537 +
"appears to be segmented with reference number [ %d ]", referenceNumber.get()));
539 if (referenceNumbersSeen.contains(referenceNumber.get())) {
540 logger.log(Level.SEVERE, String.format(
"[XRY DSP] This reference [ %d ] has already "
541 +
"been seen. This means that the segments are not "
542 +
"contiguous. Any segments contiguous with this "
543 +
"one will be aggregated and another "
544 +
"(otherwise duplicate) artifact will be created.", referenceNumber.get()));
547 referenceNumbersSeen.add(referenceNumber.get());
549 Optional<Integer> segmentNumber = getMetaKeyValue(xryEntity, XryMetaKey.SEGMENT_NUMBER);
550 if (!segmentNumber.isPresent()) {
551 logger.log(Level.SEVERE, String.format(
"No segment "
552 +
"number was found on the message entity"
553 +
"with reference number [%d]", referenceNumber.get()));
557 StringBuilder segmentedText =
new StringBuilder();
559 int currentSegmentNumber = segmentNumber.get();
560 while (reader.hasNextEntity()) {
562 String nextEntity = reader.peek();
563 String[] nextEntityLines = nextEntity.split(
"\n");
564 Optional<Integer> nextReferenceNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.REFERENCE_NUMBER);
566 if (!nextReferenceNumber.isPresent()
567 || !Objects.equals(nextReferenceNumber, referenceNumber)) {
576 Optional<Integer> nextSegmentNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.SEGMENT_NUMBER);
578 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ] "
579 +
"segment with reference number [ %d ]", nextEntityLines[0], referenceNumber.get()));
581 if (!nextSegmentNumber.isPresent()) {
582 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Segment with reference"
583 +
" number [ %d ] did not have a segment number associated with it."
584 +
" It cannot be determined if the reconstructed text will be in order.", referenceNumber.get()));
585 }
else if (nextSegmentNumber.get() != currentSegmentNumber + 1) {
586 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Contiguous "
587 +
"segments are not ascending incrementally. Encountered "
588 +
"segment [ %d ] after segment [ %d ]. This means the reconstructed "
589 +
"text will be out of order.", nextSegmentNumber.get(), currentSegmentNumber));
592 int keyCount = getCountOfKeyValuePairs(nextEntityLines);
593 for (
int i = 1; i <= keyCount; i++) {
594 XRYKeyValuePair pair = getKeyValuePairByIndex(nextEntityLines, i).get();
595 if (pair.hasKey(XryKey.TEXT.getDisplayName())
596 || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
597 segmentedText.append(pair.getValue()).append(
' ');
601 if (nextSegmentNumber.isPresent()) {
602 currentSegmentNumber = nextSegmentNumber.get();
607 if (segmentedText.length() > 0) {
608 segmentedText.setLength(segmentedText.length() - 1);
611 return segmentedText.toString();
622 private Optional<Integer> getMetaKeyValue(String[] xryLines, XryMetaKey metaKey) {
623 for (String xryLine : xryLines) {
624 if (!XRYKeyValuePair.isPair(xryLine)) {
628 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
629 if (pair.hasKey(metaKey.getDisplayName())) {
631 return Optional.of(Integer.parseInt(pair.getValue()));
632 }
catch (NumberFormatException ex) {
633 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Value [ %s ] for "
634 +
"meta key [ %s ] was not an integer.", pair.getValue(), metaKey), ex);
638 return Optional.empty();
652 private Optional<XRYKeyValuePair> getKeyValuePairByIndex(String[] xryLines,
int index) {
654 String
namespace = "";
655 for (
int i = 1; i < xryLines.length; i++) {
656 String xryLine = xryLines[i];
657 if (XryNamespace.contains(xryLine)) {
658 namespace = xryLine.trim();
662 if (!XRYKeyValuePair.isPair(xryLine)) {
663 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Expected a key value "
664 +
"pair on this line (in brackets) [ %s ], but one was not detected."
665 +
" Discarding...", xryLine));
669 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
670 String value = pair.getValue();
672 for (; (i + 1) < xryLines.length
673 && !XRYKeyValuePair.isPair(xryLines[i + 1])
674 && !XryNamespace.contains(xryLines[i + 1]); i++) {
675 String continuedValue = xryLines[i + 1].trim();
677 value = value +
" " + continuedValue;
680 pair =
new XRYKeyValuePair(pair.getKey(), value,
namespace);
682 if (pairsParsed == index) {
683 return Optional.of(pair);
687 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)