Merge pull request #707 from guardicore/feature/configuration_improvement

Configuration UI improvements
This commit is contained in:
VakarisZ 2020-07-27 20:32:48 +03:00 committed by GitHub
commit c1c412f176
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
92 changed files with 1963 additions and 1368 deletions

View File

@ -83,7 +83,7 @@ script:
- cd monkey_island/cc/ui - cd monkey_island/cc/ui
- npm ci # See https://docs.npmjs.com/cli/ci.html - npm ci # See https://docs.npmjs.com/cli/ci.html
- eslint ./src --quiet # Test for errors - eslint ./src --quiet # Test for errors
- JS_WARNINGS_AMOUNT_UPPER_LIMIT=70 - JS_WARNINGS_AMOUNT_UPPER_LIMIT=25
- eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT # Test for max warnings - eslint ./src --max-warnings $JS_WARNINGS_AMOUNT_UPPER_LIMIT # Test for max warnings
# Build documentation # Build documentation

View File

@ -5,4 +5,4 @@ draft: false
tags: ["exploit", "windows"] tags: ["exploit", "windows"]
--- ---
Brute forces WMI (Windows Management Instrumentation) using credentials provided by user ((see ["Configuration"](../usage/configuration))) and hashes gathered by mimikatz. Brute forces WMI (Windows Management Instrumentation) using credentials provided by user (see ["Configuration"](../usage/configuration)) and hashes gathered by mimikatz.

View File

@ -2,3 +2,5 @@ AWS_COLLECTOR = "AwsCollector"
HOSTNAME_COLLECTOR = "HostnameCollector" HOSTNAME_COLLECTOR = "HostnameCollector"
ENVIRONMENT_COLLECTOR = "EnvironmentCollector" ENVIRONMENT_COLLECTOR = "EnvironmentCollector"
PROCESS_LIST_COLLECTOR = "ProcessListCollector" PROCESS_LIST_COLLECTOR = "ProcessListCollector"
MIMIKATZ_COLLECTOR = "MimikatzCollector"
AZURE_CRED_COLLECTOR = "AzureCollector"

View File

@ -0,0 +1,3 @@
# Defined in UI on ValidationFormats.js
IP_RANGE = "ip-range"
IP = "ip"

View File

@ -129,13 +129,13 @@ class Configuration(object):
finger_classes = [] finger_classes = []
exploiter_classes = [] exploiter_classes = []
system_info_collectors_classes = [] system_info_collector_classes = []
# how many victims to look for in a single scan iteration # how many victims to look for in a single scan iteration
victims_max_find = 100 victims_max_find = 100
# how many victims to exploit before stopping # how many victims to exploit before stopping
victims_max_exploit = 15 victims_max_exploit = 100
# depth of propagation # depth of propagation
depth = 2 depth = 2
@ -267,16 +267,6 @@ class Configuration(object):
# Shares to not check if they're writable. # Shares to not check if they're writable.
sambacry_shares_not_to_check = ["IPC$", "print$"] sambacry_shares_not_to_check = ["IPC$", "print$"]
# system info collection
collect_system_info = True
should_use_mimikatz = True
###########################
# systeminfo config
###########################
extract_azure_creds = True
########################### ###########################
# post breach actions # post breach actions
########################### ###########################

View File

@ -99,7 +99,7 @@
], ],
"timeout_between_iterations": 10, "timeout_between_iterations": 10,
"use_file_logging": true, "use_file_logging": true,
"victims_max_exploit": 15, "victims_max_exploit": 100,
"victims_max_find": 100, "victims_max_find": 100,
"post_breach_actions": [] "post_breach_actions": []
custom_PBA_linux_cmd = "" custom_PBA_linux_cmd = ""

View File

@ -190,23 +190,23 @@ class InfectionMonkey(object):
if self._default_server: if self._default_server:
if self._network.on_island(self._default_server): if self._network.on_island(self._default_server):
machine.set_default_server(get_interface_to_target(machine.ip_addr) + machine.set_default_server(get_interface_to_target(machine.ip_addr) +
(':' + self._default_server_port if self._default_server_port else '')) (
':' + self._default_server_port if self._default_server_port else ''))
else: else:
machine.set_default_server(self._default_server) machine.set_default_server(self._default_server)
LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server)) LOG.debug("Default server for machine: %r set to %s" % (machine, machine.default_server))
# Order exploits according to their type # Order exploits according to their type
if WormConfiguration.should_exploit: self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value)
self._exploiters = sorted(self._exploiters, key=lambda exploiter_: exploiter_.EXPLOIT_TYPE.value) host_exploited = False
host_exploited = False for exploiter in [exploiter(machine) for exploiter in self._exploiters]:
for exploiter in [exploiter(machine) for exploiter in self._exploiters]: if self.try_exploiting(machine, exploiter):
if self.try_exploiting(machine, exploiter): host_exploited = True
host_exploited = True VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send()
VictimHostTelem('T1210', ScanStatus.USED, machine=machine).send() break
break if not host_exploited:
if not host_exploited: self._fail_exploitation_machines.add(machine)
self._fail_exploitation_machines.add(machine) VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
VictimHostTelem('T1210', ScanStatus.SCANNED, machine=machine).send()
if not self._keep_running: if not self._keep_running:
break break
@ -242,11 +242,10 @@ class InfectionMonkey(object):
LOG.debug("Running with depth: %d" % WormConfiguration.depth) LOG.debug("Running with depth: %d" % WormConfiguration.depth)
def collect_system_info_if_configured(self): def collect_system_info_if_configured(self):
if WormConfiguration.collect_system_info: LOG.debug("Calling system info collection")
LOG.debug("Calling system info collection") system_info_collector = SystemInfoCollector()
system_info_collector = SystemInfoCollector() system_info = system_info_collector.get_info()
system_info = system_info_collector.get_info() SystemInfoTelem(system_info).send()
SystemInfoTelem(system_info).send()
def shutdown_by_not_alive_config(self): def shutdown_by_not_alive_config(self):
if not WormConfiguration.alive: if not WormConfiguration.alive:
@ -387,7 +386,8 @@ class InfectionMonkey(object):
:raises PlannedShutdownException if couldn't find the server. :raises PlannedShutdownException if couldn't find the server.
""" """
if not ControlClient.find_server(default_tunnel=self._default_tunnel): if not ControlClient.find_server(default_tunnel=self._default_tunnel):
raise PlannedShutdownException("Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel)) raise PlannedShutdownException(
"Monkey couldn't find server with {} default tunnel.".format(self._default_tunnel))
self._default_server = WormConfiguration.current_server self._default_server = WormConfiguration.current_server
LOG.debug("default server set to: %s" % self._default_server) LOG.debug("default server set to: %s" % self._default_server)

View File

@ -4,6 +4,7 @@ from enum import IntEnum
import psutil import psutil
from common.data.system_info_collectors_names import AZURE_CRED_COLLECTOR
from infection_monkey.network.info import get_host_subnets from infection_monkey.network.info import get_host_subnets
from infection_monkey.system_info.azure_cred_collector import AzureCollector from infection_monkey.system_info.azure_cred_collector import AzureCollector
from infection_monkey.system_info.netstat_collector import NetstatCollector from infection_monkey.system_info.netstat_collector import NetstatCollector
@ -91,7 +92,7 @@ class InfoCollector(object):
# noinspection PyBroadException # noinspection PyBroadException
try: try:
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
if not WormConfiguration.extract_azure_creds: if AZURE_CRED_COLLECTOR not in WormConfiguration.system_info_collector_classes:
return return
LOG.debug("Harvesting creds if on an Azure machine") LOG.debug("Harvesting creds if on an Azure machine")
azure_collector = AzureCollector() azure_collector = AzureCollector()

View File

@ -19,7 +19,7 @@ class SystemInfoCollector(Plugin, metaclass=ABCMeta):
@staticmethod @staticmethod
def should_run(class_name) -> bool: def should_run(class_name) -> bool:
return class_name in WormConfiguration.system_info_collectors_classes return class_name in WormConfiguration.system_info_collector_classes
@staticmethod @staticmethod
def base_package_file(): def base_package_file():

View File

@ -2,6 +2,7 @@ import logging
import os import os
import sys import sys
from common.data.system_info_collectors_names import MIMIKATZ_COLLECTOR
from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import \ from infection_monkey.system_info.windows_cred_collector.mimikatz_cred_collector import \
MimikatzCredentialCollector MimikatzCredentialCollector
@ -44,7 +45,7 @@ class WindowsInfoCollector(InfoCollector):
# TODO: Think about returning self.get_wmi_info() # TODO: Think about returning self.get_wmi_info()
self.get_installed_packages() self.get_installed_packages()
from infection_monkey.config import WormConfiguration from infection_monkey.config import WormConfiguration
if WormConfiguration.should_use_mimikatz: if MIMIKATZ_COLLECTOR in WormConfiguration.system_info_collector_classes:
self.get_mimikatz_info() self.get_mimikatz_info()
return self.info return self.info

View File

@ -14,6 +14,6 @@ class T1065(AttackTechnique):
@staticmethod @staticmethod
def get_report_data(): def get_report_data():
port = ConfigService.get_config_value(['cnc', 'servers', 'current_server']).split(':')[1] port = ConfigService.get_config_value(['internal', 'island_server', 'current_server']).split(':')[1]
T1065.used_msg = T1065.message % port T1065.used_msg = T1065.message % port
return T1065.get_base_data_by_status(ScanStatus.USED.value) return T1065.get_base_data_by_status(ScanStatus.USED.value)

View File

@ -10,8 +10,7 @@ import monkey_island.cc.services.post_breach_files
from monkey_island.cc.database import mongo from monkey_island.cc.database import mongo
from monkey_island.cc.encryptor import encryptor from monkey_island.cc.encryptor import encryptor
from monkey_island.cc.network_utils import local_ip_addresses from monkey_island.cc.network_utils import local_ip_addresses
from monkey_island.cc.services.config_schema.config_schema import SCHEMA
from .config_schema import SCHEMA
__author__ = "itay.mizeretz" __author__ = "itay.mizeretz"
@ -218,8 +217,8 @@ class ConfigService:
@staticmethod @staticmethod
def set_server_ips_in_config(config): def set_server_ips_in_config(config):
ips = local_ip_addresses() ips = local_ip_addresses()
config["cnc"]["servers"]["command_servers"] = ["%s:%d" % (ip, env_singleton.env.get_island_port()) for ip in ips] config["internal"]["island_server"]["command_servers"] = ["%s:%d" % (ip, env_singleton.env.get_island_port()) for ip in ips]
config["cnc"]["servers"]["current_server"] = "%s:%d" % (ips[0], env_singleton.env.get_island_port()) config["internal"]["island_server"]["current_server"] = "%s:%d" % (ips[0], env_singleton.env.get_island_port())
@staticmethod @staticmethod
def save_initial_config_if_needed(): def save_initial_config_if_needed():

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,75 @@
BASIC = {
"title": "Exploits",
"type": "object",
"primary": True,
"properties": {
"exploiters": {
"title": "Exploiters",
"type": "object",
"description": "Choose which exploiters the Monkey will attempt.",
"properties": {
"exploiter_classes": {
"title": "Exploiters",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/exploiter_classes"
},
"default": [
"SmbExploiter",
"WmiExploiter",
"SSHExploiter",
"ShellShockExploiter",
"SambaCryExploiter",
"ElasticGroovyExploiter",
"Struts2Exploiter",
"WebLogicExploiter",
"HadoopExploiter",
"VSFTPDExploiter",
"MSSQLExploiter"
]
}
}
},
"credentials": {
"title": "Credentials",
"type": "object",
"properties": {
"exploit_user_list": {
"title": "Exploit user list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"Administrator",
"root",
"user"
],
"description": "List of user names that will be used by exploiters that need credentials, like "
"SSH brute-forcing."
},
"exploit_password_list": {
"title": "Exploit password list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"root",
"123456",
"password",
"123456789",
"qwerty",
"111111",
"iloveyou"
],
"description": "List of passwords that will be used by exploiters that need credentials, like "
"SSH brute-forcing."
}
}
}
}
}

