Autopsy  4.19.3
Graphical digital forensics platform for The Sleuth Kit and other tools.
VcardParser.java
Go to the documentation of this file.
1 /*
2  * Autopsy Forensic Browser
3  *
4  * Copyright 2019 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  */
19 package org.sleuthkit.autopsy.thunderbirdparser;
20 
21 import ezvcard.Ezvcard;
22 import ezvcard.VCard;
23 import ezvcard.parameter.EmailType;
24 import ezvcard.parameter.TelephoneType;
25 import ezvcard.property.Email;
26 import ezvcard.property.Organization;
27 import ezvcard.property.Photo;
28 import ezvcard.property.Telephone;
29 import ezvcard.property.Url;
30 import java.io.BufferedInputStream;
31 import java.io.File;
32 import java.io.FileOutputStream;
33 import java.io.IOException;
34 import java.io.InputStreamReader;
35 import java.nio.charset.StandardCharsets;
36 import java.nio.file.Paths;
37 import java.util.ArrayList;
38 import java.util.Arrays;
39 import java.util.Collection;
40 import java.util.HashMap;
41 import java.util.List;
42 import java.util.Map;
43 import java.util.concurrent.ConcurrentMap;
44 import java.util.logging.Level;
45 import org.apache.commons.lang3.StringUtils;
46 import org.openide.util.NbBundle;
55 import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
56 import org.sleuthkit.datamodel.AbstractFile;
57 import org.sleuthkit.datamodel.Account;
58 import org.sleuthkit.datamodel.AccountFileInstance;
59 import org.sleuthkit.datamodel.Blackboard;
60 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
61 import org.sleuthkit.datamodel.BlackboardArtifact;
62 import org.sleuthkit.datamodel.BlackboardAttribute;
63 import org.sleuthkit.datamodel.Content;
64 import org.sleuthkit.datamodel.DataSource;
65 import org.sleuthkit.datamodel.ReadContentInputStream;
66 import org.sleuthkit.datamodel.Relationship;
67 import org.sleuthkit.datamodel.SleuthkitCase;
68 import org.sleuthkit.datamodel.TskCoreException;
69 import org.sleuthkit.datamodel.TskData;
70 import org.sleuthkit.datamodel.TskDataException;
71 import org.sleuthkit.datamodel.TskException;
72 
77 final class VcardParser {
78  private static final String VCARD_HEADER = "BEGIN:VCARD";
79  private static final long MIN_FILE_SIZE = 22;
80 
81  private static final String PHOTO_TYPE_BMP = "bmp";
82  private static final String PHOTO_TYPE_GIF = "gif";
83  private static final String PHOTO_TYPE_JPEG = "jpeg";
84  private static final String PHOTO_TYPE_PNG = "png";
85  private static final Map<String, String> photoTypeExtensions;
86  static {
87  photoTypeExtensions = new HashMap<>();
88  photoTypeExtensions.put(PHOTO_TYPE_BMP, ".bmp");
89  photoTypeExtensions.put(PHOTO_TYPE_GIF, ".gif");
90  photoTypeExtensions.put(PHOTO_TYPE_JPEG, ".jpg");
91  photoTypeExtensions.put(PHOTO_TYPE_PNG, ".png");
92  }
93 
94  private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
95 
96  private final IngestServices services = IngestServices.getInstance();
97  private final FileManager fileManager;
98  private final IngestJobContext context;
99  private final Blackboard blackboard;
100  private final Case currentCase;
101  private final SleuthkitCase tskCase;
106  private final ConcurrentMap<String, BlackboardAttribute.Type> customAttributeCache;
107 
111  VcardParser(Case currentCase, IngestJobContext context, ConcurrentMap<String, BlackboardAttribute.Type> customAttributeCache) {
112  this.context = context;
113  this.currentCase = currentCase;
114  tskCase = currentCase.getSleuthkitCase();
115  blackboard = tskCase.getBlackboard();
116  fileManager = currentCase.getServices().getFileManager();
117  this.customAttributeCache = customAttributeCache;
118  }
119 
127  static boolean isVcardFile(Content content) {
128  try {
129  if (content.getSize() > MIN_FILE_SIZE) {
130  byte[] buffer = new byte[VCARD_HEADER.length()];
131  int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
132  if (byteRead > 0) {
133  String header = new String(buffer);
134  return header.equalsIgnoreCase(VCARD_HEADER);
135  }
136  }
137  } catch (TskException ex) {
138  logger.log(Level.WARNING, String.format("Exception while detecting if the file '%s' (id=%d) is a vCard file.",
139  content.getName(), content.getId())); //NON-NLS
140  }
141 
142  return false;
143  }
144 
156  void parse(AbstractFile abstractFile) throws IOException, NoCurrentCaseException {
157  for (VCard vcard: Ezvcard.parse(new InputStreamReader(new BufferedInputStream(new ReadContentInputStream(abstractFile)), StandardCharsets.UTF_8)).all()) {
158  addContactArtifact(vcard, abstractFile);
159  }
160  }
161 
162 
163 
174  @NbBundle.Messages({"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
175  private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile) throws NoCurrentCaseException {
176  List<BlackboardAttribute> attributes = new ArrayList<>();
177  List<AccountFileInstance> accountInstances = new ArrayList<>();
178 
179  String name = "";
180  if (vcard.getFormattedName() != null) {
181  name = vcard.getFormattedName().getValue();
182  } else {
183  if (vcard.getStructuredName() != null) {
184  // Attempt to put the name together if there was no formatted version
185  for (String prefix:vcard.getStructuredName().getPrefixes()) {
186  name += prefix + " ";
187  }
188  if (vcard.getStructuredName().getGiven() != null) {
189  name += vcard.getStructuredName().getGiven() + " ";
190  }
191  if (vcard.getStructuredName().getFamily() != null) {
192  name += vcard.getStructuredName().getFamily() + " ";
193  }
194  for (String suffix:vcard.getStructuredName().getSuffixes()) {
195  name += suffix + " ";
196  }
197  if (! vcard.getStructuredName().getAdditionalNames().isEmpty()) {
198  name += "(";
199  for (String addName:vcard.getStructuredName().getAdditionalNames()) {
200  name += addName + " ";
201  }
202  name += ")";
203  }
204  }
205  }
206  ThunderbirdMboxFileIngestModule.addArtifactAttribute(name, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, attributes);
207 
208  for (Telephone telephone : vcard.getTelephoneNumbers()) {
209  addPhoneAttributes(telephone, abstractFile, attributes);
210  addPhoneAccountInstances(telephone, abstractFile, accountInstances);
211  }
212 
213  for (Email email : vcard.getEmails()) {
214  addEmailAttributes(email, abstractFile, attributes);
215  addEmailAccountInstances(email, abstractFile, accountInstances);
216  }
217 
218  for (Url url : vcard.getUrls()) {
219  ThunderbirdMboxFileIngestModule.addArtifactAttribute(url.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, attributes);
220  }
221 
222  for (Organization organization : vcard.getOrganizations()) {
223  List<String> values = organization.getValues();
224  if (values.isEmpty() == false) {
225  ThunderbirdMboxFileIngestModule.addArtifactAttribute(values.get(0), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ORGANIZATION, attributes);
226  }
227  }
228 
229  AccountFileInstance deviceAccountInstance = addDeviceAccountInstance(abstractFile);
230 
231  BlackboardArtifact artifact = null;
232  org.sleuthkit.datamodel.Blackboard tskBlackboard = tskCase.getBlackboard();
233  try {
234  // Create artifact if it doesn't already exist.
235  if (!tskBlackboard.artifactExists(abstractFile, BlackboardArtifact.Type.TSK_CONTACT, attributes)) {
236  artifact = abstractFile.newDataArtifact(new BlackboardArtifact.Type(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT), attributes);
237 
238  extractPhotos(vcard, abstractFile, artifact);
239 
240  // Add account relationships.
241  if (deviceAccountInstance != null) {
242  try {
243  currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(
244  deviceAccountInstance, accountInstances, artifact, Relationship.Type.CONTACT, abstractFile.getCrtime());
245  } catch (TskDataException ex) {
246  logger.log(Level.SEVERE, String.format("Failed to create phone and e-mail account relationships (fileName='%s'; fileId=%d; accountId=%d).",
247  abstractFile.getName(), abstractFile.getId(), deviceAccountInstance.getAccount().getAccountID()), ex); //NON-NLS
248  }
249  }
250 
251  // Index the artifact for keyword search.
252  try {
253  blackboard.postArtifact(artifact, EmailParserModuleFactory.getModuleName(), context.getJobId());
254  } catch (Blackboard.BlackboardException ex) {
255  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
256  MessageNotifyUtil.Notify.error(Bundle.VcardParser_addContactArtifact_indexError(), artifact.getDisplayName());
257  }
258  }
259  } catch (TskCoreException ex) {
260  logger.log(Level.SEVERE, String.format("Failed to create contact artifact for vCard file '%s' (id=%d).",
261  abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
262  }
263 
264  return artifact;
265  }
266 
275  private void extractPhotos(VCard vcard, AbstractFile abstractFile, BlackboardArtifact artifact) throws NoCurrentCaseException {
276  String parentFileName = getUniqueName(abstractFile);
277  // Skip files that already have been extracted.
278  try {
279  String outputPath = getOutputFolderPath(parentFileName);
280  if (new File(outputPath).exists()) {
281  List<Photo> vcardPhotos = vcard.getPhotos();
282  List<AbstractFile> derivedFilesCreated = new ArrayList<>();
283  for (int i=0; i < vcardPhotos.size(); i++) {
284  Photo photo = vcardPhotos.get(i);
285 
286  if (photo.getUrl() != null) {
287  // Skip this photo since its data is not embedded.
288  continue;
289  }
290 
291  String type = photo.getType();
292  if (type == null) {
293  // Skip this photo since no type is defined.
294  continue;
295  }
296 
297  // Get the file extension for the subtype.
298  type = type.toLowerCase();
299  if (type.startsWith("image/")) {
300  type = type.substring(6);
301  }
302  String extension = photoTypeExtensions.get(type);
303 
304  // Read the photo data and create a derived file from it.
305  byte[] data = photo.getData();
306  String extractedFileName = String.format("photo_%d%s", i, extension == null ? "" : extension);
307  String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
308  try {
309  writeExtractedImage(extractedFilePath, data);
310  derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
311  abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
312  true, artifact, null, EmailParserModuleFactory.getModuleName(), EmailParserModuleFactory.getModuleVersion(), "", TskData.EncodingType.NONE));
313  } catch (IOException | TskCoreException ex) {
314  logger.log(Level.WARNING, String.format("Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex); //NON-NLS
315  }
316  }
317  if (!derivedFilesCreated.isEmpty()) {
318  services.fireModuleContentEvent(new ModuleContentEvent(abstractFile));
319  context.addFilesToJob(derivedFilesCreated);
320  }
321  }
322  else {
323  logger.log(Level.INFO, String.format("Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
324  abstractFile.getName(), abstractFile.getId())); //NON-NLS
325  }
326  } catch (SecurityException ex) {
327  logger.log(Level.WARNING, String.format("Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
328  }
329  }
330 
338  private void writeExtractedImage(String outputPath, byte[] data) throws IOException {
339  File outputFile = new File(outputPath);
340  FileOutputStream outputStream = new FileOutputStream(outputFile);
341  outputStream.write(data);
342  }
343 
352  private String getUniqueName(AbstractFile file) {
353  return file.getName() + "_" + file.getId();
354  }
355 
365  private String getFileRelativePath(String parentFileName, String fileName) throws NoCurrentCaseException {
366  // Used explicit FWD slashes to maintain DB consistency across operating systems.
367  return Paths.get(getRelModuleOutputPath(), parentFileName, fileName).toString();
368  }
369 
380  private String getOutputFolderPath(String parentFileName) throws NoCurrentCaseException {
381  String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
382  File outputFilePath = new File(outputFolderPath);
383  if (!outputFilePath.exists()) {
384  outputFilePath.mkdirs();
385  }
386  return outputFolderPath;
387  }
388 
397  private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
398  String telephoneText = telephone.getText();
399 
400  if (telephoneText == null || telephoneText.isEmpty()) {
401  if (telephone.getUri() == null) {
402  return;
403  }
404  telephoneText = telephone.getUri().getNumber();
405  if (telephoneText == null || telephoneText.isEmpty()) {
406  return;
407  }
408  }
409 
410  // Add phone number to collection for later creation of TSK_CONTACT.
411  List<TelephoneType> telephoneTypes = telephone.getTypes();
412  if (telephoneTypes.isEmpty()) {
413  ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
414  } else {
415  TelephoneType type = telephoneTypes.get(0);
416  /*
417  * Unfortunately, if the types are lower-case, they don't
418  * get separated correctly into individual TelephoneTypes by
419  * ez-vcard. Therefore, we must read them manually
420  * ourselves.
421  */
422  List<String> splitTelephoneTypes = Arrays.asList(
423  type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
424 
425  if (splitTelephoneTypes.size() > 0) {
426  String splitType = splitTelephoneTypes.get(0);
427  String attributeTypeName = "TSK_PHONE_NUMBER";
428  if (splitType != null && !splitType.isEmpty()) {
429  attributeTypeName = "TSK_PHONE_NUMBER_" + splitType;
430  }
431 
432  final String finalAttrTypeName = attributeTypeName;
433 
434  // handled in computeIfAbsent to remove concurrency issues when adding to this concurrent hashmap.
435  BlackboardAttribute.Type attributeType
436  = this.customAttributeCache.computeIfAbsent(finalAttrTypeName, k -> {
437  try {
438  // Add this attribute type to the case database.
439  return tskCase.getBlackboard().getOrAddAttributeType(finalAttrTypeName,
440  BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
441  String.format("Phone Number (%s)", StringUtils.capitalize(splitType.toLowerCase())));
442 
443  } catch (BlackboardException ex) {
444  VcardParser.logger.log(Level.WARNING, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).",
445  finalAttrTypeName, abstractFile.getName(), abstractFile.getId()), ex);
446  return null;
447  }
448  });
449 
450  if (attributeType != null) {
451  ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephoneText, attributeType, attributes);
452  }
453  }
454  }
455  }
456 
465  private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
466  String emailValue = email.getValue();
467  if (emailValue == null || emailValue.isEmpty()) {
468  return;
469  }
470 
471  // Add phone number to collection for later creation of TSK_CONTACT.
472  List<EmailType> emailTypes = email.getTypes();
473  if (emailTypes.isEmpty()) {
474  ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
475  } else {
476  EmailType type = emailTypes.get(0); /*
477  * Unfortunately, if the types are lower-case, they don't
478  * get separated correctly into individual EmailTypes by
479  * ez-vcard. Therefore, we must read them manually
480  * ourselves.
481  */
482  List<String> splitEmailTypes = Arrays.asList(
483  type.getValue().toUpperCase().replaceAll("\\s+", "").split(","));
484 
485  if (splitEmailTypes.size() > 0) {
486  String splitType = splitEmailTypes.get(0);
487  String attributeTypeName = "TSK_EMAIL_" + splitType;
488  if (splitType.isEmpty()) {
489  attributeTypeName = "TSK_EMAIL";
490  }
491 
492  final String finalAttributeTypeName = attributeTypeName;
493 
494  BlackboardAttribute.Type attributeType
495  = this.customAttributeCache.computeIfAbsent(finalAttributeTypeName, k -> {
496  try {
497  // Add this attribute type to the case database.
498  return tskCase.getBlackboard().getOrAddAttributeType(finalAttributeTypeName,
499  BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
500  String.format("Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
501  } catch (BlackboardException ex) {
502  logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).",
503  finalAttributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
504  }
505 
506  return null;
507  });
508 
509  if (attributeType != null) {
510  ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
511  }
512  }
513  }
514  }
515 
525  private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
526  String telephoneText = telephone.getText();
527  if (telephoneText == null || telephoneText.isEmpty()) {
528  if (telephone.getUri() == null) {
529  return;
530  }
531  telephoneText = telephone.getUri().getNumber();
532  if (telephoneText == null || telephoneText.isEmpty()) {
533  return;
534  }
535 
536  }
537 
538  // Add phone number as a TSK_ACCOUNT.
539  try {
540  AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
541  telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile, null, context.getJobId());
542  accountInstances.add(phoneAccountInstance);
543  }
544  catch(TskCoreException ex) {
545  logger.log(Level.WARNING, String.format(
546  "Failed to create account for phone number '%s' (content='%s'; id=%d).",
547  telephoneText, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
548  }
549  }
550 
560  private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
561  String emailValue = email.getValue();
562  if (emailValue == null || emailValue.isEmpty()) {
563  return;
564  }
565 
566  // Add e-mail as a TSK_ACCOUNT.
567  try {
568  AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
569  emailValue, EmailParserModuleFactory.getModuleName(), abstractFile, null, context.getJobId());
570  accountInstances.add(emailAccountInstance);
571  }
572  catch(TskCoreException ex) {
573  logger.log(Level.WARNING, String.format(
574  "Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
575  emailValue, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
576  }
577  }
578 
586  private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
587  // Add 'DEVICE' TSK_ACCOUNT.
588  AccountFileInstance deviceAccountInstance = null;
589  String deviceId = null;
590  try {
591  long dataSourceObjId = abstractFile.getDataSourceObjectId();
592  DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
593  deviceId = dataSource.getDeviceId();
594  deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
595  deviceId, EmailParserModuleFactory.getModuleName(), abstractFile, null, context.getJobId());
596  }
597  catch (TskCoreException ex) {
598  logger.log(Level.WARNING, String.format(
599  "Failed to create device account for '%s' (content='%s'; id=%d).",
600  deviceId, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
601  }
602  catch (TskDataException ex) {
603  logger.log(Level.WARNING, String.format(
604  "Failed to get the data source from the case database (id=%d).",
605  abstractFile.getId()), ex); //NON-NLS
606  }
607 
608  return deviceAccountInstance;
609  }
610 }

Copyright © 2012-2022 Basis Technology. Generated on: Tue Jun 27 2023
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.