Autopsy  4.17.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;
51 import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
52 import org.sleuthkit.datamodel.AbstractFile;
53 import org.sleuthkit.datamodel.Account;
54 import org.sleuthkit.datamodel.AccountFileInstance;
55 import org.sleuthkit.datamodel.Blackboard;
56 import org.sleuthkit.datamodel.Blackboard.BlackboardException;
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.ReadContentInputStream;
62 import org.sleuthkit.datamodel.Relationship;
63 import org.sleuthkit.datamodel.SleuthkitCase;
64 import org.sleuthkit.datamodel.TskCoreException;
65 import org.sleuthkit.datamodel.TskData;
66 import org.sleuthkit.datamodel.TskDataException;
67 import org.sleuthkit.datamodel.TskException;
68 
73 final class VcardParser {
74  private static final String VCARD_HEADER = "BEGIN:VCARD";
75  private static final long MIN_FILE_SIZE = 22;
76 
77  private static final String PHOTO_TYPE_BMP = "bmp";
78  private static final String PHOTO_TYPE_GIF = "gif";
79  private static final String PHOTO_TYPE_JPEG = "jpeg";
80  private static final String PHOTO_TYPE_PNG = "png";
81  private static final Map<String, String> photoTypeExtensions;
82  static {
83  photoTypeExtensions = new HashMap<>();
84  photoTypeExtensions.put(PHOTO_TYPE_BMP, ".bmp");
85  photoTypeExtensions.put(PHOTO_TYPE_GIF, ".gif");
86  photoTypeExtensions.put(PHOTO_TYPE_JPEG, ".jpg");
87  photoTypeExtensions.put(PHOTO_TYPE_PNG, ".png");
88  }
89 
90  private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
91 
92  private final IngestServices services = IngestServices.getInstance();
93  private final FileManager fileManager;
94  private final IngestJobContext context;
95  private final Blackboard blackboard;
96  private final Case currentCase;
97  private final SleuthkitCase tskCase;
98 
102  VcardParser(Case currentCase, IngestJobContext context) {
103  this.context = context;
104  this.currentCase = currentCase;
105  tskCase = currentCase.getSleuthkitCase();
106  blackboard = tskCase.getBlackboard();
107  fileManager = currentCase.getServices().getFileManager();
108  }
109 
117  static boolean isVcardFile(Content content) {
118  try {
119  if (content.getSize() > MIN_FILE_SIZE) {
120  byte[] buffer = new byte[VCARD_HEADER.length()];
121  int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
122  if (byteRead > 0) {
123  String header = new String(buffer);
124  return header.equalsIgnoreCase(VCARD_HEADER);
125  }
126  }
127  } catch (TskException ex) {
128  logger.log(Level.WARNING, String.format("Exception while detecting if the file '%s' (id=%d) is a vCard file.",
129  content.getName(), content.getId())); //NON-NLS
130  }
131 
132  return false;
133  }
134 
146  void parse(AbstractFile abstractFile) throws IOException, NoCurrentCaseException {
147  for (VCard vcard: Ezvcard.parse(new ReadContentInputStream(abstractFile)).all()) {
148  addContactArtifact(vcard, abstractFile);
149  }
150  }
151 
152 
153 
164  @NbBundle.Messages({"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
165  private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile) throws NoCurrentCaseException {
166  List<BlackboardAttribute> attributes = new ArrayList<>();
167  List<AccountFileInstance> accountInstances = new ArrayList<>();
168 
169  String name = "";
170  if (vcard.getFormattedName() != null) {
171  name = vcard.getFormattedName().getValue();
172  } else {
173  if (vcard.getStructuredName() != null) {
174  // Attempt to put the name together if there was no formatted version
175  for (String prefix:vcard.getStructuredName().getPrefixes()) {
176  name += prefix + " ";
177  }
178  if (vcard.getStructuredName().getGiven() != null) {
179  name += vcard.getStructuredName().getGiven() + " ";
180  }
181  if (vcard.getStructuredName().getFamily() != null) {
182  name += vcard.getStructuredName().getFamily() + " ";
183  }
184  for (String suffix:vcard.getStructuredName().getSuffixes()) {
185  name += suffix + " ";
186  }
187  if (! vcard.getStructuredName().getAdditionalNames().isEmpty()) {
188  name += "(";
189  for (String addName:vcard.getStructuredName().getAdditionalNames()) {
190  name += addName + " ";
191  }
192  name += ")";
193  }
194  }
195  }
196  ThunderbirdMboxFileIngestModule.addArtifactAttribute(name, BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME, attributes);
197 
198  for (Telephone telephone : vcard.getTelephoneNumbers()) {
199  addPhoneAttributes(telephone, abstractFile, attributes);
200  addPhoneAccountInstances(telephone, abstractFile, accountInstances);
201  }
202 
203  for (Email email : vcard.getEmails()) {
204  addEmailAttributes(email, abstractFile, attributes);
205  addEmailAccountInstances(email, abstractFile, accountInstances);
206  }
207 
208  for (Url url : vcard.getUrls()) {
209  ThunderbirdMboxFileIngestModule.addArtifactAttribute(url.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, attributes);
210  }
211 
212  for (Organization organization : vcard.getOrganizations()) {
213  List<String> values = organization.getValues();
214  if (values.isEmpty() == false) {
215  ThunderbirdMboxFileIngestModule.addArtifactAttribute(values.get(0), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ORGANIZATION, attributes);
216  }
217  }
218 
219  AccountFileInstance deviceAccountInstance = addDeviceAccountInstance(abstractFile);
220 
221  BlackboardArtifact artifact = null;
222  org.sleuthkit.datamodel.Blackboard tskBlackboard = tskCase.getBlackboard();
223  try {
224  // Create artifact if it doesn't already exist.
225  if (!tskBlackboard.artifactExists(abstractFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT, attributes)) {
226  artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT);
227  artifact.addAttributes(attributes);
228 
229  extractPhotos(vcard, abstractFile, artifact);
230 
231  // Add account relationships.
232  if (deviceAccountInstance != null) {
233  try {
234  currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(
235  deviceAccountInstance, accountInstances, artifact, Relationship.Type.CONTACT, abstractFile.getCrtime());
236  } catch (TskDataException ex) {
237  logger.log(Level.SEVERE, String.format("Failed to create phone and e-mail account relationships (fileName='%s'; fileId=%d; accountId=%d).",
238  abstractFile.getName(), abstractFile.getId(), deviceAccountInstance.getAccount().getAccountID()), ex); //NON-NLS
239  }
240  }
241 
242  // Index the artifact for keyword search.
243  try {
244  blackboard.postArtifact(artifact, EmailParserModuleFactory.getModuleName());
245  } catch (Blackboard.BlackboardException ex) {
246  logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
247  MessageNotifyUtil.Notify.error(Bundle.VcardParser_addContactArtifact_indexError(), artifact.getDisplayName());
248  }
249  }
250  } catch (TskCoreException ex) {
251  logger.log(Level.SEVERE, String.format("Failed to create contact artifact for vCard file '%s' (id=%d).",
252  abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
253  }
254 
255  return artifact;
256  }
257 
266  private void extractPhotos(VCard vcard, AbstractFile abstractFile, BlackboardArtifact artifact) throws NoCurrentCaseException {
267  String parentFileName = getUniqueName(abstractFile);
268  // Skip files that already have been extracted.
269  try {
270  String outputPath = getOutputFolderPath(parentFileName);
271  if (new File(outputPath).exists()) {
272  List<Photo> vcardPhotos = vcard.getPhotos();
273  List<AbstractFile> derivedFilesCreated = new ArrayList<>();
274  for (int i=0; i < vcardPhotos.size(); i++) {
275  Photo photo = vcardPhotos.get(i);
276 
277  if (photo.getUrl() != null) {
278  // Skip this photo since its data is not embedded.
279  continue;
280  }
281 
282  String type = photo.getType();
283  if (type == null) {
284  // Skip this photo since no type is defined.
285  continue;
286  }
287 
288  // Get the file extension for the subtype.
289  type = type.toLowerCase();
290  if (type.startsWith("image/")) {
291  type = type.substring(6);
292  }
293  String extension = photoTypeExtensions.get(type);
294 
295  // Read the photo data and create a derived file from it.
296  byte[] data = photo.getData();
297  String extractedFileName = String.format("photo_%d%s", i, extension == null ? "" : extension);
298  String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
299  try {
300  writeExtractedImage(extractedFilePath, data);
301  derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
302  abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
303  true, artifact, null, EmailParserModuleFactory.getModuleName(), EmailParserModuleFactory.getModuleVersion(), "", TskData.EncodingType.NONE));
304  } catch (IOException | TskCoreException ex) {
305  logger.log(Level.WARNING, String.format("Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex); //NON-NLS
306  }
307  }
308  if (!derivedFilesCreated.isEmpty()) {
309  services.fireModuleContentEvent(new ModuleContentEvent(abstractFile));
310  context.addFilesToJob(derivedFilesCreated);
311  }
312  }
313  else {
314  logger.log(Level.INFO, String.format("Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
315  abstractFile.getName(), abstractFile.getId())); //NON-NLS
316  }
317  } catch (SecurityException ex) {
318  logger.log(Level.WARNING, String.format("Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
319  }
320  }
321 
329  private void writeExtractedImage(String outputPath, byte[] data) throws IOException {
330  File outputFile = new File(outputPath);
331  FileOutputStream outputStream = new FileOutputStream(outputFile);
332  outputStream.write(data);
333  }
334 
343  private String getUniqueName(AbstractFile file) {
344  return file.getName() + "_" + file.getId();
345  }
346 
356  private String getFileRelativePath(String parentFileName, String fileName) throws NoCurrentCaseException {
357  // Used explicit FWD slashes to maintain DB consistency across operating systems.
358  return Paths.get(getRelModuleOutputPath(), parentFileName, fileName).toString();
359  }
360 
371  private String getOutputFolderPath(String parentFileName) throws NoCurrentCaseException {
372  String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
373  File outputFilePath = new File(outputFolderPath);
374  if (!outputFilePath.exists()) {
375  outputFilePath.mkdirs();
376  }
377  return outputFolderPath;
378  }
379 
388  private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
389  String telephoneText = telephone.getText();
390 
391  if (telephoneText == null || telephoneText.isEmpty()) {
392  telephoneText = telephone.getUri().getNumber();
393  if (telephoneText == null || telephoneText.isEmpty()) {
394  return;
395  }
396  }
397 
398  // Add phone number to collection for later creation of TSK_CONTACT.
399  List<TelephoneType> telephoneTypes = telephone.getTypes();
400  if (telephoneTypes.isEmpty()) {
401  ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
402  } else {
403  TelephoneType type = telephoneTypes.get(0);
404  /*
405  * Unfortunately, if the types are lower-case, they don't
406  * get separated correctly into individual TelephoneTypes by
407  * ez-vcard. Therefore, we must read them manually
408  * ourselves.
409  */
410  List<String> splitTelephoneTypes = Arrays.asList(
411  type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
412 
413  if (splitTelephoneTypes.size() > 0) {
414  String splitType = splitTelephoneTypes.get(0);
415  String attributeTypeName = "TSK_PHONE_NUMBER";
416  if (splitType != null && !splitType.isEmpty()) {
417  attributeTypeName = "TSK_PHONE_NUMBER_" + splitType;
418  }
419 
420  try {
421  BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
422  if (attributeType == null) {
423  try{
424  // Add this attribute type to the case database.
425  attributeType = tskCase.getBlackboard().getOrAddAttributeType(attributeTypeName,
426  BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
427  String.format("Phone Number (%s)", StringUtils.capitalize(splitType.toLowerCase())));
428 
429  ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephoneText, attributeType, attributes);
430  }catch (BlackboardException ex) {
431  logger.log(Level.WARNING, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
432  }
433  }
434 
435  } catch (TskCoreException ex) {
436  logger.log(Level.WARNING, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
437  }
438  }
439  }
440  }
441 
450  private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
451  String emailValue = email.getValue();
452  if (emailValue == null || emailValue.isEmpty()) {
453  return;
454  }
455 
456  // Add phone number to collection for later creation of TSK_CONTACT.
457  List<EmailType> emailTypes = email.getTypes();
458  if (emailTypes.isEmpty()) {
459  ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
460  } else {
461  EmailType type = emailTypes.get(0); /*
462  * Unfortunately, if the types are lower-case, they don't
463  * get separated correctly into individual EmailTypes by
464  * ez-vcard. Therefore, we must read them manually
465  * ourselves.
466  */
467  List<String> splitEmailTypes = Arrays.asList(
468  type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
469 
470  if (splitEmailTypes.size() > 0) {
471  String splitType = splitEmailTypes.get(0);
472  String attributeTypeName = "TSK_EMAIL_" + splitType;
473  if(splitType.isEmpty()) {
474  attributeTypeName = "TSK_EMAIL";
475  }
476  try {
477  BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
478  if (attributeType == null) {
479  // Add this attribute type to the case database.
480  attributeType = tskCase.getBlackboard().getOrAddAttributeType(attributeTypeName,
481  BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
482  String.format("Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
483  }
484  ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
485  } catch (TskCoreException ex) {
486  logger.log(Level.SEVERE, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
487  } catch (BlackboardException ex) {
488  logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
489  }
490  }
491  }
492  }
493 
503  private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
504  String telephoneText = telephone.getText();
505  if (telephoneText == null || telephoneText.isEmpty()) {
506  telephoneText = telephone.getUri().getNumber();
507  if (telephoneText == null || telephoneText.isEmpty()) {
508  return;
509  }
510 
511  }
512 
513  // Add phone number as a TSK_ACCOUNT.
514  try {
515  AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
516  telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile);
517  accountInstances.add(phoneAccountInstance);
518  }
519  catch(TskCoreException ex) {
520  logger.log(Level.WARNING, String.format(
521  "Failed to create account for phone number '%s' (content='%s'; id=%d).",
522  telephoneText, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
523  }
524  }
525 
535  private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
536  String emailValue = email.getValue();
537  if (emailValue == null || emailValue.isEmpty()) {
538  return;
539  }
540 
541  // Add e-mail as a TSK_ACCOUNT.
542  try {
543  AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
544  emailValue, EmailParserModuleFactory.getModuleName(), abstractFile);
545  accountInstances.add(emailAccountInstance);
546  }
547  catch(TskCoreException ex) {
548  logger.log(Level.WARNING, String.format(
549  "Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
550  emailValue, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
551  }
552  }
553 
561  private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
562  // Add 'DEVICE' TSK_ACCOUNT.
563  AccountFileInstance deviceAccountInstance = null;
564  String deviceId = null;
565  try {
566  long dataSourceObjId = abstractFile.getDataSourceObjectId();
567  DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
568  deviceId = dataSource.getDeviceId();
569  deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
570  deviceId, EmailParserModuleFactory.getModuleName(), abstractFile);
571  }
572  catch (TskCoreException ex) {
573  logger.log(Level.WARNING, String.format(
574  "Failed to create device account for '%s' (content='%s'; id=%d).",
575  deviceId, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
576  }
577  catch (TskDataException ex) {
578  logger.log(Level.WARNING, String.format(
579  "Failed to get the data source from the case database (id=%d).",
580  abstractFile.getId()), ex); //NON-NLS
581  }
582 
583  return deviceAccountInstance;
584  }
585 }

Copyright © 2012-2021 Basis Technology. Generated on: Tue Jan 19 2021
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.