forked from p15670423/monkey
Merge pull request #2093 from guardicore/1965-credentials-report
1965 credentials report
This commit is contained in:
commit
e93455031a
|
@ -7,6 +7,5 @@ from .monkey import Monkey
|
||||||
from .monkey_ttl import MonkeyTtl
|
from .monkey_ttl import MonkeyTtl
|
||||||
from .pba_results import PbaResults
|
from .pba_results import PbaResults
|
||||||
from monkey_island.cc.models.report.report import Report
|
from monkey_island.cc.models.report.report import Report
|
||||||
from .stolen_credentials import StolenCredentials
|
|
||||||
from .simulation import Simulation, SimulationSchema, IslandMode
|
from .simulation import Simulation, SimulationSchema, IslandMode
|
||||||
from .user_credentials import UserCredentials
|
from .user_credentials import UserCredentials
|
||||||
|
|
|
@ -1,21 +0,0 @@
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
from mongoengine import Document, ListField, ReferenceField
|
|
||||||
|
|
||||||
from monkey_island.cc.models import Monkey
|
|
||||||
|
|
||||||
|
|
||||||
class StolenCredentials(Document):
|
|
||||||
"""
|
|
||||||
This class has 2 main section:
|
|
||||||
* The schema section defines the DB fields in the document. This is the
|
|
||||||
data of the object.
|
|
||||||
* The logic section defines complex questions we can ask about a single document
|
|
||||||
which are asked multiple times, somewhat like an API.
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# SCHEMA
|
|
||||||
monkey = ReferenceField(Monkey)
|
|
||||||
identities = ListField()
|
|
||||||
secrets = ListField()
|
|
|
@ -1,7 +1,7 @@
|
||||||
from common.utils.attack_utils import ScanStatus
|
from common.utils.attack_utils import ScanStatus
|
||||||
from monkey_island.cc.models import StolenCredentials
|
from monkey_island.cc.repository import ICredentialsRepository
|
||||||
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
from monkey_island.cc.services.attack.technique_reports import AttackTechnique
|
||||||
from monkey_island.cc.services.reporting.stolen_credentials import get_stolen_creds
|
from monkey_island.cc.services.reporting import format_creds_for_reporting
|
||||||
|
|
||||||
|
|
||||||
class T1003(AttackTechnique):
|
class T1003(AttackTechnique):
|
||||||
|
@ -16,8 +16,21 @@ class T1003(AttackTechnique):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_report_data():
|
def get_report_data():
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class T1003GetReportData:
|
||||||
|
"""
|
||||||
|
Class to patch the T1003 attack technique which
|
||||||
|
needs stolen credentials from db.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, credentials_repository: ICredentialsRepository):
|
||||||
|
self._credentials_repository = credentials_repository
|
||||||
|
|
||||||
|
def __call__(self):
|
||||||
def get_technique_status_and_data():
|
def get_technique_status_and_data():
|
||||||
if list(StolenCredentials.objects()):
|
if list(self._credentials_repository.get_stolen_credentials()):
|
||||||
status = ScanStatus.USED.value
|
status = ScanStatus.USED.value
|
||||||
else:
|
else:
|
||||||
status = ScanStatus.UNSCANNED.value
|
status = ScanStatus.UNSCANNED.value
|
||||||
|
@ -28,5 +41,7 @@ class T1003(AttackTechnique):
|
||||||
|
|
||||||
data.update(T1003.get_message_and_status(status))
|
data.update(T1003.get_message_and_status(status))
|
||||||
data.update(T1003.get_mitigation_by_status(status))
|
data.update(T1003.get_mitigation_by_status(status))
|
||||||
data["stolen_creds"] = get_stolen_creds()
|
data["stolen_creds"] = format_creds_for_reporting(
|
||||||
|
self._credentials_repository.get_stolen_credentials()
|
||||||
|
)
|
||||||
return data
|
return data
|
||||||
|
|
|
@ -33,6 +33,7 @@ from monkey_island.cc.repository import (
|
||||||
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
|
from monkey_island.cc.server_utils.consts import MONKEY_ISLAND_ABS_PATH
|
||||||
from monkey_island.cc.server_utils.encryption import ILockableEncryptor, RepositoryEncryptor
|
from monkey_island.cc.server_utils.encryption import ILockableEncryptor, RepositoryEncryptor
|
||||||
from monkey_island.cc.services import AWSService, IslandModeService, RepositoryService
|
from monkey_island.cc.services import AWSService, IslandModeService, RepositoryService
|
||||||
|
from monkey_island.cc.services.attack.technique_reports.T1003 import T1003, T1003GetReportData
|
||||||
from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
|
from monkey_island.cc.services.run_local_monkey import LocalMonkeyRunService
|
||||||
from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import (
|
from monkey_island.cc.services.telemetry.processing.credentials.credentials_parser import (
|
||||||
CredentialsParser,
|
CredentialsParser,
|
||||||
|
@ -64,9 +65,7 @@ def initialize_services(data_dir: Path) -> DIContainer:
|
||||||
_register_repositories(container, data_dir)
|
_register_repositories(container, data_dir)
|
||||||
_register_services(container)
|
_register_services(container)
|
||||||
|
|
||||||
# Note: A hack to resolve credentials parser
|
_dirty_hacks(container)
|
||||||
# It changes telemetry processing function, this will be refactored!
|
|
||||||
_patch_credentials_parser(container)
|
|
||||||
|
|
||||||
# This is temporary until we get DI all worked out.
|
# This is temporary until we get DI all worked out.
|
||||||
ReportService.initialize(
|
ReportService.initialize(
|
||||||
|
@ -153,7 +152,16 @@ def _register_services(container: DIContainer):
|
||||||
container.register_instance(RepositoryService, container.resolve(RepositoryService))
|
container.register_instance(RepositoryService, container.resolve(RepositoryService))
|
||||||
|
|
||||||
|
|
||||||
def _patch_credentials_parser(container: DIContainer):
|
def _dirty_hacks(container: DIContainer):
|
||||||
|
# A dirty hacks function that patches some of the things that
|
||||||
|
# are needed at the current point
|
||||||
|
|
||||||
|
# Patches attack technique T1003 which is a static class
|
||||||
|
# but it needs stolen credentials from the database
|
||||||
|
T1003.get_report_data = container.resolve(T1003GetReportData)
|
||||||
|
|
||||||
|
# Note: A hack to resolve credentials parser
|
||||||
|
# It changes telemetry processing function, this will be refactored!
|
||||||
TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[TelemCategoryEnum.CREDENTIALS] = container.resolve(
|
TELEMETRY_CATEGORY_TO_PROCESSING_FUNC[TelemCategoryEnum.CREDENTIALS] = container.resolve(
|
||||||
CredentialsParser
|
CredentialsParser
|
||||||
)
|
)
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from .format_credentials import format_creds_for_reporting
|
|
@ -0,0 +1,38 @@
|
||||||
|
import logging
|
||||||
|
from typing import Mapping, Sequence
|
||||||
|
|
||||||
|
from common.credentials import CredentialComponentType, Credentials
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
def format_creds_for_reporting(credentials: Sequence[Credentials]) -> Sequence[Mapping]:
|
||||||
|
logger.info("Stolen creds generated for reporting")
|
||||||
|
|
||||||
|
formatted_creds = []
|
||||||
|
cred_type_dict = {
|
||||||
|
CredentialComponentType.PASSWORD: "Clear Password",
|
||||||
|
CredentialComponentType.LM_HASH: "LM hash",
|
||||||
|
CredentialComponentType.NT_HASH: "NTLM hash",
|
||||||
|
CredentialComponentType.SSH_KEYPAIR: "Clear SSH private key",
|
||||||
|
}
|
||||||
|
for cred in credentials:
|
||||||
|
secret = cred.secret
|
||||||
|
if secret is None:
|
||||||
|
continue
|
||||||
|
|
||||||
|
if secret.credential_type not in cred_type_dict:
|
||||||
|
continue
|
||||||
|
username = _get_username(cred)
|
||||||
|
cred_row = {
|
||||||
|
"username": username,
|
||||||
|
"_type": secret.credential_type.name,
|
||||||
|
"type": cred_type_dict[secret.credential_type],
|
||||||
|
}
|
||||||
|
if cred_row not in formatted_creds:
|
||||||
|
formatted_creds.append(cred_row)
|
||||||
|
return formatted_creds
|
||||||
|
|
||||||
|
|
||||||
|
def _get_username(credentials: Credentials) -> str:
|
||||||
|
return credentials.identity.username if credentials.identity else ""
|
|
@ -4,7 +4,6 @@ import logging
|
||||||
from itertools import chain, product
|
from itertools import chain, product
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
from common.credentials import CredentialComponentType
|
|
||||||
from common.network.network_range import NetworkRange
|
from common.network.network_range import NetworkRange
|
||||||
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
|
from common.network.segmentation_utils import get_ip_in_src_and_not_in_dst
|
||||||
from monkey_island.cc.database import mongo
|
from monkey_island.cc.database import mongo
|
||||||
|
@ -20,16 +19,11 @@ from monkey_island.cc.services.reporting.pth_report import PTHReportService
|
||||||
from monkey_island.cc.services.reporting.report_generation_synchronisation import (
|
from monkey_island.cc.services.reporting.report_generation_synchronisation import (
|
||||||
safe_generate_regular_report,
|
safe_generate_regular_report,
|
||||||
)
|
)
|
||||||
from monkey_island.cc.services.reporting.stolen_credentials import (
|
|
||||||
extract_ssh_keys,
|
|
||||||
get_stolen_creds,
|
|
||||||
)
|
|
||||||
from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses
|
from monkey_island.cc.services.utils.network_utils import get_subnets, local_ip_addresses
|
||||||
|
|
||||||
from .. import AWSService
|
from .. import AWSService
|
||||||
from . import aws_exporter
|
from . import aws_exporter
|
||||||
from .issue_processing.exploit_processing.exploiter_descriptor_enum import ExploiterDescriptorEnum
|
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
|
from .issue_processing.exploit_processing.processors.exploit import ExploiterReportInfo
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -42,8 +36,6 @@ class ReportService:
|
||||||
_credentials_repository = None
|
_credentials_repository = None
|
||||||
|
|
||||||
class DerivedIssueEnum:
|
class DerivedIssueEnum:
|
||||||
WEAK_PASSWORD = "weak_password"
|
|
||||||
STOLEN_CREDS = "stolen_creds"
|
|
||||||
ZEROLOGON_PASS_RESTORE_FAILED = "zerologon_pass_restore_failed"
|
ZEROLOGON_PASS_RESTORE_FAILED = "zerologon_pass_restore_failed"
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
|
@ -382,37 +374,6 @@ class ReportService:
|
||||||
def get_manual_monkey_hostnames():
|
def get_manual_monkey_hostnames():
|
||||||
return [monkey["hostname"] for monkey in get_manual_monkeys()]
|
return [monkey["hostname"] for monkey in get_manual_monkeys()]
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_config_users(cls):
|
|
||||||
usernames = []
|
|
||||||
configured_credentials = cls._credentials_repository.get_configured_credentials()
|
|
||||||
for credentials in configured_credentials:
|
|
||||||
usernames = chain(
|
|
||||||
usernames,
|
|
||||||
(
|
|
||||||
identity
|
|
||||||
for identity in credentials.identities
|
|
||||||
if identity.credential_type == CredentialComponentType.USERNAME
|
|
||||||
),
|
|
||||||
)
|
|
||||||
return [u.username for u in usernames]
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def get_config_passwords(cls):
|
|
||||||
passwords = []
|
|
||||||
configured_credentials = cls._credentials_repository.get_configured_credentials()
|
|
||||||
for credentials in configured_credentials:
|
|
||||||
passwords = chain(
|
|
||||||
passwords,
|
|
||||||
(
|
|
||||||
secret
|
|
||||||
for secret in credentials.secrets
|
|
||||||
if secret.credential_type == CredentialComponentType.PASSWORD
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
return [p.password for p in passwords]
|
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_config_exploits(cls):
|
def get_config_exploits(cls):
|
||||||
agent_configuration = cls._agent_configuration_repository.get_configuration()
|
agent_configuration = cls._agent_configuration_repository.get_configuration()
|
||||||
|
@ -438,42 +399,18 @@ class ReportService:
|
||||||
return agent_configuration.propagation.network_scan.targets.local_network_scan
|
return agent_configuration.propagation.network_scan.targets.local_network_scan
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_issue_set(issues, config_users, config_passwords):
|
def get_issue_set(issues):
|
||||||
issue_set = set()
|
issue_set = set()
|
||||||
|
|
||||||
for machine in issues:
|
for machine in issues:
|
||||||
for issue in issues[machine]:
|
for issue in issues[machine]:
|
||||||
if ReportService._is_weak_credential_issue(issue, config_users, config_passwords):
|
if ReportService._is_zerologon_pass_restore_failed(issue):
|
||||||
issue_set.add(ReportService.DerivedIssueEnum.WEAK_PASSWORD)
|
|
||||||
elif ReportService._is_stolen_credential_issue(issue):
|
|
||||||
issue_set.add(ReportService.DerivedIssueEnum.STOLEN_CREDS)
|
|
||||||
elif ReportService._is_zerologon_pass_restore_failed(issue):
|
|
||||||
issue_set.add(ReportService.DerivedIssueEnum.ZEROLOGON_PASS_RESTORE_FAILED)
|
issue_set.add(ReportService.DerivedIssueEnum.ZEROLOGON_PASS_RESTORE_FAILED)
|
||||||
|
|
||||||
issue_set.add(issue["type"])
|
issue_set.add(issue["type"])
|
||||||
|
|
||||||
return issue_set
|
return issue_set
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_weak_credential_issue(
|
|
||||||
issue: dict, config_usernames: List[str], config_passwords: List[str]
|
|
||||||
) -> bool:
|
|
||||||
# Only credential exploiter issues have 'credential_type'
|
|
||||||
return (
|
|
||||||
"credential_type" in issue
|
|
||||||
and issue["credential_type"] == CredentialType.PASSWORD.value
|
|
||||||
and issue["password"] in config_passwords
|
|
||||||
and issue["username"] in config_usernames
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def _is_stolen_credential_issue(issue: dict) -> bool:
|
|
||||||
# Only credential exploiter issues have 'credential_type'
|
|
||||||
return "credential_type" in issue and (
|
|
||||||
issue["credential_type"] == CredentialType.PASSWORD.value
|
|
||||||
or issue["credential_type"] == CredentialType.HASH.value
|
|
||||||
)
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _is_zerologon_pass_restore_failed(issue: dict):
|
def _is_zerologon_pass_restore_failed(issue: dict):
|
||||||
return (
|
return (
|
||||||
|
@ -490,12 +427,9 @@ class ReportService:
|
||||||
def generate_report():
|
def generate_report():
|
||||||
domain_issues = ReportService.get_domain_issues()
|
domain_issues = ReportService.get_domain_issues()
|
||||||
issues = ReportService.get_issues()
|
issues = ReportService.get_issues()
|
||||||
config_users = ReportService.get_config_users()
|
issue_set = ReportService.get_issue_set(issues)
|
||||||
config_passwords = ReportService.get_config_passwords()
|
|
||||||
issue_set = ReportService.get_issue_set(issues, config_users, config_passwords)
|
|
||||||
cross_segment_issues = ReportService.get_cross_segment_issues()
|
cross_segment_issues = ReportService.get_cross_segment_issues()
|
||||||
monkey_latest_modify_time = Monkey.get_latest_modifytime()
|
monkey_latest_modify_time = Monkey.get_latest_modifytime()
|
||||||
stolen_creds = get_stolen_creds()
|
|
||||||
|
|
||||||
scanned_nodes = ReportService.get_scanned()
|
scanned_nodes = ReportService.get_scanned()
|
||||||
exploited_cnt = len(get_monkey_exploited())
|
exploited_cnt = len(get_monkey_exploited())
|
||||||
|
@ -515,8 +449,6 @@ class ReportService:
|
||||||
"glance": {
|
"glance": {
|
||||||
"scanned": scanned_nodes,
|
"scanned": scanned_nodes,
|
||||||
"exploited_cnt": exploited_cnt,
|
"exploited_cnt": exploited_cnt,
|
||||||
"stolen_creds": stolen_creds,
|
|
||||||
"ssh_keys": extract_ssh_keys(stolen_creds),
|
|
||||||
"strong_users": PTHReportService.get_strong_users_on_crit_details(),
|
"strong_users": PTHReportService.get_strong_users_on_crit_details(),
|
||||||
},
|
},
|
||||||
"recommendations": {"issues": issues, "domain_issues": domain_issues},
|
"recommendations": {"issues": issues, "domain_issues": domain_issues},
|
||||||
|
|
|
@ -1,52 +0,0 @@
|
||||||
import logging
|
|
||||||
from typing import Mapping, Sequence
|
|
||||||
|
|
||||||
from common.credentials import CredentialComponentType
|
|
||||||
from monkey_island.cc.models import StolenCredentials
|
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
def get_stolen_creds() -> Sequence[Mapping]:
|
|
||||||
stolen_creds = _fetch_from_db()
|
|
||||||
stolen_creds = _format_creds_for_reporting(stolen_creds)
|
|
||||||
|
|
||||||
logger.info("Stolen creds generated for reporting")
|
|
||||||
return stolen_creds
|
|
||||||
|
|
||||||
|
|
||||||
def extract_ssh_keys(credentials: Sequence[Mapping]) -> Sequence[Mapping]:
|
|
||||||
return [c for c in credentials if c["_type"] == CredentialComponentType.SSH_KEYPAIR.name]
|
|
||||||
|
|
||||||
|
|
||||||
def _fetch_from_db() -> Sequence[StolenCredentials]:
|
|
||||||
return list(StolenCredentials.objects())
|
|
||||||
|
|
||||||
|
|
||||||
def _format_creds_for_reporting(credentials: Sequence[StolenCredentials]):
|
|
||||||
formatted_creds = []
|
|
||||||
cred_type_dict = {
|
|
||||||
CredentialComponentType.PASSWORD.name: "Clear Password",
|
|
||||||
CredentialComponentType.LM_HASH.name: "LM hash",
|
|
||||||
CredentialComponentType.NT_HASH.name: "NTLM hash",
|
|
||||||
CredentialComponentType.SSH_KEYPAIR.name: "Clear SSH private key",
|
|
||||||
}
|
|
||||||
|
|
||||||
for cred in credentials:
|
|
||||||
for secret_type in cred.secrets:
|
|
||||||
if secret_type not in cred_type_dict:
|
|
||||||
continue
|
|
||||||
username = _get_username(cred)
|
|
||||||
cred_row = {
|
|
||||||
"username": username,
|
|
||||||
"_type": secret_type,
|
|
||||||
"type": cred_type_dict[secret_type],
|
|
||||||
"origin": cred.monkey.hostname,
|
|
||||||
}
|
|
||||||
if cred_row not in formatted_creds:
|
|
||||||
formatted_creds.append(cred_row)
|
|
||||||
return formatted_creds
|
|
||||||
|
|
||||||
|
|
||||||
def _get_username(credentials: StolenCredentials) -> str:
|
|
||||||
return credentials.identities[0]["username"] if credentials.identities else ""
|
|
|
@ -15,10 +15,10 @@ class CredentialsParser:
|
||||||
def __init__(self, credentials_repository: ICredentialsRepository):
|
def __init__(self, credentials_repository: ICredentialsRepository):
|
||||||
self._credentials_repository = credentials_repository
|
self._credentials_repository = credentials_repository
|
||||||
|
|
||||||
def __call__(self, telemetry_dict):
|
def __call__(self, telemetry_dict, _agent_configuration):
|
||||||
self._parse_credentials(telemetry_dict)
|
self._parse_credentials(telemetry_dict, _agent_configuration)
|
||||||
|
|
||||||
def _parse_credentials(self, telemetry_dict: Mapping):
|
def _parse_credentials(self, telemetry_dict: Mapping, _agent_configuration):
|
||||||
credentials = [
|
credentials = [
|
||||||
Credentials.from_mapping(credential) for credential in telemetry_dict["data"]
|
Credentials.from_mapping(credential) for credential in telemetry_dict["data"]
|
||||||
]
|
]
|
||||||
|
|
|
@ -18,7 +18,9 @@ class T1003 extends React.Component {
|
||||||
<br/>
|
<br/>
|
||||||
{this.props.data.status === ScanStatus.USED ?
|
{this.props.data.status === ScanStatus.USED ?
|
||||||
<StolenPasswordsComponent
|
<StolenPasswordsComponent
|
||||||
data={this.props.data.stolen_creds}/>
|
data={this.props.data.stolen_creds}
|
||||||
|
format={false}
|
||||||
|
/>
|
||||||
: ''}
|
: ''}
|
||||||
<MitigationsComponent mitigations={this.props.data.mitigations}/>
|
<MitigationsComponent mitigations={this.props.data.mitigations}/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -37,7 +37,6 @@ import {
|
||||||
} from './security/issues/SharedPasswordsIssue';
|
} from './security/issues/SharedPasswordsIssue';
|
||||||
import {tunnelIssueReport, tunnelIssueOverview} from './security/issues/TunnelIssue';
|
import {tunnelIssueReport, tunnelIssueOverview} from './security/issues/TunnelIssue';
|
||||||
import {stolenCredsIssueOverview} from './security/issues/StolenCredsIssue';
|
import {stolenCredsIssueOverview} from './security/issues/StolenCredsIssue';
|
||||||
import {weakPasswordIssueOverview} from './security/issues/WeakPasswordIssue';
|
|
||||||
import {strongUsersOnCritIssueReport} from './security/issues/StrongUsersOnCritIssue';
|
import {strongUsersOnCritIssueReport} from './security/issues/StrongUsersOnCritIssue';
|
||||||
import {
|
import {
|
||||||
zerologonIssueOverview,
|
zerologonIssueOverview,
|
||||||
|
@ -45,6 +44,7 @@ import {
|
||||||
zerologonOverviewWithFailedPassResetWarning
|
zerologonOverviewWithFailedPassResetWarning
|
||||||
} from './security/issues/ZerologonIssue';
|
} from './security/issues/ZerologonIssue';
|
||||||
import {powershellIssueOverview, powershellIssueReport} from './security/issues/PowershellIssue';
|
import {powershellIssueOverview, powershellIssueReport} from './security/issues/PowershellIssue';
|
||||||
|
import UsedCredentials from './security/UsedCredentials';
|
||||||
|
|
||||||
|
|
||||||
class ReportPageComponent extends AuthComponent {
|
class ReportPageComponent extends AuthComponent {
|
||||||
|
@ -146,10 +146,8 @@ class ReportPageComponent extends AuthComponent {
|
||||||
[this.issueContentTypes.REPORT]: strongUsersOnCritIssueReport,
|
[this.issueContentTypes.REPORT]: strongUsersOnCritIssueReport,
|
||||||
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
|
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
|
||||||
},
|
},
|
||||||
'weak_password': {
|
// TODO: Add used_password issue: configured password that were
|
||||||
[this.issueContentTypes.OVERVIEW]: weakPasswordIssueOverview,
|
// successfull exploiting a machine, previously called 'weak_password'
|
||||||
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
|
|
||||||
},
|
|
||||||
'stolen_creds': {
|
'stolen_creds': {
|
||||||
[this.issueContentTypes.OVERVIEW]: stolenCredsIssueOverview,
|
[this.issueContentTypes.OVERVIEW]: stolenCredsIssueOverview,
|
||||||
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
|
[this.issueContentTypes.TYPE]: this.issueTypes.DANGER
|
||||||
|
@ -161,15 +159,32 @@ class ReportPageComponent extends AuthComponent {
|
||||||
this.state = {
|
this.state = {
|
||||||
report: props.report,
|
report: props.report,
|
||||||
graph: {nodes: [], edges: []},
|
graph: {nodes: [], edges: []},
|
||||||
nodeStateList: []
|
nodeStateList: [],
|
||||||
|
stolenCredentials: [],
|
||||||
|
configuredCredentials: [],
|
||||||
|
issues: []
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidMount() {
|
componentDidMount() {
|
||||||
this.getNodeStateListFromServer();
|
this.getNodeStateListFromServer();
|
||||||
|
this.getCredentialsFromServer();
|
||||||
this.updateMapFromServer();
|
this.updateMapFromServer();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getCredentialsFromServer = () => {
|
||||||
|
this.authFetch('/api/propagation-credentials/stolen-credentials')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(creds => {
|
||||||
|
this.setState({stolenCredentials: creds});
|
||||||
|
})
|
||||||
|
this.authFetch('/api/propagation-credentials/configured-credentials')
|
||||||
|
.then(res => res.json())
|
||||||
|
.then(creds => {
|
||||||
|
this.setState({configuredCredentials: creds});
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
getNodeStateListFromServer = () => {
|
getNodeStateListFromServer = () => {
|
||||||
this.authFetch('/api/netmap/node-states')
|
this.authFetch('/api/netmap/node-states')
|
||||||
.then(res => res.json())
|
.then(res => res.json())
|
||||||
|
@ -184,7 +199,8 @@ class ReportPageComponent extends AuthComponent {
|
||||||
|
|
||||||
componentDidUpdate(prevProps) {
|
componentDidUpdate(prevProps) {
|
||||||
if (this.props.report !== prevProps.report) {
|
if (this.props.report !== prevProps.report) {
|
||||||
this.setState({report: this.props.report})
|
this.setState({report: this.props.report});
|
||||||
|
this.addIssuesToOverviewIssues();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -273,39 +289,20 @@ class ReportPageComponent extends AuthComponent {
|
||||||
<p>
|
<p>
|
||||||
The monkeys were run with the following configuration:
|
The monkeys were run with the following configuration:
|
||||||
</p>
|
</p>
|
||||||
{
|
<UsedCredentials stolen={this.state.stolenCredentials} configured={this.state.configuredCredentials}/>
|
||||||
this.state.report.overview.config_users.length > 0 ?
|
|
||||||
<>
|
|
||||||
<p>
|
|
||||||
Usernames used for brute-forcing:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
{this.state.report.overview.config_users.map(x => <li key={x}>{x}</li>)}
|
|
||||||
</ul>
|
|
||||||
<p>
|
|
||||||
Passwords used for brute-forcing:
|
|
||||||
</p>
|
|
||||||
<ul>
|
|
||||||
{this.state.report.overview.config_passwords.map(x => <li key={x}>{x.substr(0, 3) + '******'}</li>)}
|
|
||||||
</ul>
|
|
||||||
</>
|
|
||||||
:
|
|
||||||
<p>
|
|
||||||
Brute forcing uses stolen credentials only. No credentials were supplied during Monkey’s
|
|
||||||
configuration.
|
|
||||||
</p>
|
|
||||||
}
|
|
||||||
{
|
{
|
||||||
this.state.report.overview.config_exploits.length > 0 ?
|
this.state.report.overview.config_exploits.length > 0 ?
|
||||||
<p>
|
(
|
||||||
The Monkey uses the following exploit methods:
|
<p>
|
||||||
<ul>
|
The Monkey attempted the following exploitation methods:
|
||||||
{this.state.report.overview.config_exploits.map(x => <li key={x}>{x}</li>)}
|
<ul>
|
||||||
</ul>
|
{this.state.report.overview.config_exploits.map(x => <li key={x}>{x}</li>)}
|
||||||
</p>
|
</ul>
|
||||||
|
</p>
|
||||||
|
)
|
||||||
:
|
:
|
||||||
<p>
|
<p>
|
||||||
No exploits are used by the Monkey.
|
No exploiters were enabled.
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
@ -379,6 +376,9 @@ class ReportPageComponent extends AuthComponent {
|
||||||
getPotentialSecurityIssuesOverviews() {
|
getPotentialSecurityIssuesOverviews() {
|
||||||
let overviews = [];
|
let overviews = [];
|
||||||
let issues = this.state.report.overview.issues;
|
let issues = this.state.report.overview.issues;
|
||||||
|
let state_issues = this.state.issues;
|
||||||
|
issues.push(...state_issues);
|
||||||
|
issues = [...new Set(issues)];
|
||||||
|
|
||||||
for(let i=0; i < issues.length; i++) {
|
for(let i=0; i < issues.length; i++) {
|
||||||
if (this.isIssuePotentialSecurityIssue(issues[i])) {
|
if (this.isIssuePotentialSecurityIssue(issues[i])) {
|
||||||
|
@ -420,6 +420,10 @@ class ReportPageComponent extends AuthComponent {
|
||||||
getImmediateThreatCount() {
|
getImmediateThreatCount() {
|
||||||
let threatCount = 0;
|
let threatCount = 0;
|
||||||
let issues = this.state.report.overview.issues;
|
let issues = this.state.report.overview.issues;
|
||||||
|
let state_issues = this.state.issues;
|
||||||
|
|
||||||
|
issues.push(...state_issues);
|
||||||
|
issues = [...new Set(issues)];
|
||||||
|
|
||||||
for(let i=0; i < issues.length; i++) {
|
for(let i=0; i < issues.length; i++) {
|
||||||
if(this.isIssueImmediateThreat(issues[i])) {
|
if(this.isIssueImmediateThreat(issues[i])) {
|
||||||
|
@ -439,6 +443,10 @@ class ReportPageComponent extends AuthComponent {
|
||||||
getImmediateThreatsOverviews() {
|
getImmediateThreatsOverviews() {
|
||||||
let overviews = [];
|
let overviews = [];
|
||||||
let issues = this.state.report.overview.issues;
|
let issues = this.state.report.overview.issues;
|
||||||
|
let state_issues = this.state.issues;
|
||||||
|
|
||||||
|
issues.push(...state_issues);
|
||||||
|
issues = [...new Set(issues)];
|
||||||
|
|
||||||
for(let i=0; i < issues.length; i++) {
|
for(let i=0; i < issues.length; i++) {
|
||||||
if (this.isIssueImmediateThreat(issues[i])) {
|
if (this.isIssueImmediateThreat(issues[i])) {
|
||||||
|
@ -535,7 +543,10 @@ class ReportPageComponent extends AuthComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{marginBottom: '20px'}}>
|
<div style={{marginBottom: '20px'}}>
|
||||||
<StolenPasswords data={this.state.report.glance.stolen_creds}/>
|
<StolenPasswords
|
||||||
|
data={this.state.stolenCredentials}
|
||||||
|
format={true}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<StrongUsers data={this.state.report.glance.strong_users}/>
|
<StrongUsers data={this.state.report.glance.strong_users}/>
|
||||||
|
@ -582,6 +593,23 @@ class ReportPageComponent extends AuthComponent {
|
||||||
}
|
}
|
||||||
return <ul>{issuesDivArray}</ul>;
|
return <ul>{issuesDivArray}</ul>;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
addIssuesToOverviewIssues() {
|
||||||
|
let overview_issues = this.state.issues;
|
||||||
|
|
||||||
|
if (this.shouldAddStolenCredentialsIssue()) {
|
||||||
|
overview_issues.push('stolen_creds');
|
||||||
|
}
|
||||||
|
this.setState({
|
||||||
|
issues: overview_issues
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldAddStolenCredentialsIssue() {
|
||||||
|
// TODO: This should check if any stolen credentials are used to
|
||||||
|
// exploit a machine
|
||||||
|
return ( this.state.stolenCredentials.length > 0 )
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default ReportPageComponent;
|
export default ReportPageComponent;
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
export function getAllUsernames(stolen, configured){
|
||||||
|
let usernames = [];
|
||||||
|
usernames.push(...getCredentialsUsernames(stolen));
|
||||||
|
usernames.push(...getCredentialsUsernames(configured));
|
||||||
|
return usernames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCredentialsUsernames(credentials) {
|
||||||
|
let usernames = [];
|
||||||
|
for(let i = 0; i < credentials.length; i++){
|
||||||
|
usernames.push(credentials[i]['identity']['username']);
|
||||||
|
}
|
||||||
|
return usernames;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAllSecrets(stolen, configured){
|
||||||
|
let secrets = [];
|
||||||
|
for(let i = 0; i < stolen.length; i++){
|
||||||
|
secrets.push(getSecretsFromCredential(stolen[i]['secret']));
|
||||||
|
}
|
||||||
|
for(let i = 0; i < configured.length; i++){
|
||||||
|
secrets.push(getSecretsFromCredential(configured[i]['secret']));
|
||||||
|
}
|
||||||
|
return secrets;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSecretsFromCredential(credential) {
|
||||||
|
if(credential['credential_type'] === 'SSH_KEYPAIR'){
|
||||||
|
return {'type': 'SSH keypair', 'content': credential['private_key']}
|
||||||
|
}
|
||||||
|
if(credential['credential_type'] === 'NT_HASH'){
|
||||||
|
return {'type': 'NT hash', 'content': credential['nt_hash']}
|
||||||
|
}
|
||||||
|
if(credential['credential_type'] === 'LM_HASH'){
|
||||||
|
return {'type': 'LM hash', 'content': credential['lm_hash']}
|
||||||
|
}
|
||||||
|
if(credential['credential_type'] === 'PASSWORD'){
|
||||||
|
return {'type': 'Password', 'content': credential['password']}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getCredentialsTableData(credentials) {
|
||||||
|
|
||||||
|
let table_data = [];
|
||||||
|
|
||||||
|
let identites = getCredentialsUsernames(credentials);
|
||||||
|
let secrets = getAllSecrets(credentials, [])
|
||||||
|
|
||||||
|
for(let i=0; i<credentials.length; i++) {
|
||||||
|
let row_data = {};
|
||||||
|
row_data['username'] = identites[i];
|
||||||
|
row_data['type'] = secrets[i]['type'];
|
||||||
|
table_data.push(row_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
return table_data;
|
||||||
|
}
|
|
@ -1,13 +1,13 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactTable from 'react-table'
|
import ReactTable from 'react-table'
|
||||||
|
import {getCredentialsTableData} from '../credentialParsing.js';
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
Header: 'Stolen Credentials',
|
Header: 'Stolen Credentials',
|
||||||
columns: [
|
columns: [
|
||||||
{Header: 'Username', accessor: 'username'},
|
{Header: 'Username', accessor: 'username'},
|
||||||
{Header: 'Type', accessor: 'type'},
|
{Header: 'Type', accessor: 'type'}
|
||||||
{Header: 'Stolen From', accessor: 'origin'}
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
@ -22,11 +22,18 @@ class StolenPasswordsComponent extends React.Component {
|
||||||
render() {
|
render() {
|
||||||
let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length;
|
let defaultPageSize = this.props.data.length > pageSize ? pageSize : this.props.data.length;
|
||||||
let showPagination = this.props.data.length > pageSize;
|
let showPagination = this.props.data.length > pageSize;
|
||||||
|
let table_data = this.props.data;
|
||||||
|
if(this.props.format) {
|
||||||
|
// Note: This formatting is needed because StolenPasswords
|
||||||
|
// is used in Security and Attack report with different data
|
||||||
|
table_data = getCredentialsTableData(this.props.data);
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="data-table-container">
|
<div className="data-table-container">
|
||||||
<ReactTable
|
<ReactTable
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={this.props.data}
|
data={table_data}
|
||||||
showPagination={showPagination}
|
showPagination={showPagination}
|
||||||
defaultPageSize={defaultPageSize}
|
defaultPageSize={defaultPageSize}
|
||||||
/>
|
/>
|
||||||
|
|
|
@ -0,0 +1,40 @@
|
||||||
|
import {getAllUsernames, getAllSecrets} from '../credentialParsing';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
class UsedCredentials extends React.Component {
|
||||||
|
constructor(props) {
|
||||||
|
super(props);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
render() {
|
||||||
|
let allUsernames = getAllUsernames(this.props.stolen, this.props.configured);
|
||||||
|
let allSecrets = getAllSecrets(this.props.stolen, this.props.configured);
|
||||||
|
return (
|
||||||
|
allUsernames.length > 0 ?
|
||||||
|
<>
|
||||||
|
<p>
|
||||||
|
Usernames used for brute-forcing:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{allUsernames.map(x => <li key={x}>{x}</li>)}
|
||||||
|
</ul>
|
||||||
|
<p>
|
||||||
|
Credentials used for brute-forcing:
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
{allSecrets.map((x, index) => <li
|
||||||
|
key={index}>{x['type']}: {x['content'].substr(0, 3) + '******'}</li>)}
|
||||||
|
</ul>
|
||||||
|
</>
|
||||||
|
:
|
||||||
|
<p>
|
||||||
|
No credentials were used.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export default UsedCredentials;
|
|
@ -1,6 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
|
|
||||||
export function weakPasswordIssueOverview() {
|
|
||||||
return (<li>Machines are accessible using passwords supplied by the user during the Monkey’s
|
|
||||||
configuration.</li>)
|
|
||||||
}
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
from common.credentials import (
|
||||||
|
CredentialComponentType,
|
||||||
|
Credentials,
|
||||||
|
LMHash,
|
||||||
|
NTHash,
|
||||||
|
Password,
|
||||||
|
SSHKeypair,
|
||||||
|
Username,
|
||||||
|
)
|
||||||
|
from monkey_island.cc.services.reporting import format_creds_for_reporting
|
||||||
|
|
||||||
|
monkey_hostname = "fake_hostname"
|
||||||
|
fake_monkey_guid = "abc"
|
||||||
|
|
||||||
|
fake_username = Username("m0nk3y_user")
|
||||||
|
fake_nt_hash = NTHash("AEBD4DE384C7EC43AAD3B435B51404EE")
|
||||||
|
fake_lm_hash = LMHash("7A21990FCD3D759941E45C490F143D5F")
|
||||||
|
fake_password = Password("trytostealthis")
|
||||||
|
fake_ssh_public_key = "RSA_public_key"
|
||||||
|
fake_ssh_private_key = "RSA_private_key"
|
||||||
|
fake_ssh_key = SSHKeypair(fake_ssh_private_key, fake_ssh_public_key)
|
||||||
|
|
||||||
|
identities = (fake_username,)
|
||||||
|
secrets = (fake_nt_hash, fake_lm_hash, fake_password, fake_ssh_key)
|
||||||
|
|
||||||
|
fake_credentials = [
|
||||||
|
Credentials(fake_username, fake_nt_hash),
|
||||||
|
Credentials(fake_username, fake_lm_hash),
|
||||||
|
Credentials(fake_username, fake_password),
|
||||||
|
Credentials(fake_username, fake_ssh_key),
|
||||||
|
Credentials(None, fake_ssh_key),
|
||||||
|
Credentials(fake_username, None),
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_formatting_credentials_for_report():
|
||||||
|
|
||||||
|
credentials = format_creds_for_reporting(fake_credentials)
|
||||||
|
|
||||||
|
result1 = {
|
||||||
|
"_type": CredentialComponentType.NT_HASH.name,
|
||||||
|
"type": "NTLM hash",
|
||||||
|
"username": fake_username.username,
|
||||||
|
}
|
||||||
|
result2 = {
|
||||||
|
"_type": CredentialComponentType.LM_HASH.name,
|
||||||
|
"type": "LM hash",
|
||||||
|
"username": fake_username.username,
|
||||||
|
}
|
||||||
|
result3 = {
|
||||||
|
"_type": CredentialComponentType.PASSWORD.name,
|
||||||
|
"type": "Clear Password",
|
||||||
|
"username": fake_username.username,
|
||||||
|
}
|
||||||
|
result4 = {
|
||||||
|
"_type": CredentialComponentType.SSH_KEYPAIR.name,
|
||||||
|
"type": "Clear SSH private key",
|
||||||
|
"username": fake_username.username,
|
||||||
|
}
|
||||||
|
result5 = {
|
||||||
|
"_type": CredentialComponentType.SSH_KEYPAIR.name,
|
||||||
|
"type": "Clear SSH private key",
|
||||||
|
"username": "",
|
||||||
|
}
|
||||||
|
assert result1 in credentials
|
||||||
|
assert result2 in credentials
|
||||||
|
assert result3 in credentials
|
||||||
|
assert result4 in credentials
|
||||||
|
assert result5 in credentials
|
|
@ -1,97 +0,0 @@
|
||||||
import pytest
|
|
||||||
|
|
||||||
from common.credentials import CredentialComponentType
|
|
||||||
from monkey_island.cc.models import Monkey, StolenCredentials
|
|
||||||
from monkey_island.cc.services.reporting.stolen_credentials import (
|
|
||||||
extract_ssh_keys,
|
|
||||||
get_stolen_creds,
|
|
||||||
)
|
|
||||||
|
|
||||||
monkey_hostname = "fake_hostname"
|
|
||||||
fake_monkey_guid = "abc"
|
|
||||||
|
|
||||||
fake_username = "m0nk3y_user"
|
|
||||||
fake_nt_hash = "c1c58f96cdf212b50837bc11a00be47c"
|
|
||||||
fake_lm_hash = "299BD128C1101FD6"
|
|
||||||
fake_password = "trytostealthis"
|
|
||||||
fake_ssh_key = "RSA_fake_key"
|
|
||||||
fake_credentials = {
|
|
||||||
"identities": [{"username": fake_username, "credential_type": "USERNAME"}],
|
|
||||||
"secrets": [
|
|
||||||
CredentialComponentType.NT_HASH.name,
|
|
||||||
CredentialComponentType.LM_HASH.name,
|
|
||||||
CredentialComponentType.PASSWORD.name,
|
|
||||||
CredentialComponentType.SSH_KEYPAIR.name,
|
|
||||||
],
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture
|
|
||||||
def fake_monkey():
|
|
||||||
monkey = Monkey()
|
|
||||||
monkey.guid = fake_monkey_guid
|
|
||||||
monkey.hostname = monkey_hostname
|
|
||||||
monkey.save()
|
|
||||||
return monkey.id
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("uses_database")
|
|
||||||
def test_get_credentials(fake_monkey):
|
|
||||||
StolenCredentials(
|
|
||||||
identities=fake_credentials["identities"],
|
|
||||||
secrets=fake_credentials["secrets"],
|
|
||||||
monkey=fake_monkey,
|
|
||||||
).save()
|
|
||||||
|
|
||||||
credentials = get_stolen_creds()
|
|
||||||
|
|
||||||
result1 = {
|
|
||||||
"origin": monkey_hostname,
|
|
||||||
"_type": CredentialComponentType.NT_HASH.name,
|
|
||||||
"type": "NTLM hash",
|
|
||||||
"username": fake_username,
|
|
||||||
}
|
|
||||||
result2 = {
|
|
||||||
"origin": monkey_hostname,
|
|
||||||
"_type": CredentialComponentType.LM_HASH.name,
|
|
||||||
"type": "LM hash",
|
|
||||||
"username": fake_username,
|
|
||||||
}
|
|
||||||
result3 = {
|
|
||||||
"origin": monkey_hostname,
|
|
||||||
"_type": CredentialComponentType.PASSWORD.name,
|
|
||||||
"type": "Clear Password",
|
|
||||||
"username": fake_username,
|
|
||||||
}
|
|
||||||
result4 = {
|
|
||||||
"origin": monkey_hostname,
|
|
||||||
"_type": CredentialComponentType.SSH_KEYPAIR.name,
|
|
||||||
"type": "Clear SSH private key",
|
|
||||||
"username": fake_username,
|
|
||||||
}
|
|
||||||
assert result1 in credentials
|
|
||||||
assert result2 in credentials
|
|
||||||
assert result3 in credentials
|
|
||||||
assert result4 in credentials
|
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.usefixtures("uses_database")
|
|
||||||
def test_extract_ssh_keys(fake_monkey):
|
|
||||||
StolenCredentials(
|
|
||||||
identities=fake_credentials["identities"],
|
|
||||||
secrets=fake_credentials["secrets"],
|
|
||||||
monkey=fake_monkey,
|
|
||||||
).save()
|
|
||||||
|
|
||||||
credentials = get_stolen_creds()
|
|
||||||
keys = extract_ssh_keys(credentials)
|
|
||||||
|
|
||||||
assert len(keys) == 1
|
|
||||||
|
|
||||||
result = {
|
|
||||||
"origin": monkey_hostname,
|
|
||||||
"_type": CredentialComponentType.SSH_KEYPAIR.name,
|
|
||||||
"type": "Clear SSH private key",
|
|
||||||
"username": fake_username,
|
|
||||||
}
|
|
||||||
assert result in keys
|
|
Loading…
Reference in New Issue