Metadata-Version: 2.5
Name: msad
Version: 0.12.0
Summary: msad is a commandline for interacting with Active Directory
Project-URL: Homepage, https://github.com/matteoredaelli/msad
Project-URL: Issues, https://github.com/matteoredaelli/msad/issues
Author-email: Matteo Redaelli <matteo.redaelli@gmail.com>
License-Expression: GPL-3.0-or-later
License-File: LICENSE
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Requires-Python: >=3.11
Requires-Dist: cryptography==50.0.1
Requires-Dist: ldap3==2.9.1
Requires-Dist: pydantic-settings>=2.5
Requires-Dist: pydantic>=2.9
Requires-Dist: typer==0.27.2
Provides-Extra: kerberos
Requires-Dist: gssapi==1.12.0; extra == 'kerberos'
Description-Content-Type: text/markdown

# msAD

![The msad ecosystem: msad at the core, with mcpAD (MCP), restAD (REST) and graphqlAD (GraphQL) around it](docs/ecosystem.svg)

*msad* is a library and command line tool for working with an Active Directory / LDAP
server from Unix, Linux and macOS systems.

It supports authentication with user/password and Kerberos, and pagination (it can
retrieve more than 2000 objects, a limit of AD).

Features:

- [X] search objects (users, groups, computers, ...) with a raw LDAP filter
- [X] search users by field (name, surname, mail, sAMAccountName, department)
- [X] search groups by name / cn / sAMAccountName / displayName
- [X] search computers by name / dNSHostName / operatingSystem
- [X] search organizational units (OUs) and list their contents
- [X] find inactive users / stale computers (last logon older than N days)
- [X] audit reads: domain info, default password policy, privileged groups
- [X] audit reads: FSMO role owners, domain trusts, AD sites
- [X] audit reads: GPOs, fine-grained password policies (PSOs)
- [X] list domain controllers, empty groups, and computers grouped by OS
- [X] find contacts, security groups (optionally by scope), and expired/never-expiring accounts
- [X] find risky accounts: password not required, Kerberos pre-auth not required, "sensitive and not delegated"
- [X] find users whose password violates the domain policy
- [X] test AD connectivity (health check)
- [X] get a single user or group
- [X] fetch any entry directly by its DN (resolve `manager` / `managedBy`)
- [X] list group members (direct or recursive/nested)
- [X] list a user's groups (direct or recursive/nested)
- [X] check whether a user is a (nested) member of a group
- [X] add/remove members to/from AD groups using DN or sAMAccountName
- [X] change AD passwords
- [X] unlock a locked-out user account (reset `lockoutTime` to 0)
- [X] check if a user is disabled, locked, or has an expired/never-expiring password
- [X] decode raw AD attributes (userAccountControl, groupType, FILETIME, SID, GUID)
- [X] LDAP filter escaping to prevent injection

