Skip to content

Commit

Permalink
Add a Carbon Black output for hash banning using the CBAPI (airbnb#713)
Browse files Browse the repository at this point in the history
* cb

* first output push

* Unit tests for CarbonBlack output

* Requirements Update

* Requested Updates airbnb#1 

* Requested Updates airbnb#2

* Updated Creds & Confirmation on Banned Enabled
  • Loading branch information
fusionrace authored May 2, 2018
1 parent 0285880 commit d4ec8d6
Show file tree
Hide file tree
Showing 6 changed files with 275 additions and 2 deletions.
2 changes: 1 addition & 1 deletion requirements-top-level.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ backoff
bandit
boto3
boxsdk[jwt]==2.0.0a11
cbapi
coverage
coveralls
cryptography
Expand All @@ -21,4 +22,3 @@ requests
Sphinx
sphinx-rtd-theme
yapf

1 change: 1 addition & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ backoff==1.5.0
bandit==1.4.0
boto3==1.5.12
boxsdk==2.0.0a11
cbapi==1.3.6
coverage==4.4.2
coveralls==1.2.0
cryptography==2.1.4
Expand Down
103 changes: 103 additions & 0 deletions stream_alert/alert_processor/outputs/carbonblack.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
"""
Copyright 2017-present, Airbnb Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
from collections import OrderedDict

from cbapi.response import BannedHash, Binary, CbResponseAPI

from stream_alert.alert_processor import LOGGER
from stream_alert.alert_processor.outputs.output_base import (
OutputDispatcher,
OutputProperty,
StreamAlertOutput
)


@StreamAlertOutput
class CarbonBlackOutput(OutputDispatcher):
"""CarbonBlackOutput handles all alert dispatching for CarbonBlack"""
__service__ = 'carbonblack'

@classmethod
def get_user_defined_properties(cls):
"""Get properties that must be asssigned by the user when configuring a new CarbonBlack
output. This should be sensitive or unique information for this use-case that needs
to come from the user.
Returns:
OrderedDict: Contains various OutputProperty items
"""
return OrderedDict([
('descriptor',
OutputProperty(description='a short and unique descriptor for this'
' carbonblack output')),
('url',
OutputProperty(description='URL to the CB Response server [https://hostname]:',
mask_input=False,
cred_requirement=True)),
('token',
OutputProperty(description='API token (if unknown, leave blank):',
mask_input=True,
cred_requirement=True)),
])

def dispatch(self, alert, descriptor):
"""Send ban hash command to CarbonBlack
Args:
alert (Alert): Alert instance which triggered a rule
descriptor (str): Output descriptor
Returns:
bool: True if alert was sent successfully, False otherwise
"""
if not alert.context:
LOGGER.error('[%s] Alert must contain context to run actions', self.__service__)
return self._log_status(False, descriptor)

creds = self._load_creds(descriptor)
if not creds:
return self._log_status(False, descriptor)

client = CbResponseAPI(**creds)

# Get md5 hash 'value' from streamalert's rule processor
action = alert.context.get('carbonblack', {}).get('action')
if action == 'ban':
binary_hash = alert.context.get('carbonblack', {}).get('value')
# The binary should already exist in CarbonBlack
binary = client.select(Binary, binary_hash)
# Determine if the binary is currenty listed as banned
if binary.banned:
# Determine if the banned action is enabled, if true exit
if binary.banned.enabled:
return True
# If the binary is banned and disabled, begin the banning hash operation
banned_hash = client.select(BannedHash, binary_hash)
banned_hash.enabled = True
banned_hash.save()
else:
# Create a new BannedHash object to be saved
banned_hash = client.create(BannedHash)
# Begin the banning hash operation
banned_hash.md5hash = binary.md5
banned_hash.text = "Banned from StreamAlert"
banned_hash.enabled = True
banned_hash.save()

return self._log_status(banned_hash.enabled is True, descriptor)
else:
LOGGER.error('[%s] Action not supported: %s', self.__service__, action)
return self._log_status(False, descriptor)
2 changes: 1 addition & 1 deletion stream_alert_cli/manage_lambda/package.py
Original file line number Diff line number Diff line change
Expand Up @@ -309,7 +309,7 @@ class AlertProcessorPackage(LambdaPackage):
package_files = {'stream_alert/__init__.py'}
package_name = 'alert_processor'
config_key = 'alert_processor_config'
third_party_libs = {'backoff', 'requests'}
third_party_libs = {'backoff', 'cbapi', 'requests'}
version = stream_alert_version


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
"""
Copyright 2017-present, Airbnb Inc.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
"""
# pylint: disable=no-self-use,unused-argument,attribute-defined-outside-init
from collections import namedtuple, OrderedDict

from cbapi.response import BannedHash, Binary

from mock import call, patch
from moto import mock_s3, mock_kms
from nose.tools import assert_false, assert_is_instance, assert_true

from stream_alert.alert_processor.outputs import carbonblack
from stream_alert.alert_processor.outputs.carbonblack import CarbonBlackOutput
from stream_alert_cli.helpers import put_mock_creds
from tests.unit.stream_alert_alert_processor import (
ACCOUNT_ID,
CONFIG,
FUNCTION_NAME,
KMS_ALIAS,
REGION
)
from tests.unit.stream_alert_alert_processor.helpers import (
get_alert,
remove_temp_secrets
)


class MockBannedHash(object):
"""Mock for cbapi.response.BannedHash"""

def __init__(self):
self.enabled = True
self.md5hash = None
self.text = ''

@staticmethod
def save():
return True


class MockBinary(object):
"""Mock for cbapi.response.Binary"""

def __init__(self, banned, enabled, md5):
self._banned = banned
self._enabled = enabled
self.md5 = md5

@property
def banned(self):
"""Indicates whether binary is banned"""
if self._banned:
return namedtuple('MockBanned', ['enabled'])(self._enabled)
return False


class MockCBAPI(object):
"""Mock for CbResponseAPI"""

def __init__(self, **kwargs):
return

def create(self, model):
"""Create banned hash"""
if model == BannedHash:
return MockBannedHash()

def select(self, model, file_hash):
if model == Binary:
if file_hash == 'BANNED_ENABLED_HASH':
return MockBinary(banned=True, enabled=True, md5=file_hash)
elif file_hash == 'BANNED_DISABLED_HASH':
return MockBinary(banned=True, enabled=False, md5=file_hash)
return MockBinary(banned=False, enabled=False, md5=file_hash)
elif model == BannedHash:
return MockBannedHash()

@mock_s3
@mock_kms
@patch('stream_alert.alert_processor.outputs.output_base.OutputDispatcher.MAX_RETRY_ATTEMPTS', 1)
class TestCarbonBlackOutput(object):
"""Test class for CarbonBlackOutput"""
DESCRIPTOR = 'unit_test_carbonblack'
SERVICE = 'carbonblack'
CREDS = {'url': 'carbon.foo.bar',
'ssl_verify': 'Y',
'token': '1234567890127a3d7f37f4153270bff41b105899'}

def setup(self):
"""Setup before each method"""
self._dispatcher = CarbonBlackOutput(REGION, ACCOUNT_ID, FUNCTION_NAME, CONFIG)
remove_temp_secrets()
output_name = self._dispatcher.output_cred_name(self.DESCRIPTOR)
put_mock_creds(output_name, self.CREDS, self._dispatcher.secrets_bucket, REGION, KMS_ALIAS)

def test_get_user_defined_properties(self):
"""CarbonBlackOutput - User Defined Properties"""
assert_is_instance(CarbonBlackOutput.get_user_defined_properties(), OrderedDict)

@patch('logging.Logger.error')
def test_dispatch_no_context(self, mock_logger):
"""CarbonBlackOutput - Dispatch No Context"""
assert_false(self._dispatcher.dispatch(get_alert(), self.DESCRIPTOR))
mock_logger.assert_has_calls([
call('[%s] Alert must contain context to run actions', 'carbonblack'),
call('Failed to send alert to %s:%s', 'carbonblack', 'unit_test_carbonblack')
])

@patch.object(carbonblack, 'CbResponseAPI', side_effect=MockCBAPI)
def test_dispatch_already_banned(self, mock_cb):
"""CarbonBlackOutput - Dispatch Already Banned"""
alert_context = {
'carbonblack': {
'action': 'ban',
'value': 'BANNED_ENABLED_HASH'
}
}
assert_true(self._dispatcher.dispatch(get_alert(context=alert_context), self.DESCRIPTOR))

@patch.object(carbonblack, 'CbResponseAPI', side_effect=MockCBAPI)
def test_dispatch_banned_disabled(self, mock_cb):
"""CarbonBlackOutput - Dispatch Banned Disabled"""
alert_context = {
'carbonblack': {
'action': 'ban',
'value': 'BANNED_DISABLED_HASH'
}
}
assert_true(self._dispatcher.dispatch(get_alert(context=alert_context), self.DESCRIPTOR))

@patch.object(carbonblack, 'CbResponseAPI', side_effect=MockCBAPI)
def test_dispatch_not_banned(self, mock_cb):
"""CarbonBlackOutput - Dispatch Not Banned"""
alert_context = {
'carbonblack': {
'action': 'ban',
'value': 'NOT_BANNED_HASH'
}
}
assert_true(self._dispatcher.dispatch(get_alert(context=alert_context), self.DESCRIPTOR))

@patch('logging.Logger.error')
@patch.object(carbonblack, 'CbResponseAPI', side_effect=MockCBAPI)
def test_dispatch_invalid_action(self, mock_cb, mock_logger):
"""CarbonBlackOutput - Invalid Action"""
alert_context = {
'carbonblack': {
'action': 'rickroll',
}
}
assert_false(self._dispatcher.dispatch(get_alert(context=alert_context), self.DESCRIPTOR))
mock_logger.assert_has_calls([
call('[%s] Action not supported: %s', 'carbonblack', 'rickroll'),
call('Failed to send alert to %s:%s', 'carbonblack', 'unit_test_carbonblack')
])
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ def test_output_loading():
'aws-s3',
'aws-sns',
'aws-sqs',
'carbonblack',
'github',
'jira',
'komand',
Expand Down

0 comments on commit d4ec8d6

Please sign in to comment.