Skip to content

Commit 1b1282c

Browse files
authored
Add GRR approval function (#933)
* Add GRR approval function * Add call to GRRFlowCollector * verify access to client in SetUp * Add more verbose download notification messages * Fix test * Update other tests * Linter fix
1 parent a6b44c6 commit 1b1282c

File tree

2 files changed

+64
-6
lines changed

2 files changed

+64
-6
lines changed

dftimewolf/lib/collectors/grr_hosts.py

Lines changed: 38 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,37 @@ def _FindClients(self, selectors: List[str]) -> List[Client]:
214214
clients.append(client)
215215
return clients
216216

217+
def VerifyClientAccess(self, client: Client) -> None:
218+
"""Verifies and requests access to a GRR client.
219+
220+
This call will block until the approval is granted.
221+
222+
Args:
223+
client: GRR client object to verify access to.
224+
"""
225+
client_fqdn = client.data.knowledge_base.fqdn
226+
227+
try:
228+
client.VerifyAccess()
229+
self.logger.info(f"Access to {client_fqdn} granted")
230+
return
231+
except grr_errors.AccessForbiddenError:
232+
self.logger.warning(f"No access to {client_fqdn}, requesting...")
233+
234+
approval = client.CreateApproval(
235+
reason=self.reason,
236+
notified_users=self.approvers,
237+
expiration_duration_days=30,
238+
)
239+
240+
approval_url = (
241+
f"{self.grr_url}/v2/clients/{approval.client_id}/users/"
242+
f"{approval.username}/approvals/{approval.approval_id}"
243+
)
244+
self.PublishMessage(f"Approval URL: {approval_url}")
245+
approval.WaitUntilValid()
246+
self.logger.info(f"Access to {client_fqdn} granted")
247+
217248
# TODO: change object to more specific GRR type information.
218249
def _LaunchFlow(self, client: Client, name: str, args: str) -> str:
219250
"""Creates the specified flow.
@@ -499,18 +530,19 @@ def _DownloadFiles(self, client: Client, flow_id: str) -> Optional[str]:
499530

500531
flow_name = flow_handle.data.name
501532
if flow_name == 'TimelineFlow':
502-
self.logger.debug('Downloading timeline from GRR')
533+
self.logger.info('Downloading timeline from GRR')
503534
self._DownloadTimeline(client, flow_handle, flow_output_dir)
504535
return flow_output_dir
505536

506537
if flow_name == 'OsqueryFlow':
507-
self.logger.debug('Downloading osquery results from GRR')
538+
self.logger.info('Downloading osquery results from GRR')
508539
self._DownloadOsquery(client, flow_id, flow_output_dir)
509540
return flow_output_dir
510541

511542
payloads = []
512543
for r in flow_handle.ListResults():
513544
payloads.append(r.payload)
545+
self.logger.info('Downloading data blobs from GRR')
514546
self._DownloadBlobs(client, payloads, flow_output_dir)
515547

516548
return flow_output_dir
@@ -1424,7 +1456,11 @@ def SetUp(self,
14241456
if host:
14251457
client = self._GetClientBySelector(host)
14261458
for flow_id in flows:
1459+
self.logger.info(
1460+
f'Verifying client access for {client.client_id}...'
1461+
)
14271462
try:
1463+
self.VerifyClientAccess(client)
14281464
client.Flow(flow_id).Get()
14291465
self.StoreContainer(containers.GrrFlow(host, flow_id))
14301466
except Exception as exception: # pylint: disable=broad-except

tests/lib/collectors/grr_hosts.py

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -685,10 +685,17 @@ class GRRFlowCollectorTest(unittest.TestCase):
685685
mock_grr_api: mock.Mock
686686
test_state: state.DFTimewolfState
687687

688+
@mock.patch("grr_api_client.client.Client.VerifyAccess")
688689
@mock.patch('grr_api_client.flow.FlowBase.Get')
689690
@mock.patch('grr_api_client.client.Client.ListFlows')
690691
@mock.patch('grr_api_client.api.InitHttp')
691-
def setUp(self, mock_InitHttp, mock_list_flows, unused_mock_flow_get):
692+
def setUp(
693+
self,
694+
mock_InitHttp,
695+
mock_list_flows,
696+
unused_mock_flow_get,
697+
unused_mock_verify_access,
698+
):
692699
self.mock_grr_api = mock.Mock()
693700
mock_InitHttp.return_value = self.mock_grr_api
694701
self.mock_grr_api.SearchClients.return_value = \
@@ -707,9 +714,10 @@ def setUp(self, mock_InitHttp, mock_list_flows, unused_mock_flow_get):
707714
skip_offline_clients=False,
708715
)
709716

717+
@mock.patch("grr_api_client.client.Client.VerifyAccess")
710718
@mock.patch('dftimewolf.lib.collectors.grr_hosts.GRRFlow._DownloadFiles')
711719
@mock.patch("dftimewolf.lib.collectors.grr_hosts.GRRFlow._AwaitFlow")
712-
def testProcess(self, _, mock_DownloadFiles):
720+
def testProcess(self, _, mock_DownloadFiles, unused_mock_verify_access):
713721
"""Tests that the collector can be initialized."""
714722
self.mock_grr_api.SearchClients.return_value = \
715723
mock_grr_hosts.MOCK_CLIENT_LIST
@@ -730,18 +738,25 @@ def testProcess(self, _, mock_DownloadFiles):
730738
self.assertEqual(result.name, 'tomchop')
731739
self.assertEqual(result.path, '/tmp/something')
732740

741+
@mock.patch("grr_api_client.client.Client.VerifyAccess")
733742
@mock.patch('grr_api_client.flow.FlowBase.Get')
734743
@mock.patch('grr_api_client.client.Client.ListFlows')
735744
@mock.patch('grr_api_client.api.InitHttp')
736745
def testPreProcessNoFlows(
737-
self, mock_InitHttp, mock_list_flows, unused_mock_flow_get):
746+
self,
747+
mock_InitHttp,
748+
mock_list_flows,
749+
unused_mock_flow_get,
750+
unused_mock_verify_access,
751+
):
738752
"""Tests that if no flows are found, an error is thrown."""
739753
self.mock_grr_api = mock.Mock()
740754
mock_InitHttp.return_value = self.mock_grr_api
741755
self.mock_grr_api.SearchClients.return_value = \
742756
mock_grr_hosts.MOCK_CLIENT_LIST
743757
mock_list_flows.return_value = [mock_grr_hosts.flow_pb_terminated]
744758

759+
745760
grr_flow_collector = grr_hosts.GRRFlowCollector(self.test_state)
746761
grr_flow_collector.SetUp(
747762
hostnames='C.0000000000000001',
@@ -762,13 +777,20 @@ def testPreProcessNoFlows(
762777
self.assertEqual('No flows found for collection.', error.exception.message)
763778
self.assertEqual(len(self.test_state.errors), 1)
764779

780+
@mock.patch("grr_api_client.client.Client.VerifyAccess")
765781
@mock.patch('grr_api_client.flow.FlowBase.Get')
766782
@mock.patch('grr_api_client.client.Client.ListFlows')
767783
@mock.patch('grr_api_client.api.InitHttp')
768784
@mock.patch('dftimewolf.lib.collectors.grr_hosts.GRRFlow._DownloadFiles')
769785
@mock.patch("dftimewolf.lib.collectors.grr_hosts.GRRFlow._AwaitFlow")
770786
def testProcessNoFlowData(
771-
self, _, mock_DLFiles, mock_InitHttp, mock_list_flows, unused_mock_flow_get
787+
self,
788+
_,
789+
mock_DLFiles,
790+
mock_InitHttp,
791+
mock_list_flows,
792+
unused_mock_flow_get,
793+
unused_mock_verify_access,
772794
):
773795
"""Tests Process when the flow is found but has no data collected."""
774796
self.mock_grr_api = mock.Mock()

0 commit comments

Comments
 (0)