Re-implement NAC as a class based module (#9573)

* Less sql queries for nac module

* Re-implement NAC as a class based module

* Update comments

* update module capture order

* Fix style issues
This commit is contained in:
Tony Murray
2018-12-20 19:50:12 -06:00
committed by GitHub
parent 272e511964
commit 587c17e215
11 changed files with 280 additions and 84 deletions

View File

@@ -0,0 +1,56 @@
<?php
/**
* Module.php
*
* LibreNMS Discovery/Poller Module
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Interfaces;
use LibreNMS\OS;
interface Module
{
/**
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
*/
public function discover(OS $os);
/**
* Poll data for this module and update the DB / RRD.
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
*/
public function poll(OS $os);
/**
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
*/
public function cleanup(OS $os);
}

View File

@@ -0,0 +1,36 @@
<?php
/**
* NacPolling.php
*
* Nac Polling interface
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Interfaces\Polling;
use Illuminate\Support\Collection;
interface NacPolling
{
/**
* @return Collection PortNac objects
*/
public function pollNac();
}

93
LibreNMS/Modules/Nac.php Normal file
View File

@@ -0,0 +1,93 @@
<?php
/**
* Nac.php
*
* network access controls module
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @package LibreNMS
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
*/
namespace LibreNMS\Modules;
use App\Models\PortsNac;
use LibreNMS\Interfaces\Module;
use LibreNMS\Interfaces\Polling\NacPolling;
use LibreNMS\OS;
use LibreNMS\Util\ModuleModelObserver;
class Nac implements Module
{
/**
* Discover this module. Heavier processes can be run here
* Run infrequently (default 4 times a day)
*
* @param OS $os
*/
public function discover(OS $os)
{
// not implemented
}
/**
* Poll data for this module and update the DB / RRD.
* Try to keep this efficient and only run if discovery has indicated there is a reason to run.
* Run frequently (default every 5 minutes)
*
* @param OS $os
*/
public function poll(OS $os)
{
if ($os instanceof NacPolling) {
// discovery output (but don't install it twice (testing can can do this)
if (!PortsNac::getEventDispatcher()->hasListeners('eloquent.created: App\Models\PortsNac')) {
PortsNac::observe(new ModuleModelObserver());
}
$nac_entries = $os->pollNac()->keyBy('mac_address');
$existing_entries = $os->getDeviceModel()->portsNac->keyBy('mac_address');
// update existing models
foreach ($nac_entries as $nac_entry) {
if ($existing = $existing_entries->get($nac_entry->mac_address)) {
$nac_entries->put($nac_entry->mac_address, $existing->fill($nac_entry->attributesToArray()));
}
}
// persist to DB
$os->getDeviceModel()->portsNac()->saveMany($nac_entries);
$delete = $existing_entries->diffKeys($nac_entries)->pluck('ports_nac_id');
if ($delete->isNotEmpty()) {
$count = PortsNac::query()->whereIn('ports_nac_id', $delete)->delete();
d_echo('Deleted ' . $count, str_repeat('-', $count));
}
}
}
/**
* Remove all DB data for this module.
* This will be run when the module is disabled.
*
* @param OS $os
*/
public function cleanup(OS $os)
{
$os->getDeviceModel()->portsNac()->delete();
}
}

View File

