- Add validated mitre_attack frontmatter to all 754 skills (286 distinct
techniques), verified against MITRE ATT&CK v19.1 via the official
mitreattack-python library: 0 revoked, deprecated, or invalid IDs
- Curate precise per-skill technique IDs for forensics, malware-analysis,
threat-intel, and red-team skills (e.g. DCSync -> T1003.006,
Kerberoasting -> T1558.003, Pass-the-Ticket -> T1550.003)
- Reconcile v19.1 tactic restructuring: Defense Evasion split into
Stealth (TA0005) and Defense Impairment (TA0112); revoked T1562.*
family and T1070.001/.002 remapped to active equivalents (T1685.*)
- Normalize word-split tags across 35 skills (remove filename-derived
stopword tags, add semantic cybersecurity tags)
- Add api-reference.md for 3 skills that were missing it
- Update README ATT&CK section with accurate v19.1 tactic distribution
Analyze Microsoft Outlook PST and OST files for email forensic evidence including message content, headers, attachments, deleted items, and metadata using libpff, pst-utils, and forensic email analysis tools for legal investigations and incident response.
cybersecurity
digital-forensics
email-forensics
pst
ost
outlook
mapi
email-headers
attachments
deleted-emails
libpff
eml-extraction
1.0
mahipal
Apache-2.0
MANAGE-2.4
MANAGE-3.1
MEASURE-3.1
RS.AN-01
RS.AN-03
DE.AE-02
RS.MA-01
T1114.001
T1564.008
T1070.008
Analyzing Outlook PST for Email Forensics
Overview
Microsoft Outlook PST (Personal Storage Table) and OST (Offline Storage Table) files are critical evidence sources in digital forensics investigations. PST files store email messages, calendar events, contacts, tasks, and notes in a proprietary binary format based on the MAPI (Messaging Application Programming Interface) property system. Forensic analysis of these files enables recovery of deleted emails (from the Recoverable Items folder), extraction of email headers for tracing message routes, analysis of attachments for malware or exfiltrated data, and reconstruction of communication patterns. Modern PST files use Unicode format with 4KB pages and can grow up to 50GB, while legacy ANSI format is limited to 2GB.
When to Use
When investigating security incidents that require analyzing outlook pst for email forensics
When building detection rules or threat hunting queries for this domain
When SOC analysts need structured procedures for this analysis type
When validating security monitoring coverage for related attack techniques
Prerequisites
libpff/pffexport (open-source PST parser)
Python 3.8+ with pypff or libratom libraries
MailXaminer, Forensic Email Collector, or SysTools PST Forensics (commercial)
Microsoft Outlook (optional, for native PST access)
Sufficient disk space for extracted content
PST File Locations
Source
Path
Outlook 2016+ Default
%USERPROFILE%\Documents\Outlook Files*.pst
Outlook Legacy
%LOCALAPPDATA%\Microsoft\Outlook*.pst
OST Cache
%LOCALAPPDATA%\Microsoft\Outlook*.ost
Archive
%USERPROFILE%\Documents\Outlook Files\archive.pst
Analysis with Open-Source Tools
libpff / pffexport
# Export all items from PST file
pffexport -m all evidence.pst -t exported_pst
# Export only email messages
pffexport -m items evidence.pst -t exported_emails
# Export recovered/deleted items
pffexport -m recovered evidence.pst -t recovered_items
# Get PST file information
pffinfo evidence.pst
Python PST Analysis
importpypffimportosimportjsonimporthashlibimportemailimportsysfromdatetimeimportdatetimefromcollectionsimportdefaultdictclassPSTForensicAnalyzer:"""Forensic analysis of Outlook PST/OST files."""def__init__(self,pst_path:str,output_dir:str):self.pst_path=pst_pathself.output_dir=output_diros.makedirs(output_dir,exist_ok=True)self.pst=pypff.file()self.pst.open(pst_path)self.messages=[]self.attachments=[]self.stats=defaultdict(int)defprocess_folder(self,folder,folder_path:str=""):"""Recursively process PST folders and extract messages."""folder_name=folder.nameor"Root"current_path=f"{folder_path}/{folder_name}"iffolder_pathelsefolder_nameforiinrange(folder.number_of_sub_messages):try:message=folder.get_sub_message(i)msg_data=self.extract_message(message,current_path)ifmsg_data:self.messages.append(msg_data)self.stats["total_messages"]+=1exceptExceptionase:self.stats["parse_errors"]+=1foriinrange(folder.number_of_sub_folders):try:subfolder=folder.get_sub_folder(i)self.process_folder(subfolder,current_path)exceptException:continuedefextract_message(self,message,folder_path:str)->dict:"""Extract forensic metadata from a single email message."""msg_data={"folder":folder_path,"subject":message.subjector"","sender":message.sender_nameor"","sender_email":"","creation_time":str(message.creation_time)ifmessage.creation_timeelseNone,"delivery_time":str(message.delivery_time)ifmessage.delivery_timeelseNone,"modification_time":str(message.modification_time)ifmessage.modification_timeelseNone,"has_attachments":message.number_of_attachments>0,"attachment_count":message.number_of_attachments,"body_size":len(message.plain_text_bodyorb""),"html_size":len(message.html_bodyorb""),}# Extract transport headers for routing analysisheaders=message.transport_headersifheaders:msg_data["headers_present"]=Truemsg_data["headers_size"]=len(headers)# Parse key headersparsed=email.message_from_string(headers)msg_data["from_header"]=parsed.get("From","")msg_data["to_header"]=parsed.get("To","")msg_data["date_header"]=parsed.get("Date","")msg_data["message_id"]=parsed.get("Message-ID","")msg_data["x_originating_ip"]=parsed.get("X-Originating-IP","")msg_data["received_headers"]=parsed.get_all("Received",[])# Process attachmentsforjinrange(message.number_of_attachments):try:attachment=message.get_attachment(j)att_data={"message_subject":msg_data["subject"],"name":attachment.nameorf"attachment_{j}","size":attachment.size,"content_type":"",}self.attachments.append(att_data)self.stats["total_attachments"]+=1exceptException:continuereturnmsg_datadefsave_attachments(self,max_size_mb:int=100):"""Export attachments to disk for analysis."""att_dir=os.path.join(self.output_dir,"attachments")os.makedirs(att_dir,exist_ok=True)root=self.pst.get_root_folder()self._save_attachments_recursive(root,att_dir,max_size_mb)def_save_attachments_recursive(self,folder,att_dir,max_size_mb):foriinrange(folder.number_of_sub_messages):try:message=folder.get_sub_message(i)forjinrange(message.number_of_attachments):att=message.get_attachment(j)ifatt.sizeandatt.size<max_size_mb*1024*1024:name=att.nameorf"unknown_{i}_{j}"safe_name="".join(cifc.isalnum()orcin".-_"else"_"forcinname)path=os.path.join(att_dir,safe_name)try:data=att.read_buffer(att.size)withopen(path,"wb")asf:f.write(data)exceptException:continueexceptException:continueforiinrange(folder.number_of_sub_folders):try:self._save_attachments_recursive(folder.get_sub_folder(i),att_dir,max_size_mb)exceptException:continuedefgenerate_report(self)->str:"""Generate comprehensive PST forensic analysis report."""root=self.pst.get_root_folder()self.process_folder(root)report={"analysis_timestamp":datetime.now().isoformat(),"pst_file":self.pst_path,"pst_size_bytes":os.path.getsize(self.pst_path),"statistics":dict(self.stats),"messages":self.messages[:500],"attachments":self.attachments[:200],}report_path=os.path.join(self.output_dir,"pst_forensic_report.json")withopen(report_path,"w")asf:json.dump(report,f,indent=2,default=str)print(f"[*] Total messages: {self.stats['total_messages']}")print(f"[*] Total attachments: {self.stats['total_attachments']}")print(f"[*] Parse errors: {self.stats['parse_errors']}")returnreport_pathdefclose(self):self.pst.close()defmain():iflen(sys.argv)<3:print("Usage: python process.py <pst_file> <output_dir>")sys.exit(1)analyzer=PSTForensicAnalyzer(sys.argv[1],sys.argv[2])analyzer.generate_report()analyzer.close()if__name__=="__main__":main()