commit
85da1f1c75
|
@ -328,6 +328,54 @@ Lemur supports sending certification expiration notifications through SES and SM
|
||||||
LEMUR_SECURITY_TEAM_EMAIL_INTERVALS = [15, 2]
|
LEMUR_SECURITY_TEAM_EMAIL_INTERVALS = [15, 2]
|
||||||
|
|
||||||
|
|
||||||
|
Celery Options
|
||||||
|
---------------
|
||||||
|
To make use of automated tasks within lemur (e.g. syncing source/destinations, or reissuing ACME certificates), you
|
||||||
|
need to configure celery. See :ref:`Periodic Tasks <PeriodicTasks>` for more in depth documentation.
|
||||||
|
|
||||||
|
.. data:: CELERY_RESULT_BACKEND
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
The url to your redis backend (needs to be in the format `redis://<host>:<port>/<database>`)
|
||||||
|
|
||||||
|
.. data:: CELERY_BROKER_URL
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
The url to your redis broker (needs to be in the format `redis://<host>:<port>/<database>`)
|
||||||
|
|
||||||
|
.. data:: CELERY_IMPORTS
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
The module that celery needs to import, in our case thats `lemur.common.celery`
|
||||||
|
|
||||||
|
.. data:: CELERY_TIMEZONE
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
The timezone for celery to work with
|
||||||
|
|
||||||
|
|
||||||
|
.. data:: CELERYBEAT_SCHEDULE
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
This defines the schedule, with which the celery beat makes the worker run the specified tasks.
|
||||||
|
|
||||||
|
Since the celery module, relies on the RedisHandler, the following options also need to be set.
|
||||||
|
|
||||||
|
.. data:: REDIS_HOST
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
Hostname of your redis instance
|
||||||
|
|
||||||
|
.. data:: REDIS_PORT
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
Port on which redis is running (default: 6379)
|
||||||
|
|
||||||
|
.. data:: REDIS_DB
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
Which redis database to be used, by default redis offers databases 0-15 (default: 0)
|
||||||
|
|
||||||
Authentication Options
|
Authentication Options
|
||||||
----------------------
|
----------------------
|
||||||
Lemur currently supports Basic Authentication, LDAP Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
|
Lemur currently supports Basic Authentication, LDAP Authentication, Ping OAuth2, and Google out of the box. Additional flows can be added relatively easily.
|
||||||
|
@ -1123,6 +1171,23 @@ The following configuration properties are required to use the PowerDNS ACME Plu
|
||||||
|
|
||||||
File/Dir path to CA Bundle: Verifies the TLS certificate was issued by a Certificate Authority in the provided CA bundle.
|
File/Dir path to CA Bundle: Verifies the TLS certificate was issued by a Certificate Authority in the provided CA bundle.
|
||||||
|
|
||||||
|
ACME Plugin
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
The following configration properties are optional for the ACME plugin to use. They allow reusing an existing ACME
|
||||||
|
account. See :ref:`Using a pre-existing ACME account <AcmeAccountReuse>` for more details.
|
||||||
|
|
||||||
|
|
||||||
|
.. data:: ACME_PRIVATE_KEY
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
This is the private key, the account was registered with (in JWK format)
|
||||||
|
|
||||||
|
.. data:: ACME_REGR
|
||||||
|
:noindex:
|
||||||
|
|
||||||
|
This is the registration for the ACME account, the most important part is the uri attribute (in JSON)
|
||||||
|
|
||||||
.. _CommandLineInterface:
|
.. _CommandLineInterface:
|
||||||
|
|
||||||
Command Line Interface
|
Command Line Interface
|
||||||
|
|
|
@ -49,9 +49,11 @@ The amount of effort you wish to expend ensuring that Lemur has good entropy to
|
||||||
|
|
||||||
If you wish to generate more entropy for your system we would suggest you take a look at the following resources:
|
If you wish to generate more entropy for your system we would suggest you take a look at the following resources:
|
||||||
|
|
||||||
- `WES-entropy-client <https://github.com/WhitewoodCrypto/WES-entropy-client>`_
|
- `WES-entropy-client <https://github.com/Virginian/WES-entropy-client>`_
|
||||||
- `haveged <http://www.issihosts.com/haveged/>`_
|
- `haveged <http://www.issihosts.com/haveged/>`_
|
||||||
|
|
||||||
|
The original *WES-entropy-client* repository by WhitewoodCrypto was removed, the link now points to a fork of it.
|
||||||
|
|
||||||
For additional information about OpenSSL entropy issues:
|
For additional information about OpenSSL entropy issues:
|
||||||
|
|
||||||
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
|
- `Managing and Understanding Entropy Usage <https://www.blackhat.com/docs/us-15/materials/us-15-Potter-Understanding-And-Managing-Entropy-Usage.pdf>`_
|
||||||
|
@ -313,6 +315,7 @@ It will start a shell from which you can start/stop/restart the service.
|
||||||
|
|
||||||
You can read all errors that might occur from /tmp/lemur.log.
|
You can read all errors that might occur from /tmp/lemur.log.
|
||||||
|
|
||||||
|
.. _PeriodicTasks:
|
||||||
|
|
||||||
Periodic Tasks
|
Periodic Tasks
|
||||||
==============
|
==============
|
||||||
|
@ -386,10 +389,17 @@ To enable celery support, you must also have configuration values that tell Cele
|
||||||
Here are the Celery configuration variables that should be set::
|
Here are the Celery configuration variables that should be set::
|
||||||
|
|
||||||
CELERY_RESULT_BACKEND = 'redis://your_redis_url:6379'
|
CELERY_RESULT_BACKEND = 'redis://your_redis_url:6379'
|
||||||
CELERY_BROKER_URL = 'redis://your_redis_url:6379'
|
CELERY_BROKER_URL = 'redis://your_redis_url:6379/0'
|
||||||
CELERY_IMPORTS = ('lemur.common.celery')
|
CELERY_IMPORTS = ('lemur.common.celery')
|
||||||
CELERY_TIMEZONE = 'UTC'
|
CELERY_TIMEZONE = 'UTC'
|
||||||
|
|
||||||
|
REDIS_HOST="your_redis_url"
|
||||||
|
REDIS_PORT=6379
|
||||||
|
REDIS_DB=0
|
||||||
|
|
||||||
|
Out of the box, every Redis instance supports 16 databases. The default database (`REDIS_DB`) is set to 0, however, you can use any of the databases from 0-15. Via `redis.conf` more databases can be supported.
|
||||||
|
In the `redis://` url, the database number can be added with a slash after the port. (defaults to 0, if omitted)
|
||||||
|
|
||||||
Do not forget to import crontab module in your configuration file::
|
Do not forget to import crontab module in your configuration file::
|
||||||
|
|
||||||
from celery.task.schedules import crontab
|
from celery.task.schedules import crontab
|
||||||
|
@ -501,3 +511,47 @@ The following must be added to the config file to activate the pinning (the pinn
|
||||||
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
|
KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
|
||||||
-----END CERTIFICATE-----
|
-----END CERTIFICATE-----
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
.. _AcmeAccountReuse:
|
||||||
|
|
||||||
|
LetsEncrypt: Using a pre-existing ACME account
|
||||||
|
-----------------------------------------------
|
||||||
|
|
||||||
|
Let's Encrypt allows reusing an existing ACME account, to create and especially revoke certificates. The current
|
||||||
|
implementation in the acme plugin, only allows for a single account for all ACME authorities, which might be an issue,
|
||||||
|
when you try to use Let's Encrypt together with another certificate authority that uses the ACME protocol.
|
||||||
|
|
||||||
|
To use an existing account, you need to configure the `ACME_PRIVATE_KEY` and `ACME_REGR` variables in the lemur
|
||||||
|
configuration.
|
||||||
|
|
||||||
|
`ACME_PRIVATE_KEY` needs to be in the JWK format::
|
||||||
|
|
||||||
|
{
|
||||||
|
"kty": "RSA",
|
||||||
|
"n": "yr1qBwHizA7ME_iV32bY10ILp.....",
|
||||||
|
"e": "AQAB",
|
||||||
|
"d": "llBlYhil3I.....",
|
||||||
|
"p": "-5LW2Lewogo.........",
|
||||||
|
"q": "zk6dHqHfHksd.........",
|
||||||
|
"dp": "qfe9fFIu3mu.......",
|
||||||
|
"dq": "cXFO-loeOyU.......",
|
||||||
|
"qi": "AfK1sh0_8sLTb..........."
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
Using `python-jwt` converting an existing private key in PEM format is quite easy::
|
||||||
|
|
||||||
|
import python_jwt as jwt, jwcrypto.jwk as jwk
|
||||||
|
|
||||||
|
priv_key = jwk.JWK.from_pem(b"""-----BEGIN RSA PRIVATE KEY-----
|
||||||
|
...
|
||||||
|
-----END RSA PRIVATE KEY-----""")
|
||||||
|
|
||||||
|
print(priv_key.export())
|
||||||
|
|
||||||
|
`ACME_REGR` needs to be a valid JSON with a `body` and a `uri` attribute, similar to this::
|
||||||
|
|
||||||
|
{"body": {}, "uri": "https://acme-staging-v02.api.letsencrypt.org/acme/acct/<ACCOUNT_NUMBER>"}
|
||||||
|
|
||||||
|
The URI can be retrieved from the ACME create account endpoint when creating a new account, using the existing key.
|
|
@ -9,10 +9,8 @@ from datetime import timedelta
|
||||||
|
|
||||||
import arrow
|
import arrow
|
||||||
from cryptography import x509
|
from cryptography import x509
|
||||||
from cryptography.hazmat.primitives.asymmetric import rsa, ec
|
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
from idna.core import InvalidCodepoint
|
from idna.core import InvalidCodepoint
|
||||||
from lemur.common.utils import get_key_type_from_ec_curve
|
|
||||||
from sqlalchemy import (
|
from sqlalchemy import (
|
||||||
event,
|
event,
|
||||||
Integer,
|
Integer,
|
||||||
|
@ -154,6 +152,7 @@ class Certificate(db.Model):
|
||||||
Integer, ForeignKey("authorities.id", ondelete="CASCADE")
|
Integer, ForeignKey("authorities.id", ondelete="CASCADE")
|
||||||
)
|
)
|
||||||
rotation_policy_id = Column(Integer, ForeignKey("rotation_policies.id"))
|
rotation_policy_id = Column(Integer, ForeignKey("rotation_policies.id"))
|
||||||
|
key_type = Column(String(128))
|
||||||
|
|
||||||
notifications = relationship(
|
notifications = relationship(
|
||||||
"Notification",
|
"Notification",
|
||||||
|
@ -297,6 +296,8 @@ class Certificate(db.Model):
|
||||||
def distinguished_name(self):
|
def distinguished_name(self):
|
||||||
return self.parsed_cert.subject.rfc4514_string()
|
return self.parsed_cert.subject.rfc4514_string()
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Commenting this property as key_type is now added as a column. This code can be removed in future.
|
||||||
@property
|
@property
|
||||||
def key_type(self):
|
def key_type(self):
|
||||||
if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
|
if isinstance(self.parsed_cert.public_key(), rsa.RSAPublicKey):
|
||||||
|
@ -305,6 +306,7 @@ class Certificate(db.Model):
|
||||||
)
|
)
|
||||||
elif isinstance(self.parsed_cert.public_key(), ec.EllipticCurvePublicKey):
|
elif isinstance(self.parsed_cert.public_key(), ec.EllipticCurvePublicKey):
|
||||||
return get_key_type_from_ec_curve(self.parsed_cert.public_key().curve.name)
|
return get_key_type_from_ec_curve(self.parsed_cert.public_key().curve.name)
|
||||||
|
"""
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def validity_remaining(self):
|
def validity_remaining(self):
|
||||||
|
|
|
@ -71,6 +71,23 @@ def parse_private_key(private_key):
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_key_type_from_certificate(body):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Helper function to determine key type by pasrding given PEM certificate
|
||||||
|
|
||||||
|
:param body: PEM string
|
||||||
|
:return: Key type string
|
||||||
|
"""
|
||||||
|
parsed_cert = parse_certificate(body)
|
||||||
|
if isinstance(parsed_cert.public_key(), rsa.RSAPublicKey):
|
||||||
|
return "RSA{key_size}".format(
|
||||||
|
key_size=parsed_cert.public_key().key_size
|
||||||
|
)
|
||||||
|
elif isinstance(parsed_cert.public_key(), ec.EllipticCurvePublicKey):
|
||||||
|
return get_key_type_from_ec_curve(parsed_cert.public_key().curve.name)
|
||||||
|
|
||||||
|
|
||||||
def split_pem(data):
|
def split_pem(data):
|
||||||
"""
|
"""
|
||||||
Split a string of several PEM payloads to a list of strings.
|
Split a string of several PEM payloads to a list of strings.
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from sqlalchemy import Column, Integer, String, text, Text
|
from sqlalchemy import Column, Integer, String, text
|
||||||
from sqlalchemy.dialects.postgresql import JSON
|
from sqlalchemy.dialects.postgresql import JSON
|
||||||
from sqlalchemy.orm import relationship
|
from sqlalchemy.orm import relationship
|
||||||
from sqlalchemy_utils import ArrowType
|
from sqlalchemy_utils import ArrowType
|
||||||
|
@ -12,7 +12,7 @@ class DnsProvider(db.Model):
|
||||||
__tablename__ = "dns_providers"
|
__tablename__ = "dns_providers"
|
||||||
id = Column(Integer(), primary_key=True)
|
id = Column(Integer(), primary_key=True)
|
||||||
name = Column(String(length=256), unique=True, nullable=True)
|
name = Column(String(length=256), unique=True, nullable=True)
|
||||||
description = Column(Text(), nullable=True)
|
description = Column(String(length=1024), nullable=True)
|
||||||
provider_type = Column(String(length=256), nullable=True)
|
provider_type = Column(String(length=256), nullable=True)
|
||||||
credentials = Column(Vault, nullable=True)
|
credentials = Column(Vault, nullable=True)
|
||||||
api_endpoint = Column(String(length=256), nullable=True)
|
api_endpoint = Column(String(length=256), nullable=True)
|
||||||
|
|
|
@ -67,7 +67,8 @@ def run_migrations_online():
|
||||||
context.configure(
|
context.configure(
|
||||||
connection=connection,
|
connection=connection,
|
||||||
target_metadata=target_metadata,
|
target_metadata=target_metadata,
|
||||||
**current_app.extensions["migrate"].configure_args
|
**current_app.extensions["migrate"].configure_args,
|
||||||
|
compare_type=True
|
||||||
)
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
|
|
@ -0,0 +1,26 @@
|
||||||
|
"""empty message
|
||||||
|
|
||||||
|
Revision ID: 434c29e40511
|
||||||
|
Revises: 8323a5ea723a
|
||||||
|
Create Date: 2020-09-11 17:24:51.344585
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '434c29e40511'
|
||||||
|
down_revision = '8323a5ea723a'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.add_column('certificates', sa.Column('key_type', sa.String(length=128), nullable=True))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
op.drop_column('certificates', 'key_type')
|
||||||
|
# ### end Alembic commands ###
|
|
@ -0,0 +1,114 @@
|
||||||
|
"""
|
||||||
|
|
||||||
|
This database upgrade updates the key_type information for either
|
||||||
|
still valid or expired certificates in the last 30 days. For RSA
|
||||||
|
keys, the algorithm is determined based on the key length. For
|
||||||
|
the rest of the keys, the certificate body is parsed to determine
|
||||||
|
the exact key_type information.
|
||||||
|
|
||||||
|
Each individual DB change is explicitly committed, and the respective
|
||||||
|
log is added to a file named db_upgrade.log in the current working
|
||||||
|
directory. Any error encountered while parsing a certificate will
|
||||||
|
also be logged along with the certificate ID. If faced with any issue
|
||||||
|
while running this upgrade, there is no harm in re-running the upgrade.
|
||||||
|
Each run processes only rows for which key_type information is not yet
|
||||||
|
determined.
|
||||||
|
|
||||||
|
A successful complete run will end up updating the Alembic Version to
|
||||||
|
the new Revision ID c301c59688d2. Currently, Lemur supports only RSA
|
||||||
|
and ECC certificates. This could be a long-running job depending upon
|
||||||
|
the number of DB entries it may process.
|
||||||
|
|
||||||
|
Revision ID: c301c59688d2
|
||||||
|
Revises: 434c29e40511
|
||||||
|
Create Date: 2020-09-21 14:28:50.757998
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = 'c301c59688d2'
|
||||||
|
down_revision = '434c29e40511'
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
from sqlalchemy.sql import text
|
||||||
|
from lemur.common import utils
|
||||||
|
import time
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
log_file = open('db_upgrade.log', 'a')
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade():
|
||||||
|
log_file.write("\n*** Starting new run(%s) ***\n" % datetime.datetime.now())
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Update RSA keys using the key length information
|
||||||
|
update_key_type_rsa(1024)
|
||||||
|
update_key_type_rsa(2048)
|
||||||
|
update_key_type_rsa(4096)
|
||||||
|
|
||||||
|
# Process remaining certificates. Though below method does not make any assumptions, most of the remaining ones should be ECC certs.
|
||||||
|
update_key_type()
|
||||||
|
|
||||||
|
log_file.write("--- Total %s seconds ---\n" % (time.time() - start_time))
|
||||||
|
log_file.close()
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade():
|
||||||
|
# Change key type column back to null
|
||||||
|
# Going back 32 days instead of 31 to make sure no certificates are skipped
|
||||||
|
stmt = text(
|
||||||
|
"update certificates set key_type=null where not_after > CURRENT_DATE - 32"
|
||||||
|
)
|
||||||
|
op.execute(stmt)
|
||||||
|
|
||||||
|
|
||||||
|
"""
|
||||||
|
Helper methods performing updates for RSA and rest of the keys
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def update_key_type_rsa(bits):
|
||||||
|
log_file.write("Processing certificate with key type RSA %s\n" % bits)
|
||||||
|
|
||||||
|
stmt = text(
|
||||||
|
f"update certificates set key_type='RSA{bits}' where bits={bits} and not_after > CURRENT_DATE - 31 and key_type is null"
|
||||||
|
)
|
||||||
|
log_file.write("Query: %s\n" % stmt)
|
||||||
|
|
||||||
|
start_time = time.time()
|
||||||
|
op.execute(stmt)
|
||||||
|
commit()
|
||||||
|
|
||||||
|
log_file.write("--- %s seconds ---\n" % (time.time() - start_time))
|
||||||
|
|
||||||
|
|
||||||
|
def update_key_type():
|
||||||
|
conn = op.get_bind()
|
||||||
|
start_time = time.time()
|
||||||
|
|
||||||
|
# Loop through all certificates that are valid today or expired in the last 30 days.
|
||||||
|
for cert_id, body in conn.execute(
|
||||||
|
text(
|
||||||
|
"select id, body from certificates where bits < 1024 and not_after > CURRENT_DATE - 31 and key_type is null")
|
||||||
|
):
|
||||||
|
try:
|
||||||
|
cert_key_type = utils.get_key_type_from_certificate(body)
|
||||||
|
except ValueError as e:
|
||||||
|
log_file.write("Error in processing certificate - ID: %s Error: %s \n" % (cert_id, str(e)))
|
||||||
|
else:
|
||||||
|
log_file.write("Processing certificate - ID: %s key_type: %s\n" % (cert_id, cert_key_type))
|
||||||
|
stmt = text(
|
||||||
|
"update certificates set key_type=:key_type where id=:id"
|
||||||
|
)
|
||||||
|
stmt = stmt.bindparams(key_type=cert_key_type, id=cert_id)
|
||||||
|
op.execute(stmt)
|
||||||
|
|
||||||
|
commit()
|
||||||
|
|
||||||
|
log_file.write("--- %s seconds ---\n" % (time.time() - start_time))
|
||||||
|
|
||||||
|
|
||||||
|
def commit():
|
||||||
|
stmt = text("commit")
|
||||||
|
op.execute(stmt)
|
|
@ -1,9 +1,12 @@
|
||||||
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
|
|
||||||
import arrow
|
import arrow
|
||||||
import requests
|
import requests
|
||||||
import json
|
import json
|
||||||
from lemur.plugins import lemur_entrust as ENTRUST
|
import sys
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
from lemur.plugins import lemur_entrust as entrust
|
||||||
|
from lemur.plugins.bases import IssuerPlugin, SourcePlugin
|
||||||
from lemur.extensions import metrics
|
from lemur.extensions import metrics
|
||||||
from lemur.common.utils import validate_conf
|
from lemur.common.utils import validate_conf
|
||||||
|
|
||||||
|
@ -17,24 +20,24 @@ def log_status_code(r, *args, **kwargs):
|
||||||
:param kwargs:
|
:param kwargs:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
metrics.send("ENTRUST_status_code_{}".format(r.status_code), "counter", 1)
|
metrics.send(f"entrust_status_code_{r.status_code}", "counter", 1)
|
||||||
|
|
||||||
|
|
||||||
def determine_end_date(end_date):
|
def determine_end_date(end_date):
|
||||||
"""
|
"""
|
||||||
Determine appropriate end date
|
Determine appropriate end date
|
||||||
:param end_date:
|
:param end_date:
|
||||||
:return: validity_end
|
:return: validity_end as string
|
||||||
"""
|
"""
|
||||||
# ENTRUST only allows 13 months of max certificate duration
|
# ENTRUST only allows 13 months of max certificate duration
|
||||||
max_validity_end = arrow.utcnow().shift(years=1, months=+1).format('YYYY-MM-DD')
|
max_validity_end = arrow.utcnow().shift(years=1, months=+1)
|
||||||
|
|
||||||
if not end_date:
|
if not end_date:
|
||||||
end_date = max_validity_end
|
end_date = max_validity_end
|
||||||
|
|
||||||
if end_date > max_validity_end:
|
if end_date > max_validity_end:
|
||||||
end_date = max_validity_end
|
end_date = max_validity_end
|
||||||
return end_date
|
return end_date.format('YYYY-MM-DD')
|
||||||
|
|
||||||
|
|
||||||
def process_options(options):
|
def process_options(options):
|
||||||
|
@ -49,7 +52,10 @@ def process_options(options):
|
||||||
# take the value as Cert product-type
|
# take the value as Cert product-type
|
||||||
# else default to "STANDARD_SSL"
|
# else default to "STANDARD_SSL"
|
||||||
authority = options.get("authority").name.upper()
|
authority = options.get("authority").name.upper()
|
||||||
product_type = current_app.config.get("ENTRUST_PRODUCT_{0}".format(authority), "STANDARD_SSL")
|
# STANDARD_SSL (cn=domain, san=www.domain),
|
||||||
|
# ADVANTAGE_SSL (cn=domain, san=[www.domain, one_more_option]),
|
||||||
|
# WILDCARD_SSL (unlimited sans, and wildcard)
|
||||||
|
product_type = current_app.config.get(f"ENTRUST_PRODUCT_{authority}", "STANDARD_SSL")
|
||||||
|
|
||||||
if options.get("validity_end"):
|
if options.get("validity_end"):
|
||||||
validity_end = determine_end_date(options.get("validity_end"))
|
validity_end = determine_end_date(options.get("validity_end"))
|
||||||
|
@ -67,6 +73,7 @@ def process_options(options):
|
||||||
"eku": "SERVER_AND_CLIENT_AUTH",
|
"eku": "SERVER_AND_CLIENT_AUTH",
|
||||||
"certType": product_type,
|
"certType": product_type,
|
||||||
"certExpiryDate": validity_end,
|
"certExpiryDate": validity_end,
|
||||||
|
# "keyType": "RSA", Entrust complaining about this parameter
|
||||||
"tracking": tracking_data
|
"tracking": tracking_data
|
||||||
}
|
}
|
||||||
return data
|
return data
|
||||||
|
@ -86,23 +93,31 @@ def handle_response(my_response):
|
||||||
404: "Unknown jobId",
|
404: "Unknown jobId",
|
||||||
429: "Too many requests"
|
429: "Too many requests"
|
||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = json.loads(my_response.content)
|
d = json.loads(my_response.content)
|
||||||
except Exception as e:
|
except ValueError:
|
||||||
# catch an empty jason object here
|
# catch an empty jason object here
|
||||||
d = {'errors': 'No detailled message'}
|
d = {'response': 'No detailed message'}
|
||||||
s = my_response.status_code
|
s = my_response.status_code
|
||||||
if s > 399:
|
if s > 399:
|
||||||
raise Exception("ENTRUST error: {0}\n{1}".format(msg.get(s, s), d['errors']))
|
raise Exception(f"ENTRUST error: {msg.get(s, s)}\n{d['errors']}")
|
||||||
current_app.logger.info("Response: {0}, {1} ".format(s, d))
|
|
||||||
|
log_data = {
|
||||||
|
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
|
||||||
|
"message": "Response",
|
||||||
|
"status": s,
|
||||||
|
"response": d
|
||||||
|
}
|
||||||
|
current_app.logger.info(log_data)
|
||||||
return d
|
return d
|
||||||
|
|
||||||
|
|
||||||
class EntrustIssuerPlugin(IssuerPlugin):
|
class EntrustIssuerPlugin(IssuerPlugin):
|
||||||
title = "ENTRUST"
|
title = "Entrust"
|
||||||
slug = "entrust-issuer"
|
slug = "entrust-issuer"
|
||||||
description = "Enables the creation of certificates by ENTRUST"
|
description = "Enables the creation of certificates by ENTRUST"
|
||||||
version = ENTRUST.VERSION
|
version = entrust.VERSION
|
||||||
|
|
||||||
author = "sirferl"
|
author = "sirferl"
|
||||||
author_url = "https://github.com/sirferl/lemur"
|
author_url = "https://github.com/sirferl/lemur"
|
||||||
|
@ -119,7 +134,6 @@ class EntrustIssuerPlugin(IssuerPlugin):
|
||||||
"ENTRUST_NAME",
|
"ENTRUST_NAME",
|
||||||
"ENTRUST_EMAIL",
|
"ENTRUST_EMAIL",
|
||||||
"ENTRUST_PHONE",
|
"ENTRUST_PHONE",
|
||||||
"ENTRUST_ISSUING",
|
|
||||||
]
|
]
|
||||||
validate_conf(current_app, required_vars)
|
validate_conf(current_app, required_vars)
|
||||||
|
|
||||||
|
@ -142,9 +156,12 @@ class EntrustIssuerPlugin(IssuerPlugin):
|
||||||
:param issuer_options:
|
:param issuer_options:
|
||||||
:return: :raise Exception:
|
:return: :raise Exception:
|
||||||
"""
|
"""
|
||||||
current_app.logger.info(
|
log_data = {
|
||||||
"Requesting options: {0}".format(issuer_options)
|
"function": f"{__name__}.{sys._getframe().f_code.co_name}",
|
||||||
)
|
"message": "Requesting options",
|
||||||
|
"options": issuer_options
|
||||||
|
}
|
||||||
|
current_app.logger.info(log_data)
|
||||||
|
|
||||||
url = current_app.config.get("ENTRUST_URL") + "/certificates"
|
url = current_app.config.get("ENTRUST_URL") + "/certificates"
|
||||||
|
|
||||||
|
@ -156,36 +173,46 @@ class EntrustIssuerPlugin(IssuerPlugin):
|
||||||
except requests.exceptions.Timeout:
|
except requests.exceptions.Timeout:
|
||||||
raise Exception("Timeout for POST")
|
raise Exception("Timeout for POST")
|
||||||
except requests.exceptions.RequestException as e:
|
except requests.exceptions.RequestException as e:
|
||||||
raise Exception("Error for POST {0}".format(e))
|
raise Exception(f"Error for POST {e}")
|
||||||
|
|
||||||
response_dict = handle_response(response)
|
response_dict = handle_response(response)
|
||||||
external_id = response_dict['trackingId']
|
external_id = response_dict['trackingId']
|
||||||
cert = response_dict['endEntityCert']
|
cert = response_dict['endEntityCert']
|
||||||
|
if len(response_dict['chainCerts']) < 2:
|
||||||
|
# certificate signed by CA directly, no ICA included ini the chain
|
||||||
|
chain = None
|
||||||
|
else:
|
||||||
chain = response_dict['chainCerts'][1]
|
chain = response_dict['chainCerts'][1]
|
||||||
current_app.logger.info(
|
|
||||||
"Received Chain: {0}".format(chain)
|
log_data["message"] = "Received Chain"
|
||||||
)
|
log_data["options"] = f"chain: {chain}"
|
||||||
|
current_app.logger.info(log_data)
|
||||||
|
|
||||||
return cert, chain, external_id
|
return cert, chain, external_id
|
||||||
|
|
||||||
def revoke_certificate(self, certificate, comments):
|
def revoke_certificate(self, certificate, comments):
|
||||||
"""Revoke a Digicert certificate."""
|
"""Revoke an Entrust certificate."""
|
||||||
base_url = current_app.config.get("ENTRUST_URL")
|
base_url = current_app.config.get("ENTRUST_URL")
|
||||||
|
|
||||||
# make certificate revoke request
|
# make certificate revoke request
|
||||||
revoke_url = "{0}/certificates/{1}/revocations".format(
|
revoke_url = f"{base_url}/certificates/{certificate.external_id}/revocations"
|
||||||
base_url, certificate.external_id
|
if not comments or comments == '':
|
||||||
)
|
|
||||||
metrics.send("entrust_revoke_certificate", "counter", 1)
|
|
||||||
if comments == '' or not comments:
|
|
||||||
comments = "revoked via API"
|
comments = "revoked via API"
|
||||||
data = {
|
data = {
|
||||||
"crlReason": "superseded",
|
"crlReason": "superseded", # enum (keyCompromise, affiliationChanged, superseded, cessationOfOperation)
|
||||||
"revocationComment": comments
|
"revocationComment": comments
|
||||||
}
|
}
|
||||||
response = self.session.post(revoke_url, json=data)
|
response = self.session.post(revoke_url, json=data)
|
||||||
|
metrics.send("entrust_revoke_certificate", "counter", 1)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
data = handle_response(response)
|
def deactivate_certificate(self, certificate):
|
||||||
|
"""Deactivates an Entrust certificate."""
|
||||||
|
base_url = current_app.config.get("ENTRUST_URL")
|
||||||
|
deactivate_url = f"{base_url}/certificates/{certificate.external_id}/deactivations"
|
||||||
|
response = self.session.post(deactivate_url)
|
||||||
|
metrics.send("entrust_deactivate_certificate", "counter", 1)
|
||||||
|
return handle_response(response)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_authority(options):
|
def create_authority(options):
|
||||||
|
@ -200,7 +227,8 @@ class EntrustIssuerPlugin(IssuerPlugin):
|
||||||
entrust_root = current_app.config.get("ENTRUST_ROOT")
|
entrust_root = current_app.config.get("ENTRUST_ROOT")
|
||||||
entrust_issuing = current_app.config.get("ENTRUST_ISSUING")
|
entrust_issuing = current_app.config.get("ENTRUST_ISSUING")
|
||||||
role = {"username": "", "password": "", "name": "entrust"}
|
role = {"username": "", "password": "", "name": "entrust"}
|
||||||
current_app.logger.info("Creating Auth: {0} {1}".format(options, entrust_issuing))
|
current_app.logger.info(f"Creating Auth: {options} {entrust_issuing}")
|
||||||
|
# body, chain, role
|
||||||
return entrust_root, "", [role]
|
return entrust_root, "", [role]
|
||||||
|
|
||||||
def get_ordered_certificate(self, order_id):
|
def get_ordered_certificate(self, order_id):
|
||||||
|
@ -211,10 +239,10 @@ class EntrustIssuerPlugin(IssuerPlugin):
|
||||||
|
|
||||||
|
|
||||||
class EntrustSourcePlugin(SourcePlugin):
|
class EntrustSourcePlugin(SourcePlugin):
|
||||||
title = "ENTRUST"
|
title = "Entrust"
|
||||||
slug = "entrust-source"
|
slug = "entrust-source"
|
||||||
description = "Enables the collecion of certificates"
|
description = "Enables the collection of certificates"
|
||||||
version = ENTRUST.VERSION
|
version = entrust.VERSION
|
||||||
|
|
||||||
author = "sirferl"
|
author = "sirferl"
|
||||||
author_url = "https://github.com/sirferl/lemur"
|
author_url = "https://github.com/sirferl/lemur"
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
from lemur.tests.conftest import * # noqa
|
|
@ -0,0 +1,54 @@
|
||||||
|
from unittest.mock import patch, Mock
|
||||||
|
|
||||||
|
import arrow
|
||||||
|
from cryptography import x509
|
||||||
|
from lemur.plugins.lemur_entrust import plugin
|
||||||
|
|
||||||
|
|
||||||
|
def config_mock(*args):
|
||||||
|
values = {
|
||||||
|
"ENTRUST_API_CERT": "-----BEGIN CERTIFICATE-----abc-----END CERTIFICATE-----",
|
||||||
|
"ENTRUST_API_KEY": False,
|
||||||
|
"ENTRUST_API_USER": "test",
|
||||||
|
"ENTRUST_API_PASS": "password",
|
||||||
|
"ENTRUST_URL": "http",
|
||||||
|
"ENTRUST_ROOT": None,
|
||||||
|
"ENTRUST_NAME": "test",
|
||||||
|
"ENTRUST_EMAIL": "test@lemur.net",
|
||||||
|
"ENTRUST_PHONE": "0123456",
|
||||||
|
"ENTRUST_PRODUCT_ENTRUST": "ADVANTAGE_SSL"
|
||||||
|
}
|
||||||
|
return values[args[0]]
|
||||||
|
|
||||||
|
|
||||||
|
@patch("lemur.plugins.lemur_entrust.plugin.current_app")
|
||||||
|
def test_process_options(mock_current_app, authority):
|
||||||
|
mock_current_app.config.get = Mock(side_effect=config_mock)
|
||||||
|
plugin.determine_end_date = Mock(return_value=arrow.get(2020, 10, 7).format('YYYY-MM-DD'))
|
||||||
|
|
||||||
|
authority.name = "Entrust"
|
||||||
|
names = [u"one.example.com", u"two.example.com", u"three.example.com"]
|
||||||
|
options = {
|
||||||
|
"common_name": "example.com",
|
||||||
|
"owner": "bob@example.com",
|
||||||
|
"description": "test certificate",
|
||||||
|
"extensions": {"sub_alt_names": {"names": [x509.DNSName(x) for x in names]}},
|
||||||
|
"organization": "Example, Inc.",
|
||||||
|
"organizational_unit": "Example Org",
|
||||||
|
"validity_end": arrow.get(2020, 10, 7),
|
||||||
|
"authority": authority,
|
||||||
|
}
|
||||||
|
|
||||||
|
expected = {
|
||||||
|
"signingAlg": "SHA-2",
|
||||||
|
"eku": "SERVER_AND_CLIENT_AUTH",
|
||||||
|
"certType": "ADVANTAGE_SSL",
|
||||||
|
"certExpiryDate": arrow.get(2020, 10, 7).format('YYYY-MM-DD'),
|
||||||
|
"tracking": {
|
||||||
|
"requesterName": mock_current_app.config.get("ENTRUST_NAME"),
|
||||||
|
"requesterEmail": mock_current_app.config.get("ENTRUST_EMAIL"),
|
||||||
|
"requesterPhone": mock_current_app.config.get("ENTRUST_PHONE")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert expected == plugin.process_options(options)
|
|
@ -1,9 +1,18 @@
|
||||||
# This is just Python which means you can inherit and tweak settings
|
# This is just Python which means you can inherit and tweak settings
|
||||||
|
|
||||||
import os
|
import os
|
||||||
|
import random
|
||||||
|
import string
|
||||||
|
|
||||||
_basedir = os.path.abspath(os.path.dirname(__file__))
|
_basedir = os.path.abspath(os.path.dirname(__file__))
|
||||||
|
|
||||||
|
|
||||||
|
# generate random secrets for unittest
|
||||||
|
def get_random_secret(length):
|
||||||
|
input_ascii = string.ascii_letters + string.digits
|
||||||
|
return ''.join(random.choice(input_ascii) for i in range(length))
|
||||||
|
|
||||||
|
|
||||||
THREADS_PER_PAGE = 8
|
THREADS_PER_PAGE = 8
|
||||||
|
|
||||||
# General
|
# General
|
||||||
|
@ -86,7 +95,6 @@ DIGICERT_CIS_API_KEY = "api-key"
|
||||||
DIGICERT_CIS_ROOTS = {"root": "ROOT"}
|
DIGICERT_CIS_ROOTS = {"root": "ROOT"}
|
||||||
DIGICERT_CIS_INTERMEDIATES = {"inter": "INTERMEDIATE_CA_CERT"}
|
DIGICERT_CIS_INTERMEDIATES = {"inter": "INTERMEDIATE_CA_CERT"}
|
||||||
|
|
||||||
|
|
||||||
VERISIGN_URL = "http://example.com"
|
VERISIGN_URL = "http://example.com"
|
||||||
VERISIGN_PEM_PATH = "~/"
|
VERISIGN_PEM_PATH = "~/"
|
||||||
VERISIGN_FIRST_NAME = "Jim"
|
VERISIGN_FIRST_NAME = "Jim"
|
||||||
|
@ -197,3 +205,41 @@ LDAP_REQUIRED_GROUP = "Lemur Access"
|
||||||
LDAP_DEFAULT_ROLE = "role1"
|
LDAP_DEFAULT_ROLE = "role1"
|
||||||
|
|
||||||
ALLOW_CERT_DELETION = True
|
ALLOW_CERT_DELETION = True
|
||||||
|
|
||||||
|
ENTRUST_API_CERT = "api-cert"
|
||||||
|
ENTRUST_API_KEY = get_random_secret(32)
|
||||||
|
ENTRUST_API_USER = "user"
|
||||||
|
ENTRUST_API_PASS = get_random_secret(32)
|
||||||
|
ENTRUST_URL = "https://api.entrust.net/enterprise/v2"
|
||||||
|
ENTRUST_ROOT = """
|
||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIEPjCCAyagAwIBAgIESlOMKDANBgkqhkiG9w0BAQsFADCBvjELMAkGA1UEBhMC
|
||||||
|
VVMxFjAUBgNVBAoTDUVudHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50
|
||||||
|
cnVzdC5uZXQvbGVnYWwtdGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3Qs
|
||||||
|
IEluYy4gLSBmb3IgYXV0aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVz
|
||||||
|
dCBSb290IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IC0gRzIwHhcNMDkwNzA3MTcy
|
||||||
|
NTU0WhcNMzAxMjA3MTc1NTU0WjCBvjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUVu
|
||||||
|
dHJ1c3QsIEluYy4xKDAmBgNVBAsTH1NlZSB3d3cuZW50cnVzdC5uZXQvbGVnYWwt
|
||||||
|
dGVybXMxOTA3BgNVBAsTMChjKSAyMDA5IEVudHJ1c3QsIEluYy4gLSBmb3IgYXV0
|
||||||
|
aG9yaXplZCB1c2Ugb25seTEyMDAGA1UEAxMpRW50cnVzdCBSb290IENlcnRpZmlj
|
||||||
|
YXRpb24gQXV0aG9yaXR5IC0gRzIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEK
|
||||||
|
AoIBAQC6hLZy254Ma+KZ6TABp3bqMriVQRrJ2mFOWHLP/vaCeb9zYQYKpSfYs1/T
|
||||||
|
RU4cctZOMvJyig/3gxnQaoCAAEUesMfnmr8SVycco2gvCoe9amsOXmXzHHfV1IWN
|
||||||
|
cCG0szLni6LVhjkCsbjSR87kyUnEO6fe+1R9V77w6G7CebI6C1XiUJgWMhNcL3hW
|
||||||
|
wcKUs/Ja5CeanyTXxuzQmyWC48zCxEXFjJd6BmsqEZ+pCm5IO2/b1BEZQvePB7/1
|
||||||
|
U1+cPvQXLOZprE4yTGJ36rfo5bs0vBmLrpxR57d+tVOxMyLlbc9wPBr64ptntoP0
|
||||||
|
jaWvYkxN4FisZDQSA/i2jZRjJKRxAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAP
|
||||||
|
BgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBRqciZ60B7vfec7aVHUbI2fkBJmqzAN
|
||||||
|
BgkqhkiG9w0BAQsFAAOCAQEAeZ8dlsa2eT8ijYfThwMEYGprmi5ZiXMRrEPR9RP/
|
||||||
|
jTkrwPK9T3CMqS/qF8QLVJ7UG5aYMzyorWKiAHarWWluBh1+xLlEjZivEtRh2woZ
|
||||||
|
Rkfz6/djwUAFQKXSt/S1mja/qYh2iARVBCuch38aNzx+LaUa2NSJXsq9rD1s2G2v
|
||||||
|
1fN2D807iDginWyTmsQ9v4IbZT+mD12q/OWyFcq1rca8PdCE6OoGcrBNOTJ4vz4R
|
||||||
|
nAuknZoh8/CbCzB428Hch0P+vGOaysXCHMnHjf87ElgI5rY97HosTvuDls4MPGmH
|
||||||
|
VHOkc8KT/1EQrBVUAdj8BbGJoX90g5pJ19xOe4pIb4tF9g==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
|
"""
|
||||||
|
ENTRUST_NAME = "lemur"
|
||||||
|
ENTRUST_EMAIL = "lemur@example.com"
|
||||||
|
ENTRUST_PHONE = "123456"
|
||||||
|
ENTRUST_ISSUING = ""
|
||||||
|
ENTRUST_PRODUCT_ENTRUST = "ADVANTAGE_SSL"
|
||||||
|
|
|
@ -2,11 +2,13 @@ import pytest
|
||||||
|
|
||||||
from lemur.tests.vectors import (
|
from lemur.tests.vectors import (
|
||||||
SAN_CERT,
|
SAN_CERT,
|
||||||
|
SAN_CERT_STR,
|
||||||
INTERMEDIATE_CERT,
|
INTERMEDIATE_CERT,
|
||||||
ROOTCA_CERT,
|
ROOTCA_CERT,
|
||||||
EC_CERT_EXAMPLE,
|
EC_CERT_EXAMPLE,
|
||||||
ECDSA_PRIME256V1_CERT,
|
ECDSA_PRIME256V1_CERT,
|
||||||
ECDSA_SECP384r1_CERT,
|
ECDSA_SECP384r1_CERT,
|
||||||
|
ECDSA_SECP384r1_CERT_STR,
|
||||||
DSA_CERT,
|
DSA_CERT,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -106,3 +108,9 @@ def test_is_selfsigned(selfsigned_cert):
|
||||||
# unsupported algorithm (DSA)
|
# unsupported algorithm (DSA)
|
||||||
with pytest.raises(Exception):
|
with pytest.raises(Exception):
|
||||||
is_selfsigned(DSA_CERT)
|
is_selfsigned(DSA_CERT)
|
||||||
|
|
||||||
|
|
||||||
|
def test_get_key_type_from_certificate():
|
||||||
|
from lemur.common.utils import get_key_type_from_certificate
|
||||||
|
assert (get_key_type_from_certificate(SAN_CERT_STR) == "RSA2048")
|
||||||
|
assert (get_key_type_from_certificate(ECDSA_SECP384r1_CERT_STR) == "ECCSECP384R1")
|
||||||
|
|
|
@ -39,7 +39,7 @@
|
||||||
"gulp-uglify": "^2.0.0",
|
"gulp-uglify": "^2.0.0",
|
||||||
"gulp-useref": "^3.1.2",
|
"gulp-useref": "^3.1.2",
|
||||||
"gulp-util": "^3.0.1",
|
"gulp-util": "^3.0.1",
|
||||||
"http-proxy": "~1.16.2",
|
"http-proxy": ">=1.18.1",
|
||||||
"jshint-stylish": "^2.2.1",
|
"jshint-stylish": "^2.2.1",
|
||||||
"karma": "^4.4.1",
|
"karma": "^4.4.1",
|
||||||
"karma-jasmine": "^1.1.0",
|
"karma-jasmine": "^1.1.0",
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in
|
# pip-compile --no-index --output-file=requirements-docs.txt requirements-docs.in
|
||||||
#
|
#
|
||||||
acme==1.7.0 # via -r requirements.txt
|
acme==1.8.0 # via -r requirements.txt
|
||||||
alabaster==0.7.12 # via sphinx
|
alabaster==0.7.12 # via sphinx
|
||||||
alembic-autogenerate-enums==0.0.2 # via -r requirements.txt
|
alembic-autogenerate-enums==0.0.2 # via -r requirements.txt
|
||||||
alembic==1.4.2 # via -r requirements.txt, flask-migrate
|
alembic==1.4.2 # via -r requirements.txt, flask-migrate
|
||||||
|
@ -17,8 +17,8 @@ bcrypt==3.1.7 # via -r requirements.txt, flask-bcrypt, paramiko
|
||||||
beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare
|
beautifulsoup4==4.9.1 # via -r requirements.txt, cloudflare
|
||||||
billiard==3.6.3.0 # via -r requirements.txt, celery
|
billiard==3.6.3.0 # via -r requirements.txt, celery
|
||||||
blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven
|
blinker==1.4 # via -r requirements.txt, flask-mail, flask-principal, raven
|
||||||
boto3==1.14.56 # via -r requirements.txt
|
boto3==1.15.2 # via -r requirements.txt
|
||||||
botocore==1.17.56 # via -r requirements.txt, boto3, s3transfer
|
botocore==1.18.2 # via -r requirements.txt, boto3, s3transfer
|
||||||
celery[redis]==4.4.2 # via -r requirements.txt
|
celery[redis]==4.4.2 # via -r requirements.txt
|
||||||
certifi==2020.6.20 # via -r requirements.txt, requests
|
certifi==2020.6.20 # via -r requirements.txt, requests
|
||||||
certsrv==2.1.1 # via -r requirements.txt
|
certsrv==2.1.1 # via -r requirements.txt
|
||||||
|
@ -29,7 +29,7 @@ cloudflare==2.8.13 # via -r requirements.txt
|
||||||
cryptography==3.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests
|
cryptography==3.1 # via -r requirements.txt, acme, josepy, paramiko, pyopenssl, requests
|
||||||
dnspython3==1.15.0 # via -r requirements.txt
|
dnspython3==1.15.0 # via -r requirements.txt
|
||||||
dnspython==1.15.0 # via -r requirements.txt, dnspython3
|
dnspython==1.15.0 # via -r requirements.txt, dnspython3
|
||||||
docutils==0.15.2 # via -r requirements.txt, botocore, sphinx
|
docutils==0.15.2 # via sphinx
|
||||||
dyn==1.8.1 # via -r requirements.txt
|
dyn==1.8.1 # via -r requirements.txt
|
||||||
flask-bcrypt==0.7.1 # via -r requirements.txt
|
flask-bcrypt==0.7.1 # via -r requirements.txt
|
||||||
flask-cors==3.0.9 # via -r requirements.txt
|
flask-cors==3.0.9 # via -r requirements.txt
|
||||||
|
|
|
@ -10,22 +10,21 @@ aws-sam-translator==1.22.0 # via cfn-lint
|
||||||
aws-xray-sdk==2.5.0 # via moto
|
aws-xray-sdk==2.5.0 # via moto
|
||||||
bandit==1.6.2 # via -r requirements-tests.in
|
bandit==1.6.2 # via -r requirements-tests.in
|
||||||
black==20.8b1 # via -r requirements-tests.in
|
black==20.8b1 # via -r requirements-tests.in
|
||||||
boto3==1.14.56 # via aws-sam-translator, moto
|
boto3==1.15.2 # via aws-sam-translator, moto
|
||||||
boto==2.49.0 # via moto
|
boto==2.49.0 # via moto
|
||||||
botocore==1.17.56 # via aws-xray-sdk, boto3, moto, s3transfer
|
botocore==1.18.2 # via aws-xray-sdk, boto3, moto, s3transfer
|
||||||
certifi==2020.6.20 # via requests
|
certifi==2020.6.20 # via requests
|
||||||
cffi==1.14.0 # via cryptography
|
cffi==1.14.0 # via cryptography
|
||||||
cfn-lint==0.29.5 # via moto
|
cfn-lint==0.29.5 # via moto
|
||||||
chardet==3.0.4 # via requests
|
chardet==3.0.4 # via requests
|
||||||
click==7.1.2 # via black, flask
|
click==7.1.2 # via black, flask
|
||||||
coverage==5.2.1 # via -r requirements-tests.in
|
coverage==5.3 # via -r requirements-tests.in
|
||||||
cryptography==3.1 # via moto, sshpubkeys
|
cryptography==3.1 # via moto, python-jose, sshpubkeys
|
||||||
decorator==4.4.2 # via networkx
|
decorator==4.4.2 # via networkx
|
||||||
docker==4.2.0 # via moto
|
docker==4.2.0 # via moto
|
||||||
docutils==0.15.2 # via botocore
|
ecdsa==0.14.1 # via moto, python-jose, sshpubkeys
|
||||||
ecdsa==0.15 # via python-jose, sshpubkeys
|
|
||||||
factory-boy==3.0.1 # via -r requirements-tests.in
|
factory-boy==3.0.1 # via -r requirements-tests.in
|
||||||
faker==4.1.2 # via -r requirements-tests.in, factory-boy
|
faker==4.1.3 # via -r requirements-tests.in, factory-boy
|
||||||
fakeredis==1.4.3 # via -r requirements-tests.in
|
fakeredis==1.4.3 # via -r requirements-tests.in
|
||||||
flask==1.1.2 # via pytest-flask
|
flask==1.1.2 # via pytest-flask
|
||||||
freezegun==1.0.0 # via -r requirements-tests.in
|
freezegun==1.0.0 # via -r requirements-tests.in
|
||||||
|
@ -43,10 +42,10 @@ jsonpatch==1.25 # via cfn-lint
|
||||||
jsonpickle==1.4 # via aws-xray-sdk
|
jsonpickle==1.4 # via aws-xray-sdk
|
||||||
jsonpointer==2.0 # via jsonpatch
|
jsonpointer==2.0 # via jsonpatch
|
||||||
jsonschema==3.2.0 # via aws-sam-translator, cfn-lint
|
jsonschema==3.2.0 # via aws-sam-translator, cfn-lint
|
||||||
markupsafe==1.1.1 # via jinja2
|
markupsafe==1.1.1 # via jinja2, moto
|
||||||
mock==4.0.2 # via moto
|
mock==4.0.2 # via moto
|
||||||
more-itertools==8.2.0 # via pytest
|
more-itertools==8.2.0 # via moto, pytest
|
||||||
moto==1.3.14 # via -r requirements-tests.in
|
moto==1.3.16 # via -r requirements-tests.in
|
||||||
mypy-extensions==0.4.3 # via black
|
mypy-extensions==0.4.3 # via black
|
||||||
networkx==2.4 # via cfn-lint
|
networkx==2.4 # via cfn-lint
|
||||||
nose==1.3.7 # via -r requirements-tests.in
|
nose==1.3.7 # via -r requirements-tests.in
|
||||||
|
@ -62,9 +61,9 @@ pyparsing==2.4.7 # via packaging
|
||||||
pyrsistent==0.16.0 # via jsonschema
|
pyrsistent==0.16.0 # via jsonschema
|
||||||
pytest-flask==1.0.0 # via -r requirements-tests.in
|
pytest-flask==1.0.0 # via -r requirements-tests.in
|
||||||
pytest-mock==3.3.1 # via -r requirements-tests.in
|
pytest-mock==3.3.1 # via -r requirements-tests.in
|
||||||
pytest==6.0.1 # via -r requirements-tests.in, pytest-flask, pytest-mock
|
pytest==6.0.2 # via -r requirements-tests.in, pytest-flask, pytest-mock
|
||||||
python-dateutil==2.8.1 # via botocore, faker, freezegun, moto
|
python-dateutil==2.8.1 # via botocore, faker, freezegun, moto
|
||||||
python-jose==3.1.0 # via moto
|
python-jose[cryptography]==3.1.0 # via moto
|
||||||
pytz==2019.3 # via moto
|
pytz==2019.3 # via moto
|
||||||
pyyaml==5.3.1 # via -r requirements-tests.in, bandit, cfn-lint, moto
|
pyyaml==5.3.1 # via -r requirements-tests.in, bandit, cfn-lint, moto
|
||||||
redis==3.5.3 # via fakeredis
|
redis==3.5.3 # via fakeredis
|
||||||
|
@ -88,7 +87,7 @@ websocket-client==0.57.0 # via docker
|
||||||
werkzeug==1.0.1 # via flask, moto, pytest-flask
|
werkzeug==1.0.1 # via flask, moto, pytest-flask
|
||||||
wrapt==1.12.1 # via aws-xray-sdk
|
wrapt==1.12.1 # via aws-xray-sdk
|
||||||
xmltodict==0.12.0 # via moto
|
xmltodict==0.12.0 # via moto
|
||||||
zipp==3.1.0 # via importlib-metadata
|
zipp==3.1.0 # via importlib-metadata, moto
|
||||||
|
|
||||||
# The following packages are considered to be unsafe in a requirements file:
|
# The following packages are considered to be unsafe in a requirements file:
|
||||||
# setuptools
|
# setuptools
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
#
|
#
|
||||||
# pip-compile --no-index --output-file=requirements.txt requirements.in
|
# pip-compile --no-index --output-file=requirements.txt requirements.in
|
||||||
#
|
#
|
||||||
acme==1.7.0 # via -r requirements.in
|
acme==1.8.0 # via -r requirements.in
|
||||||
alembic-autogenerate-enums==0.0.2 # via -r requirements.in
|
alembic-autogenerate-enums==0.0.2 # via -r requirements.in
|
||||||
alembic==1.4.2 # via flask-migrate
|
alembic==1.4.2 # via flask-migrate
|
||||||
amqp==2.5.2 # via kombu
|
amqp==2.5.2 # via kombu
|
||||||
|
@ -15,8 +15,8 @@ bcrypt==3.1.7 # via flask-bcrypt, paramiko
|
||||||
beautifulsoup4==4.9.1 # via cloudflare
|
beautifulsoup4==4.9.1 # via cloudflare
|
||||||
billiard==3.6.3.0 # via celery
|
billiard==3.6.3.0 # via celery
|
||||||
blinker==1.4 # via flask-mail, flask-principal, raven
|
blinker==1.4 # via flask-mail, flask-principal, raven
|
||||||
boto3==1.14.56 # via -r requirements.in
|
boto3==1.15.2 # via -r requirements.in
|
||||||
botocore==1.17.56 # via -r requirements.in, boto3, s3transfer
|
botocore==1.18.2 # via -r requirements.in, boto3, s3transfer
|
||||||
celery[redis]==4.4.2 # via -r requirements.in
|
celery[redis]==4.4.2 # via -r requirements.in
|
||||||
certifi==2020.6.20 # via -r requirements.in, requests
|
certifi==2020.6.20 # via -r requirements.in, requests
|
||||||
certsrv==2.1.1 # via -r requirements.in
|
certsrv==2.1.1 # via -r requirements.in
|
||||||
|
@ -27,7 +27,6 @@ cloudflare==2.8.13 # via -r requirements.in
|
||||||
cryptography==3.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests
|
cryptography==3.1 # via -r requirements.in, acme, josepy, paramiko, pyopenssl, requests
|
||||||
dnspython3==1.15.0 # via -r requirements.in
|
dnspython3==1.15.0 # via -r requirements.in
|
||||||
dnspython==1.15.0 # via dnspython3
|
dnspython==1.15.0 # via dnspython3
|
||||||
docutils==0.15.2 # via botocore
|
|
||||||
dyn==1.8.1 # via -r requirements.in
|
dyn==1.8.1 # via -r requirements.in
|
||||||
flask-bcrypt==0.7.1 # via -r requirements.in
|
flask-bcrypt==0.7.1 # via -r requirements.in
|
||||||
flask-cors==3.0.9 # via -r requirements.in
|
flask-cors==3.0.9 # via -r requirements.in
|
||||||
|
|
Loading…
Reference in New Issue