Skip to content

Commit c279831

Browse files
authored
Merge pull request #58 from trussworks/expire-inactive-accounts
Expire inactive accounts
2 parents 0b47999 + 16cb2eb commit c279831

File tree

6 files changed

+86
-55
lines changed

6 files changed

+86
-55
lines changed

README.md

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,9 @@ An auditing tool for AWS keys that audits, alerts, and disable keys if not withi
1212

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

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

@@ -81,6 +83,7 @@ module "iam_sleuth" {
8183
ENABLE_AUTO_EXPIRE = "false"
8284
EXPIRATION_AGE = 90
8385
WARNING_AGE = 50
86+
LAST_USED_AGE = 30
8487
SLACK_URL = data.aws_ssm_parameter.slack_url.value
8588
SNS_TOPIC = ""
8689
MSG_TITLE = "Key Rotation Instructions"
@@ -101,8 +104,9 @@ The behavior can be configured by environment variables.
101104
| Name | Description |
102105
|------|------------ |
103106
| ENABLE_AUTO_EXPIRE | Must be set to `true` for key disable action |
104-
| EXPIRATION_AGE | Age in days to disable a AWS key |
105-
| WARNING_AGE | Age in days of key to send notifications, must be lower than EXPIRATION_AGE |
107+
| EXPIRATION_AGE | Age of key creation (in days) to disable a AWS key |
108+
| WARNING_AGE | Age of key creation (in days) to send notifications, must be lower than EXPIRATION_AGE |
109+
| 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 |
106110
| MSG_TITLE | Title of the notification message |
107111
| MSG_TEXT | Instructions on key rotation |
108112
| SLACK_URL | Incoming webhook to send notifications to |

examples/simple/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ module "iam_sleuth" {
2929
ENABLE_AUTO_EXPIRE = false
3030
EXPIRATION_AGE = 90
3131
WARNING_AGE = 85
32+
LAST_USED_AGE = 30
3233
MSG_TITLE = "Key Rotation Instructions"
3334
MSG_TEXT = "Please run key rotation tool!"
3435
}

sleuth/sleuth/auditor.py

Lines changed: 31 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -16,47 +16,57 @@ class Key():
1616
key_id = ""
1717
created = None
1818
status = ""
19+
last_used = None
1920
audit_state = None
2021

21-
age = 0
22+
creation_age = 0
23+
access_age = 0
2224
valid_for = 0
2325

24-
def __init__(self, username, key_id, status, created):
26+
def __init__(self, username, key_id, status, created, last_used):
2527
self.username = username
2628
self.key_id = key_id
2729
self.status = status
2830
self.created = created
31+
self.last_used = last_used
2932

30-
self.age = (dt.datetime.now(dt.timezone.utc) - self.created).days
33+
self.creation_age = (dt.datetime.now(dt.timezone.utc) - self.created).days
34+
self.access_age = (dt.datetime.now(dt.timezone.utc) - self.last_used).days
3135

32-
def audit(self, rotate_age, expire_age):
36+
def audit(self, rotate_age, expire_age, max_last_used_age):
3337
"""
34-
Audits the key and sets the status state based on key age
38+
Audits the key and sets the status state based on key creation age and last used age
3539
36-
Note if the key is below rotate the audit_state=good. If the key is disabled will be marked as disabled.
40+
Note if the key is below rotate or last used age, the audit_state=good.
41+
If the key is disabled will be marked as disabled.
3742
3843
Parameters:
3944
rotate (int): Age key must be before audit_state=old
4045
expire (int): Age key must be before audit_state=expire
46+
last_used_age (int): Age of last key usage must be before audit_state=expire
4147
4248
Returns:
4349
None
4450
"""
4551
assert(rotate_age < expire_age)
52+
assert(max_last_used_age <= expire_age)
4653

4754
# set the valid_for in the object
48-
self.valid_for = expire_age - self.age
55+
self.valid_for = expire_age - self.creation_age
4956

5057
# lets audit the age
51-
if self.age < rotate_age:
52-
self.audit_state = 'good'
53-
if self.age >= rotate_age and self.age < expire_age:
54-
self.audit_state = 'old'
55-
if self.age >= expire_age:
58+
if self.creation_age >= expire_age:
5659
self.audit_state = 'expire'
57-
if self.status == 'Inactive' and os.environ['ENABLE_AUTO_EXPIRE'] == 'true':
58-
self.audit_state = 'disabled'
60+
elif self.access_age >= max_last_used_age:
61+
self.audit_state = 'expire'
62+
elif self.creation_age >= rotate_age and self.creation_age < expire_age:
63+
self.audit_state = 'old'
64+
elif self.creation_age < rotate_age:
65+
self.audit_state = 'good'
5966

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

6171
class User():
6272
username = ""
@@ -69,9 +79,9 @@ def __init__(self, user_id, username, slack_id=None):
6979
self.username = username
7080
self.slack_id = slack_id
7181

72-
def audit(self, rotate=80, expire=90):
82+
def audit(self, rotate=80, expire=90, last_used=90):
7383
for k in self.keys:
74-
k.audit(rotate, expire)
84+
k.audit(rotate, expire, last_used)
7585

7686
def print_key_report(users):
7787
"""Prints table of report
@@ -92,18 +102,20 @@ def print_key_report(users):
92102
u.slack_id,
93103
k.key_id,
94104
k.audit_state,
95-
k.age
105+
k.creation_age,
106+
k.access_age
96107
])
97108

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

100111

101112
def audit():
102113
iam_users = get_iam_users()
103114

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

108120
if os.environ.get('DEBUG', False):
109121
print_key_report(iam_users)

sleuth/sleuth/services.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,16 @@ def get_iam_key_info(user):
2525
list (Key): Return list of keys for a single user
2626
"""
2727
from sleuth.auditor import Key
28-
resp = IAM.list_access_keys(UserName=user.username)
2928
keys = []
30-
for k in resp['AccessKeyMetadata']:
29+
key_info = IAM.list_access_keys(UserName=user.username)
30+
for k in key_info['AccessKeyMetadata']:
31+
access_date=IAM.get_access_key_last_used(AccessKeyId=k['AccessKeyId'])
3132
keys.append(Key(k['UserName'],
3233
k['AccessKeyId'],
3334
k['Status'],
34-
k['CreateDate']))
35+
k['CreateDate'],
36+
access_date['AccessKeyLastUsed']['LastUsedDate'] if 'LastUsedDate' in access_date['AccessKeyLastUsed'] else k['CreateDate']))
37+
3538
return keys
3639

