// Ref.: https://docs.paloaltonetworks.com/pan-os/9-1/pan-os-panorama-api/pan-os-xml-api-request-types/apply-user-id-mapping-and-populate-dynamic-address-groups-api.html import axios from 'axios' import https from 'https' import { isIPv4 } from 'net' import qs from 'qs' import { subMinutes } from 'date-fns' import { logError, logSuccess } from './logger' import { AES, enc } from 'crypto-js' import ip from 'ip' import { performance } from 'perf_hooks' import prisma from '../prisma' const MAP_TIMEOUT_IN_MINUTES = process.env.MAPPING_TIMEOUT || '360' // 6 horas const CIDR_RE = /^([0-9]{1,3}\.){3}[0-9]{1,3}(\/([0-9]|[1-2][0-9]|3[0-2]))?$/ const REQUEST_TIMEOUT_IN_MS = 20000 const httpsAgent = new https.Agent({ rejectUnauthorized: false }) async function getDevicesWithUser() { const now = new Date() const timeoutThreshold = subMinutes(now, MAP_TIMEOUT_IN_MINUTES) const wifiDevices = await prisma.wifiDevice.findMany({ where: { userId: { not: null }, status: 'ONLINE', lastSeen: { gt: timeoutThreshold } }, select: { ip: true, user: { select: { sAMAccountName: true, displayName: true } } } }) return wifiDevices } function createCommand(devices) { const entries = devices.reduce( (entries, device) => entries + `\n`, '' ) return ` 1.0 update ${entries} ` } const isWorking = [] function clearWorkingState() { isWorking.length = 0 } setInterval(clearWorkingState, 3600000) async function updateUserIdMappings() { const allDevices = await getDevicesWithUser() const pAHosts = await prisma.pAHost.findMany() const jobs = pAHosts.map(async pAHost => { if (isWorking.includes(pAHost.id)) return `Última execução para ${pAHost.description} ainda em andamento` const startTime = performance.now() const net = ip.cidrSubnet(pAHost.cidr) const devices = allDevices.filter( ({ ip }) => isIPv4(ip) && net.contains(ip) ) try { if (devices.length == 0) return `Nenhum dispositivo encontrado para ${pAHost.description}` isWorking.push(pAHost.id) const cmd = createCommand(devices) await axios({ url: `https://${pAHost.cidr.split('/')[0]}/api/`, method: 'POST', params: { type: 'user-id', key: decryptKey(pAHost.encryptedKey) }, data: qs.stringify({ cmd }), headers: { 'Content-Type': 'application/x-www-form-urlencoded' }, timeout: REQUEST_TIMEOUT_IN_MS, httpsAgent }) const endTime = performance.now() logSuccess({ tags: ['paloalto', 'user-id'], message: `Foram mapeados ${devices.length} user-ids em ${ pAHost.description || pAHost.cidr } em ${((endTime - startTime) / 1000).toFixed(2)}s.`, data: devices }) return devices.length } catch (e) { logError({ tags: ['paloalto', 'user-id'], message: `Erro atualizando user-id mappings em ${ pAHost.description || pAHost.cidr }: ${e.message}` }) //console.log(e) // Do not add e to log DB for security reasons... return 'Não foi possível atualizar. Veja o log do servidor' } finally { const index = isWorking.indexOf(pAHost.id) if (index > -1) isWorking.splice(index, 1) } }) return Promise.allSettled(jobs) } async function getUserKey({ ipAddr, user, password }) { try { const result = await axios({ url: `https://${ipAddr}/api/`, method: 'POST', params: { type: 'keygen', user, password }, httpsAgent }) return result.data.split('')[1].split('')[0] } catch (e) { throw new Error(e.message) } } function encryptKey(key) { return AES.encrypt(key, process.env.CRYPT_SECRET).toString() } function decryptKey(encryptedKey) { return AES.decrypt(encryptedKey, process.env.CRYPT_SECRET).toString(enc.Utf8) } async function addHost({ cidr, user, password, description, note, owner }) { if (!CIDR_RE.test(cidr)) throw new Error('Este não é um CIDR válido') const ipAddr = cidr.split('/')[0] if (!isIPv4(ipAddr)) throw new Error('Este não é um IPv4 válido') const net = ip.cidrSubnet(cidr) if (net.subnetMaskLength > 32 || net.networkAddress == '0.0.0.0') throw new Error('Esta não é uma combinação de IP/máscara IPv4 válida') const pAHosts = await prisma.pAHost.findMany() try { const key = await getUserKey({ ipAddr, user, password }) const encryptedKey = encryptKey(key) const id = pAHosts.find(pAHost => pAHost.cidr.split('/')[0] == ipAddr)?.id const pAHost = { cidr, encryptedKey, user, description, note, owner: { connect: { sAMAccountName: owner.sAMAccountName } } } const host = id ? await prisma.pAHost.update({ where: { id }, data: pAHost }) : await prisma.pAHost.create({ data: pAHost }) return { ...host, key: `${key.slice(0, 5)}` } } catch (e) { logError({ message: `Não foi possível adicionar o host ${cidr}. ${e?.message}`, tags: ['paloalto'] }) throw new Error(e.message) } } export { updateUserIdMappings, addHost, decryptKey }