Skip to main content

Exhume Indexer as a library

exhume_indexer can be embedded in Rust tools that need to create or enrich an Exhume SQLite index without invoking the CLI.

The library exposes helpers for database initialization, evidence and partition registration, filesystem indexing, file signature identification, artefact identification, and artefact extraction.

Install as a library​

Create a project:

cargo new indexer_test
cd indexer_test

Open the Cargo.toml file and add the required dependencies:

[dependencies]
exhume_indexer = "=0.1.2"
exhume_body = "=0.5.5"
exhume_filesystem = "=0.5.2"
exhume_artefacts = "=0.2.6"

tokio = { version = "1", features = ["macros", "rt-multi-thread", "sync"] }
sqlx = { version = "0.8", features = ["sqlite", "runtime-tokio-rustls", "json"] }

Core API​

The most common functions are:

  • ensure_tables: create or migrate the SQLite schema expected by Exhume Indexer.
  • ensure_evidence_row: create or update the source evidence row.
  • insert_partition: register a partition or folder scope.
  • index_partition_with_format: detect and index a filesystem partition from a disk image.
  • index_folder: index a folder through the filesystem abstraction.
  • identify_file_types: enrich indexed files with signature-based file type metadata.
  • identify_artefacts: match indexed files against the embedded or custom artefact catalog.
  • extract_artefacts: run registered parsers against matched artefacts.

Progress can be observed by passing a Tokio mpsc::Sender<IndexerEvent> to the indexing and enrichment functions.

Opening the database​

The library expects a sqlx::SqlitePool. The CLI uses a single connection because indexing performs bulk inserts and transactions.

use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::SqlitePool;
use std::path::Path;
use std::time::Duration;

async fn open_pool(path: &Path) -> Result<SqlitePool, sqlx::Error> {
let opts = SqliteConnectOptions::new()
.filename(path)
.create_if_missing(true)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal);

SqlitePoolOptions::new()
.max_connections(1)
.idle_timeout(Duration::from_secs(60))
.connect_with(opts)
.await
}

Indexing a folder​

This example initializes the database, registers an evidence row, registers a folder partition, and indexes the folder.

use exhume_indexer::{
ensure_evidence_row, ensure_tables, index_folder, insert_partition, PartitionKind,
};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let source = "/path/to/folder";
let pool = open_pool(Path::new("/path/to/index.sqlite")).await?;

let evidence_id = 1;
ensure_tables(&pool).await?;
ensure_evidence_row(&pool, evidence_id, source, true).await?;

let partition_id = insert_partition(
&pool,
evidence_id,
PartitionKind::Folder,
0,
0,
0,
0,
None,
)
.await?;

index_folder(
evidence_id,
partition_id,
source.to_string(),
&pool,
None,
None,
)
.await;

Ok(())
}

Indexing a disk image partition​

When indexing a partition from a disk image, provide:

  • evidence_id: the evidence row to attach files to.
  • partition_id: the partition row to attach files to.
  • size_sectors: the partition size in sectors.
  • first_byte_addr: the partition start offset in bytes.
  • disk_image_path: the source image path.
  • body_format: auto, raw, ewf, or another format supported by exhume_body.
use exhume_indexer::{
ensure_evidence_row, ensure_tables, index_partition_with_format, insert_partition,
PartitionKind,
};
use std::path::Path;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let image_path = "/path/to/disk.E01";
let first_byte_addr = 0x100000;
let size_sectors = 0x79800;
let sector_size = 512;
let size_bytes = size_sectors * sector_size;

let pool = open_pool(Path::new("/path/to/index.sqlite")).await?;
let evidence_id = 1;

ensure_tables(&pool).await?;
ensure_evidence_row(&pool, evidence_id, image_path, false).await?;

let partition_id = insert_partition(
&pool,
evidence_id,
PartitionKind::Logical,
first_byte_addr,
size_sectors,
sector_size,
size_bytes,
None,
)
.await?;

index_partition_with_format(
evidence_id,
partition_id,
size_sectors,
first_byte_addr,
image_path.to_string(),
"auto",
&pool,
None,
None,
)
.await?;

Ok(())
}

Tracking progress​

Progress events are optional. Pass a Tokio channel sender when you need to display progress or bridge status updates to another application.

use exhume_indexer::{IndexerEvent, IndexerEventType};
use tokio::sync::mpsc;

let (tx, mut rx) = mpsc::channel::<IndexerEvent>(100);

tokio::spawn(async move {
while let Some(event) = rx.recv().await {
match event.event_type {
IndexerEventType::Progress { current, total } => {
println!("{current}/{total}: {}", event.message);
}
_ => println!("{}", event.message),
}
}
});

// Pass Some(tx) to index_folder, index_partition_with_format,
// identify_file_types, identify_artefacts, or extract_artefacts.

Enriching the index​

After indexing files, the library can add file signature information and artefact metadata.

use exhume_artefacts::parsers::build_registry;
use exhume_indexer::artifacts::{extract_artefacts, identify_artefacts};
use exhume_indexer::identification::identify_file_types;

// `fs` is a mutable filesystem object from exhume_filesystem.
identify_file_types(&mut fs, evidence_id, partition_id, &pool, None).await;

identify_artefacts(
evidence_id,
partition_id,
&pool,
None,
None, // Optional custom artifacts.yaml path.
)
.await;

let registry = build_registry();
extract_artefacts(
evidence_id,
partition_id,
&pool,
&mut fs,
&registry,
None,
None,
)
.await;

Cancellation​

Long-running indexing functions accept an optional cancellation token:

use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};

let cancel = Arc::new(AtomicBool::new(false));

// Pass Some(cancel.clone()) to indexing functions.
cancel.store(true, Ordering::Relaxed);

Cancellation is cooperative. The current operation checks the token during indexing and returns without committing unfinished work when cancellation is observed.