siip-whatsapp-notifications.../scripts-uisp/audit_client_passwords.php

687 lines
26 KiB
PHP

<?php
$initialDir = getcwd();
chdir(__DIR__ . '/../');
require_once __DIR__ . '/../vendor/autoload.php';
use Ubnt\UcrmPluginSdk\Service\UcrmApi;
use Ubnt\UcrmPluginSdk\Service\PluginConfigManager;
use GuzzleHttp\Client;
// -- Configuration --
$customAttributeKey = 'passwordAntenaCliente';
$siteAttributeKey = 'site';
$antenaSectorialAttributeKey = 'antenaSectorial';
$logFile = __DIR__ . '/audit_passwords.log';
// -- Argument Parsing --
$fixOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--fix=') === 0) {
$fixOption = substr($arg, 6);
}
}
$fixLimit = 0;
if ($fixOption === 'all') {
$fixLimit = PHP_INT_MAX;
} elseif (is_numeric($fixOption)) {
$fixLimit = (int)$fixOption;
}
// -- Helpers --
function logMessage($message)
{
$logFile = __DIR__ . '/audit_passwords.log';
$timestamp = date('Y-m-d H:i:s');
file_put_contents($logFile, "[$timestamp] $message\n", FILE_APPEND);
// Only write to STDERR if running in CLI mode, not web server (optional, but good practice)
if (php_sapi_name() === 'cli') {
fwrite(STDERR, "[$timestamp] $message\n");
}
}
function isTestEnvironment($config)
{
$ipServer = $config['ipserver'] ?? '';
return ($ipServer === '172.16.5.134' || $ipServer === 'venus.siip.mx' || $ipServer === 'pruebas.internet.mx');
}
// -- Initialization --
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
logMessage("Error: API Token is missing in plugin configuration.");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
// Initialize UNMS Client
$unmsClient = new Client([
'base_uri' => "https://{$ipServer}/nms/api/v2.1/",
'verify' => false,
'headers' => [
'X-Auth-Token' => $config['unmsApiToken'] ?? ''
]
]);
function fetchAllClients($client)
{
$allClients = [];
$page = 1;
$limit = 500;
do {
try {
$response = $client->get('clients', [
'query' => [
'limit' => $limit,
'offset' => ($page - 1) * $limit,
'isArchived' => 0,
]
]);
$data = json_decode($response->getBody()->getContents(), true);
if (empty($data)) break;
$allClients = array_merge($allClients, $data);
if (count($data) < $limit) break;
$page++;
} catch (\Exception $e) {
logMessage("Error fetching page $page: " . $e->getMessage());
break;
}
} while (true);
return $allClients;
}
function generateStrongPassword($length = 16)
{
$lower = 'abcdefghijkmnopqrstuvwxyz';
$upper = 'ABCDEFGHJKLMNPQRSTUVWXYZ';
$digits = '23456789';
$symbols = '!@#$%&*()-_=+[]{};:,.<>?';
$all = $lower . $upper . $digits . $symbols;
$pwChars = [];
$pwChars[] = $lower[random_int(0, strlen($lower) - 1)];
$pwChars[] = $upper[random_int(0, strlen($upper) - 1)];
$pwChars[] = $digits[random_int(0, strlen($digits) - 1)];
$pwChars[] = $symbols[random_int(0, strlen($symbols) - 1)];
for ($i = count($pwChars); $i < $length; $i++) {
$pwChars[] = $all[random_int(0, strlen($all) - 1)];
}
shuffle($pwChars);
return implode('', $pwChars);
}
/**
* Fix client password AND extract network data (Site, Antena/Sectorial).
* Returns an associative array with 'password', 'siteName', 'deviceInfo'.
*
* Edge cases handled:
* - Client with tag 'NS REPETIDOR' → password = 'Este cliente funciona como Repetidor'
* - Service status Ended(2) or Obsolete(5) → password = 'Servicio Finalizado'
* - Device disconnected → password = 'Antena desconectada al momento de obtener la contraseña'
*/
function fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPassword)
{
$passwordAttributeId = $attributeIds['password'];
$siteAttributeId = $attributeIds['site'] ?? null;
$antenaSectorialAttributeId = $attributeIds['antenaSectorial'] ?? null;
$result = [
'password' => '',
'siteName' => null,
'deviceInfo' => null,
];
try {
$isTest = isTestEnvironment($config);
// ── Edge Case 1: Check client tags for "NS REPETIDOR" ──
try {
$clientData = $ucrmApi->get("clients/$clientId");
$clientTags = $clientData['tags'] ?? [];
foreach ($clientTags as $tag) {
if (stripos($tag['name'] ?? '', 'NS REPETIDOR') !== false) {
$msg = 'Este cliente funciona como Repetidor';
$result['password'] = $msg;
$result['deviceInfo'] = 'REPETIDOR';
if ($currentPassword !== $msg) {
$attrUpdates = [$passwordAttributeId => $msg];
if ($antenaSectorialAttributeId) {
$attrUpdates[$antenaSectorialAttributeId] = 'REPETIDOR';
}
patchClientAttributes($ucrmApi, $clientId, $attrUpdates);
$result['changed'] = true;
} else {
$result['changed'] = false;
}
logMessage("Client $clientId: Tag NS REPETIDOR detected → skipping");
return $result;
}
}
} catch (\Exception $e) {
logMessage("Warning: Could not fetch client tags for $clientId: " . $e->getMessage());
}
// ── Get Services ──
$svcs = $ucrmApi->get('clients/services', ['clientId' => $clientId]);
if (empty($svcs)) {
$msg = '⚠️ Cliente sin servicios/antenas';
$result['password'] = $msg;
if ($currentPassword !== $msg) {
patchClientAttributes($ucrmApi, $clientId, [
$passwordAttributeId => $msg,
]);
$result['changed'] = true;
} else {
$result['changed'] = false;
}
return $result;
}
$allServicePasswords = [];
$numServices = count($svcs);
$siteName = null;
$deviceInfo = null;
foreach ($svcs as $index => $svc) {
$label = ($numServices > 1) ? "Servicio " . ($index + 1) . ":" : "";
$siteId = $svc['unmsClientSiteId'] ?? null;
$serviceStatus = $svc['status'] ?? null;
$passwordValue = "";
// ── Edge Case 2: Service Ended (2) or Obsolete (5) ──
if ($serviceStatus === 2 || $serviceStatus === 5) {
$passwordValue = 'Servicio Finalizado';
logMessage("Client $clientId: Service " . ($index + 1) . " has status $serviceStatus (Ended/Obsolete)");
$allServicePasswords[] = trim("$label $passwordValue");
continue;
}
if (!$siteId) {
$passwordValue = "⚠️ Sin sitio";
} else {
if ($isTest) {
// Test Env Logic: Preserve existing if valid
$foundInCRM = false;
if (!empty($currentPassword)) {
if ($numServices > 1) {
if (preg_match('/Servicio ' . ($index + 1) . ':\s*([^⚠️\s]+)/', $currentPassword, $matches)) {
$passwordValue = $matches[1];
$foundInCRM = true;
}
} else {
if (strpos($currentPassword, '⚠️') === false && strpos($currentPassword, 'Servicio') === false) {
$passwordValue = trim($currentPassword);
$foundInCRM = true;
}
}
}
if (!$foundInCRM) {
$passwordValue = generateStrongPassword(20);
logMessage("Test Env: Generated new password for Client $clientId (Service $index)");
} else {
logMessage("Test Env: Preserved existing password for Client $clientId");
}
} else {
// Production Logic
try {
$respDev = $unmsClient->get("devices?siteId=$siteId");
$devs = json_decode($respDev->getBody()->getContents(), true);
if (empty($devs)) {
$passwordValue = "⚠️ Sin antena";
} else {
$passVault = null;
$firstDeviceId = null;
// ── Edge Case 3: Check if device is disconnected ──
$firstDev = $devs[0] ?? null;
$deviceStatus = $firstDev['overview']['status'] ?? 'unknown';
if ($deviceStatus === 'disconnected') {
$passwordValue = 'Antena desconectada al momento de obtener la contraseña';
logMessage("Client $clientId: Device is disconnected (siteId=$siteId)");
// Still extract network data even if disconnected
if ($firstDev && $index === 0) {
$siteName = $firstDev['identification']['site']['parent']['name'] ?? null;
$apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null;
$apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null;
if ($apDeviceName && $apDeviceId) {
try {
$respApDev = $unmsClient->get("devices/$apDeviceId");
$apDevData = json_decode($respApDev->getBody()->getContents(), true);
$apDeviceIP = $apDevData['ipAddress'] ?? '';
$deviceInfo = trim("$apDeviceName $apDeviceIP");
} catch (\Exception $e) {
$deviceInfo = $apDeviceName;
}
} elseif (!$apDeviceName) {
$deviceInfo = 'REPETIDOR';
}
}
$allServicePasswords[] = trim("$label $passwordValue");
continue; // Skip vault/regenerate for this service
}
// $firstDev already set above (line 258)
if ($firstDev && $index === 0) {
// Site name from parent site
$siteName = $firstDev['identification']['site']['parent']['name'] ?? null;
// AP Device name and IP
$apDeviceName = $firstDev['attributes']['apDevice']['name'] ?? null;
$apDeviceId = $firstDev['attributes']['apDevice']['id'] ?? null;
if ($apDeviceName && $apDeviceId) {
// Fetch AP device IP
try {
$respApDev = $unmsClient->get("devices/$apDeviceId");
$apDevData = json_decode($respApDev->getBody()->getContents(), true);
$apDeviceIP = $apDevData['ipAddress'] ?? '';
$deviceInfo = trim("$apDeviceName $apDeviceIP");
} catch (\Exception $e) {
$deviceInfo = $apDeviceName;
logMessage("Warning: Could not fetch AP device IP for $apDeviceId: " . $e->getMessage());
}
} elseif (!$apDeviceName) {
// No apDevice = possibly a repeater
$deviceInfo = 'REPETIDOR';
logMessage("Client $clientId: Device is a repeater (no apDevice)");
}
}
foreach ($devs as $dev) {
$deviceId = $dev['identification']['id'] ?? null;
if (!$deviceId) continue;
if (!$firstDeviceId) $firstDeviceId = $deviceId;
try {
$respVault = $unmsClient->get("vault/$deviceId/credentials");
$vault = json_decode($respVault->getBody()->getContents(), true);
if (isset($vault['credentials'][0]['password'])) {
$passVault = $vault['credentials'][0]['password'];
break;
}
} catch (\Exception $e) {
continue;
}
}
if ($passVault) {
$passwordValue = $passVault;
} elseif ($firstDeviceId) {
// Regenerate on Device
$newPass = generateStrongPassword(16);
try {
$unmsClient->post("vault/$firstDeviceId/credentials/regenerate", [
'json' => [['username' => 'ubnt', 'password' => $newPass, 'readOnly' => true]]
]);
$passwordValue = $newPass;
logMessage("Prod Env: Regenerated password on UNMS Device $firstDeviceId");
} catch (\Exception $e) {
$passwordValue = "⚠️ Error Regenerating: " . $e->getMessage();
}
} else {
$passwordValue = "⚠️ Sin dispositivo válido";
}
}
} catch (\Exception $e) {
$passwordValue = "⚠️ Error API UNMS: " . $e->getMessage();
}
}
}
$allServicePasswords[] = trim("$label $passwordValue");
}
$finalValue = implode(' ', $allServicePasswords);
$result['password'] = $finalValue;
$result['siteName'] = $siteName;
$result['deviceInfo'] = $deviceInfo;
// Build attribute updates (always patch all available fields)
$attrUpdates = [];
// Password: only if changed
if ($finalValue !== $currentPassword) {
$attrUpdates[$passwordAttributeId] = $finalValue;
}
// Site: patch if we got data and attribute ID exists
if ($siteName !== null && $siteAttributeId) {
$attrUpdates[$siteAttributeId] = $siteName;
}
// Antena/Sectorial: patch if we got data and attribute ID exists
if ($deviceInfo !== null && $antenaSectorialAttributeId) {
$attrUpdates[$antenaSectorialAttributeId] = $deviceInfo;
}
if (!empty($attrUpdates)) {
patchClientAttributes($ucrmApi, $clientId, $attrUpdates);
logMessage("Client $clientId: Patched " . count($attrUpdates) . " attribute(s)");
}
$result['changed'] = ($finalValue !== $currentPassword) || !empty($attrUpdates);
return $result;
} catch (\Exception $e) {
$result['password'] = "Error fixing: " . $e->getMessage();
return $result;
}
}
/**
* Patch multiple client custom attributes in a single API call.
*/
function patchClientAttributes($ucrmApi, $clientId, array $attributeUpdates)
{
$attributes = [];
foreach ($attributeUpdates as $attrId => $value) {
if ($attrId && $value !== null) {
$attributes[] = ['customAttributeId' => (int)$attrId, 'value' => (string)$value];
}
}
if (!empty($attributes)) {
$ucrmApi->patch("clients/$clientId", ['attributes' => $attributes]);
}
}
/**
* Resolves UCRM custom attribute IDs based on keys.
*/
function resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $antenaSectorialAttributeKey)
{
$customAttributeId = null;
$siteAttributeId = null;
$antenaSectorialAttributeId = null;
try {
$attributes = $ucrmApi->get('custom-attributes', ['attributeType' => 'client']);
foreach ($attributes as $attr) {
match ($attr['key']) {
$customAttributeKey => $customAttributeId = $attr['id'],
$siteAttributeKey => $siteAttributeId = $attr['id'],
$antenaSectorialAttributeKey => $antenaSectorialAttributeId = $attr['id'],
default => null,
};
}
} catch (\Exception $e) {
logMessage("Error fetching attributes: " . $e->getMessage());
return null; // Return null on failure
}
if (!$customAttributeId) {
logMessage("Error: Custom attribute '$customAttributeKey' not found.");
return null;
}
// Log resolved IDs
logMessage("Resolved attribute IDs: password=$customAttributeId, site=" . ($siteAttributeId ?? 'N/A') . ", antenaSectorial=" . ($antenaSectorialAttributeId ?? 'N/A'));
return [
'password' => $customAttributeId,
'site' => $siteAttributeId,
'antenaSectorial' => $antenaSectorialAttributeId,
];
}
// -- Main Execution --
if (!defined('INCLUDED_AS_LIBRARY')) {
// Initialization
$config = PluginConfigManager::create()->loadConfig();
$ipServer = $config['ipserver'] ?? 'localhost';
$apiUrl = "https://$ipServer/crm/api/v1.0/";
$token = $config['apitoken'] ?? '';
if (empty($token)) {
logMessage("Error: API Token is missing in plugin configuration.");
exit(1);
}
// Initialize UCRM Client
$client = new Client([
'base_uri' => $apiUrl,
'verify' => false,
'headers' => [
'X-Auth-App-Key' => $token,
'Content-Type' => 'application/json',
],
]);
$ucrmApi = new UcrmApi($client, $token);
// Initialize UNMS Client
$unmsClient = new Client([
'base_uri' => "https://{$ipServer}/nms/api/v2.1/",
'verify' => false,
'headers' => [
'X-Auth-Token' => $config['unmsApiToken'] ?? ''
]
]);
// Resolve IDs
$attributeIds = resolveAttributeIds($ucrmApi, $customAttributeKey, $siteAttributeKey, $antenaSectorialAttributeKey);
if (!$attributeIds) {
exit(1);
}
if ($fixOption === null) {
// Check for --file argument
$fileOption = null;
foreach ($argv as $arg) {
if (strpos($arg, '--file=') === 0) {
$fileOption = substr($arg, 7);
}
}
if ($fileOption) {
if ($fileOption[0] !== '/') {
$fileOption = $initialDir . '/' . $fileOption;
}
if (!file_exists($fileOption)) {
logMessage("Error: File not found: $fileOption");
exit(1);
}
logMessage("Processing from file: $fileOption");
// Read all lines
$lines = file($fileOption, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
// Process lines
$originalCount = count($lines);
$processedCount = 0;
// Helper to rewrite file
$rewriteFile = function ($remainingLines) use ($fileOption) {
file_put_contents($fileOption, implode("\n", $remainingLines) . "\n");
};
$header = null;
if (!empty($lines)) {
$firstLine = $lines[0];
$parts = explode(',', $firstLine);
if (!is_numeric($parts[0])) {
$header = array_shift($lines); // Remove header from processing list
}
}
$queue = $lines; // Working copy
while (!empty($queue)) {
$line = array_shift($queue); // Take next
if (empty(trim($line))) continue;
$clientId = trim($line);
if (!is_numeric($clientId)) {
logMessage("Skipping invalid line: $line");
continue;
}
$clientId = (int)$clientId;
logMessage("Processing ID from file: $clientId");
try {
$clientDataResponse = $ucrmApi->get("clients/$clientId");
$currentAttributes = $clientDataResponse['attributes'] ?? [];
$currentPass = null;
foreach ($currentAttributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$currentPass = $attr['value'];
break;
}
}
$fixResult = fixClientData($clientId, $ucrmApi, $unmsClient, $attributeIds, $config, $currentPass);
$newPass = $fixResult['password'];
$siteInfo = $fixResult['siteName'] ? " | Site: {$fixResult['siteName']}" : '';
$devInfo = $fixResult['deviceInfo'] ? " | Antena: {$fixResult['deviceInfo']}" : '';
logMessage("Client $clientId result: $newPass$siteInfo$devInfo");
} catch (\Exception $e) {
logMessage("Error processing Client $clientId: " . $e->getMessage());
}
// Rewrite file
$validLines = $queue;
if ($header) {
array_unshift($validLines, $header);
}
$rewriteFile($validLines);
}
logMessage("File processing complete.");
exit(0);
}
}
if ($fixLimit > 0) {
logMessage("Fix mode enabled. Limit: " . ($fixOption === 'all' ? 'ALL' : $fixLimit));
}
// 1. Fetch all clients
logMessage("Fetching clients...");
$clients = fetchAllClients($client);
logMessage("Total active clients fetched: " . count($clients));
// 2. Filter & Fix
$results = [];
$fixedCount = 0;
foreach ($clients as $clientData) {
$password = null;
$attributes = $clientData['attributes'] ?? [];
foreach ($attributes as $attr) {
if ($attr['key'] === $customAttributeKey) {
$password = $attr['value'];
break;
}
}
// Validation Logic
$isValid = true;
$reason = "";
// Detect explicit error states that should be retried (e.g. "⚠️ Error Regenerating...")
$isError = (stripos((string)$password, 'Error') !== false);
$isManaged = (strpos((string)$password, 'Servicio') !== false || strpos((string)$password, '⚠️') !== false);
if (empty($password)) {
$isValid = false;
$reason = "Missing";
} elseif ($isError) {
$isValid = false;
$reason = "Error (Retry)";
} elseif (!$isManaged) {
$len = strlen($password);
if ($len < 8 || $len > 20) {
$isValid = false;
$reason = "Length ($len)";
} elseif (strpos($password, ' ') !== false) {
$isValid = false;
$reason = "Spaces";
}
}
if (!$isValid) {
$newPassword = "";
$actionTaken = "None";
// Fix Logic
if ($fixLimit > 0 && $fixedCount < $fixLimit) {
logMessage("Fixing client {$clientData['id']}...");
$fixResult = fixClientData($clientData['id'], $ucrmApi, $unmsClient, $attributeIds, $config, $password);
$newPassword = $fixResult['password'];
// Basic check if fix resulted in a change/value
if ($fixResult['changed'] ?? false) {
$actionTaken = "Updated";
$fixedCount++;
} else {
$actionTaken = "Attempted (No Change/Error)";
}
}
$results[] = [
'clientId' => $clientData['id'],
'name' => ($clientData['firstName'] ?? '') . ' ' . ($clientData['lastName'] ?? ''),
'userIdent' => $clientData['userIdent'] ?? '',
'currentPassword' => $password ?? '',
'action' => $actionTaken,
'newPassword' => $newPassword
];
}
}
// 3. Output CSV
logMessage("Found " . count($results) . " clients with invalid passwords.");
if ($fixLimit > 0) {
logMessage("Fixed $fixedCount clients.");
}
echo "clientId,name,userIdent,currentPassword,action,newPassword\n";
foreach ($results as $row) {
echo sprintf(
"%s,\"%s\",\"%s\",\"%s\",\"%s\",\"%s\"\n",
$row['clientId'],
str_replace('"', '""', $row['name']),
str_replace('"', '""', $row['userIdent']),
str_replace('"', '""', $row['currentPassword']),
$row['action'],
str_replace('"', '""', $row['newPassword'])
);
}
logMessage("Done.");
}