From 03d4445a02a7c4563158894b488f3756e70a3549 Mon Sep 17 00:00:00 2001 From: Joseph Schorr Date: Wed, 14 Sep 2016 16:48:17 -0400 Subject: [PATCH] Add notification filtering for builds based on ref regex Fixes #1835 --- endpoints/notificationevent.py | 30 +++- .../js/services/external-notification-data.js | 50 ++++++- test/test_notifications.py | 135 ++++++++++++++++++ 3 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 test/test_notifications.py diff --git a/endpoints/notificationevent.py b/endpoints/notificationevent.py index f1bf0a09d..dc0491e27 100644 --- a/endpoints/notificationevent.py +++ b/endpoints/notificationevent.py @@ -1,6 +1,7 @@ import logging import time import json +import re from datetime import datetime from notificationhelper import build_event_data @@ -138,7 +139,28 @@ class VulnerabilityFoundEvent(NotificationEvent): ', '.join(event_data['tags'])) -class BuildQueueEvent(NotificationEvent): +class BaseBuildEvent(NotificationEvent): + def should_perform(self, event_data, notification_data): + event_config = json.loads(notification_data.event_config_json) + ref_regex = event_config.get('ref-regex') or None + if ref_regex is None: + return True + + # Lookup the ref. If none, this is a non-git build and we should not fire the event. + ref = event_data.get('trigger_metadata', {}).get('ref', None) + if ref is None: + return False + + # Try parsing the regex string as a regular expression. If we fail, we fail to fire + # the event. + try: + return bool(re.compile(str(ref_regex)).match(ref)) + except Exception: + logger.warning('Regular expression error for build event filter: %s', ref_regex) + return False + + +class BuildQueueEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_queued' @@ -177,7 +199,7 @@ class BuildQueueEvent(NotificationEvent): return 'Build queued ' + _build_summary(event_data) -class BuildStartEvent(NotificationEvent): +class BuildStartEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_start' @@ -205,7 +227,7 @@ class BuildStartEvent(NotificationEvent): return 'Build started ' + _build_summary(event_data) -class BuildSuccessEvent(NotificationEvent): +class BuildSuccessEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_success' @@ -234,7 +256,7 @@ class BuildSuccessEvent(NotificationEvent): return 'Build succeeded ' + _build_summary(event_data) -class BuildFailureEvent(NotificationEvent): +class BuildFailureEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_failure' diff --git a/static/js/services/external-notification-data.js b/static/js/services/external-notification-data.js index 229a84ae0..1618b7909 100644 --- a/static/js/services/external-notification-data.js +++ b/static/js/services/external-notification-data.js @@ -20,22 +20,62 @@ function(Config, Features, VulnerabilityService) { { 'id': 'build_queued', 'title': 'Dockerfile Build Queued', - 'icon': 'fa-tasks' + 'icon': 'fa-tasks', + 'fields': [ + { + 'name': 'ref-regex', + 'type': 'regex', + 'title': 'matching ref(s)', + 'help_text': 'An optional regular expression for matching the git branch or tag ' + + 'git ref. If left blank, the notification will fire for all builds.', + 'optional': true, + } + ] }, { 'id': 'build_start', 'title': 'Dockerfile Build Started', - 'icon': 'fa-circle-o-notch' + 'icon': 'fa-circle-o-notch', + 'fields': [ + { + 'name': 'ref-regex', + 'type': 'regex', + 'title': 'matching ref(s)', + 'help_text': 'An optional regular expression for matching the git branch or tag ' + + 'git ref. If left blank, the notification will fire for all builds.', + 'optional': true, + } + ] }, { 'id': 'build_success', 'title': 'Dockerfile Build Successfully Completed', - 'icon': 'fa-check-circle-o' + 'icon': 'fa-check-circle-o', + 'fields': [ + { + 'name': 'ref-regex', + 'type': 'regex', + 'title': 'matching ref(s)', + 'help_text': 'An optional regular expression for matching the git branch or tag ' + + 'git ref. If left blank, the notification will fire for all builds.', + 'optional': true, + } + ] }, { 'id': 'build_failure', 'title': 'Dockerfile Build Failed', - 'icon': 'fa-times-circle-o' + 'icon': 'fa-times-circle-o', + 'fields': [ + { + 'name': 'ref-regex', + 'type': 'regex', + 'title': 'matching ref(s)', + 'help_text': 'An optional regular expression for matching the git branch or tag ' + + 'git ref. If left blank, the notification will fire for all builds.', + 'optional': true, + } + ] }]; for (var i = 0; i < buildEvents.length; ++i) { @@ -52,7 +92,7 @@ function(Config, Features, VulnerabilityService) { { 'name': 'level', 'type': 'enum', - 'title': 'Minimum Severity Level', + 'title': 'minimum severity level', 'values': VulnerabilityService.LEVELS, 'help_text': 'A vulnerability must have a severity of the chosen level (or higher) ' + 'for this notification to fire. Defcon 1 is a special severity level ' + diff --git a/test/test_notifications.py b/test/test_notifications.py new file mode 100644 index 000000000..3da728912 --- /dev/null +++ b/test/test_notifications.py @@ -0,0 +1,135 @@ +import unittest + +from endpoints.notificationevent import BuildSuccessEvent +from util.morecollections import AttrDict + +class TestShouldPerform(unittest.TestCase): + def test_build_nofilter(self): + notification_data = AttrDict({ + 'event_config_json': '{}', + }) + + # No build data at all. + self.assertTrue(BuildSuccessEvent().should_perform({}, notification_data)) + + # With trigger metadata but no ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': {}, + }, notification_data)) + + # With trigger metadata and a ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/somebranch', + }, + }, notification_data)) + + + def test_build_emptyfilter(self): + notification_data = AttrDict({ + 'event_config_json': '{"ref-regex": ""}', + }) + + # No build data at all. + self.assertTrue(BuildSuccessEvent().should_perform({}, notification_data)) + + # With trigger metadata but no ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': {}, + }, notification_data)) + + # With trigger metadata and a ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/somebranch', + }, + }, notification_data)) + + + def test_build_invalidfilter(self): + notification_data = AttrDict({ + 'event_config_json': '{"ref-regex": "]["}', + }) + + # No build data at all. + self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data)) + + # With trigger metadata but no ref. + self.assertFalse(BuildSuccessEvent().should_perform({ + 'trigger_metadata': {}, + }, notification_data)) + + # With trigger metadata and a ref. + self.assertFalse(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/somebranch', + }, + }, notification_data)) + + + def test_build_withfilter(self): + notification_data = AttrDict({ + 'event_config_json': '{"ref-regex": "refs/heads/master"}', + }) + + # No build data at all. + self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data)) + + # With trigger metadata but no ref. + self.assertFalse(BuildSuccessEvent().should_perform({ + 'trigger_metadata': {}, + }, notification_data)) + + # With trigger metadata and a not-matching ref. + self.assertFalse(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/somebranch', + }, + }, notification_data)) + + # With trigger metadata and a matching ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/master', + }, + }, notification_data)) + + + def test_build_withwildcardfilter(self): + notification_data = AttrDict({ + 'event_config_json': '{"ref-regex": "refs/heads/.+"}', + }) + + # No build data at all. + self.assertFalse(BuildSuccessEvent().should_perform({}, notification_data)) + + # With trigger metadata but no ref. + self.assertFalse(BuildSuccessEvent().should_perform({ + 'trigger_metadata': {}, + }, notification_data)) + + # With trigger metadata and a not-matching ref. + self.assertFalse(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/tags/sometag', + }, + }, notification_data)) + + # With trigger metadata and a matching ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/master', + }, + }, notification_data)) + + # With trigger metadata and another matching ref. + self.assertTrue(BuildSuccessEvent().should_perform({ + 'trigger_metadata': { + 'ref': 'refs/heads/somebranch', + }, + }, notification_data)) + + +if __name__ == '__main__': + unittest.main() +