NOC Automation with FortiSOAR Workshop

waste_management plus fortinet waste_management plus fortinet

Automation with FortiSOAR – Workshop Agenda


Day 1 – Foundations & Core Automation Concepts

1. SOAR Fundamentals Overview (~1.5 hours – Presentation)

Understand the platform’s foundational structure:

Automation Design Pattern Introduced: System Object Awareness – Understand the building blocks you’ll manipulate in automation.


2. SOAR GUI Workshop (~1.5 hours – Hands-on)

Hands-on introduction to working in FortiSOAR:

Goal: Build comfort with the interface before introducing logic-based design.


Lunch Break


3. Automation Basics (~1.5 hours – Mixed Presentation + Hands-on)

Introduces the building blocks of automation logic:

Automation Design Patterns Introduced:


4. Workshop: Build a Simple Automation Flow (~1.5 hours – Hands-on)

Put theory into practice:

Goal: Apply generalized patterns in a structured task.


Day 2 – Real Use Cases & Scenario-Based Learning

5. Use Case Implementation Workshop (~4 hours – Hands-on Labs)

Work through real-world automation examples with extensible architecture in mind:

Design Mindset Emphasis: Reusability, abstraction, and validation checks.


Lunch Break


6. Ad Hoc Playbook Building Session (~3 hours – Live Co-Creation)

Build new playbooks live based on requested scenarios:

Reinforce thinking: “How do I turn a manual task into an event-driven, validated, and auditable process?”


7. Optional Advanced Content (As Time Allows)


Intro

waste_management plus fortinet waste_management plus fortinet

This chapter will help you begin exploring FortiSOAR. Continue on to he next chapters

1 - Lab Access

Accessing Environment

  1. Navigate to the FNDN Lab Environment. Input the Passphrase provided by the instructor, your Email Address, and your Name. Complete the CAPTCHA and click Sign In to access the lab environment.

    img.png img.png

  2. Locate FortiSOAR in the menu under Your Training Instance. Click the HTTPS button to launch the sign-in page to the FortiSOAR instance in your lab environment.

    Note

    The ordering and names of your instance options may be different from the ones shown in the below screenshot.

    img_6.png img_6.png

  3. Login to FortiSOAR using the credentials below:

    KeyValue
    Usernamecsadmin
    Password$3curityFabric

    img.png img.png

  4. Expand the left hand navigation pane by clicking the arrow in the top-left corner of FortiSOAR.

    img_1.png img_1.png

Tour of FortiSOAR

This chapter aims to get you familiar with the FortiSOAR interface and key concepts. If you’re new to security orchestration and automation, this guide will help you understand the fundamental components and how they work together.

What is SOAR?

Before diving into FortiSOAR specifics, it’s helpful to understand what SOAR (Security Orchestration, Automation, and Response) is:

SOAR platforms like FortiSOAR help teams automate repetitive tasks, coordinate security tools, and manage incidents more efficiently. This results in faster response times, reduced analyst fatigue, and improved operations.

Core FortiSOAR Concepts

Below are the essential terms you’ll encounter when working with FortiSOAR:

TermDefinitionWhy It Matters
DashboardThe first page you see when you log in to FortiSOAR. It provides an overview of the system and the modules you have access to.Gives you a quick snapshot of your security posture and pending work.
ModuleA table of data that is used to store information. ie. Incidents, Alerts, Indicators, Tasks, etc.Organizes different types of security data for better management. FortiSOAR lets you customize this to extreme ends, and even build your own modules.
RecordA row in a module. ie. a singular incident, alert, indicator task, or playbook.Represents a specific item that needs attention or tracking.
ScenarioA special record that is used to simulate an incident or alert.Allows you to test playbooks and automation without requiring setup and using real data.
PlaybookA series of automated tasks that are executed in a specific order.Automates repetitive security workflows to increase efficiency.
Content HubA repository of connectors and solution packs that can be installed to extend the functionality of FortiSOAR.Allows you to expand capabilities without custom development.
ConnectorA tool that allows FortiSOAR to communicate with other security systems.Enables integration with your existing security stack.
Solution PackA collection of modules, playbooks, scenario Records, dashboards, and reports that are designed to solve a specific use case.Provides ready-made automation for common security scenarios.
Execution HistoryA log of all the playbooks that have been executed in FortiSOAR.Helps with troubleshooting and compliance documentation.
System SettingsThe settings that control the behavior of FortiSOAR.Allows customization of the platform to meet your organization’s needs.

FortiSOAR Interface Navigation

The interface is designed to give you easy access to all the functionality you need. Here are the key elements:

img_1.png img_1.png

NumberPictureNameDescriptionCommon Uses
1img_1.png img_1.pngNavigation ExpanderUsed to expand the left hand navigation menu.Toggle the navigation pane when you need more screen space.
2img_2.png img_2.pngLive Sync IconIndicates that the information on the screen is updated in real-time.Verify you’re seeing the latest data during active incidents.
3img_3.png img_3.pngSearch BarUsed as a global search for things in FortiSOAR.Quickly find specific incidents, alerts, or indicators.
4img_4.png img_4.pngSystem SettingsUsed to change the system settings of FortiSOAR.Configure user permissions, email settings, and integrations.
5img_5.png img_5.pngExecution HistoryA log of all the playbooks that have been executed in FortiSOAR.Troubleshoot automation issues or verify actions were taken.
6img_6.png img_6.pngNotificationsComment mentions or items that require your attention.Stay informed about mentions and updates to your assigned items.
7img_10.png img_10.pngPending TasksA list of tasks or playbook approvals that are assigned to you and require your attention.Quickly access your to-do list and required approvals.
8img_8.png img_8.pngUser ProfileUsed to change your user settings and log out of FortiSOAR.Update your preferences and contact information.
9
Expand Me

img_13.png img_13.png

Navigation MenuThe left hand navigation menu that contains all the modules and pages you have access to in FortiSOAR.Access different modules and functionality of the platform.
10img_11.png img_11.pngRecycle BinA place where deleted records/playbooks are stored. You can restore deleted items from the recycle bin.Recover accidentally deleted items.
11img_12.png img_12.pngVersionThe version of FortiSOAR you are currently using.Verify your version when troubleshooting or updating.
Note

The most commonly used features when demoing are the Navigation Expander (where Content Hub, Alerts, Playbooks are found), Execution History, System Settings, and Scenarios. Familiarize yourself with these to navigate FortiSOAR efficiently.

Take 5-10 minutes clicking around FortiSOAR to get a feel for the interface. You can’t break anything, so don’t be afraid to explore. 😄

As a challenge, try to find the following items in the interface:

  1. Find the Incidents module
  2. Find the Playbooks section
  3. Find the Content Hub
  4. Find the System Settings

Only proceed to the next chapter when you feel comfortable navigating the FortiSOAR interface.

Intro to API

waste_management plus fortinet waste_management plus fortinet

img.png img.png

REST API Introduction

What is a REST API?

Think of a REST API like ordering pizza over the phone. You call the pizza place (API endpoint), tell them what you want (HTTP request), and they respond with your order status or the pizza itself (HTTP response).

Key Concept: REST APIs use standard HTTP methods to perform operations

The Four Essential HTTP Methods (Pizza Edition)

MethodPizza ActionWhat It DoesNetwork Equivalent
GET“What pizzas do you have?”View the menuGet device status, view policies
POST“I’d like to order a large pepperoni”Create new orderAdd firewall rule, create VLAN
PUT“Change my order to extra cheese”Update existing orderModify interface config
DELETE“Cancel my order”Remove the orderDelete policies, remove devices

REST API Structure (Pizza Restaurant)

Base URL: https://tonys-pizza.com/api/v1/

Example Pizza Endpoints:

GET    /menu                       # See what's available
GET    /orders/12345              # Check your specific order
POST   /orders                    # Place a new order
PUT    /orders/12345              # Change your order
DELETE /orders/12345              # Cancel your order

Example Network Endpoints:

GET    /devices                   # List all devices
GET    /devices/fw-01            # Get specific device info
POST   /devices                  # Add new device
PUT    /devices/fw-01/config     # Update device config
DELETE /devices/fw-01            # Remove device

HTTP Status Codes (Pizza Responses)

Response codes are important to understand since they indicate either a success or problem that occurred with the request. The table below describes the most common response codes you will see when working with API’s

CodePizza MeaningNetwork MeaningWhen You See It
200“Your pizza is ready!”SuccessOperation completed
201“Order placed successfully!”CreatedNew resource created
400“We don’t have pineapple pizza”Bad RequestInvalid data sent
401“You need to give us your phone number”UnauthorizedAuthentication failed
404“That pizza doesn’t exist”Not FoundResource doesn’t exist
500“Our oven is broken”Server ErrorAPI/device internal error

Request/Response Format

Pizza Order Request:

POST /api/v1/orders HTTP/1.1
Host: tonys-pizza.com
Content-Type: application/json
Authorization: Bearer customer-loyalty-card-123

{
  "size": "large",
  "toppings": ["pepperoni", "mushrooms"],
  "crust": "thin",
  "delivery_address": "123 Main St"
}

Pizza Order Response:

HTTP/1.1 201 Created
Content-Type: application/json

{
  "order_id": 12345,
  "status": "preparing",
  "estimated_time": "25 minutes",
  "total": "$18.99"
}

Network Policy Request:

POST /api/v1/firewall/policies HTTP/1.1
Host: fortigate.company.com
Content-Type: application/json
Authorization: Bearer your-token-here

{
  "name": "Allow-Web-Traffic",
  "source": "LAN",
  "destination": "WAN", 
  "service": "HTTP"
}

JSON Data Format (Pizza Menu Style)

{
  "pizza": {
    "name": "Supreme Deluxe",
    "size": "large",
    "price": 22.99,
    "toppings": [
      {
        "name": "pepperoni",
        "extra": false
      },
      {
        "name": "cheese",
        "extra": true
      }
    ]
  }
}

Network Device Format:

{
  "device": {
    "hostname": "fw-branch-01",
    "ip_address": "192.168.1.1",
    "model": "FortiGate-60F",
    "interfaces": [
      {
        "name": "port1",
        "ip": "192.168.100.1/24",
        "status": "up"
      }
    ]
  }
}

Practical Examples

Check Pizza Menu (Get Device Info)

# Pizza version
curl -X GET "https://tonys-pizza.com/api/v1/menu" \
     -H "Authorization: Bearer loyalty-card-123"

# Network version  
curl -X GET "https://fortimanager.company.com/api/v2/devices/fw-01" \
     -H "Authorization: Bearer your-api-key"

Order a Pizza (Create Firewall Policy)

# Pizza version
curl -X POST "https://tonys-pizza.com/api/v1/orders" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer loyalty-card-123" \
     -d '{
       "size": "large",
       "toppings": ["pepperoni"],
       "delivery": true
     }'

# Network version
curl -X POST "https://fortigate.company.com/api/v2/policies" \
     -H "Content-Type: application/json" \
     -H "Authorization: Bearer your-api-key" \
     -d '{
       "name": "Block-Social-Media",
       "action": "deny",
       "source": "LAN-Users",
       "service": "Facebook"
     }'

Change Your Order (Update Interface)

# Pizza version
curl -X PUT "https://tonys-pizza.com/api/v1/orders/12345" \
     -H "Content-Type: application/json" \
     -d '{
       "special_instructions": "Extra cheese please!",
       "delivery_time": "ASAP"
     }'

# Network version
curl -X PUT "https://switch.company.com/api/v1/interfaces/GigE0/1" \
     -H "Content-Type: application/json" \
     -d '{
       "description": "Server Farm Connection",
       "vlan": 100
     }'

Error Handling

Pizza Error Response:

{
  "error": {
    "code": 400,
    "message": "Sorry, we're out of pineapple",
    "suggestion": "Try pepperoni instead?"
  }
}

Network Error Response:

{
  "error": {
    "code": 400,
    "message": "Invalid VLAN ID",
    "details": "VLAN ID must be between 1-4094"
  }
}

Next we’ll look at how to use the API in FortiManager

FortiManager API Guide

Introduction

The FortiManager API provides programmatic access to all FortiManager functions, enabling you to automate device management, policy deployment, and configuration tasks. This guide will teach you how to discover API calls, understand their structure, and implement them in your automation workflows.

Why Use the FortiManager API?

Common Use Cases


Chapter 1: Discovering API Calls with Browser DevTools

The easiest way to understand FortiManager APIs is to watch what happens when you use the web interface. Your browser’s Developer Tools can reveal the exact API calls behind every action.

