Skip to content

Add Fortinet FortiGate active response integration - #83

Open
lakecide wants to merge 4 commits into
wazuh:mainfrom
lakecide:fortigate-active-response
Open

Add Fortinet FortiGate active response integration#83
lakecide wants to merge 4 commits into
wazuh:mainfrom
lakecide:fortigate-active-response

Conversation

@lakecide

Copy link
Copy Markdown

Summary

Adds a Fortinet FortiGate active response integration for Wazuh.

The integration automatically blocks malicious source IPs on a FortiGate firewall when matching Wazuh rules trigger. The script dynamically creates firewall address objects and safely appends them to a preconfigured deny group through the FortiGate REST API.

Features

  • Dynamic IP blocking
  • Automatic unblock support using timeout/delete
  • Safe address-group append logic
  • Duplicate handling using Wazuh check_keys
  • Whitelist support
  • Debug logging
  • Manual test script included

Tested Environment

  • FortiOS 7.4.11
  • Wazuh Manager 4.14
  • Ubuntu 22.04

Notes

The integration intentionally uses the FortiGate address-group member append endpoint instead of PUT operations on the group object to avoid overwriting existing members.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a new Wazuh Active Response integration that blocks/unblocks source IPv4 addresses on a Fortinet FortiGate firewall by creating a /32 address object and appending/removing it from a configured deny address group via the FortiGate REST API.

Changes:

  • Added fortigate-block.sh active response script implementing Wazuh check_keys handshake + FortiGate CMDB API calls.
  • Added an example configuration file and an integration-local .gitignore to prevent committing live secrets.
  • Added integration documentation and a manual test harness script.

Reviewed changes

Copilot reviewed 5 out of 7 changed files in this pull request and generated 9 comments.

Show a summary per file
File Description
integrations/fortinet_fortigate-active-response/active-response/fortigate-block.sh Implements FortiGate REST API block/unblock logic and Wazuh execd handshake.
integrations/fortinet_fortigate-active-response/active-response/fortigate-ar.conf.example Provides example configuration for FortiGate host/token/group/vdom and tuning settings.
integrations/fortinet_fortigate-active-response/tests/test-ar.sh Adds a manual test harness that feeds JSON to the AR script.
integrations/fortinet_fortigate-active-response/README.md Documents prerequisites, installation, configuration, testing, and security considerations.
integrations/fortinet_fortigate-active-response/.gitignore Prevents committing live config/token and other local artifacts.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

sudo chmod 750 /var/ossec/active-response/bin/fortigate-block.sh

# 2. Config
sudo cp integrations/fortinet_fortigate-active-response/active-response/fortigate-ar.conf \
Comment on lines +105 to +108
sudo cp integrations/fortinet_fortigate-active-response/active-response/fortigate-ar-whitelist.example \
/var/ossec/etc/lists/fortigate-ar-whitelist
sudo chown root:wazuh /var/ossec/etc/lists/fortigate-ar-whitelist
sudo chmod 640 /var/ossec/etc/lists/fortigate-ar-whitelist
```

All available options are documented in
[`active-response/fortigate-ar.conf`](active-response/fortigate-ar.conf).
</active-response>
```

`<location>server</location>` is required — the script runs on the Manager because it needs to reach the FortiGate API. See [`active-response/ossec-fortigate-ar.conf`](active-response/ossec-fortigate-ar.conf) for more trigger examples.
Comment on lines +165 to +170
# Dry-run — prints the JSON without calling the API
sudo bash integrations/fortinet_fortigate/tests/test-ar.sh block 198.51.100.99 dry

# Live block test
sudo bash integrations/fortinet_fortigate/tests/test-ar.sh block 198.51.100.99

Comment on lines +188 to +194
integrations/
└── fortinet_fortigate/
├── active-response/
│ ├── fortigate-block.sh - AR script - /var/ossec/active-response/bin/
│ ├── fortigate-ar.conf - config template - /var/ossec/etc/
│ ├── fortigate-ar-whitelist - whitelist template
│ └── ossec-fortigate-ar.conf - ossec.conf snippets
Comment on lines +92 to +94
printf '%s\n%s\n' "${ALERT_JSON}" "${CONTINUE_MSG}" \
| bash "${SCRIPT_PATH}" > /dev/null
EXIT_CODE=$?
Comment on lines +229 to +233
VDOM_PARAM="vdom=${FGT_VDOM}"

