Metadata-Version: 2.4
Name: pyfsr
Version: 0.7.3
Summary: Python implementation of the FortiSOAR REST API
Project-URL: Homepage, https://github.com/ftnt-dspille/pyfsr
Project-URL: Bug Tracker, https://github.com/ftnt-dspille/pyfsr/issues
Project-URL: Documentation, https://ftnt-dspille.github.io/pyfsr/
Author-email: Dylan Spille <dspille@fortinet.com>
License: MIT
License-File: LICENSE
Keywords: api,fortinet,fortisoar,rest
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Topic :: Software Development :: Libraries :: Python Modules
Requires-Python: >=3.10
Requires-Dist: pydantic>=2
Requires-Dist: requests>=2.31.0
Provides-Extra: docs
Requires-Dist: furo>=2024.8.6; extra == 'docs'
Requires-Dist: myst-parser>=4.0.0; extra == 'docs'
Requires-Dist: sphinx-autoapi>=3.4.0; extra == 'docs'
Requires-Dist: sphinx-autodoc-typehints>=3.0.0; extra == 'docs'
Requires-Dist: sphinx-copybutton>=0.5.2; extra == 'docs'
Requires-Dist: sphinx-design>=0.6.1; extra == 'docs'
Requires-Dist: sphinx>=8.1.3; extra == 'docs'
Provides-Extra: mcp
Requires-Dist: mcp>=1.0; extra == 'mcp'
Provides-Extra: playbooks
Requires-Dist: fsr-playbooks>=0.3.68; extra == 'playbooks'
Provides-Extra: test
Requires-Dist: coverage>=7.6.9; extra == 'test'
Requires-Dist: mcp>=1.0; extra == 'test'
Requires-Dist: pytest; extra == 'test'
Requires-Dist: pytest-cov; extra == 'test'
Requires-Dist: pytest-mock; extra == 'test'
Provides-Extra: typecheck
Requires-Dist: mypy>=1.13; extra == 'typecheck'
Description-Content-Type: text/markdown

# pyfsr