View File

@ -0,0 +1,95 @@
from common.data.validation_formats import IP, IP_RANGE
from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN
BASIC_NETWORK = {
"title": "Network",
"type": "object",
"properties": {
"scope": {
"title": "Scope",
"type": "object",
"properties": {
"blocked_ips": {
"title": "Blocked IPs",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string",
"format": IP,
},
"default": [
],
"description": "List of IPs that the Monkey will not scan.",
"info": "The Monkey scans its subnet if \"Local network scan\" is ticked. "
"Additionally the monkey scans machines according to \"Scan target list\"."
},
"local_network_scan": {
"title": "Local network scan",
"type": "boolean",
"default": True,
"description": "Determines whether the Monkey will scan the local subnets of machines it runs on, "
"in addition to the IPs that are configured manually in the \"Scan target list\"."
},
"depth": {
"title": "Scan depth",
"type": "integer",
"minimum": 1,
"default": 2,
"description":
"Amount of hops allowed for the Monkey to spread from the Island server. \n"
+ WARNING_SIGN
+ " Note that setting this value too high may result in the Monkey propagating too far, "
"if the \"Local network scan\" is enabled."
},
"subnet_scan_list": {
"title": "Scan target list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string",
"format": IP_RANGE
},
"default": [
],
"description":
"List of targets the Monkey will try to scan. Targets can be IPs, subnets or hosts."
" Examples:\n"
"\tTarget a specific IP: \"192.168.0.1\"\n"
"\tTarget a subnet using a network range: \"192.168.0.5-192.168.0.20\"\n"
"\tTarget a subnet using an IP mask: \"192.168.0.5/24\"\n"
"\tTarget a specific host: \"printer.example\""
}
}
},
"network_analysis": {
"title": "Network Analysis",
"type": "object",
"properties": {
"inaccessible_subnets": {
"title": "Network segmentation testing",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string",
"format": IP_RANGE
},
"default": [
],
"description":
"Test for network segmentation by providing a list of network segments "
"that should NOT be accessible to each other.\n\n"
"For example, if you configured the following three segments: "
"\"10.0.0.0/24\", \"11.0.0.2/32\", and \"12.2.3.0/24\", "
"a Monkey running on 10.0.0.5 will try to access machines in the following subnets: "
"11.0.0.2/32, 12.2.3.0/24. An alert on successful cross-segment connections "
"will be shown in the reports. \n\n"
"Network segments can be IPs, subnets or hosts. Examples:\n"
"\tDefine a single-IP segment: \"192.168.0.1\"\n"
"\tDefine a segment using a network range: \"192.168.0.5-192.168.0.20\"\n"
"\tDefine a segment using an subnet IP mask: \"192.168.0.5/24\"\n"
"\tDefine a single-host segment: \"printer.example\""
}
}
}
}
}

View File

@ -0,0 +1,33 @@
from monkey_island.cc.services.config_schema.basic import BASIC
from monkey_island.cc.services.config_schema.basic_network import BASIC_NETWORK
from monkey_island.cc.services.config_schema.definitions.exploiter_classes import \
EXPLOITER_CLASSES
from monkey_island.cc.services.config_schema.definitions.finger_classes import \
FINGER_CLASSES
from monkey_island.cc.services.config_schema.definitions.post_breach_actions import \
POST_BREACH_ACTIONS
from monkey_island.cc.services.config_schema.definitions.system_info_collector_classes import \
SYSTEM_INFO_COLLECTOR_CLASSES
from monkey_island.cc.services.config_schema.internal import INTERNAL
from monkey_island.cc.services.config_schema.monkey import MONKEY
SCHEMA = {
"title": "Monkey",
"type": "object",
"definitions": {
"exploiter_classes": EXPLOITER_CLASSES,
"system_info_collector_classes": SYSTEM_INFO_COLLECTOR_CLASSES,
"post_breach_actions": POST_BREACH_ACTIONS,
"finger_classes": FINGER_CLASSES
},
"properties": {
"basic": BASIC,
"basic_network": BASIC_NETWORK,
"monkey": MONKEY,
"internal": INTERNAL,
},
"options": {
"collapsed": True
}
}

View File

@ -0,0 +1,130 @@
from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN
EXPLOITER_CLASSES = {
"title": "Exploit class",
"description": "Click on exploiter to get more information about it." + WARNING_SIGN +
" Note that using unsafe exploits may cause crashes of the exploited machine/service.",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SmbExploiter"
],
"title": "SMB Exploiter",
"attack_techniques": ["T1110", "T1075", "T1035"],
"info": "Brute forces using credentials provided by user and"
" hashes gathered by mimikatz.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/smbexec/"
},
{
"type": "string",
"enum": [
"WmiExploiter"
],
"title": "WMI Exploiter",
"attack_techniques": ["T1110", "T1106"],
"info": "Brute forces WMI (Windows Management Instrumentation) "
"using credentials provided by user and hashes gathered by mimikatz.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/wmiexec/"
},
{
"type": "string",
"enum": [
"MSSQLExploiter"
],
"title": "MSSQL Exploiter",
"attack_techniques": ["T1110"],
"info": "Tries to brute force into MsSQL server and uses insecure "
"configuration to execute commands on server.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/mssql/"
},
{
"type": "string",
"enum": [
"Ms08_067_Exploiter"
],
"title": "MS08-067 Exploiter (UNSAFE)",
"info": "Unsafe exploiter, that might cause system crash due to the use of buffer overflow. "
"Uses MS08-067 vulnerability.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/ms08-067/"
},
{
"type": "string",
"enum": [
"SSHExploiter"
],
"title": "SSH Exploiter",
"attack_techniques": ["T1110", "T1145", "T1106"],
"info": "Brute forces using credentials provided by user and SSH keys gathered from systems.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sshexec/"
},
{
"type": "string",
"enum": [
"ShellShockExploiter"
],
"title": "ShellShock Exploiter",
"info": "CVE-2014-6271, based on logic from "
"https://github.com/nccgroup/shocker/blob/master/shocker.py .",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/shellshock/"
},
{
"type": "string",
"enum": [
"SambaCryExploiter"
],
"title": "SambaCry Exploiter",
"info": "Bruteforces and searches for anonymous shares. Uses Impacket.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/sambacry/"
},
{
"type": "string",
"enum": [
"ElasticGroovyExploiter"
],
"title": "ElasticGroovy Exploiter",
"info": "CVE-2015-1427. Logic is based on Metasploit module.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/elasticgroovy/"
},
{
"type": "string",
"enum": [
"Struts2Exploiter"
],
"title": "Struts2 Exploiter",
"info": "Exploits struts2 java web framework. CVE-2017-5638. Logic based on "
"https://www.exploit-db.com/exploits/41570 .",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/struts2/"
},
{
"type": "string",
"enum": [
"WebLogicExploiter"
],
"title": "WebLogic Exploiter",
"info": "Exploits CVE-2017-10271 and CVE-2019-2725 vulnerabilities on WebLogic server.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/weblogic/"
},
{
"type": "string",
"enum": [
"HadoopExploiter"
],
"title": "Hadoop/Yarn Exploiter",
"info": "Remote code execution on HADOOP server with YARN and default settings. "
"Logic based on https://github.com/vulhub/vulhub/tree/master/hadoop/unauthorized-yarn.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/hadoop/"
},
{
"type": "string",
"enum": [
"VSFTPDExploiter"
],
"title": "VSFTPD Exploiter",
"info": "Exploits a malicious backdoor that was added to the VSFTPD download archive. "
"Logic based on Metasploit module.",
"link": "https://www.guardicore.com/infectionmonkey/docs/reference/exploiters/vsftpd/"
}
]
}

View File

@ -0,0 +1,70 @@
FINGER_CLASSES = {
"title": "Fingerprint class",
"description": "Fingerprint modules collect info about external services "
"Infection Monkey scans.",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"SMBFinger"
],
"title": "SMBFinger",
"info": "Figures out if SMB is running and what's the version of it.",
"attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"SSHFinger"
],
"title": "SSHFinger",
"info": "Figures out if SSH is running.",
"attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"PingScanner"
],
"title": "PingScanner",
"info": "Tries to identify if host is alive and which OS it's running by ping scan."
},
{
"type": "string",
"enum": [
"HTTPFinger"
],
"title": "HTTPFinger",
"info": "Checks if host has HTTP/HTTPS ports open."
},
{
"type": "string",
"enum": [
"MySQLFinger"
],
"title": "MySQLFinger",
"info": "Checks if MySQL server is running and tries to get it's version.",
"attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"MSSQLFinger"
],
"title": "MSSQLFinger",
"info": "Checks if Microsoft SQL service is running and tries to gather information about it.",
"attack_techniques": ["T1210"]
},
{
"type": "string",
"enum": [
"ElasticFinger"
],
"title": "ElasticFinger",
"info": "Checks if ElasticSearch is running and attempts to find it's version.",
"attack_techniques": ["T1210"]
}
]
}

View File

@ -0,0 +1,75 @@
POST_BREACH_ACTIONS = {
"title": "Post breach actions",
"description": "Runs scripts/commands on infected machines. These actions safely simulate what an adversary"
"might do after breaching a new machine. Used in ATT&CK and Zero trust reports.",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
"BackdoorUser"
],
"title": "Back door user",
"info": "Attempts to create a new user on the system and delete it afterwards.",
"attack_techniques": ["T1136"]
},
{
"type": "string",
"enum": [
"CommunicateAsNewUser"
],
"title": "Communicate as new user",
"info": "Attempts to create a new user, create HTTPS requests as that user and delete the user "
"afterwards.",
"attack_techniques": ["T1136"]
},
{
"type": "string",
"enum": [
"ModifyShellStartupFiles"
],
"title": "Modify shell startup files",
"info": "Attempts to modify shell startup files, like ~/.profile, ~/.bashrc, ~/.bash_profile "
"in linux, and profile.ps1 in windows. Reverts modifications done afterwards.",
"attack_techniques": ["T1156", "T1504"]
},
{
"type": "string",
"enum": [
"HiddenFiles"
],
"title": "Hidden files and directories",
"info": "Attempts to create a hidden file and remove it afterward.",
"attack_techniques": ["T1158"]
},
{
"type": "string",
"enum": [
"TrapCommand"
],
"title": "Trap",
"info": "On Linux systems, attempts to trap an interrupt signal in order to execute a command "
"upon receiving that signal. Removes the trap afterwards.",
"attack_techniques": ["T1154"]
},
{
"type": "string",
"enum": [
"ChangeSetuidSetgid"
],
"title": "Setuid and Setgid",
"info": "On Linux systems, attempts to set the setuid and setgid bits of a new file. "
"Removes the file afterwards.",
"attack_techniques": ["T1166"]
},
{
"type": "string",
"enum": [
"ScheduleJobs"
],
"title": "Job scheduling",
"info": "Attempts to create a scheduled job on the system and remove it.",
"attack_techniques": ["T1168", "T1053"]
}
]
}

View File

