A Python tool for parsing OneNote package (.onepkg) files. Extracts notebook structure, sections, pages, text content, and embedded images.
- Parse
.onepkgcabinet archives - Extract text content and embedded images
- Search across multiple notebooks
- Generate HTML reports
- Threat intelligence analysis (detect credentials, API keys, tokens, etc.) with modular, user-extensible pattern files
- OCR image text extraction via Tesseract automatically runs pattern matching on extracted text
- AI-powered image descriptions via Ollama
This tool does not implement the full specification; instead it uses structural properties of the binary to reconstruct the notebook hierarchy to extract forensically interesting artifacts.
PropertyIDs used by the parser:
| PropertyID | Bytes (LE) | Kind | Type | MS-ONE property | Role in parser |
|---|---|---|---|---|---|
0x08001CB5 |
B5 1C 00 08 |
Non-string | Bool | IsTitleText (true variant) | Proximity marker string within 8–36 bytes after this ID classified as title |
0x14001D09 |
09 1D 00 14 |
Non-string | FourBytesOfData | CreationTimeStamp | 4-byte Unix timestamp; earliest value per section exported as created_at |
0x14001D7A |
7A 1D 00 14 |
Non-string | FourBytesOfData | LastModifiedTime | 4-byte Unix timestamp; latest value per section exported as modified_at |
0x18001D77 |
77 1D 00 18 |
Non-string | EightBytesOfData | LastModifiedTimeStamp | 8-byte Windows FILETIME; latest value per section exported as modified_at |
0x1C001C0A |
0A 1C 00 1C |
String | ObjectSpaceStream | FontName | In rgPrids block what precedes the font string in packed data is an LCID, not this PropertyID |
0x1C001CF3 |
F3 1C 00 1C |
String | ObjectSpaceStream | CachedTitleString | Explicit title tag any string under this ID is a page title |
0x1C001D3C |
3C 1D 00 1C |
String | ObjectSpaceStream | CachedTitleStringFromPage | Explicit title tag any string under this ID is a page title |
0x1C001D75 |
75 1D 00 1C |
String | ObjectSpaceStream | Author | Filtered author names discarded |
0x1C001DD7 |
D7 1D 00 1C |
String | ObjectSpaceStream | ImageFilename | Optional (rarely set); not present in test files |
0x1C001E58 |
58 1E 00 1C |
String | ObjectSpaceStream | ImageAltText | Image alt text; not present in test files |
0x1C00349B |
9B 34 00 1C |
String | ObjectSpaceStream | SectionDisplayName | Spec-defined section name; not present in test files |
AuthorOriginal (0x20001D78)andAuthorMostRecent (0x20001D79)are ObjectID-typed properties; they reference author entries rather than carrying string data directly, and require no separate filtering.
IsTitleText (0x08001CB4)is a Bool property: per MS-ONESTORE §2.6.1, the boolean value is encoded in bit 0 of the PropertyID itself rather than in the data vector. The true variant (0x08001CB5) is scanned as a proximity marker; a string appearing 8–36 bytes after it in the binary is classified as a page title.
Object-level JCIDs (FileNode headers present in binary but not adjacent to string data):
| JCID | Value | Meaning | Used in parser |
|---|---|---|---|
jcidSectionNode |
0x00060007 |
Section object | |
jcidPageSeriesNode |
0x00060008 |
Page-series container | |
jcidPageNode |
0x0006000B |
Individual page object | Image-to-page assignment |
jcidOutlineNode |
0x0006000C |
Outline / content block | |
jcidTitleNode |
0x0006002C |
Page-title object | (distinct-tag classification used instead) |
ImageFilename (0x1C001DD7) is only referenced by jcidImageNode and is not set by default; metadata-driven extraction is therefore unreliable in practice. The parser falls back to signature (magic byte) carving, scanning the raw binary data for known image file headers.
| Format | Signature (hex) | End detection |
|---|---|---|
| PNG | 89 50 4E 47 0D 0A 1A 0A |
IEND chunk + CRC (AE 42 60 82) |
| JPEG / JFIF / Exif | FF D8 FF |
FF D9 EOI marker¹ |
| GIF87a / GIF89a | GIF8 (47 49 46 38) |
00 3B block-terminator + trailer |
| BMP | 42 4D |
File size at header offset 2–5 |
| TIFF little-endian | 49 49 2A 00 |
IFD chain traversal² |
| TIFF big-endian | 4D 4D 00 2A |
IFD chain traversal² |
| WEBP | 52 49 46 46 … 57 45 42 50 |
RIFF chunk-size field (offset 4–7)³ |
¹ Exif data can embed a thumbnail JPEG before the main image stream; both end with
FF D9, so the carved extent may stop at the thumbnail boundary.
² TIFF has no footer. The parser traverses every IFD and its referenced value blocks, using the maximum byte offset reached as the practical end of the image.
³ WEBP is only accepted when bytes 8–11 of the RIFF container equal
WEBP; the total file size is8 + RIFF_fileSize_field.
Each image's byte offset in the file is compared against the sorted list of page title offsets using the same logic as content assignment. Images are heuristically assigned to the last page whose title offset precedes them in the binary. jcidPageNode markers (0x0006000B) are present in the binary and confirm page boundaries, but each page generates multiple jcidPageNode references (manifest, file-node list, etc.), making direct counting unreliable; title-offset comparison gives cleaner results without needing to understand the full FileNode structure.
Once titles and content strings are classified, each content string is assigned to the page whose title offset is the largest value ≤ the content string's offset - i.e. the last page title that appeared before it in the binary.
Once content strings are extracted and assigned to pages, threat analysis walks the parsed structure in order: notebook → sections → pages → content strings (including the page title itself). Each string is run through every compiled regex pattern via re.finditer, so a single string can produce multiple findings from different pattern categories.
A per-run seen_values set deduplicates matches: if the same credential value is extracted from more than one string or page, only the first occurrence is reported. False positives (localhost, 0.0.0.0, example.com, etc.) are filtered immediately after matching. Values longer than 100 characters (e.g. private key blobs) are truncated for display while the full match is still used for deduplication.
The page title is also scanned - credentials may occasionally appear in note titles rather than body content.
OCR text (when --ocr is active) is scanned after content strings using the same pattern engine. Findings from OCR carry "source": "ocr_image" in the JSON output so they can be distinguished from text-layer findings. Before scanning, common Tesseract character-substitution errors in credential keywords are normalised (e.g. passward → password, usernane → username, l0gin → login) so that OCR typos do not silently suppress findings. The separator pattern also accepts - in addition to =/: to handle config-dump screenshot formats such as password-secret;.
Each OneNote notebook directory contains a .onetoc2 file that lists the .one section files in display order (as UTF-16 strings). The parser reads this file to present sections in the same order they appear in the OneNote sidebar.
- Python 3.10+
cabextractsystem utility (for extracting.onepkgarchives)- Python dependencies - install via pip:
pip install -r requirements.txt| Package | Purpose | Required? |
|---|---|---|
pyyaml |
Load threat intelligence pattern files | Yes (for --threat-intel / --ocr) |
pytesseract |
Python bindings for Tesseract OCR | Yes (for --ocr) |
Pillow |
Image decoding for Tesseract input | Yes (for --ocr) |
rich |
Coloured terminal output, progress bar, summary table | Optional (degrades gracefully) |
cabarchive |
Pure-Python .onepkg extraction fallback |
Optional |
# Install system dependencies (Debian/Ubuntu)
sudo apt install cabextract tesseract-ocr
# macOS
brew install cabextract tesseract
# Install Python dependencies
pip install -r requirements.txt# Parse a single file and pretty-print JSON
python3 onenote_parser.py -f notebook.onepkg --pretty
# Parse a raw .one section file
python3 onenote_parser.py -f section.one --pretty
# Parse all notebooks in a directory (recursive)
python3 onenote_parser.py -d ./notebooks/ --pretty
# Generate an HTML report from a single file
python3 onenote_parser.py -f notebook.onepkg --html report.html
# Generate an HTML report from a directory
python3 onenote_parser.py -d ./notebooks/ --html report.html
# Parse multiple files into one report
python3 onenote_parser.py -f notebook1.onepkg -f notebook2.onepkg --html report.html
# Re-generate an HTML report from a previously saved JSON result (no re-parsing)
python3 onenote_parser.py --from-json results/MyNotebook_20260101_120000.json --html report.html
# Search for a keyword across a notebook
python3 onenote_parser.py -f notebook.onepkg -s "password"
# Search across a whole directory
python3 onenote_parser.py -d ./notebooks/ -s "api_key" --regex
# Extract images to an auto-named directory next to the input
python3 onenote_parser.py -f notebook.onepkg --extract-images
# Extract images to an explicit directory
python3 onenote_parser.py -f notebook.onepkg --extract-images ./images/
# Threat intelligence analysis - HTML report with findings
python3 onenote_parser.py -f notebook.onepkg --threat-intel --html report.html
# Threat intel across a directory, excluding low-severity findings
python3 onenote_parser.py -d ./notebooks/ --threat-intel --no-low-severity --html report.html
# Threat intel with a custom patterns directory
python3 onenote_parser.py -f notebook.onepkg --threat-intel --patterns-dir ./my-patterns/
# OCR - extract text from images and run pattern matching on results
python3 onenote_parser.py -f notebook.onepkg --ocr --html report.html
# OCR combined with full threat-intel (content text + image text both scanned)
python3 onenote_parser.py -f notebook.onepkg --ocr --threat-intel --html report.html
# Extract images to disk and generate a linked HTML report (no base64 bloat)
python3 onenote_parser.py -f notebook.onepkg --extract-images ./images/ --html report.html
# AI image descriptions via Ollama
python3 onenote_parser.py -f notebook.onepkg --analyse-images --html report.html
# AI summaries of pages, sections, and notebooks
python3 onenote_parser.py -f notebook.onepkg --summarise --html report.html
# Full analysis - threat intel, OCR, AI descriptions, summaries, HTML report
python3 onenote_parser.py -f notebook.onepkg \
--threat-intel --ocr --analyse-images --summarise \
--extract-images ./images/ --html report.html
# Full analysis against a remote Ollama instance
python3 onenote_parser.py -f notebook.onepkg \
--threat-intel --ocr --analyse-images --summarise \
--ollama-url http://192.168.8.10:11434 --html report.html| Option | Description |
|---|---|
-f, --file FILE |
Path to a .onepkg or .one file to parse (can be repeated) |
-d, --directory DIR |
Directory of .one files to parse recursively (can be repeated) |
-o, --output |
Output file path for JSON/text |
--format |
Output format: json or text |
--html FILE |
Generate HTML report |
--from-json FILE |
Re-generate an HTML report from a previously saved JSON result file (requires --html; skips re-parsing) |
-s, --search |
Search for text in notebooks |
--extract-images [DIR] |
Extract images to DIR; omit DIR to auto-name the directory next to the input. Runs alongside all other options (HTML, threat-intel, JSON output) |
--threat-intel |
Analyse for credentials, keys, tokens |
--ocr |
Extract text from images using Tesseract OCR; automatically runs pattern matching on the extracted text. Findings are tagged source: ocr_image in JSON output |
--no-low-severity |
Exclude low-severity findings (IPs, emails) from threat analysis or OCR scan |
--patterns-dir DIR |
Directory containing .yaml threat pattern files (default: patterns/ next to the script) |
--analyse-images |
Use Ollama to describe images |
--summarise |
Generate AI summaries |
--ollama-url URL |
Ollama API URL (default: http://localhost:11434) |
--vision-model MODEL |
Ollama vision model to use (default: auto-detect) |
--text-model MODEL |
Ollama text model for summaries (default: auto-detect) |
-v, --verbose |
Print progress messages |
--ocr uses Tesseract to extract text from embedded images and then runs the full threat intelligence pattern engine against what it finds.
# OCR only - pattern-match against image text, no full content scan
python3 onenote_parser.py -f notebook.onepkg --ocr
# OCR combined with full threat-intel - content text AND image text both scanned
python3 onenote_parser.py -f notebook.onepkg --ocr --threat-intelRequirements - both the Python bindings and the Tesseract binary must be installed:
# System binary
sudo apt install tesseract-ocr # Debian/Ubuntu
brew install tesseract # macOS
# Python packages
pip install pytesseract PillowIf either is missing, a clear warning is printed and the run continues without OCR.
How it works:
- After parsing, OCR is run on every embedded image. The extracted text is stored in the
ocr_textfield of each image object and included in the JSON output. - Before pattern matching, common Tesseract character substitutions in credential keywords are corrected automatically; for example
passward→password,usernane→username,l0gin→login. This prevents OCR noise from suppressing findings. - The same pattern engine used for
--threat-intelis applied to the normalised OCR text. Findings from image text carry"source": "ocr_image"in the JSON so they can be distinguished from text-layer findings. - When used alone (
--ocrwithout--threat-intel), only image-text findings are returned. Combined with--threat-intel, all content text and all image text are scanned together.
JSON output example:
{
"type": "credential",
"severity": "high",
"value": "YWRtaW4=",
"section": "Azure Creds",
"page": "Azure Creds",
"source": "ocr_image"
}Summary table - the Images column shows how many images yielded OCR text (e.g. 5 (3 OCR)). The Sev H/M/L and Match types columns cover all findings including those from OCR.
Pattern definitions live in the patterns/ directory as YAML files. Each file groups related patterns by category:
patterns/
cloud_credentials.yaml # AWS, Azure, GCP keys
credentials.yaml # passwords, basic auth, connection strings
hashes.yaml # MD5, SHA1, SHA256 hashes
network.yaml # IP addresses, URLs, email addresses
private_keys.yaml # RSA, PGP, SSH private keys
tokens.yaml # JWT, Bearer, GitHub, Slack, generic API keys
custom/ # drop your own .yaml files here
example.yaml # annotated template
Copy patterns/custom/example.yaml and add your own entries. Any .yaml file dropped into patterns/custom/ is loaded automatically; no script edits required.
Each pattern entry has three fields:
my_pattern_name:
pattern: 'MYCOMPANY-[A-Z0-9]{24}'
severity: high # high | medium | low
type: api_key # free-form label shown in reportsPattern strings are standard Python regex (passed to re.compile with IGNORECASE | MULTILINE).
Use --patterns-dir to point at a completely different directory if needed.