Tutto quello che vedi nella UI di FortiSOAR passa dalla stessa API REST che puoi chiamare tu: l’interfaccia è solo un client. Serve per far entrare gli alert da un SIEM, per chiudere un incident da uno script, e per capire perché una chiamata che «dovrebbe funzionare» restituisce trenta record invece di tutti.
La mappa: /api/3, JSON-LD e Hydra
L’API è organizzata attorno ai moduli: ogni modulo ha una collezione, e ogni modulo nuovo creato col Module Editor genera automaticamente il proprio endpoint. La radice risponde solo autenticati.
https://FortiSOAR_ROOT.URL/api/3
/api/3/alerts
Il formato è JSON-LD combinato con la specifica Hydra, e non è un dettaglio accademico: la risposta si descrive da sola. La lista arriva come collezione paginata, coi record dentro l’array hydra:member, e ognuno porta in testa la chiave @id, che è un IRI: il percorso relativo del record, non un numero.
"@id": "/api/3/alerts/028b37fa-bb35-4e3b-8afb-a3274a8cb343"
Da qui la regola che fa perdere il punto: in POST non si manda @id. L’IRI lo genera il sistema; se vuoi imporre tu l’identificativo passi la chiave uuid con un UUID di 36 caratteri, che deve essere diverso da qualunque altro o la chiamata fallisce. Le chiavi seguono la convenzione camelCase. Dal 7.0.0 la paginazione è cambiata: hydra:itemsPerPage, hydra:firstPage e hydra:lastPage non ci sono più, sostituite da hydra:view.
I quattro verbi, e i codici che rispondono
La mappatura è la solita — GET legge, POST crea, PUT aggiorna, DELETE cancella — ma non tutti gli endpoint accettano tutti i metodi. Le risposte da ricordare: POST riuscito dà 201, PUT dà 200 col record aggiornato, DELETE dà 204 No Content, quindi senza corpo.
Le operazioni massive hanno rotte proprie e verbi propri, ed è qui che si sbaglia:
POST /api/3/insert/alerts BODY: { "data": [ {...}, {...} ] }
DELETE /api/3/delete/alerts BODY: { "ids": ["uuid1", "uuid2"] }
PUT /api/3/update/alerts BODY: { "data": [ {...} ] }
POST /api/3/upsert/alerts (singolo)
PUT /api/3/bulkupsert/alerts (massivo)
Il codice che distingue le risposte è 207: tutto riuscito dà 200, riuscito in parte dà 207 con l’elenco di ciò che non è passato. Sull’upsert c’è un prerequisito nascosto: funziona solo se nel Module Editor hai definito l’unicità del record per quel modulo. Senza unicità l’upsert inserisce sempre e non aggiorna mai, senza avvisarti. Il lotto consigliato è di 200 record piccoli come asset o indicator, 100 per alert e incident con sourcedata pesanti.
Le relazioni non si gestiscono con una POST sulla relazione: si fa una PUT sul record padre usando le chiavi riservate __link e __unlink.
PUT /api/3/incidents/bbdc13f8-015f-4615-8c63-4cebb6ec991b
{"__link": ["/api/3/assets/5434a6e1-e9cd-4ecf-ba6b-f8ac82c3effc"]}
Filtrare senza portarsi via tutto il database
Il parametro più importante è quello che nessuno scrive: il limite predefinito è 30 record. Se la tua integrazione «perde» risultati, guarda lì prima di cercare bug. I parametri operativi iniziano col simbolo di dollaro, i filtri no; gli operatori si attaccano al nome del campo col dollaro, e i sotto-campi si raggiungono col doppio underscore.
/api/3/alerts?status__itemValue=Open
/api/3/alerts?eventCount$gte=10&eventCount$lt=20
/api/3/alerts?$limit=100&$orderby=-createDate&$relationships=true
Quando la condizione si complica la query string non basta e si passa alla Query API, in POST, con un oggetto che contiene logic (AND oppure OR), l’array filters e, volendo, sort e aggregates. Gli operatori sono eq, neq, lt, lte, gt, gte, in, nin, contains, like, notlike e isnull; nei pattern di like la percentuale vale zero o più caratteri e l’underscore esattamente uno. La stessa rotta accetta anche una query salvata, richiamata per UUID.
POST /api/query/incidents
GET /api/query/incidents/2e77a714-f0c1-45ca-bd49-b71efbd9328c
Autenticarsi: tre metodi, tre scopi diversi
Il token JWT si ottiene con una POST su /auth/authenticate passando loginid e password, e si usa nell’header come Bearer. È temporaneo — la durata tipica è trenta minuti — e va bene per prove con curl o Postman. Per l’accesso permanente si usa HMAC: crei un’appliance, ottieni una coppia di chiavi pubblica e privata (la privata è mostrata una volta sola) e firmi ogni richiesta. L’identificatore da firmare ha un ordine fisso, e il timestamp deve essere in UTC con gli orologi sincronizzati, altrimenti l’autenticazione fallisce.
ALGO.VERB.TIMESTAMP.FULL_URI.HASHED_PAYLOAD
Dal 7.5.0 c’è anche l’API key, nell’header Authorization col prefisso API-KEY. Validità da 1 a 365 giorni e due limiti da ricordare: non può eseguire le operazioni su /auth e non può creare o aggiornare API key. E la possibilità di rileggerla dipende dalla modalità di retrieval attiva quando la chiave viene creata, non dopo.
Farsi chiamare: il Custom API Endpoint Trigger
Il verso opposto è il Custom API Endpoint Trigger: definisci un nome arbitrario nel playbook e ottieni una rotta che un sistema esterno può invocare.
https://FortiSOAR.ROOT.URL/api/triggers/1/<nome>
https://FortiSOAR.ROOT.URL/api/triggers/1/deferred/<nome>
Tre vincoli valgono il punto intero. Accetta solo POST. Le rotte non sono discoverable: a differenza di quelle standard non compaiono nella definizione JSON-LD/Hydra, quindi il nome va conosciuto. E la variante deferred esiste per le autenticazioni alternative, Basic o nessuna, tipiche dei webhook. Nel playbook i dati arrivano sotto vars.input, coi parametri di query in api_params e il corpo in api_body; se scegli No Authentication, per creare davvero un record serve un passo Set API Keys, altrimenti il playbook parte ma non ha i permessi per scrivere. Il corpo, infine, non è per forza JSON: un XML va parsato con un passo dedicato prima di leggerne i campi.