-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathiam-checker.js
142 lines (123 loc) · 4.54 KB
/
iam-checker.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
/* This function duplicates some features of AWS Config - however AWS Config is expensive for reporting on just these items. */
import {parse as csvParse} from 'csv-parse/sync'
import backOff from 'exponential-backoff'
import {MAX_CREDENTIAL_AGE} from './runtime-envs.js'
import {buildApiForAccount, buildMultiAccountLambdaHandler} from './utils.js'
import {MonitorStore} from './monitor-store.js'
const dayInMillis = 24 * 60 * 60 * 1000
const monitorType = 'iam-checker'
const monitorStore = new MonitorStore(monitorType, 'IAM', formatIssues, addIssuePks)
async function checkOneAccount(accountId) {
const issues = []
const now = Date.now()
const maxCredentialAge = MAX_CREDENTIAL_AGE //in days
const iam = await buildApiForAccount(accountId, 'ParentAccountCliRole', 'IAM')
async function runChecks() {
await doWithBackoff('generateCredentialReport')
let response = await doWithBackoff('getCredentialReport')
let csv = response.Content.toString()
const data = csvParse(csv, {
columns: true
})
console.log(data)
let rootUsers = data.filter(entry => entry.user == '<root_account>')
let nonRootUsers = data.filter(entry => entry.user != '<root_account>')
if (rootUsers.length <= 0) {
assert('rootUsers', 'length', '> 0', rootUsers.length)
}
if (rootUsers.length + nonRootUsers.length != data.length) {
assert('all users', 'count', 'data.length', rootUsers.length + nonRootUsers.length)
}
// Check root user doesn't have any access keys
noRootAccessKeys(rootUsers)
// Check root user has MFA enabled
rootMfaEnabled(rootUsers)
// check MFA enabled for all users with console access
consoleUsersMfaEnabled(nonRootUsers)
// Check no access keys older than x days
checkCredentials(nonRootUsers)
}
function assert(resource, attribute, expected, actual) {
if (expected !== actual) {
issues.push({accountId, resource, attribute, expected, actual})
}
}
function noRootAccessKeys(rootUsers) {
rootUsers.forEach(user => {
assert(user.arn, 'access_key_1_active', 'false', user.access_key_1_active)
assert(user.arn, 'access_key_2_active', 'false', user.access_key_2_active)
})
}
function rootMfaEnabled(rootUsers) {
rootUsers.forEach(user => assert(user.arn, 'mfa_active', 'true', user.mfa_active))
}
function consoleUsersMfaEnabled(nonRootUsers) {
nonRootUsers
.filter(user => user.password_enabled === 'true')
.forEach(user => assert(user.arn, 'mfa_active', 'true', user.mfa_active))
}
function checkCredentials(nonRootUsers) {
nonRootUsers.forEach(user => {
if (user.password_enabled === 'true') {
dateMoreRecentThan(user, 'password_last_changed', maxCredentialAge)
}
if (user.access_key_1_active === 'true') {
dateMoreRecentThan(user, 'access_key_1_last_rotated', maxCredentialAge)
}
if (user.access_key_2_active === 'true') {
dateMoreRecentThan(user, 'access_key_2_last_rotated', maxCredentialAge)
}
})
}
function dateMoreRecentThan(user, attribute, maxDiff) {
let date = new Date(user[attribute]).getTime()
let diff = Math.round((now - date) / dayInMillis)
if (isNaN(diff) || diff >= maxDiff) {
assert(user.arn, attribute, `less than ${maxDiff} days ago`, diff)
}
}
async function doWithBackoff(delegate) {
const backoffParams = {
maxDelay: 65 * 1000, // 1 minute, 5 seconds
startingDelay: 4 * 1000 // 10 seconds
}
//if not ready, `iam.getCredentialReport()` will throw an error with a 'ReportInProgress' code which will cause the backoff to happen anyway
const runDelegate = async () => {
try {
return await iam[delegate].bind(iam)().promise()
} catch (e) {
console.error(`error making retryable call for ${delegate}`)
console.error(e)
throw e
}
}
let result = null
try {
result = await backOff.backOff(() => {
return runDelegate()
}, backoffParams)
} catch (e) {
console.error(`error calling backoff for ${delegate}`)
console.error(e)
throw e
}
return result
}
await runChecks()
return issues
}
function formatIssues(issues) {
return issues.map(
issue =>
`${issue.accountId}: ${issue.resource}.${issue.attribute} should be '${issue.expected}' but was '${issue.actual}'`
)
}
function addIssuePks(issues) {
issues.forEach(issue => (issue.pk = `${issue.accountId}-${issue.resource}-${issue.attribute}-${issue.expected}`))
}
async function summarise(invocationId, allAcountsData) {
let allIssues = allAcountsData.flat()
console.log('resolving issues across all accounts')
await monitorStore.summariseAndNotify(invocationId, allIssues)
}
export const handler = buildMultiAccountLambdaHandler(checkOneAccount, summarise)