---
name: automateyournetwork/fortianalyzer-ops
source: https://app.decimal.ai/s/automateyournetwork-fortianalyzer-ops@1/SKILL.md
source_sha256: 7bef791db1dd
---

# FortiAnalyzer Operations — the analyzer plane

## MCP Server

- **Server**: `fortinet-mcp` (NetClaw-authored, spec 080 / roadmap R3)
- **Command**: `$FORTINET_MCP_CMD`
- **Transport**: stdio · JSON-RPC over `/jsonrpc` (the same dialect FortiManager speaks)
- **Requires**: `FORTIANALYZER_HOST`, `FORTIANALYZER_API_TOKEN` (FortiAnalyzer 7.2.2+ for token auth)
- **Mode**: read-only

## The one rule that matters most here

> ### "No logs matched" is NOT "this rule is unused."

This skill exists to answer "is this rule dead?" — and that question is dangerously
easy to answer wrongly. An empty result can mean:

- nothing matched **in the window you queried** (retention is finite; history is not)
- the device never forwarded logs to this analyzer at all
- logging is disabled on the rule itself

Reporting any of those as "unused" would license someone to delete a live firewall
rule. So an empty result returns the explicit outcome **`no_logs_in_window`**, never
`ok` and never an error, with a message saying what it does and does not prove.

This is the same error class as spec 078's *"no advisories ≠ not vulnerable"* and
spec 079's *"no probes found ≠ outage"*, and it gets the same treatment: a separate,
named outcome that cannot be silently collapsed.

## Where this plane sits

| Question | Plane | Skill |
|---|---|---|
| "Has anything actually matched this rule?" | analyzer | **this skill** |
| "What policy is *intended*?" | manager | `fortimanager-ops` |
| "What is the box *running* right now?" | device | `fortigate-ops` |

The manager knows a rule **exists**. Only the analyzer knows whether anyone ever
**matched** it. A configured rule is not a used rule.

## Tools (4, all read-only)

| Tool | What it answers |
|---|---|
| `faz_query_logs` | Traffic logs matching a filter within a bounded window |
| `faz_fetch_more` | Next page, re-run at an offset |
| `faz_policy_activity` | Did anything match policy N in this window? |
| `faz_list_devices` | Which devices forward logs here — **check this first** |

## Time windows are mandatory and always stated

If you supply no window, the tools apply the **last 24 hours** and say so in
`scope.window_start` / `scope.window_end` and in `notes`. An unbounded log query
against a busy analyzer is slow, expensive, and produces a result nobody can
interpret because they do not know what period it covers.

Every response echoes the window **actually queried**, not the one requested.

## Pagination

`faz_fetch_more` re-runs the search at a new offset. It does **not** reuse
FortiAnalyzer's search task id (`tid`), because those are single-use and expire —
treating one as a durable cursor produces silent truncation, where you believe you
have all the results and you have some of them.

## Workflow: is this firewall rule dead?

1. **`faz_list_devices`** — confirm the device that owns the rule actually forwards
   logs here. If it does not, stop: an empty result would mean nothing.
2. `faz_policy_activity` with the policy id and an explicit, generous window.
3. Read the outcome:
   - `ok` with `sessions_matched > 0` → the rule is live. Do not remove it.
   - **`no_logs_in_window`** → nothing matched *in that window*. Not proof of disuse.
4. Before concluding a rule is unused, verify: log forwarding on, retention covers
   the period, logging enabled on the rule itself.
5. Only then treat it as a removal candidate — and route the actual change through
   `fortimanager-ops`, which enforces the two write gates.

## Workflow: what hit this policy?

1. `faz_query_logs` with `filter_expr` (e.g. `policyid=12`) and a bounded window.
2. Page with `faz_fetch_more` using the returned `next_offset` while `has_more`.
3. Correlate sources and destinations against `fmg_resolve_object` to check whether
   the traffic matches what the rule *intended* to permit.

## Every response carries its plane and scope

```jsonc
{ "plane": "analyzer",
  "scope": {"adom": "root", "window_start": "...", "window_end": "..."},
  "outcome": "no_logs_in_window", "message": "... This is NOT evidence the rule is unused." }
```

The window is part of the scope, not an optional detail. A log result without its
window is uninterpretable, so a response that cannot state one is an error.

## Integration with other skills

| Skill | How they compose |
|---|---|
| `fortimanager-ops` | Find the rule (intent), then come here for whether it was used |
| `fortigate-ops` | Device state; use together to separate "not configured" from "not used" |
| `fwrule-analyzer` | Shadowed rules found there + zero activity here = strong removal case |
| `servicenow-change-workflow` | Rule removal is a production change — CR required |
| `gait-session-tracking` | Every query here is GAIT-audited automatically |

## Important rules

- **Never report an empty window as "unused".**
- **Always state the window** you queried, including the default.
- **Check `faz_list_devices` before trusting silence.**
- **Read-only** — findings here justify a change; they never make one.