2015-08-25 01:17:04 +02:00
|
|
|
from __future__ import unicode_literals # at top of module
|
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
import arrow
|
2016-07-27 21:41:32 +02:00
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from collections import Counter
|
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
import os
|
|
|
|
import sys
|
|
|
|
import base64
|
2015-08-02 03:31:38 +02:00
|
|
|
import time
|
2015-09-02 18:19:06 +02:00
|
|
|
import requests
|
|
|
|
import json
|
2016-07-27 21:41:32 +02:00
|
|
|
|
|
|
|
from tabulate import tabulate
|
2015-06-22 22:47:27 +02:00
|
|
|
from gunicorn.config import make_settings
|
|
|
|
|
2015-07-03 19:30:17 +02:00
|
|
|
from cryptography.fernet import Fernet
|
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
from flask import current_app
|
2015-08-02 03:31:38 +02:00
|
|
|
from flask.ext.script import Manager, Command, Option, prompt_pass
|
2015-06-22 22:47:27 +02:00
|
|
|
from flask.ext.migrate import Migrate, MigrateCommand, stamp
|
|
|
|
from flask_script.commands import ShowUrls, Clean, Server
|
|
|
|
|
|
|
|
from lemur import database
|
2016-11-11 22:28:01 +01:00
|
|
|
from lemur.extensions import metrics
|
2015-06-22 22:47:27 +02:00
|
|
|
from lemur.users import service as user_service
|
|
|
|
from lemur.roles import service as role_service
|
|
|
|
from lemur.certificates import service as cert_service
|
2016-07-27 21:41:32 +02:00
|
|
|
from lemur.authorities import service as authority_service
|
2015-08-02 04:08:46 +02:00
|
|
|
from lemur.notifications import service as notification_service
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
from lemur.certificates.verify import verify_string
|
2016-07-28 22:08:24 +02:00
|
|
|
from lemur.sources import service as source_service
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
from lemur import create_app
|
|
|
|
|
|
|
|
# Needed to be imported so that SQLAlchemy create_all can find our models
|
2015-07-21 22:06:13 +02:00
|
|
|
from lemur.users.models import User # noqa
|
|
|
|
from lemur.roles.models import Role # noqa
|
|
|
|
from lemur.authorities.models import Authority # noqa
|
|
|
|
from lemur.certificates.models import Certificate # noqa
|
|
|
|
from lemur.destinations.models import Destination # noqa
|
|
|
|
from lemur.domains.models import Domain # noqa
|
2015-07-30 02:13:06 +02:00
|
|
|
from lemur.notifications.models import Notification # noqa
|
2015-08-02 00:29:34 +02:00
|
|
|
from lemur.sources.models import Source # noqa
|
2015-07-30 02:13:06 +02:00
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
manager = Manager(create_app)
|
|
|
|
manager.add_option('-c', '--config', dest='config')
|
|
|
|
|
|
|
|
migrate = Migrate(create_app)
|
|
|
|
|
|
|
|
KEY_LENGTH = 40
|
|
|
|
DEFAULT_CONFIG_PATH = '~/.lemur/lemur.conf.py'
|
|
|
|
DEFAULT_SETTINGS = 'lemur.conf.server'
|
|
|
|
SETTINGS_ENVVAR = 'LEMUR_CONF'
|
|
|
|
|
|
|
|
|
|
|
|
CONFIG_TEMPLATE = """
|
|
|
|
# This is just Python which means you can inherit and tweak settings
|
|
|
|
|
|
|
|
import os
|
|
|
|
_basedir = os.path.abspath(os.path.dirname(__file__))
|
|
|
|
|
|
|
|
THREADS_PER_PAGE = 8
|
|
|
|
|
2015-07-22 19:51:55 +02:00
|
|
|
# General
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
# These will need to be set to `True` if you are developing locally
|
|
|
|
CORS = False
|
|
|
|
debug = False
|
|
|
|
|
2015-07-21 18:50:33 +02:00
|
|
|
# this is the secret key used by flask session management
|
|
|
|
SECRET_KEY = '{flask_secret_key}'
|
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
# You should consider storing these separately from your config
|
2015-07-22 19:51:55 +02:00
|
|
|
LEMUR_TOKEN_SECRET = '{secret_token}'
|
2015-10-10 02:17:05 +02:00
|
|
|
LEMUR_ENCRYPTION_KEYS = '{encryption_key}'
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
# this is a list of domains as regexes that only admins can issue
|
|
|
|
LEMUR_RESTRICTED_DOMAINS = []
|
|
|
|
|
2015-07-22 19:51:55 +02:00
|
|
|
# Mail Server
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
LEMUR_EMAIL = ''
|
|
|
|
LEMUR_SECURITY_TEAM_EMAIL = []
|
|
|
|
|
2015-08-27 21:59:40 +02:00
|
|
|
# Certificate Defaults
|
|
|
|
|
|
|
|
LEMUR_DEFAULT_COUNTRY = ''
|
|
|
|
LEMUR_DEFAULT_STATE = ''
|
|
|
|
LEMUR_DEFAULT_LOCATION = ''
|
|
|
|
LEMUR_DEFAULT_ORGANIZATION = ''
|
|
|
|
LEMUR_DEFAULT_ORGANIZATIONAL_UNIT = ''
|
|
|
|
|
Define ACTIVE_PROVIDERS in default config
The configuration item ACTIVE_PROVIDERS must be initialized
Workaround for this error:
2015-12-30 13:58:48,073 ERROR: Internal Error [in /www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py:299]
Traceback (most recent call last):
File "/www/lemur/local/lib/python2.7/site-packages/flask/app.py", line 1475, in full_dispatch_request
rv = self.dispatch_request()
File "/www/lemur/local/lib/python2.7/site-packages/flask/app.py", line 1461, in dispatch_request
return self.view_functions[rule.endpoint](**req.view_args)
File "/www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py", line 462, in wrapper
resp = resource(*args, **kwargs)
File "/www/lemur/local/lib/python2.7/site-packages/flask/views.py", line 84, in view
return self.dispatch_request(*args, **kwargs)
File "/www/lemur/local/lib/python2.7/site-packages/flask_restful/__init__.py", line 572, in dispatch_request
resp = meth(*args, **kwargs)
File "/www/lemur/lemur/auth/views.py", line 276, in get
for provider in current_app.config.get("ACTIVE_PROVIDERS"):
TypeError: 'NoneType' object is not iterable
2015-12-30 14:56:59 +01:00
|
|
|
# Authentication Providers
|
|
|
|
ACTIVE_PROVIDERS = []
|
2015-08-27 21:59:40 +02:00
|
|
|
|
2015-07-22 19:51:55 +02:00
|
|
|
# Logging
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
LOG_LEVEL = "DEBUG"
|
|
|
|
LOG_FILE = "lemur.log"
|
|
|
|
|
|
|
|
|
2015-07-22 19:51:55 +02:00
|
|
|
# Database
|
2015-06-22 22:47:27 +02:00
|
|
|
|
2015-07-22 19:51:55 +02:00
|
|
|
# modify this if you are not using a local database
|
|
|
|
SQLALCHEMY_DATABASE_URI = 'postgresql://lemur:lemur@localhost:5432/lemur'
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
|
2015-07-22 19:51:55 +02:00
|
|
|
# AWS
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
#LEMUR_INSTANCE_PROFILE = 'Lemur'
|
|
|
|
|
2015-07-23 22:46:54 +02:00
|
|
|
# Issuers
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
# These will be dependent on which 3rd party that Lemur is
|
|
|
|
# configured to use.
|
|
|
|
|
2015-07-23 22:46:54 +02:00
|
|
|
# VERISIGN_URL = ''
|
|
|
|
# VERISIGN_PEM_PATH = ''
|
|
|
|
# VERISIGN_FIRST_NAME = ''
|
|
|
|
# VERISIGN_LAST_NAME = ''
|
|
|
|
# VERSIGN_EMAIL = ''
|
2015-06-22 22:47:27 +02:00
|
|
|
"""
|
|
|
|
|
2015-07-21 22:06:13 +02:00
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
@MigrateCommand.command
|
|
|
|
def create():
|
|
|
|
database.db.create_all()
|
|
|
|
stamp(revision='head')
|
|
|
|
|
|
|
|
|
2015-07-11 02:06:57 +02:00
|
|
|
@MigrateCommand.command
|
|
|
|
def drop_all():
|
|
|
|
database.db.drop_all()
|
|
|
|
|
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
@manager.command
|
|
|
|
def check_revoked():
|
|
|
|
"""
|
|
|
|
Function attempts to update Lemur's internal cache with revoked
|
|
|
|
certificates. This is called periodically by Lemur. It checks both
|
|
|
|
CRLs and OCSP to see if a certificate is revoked. If Lemur is unable
|
|
|
|
encounters an issue with verification it marks the certificate status
|
|
|
|
as `unknown`.
|
|
|
|
"""
|
|
|
|
for cert in cert_service.get_all_certs():
|
2015-09-02 18:12:05 +02:00
|
|
|
try:
|
|
|
|
if cert.chain:
|
|
|
|
status = verify_string(cert.body, cert.chain)
|
|
|
|
else:
|
|
|
|
status = verify_string(cert.body, "")
|
|
|
|
|
|
|
|
cert.status = 'valid' if status else 'invalid'
|
|
|
|
except Exception as e:
|
|
|
|
cert.status = 'unknown'
|
2015-06-22 22:47:27 +02:00
|
|
|
database.update(cert)
|
|
|
|
|
|
|
|
|
|
|
|
@manager.shell
|
|
|
|
def make_shell_context():
|
|
|
|
"""
|
|
|
|
Creates a python REPL with several default imports
|
|
|
|
in the context of the current_app
|
|
|
|
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
return dict(current_app=current_app)
|
|
|
|
|
|
|
|
|
|
|
|
def generate_settings():
|
|
|
|
"""
|
|
|
|
This command is run when ``default_path`` doesn't exist, or ``init`` is
|
|
|
|
run and returns a string representing the default data to put into their
|
|
|
|
settings file.
|
|
|
|
"""
|
|
|
|
output = CONFIG_TEMPLATE.format(
|
2015-10-10 02:17:05 +02:00
|
|
|
# we use Fernet.generate_key to make sure that the key length is
|
|
|
|
# compatible with Fernet
|
|
|
|
encryption_key=Fernet.generate_key(),
|
2015-07-21 18:50:33 +02:00
|
|
|
secret_token=base64.b64encode(os.urandom(KEY_LENGTH)),
|
|
|
|
flask_secret_key=base64.b64encode(os.urandom(KEY_LENGTH)),
|
2015-06-22 22:47:27 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
return output
|
|
|
|
|
|
|
|
|
2015-08-02 18:14:27 +02:00
|
|
|
@manager.command
|
|
|
|
def notify():
|
|
|
|
"""
|
|
|
|
Runs Lemur's notification engine, that looks for expired certificates and sends
|
|
|
|
notifications out to those that bave subscribed to them.
|
|
|
|
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
sys.stdout.write("Starting to notify subscribers about expiring certificates!\n")
|
|
|
|
count = notification_service.send_expiration_notifications()
|
|
|
|
sys.stdout.write(
|
|
|
|
"Finished notifying subscribers about expiring certificates! Sent {count} notifications!\n".format(
|
|
|
|
count=count
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
class InitializeApp(Command):
|
|
|
|
"""
|
2015-07-11 02:06:57 +02:00
|
|
|
This command will bootstrap our database with any destinations as
|
2015-06-22 22:47:27 +02:00
|
|
|
specified by our config.
|
|
|
|
|
|
|
|
Additionally a Lemur user will be created as a default user
|
|
|
|
and be used when certificates are discovered by Lemur.
|
|
|
|
"""
|
2015-09-03 23:20:51 +02:00
|
|
|
option_list = (
|
2015-09-04 01:45:49 +02:00
|
|
|
Option('-p', '--password', dest='password'),
|
2015-09-03 23:20:51 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, password):
|
2015-06-22 22:47:27 +02:00
|
|
|
create()
|
|
|
|
user = user_service.get_by_username("lemur")
|
|
|
|
|
|
|
|
if not user:
|
2015-09-03 23:20:51 +02:00
|
|
|
if not password:
|
|
|
|
sys.stdout.write("We need to set Lemur's password to continue!\n")
|
2015-09-09 02:42:57 +02:00
|
|
|
password = prompt_pass("Password")
|
|
|
|
password1 = prompt_pass("Confirm Password")
|
2015-09-03 23:20:51 +02:00
|
|
|
|
2015-09-09 02:42:57 +02:00
|
|
|
if password != password1:
|
2015-09-03 23:20:51 +02:00
|
|
|
sys.stderr.write("[!] Passwords do not match!\n")
|
|
|
|
sys.exit(1)
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
role = role_service.get_by_name('admin')
|
|
|
|
|
|
|
|
if role:
|
|
|
|
sys.stdout.write("[-] Admin role already created, skipping...!\n")
|
|
|
|
else:
|
|
|
|
# we create an admin role
|
|
|
|
role = role_service.create('admin', description='this is the lemur administrator role')
|
|
|
|
sys.stdout.write("[+] Created 'admin' role\n")
|
|
|
|
|
2015-09-09 02:42:57 +02:00
|
|
|
user_service.create("lemur", password, 'lemur@nobody', True, None, [role])
|
2015-06-22 22:47:27 +02:00
|
|
|
sys.stdout.write("[+] Added a 'lemur' user and added it to the 'admin' role!\n")
|
|
|
|
|
|
|
|
else:
|
|
|
|
sys.stdout.write("[-] Default user has already been created, skipping...!\n")
|
|
|
|
|
2015-08-02 14:10:50 +02:00
|
|
|
sys.stdout.write("[+] Creating expiration email notifications!\n")
|
2015-09-09 03:18:14 +02:00
|
|
|
sys.stdout.write("[!] Using {0} as specified by LEMUR_SECURITY_TEAM_EMAIL for notifications\n".format("LEMUR_SECURITY_TEAM_EMAIL"))
|
2015-08-02 04:08:46 +02:00
|
|
|
|
2015-09-09 02:56:20 +02:00
|
|
|
intervals = current_app.config.get("LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS", [])
|
2015-08-02 14:10:50 +02:00
|
|
|
sys.stdout.write(
|
|
|
|
"[!] Creating {num} notifications for {intervals} days as specified by LEMUR_DEFAULT_EXPIRATION_NOTIFICATION_INTERVALS\n".format(
|
|
|
|
num=len(intervals),
|
|
|
|
intervals=",".join([str(x) for x in intervals])
|
2015-08-02 04:08:46 +02:00
|
|
|
)
|
2015-08-02 14:10:50 +02:00
|
|
|
)
|
2015-08-02 04:08:46 +02:00
|
|
|
|
2015-08-02 14:10:50 +02:00
|
|
|
recipients = current_app.config.get('LEMUR_SECURITY_TEAM_EMAIL')
|
|
|
|
notification_service.create_default_expiration_notifications("DEFAULT_SECURITY", recipients=recipients)
|
2015-08-02 04:08:46 +02:00
|
|
|
|
2015-06-22 22:47:27 +02:00
|
|
|
sys.stdout.write("[/] Done!\n")
|
|
|
|
|
|
|
|
|
|
|
|
class CreateUser(Command):
|
|
|
|
"""
|
2016-05-19 19:07:15 +02:00
|
|
|
This command allows for the creation of a new user within Lemur.
|
2015-06-22 22:47:27 +02:00
|
|
|
"""
|
|
|
|
option_list = (
|
|
|
|
Option('-u', '--username', dest='username', required=True),
|
|
|
|
Option('-e', '--email', dest='email', required=True),
|
|
|
|
Option('-a', '--active', dest='active', default=True),
|
2015-10-26 18:59:20 +01:00
|
|
|
Option('-r', '--roles', dest='roles', action='append', default=[])
|
2015-06-22 22:47:27 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, username, email, active, roles):
|
|
|
|
role_objs = []
|
|
|
|
for r in roles:
|
|
|
|
role_obj = role_service.get_by_name(r)
|
|
|
|
if role_obj:
|
|
|
|
role_objs.append(role_obj)
|
|
|
|
else:
|
2016-05-19 19:07:15 +02:00
|
|
|
sys.stderr.write("[!] Cannot find role {0}\n".format(r))
|
2015-06-22 22:47:27 +02:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
password1 = prompt_pass("Password")
|
|
|
|
password2 = prompt_pass("Confirm Password")
|
|
|
|
|
|
|
|
if password1 != password2:
|
2016-05-19 19:07:15 +02:00
|
|
|
sys.stderr.write("[!] Passwords do not match!\n")
|
2015-06-22 22:47:27 +02:00
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
user_service.create(username, password1, email, active, None, role_objs)
|
2016-05-19 19:07:15 +02:00
|
|
|
sys.stdout.write("[+] Created new user: {0}\n".format(username))
|
|
|
|
|
|
|
|
|
|
|
|
class ResetPassword(Command):
|
|
|
|
"""
|
|
|
|
This command allows you to reset a user's password.
|
|
|
|
"""
|
|
|
|
option_list = (
|
|
|
|
Option('-u', '--username', dest='username', required=True),
|
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, username):
|
|
|
|
user = user_service.get_by_username(username)
|
|
|
|
|
|
|
|
if not user:
|
|
|
|
sys.stderr.write("[!] No user found for username: {0}\n".format(username))
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
sys.stderr.write("[+] Resetting password for {0}\n".format(username))
|
|
|
|
password1 = prompt_pass("Password")
|
|
|
|
password2 = prompt_pass("Confirm Password")
|
|
|
|
|
|
|
|
if password1 != password2:
|
|
|
|
sys.stderr.write("[!] Passwords do not match\n")
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
user.password = password1
|
|
|
|
user.hash_password()
|
|
|
|
database.commit()
|
2015-06-22 22:47:27 +02:00
|
|
|
|
|
|
|
|
|
|
|
class CreateRole(Command):
|
|
|
|
"""
|
|
|
|
This command allows for the creation of a new role within Lemur
|
|
|
|
"""
|
|
|
|
option_list = (
|
|
|
|
Option('-n', '--name', dest='name', required=True),
|
|
|
|
Option('-u', '--users', dest='users', default=[]),
|
|
|
|
Option('-d', '--description', dest='description', required=True)
|
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, name, users, description):
|
|
|
|
user_objs = []
|
|
|
|
for u in users:
|
|
|
|
user_obj = user_service.get_by_username(u)
|
|
|
|
if user_obj:
|
|
|
|
user_objs.append(user_obj)
|
|
|
|
else:
|
|
|
|
sys.stderr.write("[!] Cannot find user {0}".format(u))
|
|
|
|
sys.exit(1)
|
|
|
|
role_service.create(name, description=description, users=users)
|
|
|
|
sys.stdout.write("[+] Created new role: {0}".format(name))
|
|
|
|
|
|
|
|
|
|
|
|
class LemurServer(Command):
|
|
|
|
"""
|
|
|
|
This is the main Lemur server, it runs the flask app with gunicorn and
|
|
|
|
uses any configuration options passed to it.
|
|
|
|
|
|
|
|
|
|
|
|
You can pass all standard gunicorn flags to this command as if you were
|
|
|
|
running gunicorn itself.
|
|
|
|
|
|
|
|
For example:
|
|
|
|
|
|
|
|
lemur start -w 4 -b 127.0.0.0:8002
|
|
|
|
|
|
|
|
Will start gunicorn with 4 workers bound to 127.0.0.0:8002
|
|
|
|
"""
|
|
|
|
description = 'Run the app within Gunicorn'
|
|
|
|
|
|
|
|
def get_options(self):
|
|
|
|
settings = make_settings()
|
|
|
|
options = (
|
|
|
|
Option(*klass.cli, action=klass.action)
|
2016-05-05 21:52:08 +02:00
|
|
|
for setting, klass in settings.items() if klass.cli
|
2015-06-22 22:47:27 +02:00
|
|
|
)
|
|
|
|
|
|
|
|
return options
|
|
|
|
|
|
|
|
def run(self, *args, **kwargs):
|
|
|
|
from gunicorn.app.wsgiapp import WSGIApplication
|
|
|
|
|
|
|
|
app = WSGIApplication()
|
|
|
|
app.app_uri = 'lemur:create_app(config="{0}")'.format(kwargs.get('config'))
|
|
|
|
|
|
|
|
return app.run()
|
|
|
|
|
|
|
|
|
2015-07-02 22:49:31 +02:00
|
|
|
@manager.command
|
|
|
|
def create_config(config_path=None):
|
|
|
|
"""
|
|
|
|
Creates a new configuration file if one does not already exist
|
|
|
|
"""
|
|
|
|
if not config_path:
|
|
|
|
config_path = DEFAULT_CONFIG_PATH
|
|
|
|
|
|
|
|
config_path = os.path.expanduser(config_path)
|
|
|
|
dir = os.path.dirname(config_path)
|
|
|
|
if not os.path.exists(dir):
|
|
|
|
os.makedirs(dir)
|
|
|
|
|
|
|
|
config = generate_settings()
|
|
|
|
with open(config_path, 'w') as f:
|
|
|
|
f.write(config)
|
|
|
|
|
2015-07-03 19:30:17 +02:00
|
|
|
sys.stdout.write("[+] Created a new configuration file {0}\n".format(config_path))
|
|
|
|
|
|
|
|
|
|
|
|
@manager.command
|
|
|
|
def lock(path=None):
|
|
|
|
"""
|
|
|
|
Encrypts a given path. This directory can be used to store secrets needed for normal
|
|
|
|
Lemur operation. This is especially useful for storing secrets needed for communication
|
|
|
|
with third parties (e.g. external certificate authorities).
|
|
|
|
|
|
|
|
Lemur does not assume anything about the contents of the directory and will attempt to
|
|
|
|
encrypt all files contained within. Currently this has only been tested against plain
|
|
|
|
text files.
|
|
|
|
|
|
|
|
Path defaults ~/.lemur/keys
|
|
|
|
|
|
|
|
:param: path
|
|
|
|
"""
|
|
|
|
if not path:
|
|
|
|
path = os.path.expanduser('~/.lemur/keys')
|
|
|
|
|
|
|
|
dest_dir = os.path.join(path, "encrypted")
|
|
|
|
sys.stdout.write("[!] Generating a new key...\n")
|
|
|
|
|
|
|
|
key = Fernet.generate_key()
|
|
|
|
|
|
|
|
if not os.path.exists(dest_dir):
|
|
|
|
sys.stdout.write("[+] Creating encryption directory: {0}\n".format(dest_dir))
|
|
|
|
os.makedirs(dest_dir)
|
|
|
|
|
|
|
|
for root, dirs, files in os.walk(os.path.join(path, 'decrypted')):
|
|
|
|
for f in files:
|
|
|
|
source = os.path.join(root, f)
|
|
|
|
dest = os.path.join(dest_dir, f + ".enc")
|
|
|
|
with open(source, 'rb') as in_file, open(dest, 'wb') as out_file:
|
|
|
|
f = Fernet(key)
|
|
|
|
data = f.encrypt(in_file.read())
|
|
|
|
out_file.write(data)
|
|
|
|
sys.stdout.write("[+] Writing file: {0} Source: {1}\n".format(dest, source))
|
|
|
|
|
|
|
|
sys.stdout.write("[+] Keys have been encrypted with key {0}\n".format(key))
|
|
|
|
|
|
|
|
|
|
|
|
@manager.command
|
|
|
|
def unlock(path=None):
|
|
|
|
"""
|
|
|
|
Decrypts all of the files in a given directory with provided password.
|
|
|
|
This is most commonly used during the startup sequence of Lemur
|
|
|
|
allowing it to go from source code to something that can communicate
|
|
|
|
with external services.
|
|
|
|
|
|
|
|
Path defaults ~/.lemur/keys
|
|
|
|
|
|
|
|
:param: path
|
|
|
|
"""
|
|
|
|
key = prompt_pass("[!] Please enter the encryption password")
|
|
|
|
|
|
|
|
if not path:
|
|
|
|
path = os.path.expanduser('~/.lemur/keys')
|
|
|
|
|
|
|
|
dest_dir = os.path.join(path, "decrypted")
|
|
|
|
source_dir = os.path.join(path, "encrypted")
|
|
|
|
|
|
|
|
if not os.path.exists(dest_dir):
|
|
|
|
sys.stdout.write("[+] Creating decryption directory: {0}\n".format(dest_dir))
|
|
|
|
os.makedirs(dest_dir)
|
|
|
|
|
|
|
|
for root, dirs, files in os.walk(source_dir):
|
|
|
|
for f in files:
|
|
|
|
source = os.path.join(source_dir, f)
|
|
|
|
dest = os.path.join(dest_dir, ".".join(f.split(".")[:-1]))
|
|
|
|
with open(source, 'rb') as in_file, open(dest, 'wb') as out_file:
|
|
|
|
f = Fernet(key)
|
|
|
|
data = f.decrypt(in_file.read())
|
|
|
|
out_file.write(data)
|
|
|
|
sys.stdout.write("[+] Writing file: {0} Source: {1}\n".format(dest, source))
|
|
|
|
|
|
|
|
sys.stdout.write("[+] Keys have been unencrypted!\n")
|
2015-07-02 22:49:31 +02:00
|
|
|
|
|
|
|
|
2015-08-25 01:37:24 +02:00
|
|
|
def unicode_(data):
|
|
|
|
import sys
|
|
|
|
|
|
|
|
if sys.version_info.major < 3:
|
2015-08-25 05:10:03 +02:00
|
|
|
return data.decode('UTF-8')
|
2015-08-25 01:37:24 +02:00
|
|
|
return data
|
|
|
|
|
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
def print_certificate_details(details):
|
2015-12-18 05:17:27 +01:00
|
|
|
"""
|
2016-11-18 20:27:46 +01:00
|
|
|
Print the certificate details with formatting.
|
|
|
|
:param details:
|
|
|
|
:return:
|
2015-12-18 05:17:27 +01:00
|
|
|
"""
|
2016-11-18 20:27:46 +01:00
|
|
|
sys.stdout.write("[+] Re-issuing certificate with the following details: \n")
|
|
|
|
sys.stdout.write(
|
|
|
|
"[+] Common Name: {common_name}\n"
|
|
|
|
"[+] Subject Alternate Names: {sans}\n"
|
|
|
|
"[+] Authority: {authority_name}\n"
|
|
|
|
"[+] Validity Start: {validity_start}\n"
|
|
|
|
"[+] Validity End: {validity_end}\n"
|
|
|
|
"[+] Organization: {organization}\n"
|
|
|
|
"[+] Organizational Unit: {organizational_unit}\n"
|
|
|
|
"[+] Country: {country}\n"
|
|
|
|
"[+] State: {state}\n"
|
|
|
|
"[+] Location: {location}\n".format(
|
|
|
|
common_name=details['common_name'],
|
|
|
|
sans=",".join(x['value'] for x in details['extensions']['sub_alt_names']['names']),
|
|
|
|
authority_name=details['authority'].name,
|
|
|
|
validity_start=details['validity_start'].isoformat(),
|
|
|
|
validity_end=details['validity_end'].isoformat(),
|
|
|
|
organization=details['organization'],
|
|
|
|
organizational_unit=details['organizational_unit'],
|
|
|
|
country=details['country'],
|
|
|
|
state=details['state'],
|
|
|
|
location=details['location']
|
|
|
|
)
|
2015-12-18 05:17:27 +01:00
|
|
|
)
|
|
|
|
|
2016-01-10 23:20:36 +01:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
class RotateCertificate(Command):
|
2015-08-20 01:10:45 +02:00
|
|
|
"""
|
2016-11-18 20:27:46 +01:00
|
|
|
Rotates certificate on all endpoints managed by Lemur.
|
2015-08-20 01:10:45 +02:00
|
|
|
"""
|
|
|
|
option_list = (
|
2016-11-18 20:27:46 +01:00
|
|
|
Option('-n', '--cert-name', dest='new_cert_name'),
|
|
|
|
Option('-o', '--old-cert-name', dest='old_cert_name', required=True),
|
|
|
|
Option('-c', '--commit', dest='commit', action='store_true', default=False)
|
2015-08-20 01:10:45 +02:00
|
|
|
)
|
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
def run(self, new_cert_name, old_cert_name, commit):
|
|
|
|
from lemur.certificates.service import get_by_name, reissue_certificate, get_certificate_primitives
|
|
|
|
from lemur.endpoints.service import rotate_certificate
|
2015-08-20 01:10:45 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
old_cert = get_by_name(old_cert_name)
|
2015-08-20 01:10:45 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if not old_cert:
|
|
|
|
sys.stdout.write("[-] No certificate found with name: {0}\n".format(old_cert_name))
|
2015-08-28 00:48:49 +02:00
|
|
|
sys.exit(1)
|
2015-08-21 00:45:42 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if new_cert_name:
|
|
|
|
new_cert = get_by_name(new_cert_name)
|
2015-08-28 00:48:49 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if not new_cert:
|
|
|
|
sys.stdout.write("[-] No certificate found with name: {0}\n".format(old_cert_name))
|
2015-08-28 00:48:49 +02:00
|
|
|
sys.exit(1)
|
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if commit:
|
|
|
|
sys.stdout.write("[!] Running in COMMIT mode.\n")
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if not new_cert_name:
|
|
|
|
sys.stdout.write("[!] No new certificate provided. Attempting to re-issue old certificate: {0}.\n".format(old_cert_name))
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
details = get_certificate_primitives(old_cert)
|
|
|
|
print_certificate_details(details)
|
2015-08-28 00:48:49 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if commit:
|
|
|
|
new_cert = reissue_certificate(old_cert, replace=True)
|
|
|
|
sys.stdout.write("[+] Issued new certificate named: {0}\n".format(new_cert.name))
|
2015-08-28 00:48:49 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
sys.stdout.write("[+] Done! \n")
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if len(old_cert.endpoints) > 0:
|
|
|
|
for endpoint in old_cert.endpoints:
|
|
|
|
sys.stdout.write("[+] Certificate found deployed onto {0}\n ".format(endpoint.name))
|
|
|
|
sys.stdout.write("[+] Rotating certificate from: {0} to: {1}\n ".format(old_cert_name, new_cert.name))
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if commit:
|
|
|
|
rotate_certificate(endpoint, new_cert_name)
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
sys.stdout.write("[+] Done! \n")
|
|
|
|
else:
|
|
|
|
sys.stdout.write("[!] Certificate not found on any existing endpoints. Nothing to rotate.\n")
|
2015-08-21 00:45:42 +02:00
|
|
|
|
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
class ReissueCertificate(Command):
|
|
|
|
"""
|
|
|
|
Reissues a certificate based on a given certificate.
|
|
|
|
"""
|
|
|
|
option_list = (
|
|
|
|
Option('-o', '--old-cert-name', dest='old_cert_name', required=True),
|
|
|
|
Option('-c', '--commit', dest='commit', action='store_true', default=False)
|
|
|
|
)
|
2015-08-21 00:45:42 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
def run(self, old_cert_name, commit):
|
|
|
|
from lemur.certificates.service import get_by_name, reissue_certificate, get_certificate_primitives
|
2015-08-25 01:37:24 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
old_cert = get_by_name(old_cert_name)
|
2015-08-25 01:37:24 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if not old_cert:
|
|
|
|
sys.stdout.write("[-] No certificate found with name: {0}\n".format(old_cert_name))
|
2015-08-28 00:48:49 +02:00
|
|
|
sys.exit(1)
|
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if commit:
|
|
|
|
sys.stdout.write("[!] Running in COMMIT mode.\n")
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
details = get_certificate_primitives(old_cert)
|
|
|
|
print_certificate_details(details)
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
if commit:
|
|
|
|
new_cert = reissue_certificate(old_cert, replace=True)
|
|
|
|
sys.stdout.write("[+] Issued new certificate named: {0}\n".format(new_cert.name))
|
2015-08-24 21:18:15 +02:00
|
|
|
|
2016-11-18 20:27:46 +01:00
|
|
|
sys.stdout.write("[+] Done! \n")
|
2015-08-20 01:10:45 +02:00
|
|
|
|
|
|
|
|
2015-09-02 18:19:06 +02:00
|
|
|
@manager.command
|
|
|
|
def publish_verisign_units():
|
|
|
|
"""
|
|
|
|
Simple function that queries verisign for API units and posts the mertics to
|
|
|
|
Atlas API for other teams to consume.
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
from lemur.plugins import plugins
|
|
|
|
v = plugins.get('verisign-issuer')
|
|
|
|
units = v.get_available_units()
|
|
|
|
|
|
|
|
metrics = {}
|
|
|
|
for item in units:
|
|
|
|
if item['@type'] in metrics.keys():
|
|
|
|
metrics[item['@type']] += int(item['@remaining'])
|
|
|
|
else:
|
|
|
|
metrics.update({item['@type']: int(item['@remaining'])})
|
|
|
|
|
|
|
|
for name, value in metrics.items():
|
|
|
|
metric = [
|
|
|
|
{
|
|
|
|
"timestamp": 1321351651,
|
|
|
|
"type": "GAUGE",
|
|
|
|
"name": "Symantec {0} Unit Count".format(name),
|
|
|
|
"tags": {},
|
|
|
|
"value": value
|
|
|
|
}
|
|
|
|
]
|
|
|
|
|
|
|
|
requests.post('http://localhost:8078/metrics', data=json.dumps(metric))
|
|
|
|
|
|
|
|
|
2016-11-01 22:24:45 +01:00
|
|
|
@manager.command
|
|
|
|
def publish_unapproved_verisign_certificates():
|
2016-01-12 00:26:32 +01:00
|
|
|
"""
|
2016-11-01 22:24:45 +01:00
|
|
|
Query the Verisign for any certificates that need to be approved.
|
|
|
|
:return:
|
2016-01-12 00:26:32 +01:00
|
|
|
"""
|
2016-11-01 22:24:45 +01:00
|
|
|
from lemur.plugins import plugins
|
|
|
|
from lemur.extensions import metrics
|
|
|
|
v = plugins.get('verisign-issuer')
|
|
|
|
certs = v.get_pending_certificates()
|
|
|
|
metrics.send('pending_certificates', 'gauge', certs)
|
2016-01-12 00:26:32 +01:00
|
|
|
|
|
|
|
|
2016-07-27 21:41:32 +02:00
|
|
|
class Report(Command):
|
|
|
|
"""
|
|
|
|
Defines a set of reports to be run periodically against Lemur.
|
|
|
|
"""
|
|
|
|
option_list = (
|
|
|
|
Option('-n', '--name', dest='name', default=None, help='Name of the report to run.'),
|
|
|
|
Option('-d', '--duration', dest='duration', default=356, help='Number of days to run the report'),
|
|
|
|
)
|
|
|
|
|
|
|
|
def run(self, name, duration):
|
|
|
|
end = datetime.utcnow()
|
|
|
|
start = end - timedelta(days=duration)
|
2016-11-18 20:27:46 +01:00
|
|
|
|
|
|
|
if name == 'authority':
|
|
|
|
self.certificates_issued(name, start, end)
|
|
|
|
|
|
|
|
elif name == 'activeFQDNS':
|
|
|
|
self.active_fqdns()
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def active_fqdns():
|
|
|
|
"""
|
|
|
|
Generates a report that gives the number of active fqdns, but root domain.
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
from lemur.certificates.service import get_all_certs
|
|
|
|
sys.stdout.write("FQDN, Root Domain, Issuer, Total Length (days), Time until expiration (days)\n")
|
|
|
|
for cert in get_all_certs():
|
|
|
|
if not cert.expired:
|
|
|
|
now = arrow.utcnow()
|
|
|
|
ttl = now - cert.not_before
|
|
|
|
total_length = cert.not_after - cert.not_before
|
|
|
|
|
|
|
|
for fqdn in cert.domains:
|
|
|
|
root_domain = ".".join(fqdn.name.split('.')[-2:])
|
|
|
|
sys.stdout.write(", ".join([fqdn.name, root_domain, cert.issuer, str(total_length.days), str(ttl.days)]) + "\n")
|
2016-07-27 21:41:32 +02:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def certificates_issued(name=None, start=None, end=None):
|
|
|
|
"""
|
|
|
|
Generates simple report of number of certificates issued by the authority, if no authority
|
|
|
|
is specified report on total number of certificates.
|
|
|
|
|
|
|
|
:param name:
|
|
|
|
:param start:
|
|
|
|
:param end:
|
|
|
|
:return:
|
|
|
|
"""
|
|
|
|
|
|
|
|
def _calculate_row(authority):
|
|
|
|
day_cnt = Counter()
|
|
|
|
month_cnt = Counter()
|
|
|
|
year_cnt = Counter()
|
|
|
|
|
|
|
|
for cert in authority.certificates:
|
|
|
|
date = cert.date_created.date()
|
|
|
|
day_cnt[date.day] += 1
|
|
|
|
month_cnt[date.month] += 1
|
|
|
|
year_cnt[date.year] += 1
|
|
|
|
|
|
|
|
try:
|
|
|
|
day_avg = int(sum(day_cnt.values()) / len(day_cnt.keys()))
|
|
|
|
except ZeroDivisionError:
|
|
|
|
day_avg = 0
|
|
|
|
|
|
|
|
try:
|
|
|
|
month_avg = int(sum(month_cnt.values()) / len(month_cnt.keys()))
|
|
|
|
except ZeroDivisionError:
|
|
|
|
month_avg = 0
|
|
|
|
|
|
|
|
try:
|
|
|
|
year_avg = int(sum(year_cnt.values()) / len(year_cnt.keys()))
|
|
|
|
except ZeroDivisionError:
|
|
|
|
year_avg = 0
|
|
|
|
|
|
|
|
return [authority.name, authority.description, day_avg, month_avg, year_avg]
|
|
|
|
|
|
|
|
rows = []
|
|
|
|
if not name:
|
|
|
|
for authority in authority_service.get_all():
|
|
|
|
rows.append(_calculate_row(authority))
|
|
|
|
|
|
|
|
else:
|
|
|
|
authority = authority_service.get_by_name(name)
|
|
|
|
|
|
|
|
if not authority:
|
|
|
|
sys.stderr.write('[!] Authority {0} was not found.'.format(name))
|
|
|
|
sys.exit(1)
|
|
|
|
|
|
|
|
rows.append(_calculate_row(authority))
|
|
|
|
|
|
|
|
sys.stdout.write(tabulate(rows, headers=["Authority Name", "Description", "Daily Average", "Monthy Average", "Yearly Average"]) + "\n")
|
|
|
|
|
|
|
|
|
2016-07-28 22:08:24 +02:00
|
|
|
class Sources(Command):
|
|
|
|
"""
|
|
|
|
Defines a set of actions to take against Lemur's sources.
|
|
|
|
"""
|
|
|
|
option_list = (
|
2016-11-17 23:47:10 +01:00
|
|
|
Option('-s', '--sources', dest='source_strings', action='append', help='Sources to operate on.', required=True),
|
|
|
|
Option('-a', '--action', choices=['sync', 'clean'], dest='action', help='Action to take on source.', required=True)
|
2016-07-28 22:08:24 +02:00
|
|
|
)
|
|
|
|
|
2016-11-16 22:23:35 +01:00
|
|
|
def run(self, source_strings, action):
|
|
|
|
sources = []
|
|
|
|
if not source_strings:
|
2016-07-28 22:08:24 +02:00
|
|
|
table = []
|
|
|
|
for source in source_service.get_all():
|
|
|
|
table.append([source.label, source.active, source.description])
|
|
|
|
|
|
|
|
sys.stdout.write(tabulate(table, headers=['Label', 'Active', 'Description']))
|
|
|
|
sys.exit(1)
|
|
|
|
|
2016-11-16 22:23:35 +01:00
|
|
|
elif 'all' in source_strings:
|
|
|
|
sources = source_service.get_all()
|
2016-07-28 22:08:24 +02:00
|
|
|
|
2016-11-16 22:23:35 +01:00
|
|
|
else:
|
|
|
|
for source_str in source_strings:
|
|
|
|
source = source_service.get_by_label(source_str)
|
|
|
|
|
|
|
|
if not source:
|
|
|
|
sys.stderr.write("Unable to find specified source with label: {0}".format(source_str))
|
2016-07-28 22:08:24 +02:00
|
|
|
|
2016-11-16 22:23:35 +01:00
|
|
|
sources.append(source)
|
|
|
|
|
|
|
|
for source in sources:
|
2016-07-28 22:08:24 +02:00
|
|
|
if action == 'sync':
|
|
|
|
self.sync(source)
|
|
|
|
|
|
|
|
if action == 'clean':
|
|
|
|
self.clean(source)
|
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def sync(source):
|
|
|
|
start_time = time.time()
|
|
|
|
sys.stdout.write("[+] Staring to sync source: {label}!\n".format(label=source.label))
|
2016-11-11 22:28:01 +01:00
|
|
|
|
2016-11-16 22:23:35 +01:00
|
|
|
user = user_service.get_by_username('lemur')
|
|
|
|
|
2016-11-11 22:28:01 +01:00
|
|
|
try:
|
2016-11-16 22:23:35 +01:00
|
|
|
source_service.sync(source, user)
|
2016-11-11 22:28:01 +01:00
|
|
|
sys.stdout.write(
|
|
|
|
"[+] Finished syncing source: {label}. Run Time: {time}\n".format(
|
|
|
|
label=source.label,
|
|
|
|
time=(time.time() - start_time)
|
|
|
|
)
|
2016-07-28 22:08:24 +02:00
|
|
|
)
|
2016-11-11 22:28:01 +01:00
|
|
|
except Exception as e:
|
|
|
|
current_app.logger.exception(e)
|
|
|
|
|
|
|
|
sys.stdout.write(
|
2016-11-16 22:23:35 +01:00
|
|
|
"[X] Failed syncing source {label}!\n".format(label=source.label)
|
2016-11-11 22:28:01 +01:00
|
|
|
)
|
|
|
|
|
2016-11-16 22:23:35 +01:00
|
|
|
metrics.send('sync_failed', 'counter', 1, metric_tags={'source': source.label})
|
2016-07-28 22:08:24 +02:00
|
|
|
|
|
|
|
@staticmethod
|
|
|
|
def clean(source):
|
|
|
|
start_time = time.time()
|
|
|
|
sys.stdout.write("[+] Staring to clean source: {label}!\n".format(label=source.label))
|
|
|
|
source_service.clean(source)
|
|
|
|
sys.stdout.write(
|
|
|
|
"[+] Finished cleaning source: {label}. Run Time: {time}\n".format(
|
|
|
|
label=source.label,
|
|
|
|
time=(time.time() - start_time)
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
2015-07-02 23:12:39 +02:00
|
|
|
def main():
|
2015-06-22 22:47:27 +02:00
|
|
|
manager.add_command("start", LemurServer())
|
2016-07-04 22:03:46 +02:00
|
|
|
manager.add_command("runserver", Server(host='127.0.0.1', threaded=True))
|
2015-06-22 22:47:27 +02:00
|
|
|
manager.add_command("clean", Clean())
|
|
|
|
manager.add_command("show_urls", ShowUrls())
|
|
|
|
manager.add_command("db", MigrateCommand)
|
|
|
|
manager.add_command("init", InitializeApp())
|
2015-07-21 01:13:42 +02:00
|
|
|
manager.add_command("create_user", CreateUser())
|
2016-05-19 19:07:15 +02:00
|
|
|
manager.add_command("reset_password", ResetPassword())
|
2015-07-21 01:13:42 +02:00
|
|
|
manager.add_command("create_role", CreateRole())
|
2016-07-28 22:08:24 +02:00
|
|
|
manager.add_command("sources", Sources())
|
2016-07-27 21:41:32 +02:00
|
|
|
manager.add_command("report", Report())
|
2016-11-18 20:27:46 +01:00
|
|
|
manager.add_command("rotate_certificate", RotateCertificate())
|
|
|
|
manager.add_command("reissue_certificate", ReissueCertificate())
|
2015-06-22 22:47:27 +02:00
|
|
|
manager.run()
|
2015-07-02 23:12:39 +02:00
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|