19 package org.sleuthkit.autopsy.datasourceprocessors.xry;
21 import java.io.IOException;
22 import java.nio.file.Path;
23 import java.time.Instant;
24 import java.time.LocalDateTime;
25 import java.time.OffsetDateTime;
26 import java.time.ZoneId;
27 import java.time.ZonedDateTime;
28 import java.time.format.DateTimeFormatter;
29 import java.time.format.DateTimeParseException;
30 import java.time.temporal.TemporalAccessor;
31 import java.time.temporal.TemporalQueries;
32 import java.util.ArrayList;
33 import java.util.HashSet;
34 import java.util.List;
35 import java.util.Objects;
36 import java.util.Optional;
38 import java.util.logging.Level;
48 final class XRYMessagesFileParser
implements XRYFileParser {
50 private static final Logger logger = Logger.getLogger(
51 XRYMessagesFileParser.class.getName());
53 private static final String PARSER_NAME =
"XRY DSP";
57 private static final DateTimeFormatter DATE_TIME_PARSER
58 = DateTimeFormatter.ofPattern(
"[(XXX) ][O ][(O) ]a h:m:s M/d/y");
60 private static final String DEVICE_LOCALE =
"(device)";
61 private static final String NETWORK_LOCALE =
"(network)";
63 private static final int READ = 1;
64 private static final int UNREAD = 0;
71 DELETED(
"deleted", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED),
72 DIRECTION(
"direction", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION),
73 MESSAGE(
"message", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT),
74 NAME_MATCHED(
"name (matched)", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON),
75 TEXT(
"text", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_TEXT),
76 TIME(
"time", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME),
77 SERVICE_CENTER(
"service center", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER),
78 FROM(
"from", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM),
79 TO(
"to", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO),
91 private final BlackboardAttribute.ATTRIBUTE_TYPE
type;
93 XryKey(String name, BlackboardAttribute.ATTRIBUTE_TYPE type) {
98 public BlackboardAttribute.ATTRIBUTE_TYPE
getType() {
116 }
catch (IllegalArgumentException ex) {
132 String normalizedName = name.trim().toLowerCase();
134 if (normalizedName.equals(keyChoice.name)) {
139 throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found."
140 +
" All keys should be tested with contains.", name));
166 public static boolean contains(String xryNamespace) {
170 }
catch (IllegalArgumentException ex) {
187 String normalizedNamespace = xryNamespace.trim().toLowerCase();
189 if (normalizedNamespace.equals(keyChoice.name)) {
194 throw new IllegalArgumentException(String.format(
"Namespace [%s] was not found."
195 +
" All namespaces should be tested with contains.", xryNamespace));
227 }
catch (IllegalArgumentException ex) {
243 String normalizedName = name.trim().toLowerCase();
245 if (normalizedName.equals(keyChoice.name)) {
250 throw new IllegalArgumentException(String.format(
"Key [ %s ] was not found."
251 +
" All keys should be tested with contains.", name));
275 public void parse(XRYFileReader reader, Content parent)
throws IOException, TskCoreException {
276 Path reportPath = reader.getReportPath();
277 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing report at"
278 +
" [ %s ]", reportPath.toString()));
281 Set<Integer> referenceNumbersSeen =
new HashSet<>();
283 while (reader.hasNextEntity()) {
284 String xryEntity = reader.nextEntity();
285 List<BlackboardAttribute> attributes = getBlackboardAttributes(xryEntity, reader, referenceNumbersSeen);
287 if (!attributes.isEmpty()) {
288 BlackboardArtifact artifact = parent.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_MESSAGE);
289 artifact.addAttributes(attributes);
298 private List<BlackboardAttribute> getBlackboardAttributes(String xryEntity,
299 XRYFileReader reader, Set<Integer> referenceValues)
throws IOException {
300 String[] xryLines = xryEntity.split(
"\n");
302 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ]", xryLines[0]));
304 List<BlackboardAttribute> attributes =
new ArrayList<>();
307 int keyCount = getCountOfKeyValuePairs(xryLines);
308 for (
int i = 1; i <= keyCount; i++) {
311 XRYKeyValuePair pair = getKeyValuePairByIndex(xryLines, i).get();
312 if (XryMetaKey.contains(pair.getKey())) {
317 if (!XryKey.contains(pair.getKey())) {
318 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key, "
319 +
"value pair (in brackets) [ %s ], "
320 +
"was not recognized. Discarding...", pair));
324 if (pair.getValue().isEmpty()) {
325 logger.log(Level.WARNING, String.format(
"[XRY DSP] The following key "
326 +
"(in brackets) [ %s ] was recognized, but the value "
327 +
"was empty. Discarding...", pair.getKey()));
333 if (pair.hasKey(XryKey.TEXT.getDisplayName())
334 || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
335 String segmentedText = getSegmentedText(xryLines, reader, referenceValues);
336 pair =
new XRYKeyValuePair(pair.getKey(),
338 pair.getValue() +
" " + segmentedText,
339 pair.getNamespace());
343 Optional<BlackboardAttribute> attribute = getBlackboardAttribute(pair);
344 if (attribute.isPresent()) {
345 attributes.add(attribute.get());
356 private Integer getCountOfKeyValuePairs(String[] xryEntity) {
358 for (
int i = 1; i < xryEntity.length; i++) {
359 if (XRYKeyValuePair.isPair(xryEntity[i])) {
376 private String getSegmentedText(String[] xryEntity, XRYFileReader reader,
377 Set<Integer> referenceNumbersSeen)
throws IOException {
378 Optional<Integer> referenceNumber = getMetaKeyValue(xryEntity, XryMetaKey.REFERENCE_NUMBER);
380 if (!referenceNumber.isPresent()) {
384 logger.log(Level.INFO, String.format(
"[XRY DSP] Message entity "
385 +
"appears to be segmented with reference number [ %d ]", referenceNumber.get()));
387 if (referenceNumbersSeen.contains(referenceNumber.get())) {
388 logger.log(Level.SEVERE, String.format(
"[XRY DSP] This reference [ %d ] has already "
389 +
"been seen. This means that the segments are not "
390 +
"contiguous. Any segments contiguous with this "
391 +
"one will be aggregated and another "
392 +
"(otherwise duplicate) artifact will be created.", referenceNumber.get()));
395 referenceNumbersSeen.add(referenceNumber.get());
397 Optional<Integer> segmentNumber = getMetaKeyValue(xryEntity, XryMetaKey.SEGMENT_NUMBER);
398 if (!segmentNumber.isPresent()) {
399 logger.log(Level.SEVERE, String.format(
"No segment "
400 +
"number was found on the message entity"
401 +
"with reference number [%d]", referenceNumber.get()));
405 StringBuilder segmentedText =
new StringBuilder();
407 int currentSegmentNumber = segmentNumber.get();
408 while (reader.hasNextEntity()) {
410 String nextEntity = reader.peek();
411 String[] nextEntityLines = nextEntity.split(
"\n");
412 Optional<Integer> nextReferenceNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.REFERENCE_NUMBER);
414 if (!nextReferenceNumber.isPresent()
415 || !Objects.equals(nextReferenceNumber, referenceNumber)) {
424 Optional<Integer> nextSegmentNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.SEGMENT_NUMBER);
426 logger.log(Level.INFO, String.format(
"[XRY DSP] Processing [ %s ] "
427 +
"segment with reference number [ %d ]", nextEntityLines[0], referenceNumber.get()));
429 if (!nextSegmentNumber.isPresent()) {
430 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Segment with reference"
431 +
" number [ %d ] did not have a segment number associated with it."
432 +
" It cannot be determined if the reconstructed text will be in order.", referenceNumber.get()));
433 }
else if (nextSegmentNumber.get() != currentSegmentNumber + 1) {
434 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Contiguous "
435 +
"segments are not ascending incrementally. Encountered "
436 +
"segment [ %d ] after segment [ %d ]. This means the reconstructed "
437 +
"text will be out of order.", nextSegmentNumber.get(), currentSegmentNumber));
440 int keyCount = getCountOfKeyValuePairs(nextEntityLines);
441 for (
int i = 1; i <= keyCount; i++) {
442 XRYKeyValuePair pair = getKeyValuePairByIndex(nextEntityLines, i).get();
443 if (pair.hasKey(XryKey.TEXT.getDisplayName())
444 || pair.hasKey(XryKey.MESSAGE.getDisplayName())) {
445 segmentedText.append(pair.getValue()).append(
' ');
449 if (nextSegmentNumber.isPresent()) {
450 currentSegmentNumber = nextSegmentNumber.get();
455 if (segmentedText.length() > 0) {
456 segmentedText.setLength(segmentedText.length() - 1);
459 return segmentedText.toString();
469 private Optional<Integer> getMetaKeyValue(String[] xryLines, XryMetaKey metaKey) {
470 for (String xryLine : xryLines) {
471 if (!XRYKeyValuePair.isPair(xryLine)) {
475 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
476 if (pair.hasKey(metaKey.getDisplayName())) {
478 return Optional.of(Integer.parseInt(pair.getValue()));
479 }
catch (NumberFormatException ex) {
480 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Value [ %s ] for "
481 +
"meta key [ %s ] was not an integer.", pair.getValue(), metaKey), ex);
485 return Optional.empty();
497 private Optional<XRYKeyValuePair> getKeyValuePairByIndex(String[] xryLines,
int index) {
499 String
namespace = "";
500 for (
int i = 1; i < xryLines.length; i++) {
501 String xryLine = xryLines[i];
502 if (XryNamespace.contains(xryLine)) {
503 namespace = xryLine.trim();
507 if (!XRYKeyValuePair.isPair(xryLine)) {
508 logger.log(Level.SEVERE, String.format(
"[XRY DSP] Expected a key value "
509 +
"pair on this line (in brackets) [ %s ], but one was not detected."
510 +
" Discarding...", xryLine));
514 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
515 String value = pair.getValue();
517 for (; (i + 1) < xryLines.length
518 && !XRYKeyValuePair.isPair(xryLines[i + 1])
519 && !XryNamespace.contains(xryLines[i + 1]); i++) {
520 String continuedValue = xryLines[i + 1].trim();
522 value = value +
" " + continuedValue;
525 pair =
new XRYKeyValuePair(pair.getKey(), value,
namespace);
527 if (pairsParsed == index) {
528 return Optional.of(pair);
532 return Optional.empty();
544 private Optional<BlackboardAttribute> getBlackboardAttribute(XRYKeyValuePair pair) {
545 XryNamespace
namespace = XryNamespace.NONE;
546 if (XryNamespace.contains(pair.getNamespace())) {
547 namespace = XryNamespace.fromDisplayName(pair.getNamespace());
549 XryKey key = XryKey.fromDisplayName(pair.getKey());
550 String normalizedValue = pair.getValue().toLowerCase().trim();
557 return Optional.of(
new BlackboardAttribute(
558 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_FROM,
559 PARSER_NAME, pair.getValue()));
562 return Optional.of(
new BlackboardAttribute(
563 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER_TO,
564 PARSER_NAME, pair.getValue()));
566 return Optional.of(
new BlackboardAttribute(
567 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
568 PARSER_NAME, pair.getValue()));
573 long dateTimeSinceInEpoch = calculateSecondsSinceEpoch(pair.getValue());
574 return Optional.of(
new BlackboardAttribute(
575 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DATETIME_START,
576 PARSER_NAME, dateTimeSinceInEpoch));
577 }
catch (DateTimeParseException ex) {
578 logger.log(Level.WARNING, String.format(
"[XRY DSP] Assumption"
579 +
" about the date time formatting of messages is "
580 +
"not right. Here is the pair [ %s ]", pair), ex);
581 return Optional.empty();
584 switch (normalizedValue) {
587 return Optional.of(
new BlackboardAttribute(
588 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_DIRECTION,
589 PARSER_NAME, pair.getValue()));
592 case "status report":
594 return Optional.empty();
596 logger.log(Level.WARNING, String.format(
"[XRY DSP] Unrecognized "
597 +
" value for key pair [ %s ].", pair));
598 return Optional.empty();
601 switch (normalizedValue) {
603 return Optional.of(
new BlackboardAttribute(
604 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS,
607 return Optional.of(
new BlackboardAttribute(
608 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_READ_STATUS,
609 PARSER_NAME, UNREAD));
610 case "sending failed":
615 return Optional.empty();
617 logger.log(Level.WARNING, String.format(
"[XRY DSP] Unrecognized "
618 +
" value for key pair [ %s ].", pair));
619 return Optional.empty();
624 if (key.getType() != null) {
625 return Optional.of(
new BlackboardAttribute(key.getType(),
626 PARSER_NAME, pair.getValue()));
629 logger.log(Level.INFO, String.format(
"[XRY DSP] Key value pair "
630 +
"(in brackets) [ %s ] was recognized but "
631 +
"more data or time is needed to finish implementation. Discarding... ", pair));
633 return Optional.empty();
645 private String removeDateTimeLocale(String dateTime) {
646 String result = dateTime;
647 int deviceIndex = result.toLowerCase().indexOf(DEVICE_LOCALE);
648 if (deviceIndex != -1) {
649 result = result.substring(0, deviceIndex);
651 int networkIndex = result.toLowerCase().indexOf(NETWORK_LOCALE);
652 if (networkIndex != -1) {
653 result = result.substring(0, networkIndex);
664 private long calculateSecondsSinceEpoch(String dateTime) {
665 String dateTimeWithoutLocale = removeDateTimeLocale(dateTime).trim();
682 String reversedDateTime = reverseOrderOfDateTimeComponents(dateTimeWithoutLocale);
690 String reversedDateTimeWithGMT = reversedDateTime.replace(
"UTC",
"GMT");
691 TemporalAccessor result = DATE_TIME_PARSER.parseBest(reversedDateTimeWithGMT,
694 OffsetDateTime::from);
696 if (result.query(TemporalQueries.zoneId()) == null) {
698 return ZonedDateTime.of(LocalDateTime.from(result),
699 ZoneId.of(
"GMT")).toEpochSecond();
701 return Instant.from(result).getEpochSecond();
713 private String reverseOrderOfDateTimeComponents(String dateTime) {
714 StringBuilder reversedDateTime =
new StringBuilder(dateTime.length());
715 String[] dateTimeComponents = dateTime.split(
" ");
716 for (String component : dateTimeComponents) {
717 reversedDateTime.insert(0,
" ").insert(0, component);
719 return reversedDateTime.toString().trim();
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)