update
This commit is contained in:
23
README.md
23
README.md
@@ -18,6 +18,16 @@ Analyze local logs:
|
|||||||
fgai analyze-logs --logs logs/fg_syslog.jsonl
|
fgai analyze-logs --logs logs/fg_syslog.jsonl
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Test FortiGate API access:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export FORTIGATE_HOST=192.0.2.10
|
||||||
|
export FORTIGATE_API_TOKEN='...'
|
||||||
|
export FORTIGATE_VERIFY_TLS=false
|
||||||
|
fgai test-connection
|
||||||
|
fgai fetch-policies --output exports/policies.json
|
||||||
|
```
|
||||||
|
|
||||||
Audit a FortiGate policy export:
|
Audit a FortiGate policy export:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -51,6 +61,18 @@ For logs, configure FortiGate syslog to write into a local file such as `logs/fg
|
|||||||
|
|
||||||
For policies, export a FortiOS config backup and pass it to `audit-policies`.
|
For policies, export a FortiOS config backup and pass it to `audit-policies`.
|
||||||
|
|
||||||
|
Example FortiGate syslog target, run on the FortiGate CLI and replace the server IP with this machine:
|
||||||
|
|
||||||
|
```text
|
||||||
|
config log syslogd setting
|
||||||
|
set status enable
|
||||||
|
set server "192.0.2.50"
|
||||||
|
set port 514
|
||||||
|
set mode udp
|
||||||
|
set format default
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
## Environment
|
## Environment
|
||||||
|
|
||||||
- `FORTIGATE_HOST`: firewall hostname or IP.
|
- `FORTIGATE_HOST`: firewall hostname or IP.
|
||||||
@@ -68,4 +90,3 @@ The agent separates detection from enforcement:
|
|||||||
- Source IPs must be globally routable and outside the allowlist.
|
- Source IPs must be globally routable and outside the allowlist.
|
||||||
- Blocking requires `--execute`.
|
- Blocking requires `--execute`.
|
||||||
- The FortiGate API call is limited to the quarantine/banned user monitor endpoint.
|
- The FortiGate API call is limited to the quarantine/banned user monitor endpoint.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import argparse
|
import argparse
|
||||||
import json
|
import json
|
||||||
import sys
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
from .llm import ollama_summary
|
from .llm import ollama_summary
|
||||||
from .logs import read_events, summarize_events
|
from .logs import read_events, summarize_events
|
||||||
@@ -70,6 +71,28 @@ def suggest_blocks(args: argparse.Namespace) -> int:
|
|||||||
return 0
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_connection(args: argparse.Namespace) -> int:
|
||||||
|
client = FortiGateClient.from_env()
|
||||||
|
status = client.system_status()
|
||||||
|
results = status.get("results", status)
|
||||||
|
print("FortiGate API connection OK")
|
||||||
|
_print_json(results)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_policies(args: argparse.Namespace) -> int:
|
||||||
|
client = FortiGateClient.from_env()
|
||||||
|
policies = client.firewall_policies()
|
||||||
|
if args.output:
|
||||||
|
output = Path(args.output)
|
||||||
|
output.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
output.write_text(json.dumps(policies, indent=2, sort_keys=True), encoding="utf-8")
|
||||||
|
print(f"Wrote {output}")
|
||||||
|
else:
|
||||||
|
_print_json(policies)
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
def build_parser() -> argparse.ArgumentParser:
|
def build_parser() -> argparse.ArgumentParser:
|
||||||
parser = argparse.ArgumentParser(description="Local FortiGate AI/ML inspection tool")
|
parser = argparse.ArgumentParser(description="Local FortiGate AI/ML inspection tool")
|
||||||
subparsers = parser.add_subparsers(required=True)
|
subparsers = parser.add_subparsers(required=True)
|
||||||
@@ -98,6 +121,13 @@ def build_parser() -> argparse.ArgumentParser:
|
|||||||
blocks.add_argument("--expiry-minutes", type=int, default=60)
|
blocks.add_argument("--expiry-minutes", type=int, default=60)
|
||||||
blocks.set_defaults(func=suggest_blocks)
|
blocks.set_defaults(func=suggest_blocks)
|
||||||
|
|
||||||
|
connection = subparsers.add_parser("test-connection", help="Test FortiGate REST API credentials")
|
||||||
|
connection.set_defaults(func=test_connection)
|
||||||
|
|
||||||
|
fetch = subparsers.add_parser("fetch-policies", help="Fetch firewall policies through the FortiGate REST API")
|
||||||
|
fetch.add_argument("--output", help="Write JSON response to this file")
|
||||||
|
fetch.set_defaults(func=fetch_policies)
|
||||||
|
|
||||||
return parser
|
return parser
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ from typing import Iterable
|
|||||||
from urllib import error, request
|
from urllib import error, request
|
||||||
import json
|
import json
|
||||||
import ssl
|
import ssl
|
||||||
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from .logs import event_score, is_utm_event
|
from .logs import event_score, is_utm_event
|
||||||
from .models import BlockCandidate, LogEvent
|
from .models import BlockCandidate, LogEvent
|
||||||
@@ -107,3 +108,32 @@ class FortiGateClient:
|
|||||||
raise RuntimeError(f"FortiGate API returned HTTP {exc.code}: {detail}") from exc
|
raise RuntimeError(f"FortiGate API returned HTTP {exc.code}: {detail}") from exc
|
||||||
except error.URLError as exc:
|
except error.URLError as exc:
|
||||||
raise RuntimeError(f"Could not reach FortiGate API: {exc.reason}") from exc
|
raise RuntimeError(f"Could not reach FortiGate API: {exc.reason}") from exc
|
||||||
|
|
||||||
|
def get_json(self, path: str, params: dict[str, str] | None = None) -> dict[str, object]:
|
||||||
|
query = f"?{urlencode(params)}" if params else ""
|
||||||
|
url = f"https://{self.host}{path}{query}"
|
||||||
|
req = request.Request(
|
||||||
|
url,
|
||||||
|
method="GET",
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {self.api_token}",
|
||||||
|
"Accept": "application/json",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
context = None if self.verify_tls else ssl._create_unverified_context()
|
||||||
|
try:
|
||||||
|
with request.urlopen(req, timeout=15, context=context) as response:
|
||||||
|
return json.loads(response.read().decode("utf-8"))
|
||||||
|
except error.HTTPError as exc:
|
||||||
|
detail = exc.read().decode("utf-8", errors="replace")
|
||||||
|
raise RuntimeError(f"FortiGate API returned HTTP {exc.code}: {detail}") from exc
|
||||||
|
except error.URLError as exc:
|
||||||
|
raise RuntimeError(f"Could not reach FortiGate API: {exc.reason}") from exc
|
||||||
|
except json.JSONDecodeError as exc:
|
||||||
|
raise RuntimeError("FortiGate API returned invalid JSON") from exc
|
||||||
|
|
||||||
|
def system_status(self) -> dict[str, object]:
|
||||||
|
return self.get_json("/api/v2/monitor/system/status")
|
||||||
|
|
||||||
|
def firewall_policies(self) -> dict[str, object]:
|
||||||
|
return self.get_json("/api/v2/cmdb/firewall/policy")
|
||||||
|
|||||||
Reference in New Issue
Block a user