In questa guida ti mostreremo come utilizzare le API di SMARTFENSE da Google Apps Script per creare un foglio di calcolo dinamico con le informazioni provenienti dai diversi endpoint delle campagne della tua istanza.
Questo approccio non solo ti permetterà di centralizzare e aggiornare i dati in modo automatico, ma anche di preparare le informazioni per integrarle facilmente con strumenti di Business Intelligence, come Looker Studio, e così costruire report e dashboard più completi.
Come primo passo, dovrai registrare una nuova applicazione all'interno della tua istanza per ottenere il Client ID e il Client Secret. Per farlo, accedi a Configurazione > Integrazioni > API.
Premi il pulsante Registra nuova applicazione e completa i seguenti campi:
Nome: assegna un nome descrittivo all'applicazione.
Client ID: questo valore sarà generato automaticamente da SMARTFENSE.
Client Secret: questo valore sarà anch'esso generato automaticamente da SMARTFENSE.
Tipo di client: Pubblico.
Tipo di concessione di autorizzazione: Client credentials.
Ambiti: per questo caso utilizzeremo permessi in sola lettura, che ci permetteranno di consultare l'API in modo sicuro.
Prima di premere il pulsante Salva, assicurati di fare un backup del Client Secret, poiché ti servirà più avanti per configurarlo nel progetto di Google Spreadsheet.
Una volta registrata l'applicazione nella tua istanza, puoi creare un file in Google Spreadsheet. All'interno del file, accedi al menu Estensioni > Apps Script e assegna un nome al progetto.
Successivamente, configureremo due proprietà dello script che saranno utilizzate per validare e autorizzare le connessioni con l'API. Per farlo, accedi a Impostazioni del progetto.
Crea le proprietà SMARTFENSE_CLIENT_ID e SMARTFENSE_CLIENT_SECRET. In ciascuna di esse dovrai inserire il valore ottenuto registrando l'applicazione dalla piattaforma.
Completato questo passaggio inizieremo a creare gli script che negozieranno la connessione alle API. Nell'editor crea i seguenti script:
Config.gs
const SMARTFENSE_CONFIG = {
baseUrl: 'https://nome_istanza.takesecurity.com',
tokenUrl: 'https://nome_istanza.takesecurity.com/oauth/token/',
maxPages: 1000
};Sostituisci nome_istanza con il sottodominio della tua istanza.
Auth.gs
/**
* Legge le credenziali dalle Proprietà dello Script.
*/
function smartfenseGetCredentials_() {
const props = PropertiesService.getScriptProperties();
const clientId = props.getProperty('SMARTFENSE_CLIENT_ID');
const clientSecret = props.getProperty('SMARTFENSE_CLIENT_SECRET');
if (!clientId || !clientSecret) {
throw new Error('Mancano SMARTFENSE_CLIENT_ID o SMARTFENSE_CLIENT_SECRET nelle Proprietà dello Script.');
}
return { clientId, clientSecret };
}
/**
* Ottiene un token di accesso OAuth2 con client_credentials.
*/
function smartfenseGetAccessToken() {
const { clientId, clientSecret } = smartfenseGetCredentials_();
const basicAuth = Utilities.base64Encode(clientId + ':' + clientSecret);
const response = UrlFetchApp.fetch(SMARTFENSE_CONFIG.tokenUrl, {
method: 'post',
contentType: 'application/x-www-form-urlencoded',
payload: 'grant_type=client_credentials',
headers: {
Authorization: 'Basic ' + basicAuth,
Accept: 'application/json',
'Cache-Control': 'no-cache'
},
muteHttpExceptions: true
});
const status = response.getResponseCode();
const body = response.getContentText();
Logger.log('TOKEN STATUS: ' + status);
if (status = 300) {
throw new Error('Errore ottenendo token. HTTP ' + status + ' - ' + body);
}
let json;
try {
json = JSON.parse(body);
} catch (e) {
throw new Error('La risposta del token non è un JSON valido.');
}
if (!json.access_token) {
throw new Error('La risposta non include access_token.');
}
return json.access_token;
}ApiUtils.gs
/**
* Valida che un URL appartenga all'istanza prevista.
*/
function smartfenseValidateUrl_(url) {
if (!url.startsWith(SMARTFENSE_CONFIG.baseUrl + '/')) {
throw new Error('L\'URL non appartiene all\'istanza prevista: ' + url);
}
}
/**
* Esegue una GET autenticata e restituisce il JSON parsato.
*/
function smartfenseGetJson_(url, accessToken) {
smartfenseValidateUrl_(url);
const response = UrlFetchApp.fetch(url, {
method: 'get',
headers: {
Authorization: 'Bearer ' + accessToken,
Accept: 'application/json'
},
muteHttpExceptions: true
});
const status = response.getResponseCode();
const body = response.getContentText();
Logger.log('GET URL: ' + url);
Logger.log('GET STATUS: ' + status);
if (status = 300) {
throw new Error('Errore consultando API. HTTP ' + status + ' - ' + body);
}
try {
return JSON.parse(body);
} catch (e) {
throw new Error('La risposta dell\'API non è un JSON valido.');
}
}
/**
* Scorre tutte le pagine usando il campo next.
* Restituisce una lista consolidata leggendo la collezione indicata.
*
* @param {string} endpointPath Es: '/api/v1/users/campaigns/phishing'
* @param {string} collectionField Es: 'results'
* @returns {Array}
*/
function smartfenseGetAllPages(endpointPath, collectionField) {
const accessToken = smartfenseGetAccessToken();
let nextUrl = SMARTFENSE_CONFIG.baseUrl + endpointPath;
let allItems = [];
let pageCount = 0;
while (nextUrl) {
pageCount++;
if (pageCount SMARTFENSE_CONFIG.maxPages) {
throw new Error('Raggiunto il massimo numero di pagine consentite (' + SMARTFENSE_CONFIG.maxPages + ').');
}
const page = smartfenseGetJson_(nextUrl, accessToken);
const items = Array.isArray(page[collectionField]) ? page[collectionField] : [];
allItems = allItems.concat(items);
nextUrl = page && page.next ? page.next : null;
}
return allItems;
}
/**
* Ottiene o crea un foglio.
*/
function getOrCreateSheet_(spreadsheet, sheetName) {
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName);
}
return sheet;
}
function valueOrEmpty_(value) {
if (value === null || value === undefined) return '';
return value;
}
function stringifyIfNeeded_(value) {
if (value === null || value === undefined) return '';
if (typeof value === 'object') return JSON.stringify(value);
return value;
}I tre script sono commentati per interpretare la loro funzionalità.
Ora è il momento di creare gli script di ciascun endpoint che permetterà di ottenere i dati delle campagne.
Esportazione delle campagne di phishing in Google Sheets
Esportazione delle campagne di ransomware in Google Sheets
Esportazione delle campagne di smishing in Google Sheets
Esportazione delle campagne di moduli interattivi in Google Sheets
Esportazione delle campagne di video in Google Sheets
Esportazione delle campagne di videogiochi in Google Sheets
Esportazione delle campagne di newsletter in Google Sheets
Esportazione delle campagne di esami in Google Sheets
Esportazione delle campagne di sondaggi in Google Sheets
Esportazione delle campagne di phishing in Google Sheets
Obiettivo
Questo script consulta l'endpoint delle campagne di phishing per utente nell'API di SMARTFENSE ed esporta i risultati in un foglio Google Sheets chiamato PHISHING.
Cosa fa questo script?
Lo script automatizza l'intero processo di estrazione e caricamento dati:
si autentica contro l'API di SMARTFENSE,
consulta l'endpoint corrispondente,
scorre tutte le pagine dei risultati,
trasforma la risposta in una struttura tabellare,
e riversa le informazioni in un foglio Google Sheets.
Risultato
All'esecuzione dello script, viene creato o aggiornato un foglio chiamato PHISHING all'interno del foglio di calcolo attivo.
Le informazioni sono organizzate in formato tabellare, pronte per l'analisi e il riutilizzo.
A cosa serve questo foglio?
Il foglio generato funziona come base dati operativa per analisi e visualizzazione.
Il suo scopo è che i dati estratti dall'API possano essere utilizzati successivamente in:
dashboard di Looker Studio,
analisi in Google Sheets,
incroci con altre fonti,
report operativi,
cruscotti di monitoraggio.
In questo schema, lo script svolge la funzione di trasformare le informazioni dell'endpoint in una fonte tabellare pronta per essere consumata da strumenti di reporting.
Fonte dati
Endpoint consultato:
/api/v1/users/campaigns/phishing
Tipo di informazioni esportate
Il foglio include informazioni relative a:
dati dell'utente,
campagne di phishing associate,
attività dell'utente all'interno della campagna,
momenti didattici (teachable moments),
date rilevanti del processo,
e dati tecnici della simulazione.
Considerazioni
Lo script utilizza autenticazione OAuth2.
Riutilizza temporaneamente il token tramite cache.
Scorre automaticamente la paginazione dell'endpoint.
Converte i campi data affinché Google Sheets li riconosca correttamente.
Pulisce e rigenera il foglio ad ogni esecuzione.
In caso di errore, tenta di lasciare il dettaglio riflesso nel foglio stesso.
Codice
Di seguito è incluso lo script commentato per riferimento tecnico.
Phishing.gs
// Configurazione specifica per l'esportazione delle campagne di phishing.
// Centralizza:
// - il percorso dell'endpoint da consultare,
// - il nome del foglio di destinazione,
// - la chiave e durata del token in cache,
// - i campi che devono essere interpretati come data,
// - e il formato visivo della data in Google Sheets.
const PHISHING_CONFIG = {
endpointPath: '/api/v1/users/campaigns/phishing',
sheetName: 'PHISHING',
tokenCacheKey: 'SMARTFENSE_ACCESS_TOKEN',
tokenCacheSeconds: 300,
dateFields: [
'campaign_date',
'campaign_expiration_date',
'activity_sent_date',
'activity_open_date',
'activity_click_date',
'activity_post_date',
'activity_simulation_reported_date',
'activity_teachable_moment_sent_date',
'activity_teachable_moment_open_date',
'activity_teachable_moment_correct_answer_date',
'activity_teachable_moment_incorrect_answer_date'
],
dateNumberFormat: 'dd/MM/yyyy HH:mm:ss'
};
/**
* Legge le credenziali client OAuth2 dalle Proprietà dello Script.
*
* Si aspetta di trovare:
* - SMARTFENSE_CLIENT_ID
* - SMARTFENSE_CLIENT_SECRET
*
* Se manca una di queste, interrompe l'esecuzione con errore.
*/
function phishingGetCredentials_() {
const props = PropertiesService.getScriptProperties();
const clientId = props.getProperty('SMARTFENSE_CLIENT_ID');
const clientSecret = props.getProperty('SMARTFENSE_CLIENT_SECRET');
if (!clientId || !clientSecret) {
throw new Error('Mancano SMARTFENSE_CLIENT_ID o SMARTFENSE_CLIENT_SECRET nelle Proprietà dello Script.');
}
return { clientId, clientSecret };
}
/**
* Recupera un token OAuth2 precedentemente salvato in cache.
*
* Questo evita di richiedere un nuovo token ad ogni esecuzione
* finché rimane valido nel tempo configurato.
*/
function phishingGetCachedToken_() {
return CacheService.getScriptCache().get(PHISHING_CONFIG.tokenCacheKey);
}
/**
* Salva il token OAuth2 nella cache dello script.
*
* Il tempo di vita del token cacheato è definito in:
* PHISHING_CONFIG.tokenCacheSeconds
*/
function phishingSetCachedToken_(token) {
CacheService.getScriptCache().put(
PHISHING_CONFIG.tokenCacheKey,
token,
PHISHING_CONFIG.tokenCacheSeconds
);
}
/**
* Ottiene un token di accesso OAuth2 usando il flusso client_credentials.
*
* Flusso:
* 1. Tenta di riutilizzare un token cacheato.
* 2. Se non esiste, legge le credenziali dalle Proprietà dello Script.
* 3. Chiama l'endpoint di token configurato in SMARTFENSE_CONFIG.tokenUrl.
* 4. Valida la risposta HTTP.
* 5. Parsea il JSON ed estrae access_token.
* 6. Salva il token in cache per le esecuzioni successive.
*/
function phishingGetToken_() {
const cachedToken = phishingGetCachedToken_();
if (cachedToken) {
return cachedToken;
}
const { clientId, clientSecret } = phishingGetCredentials_();
const basicAuth = Utilities.base64Encode(clientId + ':' + clientSecret);
const response = UrlFetchApp.fetch(SMARTFENSE_CONFIG.tokenUrl, {
method: 'post',
contentType: 'application/x-www-form-urlencoded',
payload: 'grant_type=client_credentials',
headers: {
Authorization: 'Basic ' + basicAuth,
Accept: 'application/json',
'Cache-Control': 'no-cache'
},
muteHttpExceptions: true
});
const status = response.getResponseCode();
const body = response.getContentText();
// Se l'API risponde fuori dal range 2xx, segnala l'errore completo.
if (status = 300) {
throw new Error('Errore ottenendo token. HTTP ' + status + ' - ' + body);
}
let json;
try {
json = JSON.parse(body);
} catch (e) {
throw new Error('La risposta del token non è un JSON valido.');
}
// Verifica che la risposta includa access_token.
if (!json.access_token) {
throw new Error('La risposta non include access_token.');
}
phishingSetCachedToken_(json.access_token);
return json.access_token;
}
/**
* Valida che un URL di paginazione appartenga alla stessa istanza base.
*
* Questo aggiunge un livello di sicurezza per evitare di seguire URL inaspettate
* nel campo "next" della paginazione.
*/
function phishingValidateUrl_(url) {
if (!url.startsWith(SMARTFENSE_CONFIG.baseUrl + '/')) {
throw new Error('L\'URL di paginazione non appartiene all\'istanza prevista: ' + url);
}
}
/**
* Consulta una pagina dell'endpoint e restituisce il suo contenuto parsato come JSON.
*
* Riceve:
* - url: URL completo della pagina da consultare
* - token: token di accesso OAuth2
*
* Inoltre:
* - valida che l'URL sia sicuro,
* - esegue la GET,
* - controlla errori HTTP,
* - e assicura che la risposta sia JSON valido.
*/
function phishingGetPage_(url, token) {
phishingValidateUrl_(url);
const response = UrlFetchApp.fetch(url, {
method: 'get',
headers: {
Authorization: 'Bearer ' + token,
Accept: 'application/json'
},
muteHttpExceptions: true
});
const status = response.getResponseCode();
const body = response.getContentText();
if (status = 300) {
throw new Error('Errore consultando pagina. HTTP ' + status + ' - ' + body);
}
try {
return JSON.parse(body);
} catch (e) {
throw new Error('La risposta dell\'API non è un JSON valido.');
}
}
/**
* Scorre tutte le pagine dell'endpoint di phishing e accumula tutti i risultati.
*
* Comportamento:
* - ottiene il token,
* - costruisce l'URL iniziale,
* - segue il campo "next" finché esiste,
* - concatena tutti i record trovati in "results",
* - e interrompe se supera il massimo di pagine definito in SMARTFENSE_CONFIG.maxPages.
*
* Restituisce un array con tutti gli utenti recuperati dall'API.
*/
function phishingGetResults_() {
const token = phishingGetToken_();
let nextUrl = SMARTFENSE_CONFIG.baseUrl + PHISHING_CONFIG.endpointPath;
let allResults = [];
let pageCount = 0;
while (nextUrl) {
pageCount++;
if (pageCount SMARTFENSE_CONFIG.maxPages) {
throw new Error('Raggiunto il massimo numero di pagine consentite (' + SMARTFENSE_CONFIG.maxPages + ').');
}
const page = phishingGetPage_(nextUrl, token);
const results = Array.isArray(page.results) ? page.results : [];
allResults = allResults.concat(results);
nextUrl = page && page.next ? page.next : null;
}
return allResults;
}
/**
* Converte la struttura annidata dell'API in righe piatte pronte per l'esportazione.
*
* Logica di trasformazione:
* - ogni utente fornisce i propri dati base,
* - ogni campagna correlata genera una riga indipendente,
* - se l'utente non ha campagne, viene comunque generata una riga
* con i dati dell'utente e le colonne della campagna vuote.
*
* Risultato:
* Una riga = un utente + una campagna correlata.
*/
function phishingFlattenResults_(results) {
const rows = [];
results.forEach(function(user) {
// Dati base dell'utente che saranno ripetuti in ogni riga di campagna.
const userBase = {
first_name: phishingValueOrEmpty_(user.first_name),
last_name: phishingValueOrEmpty_(user.last_name),
email: phishingValueOrEmpty_(user.email),
groups: phishingNormalizeListValue_(user.groups),
functional_areas: phishingNormalizeListValue_(user.functional_areas),
hierarchical_levels: phishingNormalizeListValue_(user.hierarchical_levels)
};
const campaigns = Array.isArray(user.related_campaigns) ? user.related_campaigns : [];
// Se non ci sono campagne correlate, si aggiunge comunque una riga con colonne vuote.
if (campaigns.length === 0) {
rows.push(phishingNormalizeDateFields_(Object.assign({}, userBase, phishingEmptyCampaignRow_())));
return;
}
// Se ci sono campagne, ciascuna diventa una riga individuale.
campaigns.forEach(function(campaign) {
const row = {
campaign_id: phishingValueOrEmpty_(campaign.campaign_id),
campaign_mode: phishingValueOrEmpty_(campaign.campaign_mode),
campaign_name: phishingValueOrEmpty_(campaign.campaign_name),
campaign_description: phishingValueOrEmpty_(campaign.campaign_description),
campaign_state: phishingValueOrEmpty_(campaign.campaign_state),
campaign_date: phishingValueOrEmpty_(campaign.campaign_date),
campaign_expiration_date: phishingValueOrEmpty_(campaign.campaign_expiration_date),
campaign_is_test: phishingValueOrEmpty_(campaign.campaign_is_test),
content_name: phishingValueOrEmpty_(campaign.content_name),
content_type: phishingValueOrEmpty_(campaign.content_type),
content_current_state: phishingValueOrEmpty_(campaign.content_current_state),
content_code: phishingValueOrEmpty_(campaign.content_code),
activity_sent: phishingValueOrEmpty_(campaign.activity_sent),
activity_sent_date: phishingValueOrEmpty_(campaign.activity_sent_date),
activity_open: phishingValueOrEmpty_(campaign.activity_open),
activity_open_date: phishingValueOrEmpty_(campaign.activity_open_date),
activity_click: phishingValueOrEmpty_(campaign.activity_click),
activity_click_date: phishingValueOrEmpty_(campaign.activity_click_date),
activity_post: phishingValueOrEmpty_(campaign.activity_post),
activity_post_date: phishingValueOrEmpty_(campaign.activity_post_date),
activity_simulation_reported: phishingValueOrEmpty_(campaign.activity_simulation_reported),
activity_simulation_reported_date: phishingValueOrEmpty_(campaign.activity_simulation_reported_date),
teachable_moment_topic: phishingValueOrEmpty_(campaign.teachable_moment_topic),
teachable_moment_action: phishingValueOrEmpty_(campaign.teachable_moment_action),
teachable_moment_moment_type: phishingValueOrEmpty_(campaign.teachable_moment_moment_type),
activity_teachable_moment_sent: phishingValueOrEmpty_(campaign.activity_teachable_moment_sent),
activity_teachable_moment_sent_date: phishingValueOrEmpty_(campaign.activity_teachable_moment_sent_date),
activity_teachable_moment_open: phishingValueOrEmpty_(campaign.activity_teachable_moment_open),
activity_teachable_moment_open_date: phishingValueOrEmpty_(campaign.activity_teachable_moment_open_date),
activity_teachable_moment_correct_answer: phishingValueOrEmpty_(campaign.activity_teachable_moment_correct_answer),
activity_teachable_moment_correct_answer_date: phishingValueOrEmpty_(campaign.activity_teachable_moment_correct_answer_date),
activity_teachable_moment_incorrect_answer: phishingValueOrEmpty_(campaign.activity_teachable_moment_incorrect_answer),
activity_teachable_moment_incorrect_answer_date: phishingValueOrEmpty_(campaign.activity_teachable_moment_incorrect_answer_date),
activity_minutes: phishingValueOrEmpty_(campaign.activity_minutes),
campaign_random_send: phishingValueOrEmpty_(campaign.campaign_random_send),
campaign_sample_send: phishingValueOrEmpty_(campaign.campaign_sample_send),
campaign_dont_allow_password_entry: phishingValueOrEmpty_(campaign.campaign_dont_allow_password_entry),
campaign_host: phishingValueOrEmpty_(campaign.campaign_host),
campaign_domain: phishingValueOrEmpty_(campaign.campaign_domain),
campaign_phishing_url: phishingValueOrEmpty_(campaign.campaign_phishing_url)
};
// Prima di salvare la riga, si normalizzano i campi data.
rows.push(phishingNormalizeDateFields_(Object.assign({}, userBase, row)));
});
});
return rows;
}
/**
* Restituisce una riga vuota con tutte le colonne della campagna inizializzate a stringa vuota.
*
* Viene usata quando un utente non ha campagne correlate, affinché l'esportazione
* mantenga una struttura tabellare consistente.
*/
function phishingEmptyCampaignRow_() {
return {
campaign_id: '',
campaign_mode: '',
campaign_name: '',
campaign_description: '',
campaign_state: '',
campaign_date: '',
campaign_expiration_date: '',
campaign_is_test: '',
content_name: '',
content_type: '',
content_current_state: '',
content_code: '',
activity_sent: '',
activity_sent_date: '',
activity_open: '',
activity_open_date: '',
activity_click: '',
activity_click_date: '',
activity_post: '',
activity_post_date: '',
activity_simulation_reported: '',
activity_simulation_reported_date: '',
teachable_moment_topic: '',
teachable_moment_action: '',
teachable_moment_moment_type: '',
activity_teachable_moment_sent: '',
activity_teachable_moment_sent_date: '',
activity_teachable_moment_open: '',
activity_teachable_moment_open_date: '',
activity_teachable_moment_correct_answer: '',
activity_teachable_moment_correct_answer_date: '',
activity_teachable_moment_incorrect_answer: '',
activity_teachable_moment_incorrect_answer_date: '',
activity_minutes: '',
campaign_random_send: '',
campaign_sample_send: '',
campaign_dont_allow_password_entry: '',
campaign_host: '',
campaign_domain: '',
campaign_phishing_url: ''
};
}
/**
* Funzione principale di esportazione.
*
* Processo completo:
* 1. Definisce l'ordine delle colonne di uscita.
* 2. Ottiene il foglio di calcolo attivo.
* 3. Scarica tutti i dati dall'API.
* 4. Appiattisce i risultati per convertirli in righe.
* 5. Ottiene o crea il foglio PHISHING.
* 6. Pulisce il contenuto precedente.
* 7. Scrive le intestazioni.
* 8. Scrive le righe di dati.
* 9. Applica il formato visivo alle colonne data.
*
* Gestione errori:
* - Se si verifica un problema e il foglio esiste già, lo pulisce
* e lascia un messaggio di errore in A1:B1.
* - Poi rilancia l'errore affinché sia visibile anche nell'esecuzione.
*/
function exportPhishingToSheet() {
const headers = [
'first_name',
'last_name',
'email',
'groups',
'functional_areas',
'hierarchical_levels',
'campaign_id',
'campaign_mode',
'campaign_name',
'campaign_description',
'campaign_state',
'campaign_date',
'campaign_expiration_date',
'campaign_is_test',
'content_name',
'content_type',
'content_current_state',
'content_code',
'activity_sent',
'activity_sent_date',
'activity_open',
'activity_open_date',
'activity_click',
'activity_click_date',
'activity_post',
'activity_post_date',
'activity_simulation_reported',
'activity_simulation_reported_date',
'teachable_moment_topic',
'teachable_moment_action',
'teachable_moment_moment_type',
'activity_teachable_moment_sent',
'activity_teachable_moment_sent_date',
'activity_teachable_moment_open',
'activity_teachable_moment_open_date',
'activity_teachable_moment_correct_answer',
'activity_teachable_moment_correct_answer_date',
'activity_teachable_moment_incorrect_answer',
'activity_teachable_moment_incorrect_answer_date',
'activity_minutes',
'campaign_random_send',
'campaign_sample_send',
'campaign_dont_allow_password_entry',
'campaign_host',
'campaign_domain',
'campaign_phishing_url'
];
let sheet = null;
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) {
throw new Error('Non c\'è un foglio di calcolo attivo. Apri lo script da Google Sheets.');
}
const results = phishingGetResults_();
const rows = phishingFlattenResults_(results);
sheet = phishingGetOrCreateSheet_(ss, PHISHING_CONFIG.sheetName);
sheet.clearContents();
// Scrive la riga delle intestazioni.
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
// Se ci sono dati, li scrive rispettando l'ordine definito in headers.
if (rows.length 0) {
const values = rows.map(function(row) {
return headers.map(function(header) {
return row[header];
});
});
sheet.getRange(2, 1, values.length, headers.length).setValues(values);
phishingApplyDateFormats_(sheet, headers, values.length);
}
} catch (error) {
// Se qualcosa fallisce, lascia un segno visibile nel foglio.
if (sheet) {
sheet.clearContents();
sheet.getRange('A1').setValue('ERRORE');
sheet.getRange('B1').setValue(error.message);
}
throw error;
}
}
/**
* Restituisce un foglio esistente per nome o lo crea se non esiste.
*
* Questo permette che l'esportazione funzioni sia su un foglio già creato
* sia in una esecuzione iniziale senza struttura precedente.
*/
function phishingGetOrCreateSheet_(spreadsheet, sheetName) {
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName);
}
return sheet;
}
/**
* Restituisce il valore ricevuto o una stringa vuota se è null/undefined.
*
* Usato per evitare celle con valori nulli o errori durante l'esportazione.
*/
function phishingValueOrEmpty_(value) {
if (value === null || value === undefined) return '';
return value;
}
/**
* Normalizza valori tipo lista o oggetto in testo.
*
* Casi:
* - null / undefined = ''
* - array = unisce elementi con virgola e spazio
* - oggetto = JSON.stringify
* - qualsiasi altro valore = String(value)
*
* Utile per campi come gruppi, aree funzionali o livelli gerarchici.
*/
function phishingNormalizeListValue_(value) {
if (value === null || value === undefined) return '';
if (Array.isArray(value)) {
return value
.map(function(item) {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return JSON.stringify(item);
return String(item);
})
.filter(function(item) {
return item !== '';
})
.join(', ');
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
/**
* Scorre tutti i campi data configurati e converte i valori
* al tipo Date quando il contenuto può essere interpretato come data valida.
*
* Se non può essere parsato, lascia una stringa vuota.
*/
function phishingNormalizeDateFields_(row) {
PHISHING_CONFIG.dateFields.forEach(function(field) {
row[field] = phishingParseDate_(row[field]);
});
return row;
}
/**
* Tenta di convertire diversi formati di data in un oggetto Date.
*
* Formati supportati:
* - dd/MM/yyyy HH:mm:ss.SS
* - dd/MM/yyyy HH:mm:ss
* - dd/MM/yyyy HH:mm
* - yyyy-MM-dd HH:mm:ss(.SSS)
* - ISO nativo
*
* Se non riconosce il formato o la data non è valida, restituisce ''.
*
* Questo permette che Google Sheets riceva date reali
* e poi possa applicare formati visivi corretti.
*/
function phishingParseDate_(value) {
if (value === null || value === undefined) return '';
if (value === '') return '';
// Se è già un oggetto Date valido, lo riutilizza.
if (Object.prototype.toString.call(value) === '[object Date]') {
return isNaN(value.getTime()) ? '' : value;
}
const str = String(value).trim();
if (str === '') return '';
let match;
// Formato: dd/MM/yyyy HH:mm:ss.SS o dd/MM/yyyy HH:mm:ss o dd/MM/yyyy HH:mm
match = str.match(
/^(\d{2})\/(\d{2})\/(\d{4})(?:\s+(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)?$/
);
if (match) {
const day = Number(match[1]);
const month = Number(match[2]) - 1;
const year = Number(match[3]);
const hour = Number(match[4] || 0);
const minute = Number(match[5] || 0);
const second = Number(match[6] || 0);
let ms = Number(match[7] || 0);
if (match[7]) {
if (match[7].length === 1) ms = ms * 100;
if (match[7].length === 2) ms = ms * 10;
}
return new Date(year, month, day, hour, minute, second, ms);
}
// Formato: yyyy-MM-dd HH:mm:ss(.SSS) o con separatore "T"
match = str.match(
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)?$/
);
if (match) {
const year = Number(match[1]);
const month = Number(match[2]) - 1;
const day = Number(match[3]);
const hour = Number(match[4] || 0);
const minute = Number(match[5] || 0);
const second = Number(match[6] || 0);
let ms = Number(match[7] || 0);
if (match[7]) {
if (match[7].length === 1) ms = ms * 100;
if (match[7].length === 2) ms = ms * 10;
}
return new Date(year, month, day, hour, minute, second, ms);
}
// Ultimo tentativo: lascia che JavaScript interpreti il valore come data ISO.
const nativeDate = new Date(str);
if (!isNaN(nativeDate.getTime())) {
return nativeDate;
}
return '';
}
/**
* Applica formato visivo di data alle colonne configurate come dateFields.
*
* Non modifica i dati, solo il modo in cui vengono mostrati nel foglio.
* Il formato applicato è definito in:
* PHISHING_CONFIG.dateNumberFormat
*/
function phishingApplyDateFormats_(sheet, headers, rowCount) {
PHISHING_CONFIG.dateFields.forEach(function(header) {
const colIndex = headers.indexOf(header) + 1;
if (colIndex 0) {
sheet
.getRange(2, colIndex, rowCount, 1)
.setNumberFormat(PHISHING_CONFIG.dateNumberFormat);
}
});
}Esportazione delle campagne di ransomware in Google Sheets
Obiettivo
Questo script consulta l'endpoint delle campagne di ransomware per utente nell'API di SMARTFENSE ed esporta i risultati in un foglio Google Sheets chiamato RANSOMWARE.
Cosa fa questo script?
Lo script automatizza l'intero processo di estrazione e caricamento dati:
si autentica contro l'API di SMARTFENSE,
consulta l'endpoint corrispondente,
scorre tutte le pagine dei risultati,
trasforma la risposta in una struttura tabellare,
e riversa le informazioni in un foglio Google Sheets.
Risultato
All'esecuzione dello script, viene creato o aggiornato un foglio chiamato RANSOMWARE all'interno del foglio di calcolo attivo.
Le informazioni sono organizzate in formato tabellare, pronte per l'analisi e il riutilizzo.
A cosa serve questo foglio?
Il foglio generato funziona come base dati operativa per analisi e visualizzazione.
Il suo scopo è che i dati estratti dall'API possano essere utilizzati successivamente in:
dashboard di Looker Studio,
analisi in Google Sheets,
incroci con altre fonti,
report operativi,
cruscotti di monitoraggio.
In questo schema, lo script svolge la funzione di trasformare le informazioni dell'endpoint in una fonte tabellare pronta per essere consumata da strumenti di reporting.
Fonte dati
Endpoint consultato:
/api/v1/users/campaigns/ransomware
Tipo di informazioni esportate
Il foglio include informazioni relative a:
dati dell'utente,
campagne di ransomware associate,
attività dell'utente all'interno della campagna,
download e interazione con file correlati,
momenti didattici (teachable moments),
date rilevanti del processo,
e dati tecnici associati alla simulazione.
Considerazioni
Lo script utilizza autenticazione OAuth2.
Riutilizza temporaneamente il token tramite cache.
Scorre automaticamente la paginazione dell'endpoint.
Converte i campi data affinché Google Sheets li riconosca correttamente.
Pulisce e rigenera il foglio ad ogni esecuzione.
In caso di errore, tenta di lasciare il dettaglio riflesso nel foglio stesso.
Codice
Di seguito è incluso lo script commentato per riferimento tecnico.
Ransomware.gs
// Configurazione specifica per l'esportazione delle campagne di ransomware.
// Centralizza:
// - il percorso dell'endpoint da consultare,
// - il nome del foglio di destinazione,
// - la chiave e durata del token in cache,
// - i campi che devono essere interpretati come data,
// - e il formato visivo della data in Google Sheets.
const RANSOMWARE_CONFIG = {
endpointPath: '/api/v1/users/campaigns/ransomware',
sheetName: 'RANSOMWARE',
tokenCacheKey: 'SMARTFENSE_ACCESS_TOKEN',
tokenCacheSeconds: 300,
dateFields: [
'campaign_date',
'campaign_expiration_date',
'activity_sent_date',
'activity_open_date',
'activity_download_date',
'activity_download_open_date',
'activity_simulation_reported_date',
'activity_teachable_moment_sent_date',
'activity_teachable_moment_open_date',
'activity_teachable_moment_correct_answer_date',
'activity_teachable_moment_incorrect_answer_date',
'activity_attachment_open_date',
'activity_encryption_possible_date'
],
dateNumberFormat: 'dd/MM/yyyy HH:mm:ss'
};
/**
* Legge le credenziali OAuth2 dalle Proprietà dello Script.
*
* Si aspetta di trovare:
* - SMARTFENSE_CLIENT_ID
* - SMARTFENSE_CLIENT_SECRET
*
* Se manca una di queste, interrompe l'esecuzione con errore.
*/
function ransomwareGetCredentials_() {
const props = PropertiesService.getScriptProperties();
const clientId = props.getProperty('SMARTFENSE_CLIENT_ID');
const clientSecret = props.getProperty('SMARTFENSE_CLIENT_SECRET');
if (!clientId || !clientSecret) {
throw new Error('Mancano SMARTFENSE_CLIENT_ID o SMARTFENSE_CLIENT_SECRET nelle Proprietà dello Script.');
}
return { clientId, clientSecret };
}
/**
* Recupera un token precedentemente salvato in cache.
*
* Questo evita di richiedere un nuovo token ad ogni esecuzione
* finché rimane valido nel tempo configurato.
*/
function ransomwareGetCachedToken_() {
return CacheService.getScriptCache().get(RANSOMWARE_CONFIG.tokenCacheKey);
}
/**
* Salva il token OAuth2 nella cache dello script.
*
* Il tempo di vita del token cacheato è definito in:
* RANSOMWARE_CONFIG.tokenCacheSeconds
*/
function ransomwareSetCachedToken_(token) {
CacheService.getScriptCache().put(
RANSOMWARE_CONFIG.tokenCacheKey,
token,
RANSOMWARE_CONFIG.tokenCacheSeconds
);
}
/**
* Ottiene un token di accesso OAuth2 usando il flusso client_credentials.
*
* Flusso:
* 1. Tenta di riutilizzare un token cacheato.
* 2. Se non esiste, legge le credenziali dalle Proprietà dello Script.
* 3. Chiama l'endpoint di token configurato in SMARTFENSE_CONFIG.tokenUrl.
* 4. Valida la risposta HTTP.
* 5. Parsea il JSON ed estrae access_token.
* 6. Salva il token in cache per le esecuzioni successive.
*/
function ransomwareGetToken_() {
const cachedToken = ransomwareGetCachedToken_();
if (cachedToken) {
return cachedToken;
}
const { clientId, clientSecret } = ransomwareGetCredentials_();
const basicAuth = Utilities.base64Encode(clientId + ':' + clientSecret);
const response = UrlFetchApp.fetch(SMARTFENSE_CONFIG.tokenUrl, {
method: 'post',
contentType: 'application/x-www-form-urlencoded',
payload: 'grant_type=client_credentials',
headers: {
Authorization: 'Basic ' + basicAuth,
Accept: 'application/json',
'Cache-Control': 'no-cache'
},
muteHttpExceptions: true
});
const status = response.getResponseCode();
const body = response.getContentText();
// Se l'API risponde fuori dal range 2xx, segnala l'errore completo.
if (status = 300) {
throw new Error('Errore ottenendo token. HTTP ' + status + ' - ' + body);
}
let json;
try {
json = JSON.parse(body);
} catch (e) {
throw new Error('La risposta del token non è un JSON valido.');
}
// Verifica che la risposta includa access_token.
if (!json.access_token) {
throw new Error('La risposta non include access_token.');
}
ransomwareSetCachedToken_(json.access_token);
return json.access_token;
}
/**
* Valida che un URL di paginazione appartenga alla stessa istanza base.
*
* Questo aggiunge un livello di sicurezza per evitare di seguire URL inaspettate
* nel campo "next" della paginazione.
*/
function ransomwareValidateUrl_(url) {
if (!url.startsWith(SMARTFENSE_CONFIG.baseUrl + '/')) {
throw new Error('L\'URL di paginazione non appartiene all\'istanza prevista: ' + url);
}
}
/**
* Consulta una pagina dell'endpoint e restituisce il suo contenuto parsato come JSON.
*
* Riceve:
* - url: URL completo della pagina da consultare
* - token: token di accesso OAuth2
*
* Inoltre:
* - valida che l'URL sia sicuro,
* - esegue la GET,
* - controlla errori HTTP,
* - e assicura che la risposta sia JSON valido.
*/
function ransomwareGetPage_(url, token) {
ransomwareValidateUrl_(url);
const response = UrlFetchApp.fetch(url, {
method: 'get',
headers: {
Authorization: 'Bearer ' + token,
Accept: 'application/json'
},
muteHttpExceptions: true
});
const status = response.getResponseCode();
const body = response.getContentText();
if (status = 300) {
throw new Error('Errore consultando pagina. HTTP ' + status + ' - ' + body);
}
try {
return JSON.parse(body);
} catch (e) {
throw new Error('La risposta dell\'API non è un JSON valido.');
}
}
/**
* Scorre tutte le pagine dell'endpoint di ransomware e accumula tutti i risultati.
*
* Comportamento:
* - ottiene il token,
* - costruisce l'URL iniziale,
* - segue il campo "next" finché esiste,
* - concatena tutti i record trovati in "results",
* - e interrompe se supera il massimo di pagine definito in SMARTFENSE_CONFIG.maxPages.
*
* Restituisce un array con tutti gli utenti recuperati dall'API.
*/
function ransomwareGetResults_() {
const token = ransomwareGetToken_();
let nextUrl = SMARTFENSE_CONFIG.baseUrl + RANSOMWARE_CONFIG.endpointPath;
let allResults = [];
let pageCount = 0;
while (nextUrl) {
pageCount++;
if (pageCount SMARTFENSE_CONFIG.maxPages) {
throw new Error('Raggiunto il massimo numero di pagine consentite (' + SMARTFENSE_CONFIG.maxPages + ').');
}
const page = ransomwareGetPage_(nextUrl, token);
const results = Array.isArray(page.results) ? page.results : [];
allResults = allResults.concat(results);
nextUrl = page && page.next ? page.next : null;
}
return allResults;
}
/**
* Converte la struttura annidata dell'API in righe piatte pronte per l'esportazione.
*
* Logica di trasformazione:
* - ogni utente fornisce i propri dati base,
* - ogni campagna correlata genera una riga indipendente,
* - se l'utente non ha campagne, viene comunque generata una riga
* con i dati dell'utente e le colonne della campagna vuote.
*
* Risultato:
* Una riga = un utente + una campagna correlata.
*/
function ransomwareFlattenResults_(results) {
const rows = [];
results.forEach(function(user) {
// Dati base dell'utente che saranno ripetuti in ogni riga di campagna.
const userBase = {
first_name: ransomwareValueOrEmpty_(user.first_name),
last_name: ransomwareValueOrEmpty_(user.last_name),
email: ransomwareValueOrEmpty_(user.email),
groups: ransomwareNormalizeListValue_(user.groups),
functional_areas: ransomwareNormalizeListValue_(user.functional_areas),
hierarchical_levels: ransomwareNormalizeListValue_(user.hierarchical_levels)
};
const campaigns = Array.isArray(user.related_campaigns) ? user.related_campaigns : [];
// Se non ci sono campagne correlate, si aggiunge comunque una riga con colonne vuote.
if (campaigns.length === 0) {
rows.push(ransomwareNormalizeDateFields_(Object.assign({}, userBase, ransomwareEmptyCampaignRow_())));
return;
}
// Se ci sono campagne, ciascuna diventa una riga individuale.
campaigns.forEach(function(campaign) {
const row = {
campaign_id: ransomwareValueOrEmpty_(campaign.campaign_id),
campaign_mode: ransomwareValueOrEmpty_(campaign.campaign_mode),
campaign_name: ransomwareValueOrEmpty_(campaign.campaign_name),
campaign_description: ransomwareValueOrEmpty_(campaign.campaign_description),
campaign_state: ransomwareValueOrEmpty_(campaign.campaign_state),
campaign_date: ransomwareValueOrEmpty_(campaign.campaign_date),
campaign_expiration_date: ransomwareValueOrEmpty_(campaign.campaign_expiration_date),
campaign_is_test: ransomwareValueOrEmpty_(campaign.campaign_is_test),
content_name: ransomwareValueOrEmpty_(campaign.content_name),
content_type: ransomwareValueOrEmpty_(campaign.content_type),
content_current_state: ransomwareValueOrEmpty_(campaign.content_current_state),
content_code: ransomwareValueOrEmpty_(campaign.content_code),
activity_sent: ransomwareValueOrEmpty_(campaign.activity_sent),
activity_sent_date: ransomwareValueOrEmpty_(campaign.activity_sent_date),
activity_open: ransomwareValueOrEmpty_(campaign.activity_open),
activity_open_date: ransomwareValueOrEmpty_(campaign.activity_open_date),
activity_download: ransomwareValueOrEmpty_(campaign.activity_download),
activity_download_date: ransomwareValueOrEmpty_(campaign.activity_download_date),
activity_download_open: ransomwareValueOrEmpty_(campaign.activity_download_open),
activity_download_open_date: ransomwareValueOrEmpty_(campaign.activity_download_open_date),
activity_simulation_reported: ransomwareValueOrEmpty_(campaign.activity_simulation_reported),
activity_simulation_reported_date: ransomwareValueOrEmpty_(campaign.activity_simulation_reported_date),
teachable_moment_topic: ransomwareValueOrEmpty_(campaign.teachable_moment_topic),
teachable_moment_action: ransomwareValueOrEmpty_(campaign.teachable_moment_action),
teachable_moment_moment_type: ransomwareValueOrEmpty_(campaign.teachable_moment_moment_type),
activity_teachable_moment_sent: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_sent),
activity_teachable_moment_sent_date: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_sent_date),
activity_teachable_moment_open: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_open),
activity_teachable_moment_open_date: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_open_date),
activity_teachable_moment_correct_answer: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_correct_answer),
activity_teachable_moment_correct_answer_date: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_correct_answer_date),
activity_teachable_moment_incorrect_answer: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_incorrect_answer),
activity_teachable_moment_incorrect_answer_date: ransomwareValueOrEmpty_(campaign.activity_teachable_moment_incorrect_answer_date),
campaign_random_send: ransomwareValueOrEmpty_(campaign.campaign_random_send),
campaign_sample_send: ransomwareValueOrEmpty_(campaign.campaign_sample_send),
campaign_host: ransomwareValueOrEmpty_(campaign.campaign_host),
campaign_domain: ransomwareValueOrEmpty_(campaign.campaign_domain),
campaign_ransomware_url: ransomwareValueOrEmpty_(campaign.campaign_ransomware_url),
activity_attachment_open: ransomwareValueOrEmpty_(campaign.activity_attachment_open),
activity_attachment_open_date: ransomwareValueOrEmpty_(campaign.activity_attachment_open_date),
activity_encryption_possible: ransomwareValueOrEmpty_(campaign.activity_encryption_possible),
activity_encryption_possible_date: ransomwareValueOrEmpty_(campaign.activity_encryption_possible_date)
};
// Prima di salvare la riga, si normalizzano i campi data.
rows.push(ransomwareNormalizeDateFields_(Object.assign({}, userBase, row)));
});
});
return rows;
}
/**
* Restituisce una riga vuota con tutte le colonne della campagna inizializzate a stringa vuota.
*
* Viene usata quando un utente non ha campagne correlate, affinché l'esportazione
* mantenga una struttura tabellare consistente.
*/
function ransomwareEmptyCampaignRow_() {
return {
campaign_id: '',
campaign_mode: '',
campaign_name: '',
campaign_description: '',
campaign_state: '',
campaign_date: '',
campaign_expiration_date: '',
campaign_is_test: '',
content_name: '',
content_type: '',
content_current_state: '',
content_code: '',
activity_sent: '',
activity_sent_date: '',
activity_open: '',
activity_open_date: '',
activity_download: '',
activity_download_date: '',
activity_download_open: '',
activity_download_open_date: '',
activity_simulation_reported: '',
activity_simulation_reported_date: '',
teachable_moment_topic: '',
teachable_moment_action: '',
teachable_moment_moment_type: '',
activity_teachable_moment_sent: '',
activity_teachable_moment_sent_date: '',
activity_teachable_moment_open: '',
activity_teachable_moment_open_date: '',
activity_teachable_moment_correct_answer: '',
activity_teachable_moment_correct_answer_date: '',
activity_teachable_moment_incorrect_answer: '',
activity_teachable_moment_incorrect_answer_date: '',
campaign_random_send: '',
campaign_sample_send: '',
campaign_host: '',
campaign_domain: '',
campaign_ransomware_url: '',
activity_attachment_open: '',
activity_attachment_open_date: '',
activity_encryption_possible: '',
activity_encryption_possible_date: ''
};
}
/**
* Funzione principale del processo di esportazione.
*
* Flusso:
* 1. Definisce l'ordine delle colonne.
* 2. Ottiene il foglio di calcolo attivo.
* 3. Scarica tutti i dati dall'API.
* 4. Appiattisce i risultati.
* 5. Ottiene o crea il foglio RANSOMWARE.
* 6. Pulisce il contenuto precedente.
* 7. Scrive le intestazioni.
* 8. Scrive le righe di dati.
* 9. Applica formato alle colonne data.
*
* Se si verifica un errore, tenta di lasciarlo visibile nel foglio
* prima di rilanciarlo.
*/
function exportRansomwareToSheet() {
const headers = [
'first_name',
'last_name',
'email',
'groups',
'functional_areas',
'hierarchical_levels',
'campaign_id',
'campaign_mode',
'campaign_name',
'campaign_description',
'campaign_state',
'campaign_date',
'campaign_expiration_date',
'campaign_is_test',
'content_name',
'content_type',
'content_current_state',
'content_code',
'activity_sent',
'activity_sent_date',
'activity_open',
'activity_open_date',
'activity_download',
'activity_download_date',
'activity_download_open',
'activity_download_open_date',
'activity_simulation_reported',
'activity_simulation_reported_date',
'teachable_moment_topic',
'teachable_moment_action',
'teachable_moment_moment_type',
'activity_teachable_moment_sent',
'activity_teachable_moment_sent_date',
'activity_teachable_moment_open',
'activity_teachable_moment_open_date',
'activity_teachable_moment_correct_answer',
'activity_teachable_moment_correct_answer_date',
'activity_teachable_moment_incorrect_answer',
'activity_teachable_moment_incorrect_answer_date',
'campaign_random_send',
'campaign_sample_send',
'campaign_host',
'campaign_domain',
'campaign_ransomware_url',
'activity_attachment_open',
'activity_attachment_open_date',
'activity_encryption_possible',
'activity_encryption_possible_date'
];
let sheet = null;
try {
const ss = SpreadsheetApp.getActiveSpreadsheet();
if (!ss) {
throw new Error('Non c\'è un foglio di calcolo attivo. Apri lo script da Google Sheets.');
}
const results = ransomwareGetResults_();
const rows = ransomwareFlattenResults_(results);
sheet = ransomwareGetOrCreateSheet_(ss, RANSOMWARE_CONFIG.sheetName);
sheet.clearContents();
// Scrive la riga delle intestazioni.
sheet.getRange(1, 1, 1, headers.length).setValues([headers]);
// Se ci sono dati, li scrive rispettando l'ordine definito in headers.
if (rows.length 0) {
const values = rows.map(function(row) {
return headers.map(function(header) {
return row[header];
});
});
sheet.getRange(2, 1, values.length, headers.length).setValues(values);
ransomwareApplyDateFormats_(sheet, headers, values.length);
}
} catch (error) {
// Se qualcosa fallisce, lascia un segno visibile nel foglio.
if (sheet) {
sheet.clearContents();
sheet.getRange('A1').setValue('ERRORE');
sheet.getRange('B1').setValue(error.message);
}
throw error;
}
}
/**
* Cerca un foglio per nome e lo crea se non esiste.
*/
function ransomwareGetOrCreateSheet_(spreadsheet, sheetName) {
let sheet = spreadsheet.getSheetByName(sheetName);
if (!sheet) {
sheet = spreadsheet.insertSheet(sheetName);
}
return sheet;
}
/**
* Restituisce il valore ricevuto o una stringa vuota se è null/undefined.
*/
function ransomwareValueOrEmpty_(value) {
if (value === null || value === undefined) return '';
return value;
}
/**
* Converte array o oggetti in testo esportabile.
*
* - null / undefined = ''
* - array = elementi uniti da virgola
* - oggetto = JSON.stringify
* - altri = String(value)
*/
function ransomwareNormalizeListValue_(value) {
if (value === null || value === undefined) return '';
if (Array.isArray(value)) {
return value
.map(function(item) {
if (item === null || item === undefined) return '';
if (typeof item === 'object') return JSON.stringify(item);
return String(item);
})
.filter(function(item) {
return item !== '';
})
.join(', ');
}
if (typeof value === 'object') {
return JSON.stringify(value);
}
return String(value);
}
/**
* Scorre tutti i campi configurati come data e li converte.
*/
function ransomwareNormalizeDateFields_(row) {
RANSOMWARE_CONFIG.dateFields.forEach(function(field) {
row[field] = ransomwareParseDate_(row[field]);
});
return row;
}
/**
* Tenta di convertire diversi formati di data in Date.
*
* Supporta:
* - dd/MM/yyyy HH:mm:ss.SS
* - dd/MM/yyyy HH:mm:ss
* - dd/MM/yyyy HH:mm
* - yyyy-MM-dd HH:mm:ss(.SSS)
* - ISO
*
* Se non può essere interpretato come data valida, restituisce ''.
*/
function ransomwareParseDate_(value) {
if (value === null || value === undefined) return '';
if (value === '') return '';
if (Object.prototype.toString.call(value) === '[object Date]') {
return isNaN(value.getTime()) ? '' : value;
}
const str = String(value).trim();
if (str === '') return '';
let match;
// Formato: dd/MM/yyyy HH:mm:ss.SS o dd/MM/yyyy HH:mm:ss o dd/MM/yyyy HH:mm
match = str.match(
/^(\d{2})\/(\d{2})\/(\d{4})(?:\s+(\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)?$/
);
if (match) {
const day = Number(match[1]);
const month = Number(match[2]) - 1;
const year = Number(match[3]);
const hour = Number(match[4] || 0);
const minute = Number(match[5] || 0);
const second = Number(match[6] || 0);
let ms = Number(match[7] || 0);
if (match[7]) {
if (match[7].length === 1) ms = ms * 100;
if (match[7].length === 2) ms = ms * 10;
}
return new Date(year, month, day, hour, minute, second, ms);
}
// Formato: yyyy-MM-dd HH:mm:ss(.SSS)
match = str.match(
/^(\d{4})-(\d{2})-(\d{2})(?:[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\.(\d{1,3}))?)?$/
);
if (match) {
const year = Number(match[1]);
const month = Number(match[2]) - 1;
const day = Number(match[3]);
const hour = Number(match[4] || 0);
const minute = Number(match[5] || 0);
const second = Number(match[6] || 0);
let ms = Number(match[7] || 0);
if (match[7]) {
if (match[7].length === 1) ms = ms * 100;
if (match[7].length === 2) ms = ms * 10;
}
return new Date(year, month, day, hour, minute, second, ms);
}
// Ultimo tentativo: interpretazione nativa di JavaScript.
const nativeDate = new Date(str);
if (!isNaN(nativeDate.getTime())) {
return nativeDate;
}
return '';
}
/**
* Applica formato visivo alle colonne data del foglio.
*/
function ransomwareApplyDateFormats_(sheet, headers, rowCount) {
RANSOMWARE_CONFIG.dateFields.forEach(function(header) {
const colIndex = headers.indexOf(header) + 1;
if (colIndex 0) {
sheet
.getRange(2, colIndex, rowCount, 1)
.setNumberFormat(RANSOMWARE_CONFIG.dateNumberFormat);
}
});
}