Autopsy 4.22.1
Graphical digital forensics platform for The Sleuth Kit and other tools.
XRYMessagesFileParser.java
Go to the documentation of this file.
1/*
2 * Autopsy Forensic Browser
3 *
4 * Copyright 2019-2021 Basis Technology Corp.
5 * Contact: carrier <at> sleuthkit <dot> org
6 *
7 * Licensed under the Apache License, Version 2.0 (the "License");
8 * you may not use this file except in compliance with the License.
9 * You may obtain a copy of the License at
10 *
11 * http://www.apache.org/licenses/LICENSE-2.0
12 *
13 * Unless required by applicable law or agreed to in writing, software
14 * distributed under the License is distributed on an "AS IS" BASIS,
15 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16 * See the License for the specific language governing permissions and
17 * limitations under the License.
18 */
19package org.sleuthkit.autopsy.datasourceprocessors.xry;
20
21import java.io.IOException;
22import java.nio.file.Path;
23import java.time.format.DateTimeParseException;
24import java.util.ArrayList;
25import java.util.Collection;
26import java.util.HashSet;
27import java.util.List;
28import java.util.Objects;
29import java.util.Optional;
30import java.util.Set;
31import java.util.logging.Level;
32import org.sleuthkit.autopsy.coreutils.Logger;
33import org.sleuthkit.datamodel.Account;
34import org.sleuthkit.datamodel.Blackboard.BlackboardException;
35import org.sleuthkit.datamodel.BlackboardAttribute;
36import org.sleuthkit.datamodel.Content;
37import org.sleuthkit.datamodel.InvalidAccountIDException;
38import org.sleuthkit.datamodel.SleuthkitCase;
39import org.sleuthkit.datamodel.TskCoreException;
40import org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper;
41import org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper.CommunicationDirection;
42import org.sleuthkit.datamodel.blackboardutils.CommunicationArtifactsHelper.MessageReadStatus;
43
47final class XRYMessagesFileParser implements XRYFileParser {
48
49 private static final Logger logger = Logger.getLogger(
50 XRYMessagesFileParser.class.getName());
51
52 private static final String PARSER_NAME = "XRY DSP";
53
58 private enum XryKey {
59 DELETED("deleted", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED),
60 DIRECTION("direction", null),
61 MESSAGE("message", null),
62 NAME_MATCHED("name (matched)", BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON),
63 TEXT("text", null),
64 TIME("time", null),
65 SERVICE_CENTER("service center", null),
66 FROM("from", null),
67 TO("to", null),
68 //The following keys either need special processing or more time and data to find a type.
69 STORAGE("storage", null),
70 NUMBER("number", null),
71 TYPE("type", null),
72 TEL("tel", null),
73 FOLDER("folder", null),
74 NAME("name", null),
75 INDEX("index", null),
76 STATUS("status", null);
77
78 private final String name;
79 private final BlackboardAttribute.ATTRIBUTE_TYPE type;
80
81 XryKey(String name, BlackboardAttribute.ATTRIBUTE_TYPE type) {
82 this.name = name;
83 this.type = type;
84 }
85
86 public BlackboardAttribute.ATTRIBUTE_TYPE getType() {
87 return type;
88 }
89
90 public String getDisplayName() {
91 return name;
92 }
93
101 public static boolean contains(String name) {
102 try {
103 XryKey.fromDisplayName(name);
104 return true;
105 } catch (IllegalArgumentException ex) {
106 return false;
107 }
108 }
109
121 public static XryKey fromDisplayName(String name) {
122 String normalizedName = name.trim().toLowerCase();
123 for (XryKey keyChoice : XryKey.values()) {
124 if (normalizedName.equals(keyChoice.name)) {
125 return keyChoice;
126 }
127 }
128
129 throw new IllegalArgumentException(String.format("Key [ %s ] was not found."
130 + " All keys should be tested with contains.", name));
131 }
132 }
133
137 private enum XryNamespace {
138 FROM("from"),
139 PARTICIPANT("participant"),
140 TO("to"),
141 NONE(null);
142
143 private final String name;
144
146 this.name = name;
147 }
148
157 public static boolean contains(String xryNamespace) {
158 try {
159 XryNamespace.fromDisplayName(xryNamespace);
160 return true;
161 } catch (IllegalArgumentException ex) {
162 return false;
163 }
164 }
165
178 public static XryNamespace fromDisplayName(String xryNamespace) {
179 String normalizedNamespace = xryNamespace.trim().toLowerCase();
180 for (XryNamespace keyChoice : XryNamespace.values()) {
181 if (normalizedNamespace.equals(keyChoice.name)) {
182 return keyChoice;
183 }
184 }
185
186 throw new IllegalArgumentException(String.format("Namespace [%s] was not found."
187 + " All namespaces should be tested with contains.", xryNamespace));
188 }
189 }
190
194 private enum XryMetaKey {
195 REFERENCE_NUMBER("reference number"),
196 SEGMENT_COUNT("segments"),
197 SEGMENT_NUMBER("segment number");
198
199 private final String name;
200
201 XryMetaKey(String name) {
202 this.name = name;
203 }
204
205 public String getDisplayName() {
206 return name;
207 }
208
216 public static boolean contains(String name) {
217 try {
218 XryMetaKey.fromDisplayName(name);
219 return true;
220 } catch (IllegalArgumentException ex) {
221 return false;
222 }
223 }
224
236 public static XryMetaKey fromDisplayName(String name) {
237 String normalizedName = name.trim().toLowerCase();
238 for (XryMetaKey keyChoice : XryMetaKey.values()) {
239 if (normalizedName.equals(keyChoice.name)) {
240 return keyChoice;
241 }
242 }
243
244 throw new IllegalArgumentException(String.format("Key [ %s ] was not found."
245 + " All keys should be tested with contains.", name));
246 }
247 }
248
270 @Override
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()));
275
276 //Keep track of the reference numbers that have been parsed.
277 Set<Integer> referenceNumbersSeen = new HashSet<>();
278
279 while (reader.hasNextEntity()) {
280 String xryEntity = reader.nextEntity();
281
282 // This call will combine all segmented text into a single key value pair
283 List<XRYKeyValuePair> pairs = getXRYKeyValuePairs(xryEntity, reader, referenceNumbersSeen);
284
285 // Transform all the data from XRY land into the appropriate CommHelper
286 // data types.
287 final String messageType = PARSER_NAME;
288 CommunicationDirection direction = CommunicationDirection.UNKNOWN;
289 String senderId = null;
290 final List<String> recipientIdsList = new ArrayList<>();
291 long dateTime = 0L;
292 MessageReadStatus readStatus = MessageReadStatus.UNKNOWN;
293 final String subject = null;
294 String text = null;
295 final String threadId = null;
296 final Collection<BlackboardAttribute> otherAttributes = new ArrayList<>();
297
298 for (XRYKeyValuePair pair : pairs) {
299 XryNamespace namespace = XryNamespace.NONE;
300 if (XryNamespace.contains(pair.getNamespace())) {
301 namespace = XryNamespace.fromDisplayName(pair.getNamespace());
302 }
303 XryKey key = XryKey.fromDisplayName(pair.getKey());
304 String normalizedValue = pair.getValue().toLowerCase().trim();
305
306 switch (key) {
307 case TEL:
308 case NUMBER:
309 if (!XRYUtils.isPhoneValid(pair.getValue())) {
310 continue;
311 }
312
313 // Apply namespace or direction
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());
318 } else {
319 try {
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);
324 }
325
326 otherAttributes.add(new BlackboardAttribute(
327 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
328 PARSER_NAME, pair.getValue()));
329 }
330 break;
331 // Although confusing, as these are also 'name spaces', it appears
332 // later versions of XRY just made these standardized lines.
333 case FROM:
334 if (!XRYUtils.isPhoneValid(pair.getValue())) {
335 continue;
336 }
337
338 senderId = pair.getValue();
339 break;
340 case TO:
341 if (!XRYUtils.isPhoneValid(pair.getValue())) {
342 continue;
343 }
344
345 recipientIdsList.add(pair.getValue());
346 break;
347 case TIME:
348 try {
349 //Tranform value to seconds since epoch
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);
356 }
357 break;
358 case TYPE:
359 switch (normalizedValue) {
360 case "incoming":
361 direction = CommunicationDirection.INCOMING;
362 break;
363 case "outgoing":
364 direction = CommunicationDirection.OUTGOING;
365 break;
366 case "deliver":
367 case "submit":
368 case "status report":
369 //Ignore for now.
370 break;
371 default:
372 logger.log(Level.WARNING, String.format("[%s] Unrecognized "
373 + " value for key pair [ %s ].", PARSER_NAME, pair));
374 }
375 break;
376 case STATUS:
377 switch (normalizedValue) {
378 case "read":
379 readStatus = MessageReadStatus.READ;
380 break;
381 case "unread":
382 readStatus = MessageReadStatus.UNREAD;
383 break;
384 case "deleted":
385 otherAttributes.add(new BlackboardAttribute(
386 BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ISDELETED,
387 PARSER_NAME, pair.getValue()));
388 break;
389 case "sending failed":
390 case "unsent":
391 case "sent":
392 //Ignoring for now.
393 break;
394 default:
395 logger.log(Level.WARNING, String.format("[%s] Unrecognized "
396 + " value for key pair [ %s ].", PARSER_NAME, pair));
397 }
398 break;
399 case TEXT:
400 case MESSAGE:
401 text = pair.getValue();
402 break;
403 case DIRECTION:
404 switch (normalizedValue) {
405 case "incoming":
406 direction = CommunicationDirection.INCOMING;
407 break;
408 case "outgoing":
409 direction = CommunicationDirection.OUTGOING;
410 break;
411 default:
412 direction = CommunicationDirection.UNKNOWN;
413 break;
414 }
415 break;
416 case SERVICE_CENTER:
417 if (!XRYUtils.isPhoneValid(pair.getValue())) {
418 continue;
419 }
420
421 otherAttributes.add(new BlackboardAttribute(BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER,
422 PARSER_NAME, pair.getValue()));
423 break;
424 default:
425 //Otherwise, the XryKey enum contains the correct BlackboardAttribute
426 //type.
427 if (key.getType() != null) {
428 otherAttributes.add(new BlackboardAttribute(key.getType(),
429 PARSER_NAME, pair.getValue()));
430 } else {
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... ",
434 PARSER_NAME, pair));
435 }
436 }
437 }
438
439 CommunicationArtifactsHelper helper = new CommunicationArtifactsHelper(
440 currentCase, PARSER_NAME, parent, Account.Type.PHONE, null);
441
442 helper.addMessage(messageType, direction, senderId, recipientIdsList,
443 dateTime, readStatus, subject, text, threadId, otherAttributes);
444 }
445 }
446
451 private List<XRYKeyValuePair> getXRYKeyValuePairs(String xryEntity,
452 XRYFileReader reader, Set<Integer> referenceValues) throws IOException {
453 String[] xryLines = xryEntity.split("\n");
454 //First line of the entity is the title, each XRY entity is non-empty.
455 logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ]", xryLines[0]));
456
457 List<XRYKeyValuePair> pairs = new ArrayList<>();
458
459 //Count the key value pairs in the XRY entity.
460 int keyCount = getCountOfKeyValuePairs(xryLines);
461 for (int i = 1; i <= keyCount; i++) {
462 //Get the ith key value pair in the entity. Always expect to have
463 //a valid value.
464 XRYKeyValuePair pair = getKeyValuePairByIndex(xryLines, i).get();
465 if (XryMetaKey.contains(pair.getKey())) {
466 //Skip meta keys, they are being handled seperately.
467 continue;
468 }
469
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));
474 continue;
475 }
476
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()));
481 continue;
482 }
483
484 //Assume text and message are the only fields that can be segmented
485 //among multiple XRY entities.
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(),
490 //Assume text is segmented by word.
491 pair.getValue() + " " + segmentedText,
492 pair.getNamespace());
493 }
494
495 pairs.add(pair);
496 }
497
498 return pairs;
499 }
500
505 private Integer getCountOfKeyValuePairs(String[] xryEntity) {
506 int count = 0;
507 for (int i = 1; i < xryEntity.length; i++) {
508 if (XRYKeyValuePair.isPair(xryEntity[i])) {
509 count++;
510 }
511 }
512 return count;
513 }
514
528 private String getSegmentedText(String[] xryEntity, XRYFileReader reader,
529 Set<Integer> referenceNumbersSeen) throws IOException {
530 Optional<Integer> referenceNumber = getMetaKeyValue(xryEntity, XryMetaKey.REFERENCE_NUMBER);
531 //Check if there is any segmented text.
532 if (!referenceNumber.isPresent()) {
533 return "";
534 }
535
536 logger.log(Level.INFO, String.format("[XRY DSP] Message entity "
537 + "appears to be segmented with reference number [ %d ]", referenceNumber.get()));
538
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()));
545 }
546
547 referenceNumbersSeen.add(referenceNumber.get());
548
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()));
554 return "";
555 }
556
557 StringBuilder segmentedText = new StringBuilder();
558
559 int currentSegmentNumber = segmentNumber.get();
560 while (reader.hasNextEntity()) {
561 //Peek at the next to see if it has the same reference number.
562 String nextEntity = reader.peek();
563 String[] nextEntityLines = nextEntity.split("\n");
564 Optional<Integer> nextReferenceNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.REFERENCE_NUMBER);
565
566 if (!nextReferenceNumber.isPresent()
567 || !Objects.equals(nextReferenceNumber, referenceNumber)) {
568 //Don't consume the next entity. It is not related
569 //to the current message thread.
570 break;
571 }
572
573 //Consume the entity, it is a part of the message thread.
574 reader.nextEntity();
575
576 Optional<Integer> nextSegmentNumber = getMetaKeyValue(nextEntityLines, XryMetaKey.SEGMENT_NUMBER);
577
578 logger.log(Level.INFO, String.format("[XRY DSP] Processing [ %s ] "
579 + "segment with reference number [ %d ]", nextEntityLines[0], referenceNumber.get()));
580
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));
590 }
591
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(' ');
598 }
599 }
600
601 if (nextSegmentNumber.isPresent()) {
602 currentSegmentNumber = nextSegmentNumber.get();
603 }
604 }
605
606 //Remove the trailing space.
607 if (segmentedText.length() > 0) {
608 segmentedText.setLength(segmentedText.length() - 1);
609 }
610
611 return segmentedText.toString();
612 }
613
622 private Optional<Integer> getMetaKeyValue(String[] xryLines, XryMetaKey metaKey) {
623 for (String xryLine : xryLines) {
624 if (!XRYKeyValuePair.isPair(xryLine)) {
625 continue;
626 }
627
628 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
629 if (pair.hasKey(metaKey.getDisplayName())) {
630 try {
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);
635 }
636 }
637 }
638 return Optional.empty();
639 }
640
652 private Optional<XRYKeyValuePair> getKeyValuePairByIndex(String[] xryLines, int index) {
653 int pairsParsed = 0;
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();
659 continue;
660 }
661
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));
666 continue;
667 }
668
669 XRYKeyValuePair pair = XRYKeyValuePair.from(xryLine);
670 String value = pair.getValue();
671 //Build up multiple lines.
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();
676 //Assume multi lined values are split by word.
677 value = value + " " + continuedValue;
678 }
679
680 pair = new XRYKeyValuePair(pair.getKey(), value, namespace);
681 pairsParsed++;
682 if (pairsParsed == index) {
683 return Optional.of(pair);
684 }
685 }
686
687 return Optional.empty();
688 }
689}

Copyright © 2012-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.