Provider-agnostic identity observability for detecting identity control drift, privilege creep, and related risky access patterns over time.
IdentityTide’s core engine is never aware of the identity provider. Provider-specific logic lives behind adapters (e.g., Entra ID / Microsoft Graph).
Identity systems evolve: temporary access becomes permanent, roles accumulate, offboarding is incomplete, and “emergency” elevation persists. IdentityTide tracks identity/entitlement/assignment state over time and produces:
- Snapshots of identities, entitlements, assignments, and (optionally) activity
- Time-series change events between snapshots (added/removed/updated)
- Drift findings (MVP)
- Privilege creep (privileged entitlement count increased since last snapshot)
- Dormant privileged identities (privileged but no recent activity)
- Explainable risk scoring (deterministic, auditor-friendly)
- Policy-as-code evaluation (JSON policies; violations are explicit)
cli/ # CLI entrypoints
core/ # Provider-agnostic models + drift/risk/policy engines
providers/ # Provider adapters (entra/, okta/, google_workspace/, mock/)
storage/ # Local snapshot + event storage (default location)
policies/ # Policy-as-code JSON
tests/ # Minimal unit tests
The engine operates on a canonical model independent of providers:
- Identity:
id,type(user/service/admin),display_name,manager_id,status,source - Entitlement:
id,name,category(role/group/permission),privilege_level,scope - Assignment:
identity_id,entitlement_id,assigned_at,expires_at,assignment_type,justification,source_ticket - ActivityEvent:
identity_id,occurred_at,action
Provider adapters map native objects (Graph/Okta/etc.) into this model.
- Python 3.12+
No external Python dependencies are required for the MVP (the Entra adapter uses the standard library urllib).
python -m cli scan --provider mock --out "storage\\report.json"You should see a brief summary printed and a full JSON report written to storage/report.json.
The Entra adapter uses client credentials to call Microsoft Graph.
Create an app registration and a client secret, then grant the app the permissions required to read:
- Users
- Groups
- Directory roles
- (Optional) Audit logs for activity telemetry
Exact permissions depend on tenant policy; the adapter will return empty activity if audit log access is not granted.
$env:AZURE_TENANT_ID="<tenant-guid>"
$env:AZURE_CLIENT_ID="<app-client-id>"
$env:AZURE_CLIENT_SECRET="<client-secret>"python -m cli scan --provider entra --out "storage\\entra-report.json"
## Using Okta
The Okta adapter uses the Okta Core API with an API token.
### 1) Set environment variables
```powershell
$env:OKTA_ORG_URL="https://your-org.okta.com"
$env:OKTA_API_TOKEN="<api-token>"python -m cli scan --provider okta --out "storage\\okta-report.json"Notes:
- Groups and group memberships are collected via
/api/v1/groupsand related membership endpoints. - Okta admin roles are inferred via per-user role listings (
/api/v1/users/{id}/roles) and represented as canonical entitlements. - Activity is best-effort via the System Log (
/api/v1/logs); if it fails/permissioned out, activity will be empty.
The Google Workspace adapter uses the Admin SDK Directory API and expects an OAuth access token.
$env:GOOGLE_WORKSPACE_ACCESS_TOKEN="<oauth-access-token>"
# Optional (defaults to my_customer)
$env:GOOGLE_WORKSPACE_CUSTOMER_ID="my_customer"python -m cli scan --provider google --out "storage\\google-report.json"Notes:
- Users, groups, and group memberships are collected from the Directory API.
- Admin roles and role assignments are best-effort (require additional scopes); if not available, those portions will be skipped.
## CLI
### `scan`
Fetches provider data, persists a snapshot, diffs against the last snapshot (if any), and returns a report containing drift findings, risk scores, and policy violations.
Common flags:
- `--provider entra|okta|google|mock`
- `--storage-dir <path>` (default: `storage`)
- `--policy <path>` (default: `policies/default.json`)
- `--no-policy` (skip policy evaluation)
- `--out <path>` (write full JSON report)
Examples:
```powershell
python -m cli scan --provider mock --storage-dir storage --policy policies\\default.json --out storage\\report.json
python -m cli scan --provider mock --no-policy
IdentityTide stores data locally by default:
- Snapshots:
storage/snapshots/<provider>/<timestamp>.json - Event log:
storage/events.jsonl(one JSON object per line) - State:
storage/state.json(tracks last snapshot per provider)
This enables historical analysis and audit evidence without relying on a vendor-specific database.
The drift engine currently detects:
- Privilege creep: privileged entitlement count for an identity increased compared to the previous snapshot.
- Dormant privileged identity:
- If activity telemetry exists: privileged identity’s last activity is older than
dormant_privileged_days. - If no activity telemetry exists: identity is flagged as "no activity telemetry" (lower confidence).
- If activity telemetry exists: privileged identity’s last activity is older than
Configuration (defaults) is in core/drift/detectors.py.
Risk scoring is deterministic and returns a breakdown per identity:
privileged_roles(count)*30 + days_active(days)/10 + emergency_access(count)*25
Weights and thresholds can be tuned in core/risk/scoring.py.
Policies are simple JSON.
Default policy: policies/default.json
Supported policy controls (MVP):
privileged_threshold(default 70)max_privileged_roles(max privileged entitlements per identity)require_expiration_for_emergency(if true, flags emergency assignments withoutexpires_at)
providers/mock: deterministic sample data useful for local testingproviders/entra: Entra ID / Microsoft Graph adapterproviders/okta: Okta adapter (Core API + best-effort System Log)providers/google_workspace: Google Workspace adapter (Directory API)
Provider contract: providers/base.py.
python -m unittest discover -s testspython -m compileall .- Activity collection is best-effort and depends on tenant permissions; drift detections that rely on activity may be lower confidence when activity is unavailable.
- Assignments are currently treated as set membership by
(identity_id, entitlement_id, assignment_type)for diffing. - The engine currently persists to local JSON/JSONL; pluggable storage can be added later behind the same interface.