Autopsy  4.11.0
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.File;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.nio.file.Paths;
34 import java.util.ArrayList;
35 import java.util.Arrays;
36 import java.util.Collection;
37 import java.util.HashMap;
38 import java.util.List;
39 import java.util.Map;
40 import java.util.logging.Level;
41 import org.apache.commons.lang3.StringUtils;
42 import org.openide.util.NbBundle;
53 import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
54 import org.sleuthkit.datamodel.AbstractFile;
55 import org.sleuthkit.datamodel.Account;
56 import org.sleuthkit.datamodel.AccountFileInstance;
57 import org.sleuthkit.datamodel.BlackboardArtifact;
58 import org.sleuthkit.datamodel.BlackboardAttribute;
59 import org.sleuthkit.datamodel.Content;
60 import org.sleuthkit.datamodel.DataSource;
61 import org.sleuthkit.datamodel.Relationship;
62 import org.sleuthkit.datamodel.SleuthkitCase;
63 import org.sleuthkit.datamodel.TskCoreException;
64 import org.sleuthkit.datamodel.TskData;
65 import org.sleuthkit.datamodel.TskDataException;
66 import org.sleuthkit.datamodel.TskException;
67 
72 final class VcardParser {
73  private static final String VCARD_HEADER = "BEGIN:VCARD";
74  private static final long MIN_FILE_SIZE = 22;
75 
76  private static final String PHOTO_TYPE_BMP = "bmp";
77  private static final String PHOTO_TYPE_GIF = "gif";
78  private static final String PHOTO_TYPE_JPEG = "jpeg";
79  private static final String PHOTO_TYPE_PNG = "png";
80  private static final Map<String, String> photoTypeExtensions;
81  static {
82  photoTypeExtensions = new HashMap<>();
83  photoTypeExtensions.put(PHOTO_TYPE_BMP, ".bmp");
84  photoTypeExtensions.put(PHOTO_TYPE_GIF, ".gif");
85  photoTypeExtensions.put(PHOTO_TYPE_JPEG, ".jpg");
86  photoTypeExtensions.put(PHOTO_TYPE_PNG, ".png");
87  }
88 
89  private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
90 
91  private final IngestServices services = IngestServices.getInstance();
92  private final FileManager fileManager;
93  private final IngestJobContext context;
94  private final Blackboard blackboard;
95  private final Case currentCase;
96  private final SleuthkitCase tskCase;
97 
101  VcardParser(Case currentCase, IngestJobContext context) {
102  this.context = context;
103  this.currentCase = currentCase;
104  tskCase = currentCase.getSleuthkitCase();
105  blackboard = currentCase.getServices().getBlackboard();
106  fileManager = currentCase.getServices().getFileManager();
107  }
108 
116  static boolean isVcardFile(Content content) {
117  try {
118  if (content.getSize() > MIN_FILE_SIZE) {
119  byte[] buffer = new byte[VCARD_HEADER.length()];
120  int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
121  if (byteRead > 0) {
122  String header = new String(buffer);
123  return header.equalsIgnoreCase(VCARD_HEADER);
124  }
125  }
126  } catch (TskException ex) {
127  logger.log(Level.WARNING, String.format("Exception while detecting if the file '%s' (id=%d) is a vCard file.",
128  content.getName(), content.getId())); //NON-NLS
129  }
130 
131  return false;
132  }
133 
145  void parse(File vcardFile, AbstractFile abstractFile) throws IOException, NoCurrentCaseException {
146  for (VCard vcard: Ezvcard.parse(vcardFile).all()) {
147  addContactArtifact(vcard, abstractFile);
148  }
149  }
150 
151 
152 
163  @NbBundle.Messages({"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
164  private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile) throws NoCurrentCaseException {
165  List<BlackboardAttribute> attributes = new ArrayList<>();
166  List<AccountFileInstance> accountInstances = new ArrayList<>();
167 
168  extractPhotos(vcard, abstractFile);
169 
170  String name = "";
171  if (vcard.getFormattedName() != null) {
172  name = vcard.getFormattedName().getValue();
173  } else {
174  if (vcard.getStructuredName() != null) {
175  // Attempt to put the name together if there was no formatted version
176  for (String prefix:vcard.getStructuredName().getPrefixes()) {
177  name += prefix + " ";
178  }
179  if (vcard.getStructuredName().getGiven() != null) {
180  name += vcard.getStructuredName().getGiven() + " ";
181  }
182  if (vcard.getStructuredName().getFamily() != null) {
183  name += vcard.getStructuredName().getFamily() + " ";
184  }
185  for (String suffix:vcard.getStructuredName().getSuffixes()) {
186  name += suffix + " ";
187  }
188  if (! vcard.getStructuredName().getAdditionalNames().isEmpty()) {
189  name += "(";
190  for (String addName:vcard.getStructuredName().getAdditionalNames()) {
191  name += addName + " ";
192  }
193  name += ")";
194  }
195  }
196  }
197  ThunderbirdMboxFileIngestModule.addArtifactAttribute(name, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, attributes);
198 
199  for (Telephone telephone : vcard.getTelephoneNumbers()) {
200  addPhoneAttributes(telephone, abstractFile, attributes);
201  addPhoneAccountInstances(telephone, abstractFile, accountInstances);
202  }
203 
204  for (Email email : vcard.getEmails()) {
205  addEmailAttributes(email, abstractFile, attributes);
206  addEmailAccountInstances(email, abstractFile, accountInstances);
207  }
208 
209  for (Url url : vcard.getUrls()) {
210  ThunderbirdMboxFileIngestModule.addArtifactAttribute(url.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, attributes);
211  }
212 
213  for (Organization organization : vcard.getOrganizations()) {
214  List<String> values = organization.getValues();
215  if (values.isEmpty() == false) {
216  ThunderbirdMboxFileIngestModule.addArtifactAttribute(values.get(0), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ORGANIZATION, attributes);
217  }
218  }
219 
220  AccountFileInstance deviceAccountInstance = addDeviceAccountInstance(abstractFile);
221 
222  BlackboardArtifact artifact = null;
223  org.sleuthkit.datamodel.Blackboard tskBlackboard = tskCase.getBlackboard();
224  try {
225  // Create artifact if it doesn't already exist.
226  if (!tskBlackboard.artifactExists(abstractFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT, attributes)) {
227  artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT);
228  artifact.addAttributes(attributes);
229  List<BlackboardArtifact> blackboardArtifacts = new ArrayList<>();
230  blackboardArtifacts.add(artifact);
231 
232  // Add account relationships.
233  if (deviceAccountInstance != null) {
234  try {
235  currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(
236  deviceAccountInstance, accountInstances, artifact, Relationship.Type.CONTACT, abstractFile.getCrtime());
237  } catch (TskDataException ex) {
238  logger.log(Level.SEVERE, String.format("Failed to create phone and e-mail account relationships (fileName='%s'; fileId=%d; accountId=%d).",
239  abstractFile.getName(), abstractFile.getId(), deviceAccountInstance.getAccount().getAccountID()), ex); //NON-NLS
240  }
241  }
242 
243  // Index the artifact for keyword search.
244  try {
245  blackboard.indexArtifact(artifact);
246  } catch (Blackboard.BlackboardException ex) {
247  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
248  MessageNotifyUtil.Notify.error(Bundle.VcardParser_addContactArtifact_indexError(), artifact.getDisplayName());
249  }
250 
251  // Fire event to notify UI of this new artifact.
252  IngestServices.getInstance().fireModuleDataEvent(new ModuleDataEvent(
253  EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT,
254  blackboardArtifacts));
255  }
256  } catch (TskCoreException ex) {
257  logger.log(Level.SEVERE, String.format("Failed to create contact artifact for vCard file '%s' (id=%d).",
258  abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
259  }
260 
261  return artifact;
262  }
263 
272  private void extractPhotos(VCard vcard, AbstractFile abstractFile) throws NoCurrentCaseException {
273  String parentFileName = getUniqueName(abstractFile);
274  // Skip files that already have been extracted.
275  try {
276  String outputPath = getOutputFolderPath(parentFileName);
277  if (new File(outputPath).exists()) {
278  List<Photo> vcardPhotos = vcard.getPhotos();
279  List<AbstractFile> derivedFilesCreated = new ArrayList<>();
280  for (int i=0; i < vcardPhotos.size(); i++) {
281  Photo photo = vcardPhotos.get(i);
282 
283  if (photo.getUrl() != null) {
284  // Skip this photo since its data is not embedded.
285  continue;
286  }
287 
288  String type = photo.getType();
289  if (type == null) {
290  // Skip this photo since no type is defined.
291  continue;
292  }
293 
294  // Get the file extension for the subtype.
295  type = type.toLowerCase();
296  if (type.startsWith("image/")) {
297  type = type.substring(6);
298  }
299  String extension = photoTypeExtensions.get(type);
300 
301  // Read the photo data and create a derived file from it.
302  byte[] data = photo.getData();
303  String extractedFileName = String.format("photo_%d%s", i, extension == null ? "" : extension);
304  String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
305  try {
306  writeExtractedImage(extractedFilePath, data);
307  derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
308  abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
309  true, abstractFile, null, EmailParserModuleFactory.getModuleName(), null, null, TskData.EncodingType.NONE));
310  } catch (IOException | TskCoreException ex) {
311  logger.log(Level.WARNING, String.format("Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex); //NON-NLS
312  }
313  }
314  if (!derivedFilesCreated.isEmpty()) {
315  services.fireModuleContentEvent(new ModuleContentEvent(abstractFile));
316  context.addFilesToJob(derivedFilesCreated);
317  }
318  }
319  else {
320  logger.log(Level.INFO, String.format("Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
321  abstractFile.getName(), abstractFile.getId())); //NON-NLS
322  }
323  } catch (SecurityException ex) {
324  logger.log(Level.WARNING, String.format("Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
325  }
326  }
327 
335  private void writeExtractedImage(String outputPath, byte[] data) throws IOException {
336  File outputFile = new File(outputPath);
337  FileOutputStream outputStream = new FileOutputStream(outputFile);
338  outputStream.write(data);
339  }
340 
349  private String getUniqueName(AbstractFile file) {
350  return file.getName() + "_" + file.getId();
351  }
352 
362  private String getFileRelativePath(String parentFileName, String fileName) throws NoCurrentCaseException {
363  // Used explicit FWD slashes to maintain DB consistency across operating systems.
364  return Paths.get(getRelModuleOutputPath(), parentFileName, fileName).toString();
365  }
366 
377  private String getOutputFolderPath(String parentFileName) throws NoCurrentCaseException {
378  String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
379  File outputFilePath = new File(outputFolderPath);
380  if (!outputFilePath.exists()) {
381  outputFilePath.mkdirs();
382  }
383  return outputFolderPath;
384  }
385 
394  private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
395  String telephoneText = telephone.getText();
396  if (telephoneText == null || telephoneText.isEmpty()) {
397  return;
398  }
399 
400  // Add phone number to collection for later creation of TSK_CONTACT.
401  List<TelephoneType> telephoneTypes = telephone.getTypes();
402  if (telephoneTypes.isEmpty()) {
403  ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
404  } else {
405  for (TelephoneType type : telephoneTypes) {
406  /*
407  * Unfortunately, if the types are lower-case, they don't
408  * get separated correctly into individual TelephoneTypes by
409  * ez-vcard. Therefore, we must read them manually
410  * ourselves.
411  */
412  List<String> splitTelephoneTypes = Arrays.asList(
413  type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
414 
415  for (String splitType : splitTelephoneTypes) {
416  String attributeTypeName = "TSK_PHONE_NUMBER_" + splitType;
417  try {
418  BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
419  if (attributeType == null) {
420  // Add this attribute type to the case database.
421  attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
422  BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
423  String.format("Phone (%s)", StringUtils.capitalize(splitType.toLowerCase())));
424  }
425  ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), attributeType, attributes);
426  } catch (TskCoreException ex) {
427  logger.log(Level.SEVERE, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
428  } catch (TskDataException ex) {
429  logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
430  }
431  }
432  }
433  }
434  }
435 
444  private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
445  String emailValue = email.getValue();
446  if (emailValue == null || emailValue.isEmpty()) {
447  return;
448  }
449 
450  // Add phone number to collection for later creation of TSK_CONTACT.
451  List<EmailType> emailTypes = email.getTypes();
452  if (emailTypes.isEmpty()) {
453  ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
454  } else {
455  for (EmailType type : emailTypes) {
456  /*
457  * Unfortunately, if the types are lower-case, they don't
458  * get separated correctly into individual EmailTypes by
459  * ez-vcard. Therefore, we must read them manually
460  * ourselves.
461  */
462  List<String> splitEmailTypes = Arrays.asList(
463  type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
464 
465  for (String splitType : splitEmailTypes) {
466  String attributeTypeName = "TSK_EMAIL_" + splitType;
467  try {
468  BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
469  if (attributeType == null) {
470  // Add this attribute type to the case database.
471  attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
472  BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
473  String.format("Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
474  }
475  ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
476  } catch (TskCoreException ex) {
477  logger.log(Level.SEVERE, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
478  } catch (TskDataException ex) {
479  logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
480  }
481  }
482  }
483  }
484  }
485 
495  private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
496  String telephoneText = telephone.getText();
497  if (telephoneText == null || telephoneText.isEmpty()) {
498  return;
499  }
500 
501  // Add phone number as a TSK_ACCOUNT.
502  try {
503  AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
504  telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile);
505  accountInstances.add(phoneAccountInstance);
506  }
507  catch(TskCoreException ex) {
508  logger.log(Level.WARNING, String.format(
509  "Failed to create account for phone number '%s' (content='%s'; id=%d).",
510  telephoneText, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
511  }
512  }
513 
523  private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
524  String emailValue = email.getValue();
525  if (emailValue == null || emailValue.isEmpty()) {
526  return;
527  }
528 
529  // Add e-mail as a TSK_ACCOUNT.
530  try {
531  AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
532  emailValue, EmailParserModuleFactory.getModuleName(), abstractFile);
533  accountInstances.add(emailAccountInstance);
534  }
535  catch(TskCoreException ex) {
536  logger.log(Level.WARNING, String.format(
537  "Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
538  emailValue, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
539  }
540  }
541 
549  private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
550  // Add 'DEVICE' TSK_ACCOUNT.
551  AccountFileInstance deviceAccountInstance = null;
552  String deviceId = null;
553  try {
554  long dataSourceObjId = abstractFile.getDataSourceObjectId();
555  DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
556  deviceId = dataSource.getDeviceId();
557  deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
558  deviceId, EmailParserModuleFactory.getModuleName(), abstractFile);
559  }
560  catch (TskCoreException ex) {
561  logger.log(Level.WARNING, String.format(
562  "Failed to create device account for '%s' (content='%s'; id=%d).",
563  deviceId, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
564  }
565  catch (TskDataException ex) {
566  logger.log(Level.WARNING, String.format(
567  "Failed to get the data source from the case database (id=%d).",
568  abstractFile.getId()), ex); //NON-NLS
569  }
570 
571  return deviceAccountInstance;
572  }
573 }

Copyright © 2012-2018 Basis Technology. Generated on: Fri Jun 21 2019
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.