fgt_api() {
local method="$1" endpoint="$2" data="${3:-}"
local url="${BASE_URL}/${endpoint}?${VDOM_PARAM}"
Comment on lines +235 to +242
local cmd=(
curl --silent --max-time "${FGT_CURL_TIMEOUT}"
${CURL_SSL_FLAG}
-w "\n__STATUS__%{http_code}"
-X "${method}"
-H "Authorization: Bearer ${FGT_API_TOKEN}"
-H "Content-Type: application/json"
)
@MiguelCasaresRobles

Copy link
Copy Markdown
Member

@lakecide could you update based on copilot suggestions?

Regards,

lakecide added 2 commits June 8, 2026 17:42
- README: use fortigate-ar.conf.example in install step
- README: replace whitelist cp with install command
- README: fix test script paths to fortinet_fortigate-active-response
- README: fix structure block dir name
- test-ar.sh: wrap pipeline in if/else to capture exit code under set -e
- fortigate-block.sh: add validate_name_field() for URL-path config values
- fortigate-block.sh: add --show-error to curl for better failure diagnostics
- active-response: add ossec-fortigate-ar.conf with ossec.conf trigger examples
…ME references

- Rename ossec-fortigate-ar.conf - fortigate-ar.conf for consistency
- Update all README references accordingly
- modified .gitignore to only exclude live config, not .example file
@lakecide

lakecide commented Jun 9, 2026

Copy link
Copy Markdown
Author

@MiguelCasaresRobles All Copilot review comments have been addressed.

Thank you

@MiguelCasaresRobles

Copy link
Copy Markdown
Member

Hi @lakecide — thanks for this, it's well-structured and clearly security-aware (allowlist-validated URL params, IP strictly validated before use, no committed secrets, TLS-off documented as a warning). One blocker before it can merge:

Auto-unblock is broken (critical). In active-response/fortigate-block.sh, the check_keys/continue-abort handshake (~lines 683-702) runs unconditionally before the add/delete case switch. But Wazuh's execd only does that handshake on the initial add invocation (it binds stdin+stdout and reads the reply). The deferred delete invocation from the AR timeout is launched with stdin only — execd writes the alert and closes without ever reading/replying. So on every unblock, the script's read -r -t 30 EXECD_RESPONSE gets nothing, times out after 30s, hits the error/exit path, and never reaches group_remove_member/delete_addr_object. Net effect: blocked IPs are never cleaned up on the FortiGate — the opposite of the PR's headline "automatic unblock" feature. The fix is to run the handshake only inside the add branch (mirroring the upstream C AR scripts, which call it only on ADD_COMMAND).

Non-blocking nits:

  • README "Repository structure" lists active-response/fortigate-ar-whitelist as shipped, but it isn't in the PR (it's created during install) — the diagram is misleading.
  • Naming collision: the checked-in XML snippet fortigate-ar.conf shares the name of the runtime config path populated from .conf.example; combined with the .gitignore comment it could confuse users. Consider renaming the snippet.
  • is_whitelisted() arithmetic on a leading-zero octet (e.g. 172.08.x.x) hits bash octal parsing; use 10#$second or strip zeros.
  • fgt_api() only treats HTTP ≥500 as an error; a 4xx with a non-JSON body would log a generic failure without surfacing the code.

No CI runs on this repo, so please also paste a manual add/delete test against a real FortiGate once the handshake fix is in.

… handling

Critical: the check_keys/continue handshake ran unconditionally before the
add/delete switch. execd only performs that exchange on the initial add
invocation; on the deferred timeout delete it writes the alert and closes
without replying, so the script stalled on the 30s read timeout and never
reached group_remove_member. Blocked IPs were never removed from the
FortiGate. The handshake now runs only inside the add branch, mirroring the
upstream Wazuh AR helpers.

