In FortiSOAR il Jinja non è un abbellimento: ogni campo di ogni step di playbook lo accetta, e quello che ci scrivi dentro è il valore che finisce nel record, nella chiamata al connettore o nella condizione di un decision step. Le domande d’esame su questo Task raramente chiedono cosa faccia il filtro X. Chiedono quale filtro produce esattamente quel risultato e di che tipo è il dato che ne esce. Siccome non esiste credito parziale, azzeccare il filtro e sbagliare il tipo vale zero come non aver risposto.

Da dove nasce l’espressione: Dynamic Values e Jinja Editor

Il Jinja non lo digiti in un campo vuoto: clicchi il campo dello step e si apre la finestra Dynamic Values. La scheda Input/Output offre quattro sorgenti. Custom Variables sono le variabili create con lo step Set Variable, e il loro scope è locale: valgono nel playbook che le definisce e nei suoi figli, a qualunque livello di annidamento, non altrove. Input contiene i record del trigger e i parametri. Step Utilities, introdotta nella 7.4.1, espone l’elemento corrente di un loop nella forma vars.item. Step Results contiene l’output degli step già eseguiti.

Due dettagli di Step Results costano punti interi. Il primo: l’output si legge sempre nella forma qui sotto, e se lo step restituisce un array la posizione la indichi tu, con indice che parte da zero. Non c’è nessun «prendi il primo» implicito.

vars.steps.<step_name>.keyname
vars.steps.Find_all_Open_Alerts[0].name
vars.steps.Find_all_Open_Alerts[3].name

Il secondo: l’opzione Show Last Run Result if Available pesca lo schema dall’esecuzione precedente, ma quei dati esistono solo se il playbook è girato in modalità DEBUG.

Per collaudare un’espressione prima di infilarla nel playbook hai due strade. Nella scheda Functions il pulsante Run and Preview Result chiede in quale Evaluation Environment valutare e mostra il risultato. Il Jinja Editor, nel menu Tools del Playbook Designer, affianca template, JSON di input e output con un pulsante Render: dalla 7.6.0 l’ENV caricabile include anche vars.steps, e l’elenco delle esecuzioni recenti si ferma alle ultime 30.

I filtri che compaiono davvero nei playbook

FortiSOAR mescola tre insiemi: i built-in di Jinja, quelli importati da Ansible e una manciata scritta da Fortinet. Sono i terzi a distinguere l’esame. fromIRI risolve un IRI e restituisce l’oggetto, con accesso a punto e annidabile su sé stesso. picklist carica l’item di una picklist ed è ciò che genera l’IRI Lookup. toDict e xml_to_dict trasformano stringhe in dizionari. extract_artifacts tira fuori gli IOC da un testo, parse_cef legge una stringa CEF. ip_range verifica l’appartenenza a un CIDR, counter conta le occorrenze in una lista, resolveRange mappa un valore su un dizionario per corrispondenza esatta o per intervallo, loadRelationships recupera un record correlato.

{{ (vars.person_iri | fromIRI).name }}
{{ ((vars.event.alert | fromIRI).owner | fromIRI).name }}
{{ "AlertType" | picklist("Phishing", "@id") }}
{{ vars.ip | ip_range('198.162.0.0/24') }}

Prima trappola, e non è teorica: readfile è documentato ma bloccato di serie. Il parametro DISALLOWED_JINJA_FILTERS in /opt/cyops-workflow/sealab/sealab/config.ini contiene già readfile. Un playbook che lo usa fallisce su un sistema non modificato, e la causa non è nel playbook.

L’auto-conversion: la trappola che costa il punto intero

Il playbook engine tenta di dedurre il tipo di ogni variabile dal suo valore, ogni volta che la variabile viene referenziata. Senza questo comportamento tutto sarebbe stringa. Con questo comportamento, un filtro che tu credi produca una stringa può restituire altro. L’esempio della documentazione è il più chiaro possibile: con data = [1,2,3], l’espressione join restituisce di nuovo la lista [1,2,3]. Per ottenere la stringa devi racchiudere l’intera espressione fra virgolette.

{{ vars.data | join(',') }}
"{{ vars.data | join(',') }}"

Stessa famiglia di problemi altrove. round restituisce sempre un float, anche con precisione zero: se ti serve un intero devi concatenare int. int e float non sollevano errori quando la conversione fallisce, restituiscono 0 e 0.0 in silenzio, a meno che tu non passi un default diverso. default non scatta sui valori falsi, come la stringa vuota, se non gli passi true come secondo argomento. E i filtri che restituiscono un iteratore, come map, select, selectattr, permutations, combinations e zip, vanno chiusi con list o non vedrai i dati.

{{ 42.55 | round | int }}
{{ '' | default('la stringa era vuota', true) }}
{{ users | map(attribute='username') | list }}

Nei decision step vale una regola a parte: l’espressione restituisce la stringa ‘True’ o ‘False’, che viene convertita in un booleano vero, e l’interfaccia avanzata non vuole le doppie parentesi graffe.

Le trappole che non lasciano traccia nei log

Tutti i filtri sono case-sensitive, e dalla 7.2.0 il filtro picklist pretende la corrispondenza esatta anche nel valore: se la severity reale è Medium e tu scrivi MEDIUM, il playbook fallisce. Un blocco if senza else che restituisce stringa vuota fa fallire lo step quando il campo di destinazione ha un tipo: aggiungi sempre un ramo else. Dalla 7.0.0 l’attributo timestamp della libreria arrow è diventato int_timestamp, e i playbook nuovi devono usare quello. Dalla 7.4.0 il sandbox blocca l’accesso ai membri privati, con errore CS-WF-7: __setitem__ va riscritto come update. Infine, nei template email funzionano solo le espressioni Jinja: i filtri Ansible lì dentro non lavorano, quindi niente join o combine nel corpo di una notifica.