Skip to content

eDiscovery — define a search that also covers partially indexed items.

Partially indexed items (unsupported file types, encrypted content, or items the indexer could not parse) are invisible to a normal search but can still be relevant to an investigation. A non-custodial search scoped with dataSourceScopes is how you reach them.

This script creates a short-lived case and a search over all tenant mailboxes and sites, prints the search id, then removes the case again.

python search.py
python search.py --query "*" --scopes allTenantMailboxes,allTenantSites
python search.py --keep

Permissions

Requires the application permission eDiscovery.ReadWrite.All.

Reference

View source

from __future__ import annotations

import argparse

from office365.graph_client import GraphClient
from office365.runtime.exceptions import AccessDeniedException
from tests import create_unique_name
from tests.settings import client_id, client_secret, tenant

_SCOPE = "eDiscovery.ReadWrite.All"
_DEFAULT_SCOPES = "allTenantMailboxes,allTenantSites"


def _client() -> GraphClient:
    return GraphClient(tenant=tenant).with_client_secret(client_id, client_secret)


def main() -> None:
    parser = argparse.ArgumentParser(description="Create an eDiscovery non-custodial search")
    parser.add_argument("--query", default="*", help="Search query (default: everything)")
    parser.add_argument("--scopes", default=_DEFAULT_SCOPES, help="Comma-separated dataSourceScopes")
    parser.add_argument("--keep", action="store_true", help="Keep the case instead of deleting it")
    args = parser.parse_args()

    client = _client()
    case = None
    try:
        case = client.security.cases.ediscovery_cases.add(
            displayName=create_unique_name("Partially indexed scan"),
        ).execute_query()

        search = case.searches.add(
            displayName="Partially indexed items",
            contentQuery=args.query,
            dataSourceScopes=args.scopes,
        ).execute_query()
        print(f"Case:   {case.get_property('displayName')}  ({case.id})")
        print(f"Search: {search.get_property('displayName')}  ({search.id})")
        print(f"Scopes: {args.scopes}")
    except AccessDeniedException as exc:
        print(f"Access denied: {exc.message}")
        print(f"Grant the application permission {_SCOPE} to the app registration and re-run.")
        raise SystemExit(0) from None

    print("\nStart the estimate from the Purview portal (or POST /searches/{id}/estimateStatistics);")
    print("the operation's itemCount vs indexedItemCount reveals the partially indexed items.")

    if args.keep:
        print(f"\nCase retained — open it in the Purview portal (id {case.id}).")
    elif case is not None:
        case.delete_object().execute_query()
        print("\nCase deleted.")


if __name__ == "__main__":
    main()

← Back to eDiscovery