423 lines
13 KiB
Python
423 lines
13 KiB
Python
|
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
|
||
|
})
|
||
|
|
||
|
|
||
|
class RepoMirrorSyncStartedEvent(NotificationEvent):
|
||
|
@classmethod
|
||
|
def event_name(cls):
|
||
|
return 'repo_mirror_sync_started'
|
||
|
|
||
|
def get_level(self, event_data, notification_data):
|
||
|
return 'info'
|
||
|
|
||
|
def get_summary(self, event_data, notification_data):
|
||
|
return 'Repository Mirror started for %s' % (event_data['message'])
|
||
|
|
||
|
def get_sample_data(self, namespace_name, repo_name, event_config):
|
||
|
return build_repository_event_data(namespace_name, repo_name, {
|
||
|
'message': 'TEST NOTIFICATION'
|
||
|
})
|
||
|
|
||
|
|
||
|
class RepoMirrorSyncSuccessEvent(NotificationEvent):
|
||
|
@classmethod
|
||
|
def event_name(cls):
|
||
|
return 'repo_mirror_sync_success'
|
||
|
|
||
|
def get_level(self, event_data, notification_data):
|
||
|
return 'success'
|
||
|
|
||
|
def get_summary(self, event_data, notification_data):
|
||
|
return 'Repository Mirror success for %s' % (event_data['message'])
|
||
|
|
||
|
def get_sample_data(self, namespace_name, repo_name, event_config):
|
||
|
return build_repository_event_data(namespace_name, repo_name, {
|
||
|
'message': 'TEST NOTIFICATION'
|
||
|
})
|
||
|
|
||
|
|
||
|
class RepoMirrorSyncFailedEvent(NotificationEvent):
|
||
|
@classmethod
|
||
|
def event_name(cls):
|
||
|
return 'repo_mirror_sync_failed'
|
||
|
|
||
|
def get_level(self, event_data, notification_data):
|
||
|
return 'error'
|
||
|
|
||
|
def get_summary(self, event_data, notification_data):
|
||
|
return 'Repository Mirror failed for %s' % (event_data['message'])
|
||
|
|
||
|
def get_sample_data(self, namespace_name, repo_name, event_config):
|
||
|
return build_repository_event_data(namespace_name, repo_name, {
|
||
|
'message': 'TEST NOTIFICATION'
|
||
|
})
|
||
|
|
||
|
|
||
|
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)
|