209 lines
5.4 KiB
JavaScript
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 }
|