Skip to content

Commit

Permalink
Merge pull request #147 from isimluk/arc
Browse files Browse the repository at this point in the history
Azure Arc Autodiscovery
  • Loading branch information
isimluk authored Nov 25, 2022
2 parents 4576abd + 01cd376 commit ed9fefa
Show file tree
Hide file tree
Showing 15 changed files with 229 additions and 17 deletions.
3 changes: 3 additions & 0 deletions config/config.ini
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,9 @@
# Uncomment to provide Azure Primary Key. Alternatively, use PRIMARY_KEY env variable.
#primary_key =

# Uncoment to enable RTR based auto discovery of Azure Arc Systems
# arc_autodiscovery = true

[aws]
# AWS section is applicable only when AWS backend is enabled in the [main] section.

Expand Down
2 changes: 2 additions & 0 deletions config/defaults.ini
Original file line number Diff line number Diff line change
Expand Up @@ -18,13 +18,15 @@ client_id =
client_secret =
application_id = fig-default-app-id
reconnect_retry_count = 36
rtr_quarantine_keyword = infected

[gcp]
# Use GOOGLE_APPLICATION_CREDENTIALS env variable

[azure]
workspace_id =
primary_key =
arc_autodiscovery = false

[aws]
region =
Expand Down
14 changes: 14 additions & 0 deletions fig/backends/azure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,22 @@ backends=AZURE
#workspace_id =
# Uncomment to provide Azure Primary Key. Alternatively, use PRIMARY_KEY env variable.
#primary_key =
# Uncoment to enable RTR based auto discovery of Azure Arc Systems
# arc_autodiscovery = true
```

### Azure Arc Autodiscovery

Azure Arc is service within Microsoft Azure that allows users to connect and manage systems outside Azure using single pane of glass (Azure user interface).

Falcon Integration Gateway is able to identify Azure Arc system properties (resourceName, resourceGroup, subscriptionId, tenantId, and vmId) using RTR and send these details over to Azure Log Analytics.

To enable this feature:
- set `azure_autodiscovery=true` in config.ini
- grant extra Falcon permission to API keys in CrowdStrike Falcon
- Real Time Response: [Read, Write]

### Developer Guide

- Build the image
Expand Down
57 changes: 56 additions & 1 deletion fig/backends/azure/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
from requests import post
from ...log import log
from ...config import config
from ...falcon.errors import RTRConnectionError


def build_signature(workspace_id, primary_key, date, content_length, method, content_type, resource):
Expand Down Expand Up @@ -45,10 +46,55 @@ def post_data(workspace_id, primary_key, body, log_type):


class Submitter():
AZURE_ARC_KEYS = ['resourceName', 'resourceGroup', 'subscriptionId', 'tenantId', 'vmId']

def __init__(self, event):
self.event = event
self.workspace_id = config.get('azure', 'workspace_id')
self.primary_key = config.get('azure', 'primary_key')
self.azure_arc_config = self.autodiscovery()

def autodiscovery(self):
if self.event.cloud_provider == 'AZURE' or not config.getboolean('azure', 'arc_autodiscovery'):
return None

if self.event.device_details['platform_name'] != 'Linux':
log.debug('Skipping Azure Arc Autodiscovery for %s (aid=%s, name=%s)',
self.event.device_details['platform_name'],
self.event.original_event.sensor_id,
self.event.original_event.computer_name
)
return None
if self.event.device_details['product_type_desc'] == 'Pod':
log.debug('Skipping Azure Arc Autodiscovery for k8s pod (aid=%s, name=%s)',
self.event.original_event.sensor_id,
self.event.original_event.computer_name
)
return None

try:
azure_arc_config = self.event.azure_arc_config()
except RTRConnectionError as e:
log.error("Cannot fetch Azure Arc info from host (aid=%s, hostname=%s, last_seen=%s): %s",
self.event.original_event.sensor_id,
self.event.device_details['hostname'],
self.event.device_details['last_seen'],
e
)
return None
except Exception as e: # pylint: disable=W0703
log.exception("Cannot fetch Azure Arc info from host (aid=%s, hostname=%s, last_seen=%s): %s",
self.event.original_event.sensor_id,
self.event.device_details['hostname'],
self.event.device_details['last_seen'],
e
)
return None

return {k: v
for k, v in azure_arc_config.items()
if k in self.AZURE_ARC_KEYS
}

def submit(self):
log.info("Processing detection: %s", self.event.detect_description)
Expand All @@ -58,7 +104,7 @@ def log(self):
json_data = [{
'ExternalUri': self.event.falcon_link,
'FalconEventId': self.event.event_id,
'ComputerName': self.event.original_event['event']['ComputerName'],
'ComputerName': self.event.original_event.computer_name,
'Description': self.event.detect_description,
'Severity': self.event.severity,
'Title': 'Falcon Alert. Instance {}'.format(self.event.instance_id),
Expand All @@ -68,10 +114,19 @@ def log(self):
'DetectName': self.event.detect_name,
'AccountId': self.event.cloud_provider_account_id,
'InstanceId': self.event.instance_id,
'CloudProvider': self.cloud,
'ResourceGroup': self.event.device_details.get('zone_group', None)
}]

if self.azure_arc_config is not None:
json_data[0]['arc'] = self.azure_arc_config

return dumps(json_data)

@property
def cloud(self):
return self.event.cloud_provider if self.event.cloud_provider is not None else 'Unrecognized'


class Runtime():
RELEVANT_EVENT_TYPES = ['DetectionSummaryEvent']
Expand Down
2 changes: 1 addition & 1 deletion fig/backends/chronicle/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ def udm(self):
},
"principal": {
"asset_id": "CrowdStrike.Falcon:" + event["SensorId"],
"hostname": event["ComputerName"],
"hostname": self.event.original_event.computer_name,
"user": {
"userid": event["UserName"]
},
Expand Down
2 changes: 1 addition & 1 deletion fig/backends/gcp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ def finding(self):
severity=self.severity,
source_properties={
'FalconEventId': self.event.event_id,
'ComputerName': self.event.original_event['event']['ComputerName'],
'ComputerName': self.event.original_event.computer_name,
'Description': self.event.detect_description,
'Severity': self.severity,
'Title': 'Falcon Alert. Instance {}'.format(self.event.instance_id),
Expand Down
2 changes: 2 additions & 0 deletions fig/config/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,8 @@ def validate_backends(self):
raise Exception('Malformed Configuration: expected azure.workspace_id to be non-empty')
if len(self.get('azure', 'primary_key')) == 0:
raise Exception('Malformed Configuration: expected azure.primary_key to be non-empty')
if self.get('azure', 'arc_autodiscovery') not in ['false', 'true']:
raise Exception('Malformed Configuration: expected azure.arc_autodiscovery must be either true or false')

@cached_property
def backends(self):
Expand Down
38 changes: 26 additions & 12 deletions fig/falcon/api.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,9 @@
from falconpy import api_complete as FalconSDK
from ..config import config
from .errors import ApiError, NoStreamsError
from .models import Stream


class ApiError(Exception):
pass


class NoStreamsError(ApiError):
def __init__(self, app_id):
super().__init__(
'Falcon Streaming API not discovered. This may be caused by second instance of this application '
'already running in your environment with the same application_id={}, or by missing streaming API '
'capability.'.format(app_id))
from .rtr import RTRSession
from .. import __version__


class FalconAPI():
Expand All @@ -27,6 +18,7 @@ def __init__(self):
self.client = FalconSDK.APIHarness(creds={
'client_id': config.get('falcon', 'client_id'),
'client_secret': config.get('falcon', 'client_secret')},
user_agent=f"falcon-integration-gateway/{__version__}",
base_url=self.__class__.base_url())

@classmethod
Expand Down Expand Up @@ -78,6 +70,28 @@ def check_rtr_command_status(self, cloud_request_id, sequence_id):
}
)

def rtr_fetch_file(self, device_id, filepath):
session = RTRSession(self, device_id)

z7pack = None
try:
z7pack = session.get_file(filepath)
finally:
session.close()

import io # pylint: disable=C0415
import py7zr # pylint: disable=C0415

flo = io.BytesIO(z7pack)
with py7zr.SevenZipFile(flo, password=config.get('falcon', 'rtr_quarantine_keyword')) as archive:
content = archive.readall()
if len(content) != 1:
raise ApiError('Cannot extract RTR file from 7z')

for _fname, bio in content.items():
return bio.read()
raise ApiError(f'Cannot extract file {filepath} from device {device_id}')

def _resources(self, *args, **kwargs):
response = self._command(*args, **kwargs)
body = response['body']
Expand Down
18 changes: 18 additions & 0 deletions fig/falcon/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
class ApiError(Exception):
pass


class NoStreamsError(ApiError):
def __init__(self, app_id):
super().__init__(
'Falcon Streaming API not discovered. This may be caused by second instance of this application '
'already running in your environment with the same application_id={}, or by missing streaming API '
'capability.'.format(app_id))


class RTRError(ApiError):
pass


class RTRConnectionError(ApiError):
pass
4 changes: 4 additions & 0 deletions fig/falcon/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ def creation_time(self):
def sensor_id(self):
return self['event']['SensorId']

@property
def computer_name(self):
return self['event']['ComputerName']

@classmethod
def parse_cs_time(cls, cs_timestamp):
return datetime.datetime.utcfromtimestamp(float(cs_timestamp) / 1000.0)
Expand Down
85 changes: 85 additions & 0 deletions fig/falcon/rtr.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
from .errors import ApiError, RTRError, RTRConnectionError


class RTRSession:
def __init__(self, falcon_api, device_id):
self.falcon = falcon_api
self.device_id = device_id
self.session = self._connect()

def close(self):
# return self.falcon.client.command('RTR_DeleteSession', session_id=self.id)
# Below is a workaround for the above not working properly (/cc @jshcodes)

from falconpy import OAuth2, RealTimeResponse # pylint: disable=C0415
from ..config import config # pylint: disable=C0415
from ..log import log # pylint: disable=C0415
falcon_auth = OAuth2(
client_id=config.get('falcon', 'client_id'),
client_secret=config.get('falcon', 'client_secret')
)
falcon_rtr = RealTimeResponse(falcon_auth)
response = falcon_rtr.delete_session(session_id=self.id)
if response['status_code'] != 204:
log.debug('Unable to close the RTR session: reponse was: %s', response)

def execute_and_wait(self, action, base_command, command_string):
command = self._execute(action, base_command, command_string)
response = self._rtr_wait(command[0])

if response['stderr']:
raise RTRError(f'RTR Execute device: {self.device_id}, stderr: {response["stderr"]}')
return response

def get_file(self, filepath):
command = self.execute_and_wait('RTR_ExecuteActiveResponderCommand', 'get', 'get ' + filepath)
for f in self._list_files():
if f['cloud_request_id'] == command['task_id']:
return self._fetch_file(f['sha256'], filepath)

raise RTRError(f'RTR File Not Found: device: {self.device_id}, file {filepath}')

@property
def id(self):
return self.session['session_id']

def _list_files(self):
return self.falcon._resources( # pylint: disable=W0212
'RTR_ListFiles',
parameters={
'session_id': self.id
}
)

def _fetch_file(self, sha256, filepath):
response = self.falcon.client.command(
'RTR_GetExtractedFileContents',
parameters={
'session_id': self.id,
'sha256': sha256,
'filepath': filepath,
}
)
if not isinstance(response, (bytes, bytearray)):
raise RTRError(f"Could not fetch RTR file from Falcon: {response['body']}")
return response

def _connect(self):
response = None
try:
response = self.falcon.init_rtr_session(self.device_id)
except ApiError as e:
raise RTRConnectionError(f"{e}") from e

if len(response) != 1:
raise RTRError(f'Unexpected response from RTR Init: {response}')
return response[0]

def _rtr_wait(self, command):
check = self.falcon.check_rtr_command_status(command['cloud_request_id'], 0)[0]
while not check['complete']:
check = self.falcon.check_rtr_command_status(command['cloud_request_id'], 0)[0]
return check

def _execute(self, action, base_command, command_string):
return self.falcon.execute_rtr_command(action, self.id, base_command, command_string)
13 changes: 13 additions & 0 deletions fig/falcon_data.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import json
from .falcon import Event


Expand All @@ -18,6 +19,7 @@ def __init__(self, falcon_api):
self.falcon_api = falcon_api
self._host_detail = {}
self._mdm_id = {}
self._arc_config = {}

def device_details(self, sensor_id):
if not sensor_id:
Expand All @@ -35,6 +37,14 @@ def device_details(self, sensor_id):

return self._host_detail[sensor_id]

def azure_arc_config(self, sensor_id):
if not sensor_id:
return EventDataError("Cannot fetch Azure Arc info. SensorId field is missing")
if sensor_id not in self._arc_config:
file_bytes = self.falcon_api.rtr_fetch_file(sensor_id, '/var/opt/azcmagent/agentconfig.json')
self._arc_config[sensor_id] = json.loads(file_bytes)
return self._arc_config[sensor_id]

def mdm_identifier(self, sensor_id, event_platform):
if not sensor_id:
return EventDataError("Cannot process event. SensorId field is missing: ")
Expand Down Expand Up @@ -89,6 +99,9 @@ def mdm_identifier(self):
device_details = self.cache.device_details(self.original_event.sensor_id)
return self.cache.mdm_identifier(self.original_event.sensor_id, device_details['platform_name'])

def azure_arc_config(self):
return self.cache.azure_arc_config(self.original_event.sensor_id)

@property
def cloud_provider(self):
return self.device_details.get('service_provider', None)
Expand Down
1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ google-cloud-resource-manager >= 1.0.2
tls-syslog
google-auth
google-api-python-client
py7zr
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ ignore = C901,E501

[pylint.MASTER]
disable=
C0114,C0115,C0116,C0209,
C0114,C0115,C0116,C0209,C0103,
R0903,R0912,R0915,
W0511,W0613,C0301

Expand Down
Loading

0 comments on commit ed9fefa

Please sign in to comment.