Method 1: Using Chrome DevTools

Step-by-Step Process:

  1. Login to FortiManager. img_2.png img_2.png

  2. Open Developer Tools

  3. Prepare Network Monitoring

  4. Execute the Action

  5. Filter and Analyze

img.png img.png

What You’ll See in DevTools

Request Headers Section:

Request URL: https://your-fmg.company.com/jsonrpc
Request Method: POST
Content-Type: application/json

Request Payload (Body):

{
  "method": "get",
  "params": [
    {
      "url": "/pm/config/adom/root/obj/firewall/policy",
      "option": [
        "get meta"
      ]
    }
  ],
  "session": "abc123session456",
  "id": 1
}

Response:

{
  "result": [
    {
      "status": {
        "code": 0,
        "message": "OK"
      },
      "url": "/pm/config/adom/root/obj/firewall/policy",
      "data": [
        ...
      ]
    }
  ],
  "id": 1
}

Method 2: Use the Fortinet Developer Network (FNDN)

Access FNDN API Docs
Access FNDN API Docs
Using API Docs
Using API Docs
API Tester
API Tester

Method 3: FMG API By Example

This is my favorite method by far. This is an example based API Doc that has examples of API calls with an easily searchable interface.

https://how-to-fortimanager-api.readthedocs.io/en/latest/index.html

img.png img.png


Chapter 2: Understanding FortiManager API Structure

JSON-RPC Format

FortiManager uses JSON-RPC 2.0 protocol for API communication. Every request follows this structure:

{
  "method": "operation_type",
  "params": [
    {
      "url": "/api/endpoint/path",
      "data": {
        object_data
      },
      "option": [
        "additional_options"
      ]
    }
  ],
  "session": "session_token",
  "id": request_id
}

Common Methods

MethodPurposeExample Use
getRetrieve dataGet firewall policies, device status
addCreate new objectsAdd address objects, create policies
setModify existing objectsUpdate policy settings, change device config
deleteRemove objectsDelete policies, remove devices
updateBulk updatesMass policy changes
cloneCopy objectsDuplicate policies or objects
moveReorder itemsChange policy sequence

URL Structure Patterns

FortiManager API URLs follow predictable patterns:

Device Management:

Policy & Objects:

System Operations:

Response Structure

All API responses follow this format:

{
  "result": [
    {
      "status": {
        "code": 0,
        "message": "OK"
      },
      "url": "/pm/config/adom/root/obj/firewall/address",
      "data": [
        {
          "name": "Test-Server",
          "uuid": "550e8400-e29b-41d4-a716-446655440000",
          "type": "ipmask",
          "subnet": [
            "192.168.100.50",
            "255.255.255.255"
          ]
        }
      ]
    }
  ],
  "id": 1
}

Status Codes:


Chapter 3: Common API Operations

Getting Firewall Policies

Request:

{
  "method": "get",
  "params": [
    {
      "url": "/pm/config/adom/root/pkg/default/firewall/policy"
    }
  ],
  "session": "your-session",
  "id": 1
}

Creating Address Objects

Request:

{
  "method": "add",
  "params": [
    {
      "url": "/pm/config/adom/root/obj/firewall/address",
      "data": {
        "name": "WebServer-DMZ",
        "type": "ipmask",
        "subnet": [
          "10.0.100.10",
          "255.255.255.255"
        ],
        "comment": "DMZ Web Server"
      }
    }
  ],
  "session": "your-session",
  "id": 1
}

Bulk Operations: Creating Multiple Objects

Request:

{
  "method": "add",
  "params": [
    {
      "url": "/pm/config/adom/root/obj/firewall/address",
      "data": [
        {
          "name": "Server-01",
          "type": "ipmask",
          "subnet": [
            "192.168.10.10",
            "255.255.255.255"
          ]
        },
        {
          "name": "Server-02",
          "type": "ipmask",
          "subnet": [
            "192.168.10.11",
            "255.255.255.255"
          ]
        },
        {
          "name": "Server-03",
          "type": "ipmask",
          "subnet": [
            "192.168.10.12",
            "255.255.255.255"
          ]
        }
      ]
    }
  ],
  "session": "your-session",
  "id": 1
}

Installing Policies to Devices

After creating or modifying policies, install them to devices:

Request:

{
  "method": "exec",
  "params": [
    {
      "url": "/securityconsole/install/package",
      "data": {
        "adom": "root",
        "pkg": "default",
        "scope": [
          {
            "name": "FortiGate-01",
            "vdom": "root"
          },
          {
            "name": "FortiGate-02",
            "vdom": "root"
          }
        ]
      }
    }
  ],
  "session": "your-session",
  "id": 1
}

FortiGate API Calls via FortiManager (Proxy Method)

FortiManager maintains established tunnels to FortiGate devices, allowing you to leverage these connections for API calls. This approach offers several advantages:

Making Proxy API Calls

To execute API calls through FortiManager, you need to specify the FortiOS API endpoint within the FortiManager request structure.

Request Structure

ParameterDescription
targetDevice or group identifier (e.g., All_FortiGate group)
actionHTTP method (get, post, put, delete)
resourceFortiOS API endpoint path

Example: Query Admin Account Settings

This example demonstrates querying admin users from all FortiGates in the ADOM using the hidden All_FortiGate group:

{
  "id": "1",
  "method": "exec",
  "params": [
    {
      "url": "/sys/proxy/json",
      "data": {
        "target": "adom/root/group/All_FortiGate",
        "action": "get",
        "resource": "/api/v2/cmdb/system/admin?format=name"
      }
    }
  ],
  "verbose": 1,
  "session": "{{session}}"
}

This is what the api looked like in FNDN for querying fortigate admin users (/api/v2/cmdb + /system/admin) img.png img.png

Make sure to check out all the options available with /sys/proxy/json here

API in FortiSOAR

This guide demonstrates how to make generic API calls directly within SOAR playbooks when specific connectors aren’t available. We’ll use the Dad Joke API as a practical example.

Dad Joke API Overview

Base URL: https://icanhazdadjoke.com/

Key Features:

API Response Formats:

Example JSON Response:

{
  "id": "R7UfaahVfFd",
  "joke": "My dog used to chase people on a bike a lot. It got so bad I had to take his bike away.",
  "status": 200
}

img.png img.png

Creating the SOAR Playbook

Step 1: Set Up Collection and Playbook

  1. Navigate to Automation > Playbooks
  2. Click New Collection
  3. Click Create
  4. Click Add Playbook
  5. Click Create

Step 2: Configure Manual Trigger

You should now see this screen: img.png img.png

  1. Select the Manual trigger start step
  2. Configure the following settings:
  3. (Optional) Add a custom popup message

img.png img.png

Step 3: Add API Call Step

  1. Click and drag from the blue arrows to create a new action step Description Description
  2. Select Utilities Step img.png img.png
  3. Configure the API call:
  4. Click Save

Your step should look like this: img.png img.png

Testing the Playbook

Run the Playbook

  1. Click the Play button (top right) img.png img.png
  2. Click Save and Test img.png img.png
  3. Click Trigger Playbook (bottom left) img.png img.png

The execution history will display automatically, showing real-time playbook execution with input/output details for each step. img.png img.png

Review Results

  1. Double-click the “Make API Call” step to view results img.png img.png
  2. Click Expand to view the complete JSON response img.png img.png img.png img.png
  3. Verify the API call returned the expected joke data structure

Trigger from Alerts Page

  1. Navigate to Incident Response > Alerts
  2. Select Execute and click the drop down item “Get Dad Joke” img.png img.png
  3. Open the playbook execution history at the top right img.png img.png
  4. Open the step Make API Call results and see what joke you got this time.

Having SOAR make API calls internally is fun and all, but what if you wanted to display the joke with a pop-up, or add the joke to an Alert? Or what if we wanted to schedule this playbook to run twice a day? These concepts and more will be covered in the Playbooks section.

Takeaways

FMG API in SOAR

The FortiManager JSON RPC connector was created by myself (Dylan Spille) to better work with the RPC actions that FortiManager supports. In order to use this connector, you do need to have familiarity with FMG API endpoints, and how to format the request data. Make sure you’ve read the FMG API section before this chapter.

Prerequisites

The Venn diagram between playbooks, jinja, and API in SOAR is very much like a complete circle, so it’s a little challenging to have different chapters but not involve the other sections. If you find yourself stuck, you may want to visit the Playbooks chapter and come back to this later.

Create an API Account on the FortiManager

  1. Login to the FortiManager UI (Get IP of the FortiManager VM from your Workshop Dashboard) with the following credentials:
  2. Open the integrated SSH terminal ssh_terminal ssh_terminal
  3. Run the following command to create a new API Admin (Use the copy button below)
    config system admin user
        edit "fortisoar"
            set password fortinet
            set profileid "Super_User"
            set avatar "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"
            set rpc-permit read-write
        next
    end

Setup FMG JSON RPC Connector

  1. In FortiSOAR, navigate to Content Hub > Connectors.
  2. Search for FortiManager JSON RPC
  3. Open the connector.
  4. Configure the connector
  5. Make sure the health check passes img.png img.png

Register Enterprise Core to FMG

  1. Login to Enterprise_Core Fortigate. Follow the steps outlined here to register the fortigate to fortimanager.
  2. Don’t authorize the device to FMG yet. We’ll do that from SOAR

Basic Queries

Get list of device in SOAR

  1. Navigate to Automation > Playbooks
  2. Create a new collection called 00 - FMG API
  3. Create a new playbook called Get FMG Devices
  4. Choose the Referenced Trigger step
  5. Drag a new step and pick Connector
  6. Search for JSON RPC and select the Fortimanager Connector img.png img.png
  7. Provide the Follow details
    {
      "fields": [
        "name",
        "sn",
        "extra info",
        "os_ver",
        "mr",
        "patch",
        "mgmt_mode"
      ],
      "option": [
        "extra info"
      ],
      "loadsub": 0
    }
    Your step should look like this img.png img.png
  8. Save the Step
  9. Click Save Playbook
  10. Click the Play button img.png img.png
  11. Click Trigger Playbook
  12. Once the Executed Playbook Logs opens, click the step Get Devices. Expand the output data and you will see the json responses from the api call img_1.png img_1.png
    Note
    Important things to see in the API Output are the **name**, **sn**, and **mgmt_mode** values. mgmt_mode is `unreg` because we have not authorized the device yet
    

Authorize a Device

Now that we can see unregistered devices, let’s create a playbook to authorize them to FortiManager.

Create Device Authorization Playbook

  1. Create a new playbook called Authorize FMG Device

  2. Add a JSON RPC connector step:

    {
      "adom": "root",
      "device": {
        "device action": "promote_unreg",
        "name": "Enterprise_Core"
      },
      "flags": [
        "create_task"
      ]
    }

    Your step should look like this img_1.png img_1.png

  3. Save the Playbook

  4. Run/Trigger the playbook

  5. If you switch to the FMG soon enough, you will see the add device task running img_1.png img_1.png

  6. If successfully added, you will see this json output in SOAR img_1.png img_1.png

Get Configuration from FMG CMDB

Retrieve Device Configuration

  1. Create a new playbook called Get Device Config

  2. Add JSON RPC connector step:

    {
      "option": ["get meta"]
    }
  3. Save/Trigger the playbook. Your step output should look like this

    img_1.png img_1.png

Additional use cases

Additional cases will be added depending on time, such as:

Playbooks

waste_management plus fortinet waste_management plus fortinet

This section will provide an overview of playbooks and how they are used in FortiSOAR.

What are Playbooks

What is a playbook

A playbook is a set of steps that can be executed in response to a trigger or alert. Playbooks can be used to automate repetitive tasks, such as gathering information, enriching data, and executing remediation actions. Playbooks can be created using the FortiSOAR Playbook Editor, which provides a visual interface for designing and configuring playbooks.

Why use playbooks

Playbooks can embody best practices and guide users through the process of responding to a security incident. This means that the users of playbooks don’t need to be experts in the tools and processes that are used to respond to an incident. More advanced users can create playbooks and expose them to less experienced users, who can then execute the playbooks to respond to incidents.

Triggers

Playbooks can be triggered by a variety of events, such as a new incident being created, the severity of an alert changing, or even via webhook from an external tool. These events are called triggers. playbook_triggers playbook_triggers

Types of Triggers

Manual Trigger

A manual trigger playbooks means that you want the playbook to only be started manually by a user. These playbooks will be accessible by clicking a button in the FortiSOAR UI on some record.