@ -0,0 +1,68 @@
from common.data.system_info_collectors_names import (AWS_COLLECTOR,
AZURE_CRED_COLLECTOR,
ENVIRONMENT_COLLECTOR,
HOSTNAME_COLLECTOR,
MIMIKATZ_COLLECTOR,
PROCESS_LIST_COLLECTOR)
SYSTEM_INFO_COLLECTOR_CLASSES = {
"title": "System Information Collectors",
"description": "Click on a system info collector to find out what it collects.",
"type": "string",
"anyOf": [
{
"type": "string",
"enum": [
ENVIRONMENT_COLLECTOR
],
"title": "Environment collector",
"info": "Collects information about machine's environment (on premise/GCP/AWS).",
"attack_techniques": ["T1082"]
},
{
"type": "string",
"enum": [
MIMIKATZ_COLLECTOR
],
"title": "Mimikatz collector",
"info": "Collects credentials from Windows credential manager.",
"attack_techniques": ["T1003", "T1005"]
},
{
"type": "string",
"enum": [
AWS_COLLECTOR
],
"title": "AWS collector",
"info": "If on AWS, collects more information about the AWS instance currently running on.",
"attack_techniques": ["T1082"]
},
{
"type": "string",
"enum": [
HOSTNAME_COLLECTOR
],
"title": "Hostname collector",
"info": "Collects machine's hostname.",
"attack_techniques": ["T1082", "T1016"]
},
{
"type": "string",
"enum": [
PROCESS_LIST_COLLECTOR
],
"title": "Process list collector",
"info": "Collects a list of running processes on the machine.",
"attack_techniques": ["T1082"]
},
{
"type": "string",
"enum": [
AZURE_CRED_COLLECTOR
],
"title": "Azure credential collector",
"info": "Collects password credentials from Azure VMs",
"attack_techniques": ["T1003", "T1005"]
}
]
}

View File

@ -0,0 +1,490 @@
from monkey_island.cc.services.utils.typographic_symbols import WARNING_SIGN
INTERNAL = {
"title": "Internal",
"type": "object",
"properties": {
"general": {
"title": "General",
"type": "object",
"properties": {
"singleton_mutex_name": {
"title": "Singleton mutex name",
"type": "string",
"default": "{2384ec59-0df8-4ab9-918c-843740924a28}",
"description":
"The name of the mutex used to determine whether the monkey is already running"
},
"keep_tunnel_open_time": {
"title": "Keep tunnel open time",
"type": "integer",
"default": 60,
"description": "Time to keep tunnel open before going down after last exploit (in seconds)"
},
"monkey_dir_name": {
"title": "Monkey's directory name",
"type": "string",
"default": r"monkey_dir",
"description": "Directory name for the directory which will contain all of the monkey files"
},
"started_on_island": {
"title": "Started on island",
"type": "boolean",
"default": False,
"description": "Was exploitation started from island"
"(did monkey with max depth ran on island)"
},
}
},
"monkey": {
"title": "Monkey",
"type": "object",
"properties": {
"victims_max_find": {
"title": "Max victims to find",
"type": "integer",
"default": 100,
"description": "Determines the maximum number of machines the monkey is allowed to scan"
},
"victims_max_exploit": {
"title": "Max victims to exploit",
"type": "integer",
"default": 100,
"description":
"Determines the maximum number of machines the monkey"
" is allowed to successfully exploit. " + WARNING_SIGN
+ " Note that setting this value too high may result in the monkey propagating to "
"a high number of machines"
},
"internet_services": {
"title": "Internet services",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"monkey.guardicore.com",
"www.google.com"
],
"description":
"List of internet services to try and communicate with to determine internet"
" connectivity (use either ip or domain)"
},
"self_delete_in_cleanup": {
"title": "Self delete on cleanup",
"type": "boolean",
"default": True,
"description": "Should the monkey delete its executable when going down"
},
"use_file_logging": {
"title": "Use file logging",
"type": "boolean",
"default": True,
"description": "Should the monkey dump to a log file"
},
"serialize_config": {
"title": "Serialize config",
"type": "boolean",
"default": False,
"description": "Should the monkey dump its config on startup"
},
"alive": {
"title": "Alive",
"type": "boolean",
"default": True,
"description": "Is the monkey alive"
}
}
},
"island_server": {
"title": "Island server",
"type": "object",
"properties": {
"command_servers": {
"title": "Island server's IP's",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"192.0.2.0:5000"
],
"description": "List of command servers/network interfaces to try to communicate with "
"(format is <ip>:<port>)"
},
"current_server": {
"title": "Current server",
"type": "string",
"default": "192.0.2.0:5000",
"description": "The current command server the monkey is communicating with"
}
}
},
"network": {
"title": "Network",
"type": "object",
"properties": {
"tcp_scanner": {
"title": "TCP scanner",
"type": "object",
"properties": {
"HTTP_PORTS": {
"title": "HTTP ports",
"type": "array",
"uniqueItems": True,
"items": {
"type": "integer"
},
"default": [
80,
8080,
443,
8008,
7001
],
"description": "List of ports the monkey will check if are being used for HTTP"
},
"tcp_target_ports": {
"title": "TCP target ports",
"type": "array",
"uniqueItems": True,
"items": {
"type": "integer"
},
"default": [
22,
2222,
445,
135,
3389,
80,
8080,
443,
8008,
3306,
9200,
7001,
8088
],
"description": "List of TCP ports the monkey will check whether they're open"
},
"tcp_scan_interval": {
"title": "TCP scan interval",
"type": "integer",
"default": 0,
"description": "Time to sleep (in milliseconds) between scans"
},
"tcp_scan_timeout": {
"title": "TCP scan timeout",
"type": "integer",
"default": 3000,
"description": "Maximum time (in milliseconds) to wait for TCP response"
},
"tcp_scan_get_banner": {
"title": "TCP scan - get banner",
"type": "boolean",
"default": True,
"description": "Determines whether the TCP scan should try to get the banner"
}
}
},
"ping_scanner": {
"title": "Ping scanner",
"type": "object",
"properties": {
"ping_scan_timeout": {
"title": "Ping scan timeout",
"type": "integer",
"default": 1000,
"description": "Maximum time (in milliseconds) to wait for ping response"
}
}
}
}
},
"classes": {
"title": "Classes",
"type": "object",
"properties": {
"finger_classes": {
"title": "Fingerprint classes",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/finger_classes"
},
"default": [
"SMBFinger",
"SSHFinger",
"PingScanner",
"HTTPFinger",
"MySQLFinger",
"MSSQLFinger",
"ElasticFinger"
]
}
}
},
"kill_file": {
"title": "Kill file",
"type": "object",
"properties": {
"kill_file_path_windows": {
"title": "Kill file path on Windows",
"type": "string",
"default": "%windir%\\monkey.not",
"description": "Path of file which kills monkey if it exists (on Windows)"
},
"kill_file_path_linux": {
"title": "Kill file path on Linux",
"type": "string",
"default": "/var/run/monkey.not",
"description": "Path of file which kills monkey if it exists (on Linux)"
}
}
},
"dropper": {
"title": "Dropper",
"type": "object",
"properties": {
"dropper_set_date": {
"title": "Dropper sets date",
"type": "boolean",
"default": True,
"description":
"Determines whether the dropper should set the monkey's file date to be the same as"
" another file"
},
"dropper_date_reference_path_windows": {
"title": "Dropper date reference path (Windows)",
"type": "string",
"default": "%windir%\\system32\\kernel32.dll",
"description":
"Determines which file the dropper should copy the date from if it's configured to do"
" so on Windows (use fullpath)"
},
"dropper_date_reference_path_linux": {
"title": "Dropper date reference path (Linux)",
"type": "string",
"default": "/bin/sh",
"description":
"Determines which file the dropper should copy the date from if it's configured to do"
" so on Linux (use fullpath)"
},
"dropper_target_path_linux": {
"title": "Dropper target path on Linux",
"type": "string",
"default": "/tmp/monkey",
"description": "Determines where should the dropper place the monkey on a Linux machine"
},
"dropper_target_path_win_32": {
"title": "Dropper target path on Windows (32bit)",
"type": "string",
"default": "C:\\Windows\\temp\\monkey32.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(32bit)"
},
"dropper_target_path_win_64": {
"title": "Dropper target path on Windows (64bit)",
"type": "string",
"default": "C:\\Windows\\temp\\monkey64.exe",
"description": "Determines where should the dropper place the monkey on a Windows machine "
"(64 bit)"
},
"dropper_try_move_first": {
"title": "Try to move first",
"type": "boolean",
"default": True,
"description":
"Determines whether the dropper should try to move itself instead of copying itself"
" to target path"
}
}
},
"logging": {
"title": "Logging",
"type": "object",
"properties": {
"dropper_log_path_linux": {
"title": "Dropper log file path on Linux",
"type": "string",
"default": "/tmp/user-1562",
"description": "The fullpath of the dropper log file on Linux"
},
"dropper_log_path_windows": {
"title": "Dropper log file path on Windows",
"type": "string",
"default": "%temp%\\~df1562.tmp",
"description": "The fullpath of the dropper log file on Windows"
},
"monkey_log_path_linux": {
"title": "Monkey log file path on Linux",
"type": "string",
"default": "/tmp/user-1563",
"description": "The fullpath of the monkey log file on Linux"
},
"monkey_log_path_windows": {
"title": "Monkey log file path on Windows",
"type": "string",
"default": "%temp%\\~df1563.tmp",
"description": "The fullpath of the monkey log file on Windows"
},
"send_log_to_server": {
"title": "Send log to server",
"type": "boolean",
"default": True,
"description": "Determines whether the monkey sends its log to the Monkey Island server"
}
}
},
"exploits": {
"title": "Exploits",
"type": "object",
"properties": {
"exploit_lm_hash_list": {
"title": "Exploit LM hash list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [],
"description": "List of LM hashes to use on exploits using credentials"
},
"exploit_ntlm_hash_list": {
"title": "Exploit NTLM hash list",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [],
"description": "List of NTLM hashes to use on exploits using credentials"
},
"exploit_ssh_keys": {
"title": "SSH key pairs list",
"type": "array",
"uniqueItems": True,
"default": [],
"items": {
"type": "string"
},
"description": "List of SSH key pairs to use, when trying to ssh into servers"
}, "general": {
"title": "General",
"type": "object",
"properties": {
"skip_exploit_if_file_exist": {
"title": "Skip exploit if file exists",
"type": "boolean",
"default": False,
"description": "Determines whether the monkey should skip the exploit if the monkey's file"
" is already on the remote machine"
}
}
},
"ms08_067": {
"title": "MS08_067",
"type": "object",
"properties": {
"ms08_067_exploit_attempts": {
"title": "MS08_067 exploit attempts",
"type": "integer",
"default": 5,
"description": "Number of attempts to exploit using MS08_067"
},
"user_to_add": {
"title": "Remote user",
"type": "string",
"default": "Monkey_IUSER_SUPPORT",
"description": "Username to add on successful exploit"
},
"remote_user_pass": {
"title": "Remote user password",
"type": "string",
"default": "Password1!",
"description": "Password to use for created user"
}
}
},
"sambacry": {
"title": "SambaCry",
"type": "object",
"properties": {
"sambacry_trigger_timeout": {
"title": "SambaCry trigger timeout",
"type": "integer",
"default": 5,
"description": "Timeout (in seconds) of SambaCry trigger"
},
"sambacry_folder_paths_to_guess": {
"title": "SambaCry folder paths to guess",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
'/',
'/mnt',
'/tmp',
'/storage',
'/export',
'/share',
'/shares',
'/home'
],
"description": "List of full paths to share folder for SambaCry to guess"
},
"sambacry_shares_not_to_check": {
"title": "SambaCry shares not to check",
"type": "array",
"uniqueItems": True,
"items": {
"type": "string"
},
"default": [
"IPC$", "print$"
],
"description": "These shares won't be checked when exploiting with SambaCry"
}
}
}
},
"smb_service": {
"title": "SMB service",
"type": "object",
"properties": {
"smb_download_timeout": {
"title": "SMB download timeout",
"type": "integer",
"default": 300,
"description":
"Timeout (in seconds) for SMB download operation (used in various exploits using SMB)"
},
"smb_service_name": {
"title": "SMB service name",
"type": "string",
"default": "InfectionMonkey",
"description": "Name of the SMB service that will be set up to download monkey"
}
}
}
},
"testing": {
"title": "Testing",
"type": "object",
"properties": {
"export_monkey_telems": {
"title": "Export monkey telemetries",
"type": "boolean",
"default": False,
"description": "Exports unencrypted telemetries that can be used for tests in development."
" Do not turn on!"
}
}
}
}
}

