Skip to content

Commit 1cbaeb3

Browse files
authored
GCS Output Plugin for FileFinderResults (#1128)
1 parent 58a11b1 commit 1cbaeb3

File tree

6 files changed

+246
-0
lines changed

6 files changed

+246
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,3 +33,6 @@ installers/
3333
GRRlog.txt
3434
grr/server/grr_response_server/gui/static/third-party/
3535
grr/server/grr_response_server/gui/ui/.angular/
36+
docker_config_files/*.pem
37+
compose.watch.yaml
38+
Dockerfile.client

compose.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,7 @@ services:
145145
container_name: grr-worker
146146
volumes:
147147
- ./docker_config_files:/configs
148+
- ${HOME}/.config/gcloud:/root/.config/gcloud
148149
hostname: grr-worker
149150
depends_on:
150151
db:

grr/proto/grr_response_proto/output_plugin.proto

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,3 +104,13 @@ message ElasticsearchOutputPluginArgs {
104104
description: "The Elasticsearch index to place the events in"
105105
}];
106106
}
107+
108+
message GcsOutputPluginArgs {
109+
optional string gcs_bucket = 1 [(sem_type) = {
110+
description: "The GCS bucket to which objects will be stored.",
111+
}];
112+
optional string project_id = 2 [(sem_type) = {
113+
description: "The Project ID with the GCS bucket.",
114+
}];
115+
}
116+

grr/server/grr_response_server/output_plugins/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@
1515
from grr_response_server.output_plugins import splunk_plugin
1616
from grr_response_server.output_plugins import sqlite_plugin
1717
from grr_response_server.output_plugins import yaml_plugin
18+
from grr_response_server.output_plugins import gcs_plugin
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
#!/usr/bin/env python
2+
"""GCS output plugin for FileFinder flow results."""
3+
4+
import jinja2
5+
import json
6+
import logging
7+
8+
from typing import Any, Dict
9+
10+
from google.cloud import storage
11+
from google.protobuf import json_format
12+
from grr_response_core import config
13+
from grr_response_core.lib import rdfvalue
14+
from grr_response_core.lib import utils
15+
from grr_response_core.lib.rdfvalues import standard as rdf_standard
16+
from grr_response_core.lib.rdfvalues import structs as rdf_structs
17+
from grr_response_proto import output_plugin_pb2
18+
from grr_response_server import data_store
19+
from grr_response_server import file_store
20+
from grr_response_server import output_plugin
21+
from grr_response_server.databases import db
22+
23+
JsonDict = Dict[str, Any]
24+
25+
class GcsOutputPluginArgs(rdf_structs.RDFProtoStruct):
26+
protobuf = output_plugin_pb2.GcsOutputPluginArgs
27+
28+
29+
class GcsOutputPlugin(output_plugin.OutputPlugin):
30+
"""An output plugin that sends the object to GCS for each response received."""
31+
name = "GCS Bucket"
32+
description = "Send to GCS for each result."
33+
args_type = GcsOutputPluginArgs
34+
produces_output_streams = False
35+
uploaded_files = 0
36+
37+
38+
def UploadBlobFromStream(self, project_id, bucket_name, client_path, client_id, flow_id, destination_blob_name):
39+
"""Uploads bytes from a stream to a blob"""
40+
storage_client = storage.Client(project=project_id)
41+
bucket = storage_client.bucket(bucket_name)
42+
blob = bucket.blob(client_id+"/"+flow_id+destination_blob_name)
43+
fd = file_store.OpenFile(client_path)
44+
fd.seek(0)
45+
blob.upload_from_file(fd)
46+
47+
def ProcessResponse(self, state, response):
48+
"""Sends objects to GCS for each response."""
49+
50+
client_id = response.client_id
51+
flow_id = response.flow_id
52+
53+
if response.HasField("payload"):
54+
if response.payload.HasField("transferred_file"):
55+
if response.payload.stat_entry.st_size > 0 :
56+
client_path = db.ClientPath.FromPathSpec(client_id, response.payload.stat_entry.pathspec)
57+
self.UploadBlobFromStream(self.args.project_id, self.args.gcs_bucket, client_path, client_id, flow_id, response.payload.stat_entry.pathspec.path)
58+
logging.info("response client_id: %s, flow_id: %s, transferred_file: %s", client_id, flow_id, response.payload.stat_entry.pathspec.path)
59+
self.uploaded_files += 1
60+
else:
61+
logging.warning("%s : file size is 0, nothing to push to GCS", response.payload.stat_entry.pathspec.path)
62+
63+
64+
def ProcessResponses(self, state, responses):
65+
if self.args.gcs_bucket == "" or self.args.project_id == "":
66+
logging.error("both GCS bucket and Project ID must be set")
67+
return
68+
else:
69+
logging.info("Project ID: %s, GCS bucket: %s", self.args.project_id, self.args.gcs_bucket)
70+
71+
for response in responses:
72+
self.ProcessResponse(state, response)
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
#!/usr/bin/env python
2+
"""Tests for GCS output plugin."""
3+
4+
import json
5+
import requests
6+
7+
from absl import app
8+
9+
from unittest import mock
10+
from unittest.mock import MagicMock
11+
12+
from grr_response_core.lib import rdfvalue
13+
from grr_response_core.lib.rdfvalues import client as rdf_client
14+
from grr_response_core.lib.rdfvalues import client_fs as rdf_client_fs
15+
from grr_response_core.lib.rdfvalues import file_finder as rdf_file_finder
16+
from grr_response_core.lib.rdfvalues import paths as rdf_paths
17+
18+
from grr_response_server.output_plugins import gcs_plugin
19+
from grr_response_server.rdfvalues import flow_objects as rdf_flow_objects
20+
21+
from grr.test_lib import flow_test_lib
22+
from grr.test_lib import test_lib
23+
24+
25+
class GcsOutputPluginTest(flow_test_lib.FlowTestsBaseclass):
26+
"""Tests GCS output plugin."""
27+
28+
def setUp(self):
29+
super().setUp()
30+
31+
self.client_id = self.SetupClient(0)
32+
self.flow_id = '12345678'
33+
self.source_id = (
34+
rdf_client.ClientURN(self.client_id)
35+
.Add('Results')
36+
.RelativeName('aff4:/')
37+
)
38+
39+
40+
def _CallPlugin(self, plugin_args=None, responses=None):
41+
plugin_cls = gcs_plugin.GcsOutputPlugin
42+
plugin, plugin_state = plugin_cls.CreatePluginAndDefaultState(
43+
source_urn=self.source_id, args=plugin_args
44+
)
45+
46+
plugin_cls.UploadBlobFromStream = MagicMock()
47+
48+
messages = []
49+
for response in responses:
50+
messages.append(
51+
rdf_flow_objects.FlowResult(
52+
client_id=self.client_id, flow_id=self.flow_id, payload=response
53+
)
54+
)
55+
56+
with test_lib.FakeTime(1445995873):
57+
plugin.ProcessResponses(plugin_state, messages)
58+
plugin.Flush(plugin_state)
59+
plugin.UpdateState(plugin_state)
60+
61+
return plugin.uploaded_files
62+
63+
64+
def testClientFileFinderResponseUploaded(self):
65+
rdf_payload = rdf_file_finder.FileFinderResult()
66+
rdf_payload.stat_entry.st_size = 1234
67+
rdf_payload.stat_entry.pathspec.path = ("/var/log/test.log")
68+
rdf_payload.stat_entry.pathspec.pathtype = "OS"
69+
rdf_payload.transferred_file = rdf_client_fs.BlobImageDescriptor(chunks=[])
70+
71+
with test_lib.FakeTime(rdfvalue.RDFDatetime.FromSecondsSinceEpoch(15)):
72+
uploaded_files = self._CallPlugin(
73+
plugin_args=gcs_plugin.GcsOutputPluginArgs(
74+
project_id="test-project-id",
75+
gcs_bucket="text-gcs-bucket"
76+
),
77+
responses=[rdf_payload],
78+
)
79+
80+
self.assertEqual(uploaded_files, 1)
81+
82+
83+
def testClientFileFinderResponseNoProjectId(self):
84+
rdf_payload = rdf_file_finder.FileFinderResult()
85+
rdf_payload.stat_entry.st_size = 1234
86+
rdf_payload.stat_entry.pathspec.path = ("/var/log/test.log")
87+
rdf_payload.stat_entry.pathspec.pathtype = "OS"
88+
rdf_payload.transferred_file = rdf_client_fs.BlobImageDescriptor(chunks=[])
89+
90+
with test_lib.FakeTime(rdfvalue.RDFDatetime.FromSecondsSinceEpoch(15)):
91+
uploaded_files = self._CallPlugin(
92+
plugin_args=gcs_plugin.GcsOutputPluginArgs(
93+
project_id="",
94+
gcs_bucket="text-gcs-bucket"
95+
),
96+
responses=[rdf_payload],
97+
)
98+
99+
self.assertEqual(uploaded_files, 0)
100+
101+
102+
def testClientFileFinderResponseNoGcsBucket(self):
103+
rdf_payload = rdf_file_finder.FileFinderResult()
104+
rdf_payload.stat_entry.st_size = 1234
105+
rdf_payload.stat_entry.pathspec.path = ("/var/log/test.log")
106+
rdf_payload.stat_entry.pathspec.pathtype = "OS"
107+
rdf_payload.transferred_file = rdf_client_fs.BlobImageDescriptor(chunks=[])
108+
109+
with test_lib.FakeTime(rdfvalue.RDFDatetime.FromSecondsSinceEpoch(15)):
110+
uploaded_files = self._CallPlugin(
111+
plugin_args=gcs_plugin.GcsOutputPluginArgs(
112+
project_id="test-project-id",
113+
gcs_bucket=""
114+
),
115+
responses=[rdf_payload],
116+
)
117+
118+
self.assertEqual(uploaded_files, 0)
119+
120+
121+
def testClientFileFinderResponseEmptyFile(self):
122+
rdf_payload = rdf_file_finder.FileFinderResult()
123+
rdf_payload.stat_entry.st_size = 0
124+
rdf_payload.stat_entry.pathspec.path = ("/var/log/test.log")
125+
rdf_payload.stat_entry.pathspec.pathtype = "OS"
126+
rdf_payload.transferred_file = rdf_client_fs.BlobImageDescriptor(chunks=[])
127+
128+
with test_lib.FakeTime(rdfvalue.RDFDatetime.FromSecondsSinceEpoch(15)):
129+
uploaded_files = self._CallPlugin(
130+
plugin_args=gcs_plugin.GcsOutputPluginArgs(
131+
project_id="test-project-id",
132+
gcs_bucket="text-gcs-bucket"
133+
),
134+
responses=[rdf_payload],
135+
)
136+
137+
self.assertEqual(uploaded_files, 0)
138+
139+
140+
def testClientFileFinderResponseNoTransferredFile(self):
141+
rdf_payload = rdf_file_finder.FileFinderResult()
142+
rdf_payload.stat_entry.st_size = 1234
143+
rdf_payload.stat_entry.pathspec.path = ("/var/log/test.log")
144+
rdf_payload.stat_entry.pathspec.pathtype = "OS"
145+
146+
with test_lib.FakeTime(rdfvalue.RDFDatetime.FromSecondsSinceEpoch(15)):
147+
uploaded_files = self._CallPlugin(
148+
plugin_args=gcs_plugin.GcsOutputPluginArgs(
149+
project_id="test-project-id",
150+
gcs_bucket="text-gcs-bucket"
151+
),
152+
responses=[rdf_payload],
153+
)
154+
155+
self.assertEqual(uploaded_files, 0)
156+
157+
158+
if __name__ == '__main__':
159+
app.run(test_lib.main)

0 commit comments

Comments
 (0)