Skip to content

Commit

Permalink
Merge pull request #58 from trussworks/expire-inactive-accounts
Browse files Browse the repository at this point in the history
Expire inactive accounts
  • Loading branch information
sheenamt authored Feb 8, 2021
2 parents 0b47999 + 16cb2eb commit c279831
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 55 deletions.
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,9 @@ An auditing tool for AWS keys that audits, alerts, and disable keys if not withi

Sleuth runs periodically, normally once a day in the middle of business hours. Sleuth does the following:

- Inspect each Access Key based on set age threshold (default 90 days)
- Inspect each Access Key based on:
- set creation age threshold (default 90 days)
- set last accessed age threshold (optional, set to creation age threshold as default)
- If Access Key is approaching threshold will ping user with a reminder to cycle key
- If key age is at or over threshold will disable Access Key along with a final notice

Expand Down Expand Up @@ -81,6 +83,7 @@ module "iam_sleuth" {
ENABLE_AUTO_EXPIRE = "false"
EXPIRATION_AGE = 90
WARNING_AGE = 50
LAST_USED_AGE = 30
SLACK_URL = data.aws_ssm_parameter.slack_url.value
SNS_TOPIC = ""
MSG_TITLE = "Key Rotation Instructions"
Expand All @@ -101,8 +104,9 @@ The behavior can be configured by environment variables.
| Name | Description |
|------|------------ |
| ENABLE_AUTO_EXPIRE | Must be set to `true` for key disable action |
| EXPIRATION_AGE | Age in days to disable a AWS key |
| WARNING_AGE | Age in days of key to send notifications, must be lower than EXPIRATION_AGE |
| EXPIRATION_AGE | Age of key creation (in days) to disable a AWS key |
| WARNING_AGE | Age of key creation (in days) to send notifications, must be lower than EXPIRATION_AGE |
| LAST_USED_AGE | OPTIONAL, defaults to EXPIRATION_AGE, Age of last key usage (in days) to send notifications, must be lower than or equal to EXPIRATION_AGE |
| MSG_TITLE | Title of the notification message |
| MSG_TEXT | Instructions on key rotation |
| SLACK_URL | Incoming webhook to send notifications to |
Expand Down
1 change: 1 addition & 0 deletions examples/simple/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ module "iam_sleuth" {
ENABLE_AUTO_EXPIRE = false
EXPIRATION_AGE = 90
WARNING_AGE = 85
LAST_USED_AGE = 30
MSG_TITLE = "Key Rotation Instructions"
MSG_TEXT = "Please run key rotation tool!"
}
Expand Down
50 changes: 31 additions & 19 deletions sleuth/sleuth/auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,47 +16,57 @@ class Key():
key_id = ""
created = None
status = ""
last_used = None
audit_state = None

age = 0
creation_age = 0
access_age = 0
valid_for = 0

def __init__(self, username, key_id, status, created):
def __init__(self, username, key_id, status, created, last_used):
self.username = username
self.key_id = key_id
self.status = status
self.created = created
self.last_used = last_used

self.age = (dt.datetime.now(dt.timezone.utc) - self.created).days
self.creation_age = (dt.datetime.now(dt.timezone.utc) - self.created).days
self.access_age = (dt.datetime.now(dt.timezone.utc) - self.last_used).days

def audit(self, rotate_age, expire_age):
def audit(self, rotate_age, expire_age, max_last_used_age):
"""
Audits the key and sets the status state based on key age
Audits the key and sets the status state based on key creation age and last used age
Note if the key is below rotate the audit_state=good. If the key is disabled will be marked as disabled.
Note if the key is below rotate or last used age, the audit_state=good.
If the key is disabled will be marked as disabled.
Parameters:
rotate (int): Age key must be before audit_state=old
expire (int): Age key must be before audit_state=expire
last_used_age (int): Age of last key usage must be before audit_state=expire
Returns:
None
"""
assert(rotate_age < expire_age)
assert(max_last_used_age <= expire_age)

# set the valid_for in the object
self.valid_for = expire_age - self.age
self.valid_for = expire_age - self.creation_age

# lets audit the age
if self.age < rotate_age:
self.audit_state = 'good'
if self.age >= rotate_age and self.age < expire_age:
self.audit_state = 'old'
if self.age >= expire_age:
if self.creation_age >= expire_age:
self.audit_state = 'expire'
if self.status == 'Inactive' and os.environ['ENABLE_AUTO_EXPIRE'] == 'true':
self.audit_state = 'disabled'
elif self.access_age >= max_last_used_age:
self.audit_state = 'expire'
elif self.creation_age >= rotate_age and self.creation_age < expire_age:
self.audit_state = 'old'
elif self.creation_age < rotate_age:
self.audit_state = 'good'

# lets audit the status
if self.status == 'Inactive' and os.environ.get('ENABLE_AUTO_EXPIRE', False) == 'true':
self.audit_state = 'disabled'

class User():
username = ""
Expand All @@ -69,9 +79,9 @@ def __init__(self, user_id, username, slack_id=None):
self.username = username
self.slack_id = slack_id

def audit(self, rotate=80, expire=90):
def audit(self, rotate=80, expire=90, last_used=90):
for k in self.keys:
k.audit(rotate, expire)
k.audit(rotate, expire, last_used)

def print_key_report(users):
"""Prints table of report
Expand All @@ -92,18 +102,20 @@ def print_key_report(users):
u.slack_id,
k.key_id,
k.audit_state,
k.age
k.creation_age,
k.access_age
])

print(tabulate(tbl_data, headers=['UserName', 'Slack ID', 'Key ID', 'Status', 'Age in Days']))
print(tabulate(tbl_data, headers=['UserName', 'Slack ID', 'Key ID', 'Status', 'Age in Days', 'Last Access Age']))


def audit():
iam_users = get_iam_users()

# lets audit keys so the ages and state are set
for u in iam_users:
u.audit(int(os.environ['WARNING_AGE']), int(os.environ['EXPIRATION_AGE']))
# Do not require last used age, set to expiration age as default
u.audit(int(os.environ['WARNING_AGE']), int(os.environ['EXPIRATION_AGE']), int(os.environ.get('LAST_USED_AGE', os.environ['EXPIRATION_AGE'])))

if os.environ.get('DEBUG', False):
print_key_report(iam_users)
Expand Down
9 changes: 6 additions & 3 deletions sleuth/sleuth/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,16 @@ def get_iam_key_info(user):
list (Key): Return list of keys for a single user
"""
from sleuth.auditor import Key
resp = IAM.list_access_keys(UserName=user.username)
keys = []
for k in resp['AccessKeyMetadata']:
key_info = IAM.list_access_keys(UserName=user.username)
for k in key_info['AccessKeyMetadata']:
access_date=IAM.get_access_key_last_used(AccessKeyId=k['AccessKeyId'])
keys.append(Key(k['UserName'],
k['AccessKeyId'],
k['Status'],
k['CreateDate']))
k['CreateDate'],
access_date['AccessKeyLastUsed']['LastUsedDate'] if 'LastUsedDate' in access_date['AccessKeyLastUsed'] else k['CreateDate']))

return keys


Expand Down
66 changes: 38 additions & 28 deletions sleuth/tests/test_auditor.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,59 +5,69 @@

from sleuth.auditor import Key


@freeze_time("2019-01-16")
class TestKey():
def test_normal(self):
"""Normal happy path, key is good"""
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Active', created)
k.audit(60, 80)

assert k.age == 15
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Active', created, last_used)
k.audit(60, 80, 20)
assert k.creation_age == 15
assert k.audit_state == 'good'

def test_rotate(self):
"""Key is past rotate age, key is marked as old"""
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Active', created)
k.audit(10, 80)

assert k.audit_state == 'old'
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
key = Key('username', 'keyid', 'Active', created, last_used)
key.audit(10, 80, 20)
assert key.audit_state == 'old'

def test_old(self):
"""Key is past max threshold, key is marked as expired"""
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Active', created)
k.audit(10, 11)

assert k.audit_state == 'expire'
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
key = Key('username', 'keyid', 'Active', created, last_used)
key.audit(10, 11, 10)
assert key.audit_state == 'expire'

def test_no_disable(self, monkeypatch):
"""Key is disabled AWS status of Inactive, but disabling is turned off so key remains audit state expire"""
monkeypatch.setenv('ENABLE_AUTO_EXPIRE', 'false')
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Inactive', created)

k.audit(10, 11)
assert k.audit_state == 'expire'
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
key = Key('user2', 'ldasfkk', 'Inactive', created, last_used)
key.audit(10, 11, 10)
assert key.audit_state == 'expire'

def test_inactive(self, monkeypatch):
def test_last_used(self, monkeypatch):
"""Key has not been used in X days, key marked is disabled"""
monkeypatch.setenv('ENABLE_AUTO_EXPIRE', 'true')
monkeypatch.setenv('LAST_USED_AGE', '10')
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
key = Key('user3', 'kljin', 'Active', created, last_used)
key.audit(10, 11, 1)
assert key.audit_state == 'expire'
key.audit(60, 80, 1)
assert key.audit_state == 'expire'

def test_disabled(self, monkeypatch):
"""Key is disabled AWS status of Inactive, key marked is disabled"""
monkeypatch.setenv('ENABLE_AUTO_EXPIRE', 'true')
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Inactive', created)

k.audit(10, 11)
assert k.audit_state == 'disabled'
k.audit(60, 80)
assert k.audit_state == 'disabled'

last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
key = Key('user2', 'ldasfkk', 'Inactive', created, last_used)
key.audit(10, 11, 10)
assert key.audit_state == 'disabled'
key.audit(60, 80, 30)
assert key.audit_state == 'disabled'

def test_invalid(self):
"""Key is disabled AWS status of Inactive, key marked is disabled"""
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
k = Key('username', 'keyid', 'Inactive', created)

last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
key = Key('user2', 'ldasfkk', 'Inactive', created, last_used)
with pytest.raises(AssertionError):
k.audit(5, 1)
key.audit(5, 1, 1)
5 changes: 3 additions & 2 deletions sleuth/tests/test_services.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@
from sleuth.auditor import Key, User

created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
lastused = created = datetime.datetime(2019, 1, 3, tzinfo=datetime.timezone.utc)
user1 = User('user1', 'slackuser1', 'U12345')
user2 = User('user1', 'slackuser1', 'U67890')
key1 = Key('user1', 'asdfksakfa', 'Active', created)
key1 = Key('user1', 'asdfksakfa', 'Active', created, lastused)
key1.audit_state = 'old'
key2 = Key('user2', 'ldasfkk', 'Active', created)
key2 = Key('user2', 'ldasfkk', 'Active', created, lastused)
key2.audit_state = 'expire'
user1.keys = [key1]
user2.keys = [key2]
Expand Down

0 comments on commit c279831

Please sign in to comment.