ifms-pti/server/src/lib/paloalto.js
2022-06-14 13:26:01 +00:00

209 lines
5.4 KiB
JavaScript

// 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 +
`<entry name="ifms\\${device.user.sAMAccountName}" ip="${device.ip}" timeout="${MAP_TIMEOUT_IN_MINUTES}"/>\n`,
''
)
return `
<uid-message>
<version>1.0</version>
<type>update</type>
<payload>
<login>
${entries}
</login>
</payload>
</uid-message>`
}
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('<key>')[1].split('</key>')[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 }