Autopsy 4.22.1
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 */
19package org.sleuthkit.autopsy.thunderbirdparser;
20
21import ezvcard.Ezvcard;
22import ezvcard.VCard;
23import ezvcard.parameter.EmailType;
24import ezvcard.parameter.TelephoneType;
25import ezvcard.property.Email;
26import ezvcard.property.Organization;
27import ezvcard.property.Photo;
28import ezvcard.property.Telephone;
29import ezvcard.property.Url;
30import java.io.BufferedInputStream;
31import java.io.File;
32import java.io.FileOutputStream;
33import java.io.IOException;
34import java.io.InputStreamReader;
35import java.nio.charset.StandardCharsets;
36import java.nio.file.Paths;
37import java.util.ArrayList;
38import java.util.Arrays;
39import java.util.Collection;
40import java.util.HashMap;
41import java.util.List;
42import java.util.Map;
43import java.util.concurrent.ConcurrentMap;
44import java.util.logging.Level;
45import org.apache.commons.lang3.StringUtils;
46import org.openide.util.NbBundle;
47import org.sleuthkit.autopsy.casemodule.Case;
48import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
49import org.sleuthkit.autopsy.casemodule.services.FileManager;
50import org.sleuthkit.autopsy.coreutils.Logger;
51import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
52import org.sleuthkit.autopsy.ingest.IngestJobContext;
53import org.sleuthkit.autopsy.ingest.IngestServices;
54import org.sleuthkit.autopsy.ingest.ModuleContentEvent;
55import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
56import org.sleuthkit.datamodel.AbstractFile;
57import org.sleuthkit.datamodel.Account;
58import org.sleuthkit.datamodel.AccountFileInstance;
59import org.sleuthkit.datamodel.Blackboard;
60import org.sleuthkit.datamodel.Blackboard.BlackboardException;
61import org.sleuthkit.datamodel.BlackboardArtifact;
62import org.sleuthkit.datamodel.BlackboardAttribute;
63import org.sleuthkit.datamodel.Content;
64import org.sleuthkit.datamodel.DataSource;
65import org.sleuthkit.datamodel.ReadContentInputStream;
66import org.sleuthkit.datamodel.Relationship;
67import org.sleuthkit.datamodel.SleuthkitCase;
68import org.sleuthkit.datamodel.TskCoreException;
69import org.sleuthkit.datamodel.TskData;
70import org.sleuthkit.datamodel.TskDataException;
71import org.sleuthkit.datamodel.TskException;
72
77final 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-2024 Sleuth Kit Labs. Generated on:
This work is licensed under a Creative Commons Attribution-Share Alike 3.0 United States License.