View File

@ -0,0 +1,128 @@
from common.data.system_info_collectors_names import (AWS_COLLECTOR,
AZURE_CRED_COLLECTOR,
ENVIRONMENT_COLLECTOR,
HOSTNAME_COLLECTOR,
MIMIKATZ_COLLECTOR,
PROCESS_LIST_COLLECTOR)
MONKEY = {
"title": "Monkey",
"type": "object",
"properties": {
"post_breach": {
"title": "Post breach",
"type": "object",
"properties": {
"custom_PBA_linux_cmd": {
"title": "Linux post breach command",
"type": "string",
"default": "",
"description": "Linux command to be executed after breaching."
},
"PBA_linux_file": {
"title": "Linux post breach file",
"type": "string",
"format": "data-url",
"description": "File to be executed after breaching. "
"If you want custom execution behavior, "
"specify it in 'Linux post breach command' field. "
"Reference your file by filename."
},
"custom_PBA_windows_cmd": {
"title": "Windows post breach command",
"type": "string",
"default": "",
"description": "Windows command to be executed after breaching."
},
"PBA_windows_file": {
"title": "Windows post breach file",
"type": "string",
"format": "data-url",
"description": "File to be executed after breaching. "
"If you want custom execution behavior, "
"specify it in 'Windows post breach command' field. "
"Reference your file by filename."
},
"PBA_windows_filename": {
"title": "Windows PBA filename",
"type": "string",
"default": ""
},
"PBA_linux_filename": {
"title": "Linux PBA filename",
"type": "string",
"default": ""
},
"post_breach_actions": {
"title": "Post breach actions",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/post_breach_actions"
},
"default": [
"BackdoorUser",
"CommunicateAsNewUser",
"ModifyShellStartupFiles",
"HiddenFiles",
"TrapCommand",
"ChangeSetuidSetgid",
"ScheduleJobs"
]
},
}
},
"system_info": {
"title": "System info",
"type": "object",
"properties": {
"system_info_collector_classes": {
"title": "System info collectors",
"type": "array",
"uniqueItems": True,
"items": {
"$ref": "#/definitions/system_info_collector_classes"
},
"default": [
ENVIRONMENT_COLLECTOR,
AWS_COLLECTOR,
HOSTNAME_COLLECTOR,
PROCESS_LIST_COLLECTOR,
MIMIKATZ_COLLECTOR,
AZURE_CRED_COLLECTOR
]
},
}
},
"persistent_scanning": {
"title": "Persistent scanning",
"type": "object",
"properties": {
"max_iterations": {
"title": "Max iterations",
"type": "integer",
"default": 1,
"minimum": 1,
"description": "Determines how many iterations of the monkey's full lifecycle should occur "
"(how many times to do the scan)"
},
"timeout_between_iterations": {
"title": "Wait time between iterations",
"type": "integer",
"default": 100,
"minimum": 0,
"description":
"Determines for how long (in seconds) should the monkey wait before starting another scan"
},
"retry_failed_explotation": {
"title": "Retry failed exploitation",
"type": "boolean",
"default": True,
"description":
"Determines whether the monkey should retry exploiting machines"
" it didn't successfully exploit on previous scans"
}
}
}
}
}

View File

@ -9,8 +9,8 @@ __author__ = "VakarisZ"
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Where to find file names in config # Where to find file names in config
PBA_WINDOWS_FILENAME_PATH = ['monkey', 'behaviour', 'PBA_windows_filename'] PBA_WINDOWS_FILENAME_PATH = ['monkey', 'post_breach', 'PBA_windows_filename']
PBA_LINUX_FILENAME_PATH = ['monkey', 'behaviour', 'PBA_linux_filename'] PBA_LINUX_FILENAME_PATH = ['monkey', 'post_breach', 'PBA_linux_filename']
UPLOADS_DIR = Path('monkey_island', 'cc', 'userUploads') UPLOADS_DIR = Path('monkey_island', 'cc', 'userUploads')
@ -41,5 +41,5 @@ def set_config_PBA_files(config_json):
if monkey_island.cc.services.config.ConfigService.get_config(): if monkey_island.cc.services.config.ConfigService.get_config():
linux_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH) linux_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_LINUX_FILENAME_PATH)
windows_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH) windows_filename = monkey_island.cc.services.config.ConfigService.get_config_value(PBA_WINDOWS_FILENAME_PATH)
config_json['monkey']['behaviour']['PBA_linux_filename'] = linux_filename config_json['monkey']['post_breach']['PBA_linux_filename'] = linux_filename
config_json['monkey']['behaviour']['PBA_windows_filename'] = windows_filename config_json['monkey']['post_breach']['PBA_windows_filename'] = windows_filename

View File

@ -618,7 +618,7 @@ class ReportService:
@staticmethod @staticmethod
def get_config_exploits(): def get_config_exploits():
exploits_config_value = ['exploits', 'general', 'exploiter_classes'] exploits_config_value = ['basic', 'exploiters', 'exploiter_classes']
default_exploits = ConfigService.get_default_config(False) default_exploits = ConfigService.get_default_config(False)
for namespace in exploits_config_value: for namespace in exploits_config_value:
default_exploits = default_exploits[namespace] default_exploits = default_exploits[namespace]
@ -632,11 +632,11 @@ class ReportService:
@staticmethod @staticmethod
def get_config_ips(): def get_config_ips():
return ConfigService.get_config_value(['basic_network', 'general', 'subnet_scan_list'], True, True) return ConfigService.get_config_value(['basic_network', 'scope', 'subnet_scan_list'], True, True)
@staticmethod @staticmethod
def get_config_scan(): def get_config_scan():
return ConfigService.get_config_value(['basic_network', 'general', 'local_network_scan'], True, True) return ConfigService.get_config_value(['basic_network', 'scope', 'local_network_scan'], True, True)
@staticmethod @staticmethod
def get_issues_overview(issues, config_users, config_passwords): def get_issues_overview(issues, config_users, config_passwords):

View File

@ -0,0 +1 @@
WARNING_SIGN = " \u26A0"

View File