manual trigger manual trigger

Note

Notice from the image the Trigger Button Label and the selection of Alerts module. This means that the manual trigger will only be available on the Alerts module.

After creating a manual playbook and telling FortiSOAR what module to display it in, you can run the playbook by clicking the Execute button and selecting the playbook you want to run manual_playbook_on_alert manual_playbook_on_alert

Tip

You can have any number of manual triggers on any module, and you can also have multiple manual triggers on the same module.

On-Create Trigger

An on-create trigger playbook means that the playbook will be started automatically when a new record is created in the module you specify. This is useful for creating a playbook that will run when a new incident is created, for example. You can also specify a filter to only run the playbook when certain conditions are met. For example, a playbook that filter for incidents with a severity of “High”, and the name of the incident contains “Ransomware”. on_create_trigger on_create_trigger

Tip

There is no limit to the number of on-create triggers you can have on a module

Typically, organizations will have different playbooks that run for different types of incidents.

On-Update Trigger

An on-update trigger playbook means that the playbook will be started automatically when a record in the module you specify is updated/changed. This is useful for creating a playbook that will run when the severity of an incident changes, for example. You can also specify a filter to only run the playbook when certain conditions are met. For example, a playbook could trigger when the status of an incident changes to “In Progress”, so that FortiSOAR could automatically update the MTTA (Mean Time to Acknowledge) field in the incident record. on_update_trigger on_update_trigger

Note

This trigger will only run when the status has changed AND changed to “In Progress”.

Reference Trigger

A reference trigger is used to signify that you want this playbook to be activated from another playbook. This is used to abstract out common functionality into a separate playbook, and then call that playbook from other playbooks. This is useful for creating a playbook that will run when a new incident is created, for example.

Custom API Endpoint Trigger

A custom API trigger allows you to create an API endpoint to enable external systems to trigger playbooks. Tools like Servicenow or Jira can be configured to call this API endpoint when certain events occur, and then FortiSOAR will run the playbook you specify.

custom_api_trigger custom_api_trigger

curl  -X POST -u 'username:password' https://<FortiSOAR>/api/triggers/1/deferred/api_endpoint
import requests
requests.post('https://<FortiSOAR>/api/triggers/1/deferred/api_endpoint', auth=('username', 'password') )

Actions

Actions are the building blocks of playbooks. They are the individual steps that connect together after the trigger to form a playbook. There are many different types of actions, and they can be used to perform a wide variety of tasks such as but not limited to, sending an email, creating a ticket in a ticketing system, creating and updating FortiSOAR records, or even running a script on a remote server.

Core Actions
Core Actions
Evaluate Actions
Evaluate Actions
Execute Actions
Execute Actions
Reference Actions
Reference Actions

Core Actions

Core actions are used to do things like creating, updating, and searching for records in SOAR. You can also create variables and use them in other actions within the same playbook

Create Record

The Create Record action lets you add new entries to any module in SOAR, such as alerts, incidents, or assets. This is commonly used when you receive alerts from external security tools and need to create corresponding records in SOAR for tracking and investigation.

What makes this action powerful is its built-in intelligence for handling duplicate records. You can specify which fields should be unique - for example, ensuring you don’t create multiple alerts for the same IP address or incident.

img.png img.png

When SOAR detects that a new record would duplicate an existing one based on your uniqueness rules, you have several options for how to handle the situation. Instead of creating a duplicate record, you can choose to update the existing record with new information, skip the creation entirely, or take other custom actions. This prevents your system from being cluttered with duplicate entries while ensuring important information isn’t lost.

For instance, if your playbook tries to create an alert for IP address 192.168.1.100 but an alert for that IP already exists, SOAR can automatically update the existing alert with any new details rather than creating a second alert for the same IP address.

Update Record

Update any record in SOAR. In order to upgrade records, you need to know the record’s “IRI” which is a unique identified. An example of this would be a step that changes an alerts status from New to In progress automatically after the playbook looks up information. Often used as an “enriching” step.

Find Records

Use the Find Records step to find a record in a module within FortiSOAR, using a query or search criteria. You can build nested queries and use any field to search for records img.png img.png

Set Variable

Oftentimes you want to save specific information from a step, or have a dedicated step to use as a midpoint before another step in order to do data transformation or extraction. Set variables are also used to “see” what the data looked like at particular point in the playbook.

img_1.png img_1.png Before img_1.png img_1.png After

Evaluate Actions

Evaluate actions are used to make decisions, gather input from analysts, or halt a playbook. They can be used to check if a condition is true or false and then take different actions based on the result.

Decision Step

This step lets you use Jinja to use simple or complex logic to “route” which path the playbook takes. For example, If Severity is “Low”, take the left path, or if it’s “High”, take the right path, else take the middle

Decision step before configuration
Decision step before configuration
Decision step configuration editor
Decision step configuration editor
Decision step execution results
Decision step execution results

Manual Input

This step lets you build an input form in case you need a user to provide information to a playbook, or use it to simply have two buttons which could have the playbook take either Path A or Path B depending on the button. img.png img.png

Execute Steps

Execute actions let you use any of the configured Connectors to perform actions on external systems. Utilities is a special connector that has common utility-like actions. The Code Snippet connector allows you to write and execute raw Python code.

Connectors

FortiSOAR has prebuilt connectors to various tools and products, all you need to do is provide credentials to get access to them. Afterwards you can use the visual editor to pick which action you want on the step

Here’s an example of using the FortiGate connector to create an address objects img.png img.png

Code Snippet

Code snippet is a special connector that lets you run python code from a playbook. Here is an example of using python to break up an ip address into octets. The first picture shows the playbook editor step, and the second shows what is printed from the playbook execution history img.png img.png —-> img.png img.png

Reference Actions

Reference a Playbook lets you execute another playbook from within the current playbook. This is useful for reusing common tasks across multiple playbooks. In that context, the playbook being called is often referred to a child playbook, and the playbook calling it is referred to as the parent playbook. The child playbook returns the last step results up to the parent playbook, that way you can use the data returned.

Hands on

Now that we’ve covered the playbook basics, let’s get hands-on with building a couple of playbooks.

Create a new playbook collection and playbook

  1. On the left pane Select Automation > Playbooks Playbooks Playbooks

  2. Click the + New Collection Button and enter

  3. Click Create New Collection New Collection

  4. Click the + New Playbook Button and enter

  5. Click Create

  6. Select the Manual Trigger start step and enter

    Note

    Selecting does not require a record means that we don’t need to have an alert to start the playbook. This is useful for testing and for playbooks that don’t need to be tied to an alert.

  7. Click Save

  8. Click and hold the blue glowing arrows and drag your mouse out and let go to create a new action step. This will pop up the list of steps we can choose next

    Description Description

  9. Select the Send Email action, towards the bottom left of the page

  10. Fill in the fields

  11. Click Save

  12. Click Save Playbook at the top right of the page

  13. Click the play button (Looks like a right arrow) at the top right of the page Trigger Playbook Trigger Playbook

  14. Click the Trigger Playbook button at the bottom left of the page. You should see the execution of the playbook pop up after this Trigger Playbook Trigger Playbook

You have now created and triggered your first playbook. You should receive an email shortly.

Tip

You can click on the step output to see the details of the step. This is useful for debugging and understanding what is happening in the playbook.

img.png img.png

Trigger playbook from a button

  1. Navigate to Incident Response > Alerts
  2. Click the Execute button and select Workshop: Send Email Trigger Playbook Trigger Playbook

You will see a popup showing the playbook executed. You should receive another email shortly. Trigger Playbook Trigger Playbook

Challenges

Tip

Reach out to the instructor if you need help with the challenges.

Challenge 1

Create a playbook that triggers when an incident is created AND the severity is critical AND the type is Lateral Movement. The playbook should email an address of your choosing.

Challenge 2

Create a manual trigger playbook that prompts the user for an IP address and then blocks the IP address on a FortiGate.

Challenge 3

Create a playbook that adds the text Alert has been Closed to the description field of an alert when the status is updated to Closed.

Jinja

waste_management plus fortinet waste_management plus fortinet

Jinja Basics

Course Introduction

Welcome to this comprehensive guide on using Jinja templates for FortiGate configuration management. This tutorial is designed specifically for those who want to automate and standardize their Fortinet device configurations without requiring extensive programming knowledge.

What is Jinja2?

Jinja2 is a powerful templating language that allows you to create dynamic text-based documents. Instead of manually creating individual configuration files for each FortiGate device, you can create one template and automatically generate customized configurations for hundreds of devices.

Where is Jinja Used in Network Automation?

Some notable tools that use Jinja2 for network automation include:

Why Use Jinja?

Benefits

  1. Consistency - Eliminate configuration errors and ensure standardization across all devices
  2. Efficiency - Generate configurations for multiple devices in seconds instead of hours

How Jinja Templating Works

Jinja2 needs two main components to generate a configuration:

  1. Template - A configuration file with placeholders for dynamic values
  2. Data - The specific values that will replace the placeholders
graph LR
    A[Template File] --> C["Jinja Engine (FortiSOAR)"]
    B[Data Values] --> C
    C --> D[Generated Configuration]
    
    classDef default fill:#f8fafc,stroke:#374151,stroke-width:2px,color:#1f2937
    classDef engine fill:#4f46e5,stroke:#312e81,stroke-width:3px,color:#ffffff
    
    class C engine
    
    linkStyle 0 stroke:#dc2626,stroke-width:3px
    linkStyle 1 stroke:#dc2626,stroke-width:3px
    linkStyle 2 stroke:#dc2626,stroke-width:3px

The template contains your standard FortiGate configuration with variables marked by special syntax, and the data provides the specific values for each device.


Understanding JSON

Before diving into Jinja templates, let’s understand JSON (JavaScript Object Notation) - the format we’ll use to store our configuration data.

What is JSON?

JSON is a simple, text-based format for storing and exchanging data. Think of it as a digital filing system where information is organized in a structured, easy-to-read way. Despite its name mentioning JavaScript, JSON is widely used across all programming languages and automation tools.

Why JSON for Network Automation?

JSON Syntax Rules

JSON has only a few simple rules:

  1. Data is in name/value pairs - "hostname": "fw-branch-01"
  2. Data is separated by commas - "hostname": "fw-01", "timezone": "EST"
  3. Objects are wrapped in curly braces - { "hostname": "fw-01" }
  4. Arrays are wrapped in square brackets - ["8.8.8.8", "1.1.1.1"]
  5. Strings must be in double quotes - "hostname" not hostname

JSON Data Types

Strings (Text):

{
  "hostname": "branch-office-fw-01",
  "description": "Main office firewall"
}

Numbers:

{
  "port_number": 443,
  "vlan_id": 100,
  "mtu": 1500
}

Lists/Arrays (Multiple Values):

{
  "dns_servers": [
    "8.8.8.8",
    "1.1.1.1"
  ],
  "allowed_ports": [
    80,
    443,
    22,
    3389
  ]
}

Objects (Grouped Information):

{
  "interface": {
    "name": "port1",
    "ip": "192.168.1.1",
    "mask": "255.255.255.0"
  }
}

Chapter 1: Basic Variable Substitution

Let’s start with a simple FortiGate configuration example to understand how Jinja variable substitution works.

Example: Basic System Configuration

Here’s a typical FortiGate system configuration snippet:

config system global
    set hostname "branch-office-fw-01"
    set timezone "America/New_York"
end

config system dns
    set primary 8.8.8.8
    set secondary 1.1.1.1
end

config system ntp
    set ntpsync enable
    set server-mode disable
    set syncinterval 60
    config ntpserver
        edit 1
            set server "pool.ntp.org"
            set ntpv3 enable
        next
    end
end

Converting to a Jinja Template

The first step is identifying which parts should be variables. In our example:

Here’s the same configuration as a Jinja template:

config system global
    set hostname "{{ hostname }}"
    set timezone "{{ timezone }}"
end

config system dns
    set primary {{ dns_primary }}
    set secondary {{ dns_secondary }}
end

config system ntp
    set ntpsync enable
    set server-mode disable
    set syncinterval 60
    config ntpserver
        edit 1
            set server "{{ ntp_server }}"
            set ntpv3 enable
        next
    end
end

Understanding the Syntax

In Jinja2, anything between {{ }} (double curly braces) tells the engine to:

  1. Look for a variable with that name
  2. Replace the entire {{ variable_name }} with the variable’s value

Data for the Template (JSON Format)

The data that feeds into our template is stored in JSON format:

