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;
39 import org.
sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper;
40 import org.
sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper.CommunicationDirection;
41 import org.
sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper.MessageReadStatus;
46 final class XRYMessagesFileParser
implements XRYFileParser {
48 private static final Logger logger = Logger.getLogger(
49 XRYMessagesFileParser.class.getName());
51 private static final String PARSER_NAME =
"XRY DSP";
58 DELETED(
"deleted", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED),
61 NAME_MATCHED(
"name (matched)", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON),
78 private final BlackboardAttribute.ATTRIBUTE_TYPE
type;
80 XryKey(String name, BlackboardAttribute.ATTRIBUTE_TYPE type) {
85 public BlackboardAttribute.ATTRIBUTE_TYPE
getType() {
103 }
catch (IllegalArgumentException ex) {
119 String normalizedName = name.trim().toLowerCase();
121 if (normalizedName.equals(keyChoice.name)) {
126 throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found."
127 +
" All keys should be tested with contains.", name));
153 public static boolean contains(String xryNamespace) {
157 }
catch (IllegalArgumentException ex) {
174 String normalizedNamespace = xryNamespace.trim().toLowerCase();
176 if (normalizedNamespace.equals(keyChoice.name)) {
181 throw new IllegalArgumentException(String.format(
"Namespace [%s] was not found."
182 +
" All namespaces should be tested with contains.", xryNamespace));
214 }
catch (IllegalArgumentException ex) {
230 String normalizedName = name.trim().toLowerCase();
232 if (normalizedName.equals(keyChoice.name)) {
237 throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found."
238 +
" All keys should be tested with contains.", name));
262 public void parse(XRYFileReader reader, Content parent, SleuthkitCase currentCase)
throws IOException, TskCoreException, BlackboardException {
263 Path reportPath = reader.getReportPath();
264 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing report at"
265 +
" [ %s ]", reportPath.toString()));
268 Set<Integer> referenceNumbersSeen =
new HashSet<>();
270 while (reader.hasNextEntity()) {
271 String xryEntity = reader.nextEntity();
274 List<XRYKeyValuePair> pairs = getXRYKeyValuePairs(xryEntity, reader, referenceNumbersSeen);
278 final String messageType = PARSER_NAME;
279 CommunicationDirection direction = CommunicationDirection.UNKNOWN;
280 String senderId = null;
281 final List<String> recipientIdsList =
new ArrayList<>();
283 MessageReadStatus readStatus = MessageReadStatus.UNKNOWN;
284 final String subject = null;
286 final String threadId = null;
287 final Collection<BlackboardAttribute> otherAttributes =
new ArrayList<>();
289 for(XRYKeyValuePair pair : pairs) {
290 XryNamespace
namespace = XryNamespace.NONE;
291 if (XryNamespace.contains(pair.getNamespace())) {
292 namespace = XryNamespace.fromDisplayName(pair.getNamespace());
294 XryKey key = XryKey.fromDisplayName(pair.getKey());
295 String normalizedValue = pair.getValue().toLowerCase().trim();
300 if(!XRYUtils.isPhoneValid(pair.getValue())) {
305 if(
namespace == XryNamespace.FROM || direction == CommunicationDirection.INCOMING) {
306 senderId = pair.getValue();
307 }
else if(
namespace == XryNamespace.TO || direction == CommunicationDirection.OUTGOING) {
308 recipientIdsList.add(pair.getValue());
310 currentCase.getCommunicationsManager().createAccountFileInstance(
311 Account.Type.PHONE, pair.getValue(), PARSER_NAME, parent);
312 otherAttributes.add(
new BlackboardAttribute(
313 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
314 PARSER_NAME, pair.getValue()));
320 if(!XRYUtils.isPhoneValid(pair.getValue())) {
324 senderId = pair.getValue();
327 if(!XRYUtils.isPhoneValid(pair.getValue())) {
331 recipientIdsList.add(pair.getValue());
336 long dateTimeSinceInEpoch = XRYUtils.calculateSecondsSinceEpoch(pair.getValue());
337 dateTime = dateTimeSinceInEpoch;
338 }
catch (DateTimeParseException ex) {
339 logger.log(Level.WARNING, String.format(
"[%s] Assumption"
340 +
" about the date time formatting of messages is "
341 +
"not right. Here is the pair [ %s ]", PARSER_NAME, pair), ex);
345 switch (normalizedValue) {
347 direction = CommunicationDirection.INCOMING;
350 direction = CommunicationDirection.OUTGOING;
354 case "status report":
358 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized "
359 +
" value for key pair [ %s ].", PARSER_NAME, pair));
363 switch (normalizedValue) {
365 readStatus = MessageReadStatus.READ;
368 readStatus = MessageReadStatus.UNREAD;
371 otherAttributes.add(
new BlackboardAttribute(
372 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED,
373 PARSER_NAME, pair.getValue()));
375 case "sending failed":
381 logger.log(Level.WARNING, String.format(
"[%s] Unrecognized "
382 +
" value for key pair [ %s ].", PARSER_NAME, pair));
387 text = pair.getValue();
390 switch (normalizedValue) {
392 direction = CommunicationDirection.INCOMING;
395 direction = CommunicationDirection.OUTGOING;
398 direction = CommunicationDirection.UNKNOWN;
403 if(!XRYUtils.isPhoneValid(pair.getValue())) {
407 otherAttributes.add(
new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
408 PARSER_NAME, pair.getValue()));
413 if (key.getType() != null) {
414 otherAttributes.add(
new BlackboardAttribute(key.getType(),
415 PARSER_NAME, pair.getValue()));
417 logger.log(Level.INFO, String.format(
"[%s] Key value pair "
418 +
"(in brackets) [ %s ] was recognized but "
419 +
"more data or time is needed to finish implementation. Discarding... ",
425 CommunicationArtifactsHelper helper =
new CommunicationArtifactsHelper(
426 currentCase, PARSER_NAME, parent, Account.Type.PHONE);
428 helper.addMessage(messageType, direction, senderId, recipientIdsList,
429 dateTime, readStatus, subject, text, threadId, otherAttributes);
437 private List<XRYKeyValuePair> getXRYKeyValuePairs(String xryEntity,
438 XRYFileReader reader, Set<Integer> referenceValues)
throws IOException {
439 String[] xryLines = xryEntity.split(
"\n");
441 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ]", xryLines[0]));
443 List<XRYKeyValuePair> pairs =
new ArrayList<>();
446 int keyCount = getCountOfKeyValuePairs(xryLines);
447 for (
int i = 1; i <= keyCount; i++) {
450 XRYKeyValuePair pair = getKeyValuePairByIndex(xryLines, i).get();
451 if (XryMetaKey.contains(pair.getKey())) {
456 if (!XryKey.contains(pair.getKey())) {
457 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key, "
458 +
"value pair (in brackets) [ %s ], "
459 +
"was not recognized. Discarding...", pair));
463 if (pair.getValue().isEmpty()) {
464 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key "
465 +
"(in brackets) [ %s ] was recognized, but the value "
466 +
"was empty. Discarding...", pair.getKey()));
472 if (pair.hasKey(XryKey.TEXT.getDisplayName())
473 || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
474 String segmentedText = getSegmentedText(xryLines, reader, referenceValues);
475 pair =
new XRYKeyValuePair(pair.getKey(),
477 pair.getValue() +
" " + segmentedText,
478 pair.getNamespace());
491 private Integer getCountOfKeyValuePairs(String[] xryEntity) {
493 for (
int i = 1; i < xryEntity.length; i++) {
494 if (XRYKeyValuePair.isPair(xryEntity[i])) {
511 private String getSegmentedText(String[] xryEntity, XRYFileReader reader,
512 Set<Integer> referenceNumbersSeen)
throws IOException {
513 Optional<Integer> referenceNumber = getMetaKeyValue(xryEntity, XryMetaKey.REFERENCE_NUMBER);
515 if (!referenceNumber.isPresent()) {
519 logger.log(Level.INFO, String.format(
"[XRY DSP] Message entity "
520 +
"appears to be segmented with reference number [ %d ]", referenceNumber.get()));
522 if (referenceNumbersSeen.contains(referenceNumber.get())) {
523 logger.log(Level.SEVERE, String.format(
"[XRY DSP] This reference [ %d ] has already "
524 +
"been seen. This means that the segments are not "
525 +
"contiguous. Any segments contiguous with this "
526 +
"one will be aggregated and another "
527 +
"(otherwise duplicate) artifact will be created.", referenceNumber.get()));
530 referenceNumbersSeen.add(referenceNumber.get());
532 Optional<Integer> segmentNumber = getMetaKeyValue(xryEntity, XryMetaKey.SEGMENT_NUMBER);
533 if (!segmentNumber.isPresent()) {
534 logger.log(Level.SEVERE, String.format(
"No segment "
535 +
"number was found on the message entity"
536 +
"with reference number [%d]", referenceNumber.get()));
540 StringBuilder segmentedText =
new StringBuilder();
542 int currentSegmentNumber = segmentNumber.get();
543 while (reader.hasNextEntity()) {
545 String nextEntity = reader.peek();
546 String[] nextEntityLines = nextEntity.split(
"\n");
547 Optional<Integer> nextReferenceNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.REFERENCE_NUMBER);
549 if (!nextReferenceNumber.isPresent()
550 || !Objects.equals(nextReferenceNumber, referenceNumber)) {
559 Optional<Integer> nextSegmentNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.SEGMENT_NUMBER);
561 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ] "
562 +
"segment with reference number [ %d ]", nextEntityLines[0], referenceNumber.get()));
564 if (!nextSegmentNumber.isPresent()) {
565 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Segment with reference"
566 +
" number [ %d ] did not have a segment number associated with it."
567 +
" It cannot be determined if the reconstructed text will be in order.", referenceNumber.get()));
568 }
else if (nextSegmentNumber.get() != currentSegmentNumber + 1) {
569 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Contiguous "
570 +
"segments are not ascending incrementally. Encountered "
571 +
"segment [ %d ] after segment [ %d ]. This means the reconstructed "
572 +
"text will be out of order.", nextSegmentNumber.get(), currentSegmentNumber));
575 int keyCount = getCountOfKeyValuePairs(nextEntityLines);
576 for (
int i = 1; i <= keyCount; i++) {
577 XRYKeyValuePair pair = getKeyValuePairByIndex(nextEntityLines, i).get();
578 if (pair.hasKey(XryKey.TEXT.getDisplayName())
579 || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
580 segmentedText.append(pair.getValue()).append(
' ');
584 if (nextSegmentNumber.isPresent()) {
585 currentSegmentNumber = nextSegmentNumber.get();
590 if (segmentedText.length() > 0) {
591 segmentedText.setLength(segmentedText.length() - 1);
594 return segmentedText.toString();
604 private Optional<Integer> getMetaKeyValue(String[] xryLines, XryMetaKey metaKey) {
605 for (String xryLine : xryLines) {
606 if (!XRYKeyValuePair.isPair(xryLine)) {
610 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
611 if (pair.hasKey(metaKey.getDisplayName())) {
613 return Optional.of(Integer.parseInt(pair.getValue()));
614 }
catch (NumberFormatException ex) {
615 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Value [ %s ] for "
616 +
"meta key [ %s ] was not an integer.", pair.getValue(), metaKey), ex);
620 return Optional.empty();
632 private Optional<XRYKeyValuePair> getKeyValuePairByIndex(String[] xryLines,
int index) {
634 String
namespace = "";
635 for (
int i = 1; i < xryLines.length; i++) {
636 String xryLine = xryLines[i];
637 if (XryNamespace.contains(xryLine)) {
638 namespace = xryLine.trim();
642 if (!XRYKeyValuePair.isPair(xryLine)) {
643 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Expected a key value "
644 +
"pair on this line (in brackets) [ %s ], but one was not detected."
645 +
" Discarding...", xryLine));
649 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
650 String value = pair.getValue();
652 for (; (i + 1) < xryLines.length
653 && !XRYKeyValuePair.isPair(xryLines[i + 1])
654 && !XryNamespace.contains(xryLines[i + 1]); i++) {
655 String continuedValue = xryLines[i + 1].trim();
657 value = value +
" " + continuedValue;
660 pair =
new XRYKeyValuePair(pair.getKey(), value,
namespace);
662 if (pairsParsed == index) {
663 return Optional.of(pair);
667 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)