By default, user lookups return a useful attribute set — identity fields plus
`manager` (the DN of the user's manager), `memberOf` (direct group DNs),
`userAccountControl`, `accountExpires`, `lastLogonTimestamp`, `whenCreated` and
all `extensionAttribute1`-`extensionAttribute15` (commonly used for custom/HR
data such as company code, employee type or division).
Group lookups include `managedBy` (owner DN), `member`, `groupType` and
`whenCreated`. Pass `--attributes` to override the defaults.

## Prerequisites

Python >= 3.11

For Kerberos authentication, install the optional `kerberos` extra
(`msad[kerberos]`), which pulls in the `gssapi` package. `gssapi` is a native
extension and needs system Kerberos headers to build:

- krb5 library and tools (like `kinit`, ...)
- krb5 dev headers: `libkrb5-dev` on Debian/Ubuntu, `krb5-devel` on
  RHEL/Fedora, or `krb5` via Homebrew on macOS
- a keytab file or `krb5.conf` configured

If you only authenticate with user/password, skip the `kerberos` extra
entirely \u2014 no native build, no system headers needed.

## Installation

### With uv (recommended)

Install as an isolated tool on your `PATH`:

```bash
uv tool install msad

# with Kerberos support:
uv tool install 'msad[kerberos]'
```

Run it directly without installing (ephemeral, great for one-off use):

```bash
uvx msad --help
# uvx is shorthand for: uv tool run msad
```

Upgrade or uninstall the installed tool:

```bash
uv tool upgrade msad
uv tool uninstall msad
```

### With pipx

```bash
pipx install msad

# with Kerberos support:
pipx install 'msad[kerberos]'
```

## Configuration

msad reads a TOML file, `$HOME/.msad.toml` by default (override with
`--config-file <path>`). The `init` command creates that file from a
ready-to-edit template (it will not overwrite an existing file unless you pass
`--force`):

```bash
msad init
# creates ~/.msad.toml (warns if it already exists)
# then edit ~/.msad.toml
```

The template looks like this (abridged):

```toml
[defaults]
# Domain used when --domain is not passed on the command line.
domain = "mydomain"

[domains.mydomain]
host = "dc.example.com"
base = "dc=example,dc=com"
port = 636          # 389 for plain LDAP
use_ssl = true      # false for plain LDAP

# Optional: abort an LDAP operation that gets no response within N seconds
# (ldap3 receive_timeout). Omit to keep ldap3's default (no timeout).
# query_timeout = 30

# Omit user/password to authenticate with Kerberos (SASL); get a ticket
# first with `kinit`. Set BOTH to authenticate with user/password:
# user = "svc_account"
# password = "..."
```

You can declare multiple domains under `[domains.<name>]` and select one at
runtime with `--domain <name>`.

### Multiple domains

Declare as many domains as you need; each can use its own authentication
(Kerberos or user/password). `[defaults].domain` is used when `--domain` is
not given:

```toml
[defaults]
domain = "corp"

# Kerberos (SASL): omit user/password, run `kinit` first.
[domains.corp]
host = "dc.corp.example.com"
base = "dc=corp,dc=example,dc=com"
port = 636
use_ssl = true

# A second domain using user/password authentication.
[domains.lab]
host = "dc.lab.example.com"
base = "dc=lab,dc=example,dc=com"
port = 636
use_ssl = true
user = "svc_reader"
password = "s3cr3t"
```

Then target a domain per command (defaults to `[defaults].domain` when omitted):

```bash
msad user-get matteo                 # uses the default domain (corp)
msad user-get jdoe --domain lab      # runs against the lab domain
msad test-connection --domain lab    # check connectivity to a specific domain
```

## Usage

```bash
msad --help
python -m msad --help
```

For Kerberos authentication, first obtain a ticket:

```bash
kinit            # or: kinit myaduser
```

Verify connectivity and authentication before running other commands:

```bash
# Prints an ok/target/auth/error status; exits non-zero on failure
msad test-connection
```

### Searching

```bash
# Raw LDAP filter (all attributes as JSON)
msad search "(samaccountname=matteo)" --out-format json

# Restrict returned attributes
msad search "(cn=redaelli*)" --attributes mail --attributes sAMAccountName --out-format json

# Find users by field (criteria are ANDed; values may contain *)
msad user-search --surname "Rossi" --department "IT"

# Get a single user (exact match on sAMAccountName / UPN / mail / cn)
msad user-get matteo

# Find groups (supports * wildcards)
msad group-search "qlik_*"

# Get a single group
msad group-get qlik_analyzer_users

# Find computers (name / dNSHostName / operatingSystem, values may contain *)
msad computer-search --name "PC0*" --os "Windows Server*"

# Exclude domain controllers from a computer search
msad computer-search --exclude-domain-controllers

# Get a single computer (sAMAccountName with or without trailing $, cn, or FQDN)
msad computer-get PC001

# Restrict any search to a specific OU subtree with --base
msad user-search --surname "Rossi" --base "OU=Sales,DC=group,DC=example,DC=com"

# Find organizational units (name matches the ou attribute; may contain *)
msad ou-search --name "Sales*"

# Get a single OU by its ou name or full DN
msad ou-get "OU=Sales,DC=group,DC=example,DC=com"

# List everything under an OU (optionally filter by objectClass)
msad ou-contents "OU=Sales,DC=group,DC=example,DC=com"
msad ou-contents "OU=Sales,DC=group,DC=example,DC=com" --object-class user

# Find users who haven't logged in for 90 days (default)
msad inactive-users

# Inactive users in a specific OU, last 30 days
msad inactive-users --days 30 --base "OU=Staff,DC=group,DC=example,DC=com"

# Find computers that haven't logged on in 180 days
msad stale-computers --days 180

# Include accounts that have never logged on (no lastLogonTimestamp)
msad inactive-users --days 90 --include-never
msad stale-computers --days 180 --include-never

# Fetch any entry by its DN (e.g. resolve a user's manager)
msad get-by-dn "CN=Anna Bianchi,OU=Staff,DC=group,DC=example,DC=com"

# Find contacts (cn / mail / displayName, values may contain *)
msad contact-search "anna*"

# Find security-enabled groups, optionally by scope
msad security-groups
msad security-groups --scope global

# Risk: accounts with no password required / no Kerberos pre-auth / not delegated
msad password-not-required
msad kerberos-preauth-not-required
msad not-delegated

# Accounts whose expiration is set to "never", or whose expiration date has passed
msad never-expiring-accounts
msad expired-accounts
```

### Groups and membership

```bash
# Direct members of a group
msad group-members qlik_analyzer_users

# All (nested) members
msad group-members qlik_analyzer_users --nested

# A user's groups (direct, or nested)
msad user-groups matteo
msad user-groups matteo --nested

# Is a user a (nested) member of a group? Prints true/false
msad is-member qlik_analyzer_users matteo

# Add / remove a member (by DN or sAMAccountName)
msad group-add-member qlik_analyzer_users matteo
msad group-remove-member qlik_analyzer_users matteo
```

### Auditing (read-only)

```bash
# Domain object: security-relevant settings and metadata
msad domain-info

# Default domain password policy (maxPwdAge, minPwdLength, lockout, ...)
msad password-policy

# Users whose password violates the policy (expired, or must change at logon)
msad password-policy-violations

# Well-known privileged groups with member counts
msad privileged-groups

# Include the actual members (optionally expand nested membership)
msad privileged-groups --with-members
msad privileged-groups --with-members --nested
```

### Account checks

```bash
# Individual checks (print true / false / not found)
msad is-disabled matteo
msad is-locked matteo
msad has-expired-password matteo --max-age 90
msad has-never-expires-password matteo

# Run all checks at once, optionally checking group memberships
msad check-user matteo --group qlik_analyzer_users --group domain_admins
```

### Passwords

```bash
# Change a user's password (prompts interactively)
msad change-password matteo
```

### Output formats

Most commands accept `--out-format`:

- `jsonl` (default): one JSON object per line (JSON Lines / NDJSON)
- `json`: a single JSON array
- `csv`: tab-separated, list values joined with `|`

## Projects using msad

`msad` ships type hints and a stable public API (`msad.__all__`), so it can be
embedded in other tools. Three front ends build on it:

- **[mcpAD](https://pypi.org/project/mcpad/)** — a [FastMCP](https://github.com/jlowin/fastmcp)
  server that exposes `msad` as **read-only MCP tools** (search users / groups /
  computers / OUs, memberships, account status, audit reads) for use by AI
  agents and LLM clients. It reuses the `msad` TOML configuration and opens a
  short-lived connection per tool call. Install with `pip install mcpad` or run
  with `uvx mcpad`.

- **[restAD](https://pypi.org/project/restad/)** — a **read-only REST
  interface** over `msad`, built with FastAPI + Pydantic. It exposes the same
  search and audit operations as HTTP `GET` endpoints (with OpenAPI docs at
  `/docs`), resolving the connection from the `msad` TOML file plus `MSAD_*`
  environment overrides. Install with `pip install restad` or run with
  `uvx restad`.

- **[graphqlAD](https://pypi.org/project/graphqlad/)** — a **read-only GraphQL
  interface** over `msad`, built with Strawberry + FastAPI. It exposes the same
  search and audit operations as a single GraphQL `Query` type (no mutations),
  with an in-browser GraphiQL IDE at `/graphql`, resolving the connection from
  the `msad` TOML file plus `MSAD_*` environment overrides. Install with
  `pip install graphqlad` or run with `uvx graphqlad`.

All three are read-only: no write operation of `msad` (add/remove member,
change password, unlock account) is reachable through them.

## Development

This project uses [uv](https://docs.astral.sh/uv/). A `Makefile` wraps the common
tasks:

```bash
make sync       # install/refresh the dev environment
make test       # run the test suite with coverage
make lint       # ruff check + format check + pyrefly (strict)
make format     # apply ruff autofixes and formatting
make build      # build wheel and sdist into dist/
make publish    # upload to the package index (needs credentials)
make clean      # remove build artifacts and caches
```

Equivalent raw commands:

```bash
uv sync --group dev
uv run pytest
uv run ruff check src/ tests/
uv run pyrefly check
uv build
```

The library ships type hints (`py.typed`) and its public API is declared in
`msad.__all__`, so it can be imported and type-checked by downstream projects.

## License

Copyright © 2021 - 2025 Matteo Redaelli

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