3740

sleuth/tests/test_auditor.py

Lines changed: 38 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -5,59 +5,69 @@
55

66
from sleuth.auditor import Key
77

8-
98
@freeze_time("2019-01-16")
109
class TestKey():
1110
def test_normal(self):
1211
"""Normal happy path, key is good"""
1312
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
14-
k = Key('username', 'keyid', 'Active', created)
15-
k.audit(60, 80)
16-
17-
assert k.age == 15
13+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
14+
k = Key('username', 'keyid', 'Active', created, last_used)
15+
k.audit(60, 80, 20)
16+
assert k.creation_age == 15
1817
assert k.audit_state == 'good'
1918

2019
def test_rotate(self):
2120
"""Key is past rotate age, key is marked as old"""
2221
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
23-
k = Key('username', 'keyid', 'Active', created)
24-
k.audit(10, 80)
25-
26-
assert k.audit_state == 'old'
22+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
23+
key = Key('username', 'keyid', 'Active', created, last_used)
24+
key.audit(10, 80, 20)
25+
assert key.audit_state == 'old'
2726

2827
def test_old(self):
2928
"""Key is past max threshold, key is marked as expired"""
3029
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
31-
k = Key('username', 'keyid', 'Active', created)
32-
k.audit(10, 11)
33-
34-
assert k.audit_state == 'expire'
30+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
31+
key = Key('username', 'keyid', 'Active', created, last_used)
32+
key.audit(10, 11, 10)
33+
assert key.audit_state == 'expire'
3534

3635
def test_no_disable(self, monkeypatch):
3736
"""Key is disabled AWS status of Inactive, but disabling is turned off so key remains audit state expire"""
3837
monkeypatch.setenv('ENABLE_AUTO_EXPIRE', 'false')
3938
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
40-
k = Key('username', 'keyid', 'Inactive', created)
41-
42-
k.audit(10, 11)
43-
assert k.audit_state == 'expire'
39+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
40+
key = Key('user2', 'ldasfkk', 'Inactive', created, last_used)
41+
key.audit(10, 11, 10)
42+
assert key.audit_state == 'expire'
4443

45-
def test_inactive(self, monkeypatch):
44+
def test_last_used(self, monkeypatch):
45+
"""Key has not been used in X days, key marked is disabled"""
46+
monkeypatch.setenv('ENABLE_AUTO_EXPIRE', 'true')
47+
monkeypatch.setenv('LAST_USED_AGE', '10')
48+
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
49+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
50+
key = Key('user3', 'kljin', 'Active', created, last_used)
51+
key.audit(10, 11, 1)
52+
assert key.audit_state == 'expire'
53+
key.audit(60, 80, 1)
54+
assert key.audit_state == 'expire'
55+
56+
def test_disabled(self, monkeypatch):
4657
"""Key is disabled AWS status of Inactive, key marked is disabled"""
4758
monkeypatch.setenv('ENABLE_AUTO_EXPIRE', 'true')
4859
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
49-
k = Key('username', 'keyid', 'Inactive', created)
50-
51-
k.audit(10, 11)
52-
assert k.audit_state == 'disabled'
53-
k.audit(60, 80)
54-
assert k.audit_state == 'disabled'
55-
60+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
61+
key = Key('user2', 'ldasfkk', 'Inactive', created, last_used)
62+
key.audit(10, 11, 10)
63+
assert key.audit_state == 'disabled'
64+
key.audit(60, 80, 30)
65+
assert key.audit_state == 'disabled'
5666

5767
def test_invalid(self):
5868
"""Key is disabled AWS status of Inactive, key marked is disabled"""
5969
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
60-
k = Key('username', 'keyid', 'Inactive', created)
61-
70+
last_used = datetime.datetime(2019, 1, 2, tzinfo=datetime.timezone.utc)
71+
key = Key('user2', 'ldasfkk', 'Inactive', created, last_used)
6272
with pytest.raises(AssertionError):
63-
k.audit(5, 1)
73+
key.audit(5, 1, 1)

sleuth/tests/test_services.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
from sleuth.auditor import Key, User
88

99
created = datetime.datetime(2019, 1, 1, tzinfo=datetime.timezone.utc)
10+
lastused = created = datetime.datetime(2019, 1, 3, tzinfo=datetime.timezone.utc)
1011
user1 = User('user1', 'slackuser1', 'U12345')
1112
user2 = User('user1', 'slackuser1', 'U67890')
12-
key1 = Key('user1', 'asdfksakfa', 'Active', created)
13+
key1 = Key('user1', 'asdfksakfa', 'Active', created, lastused)
1314
key1.audit_state = 'old'
14-
key2 = Key('user2', 'ldasfkk', 'Active', created)
15+
key2 = Key('user2', 'ldasfkk', 'Active', created, lastused)
1516
key2.audit_state = 'expire'
1617
user1.keys = [key1]
1718
user2.keys = [key2]

0 commit comments

Comments
 (0)