forked from p15670423/monkey
This commit isn't final. I want to reorganise the code structure a bit,
to make it prettier and readable, also to add docs. Still need to update the report's text.
This commit is contained in:
parent
9a05d0e87d
commit
822e54f373
|
@ -197,11 +197,19 @@ class Telemetry(flask_restful.Resource):
|
||||||
Telemetry.create_group_user_connection(info_for_mongo, group_user_dict)
|
Telemetry.create_group_user_connection(info_for_mongo, group_user_dict)
|
||||||
for entity in info_for_mongo.values():
|
for entity in info_for_mongo.values():
|
||||||
if entity['machine_id']:
|
if entity['machine_id']:
|
||||||
|
# Handling for local entities.
|
||||||
mongo.db.groupsandusers.update({'SID': entity['SID'],
|
mongo.db.groupsandusers.update({'SID': entity['SID'],
|
||||||
'machine_id': entity['machine_id']}, entity, upsert=True)
|
'machine_id': entity['machine_id']}, entity, upsert=True)
|
||||||
else:
|
else:
|
||||||
|
# Handlings for domain entities.
|
||||||
if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}):
|
if not mongo.db.groupsandusers.find_one({'SID': entity['SID']}):
|
||||||
mongo.db.groupsandusers.insert_one(entity)
|
mongo.db.groupsandusers.insert_one(entity)
|
||||||
|
else:
|
||||||
|
# if entity is domain entity, add the monkey id of current machine to secrets_location.
|
||||||
|
# (found on this machine)
|
||||||
|
if entity.get('NTLM_secret'):
|
||||||
|
mongo.db.groupsandusers.update_one({'SID': entity['SID'], 'type': 1},
|
||||||
|
{'$addToSet': {'secret_location': monkey_id}})
|
||||||
|
|
||||||
Telemetry.add_admin(info_for_mongo[group_info.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id)
|
Telemetry.add_admin(info_for_mongo[group_info.ADMINISTRATORS_GROUP_KNOWN_SID], monkey_id)
|
||||||
Telemetry.update_admins_retrospective(info_for_mongo)
|
Telemetry.update_admins_retrospective(info_for_mongo)
|
||||||
|
@ -209,6 +217,8 @@ class Telemetry(flask_restful.Resource):
|
||||||
telemetry_json['data']['wmi']['Win32_Product'],
|
telemetry_json['data']['wmi']['Win32_Product'],
|
||||||
monkey_id)
|
monkey_id)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def update_critical_services(wmi_services, wmi_products, machine_id):
|
def update_critical_services(wmi_services, wmi_products, machine_id):
|
||||||
critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL')
|
critical_names = ("W3svc", "MSExchangeServiceHost", "MSSQLServer", "dns", 'MSSQL$SQLEXPRESS', 'SQL')
|
||||||
|
|
|
@ -97,6 +97,11 @@ class NodeService:
|
||||||
def get_monkey_label_by_id(monkey_id):
|
def get_monkey_label_by_id(monkey_id):
|
||||||
return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id))
|
return NodeService.get_monkey_label(NodeService.get_monkey_by_id(monkey_id))
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def get_monkey_critical_services(monkey_id):
|
||||||
|
critical_services = mongo.db.monkey.find_one({'_id': monkey_id}, {'critical_services': 1}).get('critical_services', [])
|
||||||
|
return critical_services
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_monkey_label(monkey):
|
def get_monkey_label(monkey):
|
||||||
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
|
label = monkey["hostname"] + " : " + monkey["ip_addresses"][0]
|
||||||
|
|
|
@ -1,3 +1,7 @@
|
||||||
|
import uuid
|
||||||
|
from itertools import combinations, product
|
||||||
|
|
||||||
|
from cc.services.edge import EdgeService
|
||||||
from cc.services.pth_report_utils import PassTheHashReport, Machine
|
from cc.services.pth_report_utils import PassTheHashReport, Machine
|
||||||
from cc.database import mongo
|
from cc.database import mongo
|
||||||
from bson import ObjectId
|
from bson import ObjectId
|
||||||
|
@ -49,7 +53,6 @@ class PTHReportService(object):
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_duplicated_passwords_issues():
|
def get_duplicated_passwords_issues():
|
||||||
# TODO: Fix bug if both local and non local account share the same password
|
|
||||||
user_groups = PTHReportService.get_duplicated_passwords_nodes()
|
user_groups = PTHReportService.get_duplicated_passwords_nodes()
|
||||||
issues = []
|
issues = []
|
||||||
users_gathered = []
|
users_gathered = []
|
||||||
|
@ -98,195 +101,130 @@ class PTHReportService(object):
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def old_get_shared_local_admins_nodes(pth):
|
def get_strong_users_on_critical_machines_nodes():
|
||||||
dups = dict(map(lambda x: (x, len(pth.GetSharedAdmins(x))), pth.machines))
|
crit_machines = {}
|
||||||
shared_admin_machines = []
|
pipeline = [
|
||||||
for m, count in sorted(dups.iteritems(), key=lambda (k, v): (v, k), reverse=True):
|
{
|
||||||
if count <= 0:
|
'$unwind': '$admin_on_machines'
|
||||||
continue
|
},
|
||||||
shared_admin_account_list = []
|
{
|
||||||
|
'$match': {'type': 1, 'domain_name': {'$ne': None}}
|
||||||
for sid in pth.GetSharedAdmins(m):
|
},
|
||||||
shared_admin_account_list.append(pth.GetUsernameBySid(sid))
|
{
|
||||||
|
'$lookup':
|
||||||
machine = {
|
{
|
||||||
'ip': m.GetIp(),
|
'from': 'monkey',
|
||||||
'hostname': m.GetHostName(),
|
'localField': 'admin_on_machines',
|
||||||
'domain': m.GetDomainName(),
|
'foreignField': '_id',
|
||||||
'services_names': m.GetCriticalServicesInstalled(),
|
'as': 'critical_machine'
|
||||||
'user_count': count,
|
|
||||||
'admins_accounts': shared_admin_account_list
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
shared_admin_machines.append(machine)
|
{
|
||||||
|
'$match': {'critical_machine.critical_services': {'$ne': []}}
|
||||||
return shared_admin_machines
|
},
|
||||||
|
{
|
||||||
@staticmethod
|
'$unwind': '$critical_machine'
|
||||||
def get_strong_users_on_crit_services_by_machine(pth):
|
|
||||||
threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetCritialServers()))
|
|
||||||
strong_users_crit_list = []
|
|
||||||
|
|
||||||
for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True):
|
|
||||||
if count <= 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
threatening_users_attackers_dict = {}
|
|
||||||
for sid in pth.GetThreateningUsersByVictim(m):
|
|
||||||
username = pth.GetUsernameBySid(sid)
|
|
||||||
threatening_users_attackers_dict[username] = []
|
|
||||||
for mm in pth.GetAttackersBySid(sid):
|
|
||||||
if m == mm:
|
|
||||||
continue
|
|
||||||
threatening_users_attackers_dict[username] = mm.GetIp()
|
|
||||||
|
|
||||||
machine = {
|
|
||||||
'ip': m.GetIp(),
|
|
||||||
'hostname': m.GetHostName(),
|
|
||||||
'domain': m.GetDomainName(),
|
|
||||||
'services': m.GetCriticalServicesInstalled(),
|
|
||||||
'threatening_users': threatening_users_attackers_dict
|
|
||||||
}
|
}
|
||||||
strong_users_crit_list.append(machine)
|
]
|
||||||
return strong_users_crit_list
|
docs = mongo.db.groupsandusers.aggregate(pipeline)
|
||||||
|
for doc in docs:
|
||||||
|
hostname = str(doc['critical_machine']['hostname'])
|
||||||
|
if not hostname in crit_machines:
|
||||||
|
crit_machines[hostname] = {}
|
||||||
|
crit_machines[hostname]['threatening_users'] = []
|
||||||
|
crit_machines[hostname]['critical_services'] = doc['critical_machine']['critical_services']
|
||||||
|
crit_machines[hostname]['threatening_users'].append(
|
||||||
|
{'name': str(doc['domain_name']) + '\\' + str(doc['name']),
|
||||||
|
'creds_location': doc['secret_location']})
|
||||||
|
return crit_machines
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_strong_users_on_crit_services_by_user(pth):
|
def get_strong_users_on_crit_issues():
|
||||||
critical_servers = pth.GetCritialServers()
|
|
||||||
strong_users_dict = {}
|
|
||||||
|
|
||||||
for server in critical_servers:
|
|
||||||
users = pth.GetThreateningUsersByVictim(server)
|
|
||||||
for sid in users:
|
|
||||||
username = pth.GetUsernameBySid(sid)
|
|
||||||
if username not in strong_users_dict:
|
|
||||||
strong_users_dict[username] = {
|
|
||||||
'services_names': [],
|
|
||||||
'machines': []
|
|
||||||
}
|
|
||||||
strong_users_dict[username]['username'] = username
|
|
||||||
strong_users_dict[username]['domain'] = server.GetDomainName()
|
|
||||||
strong_users_dict[username]['services_names'] += server.GetCriticalServicesInstalled()
|
|
||||||
strong_users_dict[username]['machines'].append(server.GetHostName())
|
|
||||||
|
|
||||||
return strong_users_dict.values()
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get_strong_users_on_non_crit_services(pth):
|
|
||||||
threatening = dict(map(lambda x: (x, len(pth.GetThreateningUsersByVictim(x))), pth.GetNonCritialServers()))
|
|
||||||
|
|
||||||
strong_users_non_crit_list = []
|
|
||||||
|
|
||||||
for m, count in sorted(threatening.iteritems(), key=lambda (k, v): (v, k), reverse=True):
|
|
||||||
if count <= 0:
|
|
||||||
continue
|
|
||||||
|
|
||||||
threatening_users_attackers_dict = {}
|
|
||||||
for sid in pth.GetThreateningUsersByVictim(m):
|
|
||||||
username = pth.GetUsernameBySid(sid)
|
|
||||||
threatening_users_attackers_dict[username] = []
|
|
||||||
for mm in pth.GetAttackersBySid(sid):
|
|
||||||
if m == mm:
|
|
||||||
continue
|
|
||||||
threatening_users_attackers_dict[username] = mm.GetIp()
|
|
||||||
|
|
||||||
machine = {
|
|
||||||
'ip': m.GetIp(),
|
|
||||||
'hostname': m.GetHostName(),
|
|
||||||
'domain': m.GetDomainName(),
|
|
||||||
'services_names': [],
|
|
||||||
'user_count': count,
|
|
||||||
'threatening_users': threatening_users_attackers_dict
|
|
||||||
}
|
|
||||||
strong_users_non_crit_list.append(machine)
|
|
||||||
return strong_users_non_crit_list
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def strong_users_on_crit_issues(strong_users):
|
|
||||||
issues = []
|
issues = []
|
||||||
for machine in strong_users:
|
crit_machines = PTHReportService.get_strong_users_on_critical_machines_nodes()
|
||||||
|
for machine in crit_machines:
|
||||||
issues.append(
|
issues.append(
|
||||||
{
|
{
|
||||||
'type': 'strong_users_on_crit',
|
'type': 'strong_users_on_crit',
|
||||||
'machine': machine.get('hostname'),
|
'machine': machine,
|
||||||
'services': machine.get('services'),
|
'services': crit_machines[machine].get('critical_services'),
|
||||||
'ip': machine.get('ip'),
|
'threatening_users': [i['name'] for i in crit_machines[machine]['threatening_users']]
|
||||||
'threatening_users': machine.get('threatening_users').keys()
|
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
return issues
|
return issues
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_machine_details(node_id):
|
def generate_map_nodes():
|
||||||
machine = Machine(node_id)
|
|
||||||
node = {}
|
|
||||||
if machine.latest_system_info:
|
|
||||||
node = {
|
|
||||||
"id": str(node_id),
|
|
||||||
"label": '{0} : {1}'.format(machine.GetHostName(), machine.GetIp()),
|
|
||||||
'group': 'critical' if machine.IsCriticalServer() else 'normal',
|
|
||||||
'users': list(machine.GetCachedUsernames()),
|
|
||||||
'ips': [machine.GetIp()],
|
|
||||||
'services': machine.GetCriticalServicesInstalled(),
|
|
||||||
'hostname': machine.GetHostName()
|
|
||||||
}
|
|
||||||
return node
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def generate_map_nodes(pth):
|
|
||||||
nodes_list = []
|
nodes_list = []
|
||||||
for node_id in pth.vertices:
|
monkeys = mongo.db.monkey.find({}, {'_id': 1, 'hostname': 1, 'critical_services': 1, 'ip_addresses': 1})
|
||||||
node = PTHReportService.get_machine_details(node_id)
|
for monkey in monkeys:
|
||||||
nodes_list.append(node)
|
critical_services = monkey.get('critical_services', [])
|
||||||
|
nodes_list.append({
|
||||||
|
'id': monkey['_id'],
|
||||||
|
'label': '{0} : {1}'.format(monkey['hostname'], monkey['ip_addresses'][0]),
|
||||||
|
'group': 'critical' if critical_services else 'normal',
|
||||||
|
'services': critical_services,
|
||||||
|
'hostname': monkey['hostname']
|
||||||
|
})
|
||||||
|
|
||||||
return nodes_list
|
return nodes_list
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_issues_list(issues):
|
def generate_edge_nodes():
|
||||||
issues_dict = {}
|
edges_list = []
|
||||||
|
pipeline = [
|
||||||
|
{
|
||||||
|
'$match': {'admin_on_machines': {'$ne': []}, 'secret_location': {'$ne': []}, 'type': 1}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'$project': {'admin_on_machines': 1, 'secret_location': 1}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
comp_users = mongo.db.groupsandusers.aggregate(pipeline)
|
||||||
|
|
||||||
for issue in issues:
|
for user in comp_users:
|
||||||
machine = issue['machine']
|
pairs = PTHReportService.generate_edges_tuples(user['admin_on_machines'], user['secret_location'])
|
||||||
if machine not in issues_dict:
|
for pair in pairs:
|
||||||
issues_dict[machine] = []
|
edges_list.append(
|
||||||
issues_dict[machine].append(issue)
|
{
|
||||||
|
'from': pair[0],
|
||||||
|
'to': pair[1],
|
||||||
|
'id': str(uuid.uuid4())
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return edges_list
|
||||||
|
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def generate_edges_tuples(*lists):
|
||||||
|
|
||||||
|
for t in combinations(lists, 2):
|
||||||
|
for pair in product(*t):
|
||||||
|
# Don't output pairs containing duplicated elements
|
||||||
|
if pair[0] != pair[1]:
|
||||||
|
yield pair
|
||||||
|
|
||||||
return issues_dict
|
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_report():
|
def get_report():
|
||||||
|
|
||||||
|
PTHReportService.get_strong_users_on_critical_machines_nodes()
|
||||||
|
|
||||||
issues = []
|
issues = []
|
||||||
pth = PassTheHashReport()
|
|
||||||
|
|
||||||
strong_users_on_crit_services = PTHReportService.get_strong_users_on_crit_services_by_user(pth)
|
|
||||||
strong_users_on_non_crit_services = PTHReportService.get_strong_users_on_non_crit_services(pth)
|
|
||||||
|
|
||||||
issues += PTHReportService.get_duplicated_passwords_issues()
|
|
||||||
# issues += PTHReportService.get_shared_local_admins_issues(local_admin_shared)
|
|
||||||
# issues += PTHReportService.strong_users_on_crit_issues(
|
|
||||||
# PTHReportService.get_strong_users_on_crit_services_by_machine(pth))
|
|
||||||
|
|
||||||
report = \
|
report = \
|
||||||
{
|
{
|
||||||
'report_info':
|
'report_info':
|
||||||
{
|
{
|
||||||
'strong_users_on_crit_services': strong_users_on_crit_services,
|
|
||||||
'strong_users_on_non_crit_services': strong_users_on_non_crit_services,
|
|
||||||
'pth_issues': issues
|
'pth_issues': issues
|
||||||
},
|
},
|
||||||
'pthmap':
|
'pthmap':
|
||||||
{
|
{
|
||||||
'nodes': PTHReportService.generate_map_nodes(pth),
|
'nodes': PTHReportService.generate_map_nodes(),
|
||||||
'edges': pth.edges
|
'edges': PTHReportService.generate_edge_nodes()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return report
|
return report
|
|
@ -604,6 +604,10 @@ class PassTheHashReport(object):
|
||||||
RIGHT_ARROW = u"\u2192"
|
RIGHT_ARROW = u"\u2192"
|
||||||
return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label)
|
return "%s %s %s" % (attacker_label, RIGHT_ARROW, victim_label)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_edges_by_sid(self):
|
def get_edges_by_sid(self):
|
||||||
edges_list = []
|
edges_list = []
|
||||||
|
|
||||||
|
|
|
@ -118,10 +118,7 @@ class ReportService:
|
||||||
[NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \
|
[NodeService.get_displayed_node_by_id(node['_id'], True) for node in mongo.db.node.find({}, {'_id': 1})] \
|
||||||
+ [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
|
+ [NodeService.get_displayed_node_by_id(monkey['_id'], True) for monkey in
|
||||||
mongo.db.monkey.find({}, {'_id': 1})]
|
mongo.db.monkey.find({}, {'_id': 1})]
|
||||||
|
|
||||||
for node in nodes:
|
for node in nodes:
|
||||||
pth_services = PTHReportService.get_machine_details(NodeService.get_monkey_by_id(node['id'])
|
|
||||||
.get('guid', None)).get('services', None)
|
|
||||||
formatted_nodes.append(
|
formatted_nodes.append(
|
||||||
{
|
{
|
||||||
'label': node['label'],
|
'label': node['label'],
|
||||||
|
@ -130,7 +127,7 @@ class ReportService:
|
||||||
(x['hostname'] for x in
|
(x['hostname'] for x in
|
||||||
(NodeService.get_displayed_node_by_id(edge['from'], True)
|
(NodeService.get_displayed_node_by_id(edge['from'], True)
|
||||||
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))),
|
for edge in EdgeService.get_displayed_edges_by_to(node['id'], True))),
|
||||||
'services': node['services'] + pth_services if pth_services else []
|
'services': node['services']
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info('Scanned nodes generated for reporting')
|
logger.info('Scanned nodes generated for reporting')
|
||||||
|
@ -561,7 +558,8 @@ class ReportService:
|
||||||
ReportService.get_tunnels,
|
ReportService.get_tunnels,
|
||||||
ReportService.get_island_cross_segment_issues,
|
ReportService.get_island_cross_segment_issues,
|
||||||
ReportService.get_azure_issues,
|
ReportService.get_azure_issues,
|
||||||
PTHReportService.get_duplicated_passwords_issues
|
PTHReportService.get_duplicated_passwords_issues,
|
||||||
|
PTHReportService.get_strong_users_on_crit_issues
|
||||||
]
|
]
|
||||||
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
|
issues = functools.reduce(lambda acc, issue_gen: acc + issue_gen(), ISSUE_GENERATORS, [])
|
||||||
|
|
||||||
|
@ -720,7 +718,6 @@ class ReportService:
|
||||||
'pth':
|
'pth':
|
||||||
{
|
{
|
||||||
'map': pth_report.get('pthmap'),
|
'map': pth_report.get('pthmap'),
|
||||||
'info': pth_report.get('report_info')
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -410,18 +410,19 @@ class ReportPageComponent extends AuthComponent {
|
||||||
generateReportRecommendationsSection() {
|
generateReportRecommendationsSection() {
|
||||||
return (
|
return (
|
||||||
<div id="recommendations">
|
<div id="recommendations">
|
||||||
<h3>
|
|
||||||
Recommendations
|
|
||||||
</h3>
|
|
||||||
<div>
|
|
||||||
{this.generateIssues(this.state.report.recommendations.issues)}
|
|
||||||
</div>
|
|
||||||
<h3>
|
<h3>
|
||||||
Domain related recommendations
|
Domain related recommendations
|
||||||
</h3>
|
</h3>
|
||||||
<div>
|
<div>
|
||||||
{this.generateIssues(this.state.report.recommendations.domain_issues)}
|
{this.generateIssues(this.state.report.recommendations.domain_issues)}
|
||||||
</div>
|
</div>
|
||||||
|
<h3>
|
||||||
|
Machine related Recommendations
|
||||||
|
</h3>
|
||||||
|
<div>
|
||||||
|
{this.generateIssues(this.state.report.recommendations.issues)}
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -474,9 +475,6 @@ class ReportPageComponent extends AuthComponent {
|
||||||
<div style={{marginBottom: '20px'}}>
|
<div style={{marginBottom: '20px'}}>
|
||||||
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
|
<StolenPasswords data={this.state.report.glance.stolen_creds.concat(this.state.report.glance.ssh_keys)}/>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
|
||||||
<StrongUsers data = {this.state.pthreport.strong_users_on_crit_services} />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -487,6 +485,9 @@ class ReportPageComponent extends AuthComponent {
|
||||||
<h3>
|
<h3>
|
||||||
Credential Map
|
Credential Map
|
||||||
</h3>
|
</h3>
|
||||||
|
<p>
|
||||||
|
This map visualizes possible attack paths through the network using credential compromise. Paths represent lateral movement opportunities by attackers.
|
||||||
|
</p>
|
||||||
<div>
|
<div>
|
||||||
<PassTheHashMapPageComponent graph={this.state.pthmap} />
|
<PassTheHashMapPageComponent graph={this.state.pthmap} />
|
||||||
</div>
|
</div>
|
||||||
|
|
Loading…
Reference in New Issue