[![PyPI version](https://badge.fury.io/py/pyfsr.svg)](https://badge.fury.io/py/pyfsr)
[![Tests](https://github.com/ftnt-dspille/pyfsr/actions/workflows/pr-tests.yml/badge.svg)](https://github.com/ftnt-dspille/pyfsr/actions/workflows/pr-tests.yml)
[![Documentation Status](https://github.com/ftnt-dspille/pyfsr/actions/workflows/docs.yml/badge.svg)](https://github.com/ftnt-dspille/pyfsr/actions/workflows/docs.yml)
[![codecov](https://codecov.io/gh/ftnt-dspille/pyfsr/branch/main/graph/badge.svg)](https://codecov.io/gh/ftnt-dspille/pyfsr)

[Documentation](https://ftnt-dspille.github.io/pyfsr/) · [Installation](#installation) · [Quick start](#quick-start) · [CLI](#command-line-tools) · [AI / agents](#ai--agent-friendly)

**pyfsr** is a batteries-included Python client for the FortiSOAR REST API. It
gives you a typed query/CRUD layer over any module, picklist resolution,
connector execution, playbook-run history, safe deletes — and a ready-made
**AI/agent surface** (tool-schema registry + an optional MCP server) so an agent
can drive FortiSOAR with no glue code.

There's also a `pyfsr` CLI for the things you reach for outside a script:
poking at an appliance's health and services, and authoring playbooks in YAML
and pushing them to a live instance.

Python 3.10+ · Pydantic v2 · MIT.

## Installation

```bash
pip install pyfsr
# with the optional generic MCP server:
pip install 'pyfsr[mcp]'
```

## Quick start

```python
from pyfsr import FortiSOAR, Query

# API-key auth, or ("username", "password")
client = FortiSOAR("soar.example.com", "your-api-key")

# Generic, typed CRUD for ANY module via client.records(module)
incidents = client.records("incidents")

inc = incidents.get("0d2c...")          # by uuid, "module:uuid", or full IRI
inc["name"], inc.uuid                    # records are dict- AND attribute-accessible

# Structured queries with a fluent builder -> a HydraPage you can iterate
page = incidents.query(
    Query().eq("status.itemValue", "Open").like("name", "phish").limit(50)
)
for inc in incidents.iterate(Query().eq("status.itemValue", "Open")):
    ...                                  # lazily walks every page

# Create / update / delete (soft by default; hard= for permanent)
new = incidents.create({"name": "Suspicious login", "severity": "High"},
                       resolve_picklists=True)   # friendly values -> IRIs
incidents.update(new.uuid, {"status": "Closed"}, resolve_picklists=True)
incidents.delete(new.uuid)               # delete(..., hard=True) to purge
```

### Configure from the environment

```python
from pyfsr import EnvConfig

# reads FSR_BASE_URL (+ FSR_API_KEY or FSR_USERNAME/FSR_PASSWORD),
# FSR_PORT, FSR_VERIFY_SSL, FSR_TIMEOUT
client = EnvConfig.from_env().client()
```

## Features

- **Generic record access** — `client.records(module)` for CRUD on any module;
  no hand-built `/api/3/...` URLs or Hydra unwrapping.
- **Query DSL** — `Query().eq(...).in_(...).group(...).sort(...).limit(...)`,
  compiled to the FortiSOAR query-body shape (pagination handled for you).
- **Typed models** — Alert/Incident/Task/Comment come back as Pydantic v2
  models that are also dict-compatible; unknown modules fall back to a lenient
  `BaseRecord`, so custom fields/modules never break.
- **Picklists** — `client.picklists` resolves friendly values (`"High"`) to
  IRIs and discovers which picklist a `(module, field)` binds to.
- **Connectors** — `client.connectors` lists configured connectors, runs
  healthchecks, and executes operations.
- **Playbooks** — `client.playbooks` merges live + historical run history and
  resumes manual-input steps.
- **Safe deletes** — soft-delete/restore + guarded single-row hard delete.
- **Schema discovery** — `client.list_modules()` / `client.describe_module()`.
- **Resilient transport** — configurable `timeout=`, automatic retry with
  backoff on idempotent requests (429/5xx), and secrets masked in verbose logs.
- **Bundled OpenAPI spec** — `pyfsr.spec.load_spec()` for offline reference and
  `drift(client)` to compare the spec against a live appliance.

## AI / agent-friendly

pyfsr ships a transport-neutral **tool registry** for the core operations, with
token-efficient results and structured (never-raised) errors — feed it to
Anthropic tool-use, OpenAI function calling, your own agent loop, or the bundled
MCP server.

```python
from pyfsr.tools import to_anthropic_tools, to_openai_tools, dispatch

tools = to_anthropic_tools()             # or to_openai_tools(), or tool_schemas()

# ... your model picks a tool ...
result = dispatch(client, "search_records",
                  {"module": "alerts", "summary": True, "limit": 10})
# result is JSON-safe and trimmed; failures come back as {"error": {...}}
```

Reads accept `summary=True` or `fields=[...]` to keep payloads small:

```python
client.records("alerts").query(Query().limit(20), summary=True)
```

### Generic MCP server

Point any MCP-capable agent at any FortiSOAR with one command:

```bash
pip install 'pyfsr[mcp]'
FSR_BASE_URL=soar.example.com FSR_API_KEY=... python -m pyfsr.mcp
```

It exposes the same registry (record CRUD, schema discovery, picklists,
connectors, playbook runs) as MCP tools — generic and dependency-light,
distinct from any domain-specific FortiSOAR MCP.

## Command-line tools

Installing pyfsr puts a `pyfsr` command on your path with two groups.

**`pyfsr appliance`** — operational verbs against a FortiSOAR box (most run over
SSH/sudo and stay dependency-light on the far end):

```bash
pyfsr appliance info                 # host, version, content DB, device UUID
pyfsr appliance host                 # mem / swap / load / per-service RSS / disk
pyfsr appliance service restart cyops-postman --yes
pyfsr appliance db                   # Postgres verbs, multi-DB aware
pyfsr appliance es                   # Elasticsearch health + shard state
pyfsr appliance license              # licensing / identity, drift check
```

Other groups: `mq` (RabbitMQ), `ha`, `certs`, `logs`, and `diagnose` (runs
`fsr_diagnose.sh`). `--help` on any of them lists the verbs.

**`pyfsr playbook`** — author playbooks as YAML and deploy them:

```bash
pyfsr playbook validate flow.yaml    # compile + report diagnostics (offline)
pyfsr playbook compile flow.yaml     # emit the FSR import envelope (offline)
pyfsr playbook deploy flow.yaml      # compile and create it on the appliance
pyfsr playbook check-fresh           # compare the cached catalog vs a live SOAR
```

## Development

```bash
uv sync --extra dev
uv run pytest -q                 # unit tests (live tests deselected by default)
uvx ruff check src tests
```

Live integration tests run with `pytest -m integration` and need an
`examples/config.toml` pointing at a FortiSOAR instance.

## License

MIT — see [LICENSE](LICENSE).
