Skip to content

Commit ea5c31e

Browse files
authored
More configuration options and support direct invocation (#132)
1 parent 64807af commit ea5c31e

File tree

17 files changed

+181
-167
lines changed

17 files changed

+181
-167
lines changed

cli/config.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,14 @@ def name_prefix(self, value: str) -> None:
123123
self._config['name_prefix'] = value
124124

125125
@property
126-
def enable_carbon_black_downloader(self) -> int:
126+
def enable_carbon_black_downloader(self) -> bool:
127127
return self._config['enable_carbon_black_downloader']
128128

129129
@enable_carbon_black_downloader.setter
130-
def enable_carbon_black_downloader(self, value: int) -> None:
131-
if value not in {0, 1}:
130+
def enable_carbon_black_downloader(self, value: bool) -> None:
131+
if not isinstance(value, bool):
132132
raise InvalidConfigError(
133-
'enable_carbon_black_downloader "{}" must be either 0 or 1.'.format(value)
133+
'enable_carbon_black_downloader "{}" must be a boolean.'.format(value)
134134
)
135135
self._config['enable_carbon_black_downloader'] = value
136136

@@ -252,7 +252,7 @@ def configure(self) -> None:
252252
get_input('Unique name prefix, e.g. "company_team"', self.name_prefix, self, 'name_prefix')
253253
enable_downloader = get_input('Enable the CarbonBlack downloader?',
254254
'yes' if self.enable_carbon_black_downloader else 'no')
255-
self.enable_carbon_black_downloader = 1 if enable_downloader == 'yes' else 0
255+
self.enable_carbon_black_downloader = (enable_downloader == 'yes')
256256

257257
if self.enable_carbon_black_downloader:
258258
self._configure_carbon_black()

lambda_functions/analyzer/analyzer_aws_lib.py

Lines changed: 2 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -70,29 +70,14 @@ def _elide_string_middle(text: str, max_length: int) -> str:
7070
return '{} ... {}'.format(text[:half_len], text[-half_len:])
7171

7272

73-
def publish_alert_to_sns(binary: BinaryInfo, topic_arn: str) -> None:
73+
def publish_to_sns(binary: BinaryInfo, topic_arn: str, subject: str) -> None:
7474
"""Publish a JSON SNS alert: a binary has matched one or more YARA rules.
7575
7676
Args:
7777
binary: Instance containing information about the binary.
7878
topic_arn: Publish to this SNS topic ARN.
79+
subject: Message subject (for email subscriptions to the topic)
7980
"""
80-
subject = '[BinaryAlert] {} matches a YARA rule'.format(
81-
binary.filepath or binary.computed_sha)
82-
SNS.Topic(topic_arn).publish(
83-
Subject=_elide_string_middle(subject, SNS_PUBLISH_SUBJECT_MAX_SIZE),
84-
Message=(json.dumps(binary.summary(), indent=4, sort_keys=True))
85-
)
86-
87-
def publish_safe_to_sns(binary: BinaryInfo, topic_arn: str) -> None:
88-
"""Publish a JSON SNS alert: a binary has matched none and is safe.
89-
90-
Args:
91-
binary: Instance containing information about the binary.
92-
topic_arn: Publish to this SNS topic ARN.
93-
"""
94-
subject = '[BinaryAlert] {} is a safe file'.format(
95-
binary.filepath or binary.computed_sha)
9681
SNS.Topic(topic_arn).publish(
9782
Subject=_elide_string_middle(subject, SNS_PUBLISH_SUBJECT_MAX_SIZE),
9883
Message=(json.dumps(binary.summary(), indent=4, sort_keys=True))

lambda_functions/analyzer/binary_info.py

Lines changed: 17 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -86,31 +86,36 @@ def filepath(self) -> str:
8686
return self.s3_metadata.get('filepath', '')
8787

8888
def save_matches_and_alert(
89-
self, analyzer_version: int, dynamo_table_name: str, sns_topic_arn: str) -> None:
89+
self, analyzer_version: int, dynamo_table_name: str, sns_topic_arn: str,
90+
sns_enabled: bool = True) -> None:
9091
"""Save match results to Dynamo and publish an alert to SNS if appropriate.
9192
9293
Args:
9394
analyzer_version: The currently executing version of the Lambda function.
9495
dynamo_table_name: Save YARA match results to this Dynamo table.
9596
sns_topic_arn: Publish match alerts to this SNS topic ARN.
97+
sns_enabled: If True, match alerts are sent to SNS when applicable.
9698
"""
9799
table = analyzer_aws_lib.DynamoMatchTable(dynamo_table_name)
98100
needs_alert = table.save_matches(self, analyzer_version)
99101

100102
# Send alert if appropriate.
101-
if needs_alert:
102-
LOGGER.info('Publishing an SNS alert')
103-
analyzer_aws_lib.publish_alert_to_sns(self, sns_topic_arn)
104-
105-
# alerts on files that are safe
106-
def safe_alert_only(
107-
self, sns_topic_arn: str) -> None:
108-
"""Publish an alert to SNS .
103+
if needs_alert and sns_enabled:
104+
LOGGER.info('Publishing a YARA match alert to %s', sns_topic_arn)
105+
subject = '[BinaryAlert] {} matches a YARA rule'.format(
106+
self.filepath or self.computed_sha)
107+
analyzer_aws_lib.publish_to_sns(self, sns_topic_arn, subject)
108+
109+
def publish_negative_match_result(self, sns_topic_arn: str) -> None:
110+
"""Publish a negative match result (no YARA matches found).
111+
109112
Args:
110-
sns_topic_arn: Publish match alerts to this SNS topic ARN.
113+
sns_topic_arn: Target topic ARN for negative match alerts.
111114
"""
112-
LOGGER.info('Publishing an SNS alert')
113-
analyzer_aws_lib.publish_safe_to_sns(self, sns_topic_arn)
115+
LOGGER.info('Publishing a negative match result to %s', sns_topic_arn)
116+
subject = '[BinaryAlert] {} did not match any YARA rules'.format(
117+
self.filepath or self.computed_sha)
118+
analyzer_aws_lib.publish_to_sns(self, sns_topic_arn, subject)
114119

115120
def summary(self) -> Dict[str, Any]:
116121
"""Generate a summary dictionary of binary attributes."""

lambda_functions/analyzer/main.py

Lines changed: 25 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""AWS Lambda function for testing a binary against a list of YARA rules."""
22
# Expects the following environment variables:
3+
# NO_MATCHES_SNS_TOPIC_ARN: Optional ARN of an SNS topic to notify if there are no YARA matches.
34
# YARA_MATCHES_DYNAMO_TABLE_NAME: Name of the Dynamo table which stores YARA match results.
45
# YARA_ALERTS_SNS_TOPIC_ARN: ARN of the SNS topic which should be alerted on a YARA match.
56
# Expects a binary YARA rules file to be at './compiled_yara_rules.bin'
@@ -30,14 +31,21 @@ def _objects_to_analyze(event: Dict[str, Any]) -> Generator[Tuple[str, str], Non
3031
Yields:
3132
(bucket_name, object_key) string tuples to analyze
3233
"""
34+
if 'BucketName' in event and 'ObjectKeys' in event:
35+
# Direct (simple) invocation
36+
for key in event['ObjectKeys']:
37+
yield event['BucketName'], urllib.parse.unquote_plus(key)
38+
return
39+
40+
# SQS message invocation
3341
for sqs_message in event['Records']:
3442
try:
35-
msg_body = json.loads(sqs_message['body'])
43+
s3_records = json.loads(sqs_message['body'])['Records']
3644
except (KeyError, TypeError, json.JSONDecodeError):
3745
LOGGER.exception('Skipping invalid SQS message %s', json.dumps(sqs_message))
3846
continue
3947

40-
for s3_message in msg_body['Records']:
48+
for s3_message in s3_records:
4149
yield (
4250
s3_message['s3']['bucket']['name'],
4351
urllib.parse.unquote_plus(s3_message['s3']['object']['key'])
@@ -59,18 +67,25 @@ def analyze_lambda_handler(event: Dict[str, Any], lambda_context: Any) -> Dict[s
5967
'name': '...'
6068
},
6169
'object': {
62-
'key': '...'
70+
'key': '...' # URL-encoded key
6371
}
6472
},
6573
...
6674
},
6775
...
6876
]
6977
}),
70-
'messageId': '...'
78+
...
7179
}
7280
]
7381
}
82+
83+
Alternatively, direct invocation is supported with the following event - {
84+
'BucketName': '...',
85+
'EnableSNSAlerts': True,
86+
'ObjectKeys': ['key1', 'key2', ...],
87+
}
88+
7489
lambda_context: LambdaContext object (with .function_version).
7590
7691
Returns:
@@ -97,6 +112,8 @@ def analyze_lambda_handler(event: Dict[str, Any], lambda_context: Any) -> Dict[s
97112
LOGGER.warning('Invoked $LATEST instead of a versioned function')
98113
lambda_version = -1
99114

115+
alerts_enabled = event.get('EnableSNSAlerts', True)
116+
100117
for bucket_name, object_key in _objects_to_analyze(event):
101118
LOGGER.info('Analyzing "%s:%s"', bucket_name, object_key)
102119

@@ -112,12 +129,12 @@ def analyze_lambda_handler(event: Dict[str, Any], lambda_context: Any) -> Dict[s
112129
LOGGER.warning('%s matched YARA rules: %s', binary, binary.matched_rule_ids)
113130
binary.save_matches_and_alert(
114131
lambda_version, os.environ['YARA_MATCHES_DYNAMO_TABLE_NAME'],
115-
os.environ['YARA_ALERTS_SNS_TOPIC_ARN'])
132+
os.environ['YARA_ALERTS_SNS_TOPIC_ARN'],
133+
sns_enabled=alerts_enabled)
116134
else:
117135
LOGGER.info('%s did not match any YARA rules', binary)
118-
if os.environ['SAFE_SNS_TOPIC_ARN']:
119-
binary.safe_alert_only(
120-
os.environ['SAFE_SNS_TOPIC_ARN'])
136+
if alerts_enabled and os.environ['NO_MATCHES_SNS_TOPIC_ARN']:
137+
binary.publish_negative_match_result(os.environ['NO_MATCHES_SNS_TOPIC_ARN'])
121138

122139
# Publish metrics.
123140
if binaries:

manage.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python3
22
"""Command-line tool for easily managing BinaryAlert."""
33
import argparse
4+
import os
45
import sys
56

67
from cli import __version__
@@ -23,6 +24,7 @@ def main() -> None:
2324
'--version', action='version', version='BinaryAlert v{}'.format(__version__))
2425
args = parser.parse_args()
2526

27+
os.environ['TF_IN_AUTOMATION'] = '1'
2628
manager.run(args.command)
2729

2830

terraform/cloudwatch_dashboard.tf

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -179,7 +179,7 @@ EOF
179179
"FunctionName", "${module.binaryalert_analyzer.function_name}",
180180
{"label": "Analyzer"}
181181
]
182-
${var.enable_carbon_black_downloader == 1 ? local.downloader : ""}
182+
${var.enable_carbon_black_downloader ? local.downloader : ""}
183183
]
184184
}
185185
}
@@ -199,7 +199,7 @@ EOF
199199
"FunctionName", "${module.binaryalert_analyzer.function_name}",
200200
{"label": "Analyzer"}
201201
]
202-
${var.enable_carbon_black_downloader == 1 ? local.downloader : ""}
202+
${var.enable_carbon_black_downloader ? local.downloader : ""}
203203
],
204204
"annotations": {
205205
"horizontal": [
@@ -227,7 +227,7 @@ EOF
227227
"FunctionName", "${module.binaryalert_analyzer.function_name}",
228228
{"label": "Analyzer"}
229229
]
230-
${var.enable_carbon_black_downloader == 1 ? local.downloader : ""}
230+
${var.enable_carbon_black_downloader ? local.downloader : ""}
231231
]
232232
}
233233
}
@@ -247,7 +247,7 @@ EOF
247247
"FunctionName", "${module.binaryalert_analyzer.function_name}",
248248
{"label": "Analyzer"}
249249
]
250-
${var.enable_carbon_black_downloader == 1 ? local.downloader : ""}
250+
${var.enable_carbon_black_downloader ? local.downloader : ""}
251251
]
252252
}
253253
}
@@ -283,7 +283,7 @@ EOF
283283
"TopicName", "${aws_sns_topic.yara_match_alerts.name}",
284284
{"label": "YARA Match Alerts"}
285285
],
286-
[".", ".", ".", "${aws_sns_topic.metric_alarms.name}", {"label": "Metric Alarms"}]
286+
[".", ".", ".", "${element(split(":", local.alarm_target), 5)}", {"label": "Metric Alarms"}]
287287
]
288288
}
289289
}
@@ -308,7 +308,7 @@ EOF
308308
"LogGroupName", "${module.binaryalert_analyzer.log_group_name}",
309309
{"label": "Analyzer"}
310310
]
311-
${var.enable_carbon_black_downloader == 1 ? local.downloader_logs : ""}
311+
${var.enable_carbon_black_downloader ? local.downloader_logs : ""}
312312
]
313313
}
314314
}
@@ -341,7 +341,7 @@ EOF
341341
}
342342
EOF
343343

344-
dashboard_body = "${var.enable_carbon_black_downloader == 1 ? local.dashboard_body_with_downloader : local.dashboard_body_without_downloader}"
344+
dashboard_body = "${var.enable_carbon_black_downloader ? local.dashboard_body_with_downloader : local.dashboard_body_without_downloader}"
345345
}
346346

347347
resource "aws_cloudwatch_dashboard" "binaryalert" {

0 commit comments

Comments
 (0)