diff --git a/responders/Velociraptor/README.md b/responders/Velociraptor/README.md new file mode 100644 index 0000000..de26b65 --- /dev/null +++ b/responders/Velociraptor/README.md @@ -0,0 +1,9 @@ +### Velociraptor +This responder can be used to run a flow for a Velociraptor artifact. This could include gathering data, or performing initial response, as the artifact (or artifact "pack") could encompass any number of actions. The responder can be run on an observable type of `ip`, `fqdn`, or `other`, and will look for a matching client via the Velociraptor server. If a client match is found for the last seen IP, or the hostname, the responder will kick off the flow, the results will be returned, and the client ID will be added as a tag to the case and the observable. + +#### Requirements +The following options are required in the Velociraptor Responder configuration: + +- `velociraptor_client_config`: The path to the Velociraptor API client config. +(See the following for generating an API client config: https://www.velocidex.com/docs/user-interface/api/, and ensure the appropriate ACLs are granted to the API user). +- `velociraptor_artifact`: The name artifact you which to collect (as you would see it in the Velociraptor GUI) diff --git a/responders/Velociraptor/requirements.txt b/responders/Velociraptor/requirements.txt new file mode 100755 index 0000000..1f00236 --- /dev/null +++ b/responders/Velociraptor/requirements.txt @@ -0,0 +1,4 @@ +cortexutils +cryptography +grpcio-tools +pyvelociraptor diff --git a/responders/Velociraptor/velociraptor_flow.json b/responders/Velociraptor/velociraptor_flow.json new file mode 100755 index 0000000..66923a6 --- /dev/null +++ b/responders/Velociraptor/velociraptor_flow.json @@ -0,0 +1,30 @@ +{ + "name": "Velociraptor_Flow", + "version": "0.1", + "author": "Wes Lambert", + "url": "https://github.com/TheHive-Project/Cortex-Analyzers", + "license": "AGPL-V3", + "description": "Run Velociraptor flow", + "dataTypeList": ["thehive:case_artifact"], + "command": "Velociraptor/velociraptor_flow.py", + "baseConfig": "Velociraptor", + "configurationItems": [ + { + "name": "velociraptor_client_config", + "description": "Path to API client config file", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "" + }, + { + "name": "velociraptor_artifact", + "description": "Artifact to collect", + "type": "string", + "multi": false, + "required": true, + "defaultValue": "" + } + ] +} + diff --git a/responders/Velociraptor/velociraptor_flow.py b/responders/Velociraptor/velociraptor_flow.py new file mode 100755 index 0000000..29acc1a --- /dev/null +++ b/responders/Velociraptor/velociraptor_flow.py @@ -0,0 +1,127 @@ +#!/usr/bin/env python3 +from cortexutils.responder import Responder +import json +import grpc +import re +import time +import yaml +import pyvelociraptor +from pyvelociraptor import api_pb2 +from pyvelociraptor import api_pb2_grpc + +class Velociraptor(Responder): + def __init__(self): + Responder.__init__(self) + self.configpath = self.get_param('config.velociraptor_client_config', None, "File path missing!") + self.config = yaml.load(open(self.configpath).read(), Loader=yaml.FullLoader) + self.artifact = self.get_param('config.velociraptor_artifact', None, 'Artifact missing!') + self.observable_type = self.get_param('data.dataType', None, "Data type is empty") + self.observable = self.get_param('data.data', None, 'Data missing!') + + def run(self): + Responder.run(self) + creds = grpc.ssl_channel_credentials( + root_certificates=self.config["ca_certificate"].encode("utf8"), + private_key=self.config["client_private_key"].encode("utf8"), + certificate_chain=self.config["client_cert"].encode("utf8") + ) + + options = (('grpc.ssl_target_name_override', "VelociraptorServer",),) + + with grpc.secure_channel(self.config["api_connection_string"], + creds, options) as channel: + stub = api_pb2_grpc.APIStub(channel) + + if self.observable_type == "ip": + client_query = "select client_id from clients() where last_ip =~ '"+ self.observable + "'" + elif re.search(r'fqdn|other', self.observable_type): + client_query = "select client_id from clients(search='host:" + self.observable + "')" + else: + self.report({'message': "Not a valid data type!" }) + return + + # Send initial request + client_request = api_pb2.VQLCollectorArgs( + max_wait=1, + Query=[api_pb2.VQLRequest( + Name="TheHive-ClientQuery", + VQL=client_query, + )]) + + for client_response in stub.Query(client_request): + try: + client_results = json.loads(client_response.Response) + global client_id + client_id = client_results[0]['client_id'] + except: + self.report({'message': 'Could not find a suitable client.'}) + pass + + # Define initial query + init_query = "SELECT collect_client(client_id='"+ client_id +"',artifacts=['" + self.artifact + "']) FROM scope()" + + # Send initial request + request = api_pb2.VQLCollectorArgs( + max_wait=1, + Query=[api_pb2.VQLRequest( + Name="TheHive-Query", + VQL=init_query, + )]) + + for response in stub.Query(request): + try: + init_results = json.loads(response.Response) + flow=list(init_results[0].values())[0] + self.report({'message': init_results }) + + # Define second query + flow_query = "SELECT * from flows(client_id='" + str(flow['request']['client_id']) + "', flow_id='" + str(flow['flow_id']) + "')" + + state=0 + + # Check to see if the flow has completed + while (state == 0): + + followup_request = api_pb2.VQLCollectorArgs( + max_wait=1, + Query=[api_pb2.VQLRequest( + Name="TheHive-QueryForFlow", + VQL=flow_query, + )]) + + for followup_response in stub.Query(followup_request): + try: + flow_results = json.loads(followup_response.Response) + except: + pass + state = flow_results[0]['state'] + if state == 1: + time.sleep(5) + break + + # Grab the source from the artifact + source_query="SELECT * from source(client_id='"+ str(flow['request']['client_id']) + "', flow_id='" + str(flow['flow_id']) + "', artifact='" + self.artifact + "')" + source_request = api_pb2.VQLCollectorArgs( + max_wait=1, + Query=[api_pb2.VQLRequest( + Name="TheHive-SourceQuery", + VQL=source_query, + )]) + source_results=[] + for source_response in stub.Query(source_request): + try: + source_result = json.loads(source_response.Response) + source_results += source_result + self.report({'message': source_results }) + except: + pass + except: + pass + + def operations(self, raw): + global client_id + return [self.build_operation('AddTagToArtifact', tag=client_id)] + return [self.build_operation('AddTagToCase', tag=client_id)] + +if __name__ == '__main__': + Velociraptor().run()