@ -41,7 +41,7 @@
"global-strict": 0, "global-strict": 0,
"no-underscore-dangle": 0, "no-underscore-dangle": 0,
"no-console": 0, "no-console": 0,
"no-unused-vars": 1, "no-unused-vars": [1, {"vars": "all", "args": "all", "argsIgnorePattern": "^_", "varsIgnorePattern": "^React$" }],
"no-trailing-spaces": [ "no-trailing-spaces": [
1, 1,
{ {

View File

@ -13649,28 +13649,6 @@
"react-base16-styling": "^0.5.1" "react-base16-styling": "^0.5.1"
} }
}, },
"react-jsonschema-form": {
"version": "1.8.1",
"resolved": "https://registry.npmjs.org/react-jsonschema-form/-/react-jsonschema-form-1.8.1.tgz",
"integrity": "sha512-aaDloxNAcGXOOOcdKOxxqEEn5oDlPUZgWcs8unXXB9vjBRgCF8rCm/wVSv1u2G5ih0j/BX6Ewd/WjI2g00lPdg==",
"requires": {
"@babel/runtime-corejs2": "^7.4.5",
"ajv": "^6.7.0",
"core-js": "^2.5.7",
"lodash": "^4.17.15",
"prop-types": "^15.5.8",
"react-is": "^16.8.4",
"react-lifecycles-compat": "^3.0.4",
"shortid": "^2.2.14"
},
"dependencies": {
"core-js": {
"version": "2.6.11",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz",
"integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg=="
}
}
},
"react-jsonschema-form-bs4": { "react-jsonschema-form-bs4": {
"version": "1.7.1", "version": "1.7.1",
"resolved": "https://registry.npmjs.org/react-jsonschema-form-bs4/-/react-jsonschema-form-bs4-1.7.1.tgz", "resolved": "https://registry.npmjs.org/react-jsonschema-form-bs4/-/react-jsonschema-form-bs4-1.7.1.tgz",

View File

@ -74,6 +74,7 @@
"file-saver": "^2.0.2", "file-saver": "^2.0.2",
"filepond": "^4.18.0", "filepond": "^4.18.0",
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"lodash": "^4.17.15",
"marked": "^0.8.2", "marked": "^0.8.2",
"normalize.css": "^8.0.0", "normalize.css": "^8.0.0",
"npm": "^6.14.6", "npm": "^6.14.6",
@ -94,7 +95,6 @@
"react-graph-vis": "^1.0.5", "react-graph-vis": "^1.0.5",
"react-hot-loader": "^4.12.20", "react-hot-loader": "^4.12.20",
"react-json-tree": "^0.11.2", "react-json-tree": "^0.11.2",
"react-jsonschema-form": "^1.8.0",
"react-jsonschema-form-bs4": "^1.7.1", "react-jsonschema-form-bs4": "^1.7.1",
"react-particles-js": "^3.2.1", "react-particles-js": "^3.2.1",
"react-redux": "^5.1.2", "react-redux": "^5.1.2",

View File

@ -97,7 +97,7 @@ class AppComponent extends AuthComponent {
}; };
redirectTo = (userPath, targetPath) => { redirectTo = (userPath, targetPath) => {
let pathQuery = new RegExp(userPath + '[\/]?$', 'g'); let pathQuery = new RegExp(userPath + '[/]?$', 'g');
if (window.location.pathname.match(pathQuery)) { if (window.location.pathname.match(pathQuery)) {
return <Redirect to={{pathname: targetPath}}/> return <Redirect to={{pathname: targetPath}}/>
} }

View File

@ -49,7 +49,7 @@ class SideNavComponent extends React.Component {
</li> </li>
<li> <li>
<NavLink to='/report/security' <NavLink to='/report/security'
isActive={(match, location) => { isActive={(_match, location) => {
return (location.pathname === '/report/attack' return (location.pathname === '/report/attack'
|| location.pathname === '/report/zeroTrust' || location.pathname === '/report/zeroTrust'
|| location.pathname === '/report/security') || location.pathname === '/report/security')

View File

@ -4,7 +4,7 @@ import Tooltip from 'react-tooltip-lite'
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import 'filepond/dist/filepond.min.css'; import 'filepond/dist/filepond.min.css';
import '../../styles/Tooltip.scss'; import '../../styles/components/Tooltip.scss';
import {Col} from 'react-bootstrap'; import {Col} from 'react-bootstrap';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import {renderMachineFromSystemData, ScanStatus} from './Helpers'; import {renderMachineFromSystemData, ScanStatus} from './Helpers';
import MitigationsComponent from "./MitigationsComponent"; import MitigationsComponent from './MitigationsComponent';
class T1136 extends React.Component { class T1136 extends React.Component {

View File

@ -0,0 +1,20 @@
import ObjectField from 'react-jsonschema-form-bs4/lib/components/fields/ArrayField';
import * as React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
class FieldWithInfo extends React.Component {
render() {
return (
<>
<div className='alert alert-info'>
<FontAwesomeIcon icon={faInfoCircle} style={{'marginRight': '5px'}}/>
{this.props.schema.info}
</div>
<ObjectField {...this.props} />
</>);
}
}
export default FieldWithInfo;

View File

@ -0,0 +1,79 @@
import Form from 'react-jsonschema-form-bs4';
import React, {useState} from 'react';
import {Nav} from 'react-bootstrap';
const sectionOrder = [
'network',
'monkey',
'island_server',
'logging',
'exploits',
'dropper',
'classes',
'general',
'kill_file',
'testing'
];
const initialSection = sectionOrder[0];
export default function InternalConfig(props) {
const {
schema,
uiSchema,
onChange,
customFormats,
className,
formData
} = props;
const [selectedSection, setSelectedSection] = useState(initialSection);
const [displayedSchema, setDisplayedSchema] = useState(getSchemaByKey(schema, initialSection));
const [displayedSchemaUi, setDisplayedSchemaUi] = useState(uiSchema[initialSection]);
const onInnerDataChange = (innerData) => {
formData[selectedSection] = innerData.formData;
onChange({formData: formData});
}
const setSection = (sectionKey) => {
setSelectedSection(sectionKey);
setDisplayedSchema(getSchemaByKey(schema, sectionKey));
setDisplayedSchemaUi(uiSchema[sectionKey]);
}
const renderNav = () => {
return (<Nav variant='tabs'
fill
activeKey={selectedSection} onSelect={setSection}
style={{'marginBottom': '2em'}}
className={'config-nav'}>
{sectionOrder.map(section => {
return (
<Nav.Item key={section}>
<Nav.Link eventKey={section}>{getNavTitle(schema, section)}</Nav.Link>
</Nav.Item>);
})}
</Nav>)
}
return (<div>
{renderNav()}
<Form schema={displayedSchema}
uiSchema={displayedSchemaUi}
formData={formData[selectedSection]}
onChange={onInnerDataChange}
customFormats={customFormats}
className={className}
liveValidate>
<button type='submit' className={'hidden'}>Submit</button>
</Form>
</div>)
}
function getSchemaByKey(schema, key) {
let definitions = schema['definitions'];
return {definitions: definitions, properties: schema['properties'][key]['properties']};
}
function getNavTitle(schema, key) {
return schema.properties[key].title;
}

View File

@ -0,0 +1,79 @@
import React from 'react';
import AuthComponent from '../AuthComponent';
import {FilePond} from 'react-filepond';
import 'filepond/dist/filepond.min.css';
class PbaInput extends AuthComponent {
constructor(props) {
super(props);
// set schema from server
this.state = this.getStateFromProps(this.props);
}
getStateFromProps(props){
let options = props.options
// set schema from server
return {
filename: options.filename,
apiEndpoint: options.apiEndpoint,
setPbaFilename: options.setPbaFilename
};
}
componentDidUpdate(prevProps, _prevState, _snapshot) {
if(prevProps.options.filename !== this.props.options.filename && this.props.options.filename === ''){
this.setState({filename: this.props.options.filename})
}
}
getPBAfile() {
if (this.state.filename) {
return PbaInput.getFullPBAfile(this.state.filename)
}
}
static getFullPBAfile(filename) {
return [{
source: filename,
options: {
type: 'limbo'
}
}];
}
getServerParams(path) {
return {
url: path,
process: this.getRequestParams(),
revert: this.getRequestParams(),
restore: this.getRequestParams(),
load: this.getRequestParams(),
fetch: this.getRequestParams()
}
}
getRequestParams() {
return {headers: {'Authorization': this.jwtHeader}}
}
render() {
return (<FilePond
key={this.state.apiEndpoint}
server={this.getServerParams(this.state.apiEndpoint)}
files={this.getPBAfile()}
onupdatefiles={fileItems => {
if (fileItems.length > 0) {
this.state.setPbaFilename(fileItems[0].file.name)
} else {
this.state.setPbaFilename('')
}
}}
/>)
}
}
export default PbaInput;

View File

@ -0,0 +1,92 @@
import AdvancedMultiSelect from '../ui-components/AdvancedMultiSelect';
import PbaInput from './PbaInput';
import {API_PBA_LINUX, API_PBA_WINDOWS} from '../pages/ConfigurePage';
import FieldWithInfo from './FieldWithInfo';
export default function UiSchema(props) {
const UiSchema = {
basic: {
'ui:order': ['exploiters', 'credentials'],
exploiters: {
exploiter_classes: {
classNames: 'config-template-no-header',
'ui:widget': AdvancedMultiSelect
}
}
},
basic_network: {
'ui:order': ['scope', 'network_analysis'],
scope: {
blocked_ips: {
'ui:field': FieldWithInfo
},
subnet_scan_list: {
format: 'ip-list'
}
}
},
monkey: {
post_breach: {
post_breach_actions: {
classNames: 'config-template-no-header',
'ui:widget': AdvancedMultiSelect
},
custom_PBA_linux_cmd: {
'ui:widget': 'textarea',
'ui:emptyValue': ''
},
PBA_linux_file: {
'ui:widget': PbaInput,
'ui:options': {
filename: props.PBA_linux_filename,
apiEndpoint: API_PBA_LINUX,
setPbaFilename: props.setPbaFilenameLinux
}
},
custom_PBA_windows_cmd: {
'ui:widget': 'textarea',
'ui:emptyValue': ''
},
PBA_windows_file: {
'ui:widget': PbaInput,
'ui:options': {
filename: props.PBA_windows_filename,
apiEndpoint: API_PBA_WINDOWS,
setPbaFilename: props.setPbaFilenameWindows
}
},
PBA_linux_filename: {
classNames: 'linux-pba-file-info',
'ui:emptyValue': ''
},
PBA_windows_filename: {
classNames: 'windows-pba-file-info',
'ui:emptyValue': ''
}
},
system_info: {
system_info_collector_classes: {
classNames: 'config-template-no-header',
'ui:widget': AdvancedMultiSelect
}
}
},
internal: {
general: {
started_on_island: {'ui:widget': 'hidden'}
},
classes: {
finger_classes: {
classNames: 'config-template-no-header',
'ui:widget': AdvancedMultiSelect
}
},
monkey: {
alive: {
classNames: 'config-field-hidden'
}
}
}
};
return UiSchema[props.selectedSection]
}

View File

@ -0,0 +1,14 @@
import {IP, IP_RANGE} from './ValidationFormats';
export default function transformErrors(errors) {
return errors.map(error => {
if (error.name === 'type') {
error.message = 'Field can\'t be empty.'
} else if (error.name === 'format' && error.params.format === IP_RANGE) {
error.message = 'Invalid IP range, refer to description for valid examples.'
} else if (error.name === 'format' && error.params.format === IP) {
error.message = 'Invalid IP.'
}
return error;
});
}

View File

@ -0,0 +1,24 @@
const ipRegex = '((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)'
const cidrNotationRegex = '([0-9]|1[0-9]|2[0-9]|3[0-2])'
const hostnameRegex = '^([A-Za-z0-9]*[A-Za-z]+[A-Za-z0-9]*.?)*([A-Za-z0-9]*[A-Za-z]+[A-Za-z0-9]*)$'
export const IP_RANGE = 'ip-range';
export const IP = 'ip';
export const formValidationFormats = {
[IP_RANGE]: buildIpRangeRegex(),
[IP]: buildIpRegex()
};
function buildIpRangeRegex(){
return new RegExp([
'^'+ipRegex+'$|', // Single: IP
'^'+ipRegex+'-'+ipRegex+'$|', // IP range: IP-IP
'^'+ipRegex+'/'+cidrNotationRegex+'$|', // IP range with cidr notation: IP/cidr
hostnameRegex // Hostname: target.tg
].join(''))
}
function buildIpRegex(){
return new RegExp('^'+ipRegex+'$')
}

View File

@ -101,9 +101,9 @@ class PreviewPaneComponent extends AuthComponent {
.replace(/\\t/g, '\t') .replace(/\\t/g, '\t')
.replace(/\\b/g, '\b') .replace(/\\b/g, '\b')
.replace(/\\f/g, '\f') .replace(/\\f/g, '\f')
.replace(/\\"/g, '\"') .replace(/\\"/g, '"')
.replace(/\\'/g, '\'') .replace(/\\'/g, '\'')
.replace(/\\&/g, '\&'); .replace(/\\&/g, '&');
} }
downloadLog(asset) { downloadLog(asset) {

View File

@ -3,30 +3,30 @@ import Form from 'react-jsonschema-form-bs4';
import {Col, Modal, Nav, Button} from 'react-bootstrap'; import {Col, Modal, Nav, Button} from 'react-bootstrap';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import {FilePond} from 'react-filepond';
import 'filepond/dist/filepond.min.css';
import ConfigMatrixComponent from '../attack/ConfigMatrixComponent'; import ConfigMatrixComponent from '../attack/ConfigMatrixComponent';
import UiSchema from '../configuration-components/UiSchema';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamationCircle'; import {faExclamationCircle} from '@fortawesome/free-solid-svg-icons/faExclamationCircle';
import {formValidationFormats} from '../configuration-components/ValidationFormats';
import transformErrors from '../configuration-components/ValidationErrorMessages';
import InternalConfig from '../configuration-components/InternalConfig';
const ATTACK_URL = '/api/attack'; const ATTACK_URL = '/api/attack';
const CONFIG_URL = '/api/configuration/island'; const CONFIG_URL = '/api/configuration/island';
export const API_PBA_LINUX = '/api/fileUpload/PBAlinux';
export const API_PBA_WINDOWS = '/api/fileUpload/PBAwindows';
class ConfigurePageComponent extends AuthComponent { class ConfigurePageComponent extends AuthComponent {
constructor(props) { constructor(props) {
super(props); super(props);
this.PBAwindowsPond = null;
this.PBAlinuxPond = null;
this.currentSection = 'attack'; this.currentSection = 'attack';
this.currentFormData = {}; this.currentFormData = {};
this.initialConfig = {}; this.initialConfig = {};
this.initialAttackConfig = {}; this.initialAttackConfig = {};
this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'cnc', 'network', 'exploits', 'internal']; this.sectionsOrder = ['attack', 'basic', 'basic_network', 'monkey', 'internal'];
this.uiSchemas = this.getUiSchemas();
// set schema from server
this.state = { this.state = {
schema: {}, schema: {},
configuration: {}, configuration: {},
@ -34,53 +34,10 @@ class ConfigurePageComponent extends AuthComponent {
lastAction: 'none', lastAction: 'none',
sections: [], sections: [],
selectedSection: 'attack', selectedSection: 'attack',
PBAwinFile: [],
PBAlinuxFile: [],
showAttackAlert: false showAttackAlert: false
}; };
} }
getUiSchemas() {
return ({
basic: {'ui:order': ['general', 'credentials']},
basic_network: {},
monkey: {
behaviour: {
custom_PBA_linux_cmd: {
'ui:widget': 'textarea',
'ui:emptyValue': ''
},
PBA_linux_file: {
'ui:widget': this.PBAlinux
},
custom_PBA_windows_cmd: {
'ui:widget': 'textarea',
'ui:emptyValue': ''
},
PBA_windows_file: {
'ui:widget': this.PBAwindows
},
PBA_linux_filename: {
classNames: 'linux-pba-file-info',
'ui:emptyValue': ''
},
PBA_windows_filename: {
classNames: 'windows-pba-file-info',
'ui:emptyValue': ''
}
}
},
cnc: {},
network: {},
exploits: {},
internal: {
general: {
started_on_island: {'ui:widget': 'hidden'}
}
}
})
}
setInitialConfig(config) { setInitialConfig(config) {
// Sets a reference to know if config was changed // Sets a reference to know if config was changed
this.initialConfig = JSON.parse(JSON.stringify(config)); this.initialConfig = JSON.parse(JSON.stringify(config));
@ -122,7 +79,7 @@ class ConfigurePageComponent extends AuthComponent {
.then(res => res.json()) .then(res => res.json())
.then(data => { .then(data => {
this.setInitialConfig(data.configuration); this.setInitialConfig(data.configuration);
this.setState({configuration: data.configuration}) this.setState({configuration: data.configuration});
}) })
}; };
@ -271,7 +228,6 @@ class ConfigurePageComponent extends AuthComponent {
}; };
resetConfig = () => { resetConfig = () => {
this.removePBAfiles();
this.authFetch(CONFIG_URL, this.authFetch(CONFIG_URL,
{ {
method: 'POST', method: 'POST',
@ -280,14 +236,18 @@ class ConfigurePageComponent extends AuthComponent {
}) })
.then(res => res.json()) .then(res => res.json())
.then(res => { .then(res => {
this.setState({ this.setState({
lastAction: 'reset', lastAction: 'reset',
schema: res.schema, schema: res.schema,
configuration: res.configuration configuration: res.configuration
}); });
this.setInitialConfig(res.configuration); this.setInitialConfig(res.configuration);
this.props.onStatusChange(); this.props.onStatusChange();
}); }
).then(() => {
this.removePBAfile(API_PBA_WINDOWS, this.setPbaFilenameWindows)
this.removePBAfile(API_PBA_LINUX, this.setPbaFilenameLinux)
});
this.authFetch(ATTACK_URL, { this.authFetch(ATTACK_URL, {
method: 'POST', method: 'POST',
headers: {'Content-Type': 'application/json'}, headers: {'Content-Type': 'application/json'},
@ -300,21 +260,17 @@ class ConfigurePageComponent extends AuthComponent {
}) })
}; };
removePBAfiles() { removePBAfile(apiEndpoint, setFilenameFnc) {
// We need to clean files from widget, local state and configuration (to sync with bac end) this.sendPbaRemoveRequest(apiEndpoint)
if (this.PBAwindowsPond !== null) { setFilenameFnc('')
this.PBAwindowsPond.removeFile(); }
}
if (this.PBAlinuxPond !== null) { sendPbaRemoveRequest(apiEndpoint) {
this.PBAlinuxPond.removeFile();
}
let request_options = { let request_options = {
method: 'DELETE', method: 'DELETE',
headers: {'Content-Type': 'text/plain'} headers: {'Content-Type': 'text/plain'}
}; };
this.authFetch('/api/fileUpload/PBAlinux', request_options); this.authFetch(apiEndpoint, request_options);
this.authFetch('/api/fileUpload/PBAwindows', request_options);
this.setState({PBAlinuxFile: [], PBAwinFile: []});
} }
setConfigOnImport = (event) => { setConfigOnImport = (event) => {
@ -366,82 +322,6 @@ class ConfigurePageComponent extends AuthComponent {
event.target.value = null; event.target.value = null;
}; };
PBAwindows = () => {
return (<FilePond
server={{
url: '/api/fileUpload/PBAwindows',
process: {headers: {'Authorization': this.jwtHeader}},
revert: {headers: {'Authorization': this.jwtHeader}},
restore: {headers: {'Authorization': this.jwtHeader}},
load: {headers: {'Authorization': this.jwtHeader}},
fetch: {headers: {'Authorization': this.jwtHeader}}
}}
files={this.getWinPBAfile()}
onupdatefiles={fileItems => {
this.setState({
PBAwinFile: fileItems.map(fileItem => fileItem.file)
})
}}
ref={ref => this.PBAwindowsPond = ref}
/>)
};
PBAlinux = () => {
return (<FilePond
server={{
url: '/api/fileUpload/PBAlinux',
process: {headers: {'Authorization': this.jwtHeader}},
revert: {headers: {'Authorization': this.jwtHeader}},
restore: {headers: {'Authorization': this.jwtHeader}},
load: {headers: {'Authorization': this.jwtHeader}},
fetch: {headers: {'Authorization': this.jwtHeader}}
}}
files={this.getLinuxPBAfile()}
onupdatefiles={fileItems => {
this.setState({
PBAlinuxFile: fileItems.map(fileItem => fileItem.file)
})
}}
ref={ref => this.PBAlinuxPond = ref}
/>)
};
getWinPBAfile() {
if (this.state.PBAwinFile.length !== 0) {
return ConfigurePageComponent.getMockPBAfile(this.state.PBAwinFile[0])
} else if (this.state.configuration.monkey.behaviour.PBA_windows_filename) {
return ConfigurePageComponent.getFullPBAfile(this.state.configuration.monkey.behaviour.PBA_windows_filename)
}
}
getLinuxPBAfile() {
if (this.state.PBAlinuxFile.length !== 0) {
return ConfigurePageComponent.getMockPBAfile(this.state.PBAlinuxFile[0])
} else if (this.state.configuration.monkey.behaviour.PBA_linux_filename) {
return ConfigurePageComponent.getFullPBAfile(this.state.configuration.monkey.behaviour.PBA_linux_filename)
}
}
static getFullPBAfile(filename) {
return [{
source: filename,
options: {
type: 'limbo'
}
}];
}
static getMockPBAfile(mockFile) {
let pbaFile = [{
source: mockFile.name,
options: {
type: 'limbo'
}
}];
pbaFile[0].options.file = mockFile;
return pbaFile
}
renderMatrix = () => { renderMatrix = () => {
return (<ConfigMatrixComponent configuration={this.state.attackConfig} return (<ConfigMatrixComponent configuration={this.state.attackConfig}
submit={this.componentDidMount} submit={this.componentDidMount}
@ -449,43 +329,65 @@ class ConfigurePageComponent extends AuthComponent {
change={this.attackTechniqueChange}/>) change={this.attackTechniqueChange}/>)
}; };
renderConfigContent = (displayedSchema) => { renderConfigContent = (displayedSchema) => {
return (<div> let formProperties = {};
{this.renderBasicNetworkWarning()} formProperties['schema'] = displayedSchema
<Form schema={displayedSchema} formProperties['uiSchema'] = UiSchema({
uiSchema={this.uiSchemas[this.state.selectedSection]} PBA_linux_filename: this.state.configuration.monkey.post_breach.PBA_linux_filename,
formData={this.state.configuration[this.state.selectedSection]} PBA_windows_filename: this.state.configuration.monkey.post_breach.PBA_windows_filename,
onChange={this.onChange} setPbaFilenameWindows: this.setPbaFilenameWindows,
noValidate={true} setPbaFilenameLinux: this.setPbaFilenameLinux,
className={'config-form'}> selectedSection: this.state.selectedSection
<button type='submit' className={'hidden'}>Submit</button> })
</Form> formProperties['formData'] = this.state.configuration[this.state.selectedSection];
</div>) formProperties['onChange'] = this.onChange;
}; formProperties['customFormats'] = formValidationFormats;
formProperties['transformErrors'] = transformErrors;
formProperties['className'] = 'config-form';
formProperties['liveValidate'] = true;
renderBasicNetworkWarning = () => { if (this.state.selectedSection === 'internal') {
if (this.state.selectedSection === 'basic_network') { return (<InternalConfig {...formProperties}/>)
return (<div className='alert alert-info'>
<FontAwesomeIcon icon={faInfoCircle} style={{'marginRight': '5px'}}/>
The Monkey scans its subnet if 'Local network scan' is ticked. Additionally the monkey scans machines
according to its range class.
</div>)
} else { } else {
return (<div/>) return (
<div>
<Form {...formProperties}>
<button type='submit' className={'hidden'}>Submit</button>
</Form>
</div>
)
} }
}; };
setPbaFilenameWindows = (filename) => {
let config = this.state.configuration
config.monkey.post_breach.PBA_windows_filename = filename
this.setState({
configuration: config
})
}
setPbaFilenameLinux = (filename) => {
let config = this.state.configuration
config.monkey.post_breach.PBA_linux_filename = filename
this.setState({
configuration: config
})
}
renderNav = () => { renderNav = () => {
return (<Nav variant='tabs' return (<Nav variant='tabs'
fill fill
activeKey={this.state.selectedSection} onSelect={this.setSelectedSection} activeKey={this.state.selectedSection} onSelect={this.setSelectedSection}
style={{'marginBottom': '2em'}} style={{'marginBottom': '2em'}}
className={'config-nav'}> className={'config-nav'}>
{this.state.sections.map(section => {this.state.sections.map(section => {
<Nav.Item> let classProp = section.key.startsWith('basic') ? 'tab-primary' : '';
<Nav.Link eventKey={section.key}>{section.title}</Nav.Link> return (
</Nav.Item>)} <Nav.Item key={section.key}>
<Nav.Link className={classProp} eventKey={section.key}>{section.title}</Nav.Link>
</Nav.Item>);
})}
</Nav>) </Nav>)
}; };
@ -495,6 +397,7 @@ class ConfigurePageComponent extends AuthComponent {
displayedSchema = this.state.schema['properties'][this.state.selectedSection]; displayedSchema = this.state.schema['properties'][this.state.selectedSection];
displayedSchema['definitions'] = this.state.schema['definitions']; displayedSchema['definitions'] = this.state.schema['definitions'];
} }
let content = ''; let content = '';
if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0) { if (this.state.selectedSection === 'attack' && Object.entries(this.state.attackConfig).length !== 0) {
content = this.renderMatrix() content = this.renderMatrix()

View File

@ -1,8 +1,8 @@
import React from 'react'; import React from 'react';
import {Col} from 'react-bootstrap'; import {Col} from 'react-bootstrap';
import rainge from 'rainge'; import rainge from 'rainge';
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome"; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCopyright} from "@fortawesome/free-regular-svg-icons"; import {faCopyright} from '@fortawesome/free-regular-svg-icons';
class LicensePageComponent extends React.Component { class LicensePageComponent extends React.Component {
constructor(props) { constructor(props) {

View File

@ -2,9 +2,9 @@ import React from 'react';
import {Button, Col, Container, Form, Row} from 'react-bootstrap'; import {Button, Col, Container, Form, Row} from 'react-bootstrap';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import Particles from "react-particles-js"; import Particles from 'react-particles-js';
import {particleParams} from "../../styles/particle-component/AuthPageParams"; import {particleParams} from '../../styles/components/particle-component/AuthPageParams';
import monkeyGeneral from "../../images/militant-monkey.svg"; import monkeyGeneral from '../../images/militant-monkey.svg';
class LoginPageComponent extends React.Component { class LoginPageComponent extends React.Component {
login = (event) => { login = (event) => {

View File

@ -8,8 +8,8 @@ import PreviewPaneComponent from 'components/map/preview-pane/PreviewPane';
import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph'; import {ReactiveGraph} from 'components/reactive-graph/ReactiveGraph';
import {getOptions, edgeGroupToColor} from 'components/map/MapOptions'; import {getOptions, edgeGroupToColor} from 'components/map/MapOptions';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import '../../styles/Map.scss'; import '../../styles/components/Map.scss';
import {faInfoCircle} from "@fortawesome/free-solid-svg-icons/faInfoCircle"; import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
class MapPageComponent extends AuthComponent { class MapPageComponent extends AuthComponent {
constructor(props) { constructor(props) {

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import '../../styles/NotFoundPage.scss'; import '../../styles/pages/NotFoundPage.scss';
import monkeyDetective from '../../images/detective-monkey.svg'; import monkeyDetective from '../../images/detective-monkey.svg';

View File

@ -3,7 +3,7 @@ import {Row, Col, Container, Form, Button} from 'react-bootstrap';
import Particles from 'react-particles-js'; import Particles from 'react-particles-js';
import AuthService from '../../services/AuthService'; import AuthService from '../../services/AuthService';
import {particleParams} from '../../styles/particle-component/AuthPageParams'; import {particleParams} from '../../styles/components/particle-component/AuthPageParams';
import monkeyDetective from '../../images/detective-monkey.svg'; import monkeyDetective from '../../images/detective-monkey.svg';
class RegisterPageComponent extends React.Component { class RegisterPageComponent extends React.Component {

View File

@ -8,8 +8,8 @@ import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faClipboard} from '@fortawesome/free-solid-svg-icons/faClipboard'; import {faClipboard} from '@fortawesome/free-solid-svg-icons/faClipboard';
import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck'; import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
import {faSync} from '@fortawesome/free-solid-svg-icons/faSync'; import {faSync} from '@fortawesome/free-solid-svg-icons/faSync';
import {faInfoCircle} from "@fortawesome/free-solid-svg-icons/faInfoCircle"; import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons/faExclamationTriangle"; import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';

View File

@ -3,10 +3,10 @@ import {Col, Button} from 'react-bootstrap';
import {Link} from 'react-router-dom'; import {Link} from 'react-router-dom';
import AuthComponent from '../AuthComponent'; import AuthComponent from '../AuthComponent';
import StartOverModal from '../ui-components/StartOverModal'; import StartOverModal from '../ui-components/StartOverModal';
import '../../styles/StartOverPage.scss'; import '../../styles/pages/StartOverPage.scss';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faInfoCircle} from "@fortawesome/free-solid-svg-icons/faInfoCircle"; import {faInfoCircle} from '@fortawesome/free-solid-svg-icons/faInfoCircle';
import {faCheck} from "@fortawesome/free-solid-svg-icons/faCheck"; import {faCheck} from '@fortawesome/free-solid-svg-icons/faCheck';
class StartOverPageComponent extends AuthComponent { class StartOverPageComponent extends AuthComponent {
constructor(props) { constructor(props) {

View File

@ -6,8 +6,8 @@ import AuthComponent from '../AuthComponent';
import download from 'downloadjs'; import download from 'downloadjs';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import '../../styles/TelemetryPage.scss'; import '../../styles/pages/TelemetryPage.scss';
import {faDownload} from "@fortawesome/free-solid-svg-icons/faDownload"; import {faDownload} from '@fortawesome/free-solid-svg-icons/faDownload';
const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true}/>; const renderJson = (val) => <JSONTree data={val} level={1} theme="eighties" invertTheme={true}/>;
const renderTime = (val) => val.split('.')[0]; const renderTime = (val) => val.split('.')[0];

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import {Col, Button} from 'react-bootstrap'; import {Col, Button} from 'react-bootstrap';
import '../../styles/Collapse.scss'; import '../../styles/components/Collapse.scss';
import { FontAwesomeIcon } from '@fortawesome/react-fontawesome'; import { FontAwesomeIcon } from '@fortawesome/react-fontawesome';
import {faCircle} from '@fortawesome/free-solid-svg-icons/faCircle'; import {faCircle} from '@fortawesome/free-solid-svg-icons/faCircle';
import {faRadiation} from '@fortawesome/free-solid-svg-icons/faRadiation'; import {faRadiation} from '@fortawesome/free-solid-svg-icons/faRadiation';
@ -48,7 +48,7 @@ class AttackReport extends React.Component {
} }
} }
onTechniqueSelect = (technique, value) => { onTechniqueSelect = (technique, _) => {
let selectedTechnique = this.getTechniqueByTitle(technique); let selectedTechnique = this.getTechniqueByTitle(technique);
if (selectedTechnique === false){ if (selectedTechnique === false){
return; return;

View File

@ -473,7 +473,7 @@ class ReportPageComponent extends AuthComponent {
} }
generateShellshockPathListBadges(paths) { generateShellshockPathListBadges(paths) {
return paths.map(path => <span className="badge badge-warning" style={{margin: '2px'}}>{path}</span>); return paths.map(path => <span className="badge badge-warning" style={{margin: '2px'}} key={path}>{path}</span>);
} }
generateSmbPasswordIssue(issue) { generateSmbPasswordIssue(issue) {

View File

@ -3,7 +3,7 @@ import React from 'react';
import Checkbox from '../../ui-components/Checkbox'; import Checkbox from '../../ui-components/Checkbox';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import 'filepond/dist/filepond.min.css'; import 'filepond/dist/filepond.min.css';
import '../../../styles/report/ReportAttackMatrix.scss'; import '../../../styles/pages/report/ReportAttackMatrix.scss';
class ReportMatrixComponent extends React.Component { class ReportMatrixComponent extends React.Component {
constructor(props) { constructor(props) {

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import * as PropTypes from 'prop-types'; import * as PropTypes from 'prop-types';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons/faExclamationTriangle"; import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
export default class MonkeysStillAliveWarning extends Component { export default class MonkeysStillAliveWarning extends Component {
render() { render() {

View File

@ -1,7 +1,7 @@
import React, {Component} from 'react'; import React, {Component} from 'react';
import {NavLink} from 'react-router-dom'; import {NavLink} from 'react-router-dom';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faExclamationTriangle} from "@fortawesome/free-solid-svg-icons/faExclamationTriangle"; import {faExclamationTriangle} from '@fortawesome/free-solid-svg-icons/faExclamationTriangle';
export default class MustRunMonkeyWarning extends Component { export default class MustRunMonkeyWarning extends Component {
render() { render() {

View File

@ -7,7 +7,7 @@ import { faPrint } from '@fortawesome/free-solid-svg-icons/faPrint';
export default class PrintReportButton extends Component { export default class PrintReportButton extends Component {
render() { render() {
return <div className="text-center no-print"> return <div className="text-center no-print">
<Button size="md" variant={"outline-standard"} onClick={this.props.onClick}> <Button size="md" variant={'outline-standard'} onClick={this.props.onClick}>
<FontAwesomeIcon icon={faPrint}/> Print <FontAwesomeIcon icon={faPrint}/> Print
Report</Button> Report</Button>
</div> </div>

View File

@ -1,4 +1,4 @@
import React from "react"; import React from 'react';
export let renderArray = function (val) { export let renderArray = function (val) {
return <>{val.map(x => <div key={x}>{x}</div>)}</>; return <>{val.map(x => <div key={x}>{x}</div>)}</>;

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import Pluralize from 'pluralize'; import Pluralize from 'pluralize';
import {renderArray, renderIpAddresses} from "../common/RenderArrays"; import {renderArray, renderIpAddresses} from '../common/RenderArrays';
const columns = [ const columns = [

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import Pluralize from 'pluralize'; import Pluralize from 'pluralize';
import {renderIpAddresses} from "../common/RenderArrays"; import {renderIpAddresses} from '../common/RenderArrays';
let renderMachine = function (data) { let renderMachine = function (data) {
return <div>{data.label} ( {renderIpAddresses(data)} )</div> return <div>{data.label} ( {renderIpAddresses(data)} )</div>
@ -56,7 +56,7 @@ class PostBreachComponent extends React.Component {
}); });
let defaultPageSize = pbaMachines.length > pageSize ? pageSize : pbaMachines.length; let defaultPageSize = pbaMachines.length > pageSize ? pageSize : pbaMachines.length;
let showPagination = pbaMachines > pageSize; let showPagination = pbaMachines > pageSize;
const pbaCount = pbaMachines.reduce((accumulated, pbaMachine) => accumulated+pbaMachine["pba_results"].length, 0); const pbaCount = pbaMachines.reduce((accumulated, pbaMachine) => accumulated+pbaMachine['pba_results'].length, 0);
return ( return (
<> <>
<p> <p>

View File

@ -1,7 +1,7 @@
import React from 'react'; import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import Pluralize from 'pluralize'; import Pluralize from 'pluralize';
import {renderArray, renderIpAddresses} from "../common/RenderArrays"; import {renderArray, renderIpAddresses} from '../common/RenderArrays';
const columns = [ const columns = [
@ -32,7 +32,7 @@ class ScannedServersComponent extends React.Component {
let showPagination = this.props.data.length > pageSize; let showPagination = this.props.data.length > pageSize;
const scannedMachinesCount = this.props.data.length; const scannedMachinesCount = this.props.data.length;
const reducerFromScannedServerToServicesAmount = (accumulated, scannedServer) => accumulated + scannedServer["services"].length; const reducerFromScannedServerToServicesAmount = (accumulated, scannedServer) => accumulated + scannedServer['services'].length;
const scannedServicesAmount = this.props.data.reduce(reducerFromScannedServerToServicesAmount, 0); const scannedServicesAmount = this.props.data.reduce(reducerFromScannedServerToServicesAmount, 0);
return ( return (

View File

@ -1,6 +1,6 @@
import React from 'react'; import React from 'react';
import ReactTable from 'react-table' import ReactTable from 'react-table'
import {renderArray} from "../common/RenderArrays"; import {renderArray} from '../common/RenderArrays';
const columns = [ const columns = [

View File

@ -18,11 +18,13 @@ const columns = [
{ {
Header: 'Events', id: 'events', Header: 'Events', id: 'events',
accessor: x => { accessor: x => {
return <EventsButton finding_id={x.finding_id} const comp = <EventsButton finding_id={x.finding_id}
latest_events={x.latest_events} latest_events={x.latest_events}
oldest_events={x.oldest_events} oldest_events={x.oldest_events}
event_count={x.event_count} event_count={x.event_count}
exportFilename={'Events_' + x.test_key} />; exportFilename={'Events_' + x.test_key} />;
comp.displayName = 'EventsButton_' + x.finding_id;
return comp;
}, },
maxWidth: EVENTS_COLUMN_MAX_WIDTH maxWidth: EVENTS_COLUMN_MAX_WIDTH
}, },
@ -34,7 +36,9 @@ const columns = [
const pillarLabels = pillars.map((pillar) => const pillarLabels = pillars.map((pillar) =>
<PillarLabel key={pillar.name} pillar={pillar.name} status={pillar.status}/> <PillarLabel key={pillar.name} pillar={pillar.name} status={pillar.status}/>
); );
return <div style={{textAlign: 'center'}}>{pillarLabels}</div>; const comp = <div style={{textAlign: 'center'}}>{pillarLabels}</div>;
comp.displayName = 'PillarsLabels';
return comp;
}, },
maxWidth: PILLARS_COLUMN_MAX_WIDTH, maxWidth: PILLARS_COLUMN_MAX_WIDTH,
style: {'whiteSpace': 'unset'} style: {'whiteSpace': 'unset'}

View File

@ -13,7 +13,9 @@ const columns = [
{ {
Header: 'Status', id: 'status', Header: 'Status', id: 'status',
accessor: x => { accessor: x => {
return <StatusLabel status={x.status} size="3x" showText={false}/>; const comp = <StatusLabel status={x.status} size="3x" showText={false}/>;
comp.displayName = 'StatusLabel';
return comp;
}, },
maxWidth: MAX_WIDTH_STATUS_COLUMN maxWidth: MAX_WIDTH_STATUS_COLUMN
}, },
@ -25,7 +27,9 @@ const columns = [
Header: 'Monkey Tests', id: 'tests', Header: 'Monkey Tests', id: 'tests',
style: {'whiteSpace': 'unset'}, // This enables word wrap style: {'whiteSpace': 'unset'}, // This enables word wrap
accessor: x => { accessor: x => {
return <TestsStatus tests={x.tests}/>; const comp = <TestsStatus tests={x.tests}/>;
comp.displayName = 'TestsStatus';
return comp;
} }
} }
] ]

View File

@ -14,7 +14,7 @@ class ZeroTrustReportLegend extends Component {
super(props, context); super(props, context);
this.state = { this.state = {
open: false, open: false
}; };
} }

View File

@ -7,7 +7,7 @@ import {Card, Collapse} from 'react-bootstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'; import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faChevronDown} from '@fortawesome/free-solid-svg-icons'; import {faChevronDown} from '@fortawesome/free-solid-svg-icons';
import '../../../styles/report/ZeroTrustReport.scss'; import '../../../styles/pages/report/ZeroTrustReport.scss';
export default class SinglePillarPrinciplesStatus extends AuthComponent { export default class SinglePillarPrinciplesStatus extends AuthComponent {
@ -15,7 +15,7 @@ export default class SinglePillarPrinciplesStatus extends AuthComponent {
super(props, context); super(props, context);
this.state = { this.state = {
open: false, open: false
}; };
} }

View File

@ -149,7 +149,7 @@ class VennDiagram extends React.Component {
} else { } else {
// Return z indices to default // Return z indices to default
Object.keys(this.layout).forEach(function (d_, i_) { Object.keys(this.layout).forEach(function (_d, i_) {
document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode); document.querySelector('#' + self.prefix).appendChild(document.querySelector('#' + self.prefix + 'Node_' + i_).parentNode);
}) })
} }

View File

@ -67,7 +67,7 @@ class AwsRunTableComponent extends React.Component {
this.setState({selectAll, selection}); this.setState({selectAll, selection});
}; };
getTrProps = (s, r) => { getTrProps = (_, r) => {
let color = 'inherit'; let color = 'inherit';
if (r) { if (r) {
let instId = r.original.instance_id; let instId = r.original.instance_id;

View File

@ -0,0 +1,121 @@
import React, {useState} from 'react';
import {Card, Button, Form} from 'react-bootstrap';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faCheckSquare} from '@fortawesome/free-solid-svg-icons';
import {faSquare} from '@fortawesome/free-regular-svg-icons';
import {cloneDeep} from 'lodash';
import {getComponentHeight} from './utils/HeightCalculator';
import {resolveObjectPath} from './utils/ObjectPathResolver';
import InfoPane from './InfoPane';
function getSelectValuesAfterClick(valueArray, clickedValue) {
if (valueArray.includes(clickedValue)) {
return valueArray.filter((e) => {
return e !== clickedValue;
});
} else {
valueArray.push(clickedValue);
return valueArray;
}
}
function onMasterCheckboxClick(checkboxValue, defaultArray, onChangeFnc) {
if (checkboxValue) {
onChangeFnc([]);
} else {
onChangeFnc(defaultArray);
}
}
// Definitions passed to components only contains value and label,
// custom fields like "info" or "links" must be pulled from registry object using this function
function getFullDefinitionsFromRegistry(refString, registry) {
return getObjectFromRegistryByRef(refString, registry).anyOf;
}
function getObjectFromRegistryByRef(refString, registry) {
let refArray = refString.replace('#', '').split('/');
return resolveObjectPath(refArray, registry);
}
function getFullDefinitionByKey(refString, registry, itemKey) {
let fullArray = getFullDefinitionsFromRegistry(refString, registry);
return fullArray.filter(e => (e.enum[0] === itemKey))[0];
}
function setPaneInfo(refString, registry, itemKey, setPaneInfoFnc) {
let definitionObj = getFullDefinitionByKey(refString, registry, itemKey);
setPaneInfoFnc({title: definitionObj.title, content: definitionObj.info, link: definitionObj.link});
}
function getDefaultPaneParams(refString, registry) {
let configSection = getObjectFromRegistryByRef(refString, registry);
return ({title: configSection.title, content: configSection.description});
}
function AdvancedMultiSelect(props) {
const [masterCheckbox, setMasterCheckbox] = useState(true);
const {
schema,
id,
options,
value,
required,
disabled,
readonly,
multiple,
autofocus,
onChange,
registry
} = props;
const {enumOptions} = options;
const [infoPaneParams, setInfoPaneParams] = useState(getDefaultPaneParams(schema.items.$ref, registry));
getDefaultPaneParams(schema.items.$ref, registry);
const selectValue = cloneDeep(value);
return (
<div className={'advanced-multi-select'}>
<Card.Header>
<Button key={`${props.schema.title}-button`} value={value}
variant={'link'} disabled={disabled}
onClick={() => {
onMasterCheckboxClick(masterCheckbox, schema.default, onChange);
setMasterCheckbox(!masterCheckbox);
}}
>
<FontAwesomeIcon icon={masterCheckbox ? faCheckSquare : faSquare}/>
</Button>
<span className={'header-title'}>{props.schema.title}</span>
</Card.Header>
<Form.Group
style={{height: `${getComponentHeight(enumOptions.length)}px`}}
id={id}
multiple={multiple}
className='choice-block form-control'
required={required}
disabled={disabled || readonly}
autoFocus={autofocus}>
{enumOptions.map(({value, label}, i) => {
return (
<Form.Group
key={i}
onClick={() => setPaneInfo(schema.items.$ref, registry, value, setInfoPaneParams)}>
<Button value={value} variant={'link'} disabled={disabled}
onClick={() => onChange(getSelectValuesAfterClick(selectValue, value))}>
<FontAwesomeIcon icon={selectValue.includes(value) ? faCheckSquare : faSquare}/>
</Button>
<span className={'option-text'}>
{label}
</span>
</Form.Group>
);
})}
</Form.Group>
<InfoPane title={infoPaneParams.title} body={infoPaneParams.content} link={infoPaneParams.link}/>
</div>
);
}
export default AdvancedMultiSelect;

View File

@ -1,4 +1,4 @@
import '../../styles/Checkbox.scss' import '../../styles/components/Checkbox.scss'
import React from 'react'; import React from 'react';
class CheckboxComponent extends React.PureComponent { class CheckboxComponent extends React.PureComponent {

View File

@ -0,0 +1,52 @@
import {Card, Button} from 'react-bootstrap';
import React from 'react';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome';
import {faQuestionCircle} from '@fortawesome/free-solid-svg-icons';
function InfoPane(props) {
return (
<Card className={'info-pane'}>
{getTitle(props)}
{getSubtitle(props)}
{getBody(props)}
</Card>);
}
function getTitle(props) {
if (typeof (props.title) == 'string') {
return (
<Card.Title className={'pane-title'}>
{props.title}
{getLinkButton(props)}
</Card.Title>)
}
}
function getLinkButton(props) {
if (typeof (props.link) == 'string') {
return (
<Button variant={'link'} className={'pane-link'} href={props.link} target={'_blank'}>
<FontAwesomeIcon icon={faQuestionCircle}/>
</Button>)
}
}
function getSubtitle(props) {
if (typeof (props.subtitle) == 'string') {
return (
<Card.Subtitle className={'pane-subtitle'}>
{props.subtitle}
</Card.Subtitle>)
}
}
function getBody(props) {
return (
<Card.Body className={'pane-body'}>
{props.body}
</Card.Body>
)
}
export default InfoPane

View File

@ -1,6 +1,5 @@
import {Modal} from 'react-bootstrap'; import {Modal} from 'react-bootstrap';
import React from 'react'; import React from 'react';
import {GridLoader} from 'react-spinners';
class MissingBinariesModal extends React.PureComponent { class MissingBinariesModal extends React.PureComponent {

View File

@ -0,0 +1,14 @@
const defaultMinHeight = 25
const defaultMaxHeight = 250
const defaultSubcomponentHeight = 25
export function getComponentHeight(subcomponentCount,
subcomponentHeight = defaultSubcomponentHeight,
minHeight = defaultMinHeight,
maxHeight = defaultMaxHeight) {
let height = minHeight + (subcomponentHeight*subcomponentCount);
if (height > maxHeight)
height = maxHeight
return height
}

View File

@ -0,0 +1,11 @@
// Resolves object's path if it's specified in a dot notation.
// (e.g. params: "firstLevel.secondLevel.property", myObject)
export function resolveObjectPath(pathArray, obj) {
return pathArray.reduce(function(prev, curr) {
if(curr === '')
return prev;
else
return prev ? prev[curr] : null;
}, obj || self)
}

View File

@ -3,12 +3,14 @@
@import '../../node_modules/bootstrap/scss/bootstrap'; @import '../../node_modules/bootstrap/scss/bootstrap';
// Imports that require variables // Imports that require variables
@import './report/ReportPage.scss'; @import 'pages/report/ReportPage.scss';
@import './report/AttackReport.scss'; @import 'pages/report/AttackReport.scss';
@import './PreviewPane.scss'; @import 'pages/ConfigurationPage';
@import './ConfigurationPage.scss'; @import 'pages/AuthPage';
@import './AuthPage.scss'; @import 'pages/MonkeyRunPage';
@import './MonkeyRunPage.scss'; @import 'components/InfoPane';
@import 'components/PreviewPane';
@import 'components/AdvancedMultiSelect';
// Define custom elements after bootstrap import // Define custom elements after bootstrap import

View File

@ -0,0 +1,47 @@
.advanced-multi-select .choice-block.form-control {
overflow: scroll;
overflow-x: hidden;
padding: 0;
margin-left: 0;
}
.advanced-multi-select svg {
font-size: 1.15em;
}
.advanced-multi-select .card-header {
border: 1px solid $gray-300;
border-bottom: none;
border-top-left-radius: 10px;
border-top-right-radius: 10px;
padding-left: 0;
padding-bottom: 5px;
}
.advanced-multi-select .card-header button {
padding-top: 0;
}
.advanced-multi-select .card-header .header-title {
font-size: 1.2em;
}
.advanced-multi-select .choice-block .form-group {
margin: 0;
padding: 3px 0 0 0;
height: 30px;
}
.advanced-multi-select .choice-block .form-group:hover {
background-color: $gray-300;
cursor: pointer;
}
.advanced-multi-select .choice-block .form-group button {
margin: 0 3px 3px 12px;
padding: 0;
}
.advanced-multi-select .option-text {
margin-left: 10px;
}

View File

@ -0,0 +1,29 @@
.info-pane, .card {
margin-top: 5px;
}
.info-pane svg {
font-size: 1.1em;
}
.info-pane .pane-title {
margin: 10px 15px 3px 15px;
color: $monkey-alt;
}
.info-pane .pane-link {
position: absolute;
right: 0;
top: 0;
}
.info-pane .pane-subtitle {
margin: 0 15px;
color: $gray-600;
}
.info-pane .pane-body {
margin: 10px 15px;
padding: 0;
}

View File

@ -2,6 +2,11 @@
height: 50px !important; height: 50px !important;
} }
.config-nav .nav-link.tab-primary{
color: $monkey-alt;
font-weight: bold;
}
.config-nav .nav-item > a{ .config-nav .nav-item > a{
color: $black; color: $black;
padding: 15px 10px 15px 10px; padding: 15px 10px 15px 10px;
@ -25,3 +30,30 @@
.config-form .form-group { .config-form .form-group {
margin-left: 2em; margin-left: 2em;
} }
.config-form div.card.errors {
display: none;
}
.config-template-no-header > p {
display: none;
}
.config-template-no-header > label {
display: none;
}
.config-form .form-group.field > label {
margin-top: 10px;
margin-bottom: 5px;
font-size: 1.2em;
}
.config-field-hidden {
display: none;
}
.field-description {
white-space: pre-wrap;
}