- README: drop unshipped fortigate-ar-whitelist from the structure diagram
- Rename fortigate-ar.conf -> ossec-conf-snippet.xml to end the collision
  with the runtime config path
- is_whitelisted/ip_is_valid: use 10# to avoid octal parsing on octets like
  172.08.x.x, and normalise the address before use
- fgt_api: surface all HTTP >=400 with status code and non-JSON bodies
  instead of a generic failure; fixes the unreachable duplicate-object path
- test-ar.sh: feed one line for delete, two for add, matching execd
- Add example run screenshots and reference them in the README
@lakecide

lakecide commented Aug 3, 2026

Copy link
Copy Markdown
Author

Thanks @MiguelCasaresRobles for the review, and identifying the auto-unblock. Fixed in the latest commit.

Nits, all addressed:

Removed fortigate-ar-whitelist from the structure diagram, it's created empty at install time, not shipped.
Renamed the XML snippet to ossec-conf-snippet.xml, it no longer collides with the runtime config path, and the .gitignore comment now refers unambiguously to the deployed file.
ip_is_valid() and is_whitelisted() now use 10# and normalise the address. 172.08.1.1 previously aborted with "value too great for base"; it now resolves to 172.8.1.1. Normalising also stops non-canonical forms like 010.0.0.1 reaching the FortiGate.
fgt_api() surfaces every status ≥400 with its code, and logs non-JSON bodies verbatim. Since callers use command substitution and exit codes wrap above 255, the status travels as the first line of stdout (api_http/api_body). This also fixed a latent bug: FortiGate answers 500 with error: -5 for duplicate objects, and the old || return 1 made the "already exists" fallback unreachable.

test-ar.sh now feeds two lines for add and one for delete, matching execd — the old harness would have masked this bug.

Manual add/delete against FortiOS 7.4.11, Wazuh 4.14:

=== BLOCK ===
[INFO] ======= Script 2.2.0 started PID=259402 =======
[INFO] Command=add | IP=198.51.100.99 | Rule=5712 | Agent=test-agent
[INFO] Creating address object 'wazuh-198-51-100-99' for 198.51.100.99/32
[DEBUG] API POST firewall/address -> HTTP 200: {"status":"success",...}
[INFO] Address object 'wazuh-198-51-100-99' created.
[INFO] Adding 'wazuh-198-51-100-99' to group 'Deny-List'
[DEBUG] API POST firewall/addrgrp/Deny-List/member -> HTTP 200: {"status":"success",...}
[INFO] === BLOCK complete for 198.51.100.99 ===

$ curl ... /firewall/addrgrp/Deny-List | jq '.results[0].member[].name'
"_97.107.131.17_"
"wazuh-198-51-100-99"
=== UNBLOCK (1 line, no handshake — mirrors deferred timeout) ===
21:17:43 [INFO] ======= Script 2.2.0 started PID=259775 =======
21:17:43 [INFO] Command=delete | IP=198.51.100.99 | Rule=5712 | Agent=test-agent
21:17:43 [INFO] === UNBLOCK action for 198.51.100.99 ===
21:17:43 [INFO] Removing 'wazuh-198-51-100-99' from group 'Deny-List'
21:17:43 [DEBUG] API DELETE firewall/addrgrp/Deny-List/member/wazuh-198-51-100-99 -> HTTP 200: {"status":"success",...}
21:17:43 [INFO] Removed 'wazuh-198-51-100-99' from group 'Deny-List'.
21:17:43 [INFO] Deleting address object 'wazuh-198-51-100-99'
21:17:44 [DEBUG] API DELETE firewall/address/wazuh-198-51-100-99 -> HTTP 200: {"status":"success",...}
21:17:44 [INFO] Address object 'wazuh-198-51-100-99' deleted.
21:17:44 [INFO] === UNBLOCK complete for 198.51.100.99 ===
--- Script exited with code: 0 ---

$ curl ... /firewall/addrgrp/Deny-List | jq '.results[0].member[].name'
"_97.107.131.17_"

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants