Add SQLite table to JSONL export tool
This commit is contained in:
@@ -70,6 +70,7 @@ Format: `path | goal | usage`. This section is intentionally compact so `what` c
|
|||||||
|
|
||||||
- `tools/forensics/chechsqlite.py` | goal: inspect SQLite databases for password or hash style columns | usage: `python3 tools/forensics/chechsqlite.py sample.db`
|
- `tools/forensics/chechsqlite.py` | goal: inspect SQLite databases for password or hash style columns | usage: `python3 tools/forensics/chechsqlite.py sample.db`
|
||||||
- `tools/forensics/extractfolder.py` | goal: bulk-extract or sort files from a folder workflow | usage: `python3 tools/forensics/extractfolder.py input_dir`
|
- `tools/forensics/extractfolder.py` | goal: bulk-extract or sort files from a folder workflow | usage: `python3 tools/forensics/extractfolder.py input_dir`
|
||||||
|
- `tools/forensics/sqlite32jsonl.py` | goal: export each user table from a SQLite database to one JSONL file with robust filename handling | usage: `python3 tools/forensics/sqlite32jsonl.py sample.db -o outdir`
|
||||||
- `tools/forensics/process_leak.py` | goal: inspect process-leak style artifacts | usage: `python3 tools/forensics/process_leak.py artifact`
|
- `tools/forensics/process_leak.py` | goal: inspect process-leak style artifacts | usage: `python3 tools/forensics/process_leak.py artifact`
|
||||||
- `tools/forensics/mailunpack` | goal: extract mail attachments inside a constrained container workflow | usage: `tools/forensics/mailunpack message.eml`
|
- `tools/forensics/mailunpack` | goal: extract mail attachments inside a constrained container workflow | usage: `tools/forensics/mailunpack message.eml`
|
||||||
- `tools/forensics/showgm.sh` | goal: open image GPS EXIF coordinates in Google Maps | usage: `tools/forensics/showgm.sh image.jpg`
|
- `tools/forensics/showgm.sh` | goal: open image GPS EXIF coordinates in Google Maps | usage: `tools/forensics/showgm.sh image.jpg`
|
||||||
@@ -218,6 +219,7 @@ Format: `path | goal | usage`. This section is intentionally compact so `what` c
|
|||||||
|
|
||||||
- `tools/forensics/chechsqlite.py`: inspects SQLite databases for password/hash-like fields and consistency issues.
|
- `tools/forensics/chechsqlite.py`: inspects SQLite databases for password/hash-like fields and consistency issues.
|
||||||
- `tools/forensics/extractfolder.py`: folder extraction/helper script for bulk processing.
|
- `tools/forensics/extractfolder.py`: folder extraction/helper script for bulk processing.
|
||||||
|
- `tools/forensics/sqlite32jsonl.py`: exports each user table in a SQLite database to a separate JSONL file with argparse help and logging.
|
||||||
- `tools/forensics/mailunpack`: containerized `munpack` wrapper for extracting mail attachments safely.
|
- `tools/forensics/mailunpack`: containerized `munpack` wrapper for extracting mail attachments safely.
|
||||||
- `tools/forensics/process_leak.py`: process-memory or artifact triage helper.
|
- `tools/forensics/process_leak.py`: process-memory or artifact triage helper.
|
||||||
- `tools/forensics/showgm.sh`, `showosm.sh`: extract GPS EXIF data from images and open the location in Google Maps or OpenStreetMap.
|
- `tools/forensics/showgm.sh`, `showosm.sh`: extract GPS EXIF data from images and open the location in Google Maps or OpenStreetMap.
|
||||||
|
|||||||
165
tools/forensics/sqlite32jsonl.py
Executable file
165
tools/forensics/sqlite32jsonl.py
Executable file
@@ -0,0 +1,165 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import base64
|
||||||
|
import json
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import sqlite3
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger("sqlite32jsonl")
|
||||||
|
SAFE_NAME_RE = re.compile(r"[\\/\x00-\x1f\x7f]+")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_args() -> argparse.Namespace:
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Export each user table in a SQLite database to a JSONL file."
|
||||||
|
)
|
||||||
|
parser.add_argument("database", type=Path, help="Path to the SQLite database file")
|
||||||
|
parser.add_argument(
|
||||||
|
"-o",
|
||||||
|
"--output-dir",
|
||||||
|
type=Path,
|
||||||
|
default=Path.cwd(),
|
||||||
|
help="Directory for exported .jsonl files (default: current directory)",
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
"--log-level",
|
||||||
|
default="INFO",
|
||||||
|
choices=["DEBUG", "INFO", "WARNING", "ERROR"],
|
||||||
|
help="Logging verbosity (default: INFO)",
|
||||||
|
)
|
||||||
|
return parser.parse_args()
|
||||||
|
|
||||||
|
|
||||||
|
def configure_logging(level_name: str) -> None:
|
||||||
|
logging.basicConfig(
|
||||||
|
level=getattr(logging, level_name),
|
||||||
|
format="%(levelname)s %(message)s",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def safe_output_stem(table_name: str) -> str:
|
||||||
|
safe_name = SAFE_NAME_RE.sub("_", table_name).strip()
|
||||||
|
return safe_name or "table"
|
||||||
|
|
||||||
|
|
||||||
|
def unique_output_path(output_dir: Path, table_name: str, used_names: set[str]) -> Path:
|
||||||
|
stem = safe_output_stem(table_name)
|
||||||
|
candidate = f"{stem}.jsonl"
|
||||||
|
suffix = 2
|
||||||
|
|
||||||
|
while candidate in used_names:
|
||||||
|
candidate = f"{stem}_{suffix}.jsonl"
|
||||||
|
suffix += 1
|
||||||
|
|
||||||
|
used_names.add(candidate)
|
||||||
|
return output_dir / candidate
|
||||||
|
|
||||||
|
|
||||||
|
def json_value(value):
|
||||||
|
if isinstance(value, bytes):
|
||||||
|
return {"$base64": base64.b64encode(value).decode("ascii")}
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def list_tables(connection: sqlite3.Connection) -> list[str]:
|
||||||
|
cursor = connection.execute(
|
||||||
|
"""
|
||||||
|
SELECT name
|
||||||
|
FROM sqlite_master
|
||||||
|
WHERE type = 'table'
|
||||||
|
AND name NOT LIKE 'sqlite_%'
|
||||||
|
ORDER BY name
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
return [row[0] for row in cursor]
|
||||||
|
|
||||||
|
|
||||||
|
def export_table(
|
||||||
|
connection: sqlite3.Connection,
|
||||||
|
table_name: str,
|
||||||
|
output_path: Path,
|
||||||
|
) -> int:
|
||||||
|
cursor = connection.execute(f'SELECT * FROM "{table_name.replace("\"", "\"\"")}"')
|
||||||
|
columns = [description[0] for description in cursor.description]
|
||||||
|
row_count = 0
|
||||||
|
|
||||||
|
with output_path.open("w", encoding="utf-8", newline="\n") as handle:
|
||||||
|
for row in cursor:
|
||||||
|
record = {
|
||||||
|
column: json_value(value)
|
||||||
|
for column, value in zip(columns, row, strict=True)
|
||||||
|
}
|
||||||
|
handle.write(json.dumps(record, ensure_ascii=False))
|
||||||
|
handle.write("\n")
|
||||||
|
row_count += 1
|
||||||
|
|
||||||
|
return row_count
|
||||||
|
|
||||||
|
|
||||||
|
def validate_paths(database: Path, output_dir: Path) -> None:
|
||||||
|
if not database.is_file():
|
||||||
|
raise FileNotFoundError(f"Database file not found: {database}")
|
||||||
|
|
||||||
|
output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if not output_dir.is_dir():
|
||||||
|
raise NotADirectoryError(f"Output path is not a directory: {output_dir}")
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
args = parse_args()
|
||||||
|
configure_logging(args.log_level)
|
||||||
|
|
||||||
|
try:
|
||||||
|
validate_paths(args.database, args.output_dir)
|
||||||
|
except Exception as exc:
|
||||||
|
LOGGER.error("%s", exc)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
used_names: set[str] = set()
|
||||||
|
failures = 0
|
||||||
|
|
||||||
|
try:
|
||||||
|
with sqlite3.connect(args.database) as connection:
|
||||||
|
tables = list_tables(connection)
|
||||||
|
if not tables:
|
||||||
|
LOGGER.info("No user tables found in %s", args.database)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
LOGGER.info("Exporting %d table(s) from %s", len(tables), args.database)
|
||||||
|
|
||||||
|
for table_name in tables:
|
||||||
|
output_path = unique_output_path(args.output_dir, table_name, used_names)
|
||||||
|
try:
|
||||||
|
row_count = export_table(connection, table_name, output_path)
|
||||||
|
except Exception:
|
||||||
|
failures += 1
|
||||||
|
output_path.unlink(missing_ok=True)
|
||||||
|
LOGGER.exception("Failed to export table %r", table_name)
|
||||||
|
continue
|
||||||
|
|
||||||
|
LOGGER.info(
|
||||||
|
"Created %s from table %r with %d row(s)",
|
||||||
|
output_path,
|
||||||
|
table_name,
|
||||||
|
row_count,
|
||||||
|
)
|
||||||
|
except sqlite3.Error:
|
||||||
|
LOGGER.exception("Failed to read database %s", args.database)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
if failures:
|
||||||
|
LOGGER.error("Completed with %d failed table export(s)", failures)
|
||||||
|
return 1
|
||||||
|
|
||||||
|
LOGGER.info("Export complete")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
Reference in New Issue
Block a user