forked from p15670423/monkey
Merge pull request #1944 from guardicore/1928-report-exporter-removal
1928 report exporter removal
This commit is contained in:
commit
95979e6e71
|
@ -3,7 +3,6 @@ import json
|
|||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from threading import Thread
|
||||
|
||||
import gevent.hub
|
||||
from gevent.pywsgi import WSGIServer
|
||||
|
@ -29,7 +28,6 @@ from monkey_island.cc.server_utils.consts import ( # noqa: E402
|
|||
)
|
||||
from monkey_island.cc.server_utils.island_logger import reset_logger, setup_logging # noqa: E402
|
||||
from monkey_island.cc.services.initialize import initialize_services # noqa: E402
|
||||
from monkey_island.cc.services.reporting.exporter_init import populate_exporter_list # noqa: E402
|
||||
from monkey_island.cc.services.utils.network_utils import local_ip_addresses # noqa: E402
|
||||
from monkey_island.cc.setup import island_config_options_validator # noqa: E402
|
||||
from monkey_island.cc.setup.data_dir import IncompatibleDataDirectory, setup_data_dir # noqa: E402
|
||||
|
@ -132,8 +130,6 @@ def _configure_gevent_exception_handling(data_dir):
|
|||
def _start_island_server(
|
||||
should_setup_only: bool, config_options: IslandConfigOptions, container: DIContainer
|
||||
):
|
||||
# AWS exporter takes a long time to load
|
||||
Thread(target=populate_exporter_list, name="Report exporter list", daemon=True).start()
|
||||
app = init_app(mongo_setup.MONGO_URL, container)
|
||||
|
||||
if should_setup_only:
|
||||
|
|
|
@ -7,6 +7,7 @@ from monkey_island.cc.services.post_breach_files import PostBreachFilesService
|
|||
from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
|
||||
|
||||
from . import AuthenticationService, JsonFileUserDatastore
|
||||
from .reporting.report import ReportService
|
||||
|
||||
|
||||
def initialize_services(data_dir: Path) -> DIContainer:
|
||||
|
@ -22,5 +23,6 @@ def initialize_services(data_dir: Path) -> DIContainer:
|
|||
PostBreachFilesService.initialize(container.resolve(IFileStorageService))
|
||||
LocalMonkeyRunService.initialize(data_dir)
|
||||
AuthenticationService.initialize(data_dir, JsonFileUserDatastore(data_dir))
|
||||
ReportService.initialize(container.resolve(AWSService))
|
||||
|
||||
return container
|
||||
|
|
|
@ -1,21 +1,15 @@
|
|||
import logging
|
||||
import uuid
|
||||
from datetime import datetime
|
||||
from typing import Mapping
|
||||
|
||||
import boto3
|
||||
from botocore.exceptions import UnknownServiceError
|
||||
|
||||
from monkey_island.cc.services import aws_service
|
||||
from monkey_island.cc.services.reporting.exporter import Exporter
|
||||
|
||||
__authors__ = ["maor.rayzin", "shay.nehmad"]
|
||||
|
||||
|
||||
from common.aws import AWSInstance
|
||||
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_descriptor_enum import ( # noqa:E501 (Long import)
|
||||
ExploiterDescriptorEnum,
|
||||
)
|
||||
|
||||
# noqa:E501 (Long import)
|
||||
from monkey_island.cc.services.reporting.issue_processing.exploit_processing.exploiter_report_info import ( # noqa:E501 (Long import)
|
||||
CredentialType,
|
||||
)
|
||||
|
@ -25,71 +19,63 @@ logger = logging.getLogger(__name__)
|
|||
INFECTION_MONKEY_ARN = "324264561773:product/guardicore/aws-infection-monkey"
|
||||
|
||||
|
||||
class AWSExporter(Exporter):
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
|
||||
def handle_report(report_json: Mapping, aws_instance: AWSInstance):
|
||||
findings_list = []
|
||||
issues_list = report_json["recommendations"]["issues"]
|
||||
if not issues_list:
|
||||
logger.info("No issues were found by the monkey, no need to send anything")
|
||||
return True
|
||||
|
||||
current_aws_region = aws_service.get_region()
|
||||
|
||||
for machine in issues_list:
|
||||
for issue in issues_list[machine]:
|
||||
try:
|
||||
if "aws_instance_id" in issue:
|
||||
findings_list.append(
|
||||
AWSExporter._prepare_finding(issue, current_aws_region)
|
||||
)
|
||||
except AWSExporter.FindingNotFoundError as e:
|
||||
findings_list.append(_prepare_finding(issue, aws_instance))
|
||||
except FindingNotFoundError as e:
|
||||
logger.error(e)
|
||||
|
||||
if not AWSExporter._send_findings(findings_list, current_aws_region):
|
||||
if not _send_findings(findings_list, aws_instance.region):
|
||||
logger.error("Exporting findings to aws failed")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@staticmethod
|
||||
|
||||
def merge_two_dicts(x, y):
|
||||
z = x.copy() # start with x's keys and values
|
||||
z.update(y) # modifies z with y's keys and values & returns None
|
||||
return z
|
||||
|
||||
@staticmethod
|
||||
def _prepare_finding(issue, region):
|
||||
|
||||
def _prepare_finding(issue, aws_instance: AWSInstance):
|
||||
findings_dict = {
|
||||
"island_cross_segment": AWSExporter._handle_island_cross_segment_issue,
|
||||
"island_cross_segment": _handle_island_cross_segment_issue,
|
||||
ExploiterDescriptorEnum.SSH.value.class_name: {
|
||||
CredentialType.PASSWORD.value: AWSExporter._handle_ssh_issue,
|
||||
CredentialType.KEY.value: AWSExporter._handle_ssh_key_issue,
|
||||
CredentialType.PASSWORD.value: _handle_ssh_issue,
|
||||
CredentialType.KEY.value: _handle_ssh_key_issue,
|
||||
},
|
||||
"tunnel": AWSExporter._handle_tunnel_issue,
|
||||
"tunnel": _handle_tunnel_issue,
|
||||
ExploiterDescriptorEnum.SMB.value.class_name: {
|
||||
CredentialType.PASSWORD.value: AWSExporter._handle_smb_password_issue,
|
||||
CredentialType.HASH.value: AWSExporter._handle_smb_pth_issue,
|
||||
CredentialType.PASSWORD.value: _handle_smb_password_issue,
|
||||
CredentialType.HASH.value: _handle_smb_pth_issue,
|
||||
},
|
||||
"shared_passwords": AWSExporter._handle_shared_passwords_issue,
|
||||
"shared_passwords": _handle_shared_passwords_issue,
|
||||
ExploiterDescriptorEnum.WMI.value.class_name: {
|
||||
CredentialType.PASSWORD.value: AWSExporter._handle_wmi_password_issue,
|
||||
CredentialType.HASH.value: AWSExporter._handle_wmi_pth_issue,
|
||||
CredentialType.PASSWORD.value: _handle_wmi_password_issue,
|
||||
CredentialType.HASH.value: _handle_wmi_pth_issue,
|
||||
},
|
||||
"shared_passwords_domain": AWSExporter._handle_shared_passwords_domain_issue,
|
||||
"shared_admins_domain": AWSExporter._handle_shared_admins_domain_issue,
|
||||
"strong_users_on_crit": AWSExporter._handle_strong_users_on_crit_issue,
|
||||
ExploiterDescriptorEnum.HADOOP.value.class_name: AWSExporter._handle_hadoop_issue,
|
||||
"shared_passwords_domain": _handle_shared_passwords_domain_issue,
|
||||
"shared_admins_domain": _handle_shared_admins_domain_issue,
|
||||
"strong_users_on_crit": _handle_strong_users_on_crit_issue,
|
||||
ExploiterDescriptorEnum.HADOOP.value.class_name: _handle_hadoop_issue,
|
||||
}
|
||||
|
||||
configured_product_arn = INFECTION_MONKEY_ARN
|
||||
product_arn = "arn:aws:securityhub:{region}:{arn}".format(
|
||||
region=region, arn=configured_product_arn
|
||||
region=aws_instance.region, arn=configured_product_arn
|
||||
)
|
||||
instance_arn = "arn:aws:ec2:" + str(region) + ":instance:{instance_id}"
|
||||
# Not suppressing error here on purpose.
|
||||
account_id = aws_service.get_account_id()
|
||||
instance_arn = "arn:aws:ec2:" + str(aws_instance.region) + ":instance:{instance_id}"
|
||||
account_id = aws_instance.account_id
|
||||
logger.debug("aws account id acquired: {}".format(account_id))
|
||||
|
||||
aws_finding = {
|
||||
|
@ -104,11 +90,11 @@ class AWSExporter(Exporter):
|
|||
"UpdatedAt": datetime.now().isoformat() + "Z",
|
||||
}
|
||||
|
||||
processor = AWSExporter._get_issue_processor(findings_dict, issue)
|
||||
processor = _get_issue_processor(findings_dict, issue)
|
||||
|
||||
return merge_two_dicts(aws_finding, processor(issue, instance_arn))
|
||||
|
||||
return AWSExporter.merge_two_dicts(aws_finding, processor(issue, instance_arn))
|
||||
|
||||
@staticmethod
|
||||
def _get_issue_processor(finding_dict, issue):
|
||||
try:
|
||||
processor = finding_dict[issue["type"]]
|
||||
|
@ -116,14 +102,13 @@ class AWSExporter(Exporter):
|
|||
processor = processor[issue["credential_type"]]
|
||||
return processor
|
||||
except KeyError:
|
||||
raise AWSExporter.FindingNotFoundError(
|
||||
f"Finding {issue['type']} not added as AWS exportable finding"
|
||||
)
|
||||
raise FindingNotFoundError(f"Finding {issue['type']} not added as AWS exportable finding")
|
||||
|
||||
|
||||
class FindingNotFoundError(Exception):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _send_findings(findings_list, region):
|
||||
try:
|
||||
logger.debug("Trying to acquire securityhub boto3 client in " + region)
|
||||
|
@ -150,20 +135,20 @@ class AWSExporter(Exporter):
|
|||
logger.exception("AWS security hub findings failed to send. Error: {}".format(e))
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _get_finding_resource(instance_id, instance_arn):
|
||||
if instance_id:
|
||||
return [{"Type": "AwsEc2Instance", "Id": instance_arn.format(instance_id=instance_id)}]
|
||||
else:
|
||||
return [{"Type": "Other", "Id": "None"}]
|
||||
|
||||
@staticmethod
|
||||
|
||||
def _build_generic_finding(
|
||||
severity, title, description, recommendation, instance_arn, instance_id=None
|
||||
):
|
||||
finding = {
|
||||
"Severity": {"Product": severity, "Normalized": 100},
|
||||
"Resources": AWSExporter._get_finding_resource(instance_id, instance_arn),
|
||||
"Resources": _get_finding_resource(instance_id, instance_arn),
|
||||
"Title": title,
|
||||
"Description": description,
|
||||
"Remediation": {"Recommendation": {"Text": recommendation}},
|
||||
|
@ -171,10 +156,9 @@ class AWSExporter(Exporter):
|
|||
|
||||
return finding
|
||||
|
||||
@staticmethod
|
||||
def _handle_tunnel_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_tunnel_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=5,
|
||||
title="Weak segmentation - Machines were able to communicate over unused ports.",
|
||||
description="Use micro-segmentation policies to disable communication other than "
|
||||
|
@ -185,10 +169,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_smb_pth_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_smb_pth_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=5,
|
||||
title="Machines are accessible using passwords supplied by the user during the "
|
||||
"Monkey's configuration.",
|
||||
|
@ -204,10 +187,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_ssh_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_ssh_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Machines are accessible using SSH passwords supplied by the user during "
|
||||
"the Monkey's configuration.",
|
||||
|
@ -222,10 +204,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_ssh_key_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_ssh_key_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Machines are accessible using SSH passwords supplied by the user during "
|
||||
"the Monkey's configuration.",
|
||||
|
@ -241,13 +222,11 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_island_cross_segment_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_island_cross_segment_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Weak segmentation - Machines from different segments are able to "
|
||||
"communicate.",
|
||||
title="Weak segmentation - Machines from different segments are able to " "communicate.",
|
||||
description="Segment your network and make sure there is no communication between "
|
||||
"machines from different "
|
||||
"segments.",
|
||||
|
@ -260,25 +239,21 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_shared_passwords_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_shared_passwords_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Multiple users have the same password",
|
||||
description="Some users are sharing passwords, this should be fixed by changing "
|
||||
"passwords.",
|
||||
recommendation="These users are sharing access password: {0}.".format(
|
||||
issue["shared_with"]
|
||||
),
|
||||
recommendation="These users are sharing access password: {0}.".format(issue["shared_with"]),
|
||||
instance_arn=instance_arn,
|
||||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_smb_password_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_smb_password_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Machines are accessible using passwords supplied by the user during the "
|
||||
"Monkey's configuration.",
|
||||
|
@ -294,10 +269,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_wmi_password_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_wmi_password_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Machines are accessible using passwords supplied by the user during the "
|
||||
"Monkey's configuration.",
|
||||
|
@ -313,10 +287,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_wmi_pth_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_wmi_pth_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Machines are accessible using passwords supplied by the user during the "
|
||||
"Monkey's configuration.",
|
||||
|
@ -332,10 +305,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_shared_passwords_domain_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_shared_passwords_domain_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Multiple users have the same password.",
|
||||
description="Some domain users are sharing passwords, this should be fixed by "
|
||||
|
@ -347,10 +319,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_shared_admins_domain_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_shared_admins_domain_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Shared local administrator account - Different machines have the same "
|
||||
"account as a local administrator.",
|
||||
|
@ -366,10 +337,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_strong_users_on_crit_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_strong_users_on_crit_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=1,
|
||||
title="Mimikatz found login credentials of a user who has admin access to a "
|
||||
"server defined as critical.",
|
||||
|
@ -384,10 +354,9 @@ class AWSExporter(Exporter):
|
|||
instance_id=issue["aws_instance_id"] if "aws_instance_id" in issue else None,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _handle_hadoop_issue(issue, instance_arn):
|
||||
|
||||
return AWSExporter._build_generic_finding(
|
||||
def _handle_hadoop_issue(issue, instance_arn):
|
||||
return _build_generic_finding(
|
||||
severity=10,
|
||||
title="Hadoop/Yarn servers are vulnerable to remote code execution.",
|
||||
description="Run Hadoop in secure mode, add Kerberos authentication.",
|
||||
|
|
|
@ -1,7 +0,0 @@
|
|||
class Exporter(object):
|
||||
def __init__(self):
|
||||
pass
|
||||
|
||||
@staticmethod
|
||||
def handle_report(report_json):
|
||||
raise NotImplementedError
|
|
@ -1,28 +0,0 @@
|
|||
import logging
|
||||
|
||||
from monkey_island.cc.services import aws_service
|
||||
from monkey_island.cc.services.reporting.aws_exporter import AWSExporter
|
||||
from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def populate_exporter_list():
|
||||
manager = ReportExporterManager()
|
||||
# try_add_aws_exporter_to_manager(manager)
|
||||
|
||||
if len(manager.get_exporters_list()) != 0:
|
||||
logger.debug(
|
||||
"Populated exporters list with the following exporters: {0}".format(
|
||||
str(manager.get_exporters_list())
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def try_add_aws_exporter_to_manager(manager):
|
||||
# noinspection PyBroadException
|
||||
try:
|
||||
if aws_service.is_on_aws():
|
||||
manager.add_exporter_to_list(AWSExporter)
|
||||
except Exception:
|
||||
logger.error("Failed adding aws exporter to manager. Exception info:", exc_info=True)
|
|
@ -26,7 +26,6 @@ from monkey_island.cc.services.reporting.exploitations.monkey_exploitation impor
|
|||
get_monkey_exploited,
|
||||
)
|
||||
from monkey_island.cc.services.reporting.pth_report import PTHReportService
|
||||
from monkey_island.cc.services.reporting.report_exporter_manager import ReportExporterManager
|
||||
from monkey_island.cc.services.reporting.report_generation_synchronisation import (
|
||||
safe_generate_regular_report,
|
||||
)
|
||||
|
@ -36,6 +35,8 @@ from monkey_island.cc.services.reporting.stolen_credentials import (
|
|||
)
|
||||
from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses
|
||||
|
||||
from .. import AWSService
|
||||
from . import aws_exporter
|
||||
from .issue_processing.exploit_processing.exploiter_descriptor_enum import ExploiterDescriptorEnum
|
||||
from .issue_processing.exploit_processing.processors.cred_exploit import CredentialType
|
||||
from .issue_processing.exploit_processing.processors.exploit import ExploiterReportInfo
|
||||
|
@ -44,11 +45,18 @@ logger = logging.getLogger(__name__)
|
|||
|
||||
|
||||
class ReportService:
|
||||
|
||||
_aws_service = None
|
||||
|
||||
class DerivedIssueEnum:
|
||||
WEAK_PASSWORD = "weak_password"
|
||||
STOLEN_CREDS = "stolen_creds"
|
||||
ZEROLOGON_PASS_RESTORE_FAILED = "zerologon_pass_restore_failed"
|
||||
|
||||
@classmethod
|
||||
def initialize(cls, aws_service: AWSService):
|
||||
cls._aws_service = aws_service
|
||||
|
||||
@staticmethod
|
||||
def get_first_monkey_time():
|
||||
return (
|
||||
|
@ -488,8 +496,8 @@ class ReportService:
|
|||
"recommendations": {"issues": issues, "domain_issues": domain_issues},
|
||||
"meta_info": {"latest_monkey_modifytime": monkey_latest_modify_time},
|
||||
}
|
||||
ReportExporterManager().export(report)
|
||||
save_report(report)
|
||||
aws_exporter.handle_report(report, ReportService._aws_service.island_aws_instance)
|
||||
return report
|
||||
|
||||
@staticmethod
|
||||
|
|
|
@ -1,24 +0,0 @@
|
|||
import logging
|
||||
|
||||
from common.utils.code_utils import Singleton
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReportExporterManager(object, metaclass=Singleton):
|
||||
def __init__(self):
|
||||
self._exporters_set = set()
|
||||
|
||||
def get_exporters_list(self):
|
||||
return self._exporters_set
|
||||
|
||||
def add_exporter_to_list(self, exporter):
|
||||
self._exporters_set.add(exporter)
|
||||
|
||||
def export(self, report):
|
||||
for exporter in self._exporters_set:
|
||||
logger.debug("Trying to export using " + repr(exporter))
|
||||
try:
|
||||
exporter().handle_report(report)
|
||||
except Exception as e:
|
||||
logger.exception("Failed to export report, error: " + e)
|
|
@ -74,7 +74,7 @@ function AWSInstanceTable(props) {
|
|||
if (isSelected(instId)) {
|
||||
color = '#ffed9f';
|
||||
} else if (runResult) {
|
||||
color = runResult.status === "error" ? '#f00000' : '#00f01b'
|
||||
color = runResult.status === 'error' ? '#f00000' : '#00f01b'
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue