From fe9703dd943b919d34bf9c6713afdd8509bfe2bb Mon Sep 17 00:00:00 2001 From: kevgliss Date: Mon, 27 Jun 2016 14:40:46 -0700 Subject: [PATCH] Closes #284 (#336) --- gulp/build.js | 2 +- lemur/__init__.py | 4 +- lemur/authorities/service.py | 10 - lemur/certificates/models.py | 41 ++-- lemur/certificates/schemas.py | 4 +- lemur/certificates/service.py | 11 +- lemur/common/defaults.py | 5 +- lemur/endpoints/__init__.py | 0 lemur/endpoints/models.py | 80 ++++++++ lemur/endpoints/schemas.py | 43 ++++ lemur/endpoints/service.py | 144 +++++++++++++ lemur/endpoints/views.py | 106 ++++++++++ lemur/factory.py | 6 +- lemur/manage.py | 7 +- lemur/migrations/versions/368320d26c6c_.py | 48 +++++ lemur/models.py | 13 +- lemur/notifications/service.py | 1 + lemur/plugins/lemur_aws/ec2.py | 23 +++ lemur/plugins/lemur_aws/elb.py | 189 +++++++++--------- lemur/plugins/lemur_aws/plugin.py | 81 ++++++-- lemur/plugins/lemur_aws/sts.py | 42 +++- lemur/plugins/lemur_aws/tests/conftest.py | 1 + lemur/plugins/lemur_aws/tests/test_elb.py | 14 ++ lemur/plugins/lemur_aws/tests/test_plugin.py | 0 .../lemur_email/templates/expiration.html | 13 +- lemur/schemas.py | 11 + lemur/sources/service.py | 117 ++++++++--- .../angular/certificates/view/view.tpl.html | 16 +- .../static/app/angular/endpoints/services.js | 21 ++ .../static/app/angular/endpoints/view/view.js | 47 +++++ .../app/angular/endpoints/view/view.tpl.html | 108 ++++++++++ lemur/static/app/index.html | 5 +- lemur/static/app/styles/lemur.css | 5 + lemur/tests/factories.py | 19 +- lemur/tests/test_endpoints.py | 87 ++++++++ setup.py | 3 +- 36 files changed, 1140 insertions(+), 187 deletions(-) create mode 100644 lemur/endpoints/__init__.py create mode 100644 lemur/endpoints/models.py create mode 100644 lemur/endpoints/schemas.py create mode 100644 lemur/endpoints/service.py create mode 100644 lemur/endpoints/views.py create mode 100644 lemur/migrations/versions/368320d26c6c_.py create mode 100644 lemur/plugins/lemur_aws/ec2.py create mode 100644 lemur/plugins/lemur_aws/tests/conftest.py create mode 100644 lemur/plugins/lemur_aws/tests/test_elb.py create mode 100644 lemur/plugins/lemur_aws/tests/test_plugin.py create mode 100644 lemur/static/app/angular/endpoints/services.js create mode 100644 lemur/static/app/angular/endpoints/view/view.js create mode 100644 lemur/static/app/angular/endpoints/view/view.tpl.html create mode 100644 lemur/tests/test_endpoints.py diff --git a/gulp/build.js b/gulp/build.js index 1382f157..494bc8a4 100644 --- a/gulp/build.js +++ b/gulp/build.js @@ -79,7 +79,7 @@ gulp.task('dev:styles', function () { 'bower_components/angular-loading-bar/src/loading-bar.css', 'bower_components/angular-ui-switch/angular-ui-switch.css', 'bower_components/angular-wizard/dist/angular-wizard.css', - 'bower_components/ng-table/ng-table.css', + 'bower_components/ng-table/dist/ng-table.css', 'bower_components/angularjs-toaster/toaster.css', 'bower_components/angular-ui-select/dist/select.css', 'lemur/static/app/styles/lemur.css' diff --git a/lemur/__init__.py b/lemur/__init__.py index 6a8de79b..e29d47df 100644 --- a/lemur/__init__.py +++ b/lemur/__init__.py @@ -24,6 +24,7 @@ from lemur.defaults.views import mod as defaults_bp from lemur.plugins.views import mod as plugins_bp from lemur.notifications.views import mod as notifications_bp from lemur.sources.views import mod as sources_bp +from lemur.endpoints.views import mod as endpoints_bp from lemur.__about__ import ( __author__, __copyright__, __email__, __license__, __summary__, __title__, @@ -47,7 +48,8 @@ LEMUR_BLUEPRINTS = ( defaults_bp, plugins_bp, notifications_bp, - sources_bp + sources_bp, + endpoints_bp ) diff --git a/lemur/authorities/service.py b/lemur/authorities/service.py index 43e31421..d976b4a3 100644 --- a/lemur/authorities/service.py +++ b/lemur/authorities/service.py @@ -98,16 +98,6 @@ def create(**kwargs): else: kwargs['roles'] = roles - if kwargs['type'] == 'subca': - description = "This is the ROOT certificate for the {0} sub certificate authority the parent \ - authority is {1}.".format(kwargs.get('name'), kwargs.get('parent')) - else: - description = "This is the ROOT certificate for the {0} certificate authority.".format( - kwargs.get('name') - ) - - kwargs['description'] = description - cert = upload(**kwargs) kwargs['authority_certificate'] = cert diff --git a/lemur/certificates/models.py b/lemur/certificates/models.py index 63e81bec..f75b4207 100644 --- a/lemur/certificates/models.py +++ b/lemur/certificates/models.py @@ -9,8 +9,10 @@ import datetime from flask import current_app -from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean from sqlalchemy.orm import relationship +from sqlalchemy.sql.expression import case +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy import event, Integer, ForeignKey, String, DateTime, PassiveDefault, func, Column, Text, Boolean from lemur.database import db from lemur.models import certificate_associations, certificate_source_associations, \ @@ -73,6 +75,8 @@ class Certificate(db.Model): secondaryjoin=id == certificate_replacement_associations.c.replaced_certificate_id, # noqa backref='replaced') + endpoints = relationship("Endpoint", backref='certificate') + def __init__(self, **kwargs): cert = defaults.parse_certificate(kwargs['body']) @@ -104,22 +108,33 @@ class Certificate(db.Model): for domain in defaults.domains(cert): self.domains.append(Domain(name=domain)) - @property - def is_expired(self): - if self.not_after < datetime.datetime.now(): + @hybrid_property + def expired(self): + if self.not_after <= datetime.datetime.now(): return True - @property - def is_unused(self): - if self.elb_listeners.count() == 0: + @expired.expression + def expired(cls): + return case( + [ + (cls.now_after <= datetime.datetime.now(), True) + ], + else_=False + ) + + @hybrid_property + def revoked(self): + if 'revoked' == self.status: return True - @property - def is_revoked(self): - # we might not yet know the condition of the cert - if self.status: - if 'revoked' in self.status: - return True + @revoked.expression + def revoked(cls): + return case( + [ + (cls.status == 'revoked', True) + ], + else_=False + ) def get_arn(self, account_number): """ diff --git a/lemur/certificates/schemas.py b/lemur/certificates/schemas.py index b352ffe4..53d02bdd 100644 --- a/lemur/certificates/schemas.py +++ b/lemur/certificates/schemas.py @@ -10,7 +10,7 @@ from marshmallow import fields, validates_schema, post_load from marshmallow.exceptions import ValidationError from lemur.schemas import AssociatedAuthoritySchema, AssociatedDestinationSchema, AssociatedCertificateSchema, \ - AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema + AssociatedNotificationSchema, PluginInputSchema, ExtensionSchema, AssociatedRoleSchema, EndpointNestedOutputSchema from lemur.authorities.schemas import AuthorityNestedOutputSchema from lemur.destinations.schemas import DestinationNestedOutputSchema @@ -120,7 +120,7 @@ class CertificateOutputSchema(LemurOutputSchema): replaces = fields.Nested(CertificateNestedOutputSchema, many=True) authority = fields.Nested(AuthorityNestedOutputSchema) roles = fields.Nested(RoleNestedOutputSchema, many=True) - endpoints = fields.List(fields.Dict(), missing=[]) + endpoints = fields.Nested(EndpointNestedOutputSchema, many=True, missing=[]) class CertificateUploadInputSchema(CertificateSchema): diff --git a/lemur/certificates/service.py b/lemur/certificates/service.py index 59d59fc0..76e0285a 100644 --- a/lemur/certificates/service.py +++ b/lemur/certificates/service.py @@ -177,6 +177,7 @@ def upload(**kwargs): """ Allows for pre-made certificates to be imported into Lemur. """ + from lemur.users import service as user_service roles = create_certificate_roles(**kwargs) if kwargs.get('roles'): @@ -187,10 +188,14 @@ def upload(**kwargs): cert = Certificate(**kwargs) cert = database.create(cert) - g.user.certificates.append(cert) - database.update(cert) - return cert + try: + g.user.certificates.append(cert) + except AttributeError: + user = user_service.get_by_email('lemur@nobody') + user.certificates.append(cert) + + return database.update(cert) def create(**kwargs): diff --git a/lemur/common/defaults.py b/lemur/common/defaults.py index 7e6847af..e9545158 100644 --- a/lemur/common/defaults.py +++ b/lemur/common/defaults.py @@ -132,7 +132,10 @@ def bitstrength(cert): :param cert: :return: Integer """ - return cert.public_key().key_size + try: + return cert.public_key().key_size + except AttributeError: + current_app.logger.debug('Unable to get bitstrength.') def issuer(cert): diff --git a/lemur/endpoints/__init__.py b/lemur/endpoints/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/endpoints/models.py b/lemur/endpoints/models.py new file mode 100644 index 00000000..c7a5ec62 --- /dev/null +++ b/lemur/endpoints/models.py @@ -0,0 +1,80 @@ +""" +.. module: lemur.endpoints.models + :platform: unix + :synopsis: This module contains all of the models need to create a authority within Lemur. + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from sqlalchemy.orm import relationship +from sqlalchemy import Column, Integer, String, func, DateTime, PassiveDefault, Boolean, ForeignKey +from sqlalchemy.ext.hybrid import hybrid_property +from sqlalchemy.sql.expression import case + +from lemur.database import db + +from lemur.models import policies_ciphers + + +BAD_CIPHERS = [ + 'Protocol-SSLv3', + 'Protocol-SSLv2', + 'Protocol-TLSv1' +] + + +class Cipher(db.Model): + __tablename__ = 'ciphers' + id = Column(Integer, primary_key=True) + name = Column(String(128), nullable=False) + + @hybrid_property + def deprecated(self): + return self.name in BAD_CIPHERS + + @deprecated.expression + def deprecated(cls): + return case( + [ + (cls.name in BAD_CIPHERS, True) + ], + else_=False + ) + + +class Policy(db.Model): + ___tablename__ = 'policies' + id = Column(Integer, primary_key=True) + name = Column(String(128), nullable=True) + ciphers = relationship('Cipher', secondary=policies_ciphers, backref='policy') + + +class Endpoint(db.Model): + __tablename__ = 'endpoints' + id = Column(Integer, primary_key=True) + owner = Column(String(128)) + name = Column(String(128)) + dnsname = Column(String(256)) + type = Column(String(128)) + active = Column(Boolean, default=True) + port = Column(Integer) + date_created = Column(DateTime, PassiveDefault(func.now()), nullable=False) + policy_id = Column(Integer, ForeignKey('policy.id')) + policy = relationship('Policy', backref='endpoint') + certificate_id = Column(Integer, ForeignKey('certificates.id')) + + @property + def issues(self): + issues = [] + + for cipher in self.policy.ciphers: + if cipher.deprecated: + issues.append({'name': 'deprecated cipher', 'value': '{0} has been deprecated consider removing it.'.format(cipher.name)}) + + if self.certificate.expired: + issues.append({'name': 'expired certificate', 'value': 'There is an expired certificate attached to this endpoint consider replacing it.'}) + + if self.certificate.revoked: + issues.append({'name': 'revoked', 'value': 'There is a revoked certificate attached to this endpoint consider replacing it.'}) + + return issues diff --git a/lemur/endpoints/schemas.py b/lemur/endpoints/schemas.py new file mode 100644 index 00000000..63f37804 --- /dev/null +++ b/lemur/endpoints/schemas.py @@ -0,0 +1,43 @@ +""" +.. module: lemur.endpoints.schemas + :platform: unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from marshmallow import fields + +from lemur.common.schema import LemurOutputSchema +from lemur.certificates.schemas import CertificateNestedOutputSchema + + +class CipherNestedOutputSchema(LemurOutputSchema): + __envelope__ = False + id = fields.Integer() + deprecated = fields.Boolean() + name = fields.String() + + +class PolicyNestedOutputSchema(LemurOutputSchema): + __envelope__ = False + id = fields.Integer() + name = fields.String() + ciphers = fields.Nested(CipherNestedOutputSchema, many=True) + + +class EndpointOutputSchema(LemurOutputSchema): + id = fields.Integer() + description = fields.String() + name = fields.String() + dnsname = fields.String() + owner = fields.Email() + type = fields.String() + port = fields.Integer() + active = fields.Boolean() + certificate = fields.Nested(CertificateNestedOutputSchema) + policy = fields.Nested(PolicyNestedOutputSchema) + + issues = fields.List(fields.Dict()) + +endpoint_output_schema = EndpointOutputSchema() +endpoints_output_schema = EndpointOutputSchema(many=True) diff --git a/lemur/endpoints/service.py b/lemur/endpoints/service.py new file mode 100644 index 00000000..155466e0 --- /dev/null +++ b/lemur/endpoints/service.py @@ -0,0 +1,144 @@ +""" +.. module: lemur.endpoints.service + :platform: Unix + :synopsis: This module contains all of the services level functions used to + administer endpoints in Lemur + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson + +""" +from flask import g + +from lemur import database +from lemur.extensions import metrics +from lemur.endpoints.models import Endpoint, Policy, Cipher + +from sqlalchemy import func + + +def get_all(): + """ + Get all endpoints that are currently in Lemur. + + :rtype : List + :return: + """ + query = database.session_query(Endpoint) + return database.find_all(query, Endpoint, {}).all() + + +def get(endpoint_id): + """ + Retrieves an endpoint given it's ID + + :param endpoint_id: + :return: + """ + return database.get(Endpoint, endpoint_id) + + +def get_by_dnsname(endpoint_dnsname): + """ + Retrieves an endpoint given it's name. + + :param endpoint_dnsname: + :return: + """ + return database.get(Endpoint, endpoint_dnsname, field='dnsname') + + +def create(**kwargs): + """ + Creates a new endpoint. + :param kwargs: + :return: + """ + endpoint = Endpoint(**kwargs) + database.create(endpoint) + metrics.send('endpoint_added', 'counter', 1) + return endpoint + + +def get_or_create_policy(**kwargs): + policy = database.get(Policy, kwargs['name'], field='name') + + if not policy: + policy = Policy(**kwargs) + database.create(policy) + + return policy + + +def get_or_create_cipher(**kwargs): + cipher = database.get(Cipher, kwargs['name'], field='name') + + if not cipher: + cipher = Cipher(**kwargs) + database.create(cipher) + + return cipher + + +def update(endpoint_id, **kwargs): + endpoint = database.get(Endpoint, endpoint_id) + + endpoint.policy = kwargs['policy'] + endpoint.certificate = kwargs['certificate'] + database.update(endpoint) + return endpoint + + +def render(args): + """ + Helper that helps us render the REST Api responses. + :param args: + :return: + """ + query = database.session_query(Endpoint) + filt = args.pop('filter') + + if filt: + terms = filt.split(';') + if 'active' in filt: # this is really weird but strcmp seems to not work here?? + query = query.filter(Endpoint.active == terms[1]) + elif 'port' in filt: + if terms[1] != 'null': # ng-table adds 'null' if a number is removed + query = query.filter(Endpoint.port == terms[1]) + elif 'ciphers' in filt: + query = query.filter( + Cipher.name == terms[1] + ) + else: + query = database.filter(query, Endpoint, terms) + + # we make sure that a user can only use an endpoint they either own are are a member of - admins can see all + if not g.current_user.is_admin: + endpoint_ids = [] + for role in g.current_user.roles: + for endpoint in role.endpoints: + endpoint_ids.append(endpoint.id) + query = query.filter(Endpoint.id.in_(endpoint_ids)) + + return database.sort_and_page(query, Endpoint, args) + + +def stats(**kwargs): + """ + Helper that defines some useful statistics about endpoints. + + :param kwargs: + :return: + """ + attr = getattr(Endpoint, kwargs.get('metric')) + query = database.db.session.query(attr, func.count(attr)) + + items = query.group_by(attr).all() + + keys = [] + values = [] + for key, count in items: + keys.append(key) + values.append(count) + + return {'labels': keys, 'values': values} diff --git a/lemur/endpoints/views.py b/lemur/endpoints/views.py new file mode 100644 index 00000000..d974d8d1 --- /dev/null +++ b/lemur/endpoints/views.py @@ -0,0 +1,106 @@ +""" +.. module: lemur.endpoints.views + :platform: Unix + :copyright: (c) 2015 by Netflix Inc., see AUTHORS for more + :license: Apache, see LICENSE for more details. +.. moduleauthor:: Kevin Glisson +""" +from flask import Blueprint +from flask.ext.restful import reqparse, Api + +from lemur.common.utils import paginated_parser +from lemur.common.schema import validate_schema +from lemur.auth.service import AuthenticatedResource + +from lemur.endpoints import service +from lemur.endpoints.schemas import endpoint_output_schema, endpoints_output_schema + + +mod = Blueprint('endpoints', __name__) +api = Api(mod) + + +class EndpointsList(AuthenticatedResource): + """ Defines the 'endpoints' endpoint """ + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(EndpointsList, self).__init__() + + @validate_schema(None, endpoints_output_schema) + def get(self): + """ + .. http:get:: /endpoints + + The current list of endpoints + + **Example request**: + + .. sourcecode:: http + + GET /endpoints HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + + :query sortBy: field to sort on + :query sortDir: acs or desc + :query page: int default is 1 + :query filter: key value pair. format is k;v + :query limit: limit number default is 10 + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + + :note: this will only show certificates that the current user is authorized to use + """ + parser = paginated_parser.copy() + args = parser.parse_args() + return service.render(args) + + +class Endpoints(AuthenticatedResource): + def __init__(self): + self.reqparse = reqparse.RequestParser() + super(Endpoints, self).__init__() + + @validate_schema(None, endpoint_output_schema) + def get(self, endpoint_id): + """ + .. http:get:: /endpoints/1 + + One endpoint + + **Example request**: + + .. sourcecode:: http + + GET /endpoints/1 HTTP/1.1 + Host: example.com + Accept: application/json, text/javascript + + **Example response**: + + .. sourcecode:: http + + HTTP/1.1 200 OK + Vary: Accept + Content-Type: text/javascript + + + :reqheader Authorization: OAuth token to authenticate + :statuscode 200: no error + :statuscode 403: unauthenticated + """ + return service.get(endpoint_id) + + +api.add_resource(EndpointsList, '/endpoints', endpoint='endpoints') +api.add_resource(Endpoints, '/endpoints/', endpoint='endpoint') diff --git a/lemur/factory.py b/lemur/factory.py index 2854c227..9a13c0e5 100644 --- a/lemur/factory.py +++ b/lemur/factory.py @@ -14,7 +14,7 @@ import imp import errno import pkg_resources -from logging import Formatter +from logging import Formatter, StreamHandler from logging.handlers import RotatingFileHandler from flask import Flask @@ -144,6 +144,10 @@ def configure_logging(app): app.logger.setLevel(app.config.get('LOG_LEVEL', 'DEBUG')) app.logger.addHandler(handler) + stream_handler = StreamHandler() + stream_handler.setLevel(app.config.get('LOG_LEVEL')) + app.logger.addHandler(stream_handler) + def install_plugins(app): """ diff --git a/lemur/manage.py b/lemur/manage.py index be6ba7bd..86e6b8df 100755 --- a/lemur/manage.py +++ b/lemur/manage.py @@ -189,7 +189,8 @@ def generate_settings(): @manager.option('-s', '--sources', dest='labels') -def sync(labels): +@manager.option('-t', '--type', dest='type') +def sync(labels, type): """ Attempts to run several methods Certificate discovery. This is run on a periodic basis and updates the Lemur datastore with the @@ -212,7 +213,7 @@ def sync(labels): while not sync_lock.i_am_locking(): try: - sync_lock.acquire(timeout=10) # wait up to 10 seconds + sync_lock.acquire(timeout=2) # wait up to 10 seconds sys.stdout.write("[+] Staring to sync sources: {labels}!\n".format(labels=labels)) labels = labels.split(",") @@ -220,7 +221,7 @@ def sync(labels): if labels[0] == 'all': source_sync() else: - source_sync(labels=labels) + source_sync(labels=labels, type=type) sys.stdout.write( "[+] Finished syncing sources. Run Time: {time}\n".format( diff --git a/lemur/migrations/versions/368320d26c6c_.py b/lemur/migrations/versions/368320d26c6c_.py new file mode 100644 index 00000000..7c31f43f --- /dev/null +++ b/lemur/migrations/versions/368320d26c6c_.py @@ -0,0 +1,48 @@ +"""empty message + +Revision ID: 368320d26c6c +Revises: 3307381f3b88 +Create Date: 2016-05-27 13:41:47.413694 + +""" + +# revision identifiers, used by Alembic. +revision = '368320d26c6c' +down_revision = '3307381f3b88' + +from alembic import op +import sqlalchemy as sa +import sqlalchemy_utils + + +def upgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.create_table('endpoints', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('owner', sa.String(length=128), nullable=True), + sa.Column('name', sa.String(length=128), nullable=True), + sa.Column('dnsname', sa.String(length=256), nullable=True), + sa.Column('type', sa.String(length=128), nullable=True), + sa.Column('active', sa.Boolean(), nullable=True), + sa.Column('port', sa.Integer(), nullable=True), + sa.Column('date_created', sa.DateTime(), server_default=sa.text(u'now()'), nullable=False), + sa.Column('certificate_id', sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(['certificate_id'], ['certificates.id'], ), + sa.PrimaryKeyConstraint('id') + ) + op.create_table('policy', + sa.Column('id', sa.Integer(), nullable=False), + sa.Column('endpoint_id', sa.Integer(), nullable=True), + sa.Column('name', sa.String(length=32), nullable=True), + sa.Column('ciphers', sqlalchemy_utils.types.json.JSONType(), nullable=True), + sa.ForeignKeyConstraint(['endpoint_id'], ['endpoints.id'], ), + sa.PrimaryKeyConstraint('id') + ) + ### end Alembic commands ### + + +def downgrade(): + ### commands auto generated by Alembic - please adjust! ### + op.drop_table('policy') + op.drop_table('endpoints') + ### end Alembic commands ### diff --git a/lemur/models.py b/lemur/models.py index 4068240a..60328c75 100644 --- a/lemur/models.py +++ b/lemur/models.py @@ -63,9 +63,9 @@ roles_authorities = db.Table('roles_authorities', Index('roles_authorities_ix', roles_authorities.c.authority_id, roles_authorities.c.role_id) roles_certificates = db.Table('roles_certificates', - Column('certificate_id', Integer, ForeignKey('certificates.id')), - Column('role_id', Integer, ForeignKey('roles.id')) - ) + Column('certificate_id', Integer, ForeignKey('certificates.id')), + Column('role_id', Integer, ForeignKey('roles.id')) + ) Index('roles_certificates_ix', roles_certificates.c.certificate_id, roles_certificates.c.role_id) @@ -76,3 +76,10 @@ roles_users = db.Table('roles_users', ) Index('roles_users_ix', roles_users.c.user_id, roles_users.c.role_id) + + +policies_ciphers = db.Table('policies_ciphers', + Column('cipher_id', Integer, ForeignKey('ciphers.id')), + Column('policy_id', Integer, ForeignKey('policy.id'))) + +Index('policies_ciphers_ix', policies_ciphers.c.cipher_id, policies_ciphers.c.policy_id) diff --git a/lemur/notifications/service.py b/lemur/notifications/service.py index 12fe0162..7baf2c74 100644 --- a/lemur/notifications/service.py +++ b/lemur/notifications/service.py @@ -45,6 +45,7 @@ def _get_message_data(cert): cert_dict['owner'] = cert.owner cert_dict['name'] = cert.name cert_dict['body'] = cert.body + cert_dict['endpoints'] = [{'name': x.name, 'dnsname': x.dnsname} for x in cert.endpoints] return cert_dict diff --git a/lemur/plugins/lemur_aws/ec2.py b/lemur/plugins/lemur_aws/ec2.py new file mode 100644 index 00000000..03266fa6 --- /dev/null +++ b/lemur/plugins/lemur_aws/ec2.py @@ -0,0 +1,23 @@ +""" +.. module: lemur.plugins.lemur_aws.elb + :synopsis: Module contains some often used and helpful classes that + are used to deal with ELBs + +.. moduleauthor:: Kevin Glisson +""" +from lemur.plugins.lemur_aws.sts import sts_client + + +@sts_client('ec2') +def get_regions(**kwargs): + regions = kwargs['client'].describe_regions() + return [x['RegionName'] for x in regions['Regions']] + + +@sts_client('ec2') +def get_all_instances(**kwargs): + """ + Fetches all instance objects for a given account and region. + """ + paginator = kwargs['client'].get_paginator('describe_instances') + return paginator.paginate() diff --git a/lemur/plugins/lemur_aws/elb.py b/lemur/plugins/lemur_aws/elb.py index 20958f12..045beab6 100644 --- a/lemur/plugins/lemur_aws/elb.py +++ b/lemur/plugins/lemur_aws/elb.py @@ -5,12 +5,10 @@ .. moduleauthor:: Kevin Glisson """ -import boto.ec2 - from flask import current_app from lemur.exceptions import InvalidListener -from lemur.plugins.lemur_aws.sts import assume_service +from lemur.plugins.lemur_aws.sts import sts_client, assume_service def is_valid(listener_tuple): @@ -38,41 +36,34 @@ def is_valid(listener_tuple): return listener_tuple -def get_all_regions(): - """ - Retrieves all current EC2 regions. - - :return: - """ - regions = [] - for r in boto.ec2.regions(): - regions.append(r.name) - return regions - - -def get_all_elbs(account_number, region): +@sts_client('elb') +def get_all_elbs(**kwargs): """ Fetches all elb objects for a given account and region. - - :param account_number: - :param region: """ - marker = None - elbs = [] - return assume_service(account_number, 'elb', region).get_all_load_balancers() -# TODO create pull request for boto to include elb marker support -# while True: -# app.logger.debug(response.__dict__) -# raise Exception -# result = response['list_server_certificates_response']['list_server_certificates_result'] -# -# for elb in result['server_certificate_metadata_list']: -# elbs.append(elb) -# -# if result['is_truncated'] == 'true': -# marker = result['marker'] -# else: -# return elbs + return kwargs['client'].describe_load_balancers() + + +@sts_client('elb') +def describe_load_balancer_policies(load_balancer_name, policy_names, **kwargs): + """ + Fetching all policies currently associated with an ELB. + + :param load_balancer_name: + :return: + """ + return kwargs['client'].describe_load_balancer_policies(LoadBalancerName=load_balancer_name, PolicyNames=policy_names) + + +@sts_client('elb') +def describe_load_balancer_types(policies, **kwargs): + """ + Describe the policies with policy details. + + :param policies: + :return: + """ + return kwargs['client'].describe_load_balancer_policy_types(PolicyTypeNames=policies) def attach_certificate(account_number, region, name, port, certificate_id): @@ -89,67 +80,67 @@ def attach_certificate(account_number, region, name, port, certificate_id): return assume_service(account_number, 'elb', region).set_lb_listener_SSL_certificate(name, port, certificate_id) -def create_new_listeners(account_number, region, name, listeners=None): - """ - Creates a new listener and attaches it to the ELB. - - :param account_number: - :param region: - :param name: - :param listeners: - :return: - """ - listeners = [is_valid(x) for x in listeners] - return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners) - - -def update_listeners(account_number, region, name, listeners, ports): - """ - We assume that a listener with a specified port already exists. We can then - delete the old listener on the port and create a new one in it's place. - - If however we are replacing a listener e.g. changing a port from 80 to 443 we need - to make sure we kept track of which ports we needed to delete so that we don't create - two listeners (one 80 and one 443) - - :param account_number: - :param region: - :param name: - :param listeners: - :param ports: - """ - # you cannot update a listeners port/protocol instead we remove the only one and - # create a new one in it's place - listeners = [is_valid(x) for x in listeners] - - assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports) - return create_new_listeners(account_number, region, name, listeners=listeners) - - -def delete_listeners(account_number, region, name, ports): - """ - Deletes a listener from an ELB. - - :param account_number: - :param region: - :param name: - :param ports: - :return: - """ - return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports) - - -def get_listeners(account_number, region, name): - """ - Gets the listeners configured on an elb and returns a array of tuples - - :param account_number: - :param region: - :param name: - :return: list of tuples - """ - - conn = assume_service(account_number, 'elb', region) - elbs = conn.get_all_load_balancers(load_balancer_names=[name]) - if elbs: - return elbs[0].listeners +# def create_new_listeners(account_number, region, name, listeners=None): +# """ +# Creates a new listener and attaches it to the ELB. +# +# :param account_number: +# :param region: +# :param name: +# :param listeners: +# :return: +# """ +# listeners = [is_valid(x) for x in listeners] +# return assume_service(account_number, 'elb', region).create_load_balancer_listeners(name, listeners=listeners) +# +# +# def update_listeners(account_number, region, name, listeners, ports): +# """ +# We assume that a listener with a specified port already exists. We can then +# delete the old listener on the port and create a new one in it's place. +# +# If however we are replacing a listener e.g. changing a port from 80 to 443 we need +# to make sure we kept track of which ports we needed to delete so that we don't create +# two listeners (one 80 and one 443) +# +# :param account_number: +# :param region: +# :param name: +# :param listeners: +# :param ports: +# """ +# # you cannot update a listeners port/protocol instead we remove the only one and +# # create a new one in it's place +# listeners = [is_valid(x) for x in listeners] +# +# assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports) +# return create_new_listeners(account_number, region, name, listeners=listeners) +# +# +# def delete_listeners(account_number, region, name, ports): +# """ +# Deletes a listener from an ELB. +# +# :param account_number: +# :param region: +# :param name: +# :param ports: +# :return: +# """ +# return assume_service(account_number, 'elb', region).delete_load_balancer_listeners(name, ports) +# +# +# def get_listeners(account_number, region, name): +# """ +# Gets the listeners configured on an elb and returns a array of tuples +# +# :param account_number: +# :param region: +# :param name: +# :return: list of tuples +# """ +# +# conn = assume_service(account_number, 'elb', region) +# elbs = conn.get_all_load_balancers(load_balancer_names=[name]) +# if elbs: +# return elbs[0].listeners diff --git a/lemur/plugins/lemur_aws/plugin.py b/lemur/plugins/lemur_aws/plugin.py index 97d011d0..7eaa2d25 100644 --- a/lemur/plugins/lemur_aws/plugin.py +++ b/lemur/plugins/lemur_aws/plugin.py @@ -6,18 +6,16 @@ .. moduleauthor:: Kevin Glisson """ +from flask import current_app from boto.exception import BotoServerError + from lemur.plugins.bases import DestinationPlugin, SourcePlugin -from lemur.plugins.lemur_aws import iam, elb +from lemur.plugins.lemur_aws import iam +from lemur.plugins.lemur_aws.ec2 import get_regions +from lemur.plugins.lemur_aws.elb import get_all_elbs, describe_load_balancer_policies, attach_certificate from lemur.plugins import lemur_aws as aws -def find_value(name, options): - for o in options: - if o['name'] == name: - return o['value'] - - class AWSDestinationPlugin(DestinationPlugin): title = 'AWS' slug = 'aws-destination' @@ -45,14 +43,14 @@ class AWSDestinationPlugin(DestinationPlugin): def upload(self, name, body, private_key, cert_chain, options, **kwargs): if private_key: try: - iam.upload_cert(find_value('accountNumber', options), name, body, private_key, cert_chain=cert_chain) + iam.upload_cert(self.get_option('accountNumber', options), name, body, private_key, cert_chain=cert_chain) except BotoServerError as e: if e.error_code != 'EntityAlreadyExists': raise Exception(e) - e = find_value('elb', options) + e = self.get_option('elb', options) if e: - elb.attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId']) + attach_certificate(kwargs['accountNumber'], ['region'], e['name'], e['port'], e['certificateId']) else: raise Exception("Unable to upload to AWS, private key is required") @@ -60,7 +58,7 @@ class AWSDestinationPlugin(DestinationPlugin): class AWSSourcePlugin(SourcePlugin): title = 'AWS' slug = 'aws-source' - description = 'Discovers all SSL certificates in an AWS account' + description = 'Discovers all SSL certificates and ELB endpoints in an AWS account' version = aws.VERSION author = 'Kevin Glisson' @@ -74,11 +72,16 @@ class AWSSourcePlugin(SourcePlugin): 'validation': '/^[0-9]{12,12}$/', 'helpMessage': 'Must be a valid AWS account number!', }, + { + 'name': 'regions', + 'type': 'str', + 'helpMessage': 'Comma separated list of regions to search in, if no region is specified we look in all regions.' + }, ] def get_certificates(self, options, **kwargs): certs = [] - arns = iam.get_all_server_certs(find_value('accountNumber', options)) + arns = iam.get_all_server_certs(self.get_option('accountNumber', options)) for arn in arns: cert_body, cert_chain = iam.get_cert_from_arn(arn) cert_name = iam.get_name_from_arn(arn) @@ -89,3 +92,57 @@ class AWSSourcePlugin(SourcePlugin): ) certs.append(cert) return certs + + def get_endpoints(self, options, **kwargs): + endpoints = [] + account_number = self.get_option('accountNumber', options) + regions = self.get_option('regions', options) + + if not regions: + regions = get_regions(account_number=account_number) + else: + regions = regions.split(',') + + for region in regions: + elbs = get_all_elbs(account_number=account_number, region=region) + current_app.logger.info("Describing load balancers in {0}-{1}".format(account_number, region)) + for elb in elbs['LoadBalancerDescriptions']: + for listener in elb['ListenerDescriptions']: + if not listener['Listener'].get('SSLCertificateId'): + continue + + endpoint = dict( + name=elb['LoadBalancerName'], + dnsname=elb['DNSName'], + type='elb', + port=listener['Listener']['LoadBalancerPort'], + certificate_name=iam.get_name_from_arn(listener['Listener']['SSLCertificateId']) + ) + + if listener['PolicyNames']: + policy = describe_load_balancer_policies(elb['LoadBalancerName'], listener['PolicyNames'], account_number=account_number, region=region) + endpoint['policy'] = format_elb_cipher_policy(policy) + + endpoints.append(endpoint) + + return endpoints + + +def format_elb_cipher_policy(policy): + """ + Attempts to format cipher policy information into a common format. + :param policy: + :return: + """ + ciphers = [] + name = None + for descr in policy['PolicyDescriptions']: + for attr in descr['PolicyAttributeDescriptions']: + if attr['AttributeName'] == 'Reference-Security-Policy': + name = attr['AttributeValue'] + continue + + if attr['AttributeValue'] == 'true': + ciphers.append(attr['AttributeName']) + + return dict(name=name, ciphers=ciphers) diff --git a/lemur/plugins/lemur_aws/sts.py b/lemur/plugins/lemur_aws/sts.py index 843cf0f8..ab58b776 100644 --- a/lemur/plugins/lemur_aws/sts.py +++ b/lemur/plugins/lemur_aws/sts.py @@ -5,13 +5,16 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ +from functools import wraps + import boto import boto.ec2.elb +import boto3 from flask import current_app -def assume_service(account_number, service, region=None): +def assume_service(account_number, service, region='us-east-1'): conn = boto.connect_sts() role = conn.assume_role('arn:aws:iam::{0}:role/{1}'.format( @@ -35,3 +38,40 @@ def assume_service(account_number, service, region=None): aws_access_key_id=role.credentials.access_key, aws_secret_access_key=role.credentials.secret_key, security_token=role.credentials.session_token) + + +def sts_client(service, service_type='client'): + def decorator(f): + @wraps(f) + def decorated_function(*args, **kwargs): + sts = boto3.client('sts') + arn = 'arn:aws:iam::{0}:role/{1}'.format( + kwargs.pop('account_number'), + current_app.config.get('LEMUR_INSTANCE_PROFILE', 'Lemur') + ) + # TODO add user specific information to RoleSessionName + role = sts.assume_role(RoleArn=arn, RoleSessionName='lemur') + + if service_type == 'client': + client = boto3.client( + service, + region_name=kwargs.pop('region', 'us-east-1'), + aws_access_key_id=role['Credentials']['AccessKeyId'], + aws_secret_access_key=role['Credentials']['SecretAccessKey'], + aws_session_token=role['Credentials']['SessionToken'] + ) + kwargs['client'] = client + elif service_type == 'resource': + resource = boto3.resource( + service, + region_name=kwargs.pop('region', 'us-east-1'), + aws_access_key_id=role['Credentials']['AccessKeyId'], + aws_secret_access_key=role['Credentials']['SecretAccessKey'], + aws_session_token=role['Credentials']['SessionToken'] + ) + kwargs['resource'] = resource + return f(*args, **kwargs) + + return decorated_function + + return decorator diff --git a/lemur/plugins/lemur_aws/tests/conftest.py b/lemur/plugins/lemur_aws/tests/conftest.py new file mode 100644 index 00000000..0e1cd89f --- /dev/null +++ b/lemur/plugins/lemur_aws/tests/conftest.py @@ -0,0 +1 @@ +from lemur.tests.conftest import * # noqa diff --git a/lemur/plugins/lemur_aws/tests/test_elb.py b/lemur/plugins/lemur_aws/tests/test_elb.py new file mode 100644 index 00000000..c040d60c --- /dev/null +++ b/lemur/plugins/lemur_aws/tests/test_elb.py @@ -0,0 +1,14 @@ +import boto +from moto import mock_sts, mock_elb + + +@mock_sts() +@mock_elb() +def test_get_all_elbs(app): + from lemur.plugins.lemur_aws.elb import get_all_elbs + conn = boto.ec2.elb.connect_to_region('us-east-1') + elbs = get_all_elbs(account_number='123456789012', region='us-east-1') + assert not elbs['LoadBalancerDescriptions'] + conn.create_load_balancer('example-lb', ['us-east-1a', 'us-east-1b'], [(443, 5443, 'tcp')]) + elbs = get_all_elbs(account_number='123456789012', region='us-east-1') + assert elbs['LoadBalancerDescriptions'] diff --git a/lemur/plugins/lemur_aws/tests/test_plugin.py b/lemur/plugins/lemur_aws/tests/test_plugin.py new file mode 100644 index 00000000..e69de29b diff --git a/lemur/plugins/lemur_email/templates/expiration.html b/lemur/plugins/lemur_email/templates/expiration.html index c24e355c..a9c4569c 100644 --- a/lemur/plugins/lemur_email/templates/expiration.html +++ b/lemur/plugins/lemur_email/templates/expiration.html @@ -23,7 +23,7 @@ - @@ -83,12 +83,15 @@ - {% if not loop.last %} diff --git a/lemur/schemas.py b/lemur/schemas.py index bd4a674a..0bea9335 100644 --- a/lemur/schemas.py +++ b/lemur/schemas.py @@ -210,3 +210,14 @@ class ExtensionSchema(BaseExtensionSchema): authority_key_identifier = fields.Nested(AuthorityKeyIdentifierSchema) certificate_info_access = fields.Nested(CertificateInfoAccessSchema) custom = fields.List(fields.Nested(CustomOIDSchema)) + + +class EndpointNestedOutputSchema(LemurOutputSchema): + __envelope__ = False + id = fields.Integer() + description = fields.String() + name = fields.String() + dnsname = fields.String() + owner = fields.Email() + type = fields.String() + active = fields.Boolean() diff --git a/lemur/sources/service.py b/lemur/sources/service.py index c5381da9..d2c0d18f 100644 --- a/lemur/sources/service.py +++ b/lemur/sources/service.py @@ -5,12 +5,15 @@ :license: Apache, see LICENSE for more details. .. moduleauthor:: Kevin Glisson """ +import datetime + from flask import current_app from lemur import database from lemur.sources.models import Source from lemur.certificates.models import Certificate from lemur.certificates import service as cert_service +from lemur.endpoints import service as endpoint_service from lemur.destinations import service as destination_service from lemur.plugins.base import plugins @@ -37,7 +40,7 @@ def _disassociate_certs_from_source(current_certificates, found_certificates, so c.sources.delete(s) -def sync_create(certificate, source): +def certificate_create(certificate, source): cert = cert_service.import_certificate(**certificate) cert.description = "This certificate was automatically discovered by Lemur" cert.sources.append(source) @@ -45,7 +48,7 @@ def sync_create(certificate, source): database.update(cert) -def sync_update(certificate, source): +def certificate_update(certificate, source): for s in certificate.sources: if s.label == source.label: break @@ -66,40 +69,102 @@ def sync_update_destination(certificate, source): certificate.destinations.append(dest) -def sync(labels=None): +def sync_endpoints(source): + new, updated = 0, 0 + current_app.logger.debug("Retrieving endpoints from {0}".format(source.label)) + s = plugins.get(source.plugin_name) + + try: + endpoints = s.get_endpoints(source.options) + except NotImplementedError: + current_app.logger.warning("Unable to sync endpoints for source {0} plugin has not implemented 'get_endpoints'".format(source.label)) + return + + for endpoint in endpoints: + exists = endpoint_service.get_by_dnsname(endpoint['dnsname']) + + certificate_name = endpoint.pop('certificate_name', None) + certificate = endpoint.pop('certificate', None) + + if certificate_name: + cert = cert_service.get_by_name(certificate_name) + + elif certificate: + cert = cert_service.get_by_body(certificate['body']) + if not cert: + cert = cert_service.import_certificate(**certificate) + + if not cert: + current_app.logger.error("Unable to find associated certificate, be sure that certificates are sync'ed before endpoints") + continue + + endpoint['certificate'] = cert + + policy = endpoint.pop('policy') + + policy_ciphers = [] + for nc in policy['ciphers']: + policy_ciphers.append(endpoint_service.get_or_create_cipher(name=nc)) + + policy['ciphers'] = policy_ciphers + endpoint['policy'] = endpoint_service.get_or_create_policy(**policy) + + if not exists: + endpoint_service.create(**endpoint) + new += 1 + + else: + endpoint_service.update(exists.id, **endpoint) + updated += 1 + + +def sync_certificates(source): new, updated = 0, 0 c_certificates = cert_service.get_all_certs() + current_app.logger.debug("Retrieving certificates from {0}".format(source.label)) + s = plugins.get(source.plugin_name) + certificates = s.get_certificates(source.options) + + for certificate in certificates: + exists = cert_service.find_duplicates(certificate['body']) + + if not exists: + certificate_create(certificate, source) + new += 1 + + # check to make sure that existing certificates have the current source associated with it + elif len(exists) == 1: + certificate_update(exists[0], source) + updated += 1 + else: + current_app.logger.warning( + "Multiple certificates found, attempt to deduplicate the following certificates: {0}".format( + ",".join([x.name for x in exists]) + ) + ) + + # we need to try and find the absent of certificates so we can properly disassociate them when they are deleted + _disassociate_certs_from_source(c_certificates, certificates, source) + + +def sync(labels=None, type=None): for source in database.get_all(Source, True, field='active'): # we should be able to specify, individual sources to sync if labels: if source.label not in labels: continue - current_app.logger.debug("Retrieving certificates from {0}".format(source.label)) - s = plugins.get(source.plugin_name) - certificates = s.get_certificates(source.options) + if type == 'endpoints': + sync_endpoints(source) + elif type == 'certificates': + sync_certificates(source) + else: + sync_certificates(source) + sync_endpoints(source) - for certificate in certificates: - exists = cert_service.find_duplicates(certificate['body']) - - if not exists: - sync_create(certificate, source) - new += 1 - - # check to make sure that existing certificates have the current source associated with it - elif len(exists) == 1: - sync_update(exists[0], source) - updated += 1 - else: - current_app.logger.warning( - "Multiple certificates found, attempt to deduplicate the following certificates: {0}".format( - ",".join([x.name for x in exists]) - ) - ) - - # we need to try and find the absent of certificates so we can properly disassociate them when they are deleted - _disassociate_certs_from_source(c_certificates, certificates, source) + source.last_run = datetime.datetime.utcnow() + database.update(source) def create(label, plugin_name, options, description=None): diff --git a/lemur/static/app/angular/certificates/view/view.tpl.html b/lemur/static/app/angular/certificates/view/view.tpl.html index c9d9a062..5d9174e6 100644 --- a/lemur/static/app/angular/certificates/view/view.tpl.html +++ b/lemur/static/app/angular/certificates/view/view.tpl.html @@ -57,7 +57,7 @@
+ Lemur
{{ message.name }}
{{ message.owner }} +
+ {{ message.name }} +
+ + {{ message.endpoints | length }} Endpoints +
{{ message.owner }}
{{ message.not_after | time }} Details -
+
- + Basic Info
    @@ -114,6 +114,18 @@
+ + Endpoints +
    +
  • + +
      +
    • {{ endpoint.name }}
    • +
    • {{ endpoint.dnsname }}
    • +
    +
  • +
+
Notifications
    @@ -158,7 +170,7 @@
- + Chain diff --git a/lemur/static/app/angular/endpoints/services.js b/lemur/static/app/angular/endpoints/services.js new file mode 100644 index 00000000..62a2b0de --- /dev/null +++ b/lemur/static/app/angular/endpoints/services.js @@ -0,0 +1,21 @@ +'use strict'; +angular.module('lemur') + .service('EndpointApi', function (LemurRestangular) { + return LemurRestangular.all('endpoints'); + }) + .service('EndpointService', function ($location, EndpointApi) { + var EndpointService = this; + EndpointService.findEndpointsByName = function (filterValue) { + return EndpointApi.getList({'filter[label]': filterValue}) + .then(function (endpoints) { + return endpoints; + }); + }; + + EndpointService.getCertificates = function (endpoint) { + endpoint.getList('certificates').then(function (certificates) { + endpoint.certificates = certificates; + }); + }; + return EndpointService; + }); diff --git a/lemur/static/app/angular/endpoints/view/view.js b/lemur/static/app/angular/endpoints/view/view.js new file mode 100644 index 00000000..78c215a7 --- /dev/null +++ b/lemur/static/app/angular/endpoints/view/view.js @@ -0,0 +1,47 @@ +'use strict'; + +angular.module('lemur') + + .config(function config($stateProvider) { + $stateProvider.state('endpoints', { + url: '/endpoints', + templateUrl: '/angular/endpoints/view/view.tpl.html', + controller: 'EndpointsViewController' + }); + }) + + .controller('EndpointsViewController', function ($q, $scope, $uibModal, EndpointApi, EndpointService, MomentService, ngTableParams) { + $scope.filter = {}; + $scope.endpointsTable = new ngTableParams({ + page: 1, // show first page + count: 10, // count per page + sorting: { + id: 'desc' // initial sorting + }, + filter: $scope.filter + }, { + total: 0, // length of data + getData: function ($defer, params) { + EndpointApi.getList(params.url()).then( + function (data) { + params.total(data.total); + $defer.resolve(data); + } + ); + } + }); + + + $scope.ciphers = [ + {'title': 'Protocol-SSLv2', 'id': 'Protocol-SSLv2'}, + {'title': 'Protocol-SSLv3', 'id': 'Protocol-SSLv3'}, + {'title': 'Protocol-TLSv1', 'id': 'Protocol-TLSv1'}, + {'title': 'Protocol-TLSv1.1', 'id': 'Protocol-TLSv1.1'}, + {'title': 'Protocol-TLSv1.1', 'id': 'Protocol-TLSv1.2'}, + ]; + + $scope.momentService = MomentService; + + $scope.endpointService = EndpointService; + + }); diff --git a/lemur/static/app/angular/endpoints/view/view.tpl.html b/lemur/static/app/angular/endpoints/view/view.tpl.html new file mode 100644 index 00000000..dc2c6155 --- /dev/null +++ b/lemur/static/app/angular/endpoints/view/view.tpl.html @@ -0,0 +1,108 @@ +
+
+

Endpoints + 443 or bust

+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + +
+ + + +
    +
  • {{ endpoint.name }}
  • +
  • {{ endpoint.dnsname }}
  • +
+
+ {{ endpoint.port }} + +
    +
  • +
+
+
+ +
+
+ + + Certificate +
    +
  • + Name + + {{ endpoint.certificate.name }} + +
  • +
  • + Not Before + + {{ momentService.createMoment(endpoint.certificate.notBefore) }} + +
  • +
  • + Not After + + {{ momentService.createMoment(endpoint.certificate.notAfter) }} + +
  • +
  • + Description +

    {{ endpoint.certificate.description }}

    +
  • +
+
+ + + Issues + +
    +
  • +
      +
    • {{ issue.name | titleCase }}
    • +
    • {{ issue.value }}
    • +
    +
  • +
+
+
+ + + + Ciphers/Protocols + +
    +
  • + {{ cipher.name }} +
  • +
+
+
+
+
+
+
+
diff --git a/lemur/static/app/index.html b/lemur/static/app/index.html index af515fc9..3dcfeb8c 100644 --- a/lemur/static/app/index.html +++ b/lemur/static/app/index.html @@ -51,13 +51,14 @@
  • Dashboard
  • Certificates
  • Authorities
  • +
  • Endpoints
  • Notifications
  • -
  • Destinations
  • -
  • Sources