From 8b4a92e5f23d31ef0bb8dd7a86f634ce8c3b147e19580eb7b709a27e72660d1a Mon Sep 17 00:00:00 2001 From: larssand Date: Thu, 18 Jun 2026 21:32:52 +0200 Subject: [PATCH] update --- README.md | 23 +++++++++++++- src/fgai/__pycache__/cli.cpython-312.pyc | Bin 6480 -> 8197 bytes .../__pycache__/mitigation.cpython-312.pyc | Bin 6483 -> 8672 bytes src/fgai/cli.py | 30 ++++++++++++++++++ src/fgai/mitigation.py | 30 ++++++++++++++++++ 5 files changed, 82 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 7c236bb..c75cf46 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,16 @@ Analyze local logs: 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: ```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`. +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 - `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. - Blocking requires `--execute`. - The FortiGate API call is limited to the quarantine/banned user monitor endpoint. - diff --git a/src/fgai/__pycache__/cli.cpython-312.pyc b/src/fgai/__pycache__/cli.cpython-312.pyc index 74b1f41a224e03e4a1e21f7115ced1fb454f519a43ef13f39461f3b8983481d1..e1232bf2999409f6ed7d11d8028f4ed8d4001e8f517b67b0e5558e257003737b 100644 GIT binary patch delta 2695 zcmaJ@Z*1Gf6(=c*q9jr>ZA-Q+XQrLlGM#wCf}lvZu3Oi}@!ZBq=3oibu9_m!Hf2(D zkEhjE704^PwM7fmDbYVeQ#1pH0Yw5dgTMAeKd&7o!2)Ek4p5-LfPE-Xv>7(+%l7VA z*{PfE0etuF-Q)dv?|1KA^Z0LnlK4=TV+_!-(BHt-M9(O5rTtW+Fa#HLem=b_J^E8^up{Fi0leU-Zy9t5Mr))U+UHyZ)b z=PhT+MR5?(Fky!XBThlzA&i(Kk+2cMRKiAq`60`)Q6K3F-oN-C4h|5dRIC)O^_o$j zlO6+A@77#+mwn_2G30=0Q6v#r2IdPTr?O}+(M5c*P|(d1E)>uNF$kt6QVy6FLX%!O zGIEG6qZF&Ui3_WaWtP?pi>76u zI9$Y7GfGu#ps05|{=FV>K7%yErU*OgJr$oCp*u;q)ukoF#f8h3Q(AG+129gu;(p<@jjBqynm3g{yOAa+(9AvvuwU zaVgN15RL{C!{}TW7A%9w*(udS``Y!8#?Si^WVlreyS40$(?HlfUBrg^(7B(f5R{5h zBJfeqJ{kl~KxBG!A245b*IjJb1s5Pzb^XwifrDTXsAxQ*&bpsi3V{iF5IQb7wF-o& z-;$U(ZEvMU1^pDp=ZGN@Hke&`xFe6Z2q zvH+7DAXMJ>eH|>kcf_G5u7v8LGEFUCw=NUhA0VC8*_(TzWEuD~dB6m^Sn29KCEDMp zb9xAV<8Q!`2&uG}cUrlCy%yeozmvTDXcAr%!4P7Dg*2M>i`=HQ^$106PIxq`RP^ESku8x@cL! zRW%LQ7n~|ytztyr;0wV@e%M~oP2|U(MkY22*jU4;pR7nuac^uje39OMm=eu`)d6@V z77bil-dh!&0h0w{_A;<dLk<_JXh@ zCSH+WlsjU!C1$Tba_hugQ_Qx-lc4X2nUooeCfdI_+ZGQ|oO*wjGTiI@)<@EZA@4`Ybnb{gelyaI z<&|(fd@s=8e(%Y|QDs8U-DNA(1#eYOJv6Cn`&s~$aL*ai;0sDOGy5p0gP*i1?^@4$ z!Z{d3=e_faD_jV~zbLcp`K`WQg>BU64W8b+!Ij$K3vi{?8K;{{etv4rDbT1d(2sz7N@5JB-PZ51U2g1!r6HhQraq1 zJwwF~{As!{v1MqIH_>;gL8=ig6EsQFd_!@eMJNtk0|4k&F9Re&bc53I82sZ4L2mbB z`-k(jo!vt=dQQ6^f-L^F4?`D4AQ zOl&L0#@r5{=KhaaPxDijfv`%9qrqo>wN#wyA4 zsL%T>otZ3xmZon%WUQ4ATq)izLUnY&Em%1qhG3Zb>68MXy5RVA1f3VtGJ2`sQ4s_p*&&q-mic}(hy#@%*!-Y z1aAV8eS^Tz>Q0^|e#|zpxkO((LFOS@w-FS562ATCM(~#THW*MR(esg)NTq1QzWmq% zt#zl?O#CiGO9~D1f5ZoDN3UAOgQx~ekQC}J1N%J0vg~bU?9(`t{FsS<%*1XpgC3U| a?-5w}%ib8Up~GzNoyS=3SVrYZiTWQvv2^7C delta 1061 zcmZ`&OKa3n6wXa1nVDoVowW08r!%#!ZPSep1i?qGsI-C>QSgz`hUp~M*kn>}W}ps& z3Uwt2<~)(g;-q45vwiQG^;o9Uv53*P|MmA{!A8mka33G}}%0R?jx0qO;)A`fc$eIXRAj zJb<323hEhvP-^+@>&|L)NtiNC%V~8?lTKnlTpG3A0J=b@*u!L<6xh>bN!$yrnf~kK zGoDmfPu^a@R_Fx6e>;f^{%Imi-1MDx+)C2kuv^`ZO;hZmvL`o%Wu_5eyd2fpwlWL% zzEbkyA#m(t@0DMq#Fn%Q|5ji-neqMu?Xe(o7dU;A3B_m9QB0d-&x?U{JXZIW7Rg+{ zSJDWt%j{s~vW2su*apR#x)@69O&ZuVl!NX{tOij-VQSrXot9$3y*74xY{_ri^lxA#luh$`x6`FI5J12J3kZVPb48U}5!M!mYfgyA^SpQ$6BZ*^UJ qj@n=n*nt7CBfxL;i5vgI4Se8~51h=N)OU+jGWdOteXfrsu)uHczw7M) diff --git a/src/fgai/__pycache__/mitigation.cpython-312.pyc b/src/fgai/__pycache__/mitigation.cpython-312.pyc index 83077f127955cf25398b96e4809102e755902cb279c1a83f1765e9ac1142d6cc..0aae95ee5af751c8cabaa301a53ca6548c9a32db1ecd06d01d82da9768526db6 100644 GIT binary patch delta 2897 zcma)8Yiv}<6~41~@4nv;@7l(A{Q{O6Zg5O-45k6wP`AORfdFwtSIf>d?6SMpnR^%G z?QV<`w}Cb(aVAk{p@j4mkZGIBQWLeR)P^8c>K`Mkg>bI zwcngMGv~~iGv}N;OFf?q`u^(mx&&z5yL)q~$eb@u?$52Am=UNw?=J+#14N(>>dXfV zq47{5JRUa3u6(2r9gh|o#v5SVKx5QBD~-o!oHkM~P;u&`e(Irt3!+na^hcX$aMm{7 zXb}`U4P6b-6Wm`AEW&PqMrH*Xowr14tod3ej5keNlFd9q#=J>lI7=+A6f-JS7Bb}b zq;aEsOf4vKKFu^OU8M5StS(O+-7zBb)#5rS0Xl*U-xTwNwDS+FourlDu|7dMcuZPP z8u?CXz_$*TXd*x&@g!^IuS%<=$6&OI|62MXkrxJRf48jd0!ETBH+B}b20BAhPAElP zvjCk)uvX|;lJ`3wBYph1^E;%A|J#{>-5jnbqJuE0p`T9)?~S}a_S>;j0&C+3TkP7hIx)2H{$* z<4)*3(7UFc$FRN%Xv%0-U?iY{zu)(*B~S5>eTPWfLZAPhg{IG#9Kne!hDqI55il3aXr+}3qP?G3AI#3DpvTM zH`;Vws1W#zFk`J+b*Zj0XXA7Jh=W?GR1tCx{;yy|KebW&1eI z3l)lC9fQeiIUjGcH2ZB%;UWD2&>xOU)LF4zb=5U)7OHkV%(H>jmRt;kn)paC>UP)7 zpC^1Na8Pno9G}m&x~b=?cOH`gTGgpzKA4UAU|j+%k2Fz##fc}ZE2=nN1RJZ47aV*( z*yVRTBowGP_{YJvnUxh&L=V+f<}m*rX`KQKRv*?7{PHeSWJ)+GJ}XR-WN37me2oP` z2|Es8h`WdPvmy>v07&skq@SOu4Ky6+2qlCQ2zi8Qgfc<}p^ESVK)GX=%Jl4RNXx{I zy(0;x=p|NEXd+uYkcnP%yNW=PMM6gJJy zqSze5NrczU{W+3v0GJgFleX5(l4m%MDtaoXsl{@9<;a8iEL6sg^%-=fb`bL5l<<$Q zYA2ptHUmCjf)yrL%V~f5`r^Va8UD31Z z8TFROe=8jSq+|W%-rsD$vVC!V|6<$LTTR`cN@CP^-){H2PK$Q~LUZfe@{i@Y&a2u@ zdCv`b&yqZL+J4vKbdK>vG}<#r{&ehitog0-T=~+vcQ?GV;b-#A_P!hKeILY@+IK9) zc77%j?;!b1K>QCP&qTGE+y3xPf5#1f2e6!DvyAiN4-h}oiUCR2^~ zi@in)R+q2g=zTa zJ5s44*BUmB;Fr~KXYy%HD=7Lgm9h!ous0F#7THe`evZ(NfR3*{LiQttunYd$ll-3z z8~X2gnx#YJet%RNBlk8EY5RQ}k!rVPE@kFXtgIPC$}TW_#cIAe))~Sbk~X#xx|$DU zemu6uYZ`tC=UnV5elZq*+I*_~5|{NNm=r!**sl<9eL0fPYWj0K^J?b!VEyTGIN5Bt zQmKg&R7oY3Vi+_GBcDaMBffhE3m;TN@Q4Q6 X)21Op2JQ-o_e8-G|I~t{>Hhx#@Jzk4 delta 1218 zcmZ8hO>7%Q6y90yuI=@2Y$tYpus1Zt-KYtrtx8k1C~An>(t;zJwviDS$1@2|*52^O zZn25fO0mR&fK<&1wL;v}1IUL;Js@#GNZddIX)i^b;DSKq0w?C}f{?na{ob4RGw)~K z>`zPIA5VUpNSKOzwii!s{gu3%ERbLBmN#yPX~apj($#cpq&h+r8m3Vv)5=z}tz0!H z*Q0cdnghK$O7pZp$T+2_4Z(Qk@>|h8#ks#jn~7DT<`NK%NyB^;uF7 zTtAmAOA{WFU|EY$6kqC7`tx!%DIV(|llfuW_(3htNP$fNE4NiREQzPHo2=d8n1*FR zQ4%;Jwxh?$i{gIteKI2orX`~-m&4=Z0(?kHf156szg4BarAzcu8= zSLW%>xLn&Y&j2_5UL)eLGgvSETgta4)t+$SdNv_{m9=h6h^ERM}HQ9#CYURl~ig9UtCEyhUEU zCOzOf!kPqIgRCT8n6+;WbA@&q%9V*+b--xR)&(K@H-Y>{hZVgW7RV zYTS&=N*>KK%ptWFABS diff --git a/src/fgai/cli.py b/src/fgai/cli.py index 35e70b8..1be3611 100644 --- a/src/fgai/cli.py +++ b/src/fgai/cli.py @@ -3,6 +3,7 @@ from __future__ import annotations import argparse import json import sys +from pathlib import Path from .llm import ollama_summary from .logs import read_events, summarize_events @@ -70,6 +71,28 @@ def suggest_blocks(args: argparse.Namespace) -> int: 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: parser = argparse.ArgumentParser(description="Local FortiGate AI/ML inspection tool") 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.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 diff --git a/src/fgai/mitigation.py b/src/fgai/mitigation.py index 7713a6c..e70763a 100644 --- a/src/fgai/mitigation.py +++ b/src/fgai/mitigation.py @@ -8,6 +8,7 @@ from typing import Iterable from urllib import error, request import json import ssl +from urllib.parse import urlencode from .logs import event_score, is_utm_event from .models import BlockCandidate, LogEvent @@ -107,3 +108,32 @@ class FortiGateClient: 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 + + 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")