import logging import time import re from datetime import datetime from notifications import build_repository_event_data from util.jinjautil import get_template_env from util.secscan import PRIORITY_LEVELS, get_priority_for_index logger = logging.getLogger(__name__) TEMPLATE_ENV = get_template_env("events") class InvalidNotificationEventException(Exception): pass class NotificationEvent(object): def __init__(self): pass def get_level(self, event_data, notification_data): """ Returns a 'level' representing the severity of the event. Valid values are: 'info', 'warning', 'error', 'primary', 'success' """ raise NotImplementedError def get_summary(self, event_data, notification_data): """ Returns a human readable one-line summary for the given notification data. """ raise NotImplementedError def get_message(self, event_data, notification_data): """ Returns a human readable HTML message for the given notification data. """ return TEMPLATE_ENV.get_template(self.event_name() + '.html').render({ 'event_data': event_data, 'notification_data': notification_data }) def get_sample_data(self, namespace_name, repo_name, event_config): """ Returns sample data for testing the raising of this notification, with an example notification. """ raise NotImplementedError def should_perform(self, event_data, notification_data): """ Whether a notification for this event should be performed. By default returns True. """ return True @classmethod def event_name(cls): """ Particular event implemented by subclasses. """ raise NotImplementedError @classmethod def get_event(cls, eventname): found = NotificationEvent._get_event(cls, eventname) if found is not None: return found raise InvalidNotificationEventException('Unable to find event: %s' % eventname) @classmethod def event_names(cls): for subc in cls.__subclasses__(): if subc.event_name() is None: for subsubc in subc.__subclasses__(): yield subsubc.event_name() else: yield subc.event_name() @staticmethod def _get_event(cls, eventname): for subc in cls.__subclasses__(): if subc.event_name() is None: found = NotificationEvent._get_event(subc, eventname) if found is not None: return found elif subc.event_name() == eventname: return subc() class RepoPushEvent(NotificationEvent): @classmethod def event_name(cls): return 'repo_push' def get_level(self, event_data, notification_data): return 'primary' def get_summary(self, event_data, notification_data): return 'Repository %s updated' % (event_data['repository']) def get_sample_data(self, namespace_name, repo_name, event_config): return build_repository_event_data(namespace_name, repo_name, { 'updated_tags': ['latest', 'foo'], 'pruned_image_count': 3 }) def _build_summary(event_data): """ Returns a summary string for the build data found in the event data block. """ summary = 'for repository %s [%s]' % (event_data['repository'], event_data['build_id'][0:7]) return summary class VulnerabilityFoundEvent(NotificationEvent): CONFIG_LEVEL = 'level' PRIORITY_KEY = 'priority' VULNERABILITY_KEY = 'vulnerability' MULTIPLE_VULNERABILITY_KEY = 'vulnerabilities' @classmethod def event_name(cls): return 'vulnerability_found' def get_level(self, event_data, notification_data): vuln_data = event_data[VulnerabilityFoundEvent.VULNERABILITY_KEY] priority = vuln_data[VulnerabilityFoundEvent.PRIORITY_KEY] if priority == 'Defcon1' or priority == 'Critical': return 'error' if priority == 'Medium' or priority == 'High': return 'warning' return 'info' def get_sample_data(self, namespace_name, repo_name, event_config): level = event_config.get(VulnerabilityFoundEvent.CONFIG_LEVEL, 'Critical') return build_repository_event_data(namespace_name, repo_name, { 'tags': ['latest', 'prod', 'foo', 'bar', 'baz'], 'image': 'some-image-id', 'vulnerability': { 'id': 'CVE-FAKE-CVE', 'description': 'A futurist vulnerability', 'link': 'https://security-tracker.debian.org/tracker/CVE-FAKE-CVE', 'priority': get_priority_for_index(level) }, }) def should_perform(self, event_data, notification_data): event_config = notification_data.event_config_dict if VulnerabilityFoundEvent.CONFIG_LEVEL not in event_config: return True if VulnerabilityFoundEvent.VULNERABILITY_KEY not in event_data: return False vuln_info = event_data.get(VulnerabilityFoundEvent.VULNERABILITY_KEY, {}) event_severity = PRIORITY_LEVELS.get(vuln_info.get('priority', 'Unknown')) if event_severity is None: return False actual_level_index = int(event_severity['index']) filter_level_index = int(event_config[VulnerabilityFoundEvent.CONFIG_LEVEL]) return actual_level_index <= filter_level_index def get_summary(self, event_data, notification_data): vuln_key = VulnerabilityFoundEvent.VULNERABILITY_KEY priority_key = VulnerabilityFoundEvent.PRIORITY_KEY multiple_vulns = event_data.get(VulnerabilityFoundEvent.MULTIPLE_VULNERABILITY_KEY) if multiple_vulns is not None: top_priority = multiple_vulns[0].get(priority_key, 'Unknown') matching = [v for v in multiple_vulns if v.get(priority_key, 'Unknown') == top_priority] msg = '%s %s' % (len(matching), top_priority) if len(matching) < len(multiple_vulns): msg += ' and %s more' % (len(multiple_vulns) - len(matching)) msg += ' vulnerabilities were detected in repository %s in %s tags' return msg % (event_data['repository'], len(event_data['tags'])) else: msg = '%s vulnerability detected in repository %s in %s tags' return msg % (event_data[vuln_key][priority_key], event_data['repository'], len(event_data['tags'])) class BaseBuildEvent(NotificationEvent): @classmethod def event_name(cls): return None def should_perform(self, event_data, notification_data): if not notification_data.event_config_dict: return True event_config = notification_data.event_config_dict 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' def get_level(self, event_data, notification_data): return 'info' def get_sample_data(self, namespace_name, repo_name, event_config): build_uuid = 'fake-build-id' return build_repository_event_data(namespace_name, repo_name, { 'is_manual': False, 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], 'trigger_id': '1245634', 'trigger_kind': 'GitHub', 'trigger_metadata': { "default_branch": "master", "ref": "refs/heads/somebranch", "commit": "42d4a62c53350993ea41069e9f2cfdefb0df097d", "commit_info": { 'url': 'http://path/to/the/commit', 'message': 'Some commit message', 'date': time.mktime(datetime.now().timetuple()), 'author': { 'username': 'fakeauthor', 'url': 'http://path/to/fake/author/in/scm', 'avatar_url': 'http://www.gravatar.com/avatar/fakehash' } } } }, subpage='/build/%s' % build_uuid) def get_summary(self, event_data, notification_data): return 'Build queued ' + _build_summary(event_data) class BuildStartEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_start' def get_level(self, event_data, notification_data): return 'info' def get_sample_data(self, namespace_name, repo_name, event_config): build_uuid = 'fake-build-id' return build_repository_event_data(namespace_name, repo_name, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], 'trigger_id': '1245634', 'trigger_kind': 'GitHub', 'trigger_metadata': { "default_branch": "master", "ref": "refs/heads/somebranch", "commit": "42d4a62c53350993ea41069e9f2cfdefb0df097d" } }, subpage='/build/%s' % build_uuid) def get_summary(self, event_data, notification_data): return 'Build started ' + _build_summary(event_data) class BuildSuccessEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_success' def get_level(self, event_data, notification_data): return 'success' def get_sample_data(self, namespace_name, repo_name, event_config): build_uuid = 'fake-build-id' return build_repository_event_data(namespace_name, repo_name, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], 'trigger_id': '1245634', 'trigger_kind': 'GitHub', 'trigger_metadata': { "default_branch": "master", "ref": "refs/heads/somebranch", "commit": "42d4a62c53350993ea41069e9f2cfdefb0df097d" }, 'image_id': '1245657346' }, subpage='/build/%s' % build_uuid) def get_summary(self, event_data, notification_data): return 'Build succeeded ' + _build_summary(event_data) class BuildFailureEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_failure' def get_level(self, event_data, notification_data): return 'error' def get_sample_data(self, namespace_name, repo_name, event_config): build_uuid = 'fake-build-id' return build_repository_event_data(namespace_name, repo_name, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], 'trigger_kind': 'GitHub', 'error_message': 'This is a fake error message', 'trigger_id': '1245634', 'trigger_kind': 'GitHub', 'trigger_metadata': { "default_branch": "master", "ref": "refs/heads/somebranch", "commit": "42d4a62c53350993ea41069e9f2cfdefb0df097d", "commit_info": { 'url': 'http://path/to/the/commit', 'message': 'Some commit message', 'date': time.mktime(datetime.now().timetuple()), 'author': { 'username': 'fakeauthor', 'url': 'http://path/to/fake/author/in/scm', 'avatar_url': 'http://www.gravatar.com/avatar/fakehash' } } } }, subpage='/build/%s' % build_uuid) def get_summary(self, event_data, notification_data): return 'Build failure ' + _build_summary(event_data) class BuildCancelledEvent(BaseBuildEvent): @classmethod def event_name(cls): return 'build_cancelled' def get_level(self, event_data, notification_data): return 'info' def get_sample_data(self, namespace_name, repo_name, event_config): build_uuid = 'fake-build-id' return build_repository_event_data(namespace_name, repo_name, { 'build_id': build_uuid, 'build_name': 'some-fake-build', 'docker_tags': ['latest', 'foo', 'bar'], 'trigger_id': '1245634', 'trigger_kind': 'GitHub', 'trigger_metadata': { "default_branch": "master", "ref": "refs/heads/somebranch", "commit": "42d4a62c53350993ea41069e9f2cfdefb0df097d" }, 'image_id': '1245657346' }, subpage='/build/%s' % build_uuid) def get_summary(self, event_data, notification_data): return 'Build cancelled ' + _build_summary(event_data)