{
  "hostname": "branch-office-fw-01",
  "timezone": "America/New_York",
  "dns_primary": "8.8.8.8",
  "dns_secondary": "1.1.1.1",
  "ntp_server": "pool.ntp.org"
}

Generated Output

When the template is processed with the JSON data above, it produces:

config system global
    set hostname "branch-office-fw-01"
    set timezone "America/New_York"
end

config system dns
    set primary 8.8.8.8
    set secondary 1.1.1.1
end

config system ntp
    set ntpsync enable
    set server-mode disable
    set syncinterval 60
    config ntpserver
        edit 1
            set server "pool.ntp.org"
            set ntpv3 enable
        next
    end
end

Hands-On Exercise 1: Basic Variable Substitution

Let’s practice basic variable substitution using the FortiSOAR Jinja Editor.

To access the Jinja Editor:

  1. On the left pane, navigate to Automation > Playbooks img_4.png img_4.png

  2. Create a new playbook or edit an existing one

  3. Click the Tools dropdown on the top right and select Jinja Editor img_2.png img_2.png

Exercise:

In the Jinja Editor, you’ll see two panels:

Click Render to see the output of the Template

Task: Create a simple firewall policy configuration using variables.

Step 1: Copy this JSON data into the right panel:

{
  "policy_name": "Allow_Web_Traffic",
  "source_interface": "port1",
  "destination_interface": "port2",
  "source_address": "Internal_LAN",
  "destination_address": "all",
  "service": "HTTPS"
}

Step 2: Create a Jinja template in the right panel that generates this output:

config firewall policy
    edit 0
        set name "Allow_Web_Traffic"
        set srcintf "port1"
        set dstintf "port2"
        set srcaddr "Internal_LAN"
        set dstaddr "all"
        set service "HTTPS"
        set action accept
    next
end

Try writing the template yourself first!

Show Solution
config firewall policy
    edit 0
        set name "{{ policy_name }}"
        set srcintf "{{ source_interface }}"
        set dstintf "{{ destination_interface }}"
        set srcaddr "{{ source_address }}"
        set dstaddr "{{ destination_address }}"
        set service "{{ service }}"
        set action accept
    next
end

Chapter 2: Working with Complex Data Structures

Real network configurations often require more complex data organization. Let’s explore how to work with JSON objects and arrays in Jinja templates.

Using JSON Objects for Interface Configuration

Instead of having separate variables for each interface attribute, we can group related information together using JSON objects:

{
  "interface": {
    "name": "port1",
    "ip": "192.168.1.1",
    "netmask": "255.255.255.0",
    "description": "LAN Interface",
    "status": "up"
  }
}

JSON Object Explanation:

Template with JSON Object Access

Jinja provides convenient “dot notation” to access JSON object values:

config system interface
    edit "{{ interface.name }}"
        set ip {{ interface.ip }} {{ interface.netmask }}
        set description "{{ interface.description }}"
        set status {{ interface.status }}
    next
end

This approach makes templates more readable and data more organized.

Hands-On Exercise 2: Working with JSON Objects

Task: Create a template for configuring multiple FortiGate interfaces using nested JSON objects.

Step 1: Copy this JSON data into the right panel:

{
  "device_name": "FW-BRANCH-01",
  "interfaces": {
    "lan": {
      "name": "port1",
      "ip": "192.168.1.1",
      "netmask": "255.255.255.0",
      "description": "LAN Interface",
      "allowaccess": "ping https ssh"
    },
    "wan": {
      "name": "port2",
      "ip": "203.0.113.10",
      "netmask": "255.255.255.252",
      "description": "WAN Interface",
      "allowaccess": "ping"
    },
    "dmz": {
      "name": "port3",
      "ip": "10.10.10.1",
      "netmask": "255.255.255.0",
      "description": "DMZ Interface",
      "allowaccess": "ping https"
    }
  }
}

Step 2: Write a template that configures all three interfaces. Your output should look like this:

# Configuration for FW-BRANCH-01

config system interface
    edit "port1"
        set ip 192.168.1.1 255.255.255.0
        set description "LAN Interface"
        set allowaccess ping https ssh
    next
    edit "port2"
        set ip 203.0.113.10 255.255.255.252
        set description "WAN Interface"
        set allowaccess ping
    next
    edit "port3"
        set ip 10.10.10.1 255.255.255.0
        set description "DMZ Interface"
        set allowaccess ping https
    next
end
Show Solution
# Configuration for {{ device_name }}

config system interface
    edit "{{ interfaces.lan.name }}"
        set ip {{ interfaces.lan.ip }} {{ interfaces.lan.netmask }}
        set description "{{ interfaces.lan.description }}"
        set allowaccess {{ interfaces.lan.allowaccess }}
    next
    edit "{{ interfaces.wan.name }}"
        set ip {{ interfaces.wan.ip }} {{ interfaces.wan.netmask }}
        set description "{{ interfaces.wan.description }}"
        set allowaccess {{ interfaces.wan.allowaccess }}
    next
    edit "{{ interfaces.dmz.name }}"
        set ip {{ interfaces.dmz.ip }} {{ interfaces.dmz.netmask }}
        set description "{{ interfaces.dmz.description }}"
        set allowaccess {{ interfaces.dmz.allowaccess }}
    next
end

Working with JSON Arrays

JSON arrays are perfect for lists of similar items like DNS servers, VLANs, or firewall rules:

{
  "dns_servers": [
    "8.8.8.8",
    "1.1.1.1",
    "208.67.222.222"
  ],
  "vlans": [
    {
      "id": 10,
      "name": "LAN",
      "description": "Main LAN network"
    },
    {
      "id": 20,
      "name": "DMZ",
      "description": "DMZ network"
    }
  ]
}

JSON Array Explanation:

Hands-On Exercise 3: Accessing Array Elements

Task: Practice accessing individual elements from JSON arrays.

Step 1: Copy this JSON data:

{
  "ntp_servers": [
    "0.pool.ntp.org",
    "1.pool.ntp.org",
    "2.pool.ntp.org"
  ],
  "admin_users": [
    {
      "username": "admin",
      "profile": "super_admin",
      "password": "FortiGate123!"
    },
    {
      "username": "monitor",
      "profile": "read_only",
      "password": "Monitor456!"
    }
  ]
}

Step 2: Create a template that:

  1. Configures the first two NTP servers (using array index notation)
  2. Creates both admin users

Expected output:

config system ntp
    set ntpsync enable
    config ntpserver
        edit 1
            set server "0.pool.ntp.org"
        next
        edit 2
            set server "1.pool.ntp.org"
        next
    end
end

config system admin
    edit "admin"
        set password "FortiGate123!"
        set accprofile "super_admin"
    next
    edit "monitor"
        set password "Monitor456!"
        set accprofile "read_only"
    next
end
Show Solution
config system ntp
    set ntpsync enable
    config ntpserver
        edit 1
            set server "{{ ntp_servers[0] }}"
        next
        edit 2
            set server "{{ ntp_servers[1] }}"
        next
    end
end

config system admin
    edit "{{ admin_users[0].username }}"
        set password "{{ admin_users[0].password }}"
        set accprofile "{{ admin_users[0].profile }}"
    next
    edit "{{ admin_users[1].username }}"
        set password "{{ admin_users[1].password }}"
        set accprofile "{{ admin_users[1].profile }}"
    next
end
Note

What if we added another ntp server or user? With this declaration we’d have to update our JSON and our jinja template. Could this be optimized by using a loop somehow?

Special Characters in JSON Property Names

Sometimes you might have property names with special characters (like IP addresses as keys). When this happens, you can’t use dot notation and must use bracket notation instead:

{
  "subnets": {
    "192.168.1.0/24": {
      "description": "Main LAN",
      "vlan": 10
    },
    "10.0.0.0/8": {
      "description": "Private Network",
      "vlan": 20
    }
  }
}

Template usage:

# For subnet 192.168.1.0/24
Description: {{ subnets['192.168.1.0/24'].description }}
VLAN: {{ subnets['192.168.1.0/24'].vlan }}

Hands-On Exercise 4: Bracket Notation

Task: Use the above JSON in the Jinja Editor, and try to access the subnets without using bracket notation. What kind of error do you see?

Solution

img_7.png img_7.png We see an error that the subnets dictionary doesn’t have a key named “192” . This is because the special character . is getting treated as its own key.

JSON Validation Tips

Common JSON Mistakes to Avoid:

  1. Missing commas between items
  2. Using single quotes instead of double quotes
  3. Trailing commas after the last item
  4. Forgetting to close braces or brackets

Valid JSON:

{
  "hostname": "fw-01",
  "ports": [
    80,
    443
  ]
}

Hands-On Exercise 5: Fix the JSON

Task: The following JSON has several errors. Try to fix them in the Jinja Editor.

{
  'hostname': "fw-01"
  "interfaces": [
    {
      "name": "port1",
      "ip": "192.168.1.1"
    },
    {
      "name": 'port2',
      "ip": "10.0.0.1"
    }
  ]
}

Note

Notice how SOAR will also mention that there were issues with the JSON, which speeds up the resolution process. Json Error Json Error

Show Solution

The corrected JSON:

{
  "hostname": "fw-01",
  "interfaces": [
    {
      "name": "port1",
      "ip": "192.168.1.1"
    },
    {
      "name": "port2",
      "ip": "10.0.0.1"
    }
  ]
}

Errors fixed:

  1. Changed single quotes to double quotes for ‘hostname’
  2. Added missing comma after the hostname line
  3. Removed trailing comma after “192.168.1.1”
  4. Changed single quotes to double quotes for ‘port2’
  5. Removed trailing comma after the last array element

Chapter 3: Handling Undefined Variables

Understanding how Jinja handles missing variables is crucial for creating robust templates.

Default Behavior

By default, if a variable is not defined in your JSON data, Jinja will replace it with an empty string. This can lead to unexpected results in your FortiGate configurations.

Hands-On Exercise 6: Understanding Undefined Variables

Task: See what happens when variables are missing from your JSON data.

Step 1: Copy this JSON data (notice it’s missing the location field):

{
  "hostname": "FW-HQ-01",
  "timezone": "America/New_York"
}

Step 2: Use this template:

config system global
    set hostname "{{ hostname }}"
    set timezone "{{ timezone }}"
    set location "{{ location }}"
end

Step 3: Observe the output. What happens to the location line?

Show Result and Explanation

The output will be:

config system global
    set hostname "FW-HQ-01"
    set timezone "America/New_York"
    set location ""
end

The missing location variable is replaced with an empty string, which could cause a configuration error on the FortiGate. In later chapters, we’ll learn how to handle missing variables gracefully using conditional statements and default values.


Chapter 4: Adding Comments to Templates

Comments are essential for maintaining templates, especially when multiple team members work on the same configurations.

Comment Syntax

