Skip to content

Commit 613d5e8

Browse files
authored
Merge pull request #82 from trussworks/smt-better-slack-messaging
[MB-7353] better slack messaging
2 parents 16478d3 + 7daa3ac commit 613d5e8

File tree

8 files changed

+234
-57
lines changed

8 files changed

+234
-57
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,15 @@
33

44
All notable changes to this project will be documented in this file.
55

6+
## [1.3.0] - 2021-03-30
7+
8+
### Added
9+
10+
- New variables for INACTIVE_NOTIFICATION_TITLE and INACTIVE_NOTIFICATION_TEXT
11+
- Improved messaging for key expiration reasonings
12+
- Documentation for testing app locally
13+
- Required permission for iam:GetAccessKeyLastUsed
14+
615
## [1.2.1] - 2021-03-04
716

817
### Added

README.md

Lines changed: 82 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -84,12 +84,14 @@ module "iam_sleuth" {
8484
ENABLE_AUTO_EXPIRE = "false"
8585
EXPIRATION_AGE = 90
8686
WARNING_AGE = 50
87+
EXPIRE_NOTIFICATION_TITLE = "Key Rotation Instructions"
88+
EXPIRE_NOTIFICATION_TEXT = "Please run.\n ```aws-vault rotate AWS-PROFILE```"
8789
INACTIVITY_AGE = 30
8890
INACTIVITY_WARNING_AGE = 20
91+
INACTIVE_NOTIFICATION_TITLE = "Key Usage Instructions to prevent key auto-disable"
92+
INACTIVE_NOTIFICATION_TEXT = "Please run.\n ```aws-vault login AWS-PROFILE```"
8993
SLACK_URL = data.aws_ssm_parameter.slack_url.value
9094
SNS_TOPIC = ""
91-
MSG_TITLE = "Key Rotation Instructions"
92-
MSG_TEXT = "Please run.\n ```aws-vault rotate AWS-PROFILE```"
9395
}
9496
9597
tags = {
@@ -107,11 +109,13 @@ The behavior can be configured by environment variables.
107109
|------|------------ |
108110
| ENABLE_AUTO_EXPIRE | Must be set to `true` for key disable action |
109111
| EXPIRATION_AGE | Age of key creation (in days) to disable a AWS key |
112+
| EXPIRE_NOTIFICATION_TITLE | Title of the notification message for keys expiring due to creation age|
113+
| EXPIRE_NOTIFICATION_TEXT | Instructions on key rotation |
110114
| WARNING_AGE | Age of key creation (in days) to send notifications, must be lower than EXPIRATION_AGE |
111115
| INACTIVITY_AGE | OPTIONAL, defaults to EXPIRATION_AGE, Age of last key usage (in days) to disable AWS key, must be lower than or equal to EXPIRATION_AGE |
112116
| INACTIVITY_WARNING_AGE | REQUIRED IF INACTIVITY_AGE is set, otherwise defaults to WARNING, Age of last key usage (in days) to send notifications, must be lower than INACTIVITY_AGE |
113-
| MSG_TITLE | Title of the notification message |
114-
| MSG_TEXT | Instructions on key rotation |
117+
| INACTIVE_NOTIFICATION_TITLE | Title of the notification message for keys expiring due to inactivity |
118+
| INACTIVE_NOTIFICATION_TEXT | Instructions on key usage to prevent expiration due to inactivity |
115119
| SLACK_URL | Incoming webhook to send notifications to |
116120
| SNS_TOPIC | Topic to send a SNS formatted message to |
117121
| DEBUG | If present will log additional things |
@@ -152,8 +156,81 @@ pip install -r ./sleuth/requirements.txt
152156

153157
### Testing
154158

155-
To test the Python app:
159+
All following steps assume you have activated the virtual environment from the previous step.
160+
161+
To run the Python app unittests:
156162

157163
```sh
158164
pytest
159165
```
166+
167+
To run the python app locally, using trussworks-ci as example account:
168+
169+
1. Login to the trussworks-ci account
170+
171+
```shell
172+
aws-vault login trussworks-ci
173+
```
174+
175+
1. Create test user(s), giving them access keys and optional KeyAutoExire tag
176+
177+
| UserName | Slack ID | Key ID | AutoExpire |
178+
|--------------|--------------|----------------------|------------|
179+
| sleuth-test1 | sleuth-test1 | KEYID1 | FALSE |
180+
| sleuth-test2 | sleuth-test2 | KEYID2 | TRUE |
181+
182+
1. In the CLI, move to the sleuth subdirectory:
183+
184+
```shell
185+
cd /path/to/trussworks/terraform-aws-iam-sleuth/sleuth
186+
```
187+
188+
1. Export the relevant variables:
189+
190+
To test the warnings for creation date expiration, considering a key that was made today, use:
191+
192+
```shell
193+
export DEBUG=true
194+
export SLACK_URL=test
195+
export EXPIRATION_AGE=90
196+
export WARNING_AGE=0
197+
```
198+
199+
To test the warnings for inactivity expiration, considering a key that was made today, use:
200+
201+
```shell
202+
export DEBUG=true
203+
export SLACK_URL=test
204+
export EXPIRATION_AGE=90
205+
export WARNING_AGE=1
206+
export INACTIVITY_AGE=30
207+
export INACTIVITY_WARNING_AGE=0
208+
```
209+
210+
NOTE: Creation age expiration takes precedent over activity age, so setting both `WARNING_AGE=0` and `INACTIVITY_WARNING_AGE=0` will cause only the creation date expiration warning to appear.
211+
212+
1. Run the app
213+
214+
```shell
215+
aws-vault exec trussworks-ci -- python handler.py
216+
```
217+
218+
- Example DEBUG output for creation age, notice the 'old' status:
219+
220+
| UserName | Slack ID | Key ID | AutoExpire | Status | Age in Days | Last Access Age |
221+
|--------------|--------------|----------------------|------------|--------|-------------|-----------------|
222+
| sleuth-test1 | sleuth-test1 | KEYID1 | FALSE | good | 0 | 0 |
223+
| sleuth-test2 | sleuth-test2 | KEYID2 | TRUE | old | 0 | 0 |
224+
225+
- Example DEBUG output for inactivity age, notice the 'stagnant' status:
226+
227+
| UserName | Slack ID | Key ID | AutoExpire | Status | Age in Days | Last Access Age |
228+
|--------------|--------------|----------------------|------------|--------|-------------|-----------------|
229+
| sleuth-test1 | sleuth-test1 | KEYID1 | FALSE | good | 0 | 0 |
230+
| sleuth-test2 | sleuth-test2 | KEYID2 | TRUE | stagnant | 0 | 0 |
231+
232+
- By exporting the SLACK_URL=test in addition to DEBUG=true, you can also view the slack message output:
233+
234+
```shell
235+
slack message: {'attachments': [{'title': 'AWS IAM Key Inactivity Report', 'text': ''}, {'title': 'IAM users with access keys expiring due to inactivity. \n Please login to AWS to prevent key from being disabled', 'color': '#ffff00', 'fields': [{'title': 'Users', 'value': "sleuth-test2's key expires in 30 days due to inactivity."}]}]}
236+
```

examples/simple/main.tf

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ data "aws_iam_policy_document" "basic_task_role_policy_doc" {
5858
"iam:UpdateAccessKey",
5959
"iam:ListAccessKeys",
6060
"iam:ListUserTags",
61+
"iam:GetAccessKeyLastUsed",
6162
]
6263

6364
resources = ["arn:aws:iam::*:user/*"]

sleuth/handler.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@
3636

3737
from sleuth.auditor import audit
3838

39-
VERSION = '1.2.1'
39+
VERSION = '1.3.0'
4040

4141
def handler(event, context):
4242
"""

sleuth/sleuth/auditor.py

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,8 @@ class Key():
2121

2222
creation_age = 0
2323
access_age = 0
24-
valid_for = 0
24+
creation_valid_for = 0
25+
activity_valid_for = 0
2526

2627
def __init__(self, username, key_id, status, created, inactivity_age):
2728
self.username = username
@@ -44,7 +45,7 @@ def audit(self, rotate_age, expire_age, max_inactivity_age, inactivity_warning_a
4445
rotate (int): Age key must be before audit_state=old
4546
expire (int): Age key must be before audit_state=expire
4647
inactivity_age (int): Age of last key usage must be before audit_state=expire
47-
inactivity_warning_age (int): Age of last key usage must be before audit_state=old
48+
inactivity_warning_age (int): Age of last key usage must be before audit_state=stagnant
4849
4950
Returns:
5051
None
@@ -54,16 +55,22 @@ def audit(self, rotate_age, expire_age, max_inactivity_age, inactivity_warning_a
5455
assert(inactivity_warning_age < max_inactivity_age)
5556

5657
# set the valid_for in the object
57-
self.valid_for = expire_age - self.creation_age
58+
self.creation_valid_for = expire_age - self.creation_age
59+
self.activity_valid_for = max_inactivity_age - self.access_age
60+
5861

5962
# lets audit the age
6063
if self.creation_age >= expire_age:
6164
self.audit_state = 'expire'
6265
elif self.access_age >= max_inactivity_age:
63-
self.audit_state = 'expire'
64-
# audit key age and last used age
65-
elif (self.creation_age >= rotate_age and self.creation_age < expire_age) or (self.access_age >= inactivity_warning_age and self.access_age < max_inactivity_age):
66+
self.audit_state = 'stagnant_expire'
67+
# audit key age, which is more important than inactivity age
68+
elif (self.creation_age >= rotate_age and self.creation_age < expire_age):
6669
self.audit_state = 'old'
70+
# audit activity age, set to 'stagnant' to not confuse with AWS official "Inactive" status
71+
elif (self.access_age >= inactivity_warning_age and self.access_age < max_inactivity_age):
72+
self.audit_state = 'stagnant'
73+
# finally, if nothing requires us to alert on this key, set to good
6774
elif self.creation_age < rotate_age:
6875
self.audit_state = 'good'
6976

@@ -120,7 +127,7 @@ def audit():
120127

121128
# Check for optional env vars
122129
if (os.environ.get('INACTIVITY_AGE') and not os.environ.get('INACTIVITY_WARNING_AGE')) or (os.environ.get('INACTIVITY_WARNING_AGE') and not os.environ.get('INACTIVITY_AGE')):
123-
raise RuntimeError('Must set env var INACTIVITY_WARNING_AGE and INACTIVITY_AGE')
130+
raise RuntimeError('Must set env var INACTIVITY_WARNING_AGE and INACTIVITY_AGE together')
124131

125132
# lets audit keys so the ages and state are set
126133
for u in iam_users:
@@ -140,18 +147,21 @@ def audit():
140147
if os.environ.get('ENABLE_AUTO_EXPIRE', False) == 'true':
141148
for u in iam_users:
142149
for k in u.keys:
143-
if k.audit_state == 'expire':
150+
if k.audit_state == 'expire' or k.audit_state == 'stagnant_expire':
144151
disable_key(k, u.username)
145152
else:
146153
LOGGER.warn('Cannot disable AWS Keys, ENABLE_AUTO_EXPIRE set to False')
147154

148-
MSG_TITLE = os.environ.get('NOTIFICATION_TITLE', 'AWS IAM Key Report')
149-
MSG_TEXT = os.environ.get('NOTIFICATION_TEXT', '')
155+
# Default expiration headers
156+
EXP_MSG_TITLE = os.environ.get('EXPIRE_NOTIFICATION_TITLE', 'AWS IAM Key Expiration Report')
157+
EXP_MSG_TEXT = os.environ.get('EXPIRE_NOTIFICATION_TEXT', '')
158+
STGNT_MSG_TITLE = os.environ.get('INACTIVE_NOTIFICATION_TITLE', 'AWS IAM Key Inactivity Report')
159+
STGNT_MSG_TEXT = os.environ.get('INACTIVE_NOTIFICATION_TEXT', '')
150160

151161
# lets assemble the SNS message
152162
if os.environ.get('SNS_TOPIC', None) is not None:
153163
LOGGER.info('Detected SNS settings, preparing and sending message via SNS')
154-
send_to_slack, slack_msg = prepare_sns_message(iam_users, MSG_TITLE, MSG_TEXT)
164+
send_to_slack, slack_msg = prepare_sns_message(iam_users, EXP_MSG_TITLE, EXP_MSG_TEXT, STGNT_MSG_TITLE, STGNT_MSG_TEXT)
155165

156166
if send_to_slack:
157167
send_sns_message(os.environ['SNS_TOPIC'], slack_msg)
@@ -163,9 +173,10 @@ def audit():
163173
if os.environ.get('SLACK_URL', None) is not None:
164174
LOGGER.info('Detected Slack settings, preparing and sending message via Slack API')
165175
# lets assemble the slack message
166-
send_to_slack, slack_msg = prepare_slack_message(iam_users, MSG_TITLE, MSG_TEXT)
167-
168-
if send_to_slack:
176+
send_to_slack, slack_msg = prepare_slack_message(iam_users, EXP_MSG_TITLE, EXP_MSG_TEXT, STGNT_MSG_TITLE, STGNT_MSG_TEXT)
177+
if os.environ.get('DEBUG', False):
178+
print("slack message:", slack_msg)
179+
elif send_to_slack:
169180
send_slack_message(os.environ['SLACK_URL'], slack_msg)
170181
else:
171182
LOGGER.info('Nothing to report')

0 commit comments

Comments
 (0)