@@ -25,6 +25,7 @@
namespace LibreNMS;
use App\Models\Device;
use LibreNMS\Device\WirelessSensor;
use LibreNMS\Device\YamlDiscovery;
use LibreNMS\Interfaces\Discovery\ProcessorDiscovery;
@@ -42,6 +43,7 @@ class OS implements ProcessorDiscovery
}
private $device; // annoying use of references to make sure this is in sync with global $device variable
private $device_model;
private $cache; // data cache
private $pre_cache; // pre-fetch data cache
@@ -74,6 +76,20 @@ class OS implements ProcessorDiscovery
return (int)$this->device['device_id'];
}
/**
* Get the Eloquent Device Model for the current device
*
* @return Device
*/
public function getDeviceModel()
{
if (is_null($this->device_model)) {
$this->device_model = Device::find($this->getDeviceId());
}
return $this->device_model;
}
public function preCache()
{
if (is_null($this->pre_cache)) {

View File

@@ -21,15 +21,19 @@
* @link http://librenms.org
* @copyright 2018 Tony Murray
* @author Tony Murray <murraytony@gmail.com>
* @copyright 2018 Jose Augusto Cardoso
*/
namespace LibreNMS\OS\Shared;
use App\Models\PortsNac;
use LibreNMS\Device\Processor;
use LibreNMS\Interfaces\Discovery\ProcessorDiscovery;
use LibreNMS\Interfaces\Polling\NacPolling;
use LibreNMS\OS;
use LibreNMS\Util\IP;
class Cisco extends OS implements ProcessorDiscovery
class Cisco extends OS implements ProcessorDiscovery, NacPolling
{
/**
* Discover processors.
@@ -112,4 +116,46 @@ class Cisco extends OS implements ProcessorDiscovery
return $processors;
}
public function pollNac()
{
$nac = collect();
$portAuthSessionEntry = snmpwalk_cache_oid($this->getDevice(), 'cafSessionEntry', [], 'CISCO-AUTH-FRAMEWORK-MIB');
if (!empty($portAuthSessionEntry)) {
$cafSessionMethodsInfoEntry = collect(snmpwalk_cache_oid($this->getDevice(), 'cafSessionMethodsInfoEntry', [], 'CISCO-AUTH-FRAMEWORK-MIB'))->mapWithKeys(function ($item, $key) {
$key_parts = explode('.', $key);
$key = implode('.', array_slice($key_parts, 0, 2)); // remove the auth method
return [$key => ['method' => $key_parts[2], 'authc_status' => $item['cafSessionMethodState']]];
});
// cache port ifIndex -> port_id map
$ifIndex_map = $this->getDeviceModel()->ports()->pluck('port_id', 'ifIndex');
// update the DB
foreach ($portAuthSessionEntry as $index => $portAuthSessionEntryParameters) {
list($ifIndex, $auth_id) = explode('.', str_replace("'", '', $index));
$session_info = $cafSessionMethodsInfoEntry->get($ifIndex . '.' . $auth_id);
$mac_address = strtolower(implode(array_map('zeropad', explode(':', $portAuthSessionEntryParameters['cafSessionClientMacAddress']))));
$nac->put($mac_address, new PortsNac([
'port_id' => $ifIndex_map->get($ifIndex, 0),
'mac_address' => $mac_address,
'auth_id' => $auth_id,
'domain' => $portAuthSessionEntryParameters['cafSessionDomain'],
'username' => $portAuthSessionEntryParameters['cafSessionAuthUserName'],
'ip_address' => (string)IP::fromHexString($portAuthSessionEntryParameters['cafSessionClientAddress'], true),
'host_mode' => $portAuthSessionEntryParameters['cafSessionAuthHostMode'],
'authz_status' => $portAuthSessionEntryParameters['cafSessionStatus'],
'authz_by' => $portAuthSessionEntryParameters['cafSessionAuthorizedBy'],
'timeout' => $portAuthSessionEntryParameters['cafSessionTimeout'],
'time_left' => $portAuthSessionEntryParameters['cafSessionTimeLeft'],
'authc_status' => $session_info['authc_status'],
'method' => $session_info['method'],
]));
}
}
return $nac;
}
}

View File

@@ -27,7 +27,7 @@ namespace LibreNMS\Util;
use Illuminate\Database\Eloquent\Model as Eloquent;
class DiscoveryModelObserver
class ModuleModelObserver
{
public function saving(Eloquent $model)
{

View File

@@ -499,6 +499,11 @@ class Device extends BaseModel
return $this->hasMany('App\Models\Port', 'device_id', 'device_id');
}
public function portsNac()
{
return $this->hasMany('App\Models\PortsNac', 'device_id', 'device_id');
}
public function processors()
{
return $this->hasMany('App\Models\Processor', 'device_id');

View File

@@ -32,19 +32,19 @@ class PortsNac extends BaseModel
public $timestamps = false;
protected $fillable = [
'auth_id',
'port_id',
'device_id',
'port_id',
'domain',
'username',
'mac_address',
'ip_address',
'authz_status',
'domain',
'host_mode',
'username',
'authz_status',
'authz_by',
'timeout',
'time_left',
'authc_status',
'method',
'timeout',
'time_left',
];

View File

@@ -24,65 +24,9 @@
* @author Tony Murray <murraytony@gmail.com>
*/
use App\Models\Port;
use App\Models\PortsNac;
use LibreNMS\Util\DiscoveryModelObserver;
use LibreNMS\Util\IP;
use LibreNMS\OS;
echo "\nCisco-NAC\n";
// cache port ifIndex -> port_id map
$ports_map = Port::where('device_id', $device['device_id'])->pluck('port_id', 'ifIndex');
$port_nac_ids = [];
// discovery output (but don't install it twice (testing can can do this)
if (!PortsNac::getEventDispatcher()->hasListeners('eloquent.created: App\Models\PortsNac')) {
PortsNac::observe(new DiscoveryModelObserver());
if (!$os instanceof OS) {
$os = OS::make($device);
}
// collect data via snmp and reorganize the session method entry a bit
$portAuthSessionEntry = snmpwalk_cache_oid($device, 'cafSessionEntry', [], 'CISCO-AUTH-FRAMEWORK-MIB');
if (!empty($portAuthSessionEntry)) {
$cafSessionMethodsInfoEntry = collect(snmpwalk_cache_oid($device, 'cafSessionMethodsInfoEntry', [], 'CISCO-AUTH-FRAMEWORK-MIB'))->mapWithKeys(function ($item, $key) {
$key_parts = explode('.', $key);
$key = implode('.', array_slice($key_parts, 0, 2)); // remove the auth method
return [$key => ['method' => $key_parts[2], 'authc_status' => $item['cafSessionMethodState']]];
});
}
// update the DB
foreach ($portAuthSessionEntry as $index => $portAuthSessionEntryParameters) {
$auth_id = trim(strstr($index, "'"), "'");
$ifIndex = substr($index, 0, strpos($index, "."));
$session_info = $cafSessionMethodsInfoEntry->get($ifIndex . '.' . $auth_id);
$port_nac = PortsNac::updateOrCreate([
'port_id' => $ports_map->get($ifIndex, 0),
'mac_address' => strtolower(implode(array_map('zeropad', explode(':', $portAuthSessionEntryParameters['cafSessionClientMacAddress'])))),
], [
'auth_id' => $auth_id,
'device_id' => $device['device_id'],
'domain' => $portAuthSessionEntryParameters['cafSessionDomain'],
'username' => $portAuthSessionEntryParameters['cafSessionAuthUserName'],
'ip_address' => (string)IP::fromHexString($portAuthSessionEntryParameters['cafSessionClientAddress'], true),
'host_mode' => $portAuthSessionEntryParameters['cafSessionAuthHostMode'],
'authz_status' => $portAuthSessionEntryParameters['cafSessionStatus'],
'authz_by' => $portAuthSessionEntryParameters['cafSessionAuthorizedBy'],
'authc_status' => $session_info['authc_status'],
'timeout' => $portAuthSessionEntryParameters['cafSessionTimeout'],
'time_left' => $portAuthSessionEntryParameters['cafSessionTimeLeft'],
'method' => $session_info['method'],
]);
// save valid ids
$port_nac_ids[] = $port_nac->ports_nac_id;
}
// delete old entries
$count = \LibreNMS\DB\Eloquent::DB()->table('ports_nac')->whereNotIn('ports_nac_id', $port_nac_ids)->delete();
d_echo('Deleted ' . $count, str_repeat('-', $count));
// \App\Models\PortsNac::whereNotIn('ports_nac_id', $port_nac_ids)->get()->each->delete(); // alternate delete to trigger model events
unset($port_nac_ids, $ports_map, $portAuthSessionEntry, $cafSessionMethodsInfoEntry, $port_nac);
(new \LibreNMS\Modules\Nac())->poll($os);

View File

@@ -2577,21 +2577,6 @@
"time_left": "23",
"ifIndex": 10002
},
{
"auth_id": "000000000000000405F13EAB",
"domain": "data",
"username": "username4",
"mac_address": "788a207f0e95",
"ip_address": "0.0.0.0",
"host_mode": "multiAuth",
"authz_status": "authorizationSuccess",
"authz_by": "Authentication Server",
"authc_status": "authcSuccess",
"method": "other",
"timeout": "5",
"time_left": "12",
"ifIndex": 10003
},
{
"auth_id": "000000000000000505F17F8C",
"domain": "data",
@@ -2606,6 +2591,21 @@
"timeout": "6",
"time_left": "0",
"ifIndex": 10003
},
{
"auth_id": "000000000000000405F13EAB",
"domain": "data",
"username": "username4",
"mac_address": "788a207f0e95",
"ip_address": "0.0.0.0",
"host_mode": "multiAuth",
"authz_status": "authorizationSuccess",
"authz_by": "Authentication Server",
"authc_status": "authcSuccess",
"method": "other",
"timeout": "5",
"time_left": "12",
"ifIndex": 10003
}
]
}

View File

@@ -39,7 +39,7 @@ nac:
excluded_fields: [ports_nac_id, device_id, port_id]
joins:
- { left: ports_nac.port_id, right: ports.port_id, select: [ifIndex] }
order_by: ports.ifIndex, domain
order_by: ports.ifIndex, mac_address
os:
devices:
included_fields: [sysName, sysObjectID, sysDescr, sysContact, version, hardware, features, location, os, type, serial, icon]