Jinja uses {# ... #} for comments. Everything between these markers is ignored during template processing.

{# FortiGate Basic System Configuration Template #}
{# Version: 1.0 #}
{# Author: Network Team #}

config system global
    set hostname "{{ hostname }}"  {# Device hostname #}
    set timezone "{{ timezone }}"
end

Using Comments for Template Management

Comments are valuable for:

  1. Documenting template purpose and version
  2. Explaining complex configuration sections
  3. Noting dependencies or requirements
  4. Temporarily disabling configuration sections during testing

Hands-On Exercise 7: Working with Comments

Task: Practice using comments in your templates.

Step 1: Use this JSON data:

{
  "hostname": "FW-TEST-01",
  "timezone": "UTC",
  "ntp_server": "pool.ntp.org",
  "admin_timeout": 480
}

Step 2: Use this template:

{# 
   FortiGate System Configuration Template
   Version: 2.0
   Last Updated: 2025-06-09
   Purpose: Standard system settings for all branch firewalls
#}

config system global
    set hostname "{{ hostname }}"  {# Unique identifier for this device #}
    set timezone "{{ timezone }}"  {# Use UTC for global deployments #}
    set admin-timeout {{ admin_timeout }}  {# Session timeout in seconds #}
end

{# NTP configuration - uncomment when ready to deploy
config system ntp
    set ntpsync enable
    config ntpserver
        edit 1
            set server "{{ ntp_server }}"
        next
    end
end
#}

{# TODO: Add DNS configuration in next version #}

Step 3: Check the output. Notice how all comments are removed from the final configuration.

Show Expected Output
config system global
    set hostname "FW-TEST-01"
    set timezone "UTC"
    set admin-timeout 480
end

All comments, including the commented-out NTP section, are completely removed from the output.


Summary

In this hands-on guide, you’ve learned:

  1. Basic variable substitution using {{ variable_name }} syntax
  2. Working with JSON objects using dot notation
  3. Accessing array elements using index notation
  4. Handling special characters with bracket notation
  5. Understanding undefined variables and their behavior
  6. Using comments for documentation and debugging

These fundamentals form the foundation for more advanced Jinja features like loops, conditionals, and filters, which you’ll explore in future lessons.

Practice Tips

Jinja Advanced Features

Course Introduction

Welcome to the advanced Jinja templating guide! Building on the basics, we’ll explore conditionals, loops, and filters - the powerful features that make Jinja templates truly dynamic and efficient for FortiGate configuration management.

Chapter 1: Conditionals and Tests

Conditionals allow your templates to make decisions and generate different configurations based on your data.

Basic If Statements

Jinja uses {% if %}, {% elif %}, and {% else %} for conditional logic:

{% if condition %}
    {# Configuration when condition is true #}
{% elif other_condition %}
    {# Alternative configuration #}
{% else %}
    {# Default configuration #}
{% endif %}

Comparison Operators

Hands-On Exercise 1: Version-Based Configuration

Task: Create a template that uses different SSL VPN configurations based on FortiOS version.

JSON Data:

{
  "hostname": "FW-BRANCH-01",
  "fortios_version": 7.4,
  "ssl_vpn_port": 8443,
  "users": [
    "alice",
    "bob",
    "charlie"
  ]
}

Expected Output for FortiOS 7.4:

config system global
    set hostname "FW-BRANCH-01"
end

config vpn ssl settings
    set port 8443
    set tunnel-ip-pools "SSLVPN_TUNNEL_ADDR1"
    set tunnel-ipv6-pools "SSLVPN_TUNNEL_IPv6_ADDR1"
    set source-interface "port1"
end

Expected Output for FortiOS 7.0:

config system global
    set hostname "FW-BRANCH-01"
end

config vpn ssl settings
    set port 8443
    set tunnel-ip-pools "SSLVPN_TUNNEL_ADDR1"
    set source-interface "port1"
end

Your Task: Write a template that includes IPv6 pools only for FortiOS 7.2 and above.

Show Solution
config system global
    set hostname "{{ hostname }}"
end

config vpn ssl settings
    set port {{ ssl_vpn_port }}
    set tunnel-ip-pools "SSLVPN_TUNNEL_ADDR1"
{% if fortios_version >= 7.2 %}
    set tunnel-ipv6-pools "SSLVPN_TUNNEL_IPv6_ADDR1"
{% endif %}
    set source-interface "port1"
end

Using Tests

Tests check properties of variables and return True/False:

Hands-On Exercise 2: Safe Configuration with Tests

Task: Create a template that safely handles optional configuration parameters.

JSON Data:

{
  "hostname": "FW-TEST-01",
  "timezone": "America/New_York",
  "admin_timeout": 480,
  "ntp_servers": [
    "pool.ntp.org",
    "time.google.com"
  ]
}

Your Task: Write a template that:

  1. Only configures timezone if it’s defined
  2. Only configures admin timeout if it’s a number
  3. Only configures NTP if servers list is not empty
Show Solution
config system global
    set hostname "{{ hostname }}"
{% if timezone is defined %}
    set timezone "{{ timezone }}"
{% endif %}
{% if admin_timeout is defined and admin_timeout is number %}
    set admin-timeout {{ admin_timeout }}
{% endif %}
end

{% if ntp_servers is defined and ntp_servers|length > 0 %}
config system ntp
    set ntpsync enable
    config ntpserver
{% for server in ntp_servers %}
        edit {{ loop.index }}
            set server "{{ server }}"
        next
{% endfor %}
    end
end
{% endif %}

Chapter 2: Loops

Loops let you generate repetitive configurations efficiently using arrays and objects in your JSON data.

Basic For Loop

{% for item in collection %}
    {{ item }}
{% endfor %}

Loop Variables

Inside loops, Jinja provides helpful variables:

Hands-On Exercise 3: Multiple Interface Configuration

Task: Generate configuration for multiple interfaces using a loop.

JSON Data:

{
  "interfaces": [
    {
      "name": "port1",
      "ip": "192.168.1.1",
      "netmask": "255.255.255.0",
      "description": "LAN Interface",
      "allowaccess": [
        "ping",
        "https",
        "ssh"
      ]
    },
    {
      "name": "port2",
      "ip": "203.0.113.10",
      "netmask": "255.255.255.252",
      "description": "WAN Interface",
      "allowaccess": [
        "ping"
      ]
    },
    {
      "name": "port3",
      "ip": "10.10.10.1",
      "netmask": "255.255.255.0",
      "description": "DMZ Interface",
      "allowaccess": [
        "ping",
        "https"
      ]
    }
  ]
}

Your Task: Create a template that configures all interfaces.

Show Solution
config system interface
{% for intf in interfaces %}
    edit "{{ intf.name }}"
        set ip {{ intf.ip }} {{ intf.netmask }}
        set description "{{ intf.description }}"
        set allowaccess {{ intf.allowaccess | join(' ') }}
    next
{% endfor %}
end

Looping Over Dictionaries

When your JSON uses objects instead of arrays, you can loop over key-value pairs:

{% for key, value in dictionary.items() %}
    Key: {{ key }}, Value: {{ value }}
{% endfor %}

Hands-On Exercise 4: VLAN Configuration with Dictionary

Task: Configure VLANs using a dictionary structure.

JSON Data:

{
  "vlans": {
    "10": {
      "name": "USERS",
      "description": "User workstations",
      "interface": "port1"
    },
    "20": {
      "name": "SERVERS",
      "description": "Server network",
      "interface": "port1"
    },
    "30": {
      "name": "GUESTS",
      "description": "Guest network",
      "interface": "port1"
    }
  }
}

Expected Output:

config system interface
    edit "USERS"
        set vdom "root"
        set vlanid 10
        set interface "port1"
        set description "User workstations"
    next
    edit "SERVERS"
        set vdom "root"
        set vlanid 20
        set interface "port1"
        set description "Server network"
    next
    edit "GUESTS"
        set vdom "root"
        set vlanid 30
        set interface "port1"
        set description "Guest network"
    next
end
Show Solution
config system interface
{% for vlan_id, vlan_data in vlans.items() %}
    edit "{{ vlan_data.name }}"
        set vdom "root"
        set vlanid {{ vlan_id }}
        set interface "{{ vlan_data.interface }}"
        set description "{{ vlan_data.description }}"
    next
{% endfor %}
end

Loop Filtering

You can filter items during loops using if:

{% for item in collection if item.status == 'active' %}
    {{ item.name }}
{% endfor %}

Hands-On Exercise 5: Conditional Interface Configuration

Task: Configure only interfaces that have IP addresses assigned.

JSON Data:

{
  "interfaces": [
    {
      "name": "port1",
      "ip": "192.168.1.1",
      "netmask": "255.255.255.0",
      "description": "LAN Interface"
    },
    {
      "name": "port2",
      "description": "Unused port"
    },
    {
      "name": "port3",
      "ip": "10.10.10.1",
      "netmask": "255.255.255.0",
      "description": "DMZ Interface"
    }
  ]
}

Your Task: Only configure interfaces that have an ip field defined.

Show Solution
config system interface
{% for intf in interfaces if intf.ip is defined %}
    edit "{{ intf.name }}"
        set ip {{ intf.ip }} {{ intf.netmask }}
        set description "{{ intf.description }}"
    next
{% endfor %}
end

Chapter 3: Filters

Filters transform data in your templates. They’re applied using the pipe | symbol.

Essential Filters

join

Combines array elements into a string:

{{ servers | join(' ') }}

default

Provides fallback values:

{{ timeout | default(300) }}

length

Gets the count of items:

{% if users | length > 10 %}

upper/lower

Changes text case:

{{ hostname | upper }}

replace

{{ "branch-office-firewall" | replace('-', '_') | upper }}
> BRANCH_OFFICE_FIREWALL
Note

FortiSOAR has documentation of all the available Jinja filters here

Hands-On Exercise 6: Firewall Policy with Filters

Task: Use filters to create firewall policies with proper formatting.

JSON Data:

{
  "policies": [
    {
      "name": "allow_web_traffic",
      "srcintf": [
        "port1",
        "port3"
      ],
      "dstintf": [
        "port2"
      ],
      "srcaddr": [
        "internal_users",
        "dmz_servers"
      ],
      "dstaddr": [
        "all"
      ],
      "service": [
        "HTTP",
        "HTTPS"
      ],
      "action": "accept",
      "log": true
    },
    {
      "name": "allow_dns",
      "srcintf": [
        "port1"
      ],
      "dstintf": [
        "port2"
      ],
      "srcaddr": [
        "internal_users"
      ],
      "dstaddr": [
        "all"
      ],
      "service": [
        "DNS"
      ],
      "action": "accept"
    }
  ]
}

Your Task: Create firewall policies using filters to join arrays and provide defaults.

Show Solution
config firewall policy
{% for policy in policies %}
    edit 0
        set name "{{ policy.name | upper }}"
        set srcintf {{ policy.srcintf | join(' ') }}
        set dstintf {{ policy.dstintf | join(' ') }}
        set srcaddr {{ policy.srcaddr | join(' ') }}
        set dstaddr {{ policy.dstaddr | join(' ') }}
        set service {{ policy.service | join(' ') }}
        set action {{ policy.action }}
{% if policy.log is defined %}
        set logtraffic all
{% endif %}
    next
{% endfor %}
end

Advanced Filters

map

Extracts specific attributes from objects:

{{ interfaces | map(attribute='name') | join(' ') }}

select/reject

Filters items based on conditions:

{{ vlans | selectattr('active', 'equalto', true) }}

groupby

Groups items by an attribute:

{% for status, interfaces in all_interfaces | groupby('status') %}

Chaining Filters

You can combine multiple filters:

{{ servers | map(attribute='name') | join(', ') }}

Summary

You’ve learned to use Jinja’s most powerful features:

Conditionals:

Loops:

Filters:

Next Steps

Practice combining these features to create sophisticated FortiGate configurations. Start simple and gradually add complexity as you become more comfortable with the syntax and concepts.

Using Jinja in FortiSOAR

Now that you have the foundations of Jinja down, lets transition into applying that knowledge to SOAR. We’ll cover FortiSOAR specific Jinja expressions, the Jinja Expression Helper, as well as how to use Jinja in playbooks

Understanding Variables in FortiSOAR

All data in SOAR is accessed using the vars key with Jinja2 templating. Variables are accessed using the pattern:

{{vars.<variable_name>}}

The vars object contains all available data during playbook execution, including:


Hands-On Practice 1: Creating and Using Variables

Objective

Learn to create variables and reference them in subsequent steps using Jinja2 templating.

Steps

1. Create Your Test Playbook

  1. Navigate to Automation > Playbooks
  2. Create a new collection called 00 - Jinja Practice
  3. Create a new playbook in your workshop collection:
  4. Click Create

2. Configure the Start Step

  1. Select Reference trigger (this allows the playbook to be called from other playbooks)
  2. Click Save

3. Create Your First Variables

  1. Drag to create a new step → Select Set Variable
  2. Configure the step:
  3. Click Save

4. Test Your Variables

  1. Click Save Playbook
  2. Click the Play button to run the playbook
  3. Click Trigger Playbook to execute
  4. In the execution history, click on the Set Personal Info step
  5. Verify you can see all three variables with their values img.png img.png
  6. Click Close to exit execution history

5. Use Variables to Create New Data

  1. In the playbook editor, drag from Set Personal Info to create a new step
  2. Select Set Variable again
  3. Configure the step:
Tip

Using Dynamic Values: Instead of typing Jinja2 manually, you can see the Dynamic Values page by clicking into any field in SOAR. You’ll see your previously created variables at the top of the panel. Clicking these variables will automatically add the jinja to reference that value to your step. This helps prevent typos in variable names. img.png img.png

6. Create a Template Message

  1. Add another variable to the same step:
  2. Click Save

7. Test the Complete Workflow

  1. Click Save Playbook
  2. Run the playbook again
  3. Check the Create Full Profile step results
  4. Verify that:
Warning

Common Mistake: Variable names are case-sensitive and must match exactly. first_name ≠ First_Name. Use the Dynamic Values panel to avoid typos!

8. Add Code Snippet Processing

  1. From Create Full Profile, drag to create a new step → Select Code Snippet

  2. Configure the step:

  3. Click Save

9. Test Code Snippet Output

  1. Save and run the playbook
  2. Check the Process Name Data step execution results
  3. Expand the output data structure
  4. Note the JSON structure of the output - this shows you the data paths available img.png img.png
Info

Understanding Step Output: Code snippets return data in the data variable. This becomes accessible as {{vars.steps.Process_Name_Data.data.code_output.processed_name}} or {{vars.steps.Process_Name_Data.data.code_output.employee_summary}} in subsequent steps.

10. Access Code Snippet Results

Tip

Use the Dynamic values page to pick the results from the code snippet step. When clicking inside the Value, check the box Show Last Run Result if available, then expand the arrows showing the previous data. Clicking on the key employee_summary, fills in the Jinja for you img.png img.png

img.png img.png

  1. Add another Set Variable step after the code snippet
  2. Configure:

11. Verify Data Path Access

  1. Save and run the complete playbook
  2. In the execution results, click on Use Code Results step
  3. Verify that all variables contain the processed data from the code snippet
  4. If any variables are empty, check the execution results of Process Name Data to verify the exact JSON path structure

12. Correct Issues

The jinja for name_stats has been purposefully set to be incorrect. Try to find the issues with each jinja expression and save the playbook and rerun to check the results

Tip

Data Path Discovery: Always check the execution results of code snippet steps to see the exact structure of returned data. The JSON structure shown determines how you access the data with {{vars.steps.Step_Name.field_name}}.


Understanding Input Data

When triggering playbooks manually from records (alerts, incidents, etc.), all record data is accessible via vars.input.records. Since records is a list, you typically access data using:

{{vars.input.records[0].<field_name>}}

Hands-On Practice 2: Working with Alert Data

Objective

Learn to access and manipulate data from FortiSOAR records using manual triggers.

Steps

1. Create Alert Data Playbook

  1. Create a new playbook:

2. Configure Manual Trigger

  1. Select Manual trigger
  2. Configure:
  3. Click Save

3. Extract Alert Data

  1. Drag from the start step and click Set Variable
  2. Use the Step Name: `Extract Alert Data``
  3. Save the Step
  4. Save the playbook
    Warning

    If you don’t save the playbook, you won’t be able to see the playbook from the alerts page

4. Create a Test Alert

  1. Navigate to Incident Response > Alerts
  2. Click + Add
  3. Fill in basic information:
  4. Click Save img.png img.png

5. Test Playbook from Alert

  1. Click Execute and select Test Data Access - Jinja img.png img.png
  2. View the execution history at the top right img.png img.png
  3. Click on the Extract Alert Data step
  4. Verify all variables contain the correct data from your alert img.png img.png

6. Explore Available Data

  1. In the execution history, click ENV at the top img.png img.png
  2. Expand vars > input > records > [0] img.png img.png
  3. Scroll through and explore all available fields
  4. Notice how the name field matches your alert name
Info

Data Exploration: The ENV view shows all data available during playbook execution. This is invaluable for understanding what fields are available and how to access them with Jinja2.

You should now understand why we could access the alert data fro the playbook with vars.input.records[0].<field_name>


Hands-On Practice 3: Advanced Jinja2 Templating

Objective

Learn advanced Jinja2 techniques for data manipulation and conditional logic.

Steps

1. Create Advanced Templating Playbook

  1. Create a new playbook:

2. Set Up Manual Trigger

  1. Configure Manual trigger on Alerts module
  2. Set button label as 🚀 Advanced Templating

3. Create Data Analysis Step

  1. Add Set Variable step:

Risk Assessment:

Name: risk_level
Value: {% if vars.input.records[0].severity.itemValue == "Critical" %}High Risk{% elif vars.input.records[0].severity.itemValue == "High" %}Medium Risk{% else %}Low Risk{% endif %}

IP Classification:

Name: ip_classification
Value: {% if vars.input.records[0].sourceIp.startswith('10.') %}Internal Network{% elif vars.input.records[0].sourceIp.startswith('192.168.') %}Private Network{% else %}External Network{% endif %}

Alert Summary:

Name: alert_summary
Value: Alert "{{vars.input.records[0].name}}" from {{vars.input.records[0].sourceIp | default('Unknown IP')}} is classified as {{vars.steps.Analyze_Alert_Data.risk_level}} due to {{vars.input.records[0].severity | lower}} severity.

4. Add Conditional Processing

  1. Create new step → Select Decision
  2. Configure:

5. High Risk Path

  1. From True output, add Set Variable step:

6. Low Risk Path

  1. From False output, add Set Variable step:

String Manipulation

<!-- Case conversion -->
{{hostname | upper}}
{{alert_type | lower}}
{{description | title}}

<!-- String operations -->
{% if "DC" in hostname %}
Domain Controller Detected
{% endif %}

<!-- Clean and format data -->
Clean IP: {{ip_address | trim | replace(' ', '')}}
Formatted Name: {{alert_name | truncate(50) | title}}

Debugging Jinja2 Templates

Common Issues and Solutions

1. Variable Not Found Errors

Problem: UndefinedError: 'dict object' has no attribute 'fieldname'

Solution: Use default filters and check data structure

<!-- Instead of -->
{{vars.input.records[0].nonexistent_field}}

<!-- Use -->
{{vars.input.records[0].nonexistent_field | default('Not Available')}}

2. Type Conversion Issues

Problem: Comparing strings to numbers

Solution: Convert types explicitly

<!-- Convert to string for comparison -->
{% if port | string == '80' %}

<!-- Convert to integer for math -->
{% if severity_score | int > 7 %}

3. List Access Errors

Problem: Trying to access list items that don’t exist

Solution: Check list length first

{% if vars.input.records and vars.input.records | length > 0 %}
{{vars.input.records[0].name}}
{% endif %}

Testing Techniques

1. Use Set Variable Steps for Testing

Create temporary Set Variable steps to test your Jinja2 expressions:

Name: test_output
Value: {{your_complex_jinja_expression}}

2. Progressive Building

Start simple and add complexity:

<!-- Step 1: Basic access -->
{{vars.input.records[0].name}}

<!-- Step 2: Add conditions -->
{% if vars.input.records[0].name %}{{vars.input.records[0].name}}{% endif %}

<!-- Step 3: Add formatting -->
Alert: {{vars.input.records[0].name | upper | truncate(30)}}

3. Use ENV View

Always check the ENV view in execution history to understand available data structure.

ZTP Overview

Zero Touch Provisioning (ZTP) is an automated deployment and configuration technique used by Fortinet’s FortiGate firewalls and FortiManager central management platform. ZTP streamlines network security infrastructure deployment, allowing for automatic provisioning and configuration of new devices upon network connection, reducing manual setup, and minimizing misconfiguration risks. FortiManager acts as a central control point, storing configuration templates, policies, and security profiles for FortiGate devices, which ZTP then uses to maintain consistent security policies and simplify network security device deployment and management. This enhances network security while saving time and effort on setup and maintenance.

ZTP Flow Diagram ZTP Flow Diagram

Objectives

Time to Complete

Estimated: 45 Minutes

Understand ZTP Phases

Now that you have a foundational grasp of Zero Touch Provisioning (ZTP) and are familiar with the purpose and location of the FortiSOAR ZTP modules, let’s delve into a more detailed discussion of the ZTP Phases. When implementing ZTP, there are distinct categories of tasks that must be accomplished. These tasks are organized into specific phases. ztp phases ztp phases


The ZTP Phases within FortiSOAR outline what steps a ZTP Profile will take when provisioning a device. The ZTP Phases are as follows:

  1. Authorization

  2. Device Metadata

  3. Device Groups

  4. Execution of Linked Scripts

  5. Installation of Device Configuration

  6. Installation of Policy Package

  7. Completion

These ZTP phases are essential components of the ZTP framework, ensuring a well-structured and methodical approach to device provisioning and configuration management.

FortiSOAR FortiManager Modules

In this section we’ll explore the FortiManager ZTP modules on the system. Click around and checkout the record types inside the group. Feel free to click: Add button Add button to see what fields are used for records of the different modules. You can create records and delete records to just get a feel for the system if you like.


The FortiManager Group is where we will find records that are used for FortiManager Zero Touch Provisioning. We have provided a breakdown of what each module’s purpose is.

FortiManager dropdown FortiManager dropdown

Module NameDescription
ManagersManager records define the FortiManagers, and the current firmware and status, used in your solution. Use this module to start the integration and/or simply report on the current status of your FortiManagers.
DevicesDevices are synchronized from the FortiManager and stored in FortiSOAR. Then automation can occur on one or more devices based on operations and workflow needs. Once created in FortiSOAR they are not removed to preserve the last known status of the Device record even if the device has been removed, or moved, from your FortiManager.
Metafield TemplatesManaging metafields is crucial to the success of any network deployment and provisioning. Metafield Templates can be used to prompt required users to respond to unknown, but required, fields before deployment. Templates can be used to integrate external systems and retrieve key data requirements before deployment. Templates can be customized to create dynamic metadata based on a wide range of advanced and complex requirements. Metadata inside FortiSOAR can be exported to FortiManager to be used by already existing solutions with minimal effort.
Script TemplatesScripts in FortiSOAR for this Solution Pack can be used for creating customized FortiManager CLI, Device DB, Policy DB, and/or TCL Scripts per device. Scripts in FortiSOAR can also maintain and create Provisioning CLI Templates in FortiManager such as when new ADOMs are created and need to be setup with Templates. Device reporting scripts can also be used to create a custom dashboard with user defined content.
ZTP ProfilesThe ZTP Profiles module describes how to handle the provisioning of each device maintained by the respective FortiManager. As a device shows up in the FortiManager Device DB, unauthorized or modeled, when ZTP Profiles are assigned to those devices the defined device templates and provisioning steps are applied and reported per device. ZTP Profiles can be assigned on demand or automatically assigned when devices are created in FortiManager regardless of how the devices were created.

On the Managers Module you will see a clickable navigation menu that can take you to different modules or Dashboards. The navigation is there to reduce the number of clicks to navigate to different modules. The navigation menu is also available on the other modules. navigation bookmark navigation bookmark

Set up ZTP Solution Pack

ZTP Solution Pack Setup Guide

This guide walks you through the complete setup process for the ZTP (Zero Touch Provisioning) Solution Pack, including installation, configuration, and user setup.

Prerequisites

Before beginning, ensure you have administrative access to both FortiSOAR and FortiManager systems.

Step 1: Install the ZTP Framework

  1. Navigate to the Content Hub by clicking the Content Hub tab in the left navigation pane

  2. Search for FortiManager ZTP Flow in the Content Hub

    ZTP Search ZTP Search

  3. Open the Solution Pack and click the Install button

  4. Wait for the installation to complete

Note

Important configuration steps are required before the ZTP Solution Pack can be used effectively.

Step 2: Configure Code Snippet Connector

  1. Ensure Connectors are not filtered out in the Content Hub view

  2. Search for Code Snippet in the Content Hub

    Code Snippet Search Code Snippet Search

  3. Open the connector and check the Mark as Default Configuration checkbox

  4. Click Save

    Code Snippet Configuration Code Snippet Configuration

Step 3: Configure System Settings

Access System Settings

Navigate to System Settings by clicking the gear icon in the top right corner of the FortiSOAR UI.

System Settings System Settings

Configure Playbook Appliance Role

The configuration steps vary depending on your FSR version:

For FSR Version 7.6.0 and Later

  1. In the left sidebar, scroll down and click Access Keys

  2. Select the Appliances tab

    Appliance 7.6.0+ Appliance 7.6.0+

For FSR Version Prior to 7.6.0

  1. In the left sidebar, scroll down and click Appliances

    Appliances Pre-7.6.0 Appliances Pre-7.6.0

Complete Appliance Configuration

Note: These steps apply to all FSR versions after accessing the appropriate section above.

  1. Click on the Playbook record (click the record name, not the checkbox)

  2. In the Roles table, check the box for FortiManager-Playbook-Appliance

    Playbook Role Configuration Playbook Role Configuration

  3. Click Save at the bottom of the page

Step 4: Configure User Roles and Access

Set Up CS Admin User Role

  1. In the left sidebar, scroll down and click Users

    Users Users

  2. Click the CS Admin record (click the record name, not the checkbox)

  3. In the Roles table, check the box for “FortiManager-Admin”

    CS Admin Roles CS Admin Roles

  4. Click Save at the bottom

Switch to CS Admin Account

  1. Log out of the current FortiSOAR session

    Logout FortiSOAR Logout FortiSOAR

  2. Log back into FortiSOAR using the CS Admin credentials:

  3. Verify the setup by confirming you can see the FortiManager section in the bottom left navigation pane

    FortiManager Navigation FortiManager Navigation

Step 5: Configure FortiManager API Access

Create FortiManager API Account

  1. Access the FortiManager UI using the IP address from your Evoke instance

  2. Log in with the following credentials:

  3. Open the integrated SSH terminal to proceed with API account configuration

    ssh_terminal ssh_terminal

  4. Run the following command to create a new API Admin (Use the copy button below)

    config system admin user
        edit "fortisoar"
            set password fortinet
            set profileid "Super_User"
            set avatar "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"
            set rpc-permit read-write
        next
    end
    end
  5. Verify the new user was created by navigating to System Settings > Administrators and checking for the new user

FortiSOAR User FortiSOAR User

Create FortiManager Record

We will begin configuring FortiSOAR to connect to FortiManager, which allows us to manage FortiManager.


  1. Navigate to FortiManager > Managers and click the Add button Add button button to add a new record. Add FortiManager Record Add FortiManager Record

  2. Enter the following information:

    Create FortiManager Record Create FortiManager Record

  3. Click Save.


You will now see the FortiManager record populated with information from the FortiManager API. FortiSOAR automatically created the connector configuration for us and retrieved details about the device

FortiManager Record FortiManager Record

FMG RPC Connector FMG RPC Connector

Note

The connector could automatically be configured because the fortisoar user was already present on FortiManager. Normally you would need to create an api user with rest api permissions

Bonus Points 💸

Investigate the playbook that triggered when we created the FortiManager record. What did it do?

Create a ZTP profile

In this section we’ll create a ZTP profile that will be used to configure the FortiGate when it shows up in FortiManager.


Create ZTP Profile

  1. Navigate to FortiManager > ZTP Profiles and click the Add button Add button button to add a new record.

  2. Set the following fields (leave the rest as default):

    Note

    This ZTP profile is saying that it will be manually assigned to devices that have a name that matches the regex Branch[1-2] . It will move those matched devices to the root adom, add the devices to the device group Branch_Devices and install the policy package Golden_Branch to the device automatically.

    Note

    You will create a Policy Package with a policy later on. But if you didn’t, FortiSOAR would automatically create an empty Policy Package for you.

  3. Click Save.

Congrats! You made your first ZTP profile. But usually Zero touch configurations need more configuration than just a policy package and device group, so lets see what it takes to set that up. ZTP Profile ZTP Profile

Note

Keep in mind, we set the ZTP profile mode to Manual. We will change this later on.


Create Metafield Template Record

  1. Scroll down to the Related Records tab, click the Metafield Templates sub-tab, and click the Add button Add button button on the Metafield Template section add a new record.

    Add metafield template Add metafield template

  2. Set the following fields on the popup (leave the rest as default):

    {
      "contact_email": "socuser1@financial.local",
      "admin_user_name": "se_admin",
      "admin_timeout": "120",
      "loopback0_ip": "" 
    }
  3. Click Create.

  4. You may need to click the refresh button underneath the metafield section if you don’t see the new template there. The refresh only affects visibility, not functionality of the template.

Refresh metafield Refresh metafield

You will now see a new metafield template added and linked to the ZTP profile. This Metafield template will be used to populate the metafields for the Script Templates, or be used for overrides on the ZTP profile.

Note

The metafields are used to pass variables to the scripts. The metafields are referenced in the scripts using the following syntax: {{devmeta.metafield_name}}


Create Script Records

Create Script for Purging Config

  1. Scroll down to the Related Records tab, click the Scripts sub-tab, and click the Add button Add button button on the Scripts section add a new record. Add ZTP Profile Script Add ZTP Profile Script

  2. Set the following fields on the popup (leave the rest as default):

  3. Click Create.

Note

The order priority is used to determine the order in which the scripts are executed. The lower the number, the higher the priority. The purge script needs to be executed before the other scripts, so we set the order priority to 90.

Create Script for an Admin Account

  1. Scroll down to the Related Records tab, click the Scripts sub-tab, and click the Add button Add button button on the Scripts section add a new record. Add ZTP Profile Script Add ZTP Profile Script

  2. Set the following fields on the popup (leave the rest as default):

  3. Click Create.

You will now see 2 scripts added and linked to the ZTP profile. This script will be executed on the FortiGate along with other scripts you create based on the order priority. CLI Script ZTP Profile CLI Script ZTP Profile

Onboard a FortiGate

In this section we’ll onboard a FortiGate manually so that it checks into FortiManager. Onboarding a device to FortiManager can be done automatically using various methods (DHCP option, FortiZTP, FortiDeploy SKU), but we’ll do it manually for this lab.


Warning

Do not Authorize Branch1 during this process. We will do that later.

Onboard a FortiGate

  1. Login to Branch1 using admin/$3curityFabric
  2. Navigate to Security Fabric > Fabric Connectors.
  3. Click Central Management
  4. Click Accept so that the Fortigate trusts the Fortimanager Serial
  5. Click OK
  6. Click Close img.png img.png

Authorize FMG Authorize FMG

Confirm FortiGate is unauthorized in FortiManager

  1. Login to FortiManager using admin/$3curityFabric
  2. Navigate to Device Manager > Unauthorized Devices
  3. Confirm that the Branch1 FortiGate is listed
Warning

Do not Authorize Branch1 here. Our ZTP profile will do this for us.

Unauthorized branch1 Unauthorized branch1

Create Policy Package

  1. Navigate to Policy & Objects > Policy Package
  2. Select Policy Package and click New create_policy_package create_policy_package
  3. Type in Golden_Branch for the Name and click OK at the bottom of the page.

Create Policy

  1. Select Policy Packages > Golden_Branch > Firewall Policy golden_branch golden_branch
  2. Click Create New > Create New to create a new policy create_new_policy create_new_policy
  3. Set the following fields on the Create New Firewall Policy page (leave the rest as default):
  4. Click OK at the bottom of the page.

You now have your first policy! new_policy new_policy

Assign a ZTP Profile Manually

We’re now ready to assign a profile to the FortiGate. Now you might be thinking “Wait, I thought we were doing ZTP?”. We are, but there are some cases where you may need to restart the ZTP process. For example, if you need to change the ZTP profile, or if you need to re-onboard the device. In this case, we’ll assign the profile manually.


Assign a ZTP Profile Manually

  1. Login to FortiSOAR using csadmin/$3curityFabric
  2. Navigate to the module FortiManager > Devices
  3. Click Synchronize All FMG DeviceDb’s Sync device dbs Sync device dbs
  4. You will now see Branch1 listed in the table SOAR new device SOAR new device
  5. Select the checkbox on that device and click Change ZTP Profile > Branch ZTP Profile Assign Profile Assign Profile
  6. Switch back to FortiManager and you will see that the FortiGate is being authorized Authorizing branch1 Authorizing branch1
  7. Back on FortiSOAR the device will also show that the device is being managed Managed Branch1 Managed Branch1

Continuing the ZTP Process

When provisioning devices, there are often unique values that each device needs. For example, the device hostname, the device’s IP address, etc. These values are often unique to each device, so we need a way to provide these values to the device. We can do this using a Manual Input in FortiSOAR. This input dialog can also be emailed to a user

  1. Open the Branch1 Record by clicking on any non-hyperlinked part of the row (e.g. Manager or ZTP Profile Column values)
  2. Notice that the ZTP Phase is now Pending, and that the loopback0_ip variable is Yellow and doesn’t have a value. This means that the variable is not set. Branch1 pending Branch1 pending
  3. Exit the Device record view by clicking the X in the top left corner of the page
  4. Click the Pending Tasks button in the top right corner of the page (Looks like a clipboard with a checkmark) pending task icon pending task icon
  5. Click the pending task Fill out the empty device variables ( Branch1(FortiManager) ) Fill variable task branch1 Fill variable task branch1
  6. Type in 172.16.1.1 in the loopback0_ip field Fill in loopback Fill in loopback
  7. Click Continue

Watch ZTP in Action

  1. Open the Branch1 Record by clicking on any non-hyperlinked part of the row (e.g. Manager or ZTP Profile Column values)
  2. Notice that the loopback0_ip variable is now green and has a value. This means that the variable is set.

This process will take 1-2 minutes. You can watch the progress in the Workspace tab of the Device record. You will see the comments from the ZTP process. This is a great way to see what happened during the ZTP process. It will show rendered script templates from the Device Metadata. branch1 complete branch1 complete

Making ZTP "Zero Touch"

So far there has been a lot of touch! But we’re very close to zero now. In this section we’ll see how to make the ZTP process truly zero touch.


Modify the ZTP Profile to be Automatic

  1. Navigate to FortiManager > ZTP Profiles and edit the Branch ZTP Profile
  2. At the bottom right of the record, click Edit Record
  3. Change the Assignment Mode field from Manual to Automatic
  4. Click Save

Now the next time a device is created on FortiSOAR, the profile will be assigned automatically without manual intervention.

Set ZTP profile to automatic Set ZTP profile to automatic


Schedule the Device Synchronization

  1. Navigate to Automation > Schedules

    Schedule Management Schedule Management

  2. Click Create New Schedule

  3. Fill out the schedule with the following details:

  4. Click Save

This will automatically pull in new unauthorized devices every 5 minutes, eliminating the need for manual synchronization.


Onboard Branch2

  1. Login to the Branch2 FortiGate using the web interface
  2. Follow the steps outlined here to register the FortiGate to FortiManager
  3. The device will appear as “Unauthorized” in FortiManager

Watch the Automation in Action

Now you can observe the Branch2 device being automatically:

The entire process should complete without any manual intervention, achieving true zero-touch provisioning.

Try out ZTP Feature Examples

In this chapter, we learned about Zero Touch Provisioning (ZTP) in FortiSOAR. We installed the ZTP Framework, created a ZTP Profile, and ZTP’d a FortiGate. However, we only touched on a fraction of the features and capabilities of the ZTP Framework. To get a glimpse at the full potential we’ll install the ZTP Examples Solution Pack.

Install the ZTP Framework

  1. Go to the Content Hub by clicking the Content Hub tab in the left pane
  2. Search for FortiManager ZTP Flow - Feature Examples in the Content Hub ztp_search ztp_search
  3. Open the Solution Pack and click the Install button

Check out the ZTP Examples

  1. Navigate to Managers > ZTP Profiles in the left pane
  2. Verify that you see the new ZTP Profiles ztp_profiles ztp_profiles
    Tip

    The purpose of each ZTP Profile is to demonstrate different features and capabilities of the ZTP Framework.

  3. Click on any of the ZTP Profiles to see the details ztp_profile_details ztp_profile_details
    Note

    This profile will:

    • Authorize the FortiGate to the FortiManager and then ZTP the FortiGate.
    • Create a site_id variable
    • Prompt you to input a value for the variable
    • Update the variable on the FortiManager’s Device DB

Try it out

  1. Navigate to FortiManager > Managers in the left pane
  2. Check the box next to your FortiManager record
  3. Click Execute and select Device Model - Create Randomly execute_ztp_example execute_ztp_example
  4. Provide 101-118 to the Range of devices input field provide_models provide_models
  5. Click Submit
  6. Locate the ZTP quicklink section at the top left of any FortiManager subpage and click ZTP Profile and Phase for Devices ztp_quicklink ztp_quicklink
  7. Verify that you see the new devices (may take ~30 seconds) new_devices new_devices
  8. Watch as the devices get assigned to the different ZTP profiles
  9. You can also see click the ZTP Profile and Phase Summary quicklink to see the ZTP phase for each device ztp_profiles ztp_profiles
    Note

    Model devices in FortiManager cannot have a Policy Package pushed to them, so not everything in the ZTP flow can be demonstrated here.

If you check FortiManager, you will also see the model devices in the Devices & Groups section. devices_groups devices_groups

Wrap Up

In this chapter, we learned about Zero Touch Provisioning (ZTP) in FortiSOAR. We installed the ZTP Framework, created a ZTP Profile, and ZTP’d a FortiGate. We also installed and used the ZTP Examples Solution Pack.

Takeaways

This framework is a powerful tool for automating the deployment and configuration of FortiGate firewalls. It can save time and effort on setup and maintenance, and be sold as a solution to customers.

It’s important to remember that this solution pack was fully created by CSE’s, without the need for any product backend changes or NFR’s. This is a great example of how FortiSOAR can be used to create completely custom solutions because of its flexible nature. These solutions are easily shareable and can be installed by anyone with access to the Content Hub.

NetShot

img.png img.png

Overview

Network device maintainers often need to audit and validate that devices have the configurations they should have, and receive alerts when discrepancies occur. The Netshot SP addresses this need by providing a comprehensive framework for defining how to obtain, normalize, analyze, and alert on network anomalies.

Netshot helps you gather network data using any method and format. Raw network data is then normalized to simplify reporting and identify unexpected anomalies and exceptions to defined rules.

Key Capabilities

Data Collection: Netshot supports multiple collection methods including SSH, API calls, NMAP scans, and custom scripts. Data can be stored in various formats such as text, JSON, YAML, or CSV.

Data Normalization: Raw network data is converted into normalized formats (text, text lists, JSON, configuration blocks, or custom formats) to simplify reporting and analysis.

Rule-Based Auditing: Normalized data is used in report outputs that can audit configurations based on predefined rules or compare against previously obtained data to detect changes.

Exception Alerting: Report outputs include exception status indicators that flag when something is not as expected or desired from the network.

Use Cases

Configuration Auditing: Collect device configurations as text files, normalize them into configuration blocks, and audit against compliance rules with importance scoring.

Status Monitoring: Gather device status information such as ARP tables, BGP peers, and VPN statistics, then audit for operational anomalies.

Change Detection: When network data is collected multiple times, compare current and previous data to identify unexpected changes.

Real-World Application

A practical example involved a customer with 100+ manual audit checks required before and after device upgrades. Using Netshot, they automated the validation process to quickly determine if unexpected changes occurred during upgrades that could indicate outage-causing events.

This automation transformed a time-intensive manual process into an efficient, reliable system for maintaining network integrity and preventing service disruptions.

Setup

Install Solution Pack

Download Solution Pack

Download the Solution Pack file below

Download me

Install Solution Pack

  1. Login to FortiSOAR

  2. Navigate to the Content Hub img_1.png img_1.png

  3. Select the Manage Tab

  4. Click Upload> Upload Solution Pack img_1.png img_1.png

  5. Select the File you downloaded

  6. Click Install

    img.png img.png

  7. Click Confirm

  8. Wait for the SP to finish installing. Should take ~2 minutes

Configure Code Snippet

We need to add a default configuration to the code snippet connector to make sure the playbooks have a valid one to use.

  1. Search for Code Snippet img_2.png img_2.png
  2. Click anywhere on the connector to edit the configuration.
  3. Click the checkbox’s for both Mark as Default Configuration and Allow Universal Imports img_2.png img_2.png
    Warning

    These settings are critical to Netshot working properly.

  4. Click Save

Add Permissions for the new Modules

  1. Navigate to System Settings > Roles img_1.png img_1.png

  2. Open Full App Permissions img_1.png img_1.png

  3. Click the highlighted + icon shown in the picture below.

    img.png img.png

  4. Click Save

  5. After clicking Save, you should see a new section appear on the left navigation pane

    img_1.png img_1.png

    Tip

    If you don’t see the navigation for Netshot, log out of SOAR and log back in

Download Workshop File

Download the Workshop Example file below

Download me

Import Netshot Workshop Settings

  1. Navigate to System Settings > Import Wizard.
  2. Click Import From File and import the Solution Pack you downloaded.
  3. Select the FortiSOAR-Export-NetshotWorkshop... file img_1.png img_1.png
    Warning

    Make sure you see “X Records..” before proceeding. If you see other options, you likely selected the wrong file.

  4. Click Continue twice img_2.png img_2.png
  5. Click Run Import
  6. Wait around ~1 minute for the import to finish img_1.png img_1.png
  7. Click Done

Configure Target

Update the Fortigate Target

  1. Navigate to Netshot > Targets img.png img.png
  2. Click on FG1 img.png img.png
  3. Click Edit Record at the bottom right img.png img.png
  4. Update the following fields
  5. Click Save at the bottom left

Trigger Netshot from the device

Click the tab Netshot Data, then click the button Run Netshot img_2.png img_2.png

You will notice that the Netshot Status indicator shows Running img_2.png img_2.png

You will also notice which data queries are complete or waiting img_2.png img_2.png

Once netshot completes, you should see a total score that the device earned from the various audits performed. The audit scores come from the profiles that were assigned to the device. img_2.png img_2.png

Investigate the Results

  1. Click on the row under netshot data called get system status
  2. Click on the Source Data tab, and expand the Normalized Data img_3.png img_3.png Source data is the raw data from the query, and normalized data is what the raw data was transformed into. In this case, the data wasn’t modified or cleaned in any way
  3. Scroll down and to the Output Data Reports and click License is Valid img_4.png img_4.png

Notice the settings here. This report is saying that the text field from the normalized data must contain a regex of License Status(\s+)?: Valid . If that Regex Pattern Exists, then the report gives out 25 points img_5.png img_5.png

Understand Domains

Domains allow you to create a grouping of devices that needed audited.

  1. Navigate to Netshot > Domains
  2. Open the Netshot Workshop domain
  3. Select the Targets Tab

Notice that the domain consists of 2 Fortigates and 1 Fortimanager img_5.png img_5.png

Understand Reports

  1. Navigate to the Reports Module img_6.png img_6.png
  2. Click View on the Netshot Report Domain img_6.png img_6.png
  3. Select the Netshot Workshop Domain for the Report Input img_6.png img_6.png
  4. Click OK

Check out the report, There were some exceptions found from the FMG because it did not meet the specified 7.6 version img_6.png img_6.png

Miscellaneous Use Cases

This chapter will cover extra content and use cases that don’t necessarily fit with the other sections

Splunk Integration

Overview

The Splunk connector in FortiSOAR enables powerful SIEM integration capabilities, allowing you to run ad-hoc queries using Splunk Query Language (SPL) and automatically trigger playbooks based on search results. This integration transforms raw log data into actionable security intelligence.

What You’ll Learn

Prerequisites


Lab Environment Setup

For this guide, we’ll use a Splunk Enterprise trial on AWS, which provides:

Quick AWS Deployment

  1. Launch Splunk from AWS Marketplace

  2. Initial Access


Part 1: Configure Splunk Data Ingestion

Step 1: Enable HTTP Event Collector (HEC)

The HTTP Event Collector allows external systems to send data directly to Splunk via REST API.

  1. Navigate to Data Inputs

    Settings → Data Inputs → HTTP Event Collector

    Data Inputs Navigation Data Inputs Navigation

  2. Add New Token HTTP Event Collector HTTP Event Collector

  3. Configure Token Settings

  4. Set Input Settings

  5. Review and Create

Step 3: Test HEC Connectivity

Verify your HEC setup with a simple test:

curl -k "https://<splunk-server>:8088/services/collector" \
-H "Authorization: Splunk <your-hec-token>" \
-d '{"event": "Test event from FortiSOAR setup", "source": "fortisoar_test"}'

Expected response:

{
  "text": "Success",
  "code": 0
}

Part 2: Configure FortiSOAR Splunk Connector

Step 1: Install Splunk Connector

  1. Navigate to Connectors

    Automation → Connectors → Manage
  2. Search and Install

Step 2: Create Splunk Configuration

  1. Connector Settings

    Configuration Name: Splunk-Production
    Server URL: https://your-splunk-server:8089
    Username: your-splunk-username
    Password: your-splunk-password
    Protocol: https
    Splunk API Port: 8089
    Verify SSL: False (unchecked)

    img.png img.png

  2. Click Save, and verify you the Health Check passes


Part 3: Generate Sample Logs

Note

You can skip this step if you already have logs in Splunk

Create a Python script to continuously feed FortiGate logs to Splunk:

Tip

This python could be ran directly from the Code snippet step in fortisoar

Enhanced Log Forwarder Script

#!/usr/bin/env python3
"""
FortiGate to Splunk Log Forwarder
Reads FortiGate logs and forwards them to Splunk HEC
"""

import random
import requests
from datetime import datetime

# Configuration
SPLUNK_HEC_URL = "https://your-splunk-server:8088/services/collector"
SPLUNK_HEC_TOKEN = "your-hec-token-here"

# Device names (simulate 5 FortiGate firewalls)
devices = [f"FG100F-{i:03d}" for i in range(1, 6)]

# Sample BPDU event template
log_template = (
    '<134>date={date} time={time} devname="{devname}" devid="{devid}" '
    'eventtime={eventtime} tz="+0000" logid="0419016384" type="event" '
    'subtype="system" level="alert" vd="root" logdesc="BPDU packet received" '
    'msg="BPDU packet received on {port}, shutting down the port."'
)

# Generate 20 logs
for _ in range(20):
    now = datetime.utcnow()
    devname = random.choice(devices)
    devid = f"{devname}-SN{random.randint(100000, 999999)}"
    port = f"port{random.randint(1, 5)}"

    log = log_template.format(
        date=now.strftime("%Y-%m-%d"),
        time=now.strftime("%H:%M:%S"),
        devname=devname,
        devid=devid,
        eventtime=int(now.timestamp() * 1_000_000),
        port=port
    )
    print(log)

    payload = {
        "event": log,
        "sourcetype": "fortinet:firewall"
    }

    response = requests.post(
        SPLUNK_HEC_URL,
        headers={"Authorization": f"Splunk {SPLUNK_HEC_TOKEN}"},
        json=payload,
        verify=False  # Set to True if you have proper TLS setup
    )

    if response.status_code != 200:
        print(f"Error: {response.status_code} - {response.text}")
    else:
        print(f"Sent log from {devname}")
Warning

Make sure to update your server url and API token in the code snippet to match your environment

Verify logs in splunk

After running the script, lets check that we see logs in Splunk

  1. Navigate to Search and Reporting
  2. Search for sourcetype="fortinet:firewall" img.png img.png

Query Splunk from FortiSOAR

  1. Create a new playbook. Name it BPDU Splunk Query for easy searching later

  2. You can use the Referenced Start step for testing

  3. Drag a new step from the Start, and select Connector

  4. Select Splunk

     index=main sourcetype=fortinet:firewall "BPDU packet received"
     | bin _time span=1m
     | stats count as bpdu_count by devname, _time
     | where bpdu_count > 3
     | stats sum(bpdu_count) as total_bpdu_events by devname
  5. Save the step

  6. Save and run the playbook.

  7. Check the playbook results. If your query worked you will see output like this img.png img.png

Create a Schedule to automate running this playbook

  1. Navigate to Automation > Schedules
  2. Click Create New Schedule at the top right
  3. Click Save img.png img.png

Regenerate Certs

Identify traffic log filter.

Using the browser network debug to identify the syntax of the filter img.png img.png

/api/v2/log/memory/traffic/forward?start=0&rows=500&filter=subtype=*%22forward%22&filter=dstip=@%228.8.8.8%22&filter=_metadata.timestamp%3E=%221750246709000%22

Translated to FMG sys/proxy/json, the data looks like this. Notice the placeholders I’ve left in for dstip and epoch timestamp

{
  "action": "get",
  "resource": "/api/v2/log/memory/traffic/forward?start=0&rows=100&filter=subtype=*%22forward%22&filter=dstip=@%22{{dstip}}%22&filter=_metadata.timestamp%3E=%22{{timestamp}}%22",
  "target": [
    "adom/root/device/Enterprise_Core"
  ]
}
Version: v0.0.0
Last updated: Wed, Jun 18, 2025 08:41:52 PDT
Copyright© 2025 Fortinet, Inc. All rights reserved. Fortinet®, FortiGate®, FortiCare® and FortiGuard®, and certain other marks are registered trademarks of Fortinet, Inc., and other Fortinet names herein may also be registered and/or common law trademarks of Fortinet. All other product or company names may be trademarks of their respective owners. Performance and other metrics contained herein were attained in internal lab tests under ideal conditions, and actual performance and other results may vary. Network variables, different network environments and other conditions may affect performance results. Nothing herein represents any binding commitment by Fortinet, and Fortinet disclaims all warranties, whether express or implied, except to the extent Fortinet enters a binding written contract, signed by Fortinet’s General Counsel, with a purchaser that expressly warrants that the identified product will perform according to certain expressly-identified performance metrics and, in such event, only the specific performance metrics expressly identified in such binding written contract shall be binding on Fortinet. For absolute clarity, any such warranty will be limited to performance in the same ideal conditions as in Fortinet’s internal lab tests. Fortinet disclaims in full any covenants, representations, and guarantees pursuant hereto, whether express or implied. Fortinet reserves the right to change, modify, transfer, or otherwise revise this publication without notice, and the most current version of the publication shall be applicable.