VCard support added.

This commit is contained in:
U-BASIS\dgrove 2019-02-27 13:28:25 -05:00
parent 0394807018
commit 2f8bcf2a7a
5 changed files with 708 additions and 50 deletions

View File

@ -7,8 +7,11 @@
</configurations>
<dependencies>
<dependency conf="autopsy->default" org="org.apache.commons" name="commons-lang3" rev="3.8.1"/>
<dependency conf="autopsy->default" org="org.apache.james" name="apache-mime4j-core" rev="0.8.0"/>
<dependency conf="autopsy->default" org="org.apache.james" name="apache-mime4j-dom" rev="0.8.0"/>
<dependency conf="autopsy->default" org="org.apache.james" name="apache-mime4j-mbox-iterator" rev="0.8.0"/>
<dependency conf="autopsy->default" org="com.googlecode.ez-vcard" name="ez-vcard" rev="0.10.5"/>
<dependency conf="autopsy->default" org="com.github.mangstadt" name="vinnie" rev="2.0.2"/>
</dependencies>
</ivy-module>

View File

@ -1,7 +1,10 @@
file.reference.commons-lang3-3.8.1.jar=release/modules/ext/commons-lang3-3.8.1.jar
file.reference.apache-mime4j-core-0.8.0.jar=release/modules/ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar
file.reference.apache-mime4j-dom-0.8.0.jar=release/modules/ext/apache-mime4j-dom-0.8.0-SNAPSHOT.jar
file.reference.apache-mime4j-mbox-iterator-0.8.0.jar=release/modules/ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar
file.reference.java-libpst-1.0-SNAPSHOT.jar=release/modules/ext/java-libpst-1.0-SNAPSHOT.jar
file.reference.ez-vcard-0.10.5.jar=release/modules/ext/ez-vcard-0.10.5.jar
file.reference.vinnie-2.0.2.jar=release/modules/ext/vinnie-2.0.2.jar
javac.source=1.8
javac.compilerargs=-Xlint -Xlint:-serial
license.file=../LICENSE-2.0.txt

View File

@ -50,6 +50,10 @@
</dependency>
</module-dependencies>
<public-packages/>
<class-path-extension>
<runtime-relative-path>ext/commons-lang3-3.8.1.jar</runtime-relative-path>
<binary-origin>release/modules/ext/commons-lang3-3.8.1.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar</runtime-relative-path>
<binary-origin>release/modules/ext/apache-mime4j-core-0.8.0-SNAPSHOT.jar</binary-origin>
@ -66,6 +70,14 @@
<runtime-relative-path>ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar</runtime-relative-path>
<binary-origin>release/modules/ext/apache-mime4j-mbox-iterator-0.8.0-SNAPSHOT.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/ez-vcard-0.10.5.jar</runtime-relative-path>
<binary-origin>release/modules/ext/ez-vcard-0.10.5.jar</binary-origin>
</class-path-extension>
<class-path-extension>
<runtime-relative-path>ext/vinnie-2.0.2.jar</runtime-relative-path>
<binary-origin>release/modules/ext/vinnie-2.0.2.jar</binary-origin>
</class-path-extension>
</data>
</configuration>
</project>

View File

