from datetime import datetime
import time
import agent_util
import platform
import os
import sys
import logging
import socket
try:
# Python 2.x
import httplib
except:
import http.client as httplib
try:
import psutil
except:
psutil = None
try:
import distro
except:
distro = None
try: import json
except ImportError:
try: import simplejson as json
# it's possible that we may not need json for the action that we're taking.
# for example, for the rpm post install script, on a python version that
# doesn't have json, we'll get this far in the code. but the post
# install doesn't use json, so we're fine
except ImportError: json = None
def mac_address_octets(n):
if sys.version_info[0] == 2:
rng = xrange
else:
# python 3x uses range
rng = range
for i in rng(6):
yield n & 0xFF
n >>= 8
def int_to_mac_address(n):
"""
Expresses a decimal integer in standard MAC address format
ex:
7267271067680 -> '06:9c:0b:1c:48:20'
"""
values = ['%02x' % a for a in mac_address_octets(n)]
return ':'.join(reversed(values))
def get_fqdn():
hostname = socket.getfqdn()
if 'darwin' == sys.platform:
"""
Mac is odd around ipv4/6 addresses - see:
https://stackoverflow.com/questions/8625029/why-does-pythons-socket-getfqdn-return-a-long-string-that-looks-like-an-ipv6
"""
if hostname.lower().endswith('.arpa'):
hostname = socket.gethostname()
return hostname
def get_server_name():
if 'darwin' != sys.platform:
return None
try:
"""
For Fortisase integration, get the value we want to specify for server_name, which
is sent up in handshake and also for config changes. This allows us to match the
name provided by Forticlient.
"""
sc_util = agent_util.which('scutil', exc=True)
_, output = agent_util.execute_command('{} --get ComputerName'.format(sc_util))
return output.strip()
except:
return None
def get_platform_uuids():
log = logging.getLogger('Inspector')
my_platform = sys.platform.lower()
#
# The keys in the rv dictionary should match the textkeys
# in the table ServerIdentifierType
rv = {
'platform' : sys.platform.lower()
}
if 'darwin' == my_platform:
try:
ioreg_bin = agent_util.which('ioreg')
rc, output = agent_util.execute_command(
"{} -d2 -c IOPlatformExpertDevice | grep IOPlatformUUID".format(ioreg_bin))
if 0 == rc:
rv['uuid'] = output.strip().split("=")[1].strip().strip("\"")
else:
log.error('ioreg error: {}'.format(rc))
except Exception as e:
log.info('Gather UUID failure: {}'.format(str(e)))
pass
return rv
class Inspector(object):
SOURCE_LIST_PATHS = ['/etc/apt/sources.list', '/etc/yum.repos.d/panopta.repo']
PANOPTA_REPO = 'http://packages.panopta.com/'
def __init__(self, agent):
self.agent = agent
def get_all_facts(self, wifi_info=None):
facts = {}
log = logging.getLogger('Inspector')
try:
facts.update(self.get_agent_facts())
facts.update(self.get_python_facts())
facts.update(self.get_process_facts())
facts.update(self.get_mac_addresses())
facts.update(self.get_hardware_facts())
facts.update(self.get_time_facts())
facts.update(self.get_hostname())
facts.update(self.get_platform_facts())
facts.update(self.get_machine_facts())
facts.update(self.get_wifi_facts(wifi_info))
facts.update(self.get_dem_network_facts())
if sys.version_info >= (2, 6, 0):
# Cloud detection, but only if we're on a new enough Python to have timeout
facts.update(self.get_cloud_facts())
except Exception as e:
log.info('get_all facts: {}'.format(e))
return facts
def get_platform_facts(self):
facts = {}
if 'darwin' == sys.platform:
try:
_, output = agent_util.execute_command('sw_vers | grep ProductVersion')
pv = output.split(':')[1].strip()
facts['MacOSVersion'] = pv
except:
pass
return facts
def get_hostname(self):
facts = {}
facts['hostname'] = get_fqdn()
return facts
def get_time_facts(self):
facts = {}
try:
retcode, output = agent_util.execute_command("ls -l /etc/localtime")
timezone = "/".join(output.strip().split("/")[-2:])
facts['timezone'] = timezone
except:
log = logging.getLogger(self.__class__.__name__)
log.exception("Unable to get Time Zone")
return facts
def get_hardware_facts(self):
"""
Gather CPU and memory specs for the machine
"""
facts = {"hardware": {}}
if 'darwin' == sys.platform.lower():
try:
retcode, output = agent_util.execute_command("sysctl -a | grep machdep.cpu")
for line in output.strip().split("\n"):
try:
key, value = line.strip().split(":", 1)
if key in ('machdep.cpu.brand_string', 'machdep.cpu.core_count'):
facts['hardware'][key] = value.strip()
except:
continue
except:
log = logging.getLogger(self.__class__.__name__)
log.exception("Unable to get CPU hardware facts")
try:
total_mem = os.sysconf('SC_PHYS_PAGES') * os.sysconf('SC_PAGE_SIZE')
facts['hardware']['mem_total'] = total_mem
except:
log = logging.getLogger(self.__class__.__name__)
log.exception("Unable to get memory hardware facts")
else:
try:
retcode, output = agent_util.execute_command("lscpu")
for line in output.strip().split("\n"):
try:
key, value = line.strip().split(":", 1)
except:
continue
key = key.strip().lower().replace("(s)", "").replace(" ", "_")
value = value.strip()
facts["hardware"][key] = value
except:
log = logging.getLogger(self.__class__.__name__)
log.exception("Unable to get CPU hardware facts")
try:
retcode, output = agent_util.execute_command("free -m")
for line in output.strip().split("\n"):
fields = line.strip().split()
if fields[0].lower().startswith("mem"):
facts["hardware"]["mem_total"] = int(fields[1])
if fields[0].lower().startswith("swap"):
facts["hardware"]["swap_total"] = int(fields[1])
except:
log = logging.getLogger(self.__class__.__name__)
log.exception("Unable to get memory hardware facts")
return facts
def get_agent_facts(self):
facts = {}
# This is a little hokey, but the last time this file's metadata changed
# *should* be when this file whas created, ie. when the Agent was
# installed. I thought this was better than storing the install time in
# the local database, since there is a possibility that it could get
# corrupted.
facts['installed_time'] = os.path.getctime(os.path.abspath(__file__))
facts['used_manifest'] = os.path.exists(self.agent.manifest_file)
facts['installed_from_repo'] = False
for source_list_path in self.SOURCE_LIST_PATHS:
if os.path.exists(source_list_path):
try:
source_list = open(source_list_path)
facts['installed_from_repo'] = self.PANOPTA_REPO in source_list.read()
source_list.close()
if facts['installed_from_repo']:
break
except:
pass
brand = 'panopta'
if 'darwin' == sys.platform or os.path.exists("/etc/fm-agent"):
brand = 'fortimonitor'
# Set the agent brand, default to Panopta unless we have an FM-Agent config file
facts["agent_brand"] = brand
return facts
def get_python_facts(self):
facts = {"python": {}}
facts["python"]["platform"] = platform.platform()
facts["python"]["processor"] = platform.processor()
facts["python"]["version"] = platform.python_version()
facts["python"]["uname"] = platform.uname()
try:
facts["python"]["dist"] = platform.dist()
except AttributeError:
# Removed in Python 3.8.
# https://docs.python.org/2.7/library/platform.html#platform.linux_distribution
if distro:
facts["python"]["dist"] = ' '.join(distro.linux_distribution())
facts["python"]["sys_platform"] = sys.platform
return facts
def get_cloud_facts(self):
facts = {}
# Try Amazon
try:
h = httplib.HTTPConnection("169.254.169.254", timeout=5)
h.request("GET", "/latest/dynamic/instance-identity/document")
r = h.getresponse()
if r.status == 200:
data = json.loads(r.read())
facts["cloud_provider"] = "aws"
facts["cloud_instance_id"] = data["instanceId"]
facts["cloud_metadata"] = data
return facts
except:
pass
# Try Google
try:
headers = {"Metadata-Flavor": "Google"}
h = httplib.HTTPConnection("metadata.google.internal", timeout=5)
h.request("GET", "/computeMetadata/v1/instance/?recursive=true", headers=headers)
r = h.getresponse()
if r.status == 200:
data = json.loads(r.read())
facts["cloud_provider"] = "gcp"
facts["cloud_instance_id"] = data["id"]
# Strip out sensitive keys
if "serviceAccounts" in data:
del data["serviceAccounts"]
for key, value in data.get('attributes', {}).items():
if key in ['/attribute', '/sshkeys', '/vmdnssetting', '/enable-oslogin']:
data['attributes'].pop(key)
facts["cloud_metadata"] = data
try:
manifest = self.agent.get_manifest()
enabled_gcp_attributes = manifest.get('agent', 'enable_gcp_attributes') == 'true'
facts["cloud_metadata"]["enable_gcp_attributes"] = enabled_gcp_attributes
except Exception:
log = logging.getLogger(self.__class__.__name__)
log.exception('Unable to parse manifest file to determine gcp attributes actions.')
return facts
except:
pass
# Try Azure
try:
headers = {"Metadata": "true"}
h = httplib.HTTPConnection("169.254.169.254", timeout=5)
h.request("GET", "/metadata/instance?api-version=2017-04-02", headers=headers)
r = h.getresponse()
if r.status == 200:
data = json.loads(r.read())
facts["cloud_provider"] = "azure"
facts["cloud_instance_id"] = data["compute"]["vmId"]
facts["cloud_metadata"] = data
return facts
except:
pass
# No cloud detected
return {}
def get_process_facts(self):
facts = {}
if psutil is None:
return facts
processes = set()
for proc in psutil.process_iter():
processes.add(proc.name())
facts["processes"] = list(processes)
return facts
def mac_address_iter(self):
import psutil
for iface, addrs in psutil.net_if_addrs().items():
for addr in addrs:
if addr.family != psutil.AF_LINK:
continue
if addr.address == '00:00:00:00:00:00':
continue
yield addr.address
def get_mac_addresses(self):
facts = {}
try:
import uuid
facts['uuid_getnode'] = int_to_mac_address(uuid.getnode())
except:
log = logging.getLogger(self.__class__.__name__)
log.info("Unable to import uuid module. Skipping MAC address fact collection.")
try:
addrs = []
for i, mac_addr in enumerate(self.mac_address_iter()):
addrs.append(mac_addr)
facts['macaddress'] = addrs
except ImportError:
log = logging.getLogger(self.__class__.__name__)
log.info("Unable to import psutil, skipping MAC address fact collection")
except:
log = logging.getLogger(self.__class__.__name__)
log.info("Unknown error during MAC address collection")
return facts
def get_machine_facts(self):
md = {}
if 'darwin' != sys.platform:
return md
facts = {
'machine_description' : md
}
try:
tk = 'SPHardwareDataType'
data = self.query_system_profiler(tk)
if data:
info = data[tk][0]
md['serial'] = info.get('serial_number', '')
md['model'] = info.get('machine_model', '')
except:
facts.clear()
return facts
def get_wifi_facts(self, wifi_info=None):
md = {}
facts = {
'DEMWifiInfo' : md
}
try:
md['ssid'] = wifi_info.get('SSID', '')
md['bssid'] = wifi_info.get('BSSID', '')
md['channel'] = wifi_info.get('channel', '')
md['security_algorithm'] = wifi_info.get('link auth', '')
md['nss'] = wifi_info.get('NSS', '')
except:
md.clear()
md['timestamp'] = datetime.utcnow().timestamp()
return facts
def get_dem_network_facts(self):
if 'darwin' != sys.platform:
return {}
md = []
facts = {
'DEMNetworkInterfaceConfigurations' : md
}
try:
tk = 'SPNetworkDataType'
info = self.query_system_profiler(tk)
if info:
network_infos = info.get(tk, [])
for ni in network_infos:
router = ni.get('IPv4', {}).get('Router', None)
if not router:
continue
item = {
'name' : ni.get('_name'),
'description' : ni.get('hardware'),
'dns_servers' : [],
'gateways' : [router]
}
md.append(item)
dns_info = ni.get('DNS', None)
if dns_info:
for sa in dns_info.get('ServerAddresses', []):
item['dns_servers'].append(sa)
except:
logging.exception('get_dem_network_facts')
facts.clear()
return facts
def query_system_profiler(self, info_type):
try:
sp = agent_util.which('system_profiler')
from subprocess import Popen, PIPE
d = Popen([sp, '-json', info_type], stdout=PIPE)
data = json.loads(d.stdout.read())
return data
except:
logging.exception('query_system_profiler')
return None