@ -1,7 +1,7 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2011-2014 Basis Technology Corp.
* Copyright 2011-2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
@ -53,24 +53,30 @@ import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.BlackboardAttribute.ATTRIBUTE_TYPE;
import org.sleuthkit.datamodel.DerivedFile;
import org.sleuthkit.datamodel.Relationship;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.TskDataException;
import org.sleuthkit.datamodel.TskException;
/**
* File-level ingest module that detects MBOX files based on signature.
* Understands Thunderbird folder layout to provide additional structure and
* metadata.
* File-level ingest module that detects MBOX, PST, and vCard files based on
* signature. Understands Thunderbird folder layout to provide additional
* structure and metadata.
*/
public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
private static final Logger logger = Logger.getLogger(ThunderbirdMboxFileIngestModule.class.getName());
private IngestServices services = IngestServices.getInstance();
private final IngestServices services = IngestServices.getInstance();
private FileManager fileManager;
private IngestJobContext context;
private Blackboard blackboard;
private Case currentCase;
private SleuthkitCase tskCase;
/**
* Empty constructor.
*/
ThunderbirdMboxFileIngestModule() {
}
@ -79,6 +85,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
public void startUp(IngestJobContext context) throws IngestModuleException {
this.context = context;
try {
currentCase = Case.getCurrentCaseThrows();
tskCase = currentCase.getSleuthkitCase();
fileManager = Case.getCurrentCaseThrows().getServices().getFileManager();
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex);
@ -133,6 +141,10 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
return processPst(abstractFile);
}
if (VcardParser.isVcardFile(abstractFile)) {
return processVcard(abstractFile);
}
return ProcessResult.OK;
}
@ -177,13 +189,8 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
PstParser.ParseResult result = parser.parse(file, abstractFile.getId());
if (result == PstParser.ParseResult.OK) {
try {
// parse success: Process email and add artifacts
processEmails(parser.getResults(), abstractFile);
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
return ProcessResult.ERROR;
}
// parse success: Process email and add artifacts
processEmails(parser.getResults(), abstractFile);
} else if (result == PstParser.ParseResult.ENCRYPT) {
// encrypted pst: Add encrypted file artifact
@ -279,12 +286,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
MboxParser parser = new MboxParser(services, emailFolder);
List<EmailMessage> emails = parser.parse(file, abstractFile.getId());
try {
processEmails(emails, abstractFile);
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
return ProcessResult.ERROR;
}
processEmails(emails, abstractFile);
if (file.delete() == false) {
logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS
@ -300,6 +302,63 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
return ProcessResult.OK;
}
/**
* Parse and extract data from a vCard file.
*
* @param abstractFile The content to be processed.
*
* @return 'ERROR' whenever a NoCurrentCaseException is encountered;
* otherwise 'OK'.
*/
@Messages({
"# {0} - file name",
"# {1} - file ID",
"ThunderbirdMboxFileIngestModule.errorMessage.outOfDiskSpace=Out of disk space. Cannot copy '{0}' (id={1}) to parse."
})
private ProcessResult processVcard(AbstractFile abstractFile) {
String fileName;
try {
fileName = getTempPath() + File.separator + abstractFile.getName()
+ "-" + String.valueOf(abstractFile.getId());
} catch (NoCurrentCaseException ex) {
logger.log(Level.SEVERE, "Exception while getting open case.", ex); //NON-NLS
return ProcessResult.ERROR;
}
File file = new File(fileName);
long freeSpace = services.getFreeDiskSpace();
if ((freeSpace != IngestMonitor.DISK_FREE_SPACE_UNKNOWN) && (abstractFile.getSize() >= freeSpace)) {
logger.log(Level.WARNING, String.format("Not enough disk space to write file '%s' (id=%d) to disk.",
abstractFile.getName(), abstractFile.getId())); //NON-NLS
IngestMessage msg = IngestMessage.createErrorMessage(EmailParserModuleFactory.getModuleName(), EmailParserModuleFactory.getModuleName(),
Bundle.ThunderbirdMboxFileIngestModule_errorMessage_outOfDiskSpace(abstractFile.getName(), abstractFile.getId()));
services.postMessage(msg);
return ProcessResult.OK;
}
try {
ContentUtils.writeToFile(abstractFile, file, context::fileIngestIsCancelled);
} catch (IOException ex) {
logger.log(Level.WARNING, String.format("Failed writing the vCard file '%s' (id=%d) to disk.",
abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
return ProcessResult.OK;
}
try {
VcardParser parser = new VcardParser(currentCase, context);
parser.parse(file, abstractFile);
} catch (IOException | NoCurrentCaseException ex) {
logger.log(Level.WARNING, String.format("Exception while parsing the file '%s' (id=%d).", file.getName(), abstractFile.getId()), ex); //NON-NLS
return ProcessResult.OK;
}
if (file.delete() == false) {
logger.log(Level.INFO, "Failed to delete temp file: {0}", file.getName()); //NON-NLS
}
return ProcessResult.OK;
}
/**
* Get a path to a temporary folder.
*
@ -320,6 +379,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
* Get a module output folder.
*
* @throws NoCurrentCaseException if there is no open case.
*
* @return the module output folder
*/
static String getModuleOutputPath() throws NoCurrentCaseException {
@ -349,15 +409,14 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
*
* @param emails
* @param abstractFile
* @throws NoCurrentCaseException if there is no open case.
*/
private void processEmails(List<EmailMessage> emails, AbstractFile abstractFile) throws NoCurrentCaseException {
private void processEmails(List<EmailMessage> emails, AbstractFile abstractFile) {
List<AbstractFile> derivedFiles = new ArrayList<>();
for (EmailMessage email : emails) {
BlackboardArtifact msgArtifact = addArtifact(email, abstractFile);
BlackboardArtifact msgArtifact = addEmailArtifact(email, abstractFile);
if ((msgArtifact != null) && (email.hasAttachment())) {
derivedFiles.addAll(handleAttachments(email.getAttachments(), abstractFile, msgArtifact ));
@ -423,7 +482,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
Pattern p = Pattern.compile("\\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\\.[A-Z]{2,4}\\b",
Pattern.CASE_INSENSITIVE);
Matcher m = p.matcher(input);
Set<String> emailAddresses = new HashSet<String>();
Set<String> emailAddresses = new HashSet<>();
while (m.find()) {
emailAddresses.add( m.group());
}
@ -431,14 +490,15 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
}
/**
* Add a blackboard artifact for the given email message.
* Add a blackboard artifact for the given e-mail message.
*
* @param email
* @param abstractFile
* @throws NoCurrentCaseException if there is no open case.
* @param email The e-mail message.
* @param abstractFile The associated file.
*
* @return The generated e-mail message artifact.
*/
@Messages({"ThunderbirdMboxFileIngestModule.addArtifact.indexError.message=Failed to index email message detected artifact for keyword search."})
private BlackboardArtifact addArtifact(EmailMessage email, AbstractFile abstractFile) throws NoCurrentCaseException {
private BlackboardArtifact addEmailArtifact(EmailMessage email, AbstractFile abstractFile) {
BlackboardArtifact bbart = null;
List<BlackboardAttribute> bbattributes = new ArrayList<>();
String to = email.getRecipients();
@ -460,19 +520,17 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
AccountFileInstance senderAccountInstance = null;
Case openCase = Case.getCurrentCaseThrows();
if (senderAddressList.size() == 1) {
senderAddress = senderAddressList.get(0);
try {
senderAccountInstance = openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, senderAddress, EmailParserModuleFactory.getModuleName(), abstractFile);
senderAccountInstance = currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, senderAddress, EmailParserModuleFactory.getModuleName(), abstractFile);
}
catch(TskCoreException ex) {
logger.log(Level.WARNING, "Failed to create account for email address " + senderAddress, ex); //NON-NLS
}
}
else {
logger.log(Level.WARNING, "Failed to find sender address, from = "+ from); //NON-NLS
logger.log(Level.WARNING, "Failed to find sender address, from = {0}", from); //NON-NLS
}
List<String> recipientAddresses = new ArrayList<>();
@ -484,7 +542,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
recipientAddresses.forEach((addr) -> {
try {
AccountFileInstance recipientAccountInstance =
openCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr,
currentCase.getSleuthkitCase().getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL, addr,
EmailParserModuleFactory.getModuleName(), abstractFile);
recipientAccountInstances.add(recipientAccountInstance);
}
@ -493,25 +551,25 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
}
});
addEmailAttribute(headers, ATTRIBUTE_TYPE.TSK_HEADERS, bbattributes);
addEmailAttribute(from, ATTRIBUTE_TYPE.TSK_EMAIL_FROM, bbattributes);
addEmailAttribute(to, ATTRIBUTE_TYPE.TSK_EMAIL_TO, bbattributes);
addEmailAttribute(subject, ATTRIBUTE_TYPE.TSK_SUBJECT, bbattributes);
addArtifactAttribute(headers, ATTRIBUTE_TYPE.TSK_HEADERS, bbattributes);
addArtifactAttribute(from, ATTRIBUTE_TYPE.TSK_EMAIL_FROM, bbattributes);
addArtifactAttribute(to, ATTRIBUTE_TYPE.TSK_EMAIL_TO, bbattributes);
addArtifactAttribute(subject, ATTRIBUTE_TYPE.TSK_SUBJECT, bbattributes);
addEmailAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_RCVD, bbattributes);
addEmailAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_SENT, bbattributes);
addArtifactAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_RCVD, bbattributes);
addArtifactAttribute(dateL, ATTRIBUTE_TYPE.TSK_DATETIME_SENT, bbattributes);
addEmailAttribute(body, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN, bbattributes);
addArtifactAttribute(body, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_PLAIN, bbattributes);
addEmailAttribute(((id < 0L) ? NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.notAvail") : String.valueOf(id)),
addArtifactAttribute(((id < 0L) ? NbBundle.getMessage(this.getClass(), "ThunderbirdMboxFileIngestModule.notAvail") : String.valueOf(id)),
ATTRIBUTE_TYPE.TSK_MSG_ID, bbattributes);
addEmailAttribute(((localPath.isEmpty() == false) ? localPath : "/foo/bar"),
addArtifactAttribute(((localPath.isEmpty() == false) ? localPath : "/foo/bar"),
ATTRIBUTE_TYPE.TSK_PATH, bbattributes);
addEmailAttribute(cc, ATTRIBUTE_TYPE.TSK_EMAIL_CC, bbattributes);
addEmailAttribute(bodyHTML, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_HTML, bbattributes);
addEmailAttribute(rtf, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_RTF, bbattributes);
addArtifactAttribute(cc, ATTRIBUTE_TYPE.TSK_EMAIL_CC, bbattributes);
addArtifactAttribute(bodyHTML, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_HTML, bbattributes);
addArtifactAttribute(rtf, ATTRIBUTE_TYPE.TSK_EMAIL_CONTENT_RTF, bbattributes);
try {
@ -520,7 +578,7 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
bbart.addAttributes(bbattributes);
// Add account relationships
openCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart,Relationship.Type.MESSAGE, dateL);
currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(senderAccountInstance, recipientAccountInstances, bbart,Relationship.Type.MESSAGE, dateL);
try {
// index the artifact for keyword search
@ -536,22 +594,61 @@ public final class ThunderbirdMboxFileIngestModule implements FileIngestModule {
return bbart;
}
private void addEmailAttribute(String stringVal, ATTRIBUTE_TYPE attrType, Collection<BlackboardAttribute> bbattributes) {
/**
* Add an attribute of a specified type to a supplied Collection.
*
* @param stringVal The attribute value.
* @param attrType The type of attribute to be added.
* @param bbattributes The Collection to which the attribute will be added.
*/
static void addArtifactAttribute(String stringVal, BlackboardAttribute.Type attrType, Collection<BlackboardAttribute> bbattributes) {
if (stringVal.isEmpty() == false) {
bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), stringVal));
}
}
private void addEmailAttribute(long longVal, ATTRIBUTE_TYPE attrType, Collection<BlackboardAttribute> bbattributes) {
/**
* Add an attribute of a specified type to a supplied Collection.
*
* @param stringVal The attribute value.
* @param attrType The type of attribute to be added.
* @param bbattributes The Collection to which the attribute will be added.
*/
static void addArtifactAttribute(String stringVal, ATTRIBUTE_TYPE attrType, Collection<BlackboardAttribute> bbattributes) {
if (stringVal.isEmpty() == false) {
bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), stringVal));
}
}
/**
* Add an attribute of a specified type to a supplied Collection.
*
* @param longVal The attribute value.
* @param attrType The type of attribute to be added.
* @param bbattributes The Collection to which the attribute will be added.
*/
static void addArtifactAttribute(long longVal, ATTRIBUTE_TYPE attrType, Collection<BlackboardAttribute> bbattributes) {
if (longVal > 0) {
bbattributes.add(new BlackboardAttribute(attrType, EmailParserModuleFactory.getModuleName(), longVal));
}
}
/**
* Post an error message for the user.
*
* @param subj The error subject.
* @param details The error details.
*/
void postErrorMessage(String subj, String details) {
IngestMessage ingestMessage = IngestMessage.createErrorMessage(EmailParserModuleFactory.getModuleVersion(), subj, details);
services.postMessage(ingestMessage);
}
/**
* Get the IngestServices object.
*
* @return The IngestServices object.
*/
IngestServices getServices() {
return services;
}

View File

@ -0,0 +1,543 @@
/*
* Autopsy Forensic Browser
*
* Copyright 2019 Basis Technology Corp.
* Contact: carrier <at> sleuthkit <dot> org
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.sleuthkit.autopsy.thunderbirdparser;
import ezvcard.Ezvcard;
import ezvcard.VCard;
import ezvcard.parameter.EmailType;
import ezvcard.parameter.TelephoneType;
import ezvcard.property.Email;
import ezvcard.property.Organization;
import ezvcard.property.Photo;
import ezvcard.property.Telephone;
import ezvcard.property.Url;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import org.apache.commons.lang3.StringUtils;
import org.openide.util.NbBundle;
import org.sleuthkit.autopsy.casemodule.Case;
import org.sleuthkit.autopsy.casemodule.NoCurrentCaseException;
import org.sleuthkit.autopsy.casemodule.services.Blackboard;
import org.sleuthkit.autopsy.casemodule.services.FileManager;
import org.sleuthkit.autopsy.coreutils.Logger;
import org.sleuthkit.autopsy.coreutils.MessageNotifyUtil;
import org.sleuthkit.autopsy.ingest.IngestJobContext;
import org.sleuthkit.autopsy.ingest.IngestServices;
import org.sleuthkit.autopsy.ingest.ModuleContentEvent;
import org.sleuthkit.autopsy.ingest.ModuleDataEvent;
import static org.sleuthkit.autopsy.thunderbirdparser.ThunderbirdMboxFileIngestModule.getRelModuleOutputPath;
import org.sleuthkit.datamodel.AbstractFile;
import org.sleuthkit.datamodel.Account;
import org.sleuthkit.datamodel.AccountFileInstance;
import org.sleuthkit.datamodel.BlackboardArtifact;
import org.sleuthkit.datamodel.BlackboardAttribute;
import org.sleuthkit.datamodel.Content;
import org.sleuthkit.datamodel.DataSource;
import org.sleuthkit.datamodel.Relationship;
import org.sleuthkit.datamodel.SleuthkitCase;
import org.sleuthkit.datamodel.TskCoreException;
import org.sleuthkit.datamodel.TskData;
import org.sleuthkit.datamodel.TskDataException;
import org.sleuthkit.datamodel.TskException;
/**
* A parser that can extract information from a vCard file and create the
* appropriate artifacts.
*/
final class VcardParser {
private static final String VCARD_HEADER = "BEGIN:VCARD";
private static final long MIN_FILE_SIZE = 22;
private static final String PHOTO_TYPE_BMP = "bmp";
private static final String PHOTO_TYPE_GIF = "gif";
private static final String PHOTO_TYPE_JPEG = "jpeg";
private static final String PHOTO_TYPE_PNG = "png";
private static final Map<String, String> photoTypeExtensions;
static {
photoTypeExtensions = new HashMap<>();
photoTypeExtensions.put(PHOTO_TYPE_BMP, ".bmp");
photoTypeExtensions.put(PHOTO_TYPE_GIF, ".gif");
photoTypeExtensions.put(PHOTO_TYPE_JPEG, ".jpg");
photoTypeExtensions.put(PHOTO_TYPE_PNG, ".png");
}
private static final Logger logger = Logger.getLogger(VcardParser.class.getName());
private final IngestServices services = IngestServices.getInstance();
private final FileManager fileManager;
private final IngestJobContext context;
private final Blackboard blackboard;
private final Case currentCase;
private final SleuthkitCase tskCase;
/**
* Create a VcardParser object.
*/
VcardParser(Case currentCase, IngestJobContext context) {
this.context = context;
this.currentCase = currentCase;
tskCase = currentCase.getSleuthkitCase();
blackboard = currentCase.getServices().getBlackboard();
fileManager = currentCase.getServices().getFileManager();
}
/**
* Is the supplied content a vCard file?
*
* @param content The content to check.
*
* @return True if the supplied content is a vCard file; otherwise false.
*/
static boolean isVcardFile(Content content) {
try {
if (content.getSize() > MIN_FILE_SIZE) {
byte[] buffer = new byte[VCARD_HEADER.length()];
int byteRead = content.read(buffer, 0, VCARD_HEADER.length());
if (byteRead > 0) {
String header = new String(buffer);
return header.equalsIgnoreCase(VCARD_HEADER);
}
}
} catch (TskException ex) {
logger.log(Level.WARNING, String.format("Exception while detecting if the file '%s' (id=%d) is a vCard file.",
content.getName(), content.getId())); //NON-NLS
}
return false;
}
/**
* Parse the VCard file and compile its data in a VCard object. The
* corresponding artifacts will be created.
*
* @param vcardFile The VCard file to be parsed.
* @param abstractFile The abstract file with which to associate artifacts.
*
* @throws IOException If there is an issue parsing the VCard
* file.
* @throws NoCurrentCaseException If there is no open case.
*/
void parse(File vcardFile, AbstractFile abstractFile) throws IOException, NoCurrentCaseException {
VCard vcard = Ezvcard.parse(vcardFile).first();
addContactArtifact(vcard, abstractFile);
}
/**
* Add a blackboard artifact for the given contact.
*
* @param vcard The VCard that contains the contact information.
* @param abstractFile The file associated with the data.
*
* @throws NoCurrentCaseException if there is no open case.
*
* @return The generated contact artifact.
*/
@NbBundle.Messages({"VcardParser.addContactArtifact.indexError=Failed to index the contact artifact for keyword search."})
private BlackboardArtifact addContactArtifact(VCard vcard, AbstractFile abstractFile) throws NoCurrentCaseException {
List<BlackboardAttribute> attributes = new ArrayList<>();
List<AccountFileInstance> accountInstances = new ArrayList<>();
extractPhotos(vcard, abstractFile);
ThunderbirdMboxFileIngestModule.addArtifactAttribute(vcard.getFormattedName().getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_NAME_PERSON, attributes);
for (Telephone telephone : vcard.getTelephoneNumbers()) {
addPhoneAttributes(telephone, abstractFile, attributes);
addPhoneAccountInstances(telephone, abstractFile, accountInstances);
}
for (Email email : vcard.getEmails()) {
addEmailAttributes(email, abstractFile, attributes);
addEmailAccountInstances(email, abstractFile, accountInstances);
}
for (Url url : vcard.getUrls()) {
ThunderbirdMboxFileIngestModule.addArtifactAttribute(url.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_URL, attributes);
}
for (Organization organization : vcard.getOrganizations()) {
List<String> values = organization.getValues();
if (values.isEmpty() == false) {
ThunderbirdMboxFileIngestModule.addArtifactAttribute(values.get(0), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_ORGANIZATION, attributes);
}
}
AccountFileInstance deviceAccountInstance = addDeviceAccountInstance(abstractFile);
BlackboardArtifact artifact = null;
org.sleuthkit.datamodel.Blackboard tskBlackboard = tskCase.getBlackboard();
try {
// Create artifact if it doesn't already exist.
if (!tskBlackboard.artifactExists(abstractFile, BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT, attributes)) {
artifact = abstractFile.newArtifact(BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT);
artifact.addAttributes(attributes);
List<BlackboardArtifact> blackboardArtifacts = new ArrayList<>();
blackboardArtifacts.add(artifact);
// Add account relationships.
if (deviceAccountInstance != null) {
try {
currentCase.getSleuthkitCase().getCommunicationsManager().addRelationships(
deviceAccountInstance, accountInstances, artifact, Relationship.Type.CONTACT, abstractFile.getCrtime());
} catch (TskDataException ex) {
logger.log(Level.SEVERE, String.format("Failed to create phone and e-mail account relationships (fileName='%s'; fileId=%d; accountId=%d).",
abstractFile.getName(), abstractFile.getId(), deviceAccountInstance.getAccount().getAccountID()), ex); //NON-NLS
}
}
// Index the artifact for keyword search.
try {
blackboard.indexArtifact(artifact);
} catch (Blackboard.BlackboardException ex) {
logger.log(Level.SEVERE, "Unable to index blackboard artifact " + artifact.getArtifactID(), ex); //NON-NLS
MessageNotifyUtil.Notify.error(Bundle.VcardParser_addContactArtifact_indexError(), artifact.getDisplayName());
}
// Fire event to notify UI of this new artifact.
IngestServices.getInstance().fireModuleDataEvent(new ModuleDataEvent(
EmailParserModuleFactory.getModuleName(), BlackboardArtifact.ARTIFACT_TYPE.TSK_CONTACT,
blackboardArtifacts));
}
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Failed to create contact artifact for vCard file '%s' (id=%d).",
abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
}
return artifact;
}
/**
* Extract photos from a given VCard and add them as derived files.
*
* @param vcard The VCard from which to extract the photos.
* @param abstractFile The file associated with the data.
*
* @throws NoCurrentCaseException if there is no open case.
*/
private void extractPhotos(VCard vcard, AbstractFile abstractFile) throws NoCurrentCaseException {
String parentFileName = getUniqueName(abstractFile);
// Skip files that already have been extracted.
try {
String outputPath = getOutputFolderPath(parentFileName);
if (new File(outputPath).exists()) {
List<Photo> vcardPhotos = vcard.getPhotos();
List<AbstractFile> derivedFilesCreated = new ArrayList<>();
for (int i=0; i < vcardPhotos.size(); i++) {
Photo photo = vcardPhotos.get(i);
if (photo.getUrl() != null) {
// Skip this photo since its data is not embedded.
continue;
}
String type = photo.getType();
if (type == null) {
// Skip this photo since no type is defined.
continue;
}
type = type.toLowerCase();
if (type.startsWith("image/")) {
type = type.substring(6);
}
String extension = photoTypeExtensions.get(type);
byte[] data = photo.getData();
String extractedFileName = String.format("photo_%d%s", i, extension == null ? "" : extension);
String extractedFilePath = Paths.get(outputPath, extractedFileName).toString();
try {
writeExtractedImage(extractedFilePath, data);
derivedFilesCreated.add(fileManager.addDerivedFile(extractedFileName, getFileRelativePath(parentFileName, extractedFileName), data.length,
abstractFile.getCtime(), abstractFile.getCrtime(), abstractFile.getAtime(), abstractFile.getAtime(),
true, abstractFile, null, EmailParserModuleFactory.getModuleName(), null, null, TskData.EncodingType.NONE));
} catch (IOException | TskCoreException ex) {
logger.log(Level.WARNING, String.format("Could not write image to '%s' (id=%d).", extractedFilePath, abstractFile.getId()), ex); //NON-NLS
}
}
if (!derivedFilesCreated.isEmpty()) {
services.fireModuleContentEvent(new ModuleContentEvent(abstractFile));
context.addFilesToJob(derivedFilesCreated);
}
}
else {
logger.log(Level.INFO, String.format("Skipping photo extraction for file '%s' (id=%d), because it has already been processed.",
abstractFile.getName(), abstractFile.getId())); //NON-NLS
}
} catch (SecurityException ex) {
logger.log(Level.WARNING, String.format("Could not create extraction folder for '%s' (id=%d).", parentFileName, abstractFile.getId()));
}
}
/**
* Writes image to the module output location.
*
* @param outputPath Path where images is written.
* @param data Byte representation of the data to be written to the
* specified location.
*/
private void writeExtractedImage(String outputPath, byte[] data) throws IOException {
File outputFile = new File(outputPath);
FileOutputStream outputStream = new FileOutputStream(outputFile);
outputStream.write(data);
}
/**
* Creates a unique name for a file by concatentating the file name and the
* file object id.
*
* @param file The file.
*
* @return The unique file name.
*/
private String getUniqueName(AbstractFile file) {
return file.getName() + "_" + file.getId();
}
/**
* Gets the relative path to the file. The path is relative to the case
* folder.
*
* @param fileName Name of the the file for which the path is to be
* generated.
*
* @return The relative file path.
*/
private String getFileRelativePath(String parentFileName, String fileName) throws NoCurrentCaseException {
// Used explicit FWD slashes to maintain DB consistency across operating systems.
return "/" + getRelModuleOutputPath() + "/" + parentFileName + "/" + fileName; //NON-NLS
}
/**
* Gets path to the output folder for file extraction. If the path does not
* exist, it is created.
*
* @param parentFileName Name of the abstract file being processed.
*
* @throws NoCurrentCaseException if there is no open case.
*
* @return Path to the file extraction folder for a given abstract file.
*/
private String getOutputFolderPath(String parentFileName) throws NoCurrentCaseException {
String outputFolderPath = ThunderbirdMboxFileIngestModule.getModuleOutputPath() + File.separator + parentFileName;
File outputFilePath = new File(outputFolderPath);
if (!outputFilePath.exists()) {
outputFilePath.mkdirs();
}
return outputFolderPath;
}
/**
* Generate phone attributes for a given VCard Telephone object.
*
* @param telephone The VCard Telephone from which to generate attributes.
* @param abstractFile The VCard file.
* @param attributes The Collection to which generated attributes will be
* added.
*/
private void addPhoneAttributes(Telephone telephone, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
String telephoneText = telephone.getText();
if (telephoneText == null || telephoneText.isEmpty()) {
return;
}
// Add phone number to collection for later creation of TSK_CONTACT.
List<TelephoneType> telephoneTypes = telephone.getTypes();
if (telephoneTypes.isEmpty()) {
ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_PHONE_NUMBER, attributes);
} else {
for (TelephoneType type : telephoneTypes) {
/*
* Unfortunately, if the types are lower-case, they don't
* get separated correctly into individual TelephoneTypes by
* ez-vcard. Therefore, we must read them manually
* ourselves.
*/
List<String> splitTelephoneTypes = Arrays.asList(
type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
for (String splitType : splitTelephoneTypes) {
String attributeTypeName = "TSK_PHONE_" + splitType;
try {
BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
if (attributeType == null) {
// Add this attribute type to the case database.
attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
String.format("Phone (%s)", StringUtils.capitalize(splitType.toLowerCase())));
}
ThunderbirdMboxFileIngestModule.addArtifactAttribute(telephone.getText(), attributeType, attributes);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
} catch (TskDataException ex) {
logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
}
}
}
}
}
/**
* Generate e-mail attributes for a given VCard Email object.
*
* @param email The VCard Email from which to generate attributes.
* @param abstractFile The VCard file.
* @param attributes The Collection to which generated attributes will be
* added.
*/
private void addEmailAttributes(Email email, AbstractFile abstractFile, Collection<BlackboardAttribute> attributes) {
String emailValue = email.getValue();
if (emailValue == null || emailValue.isEmpty()) {
return;
}
// Add phone number to collection for later creation of TSK_CONTACT.
List<EmailType> emailTypes = email.getTypes();
if (emailTypes.isEmpty()) {
ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), BlackboardAttribute.ATTRIBUTE_TYPE.TSK_EMAIL, attributes);
} else {
for (EmailType type : emailTypes) {
/*
* Unfortunately, if the types are lower-case, they don't
* get separated correctly into individual EmailTypes by
* ez-vcard. Therefore, we must read them manually
* ourselves.
*/
List<String> splitEmailTypes = Arrays.asList(
type.getValue().toUpperCase().replaceAll("\\s+","").split(","));
for (String splitType : splitEmailTypes) {
String attributeTypeName = "TSK_EMAIL_" + splitType;
try {
BlackboardAttribute.Type attributeType = tskCase.getAttributeType(attributeTypeName);
if (attributeType == null) {
// Add this attribute type to the case database.
attributeType = tskCase.addArtifactAttributeType(attributeTypeName,
BlackboardAttribute.TSK_BLACKBOARD_ATTRIBUTE_VALUE_TYPE.STRING,
String.format("Email (%s)", StringUtils.capitalize(splitType.toLowerCase())));
}
ThunderbirdMboxFileIngestModule.addArtifactAttribute(email.getValue(), attributeType, attributes);
} catch (TskCoreException ex) {
logger.log(Level.SEVERE, String.format("Unable to retrieve attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
} catch (TskDataException ex) {
logger.log(Level.SEVERE, String.format("Unable to add custom attribute type '%s' for file '%s' (id=%d).", attributeTypeName, abstractFile.getName(), abstractFile.getId()), ex);
}
}
}
}
}
/**
* Generate account instances for a given VCard Telephone object.
*
* @param telephone The VCard Telephone from which to generate
* account instances.
* @param abstractFile The VCard file.
* @param accountInstances The Collection to which generated account
* instances will be added.
*/
private void addPhoneAccountInstances(Telephone telephone, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
String telephoneText = telephone.getText();
if (telephoneText == null || telephoneText.isEmpty()) {
return;
}
// Add phone number as a TSK_ACCOUNT.
try {
AccountFileInstance phoneAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.PHONE,
telephoneText, EmailParserModuleFactory.getModuleName(), abstractFile);
accountInstances.add(phoneAccountInstance);
}
catch(TskCoreException ex) {
logger.log(Level.WARNING, String.format(
"Failed to create account for phone number '%s' (content='%s'; id=%d).",
telephoneText, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
}
}
/**
* Generate account instances for a given VCard Email object.
*
* @param telephone The VCard Email from which to generate account
* instances.
* @param abstractFile The VCard file.
* @param accountInstances The Collection to which generated account
* instances will be added.
*/
private void addEmailAccountInstances(Email email, AbstractFile abstractFile, Collection<AccountFileInstance> accountInstances) {
String emailValue = email.getValue();
if (emailValue == null || emailValue.isEmpty()) {
return;
}
// Add e-mail as a TSK_ACCOUNT.
try {
AccountFileInstance emailAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.EMAIL,
emailValue, EmailParserModuleFactory.getModuleName(), abstractFile);
accountInstances.add(emailAccountInstance);
}
catch(TskCoreException ex) {
logger.log(Level.WARNING, String.format(
"Failed to create account for e-mail address '%s' (content='%s'; id=%d).",
emailValue, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
}
}
/**
* Generate device account instance for a given file.
*
* @param abstractFile The VCard file.
*
* @return The generated device account instance.
*/
private AccountFileInstance addDeviceAccountInstance(AbstractFile abstractFile) {
// Add 'DEVICE' TSK_ACCOUNT.
AccountFileInstance deviceAccountInstance = null;
String deviceId = null;
try {
long dataSourceObjId = abstractFile.getDataSourceObjectId();
DataSource dataSource = tskCase.getDataSource(dataSourceObjId);
deviceId = dataSource.getDeviceId();
deviceAccountInstance = tskCase.getCommunicationsManager().createAccountFileInstance(Account.Type.DEVICE,
deviceId, EmailParserModuleFactory.getModuleName(), abstractFile);
}
catch (TskCoreException ex) {
logger.log(Level.WARNING, String.format(
"Failed to create device account for '%s' (content='%s'; id=%d).",
deviceId, abstractFile.getName(), abstractFile.getId()), ex); //NON-NLS
}
catch (TskDataException ex) {
logger.log(Level.WARNING, String.format(
"Failed to get the data source from the case database (id=%d).",
abstractFile.getId()), ex); //NON-NLS
}
return deviceAccountInstance;
}
}