mirror of
https://github.com/BishopFox/cloudfox.git
synced 2026-11-07 17:05:53 -10:00
Compare commits
496
Commits
v1.6.0
...
codeowners
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
df3fb6eae0 | ||
|
|
8e94b3c46a | ||
|
|
94628aae73 | ||
|
|
23e8878e49 | ||
|
|
dee22b3ebc | ||
|
|
84369247a7 | ||
|
|
334583d3a2 | ||
|
|
b73cd90323 | ||
|
|
ce0310675d | ||
|
|
f9d9ec4bb8 | ||
|
|
125c1ea0cb | ||
|
|
00e63b0e18 | ||
|
|
eaac503bff | ||
|
|
cec0d49602 | ||
|
|
6a21b9665d | ||
|
|
a1903f67f7 | ||
|
|
2f99a7642f | ||
|
|
8d039323bf | ||
|
|
a40f0aa179 | ||
|
|
23409e3b82 | ||
|
|
fd01647a8a | ||
|
|
f415f991c8 | ||
|
|
15e0507ad6 | ||
|
|
80c00f3d97 | ||
|
|
59afbb381e | ||
|
|
ca437d3626 | ||
|
|
1ca5dbfb10 | ||
|
|
f00044670b | ||
|
|
ae1dac6c21 | ||
|
|
237b0735a8 | ||
|
|
18e38bf1ea | ||
|
|
b5908fc720 | ||
|
|
29514a96b5 | ||
|
|
b2c450ee02 | ||
|
|
a65610390b | ||
|
|
997c13d4de | ||
|
|
1d1d198a01 | ||
|
|
6069267758 | ||
|
|
e08967bb31 | ||
|
|
8e99347789 | ||
|
|
ff810ba17d | ||
|
|
23fc346e4f | ||
|
|
89789a05c1 | ||
|
|
5827abb2a2 | ||
|
|
13a03c5de2 | ||
|
|
dd6bfefa9d | ||
|
|
8c4bcc035b | ||
|
|
a38451c1cd | ||
|
|
5df1ca6d75 | ||
|
|
ea2ee3dd2c | ||
|
|
97e2fef388 | ||
|
|
1d53b984ac | ||
|
|
525f262460 | ||
|
|
f13df0708c | ||
|
|
7520999386 | ||
|
|
f1b6eae9f1 | ||
|
|
06f8e36be2 | ||
|
|
cba252d151 | ||
|
|
5c4d0fda89 | ||
|
|
d648e5b347 | ||
|
|
d1af1bbf8f | ||
|
|
2b19d0ac62 | ||
|
|
cef6c9e656 | ||
|
|
6e146a567a | ||
|
|
ae7ed9e19a | ||
|
|
a690ad3185 | ||
|
|
26cb99778c | ||
|
|
98af83933e | ||
|
|
82738dbf63 | ||
|
|
96ab676f49 | ||
|
|
83866f46cc | ||
|
|
7343852a66 | ||
|
|
b1c248bbe6 | ||
|
|
75a446764d | ||
|
|
9be3792c97 | ||
|
|
27a511073e | ||
|
|
a9a0c7ae30 | ||
|
|
2ed079686d | ||
|
|
5e1ed54a28 | ||
|
|
ff38b2cf65 | ||
|
|
1819b11c5f | ||
|
|
2ce1f0f82f | ||
|
|
5eff1c71ac | ||
|
|
f51c1e9f49 | ||
|
|
8f5a571ef0 | ||
|
|
7b95a05215 | ||
|
|
e0f0627de8 | ||
|
|
d505bc4e3e | ||
|
|
6a6f7586b3 | ||
|
|
06344b9421 | ||
|
|
54f784903a | ||
|
|
09f33f0908 | ||
|
|
2dc20eaa52 | ||
|
|
7656267367 | ||
|
|
b43d6ad074 | ||
|
|
1215a27e19 | ||
|
|
7d3a61afe2 | ||
|
|
909590860d | ||
|
|
4ee0da839d | ||
|
|
57d9e911f7 | ||
|
|
dad2277942 | ||
|
|
50493e6624 | ||
|
|
818c3a88f4 | ||
|
|
bcfa318b31 | ||
|
|
f9567c7fc2 | ||
|
|
187299ae82 | ||
|
|
8d145b135e | ||
|
|
729c59e198 | ||
|
|
6d1b47128e | ||
|
|
217ac7a018 | ||
|
|
4b0238c362 | ||
|
|
f78f1490d2 | ||
|
|
0159638ab7 | ||
|
|
1ff35a72ad | ||
|
|
29bfd2c863 | ||
|
|
40e0c92ef1 | ||
|
|
9f8ae08262 | ||
|
|
c85aa5de89 | ||
|
|
42d7cfef1e | ||
|
|
a5bce5ad8c | ||
|
|
8221b5ff6d | ||
|
|
17011f345c | ||
|
|
aa42c8b808 | ||
|
|
5aab3b4bea | ||
|
|
01d320d64d | ||
|
|
5132622a13 | ||
|
|
08b5cd17af | ||
|
|
102b91e2c7 | ||
|
|
3a19fdb2fe | ||
|
|
d94fafe2b4 | ||
|
|
00ed8a7336 | ||
|
|
60daff386c | ||
|
|
cef1aa3a82 | ||
|
|
e88cb77394 | ||
|
|
fc52e01192 | ||
|
|
659b2e7248 | ||
|
|
06a7e7ec28 | ||
|
|
602332a0c5 | ||
|
|
471da59da2 | ||
|
|
d4ae745e8e | ||
|
|
dc0396ff33 | ||
|
|
c11ebf9a07 | ||
|
|
6c8936cfe4 | ||
|
|
fca0c4da14 | ||
|
|
2c5fec49c3 | ||
|
|
a9b95d9142 | ||
|
|
0a74adea9a | ||
|
|
adb9bcd55a | ||
|
|
03177e8726 | ||
|
|
1fb9f4c3e0 | ||
|
|
3c6c59ec4b | ||
|
|
3f8b828a6c | ||
|
|
b8bf12f1c4 | ||
|
|
690b3c8c77 | ||
|
|
845b69e9a0 | ||
|
|
fc7a57129f | ||
|
|
f6ec6a12f7 | ||
|
|
56075588d5 | ||
|
|
7b51d9da96 | ||
|
|
a0a64c579c | ||
|
|
2b294e5845 | ||
|
|
d5956cebb0 | ||
|
|
9581425231 | ||
|
|
ce6c3f76fa | ||
|
|
497c7940d2 | ||
|
|
b29e4ea6b3 | ||
|
|
2756cd0f45 | ||
|
|
c0597764a9 | ||
|
|
ef1b37cef7 | ||
|
|
8e9bd42a1e | ||
|
|
8b2f13ca88 | ||
|
|
8722782eae | ||
|
|
0397b5df5f | ||
|
|
5f7596b497 | ||
|
|
0276ce25a4 | ||
|
|
7490d1aaa1 | ||
|
|
8ef71a6388 | ||
|
|
365286665c | ||
|
|
537d064c6b | ||
|
|
766fe55484 | ||
|
|
51f0b65087 | ||
|
|
186cd04405 | ||
|
|
56247f40d4 | ||
|
|
bc91da000c | ||
|
|
02db8a4fe8 | ||
|
|
dc7c8c6f68 | ||
|
|
1aca8cf4cf | ||
|
|
8550561cd7 | ||
|
|
7f413fc9d1 | ||
|
|
b3620ca281 | ||
|
|
e6b5604a08 | ||
|
|
47c11840d3 | ||
|
|
77c7edfc82 | ||
|
|
cecebb82cb | ||
|
|
f2f93779ca | ||
|
|
59157b40d1 | ||
|
|
07c4ddc755 | ||
|
|
05481ae748 | ||
|
|
49eada30b6 | ||
|
|
bb1313c5c8 | ||
|
|
fc736f4946 | ||
|
|
d02b5e8f0e | ||
|
|
41be2192d3 | ||
|
|
96587ff450 | ||
|
|
2e65bb319e | ||
|
|
fc3379e7ac | ||
|
|
2b686e9a3b | ||
|
|
7300fc9eb3 | ||
|
|
303341c634 | ||
|
|
31027ba4cb | ||
|
|
40933e2f81 | ||
|
|
a8b73ef60d | ||
|
|
b9f9a4fdab | ||
|
|
3238ac866a | ||
|
|
2563ebbb72 | ||
|
|
512602079b | ||
|
|
152f7eaa8d | ||
|
|
6aae684307 | ||
|
|
dbc9b1c2d3 | ||
|
|
7c55958f54 | ||
|
|
ac21f3a8f0 | ||
|
|
f207a7c095 | ||
|
|
cd66bbf52a | ||
|
|
dc7560c648 | ||
|
|
466c8fca53 | ||
|
|
2e8a454d04 | ||
|
|
99b798b556 | ||
|
|
78ff6651be | ||
|
|
f1670f67da | ||
|
|
ba57a02a20 | ||
|
|
6599ad2e83 | ||
|
|
925014f001 | ||
|
|
7058d458e0 | ||
|
|
8b5f79dc82 | ||
|
|
bb1f3388f3 | ||
|
|
4081025480 | ||
|
|
b64526f04f | ||
|
|
78cdf2ef91 | ||
|
|
3d054eadee | ||
|
|
767d2e6bca | ||
|
|
7ec51b1b96 | ||
|
|
10742b2f8b | ||
|
|
abeaa25922 | ||
|
|
91aa229679 | ||
|
|
c265e5cb86 | ||
|
|
f514b80f38 | ||
|
|
a0c8e4b2c4 | ||
|
|
1c64fa50b9 | ||
|
|
1ffd763a85 | ||
|
|
5e87e968fd | ||
|
|
12e9d5a3b1 | ||
|
|
45cd84bcaa | ||
|
|
acabb09592 | ||
|
|
b8515157f9 | ||
|
|
57aa072dda | ||
|
|
dd688afd3e | ||
|
|
f90a52c58a | ||
|
|
cb618d29de | ||
|
|
44311b5ecb | ||
|
|
d7634a8a44 | ||
|
|
3fe4ac6afa | ||
|
|
1bb3bde996 | ||
|
|
a9553c4e68 | ||
|
|
840e3d3087 | ||
|
|
b537c62dfa | ||
|
|
c52a22c4f5 | ||
|
|
d4eb166e53 | ||
|
|
097baa072a | ||
|
|
c9daf18231 | ||
|
|
fff39851a4 | ||
|
|
63d932792b | ||
|
|
2b04fbb39a | ||
|
|
6e9aa37d4a | ||
|
|
c013090792 | ||
|
|
9f0d99ab32 | ||
|
|
d92e8bb487 | ||
|
|
05bcf9c3ed | ||
|
|
376e1da619 | ||
|
|
c60c33782c | ||
|
|
c2b7a50bbd | ||
|
|
a8103f118d | ||
|
|
9b5b9fad56 | ||
|
|
0032392096 | ||
|
|
6777ef2032 | ||
|
|
8f3c24e05b | ||
|
|
529d6322f3 | ||
|
|
1c334b5da4 | ||
|
|
afbe710af9 | ||
|
|
1c12c84027 | ||
|
|
1e275d39de | ||
|
|
362fa0c91e | ||
|
|
5a6e5c1ebc | ||
|
|
91976b85e6 | ||
|
|
a5c5ec92d2 | ||
|
|
ded97a0f09 | ||
|
|
2c17ad1889 | ||
|
|
d8488e1fe1 | ||
|
|
385bf52289 | ||
|
|
2007026dbe | ||
|
|
b1dc875bec | ||
|
|
56e265bb49 | ||
|
|
4b424a3bcd | ||
|
|
0ea1895e2c | ||
|
|
6bbc528831 | ||
|
|
8ed67eadea | ||
|
|
2f518c923e | ||
|
|
17627539db | ||
|
|
3d7bf089dd | ||
|
|
5d8e9e95a7 | ||
|
|
e56634d7d5 | ||
|
|
0ceaa0a767 | ||
|
|
8dc330494b | ||
|
|
d5de698d7b | ||
|
|
433f54d007 | ||
|
|
846f28e8be | ||
|
|
d455f4cde4 | ||
|
|
01267d55e9 | ||
|
|
73207a700d | ||
|
|
97ec9f5588 | ||
|
|
3365c99559 | ||
|
|
ff04f267d2 | ||
|
|
383d28ab2c | ||
|
|
5cad2b7e28 | ||
|
|
43acb6884d | ||
|
|
66fa97ed8a | ||
|
|
ac4552d994 | ||
|
|
a0ee41fb12 | ||
|
|
56781c8303 | ||
|
|
053fc3a0c9 | ||
|
|
5b8abcd29a | ||
|
|
e9d37e9ec7 | ||
|
|
ecb41d5494 | ||
|
|
39626e44fe | ||
|
|
a61ff6bd9e | ||
|
|
c567c4f7d2 | ||
|
|
56def9b276 | ||
|
|
86dde7e600 | ||
|
|
9cba6bd4bf | ||
|
|
0f1e003ebb | ||
|
|
09e59bbd6b | ||
|
|
0ced51004d | ||
|
|
e509639aa4 | ||
|
|
8f02f3bbb2 | ||
|
|
3c4612b36e | ||
|
|
11eb8b3efb | ||
|
|
a09b549417 | ||
|
|
ca66af7e61 | ||
|
|
5fde076e09 | ||
|
|
9d826ded78 | ||
|
|
06029e8927 | ||
|
|
d179e8d540 | ||
|
|
13ae4b992e | ||
|
|
129a8ce76d | ||
|
|
8f76cd9581 | ||
|
|
b8608a183f | ||
|
|
ae3ac75f23 | ||
|
|
f042e07dd5 | ||
|
|
732ae09a59 | ||
|
|
1339a12329 | ||
|
|
274f16f24b | ||
|
|
b92f61d4d0 | ||
|
|
964dd5ef1c | ||
|
|
924c9dbd6e | ||
|
|
90261dd140 | ||
|
|
d460e63cea | ||
|
|
c450480810 | ||
|
|
72ce4f6575 | ||
|
|
ee151be593 | ||
|
|
0cce67728e | ||
|
|
037172cd22 | ||
|
|
68328987f7 | ||
|
|
5a71948556 | ||
|
|
8c00a82dc3 | ||
|
|
52ac0b6144 | ||
|
|
b1cff22686 | ||
|
|
d67a017bdd | ||
|
|
8fbfb38350 | ||
|
|
f2cdf4d3dc | ||
|
|
59a7bd5aaa | ||
|
|
18a500f1b7 | ||
|
|
e262dae64f | ||
|
|
4a992d3172 | ||
|
|
5e7bfb3cbd | ||
|
|
a7cccdb7ea | ||
|
|
127f2c0fff | ||
|
|
1926e2adb0 | ||
|
|
d1d335fa12 | ||
|
|
73da61eb37 | ||
|
|
7cb4ba349d | ||
|
|
e1af1b8bcd | ||
|
|
1caa02c309 | ||
|
|
3b26a03d8d | ||
|
|
63e9338a2d | ||
|
|
36740bca65 | ||
|
|
118ed625d6 | ||
|
|
530dcbdb67 | ||
|
|
79eb720e72 | ||
|
|
c3dac71f1d | ||
|
|
ecdd33c522 | ||
|
|
1c243c73dc | ||
|
|
ca7df5a039 | ||
|
|
8850384fc0 | ||
|
|
dfae1ce189 | ||
|
|
0e8febd690 | ||
|
|
775c233858 | ||
|
|
9fc38f5e64 | ||
|
|
d20b2b1099 | ||
|
|
eb95e887de | ||
|
|
7641a3ecf4 | ||
|
|
064f285652 | ||
|
|
3e0718636f | ||
|
|
b2033c0f82 | ||
|
|
d9b5c13b3b | ||
|
|
f672c34806 | ||
|
|
4d4992da33 | ||
|
|
a5d4124bb1 | ||
|
|
6123172784 | ||
|
|
0e12ca20b2 | ||
|
|
fe3a09ae4d | ||
|
|
6ace00021b | ||
|
|
5183b32d75 | ||
|
|
ffa8e09fbe | ||
|
|
26562574c3 | ||
|
|
2e82cc895d | ||
|
|
dadef384c0 | ||
|
|
e877139c76 | ||
|
|
206542faf7 | ||
|
|
5ec61019fb | ||
|
|
e919c3c9e3 | ||
|
|
61da82896b | ||
|
|
3e3a9e8745 | ||
|
|
40d7a3c2d0 | ||
|
|
8ce9a00697 | ||
|
|
7d8ebc27c7 | ||
|
|
45e548cf0b | ||
|
|
9498d2db13 | ||
|
|
71e85c29d2 | ||
|
|
5525266855 | ||
|
|
a28d4e8556 | ||
|
|
ee7c3c0387 | ||
|
|
467f6b459b | ||
|
|
cc38c1f147 | ||
|
|
bb879c34c2 | ||
|
|
00317a5336 | ||
|
|
8bfee76c28 | ||
|
|
712ec9c382 | ||
|
|
7c8afb6fef | ||
|
|
1a5f64e8c5 | ||
|
|
7c323f4f75 | ||
|
|
3db423b740 | ||
|
|
abe6f3da61 | ||
|
|
16fcbc2fdd | ||
|
|
7cafe1f066 | ||
|
|
59b082a40b | ||
|
|
b33653822c | ||
|
|
e15c71734e | ||
|
|
065e04affe | ||
|
|
26dbb56d60 | ||
|
|
58b9c7306e | ||
|
|
18f6cbe35f | ||
|
|
8cdb854740 | ||
|
|
dfe2fc1037 | ||
|
|
e35aa612ba | ||
|
|
89c6c0ddb3 | ||
|
|
6aee4780b3 | ||
|
|
351a9cc724 | ||
|
|
b3531faead | ||
|
|
7ad15cc599 | ||
|
|
95d1fbc621 | ||
|
|
91ad816b75 | ||
|
|
cd3a4be1ff | ||
|
|
b05254d434 | ||
|
|
79dbe13468 | ||
|
|
a63c62d5b1 | ||
|
|
a342c4da04 | ||
|
|
9a7ecc5a0f | ||
|
|
8dd7fba172 | ||
|
|
bc72d939f3 | ||
|
|
fe96654ae7 | ||
|
|
0e2a03385a | ||
|
|
b18d1a29ed | ||
|
|
a8d086bf2a | ||
|
|
752833f11d | ||
|
|
8b83d5b1bc | ||
|
|
a5f6317c6d | ||
|
|
8859c62013 | ||
|
|
95b237177d | ||
|
|
ee89a2dc67 | ||
|
|
f7c0e91f0e | ||
|
|
a9f2c5c25f | ||
|
|
ea852070b3 | ||
|
|
bf2e5aa75d | ||
|
|
fb85cb8ccb | ||
|
|
d3ac589701 | ||
|
|
31d8c8efd6 | ||
|
|
664569b70b |
No files matched your search
+1
-1
@@ -1 +1 @@
|
||||
@sethsec-bf carlosvendramini-bf
|
||||
* @bishopfaure @dbravo-bishopfox
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 434 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 273 KiB |
@@ -15,10 +15,10 @@ jobs:
|
||||
|
||||
steps:
|
||||
|
||||
- name: Go 1.18
|
||||
uses: actions/setup-go@v2
|
||||
- name: Go 1.21
|
||||
uses: actions/setup-go@v4
|
||||
with:
|
||||
go-version: ^1.18
|
||||
go-version: ^1.20
|
||||
id: go
|
||||
|
||||
- id: install-secret-key
|
||||
@@ -28,7 +28,7 @@ jobs:
|
||||
gpg --list-secret-keys --keyid-format LONG
|
||||
|
||||
- name: Check Out Code
|
||||
uses: actions/checkout@v2
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Git Fetch Tags
|
||||
run: git fetch --prune --unshallow --tags -f
|
||||
|
||||
@@ -0,0 +1,16 @@
|
||||
# This Action uses minimal steps to run in ~5 seconds to rapidly:
|
||||
# Looks for typos in the codebase using codespell
|
||||
# https://github.com/codespell-project/codespell#readme
|
||||
name: codespell
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
jobs:
|
||||
codespell:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- run: pip install --user codespell
|
||||
- run: codespell --ignore-words-list="aks" --skip="*.sum"
|
||||
+8
-2
@@ -15,6 +15,8 @@
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
.vscode
|
||||
.idea
|
||||
launch.json
|
||||
|
||||
######################################################
|
||||
|
||||
@@ -61,8 +63,12 @@ terraform.rc
|
||||
|
||||
untracked/*
|
||||
output/*
|
||||
cloudfox-output/*
|
||||
*cloudfox-output*
|
||||
cloudfox
|
||||
*.log
|
||||
*.bak
|
||||
table_output.txt
|
||||
*.txt
|
||||
*.json
|
||||
*.csv
|
||||
*.log
|
||||
dist/
|
||||
@@ -4,22 +4,43 @@ CloudFox helps you gain situational awareness in unfamiliar cloud environments.
|
||||
#### CloudFox helps you answer the following common questions (and many more):
|
||||
|
||||
* What regions is this AWS account using and roughly how many resources are in the account?
|
||||
* What secrets are lurking in EC2 userdata or service specific environment variables?
|
||||
* What secrets are lurking in EC2 userdata or service specific environment variables?
|
||||
* What workloads have administrative permissions attached?
|
||||
* What actions/permissions does this [principal] have?
|
||||
* What roles trusts are overly permissive or allow cross-account assumption?
|
||||
* What role trusts are overly permissive or allow cross-account assumption?
|
||||
* What endpoints/hostnames/IPs can I attack from an external starting point (public internet)?
|
||||
* What endpoints/hostnames/IPs can I attack from an internal starting point (assumed breach within the VPC)?
|
||||
* What filesystems can I potentially mount from a compromised resource inside the VPC?
|
||||
|
||||
## Quickstart
|
||||
CloudFox is modular (you can run one command at a time), but there is an aws `all-checks` command that will run the other aws commands for you with sane defaults:
|
||||
## Demos, Examples, Walkthroughs
|
||||
* [Blog - Introducing: CloudFox](https://bishopfox.com/blog/introducing-cloudfox)
|
||||
* [Video - CloudFox + CloudFoxable A Powerful Duo for Mastering the Art of Identifying and Exploiting AWS Attack Paths](https://www.youtube.com/watch?v=RdQiIvCrSzk)
|
||||
* [Video - Penetration Testing with CloudFox](https://www.youtube.com/watch?v=Ljt_JUp5HbM)
|
||||
* [Video - CloudFox Intro Demos](https://www.youtube.com/watch?v=ReWoUgpUuiQ)
|
||||
* [Video - Tool Talk: CloudFox AWS sub-command walkthroughs](https://youtu.be/KKsYfL5uVU4?t=360)
|
||||
|
||||
## Intentionally Vulnerable Playground
|
||||
* [CloudFoxable - A Gamified Cloud Hacking Sandbox](https://cloudfoxable.bishopfox.com/)
|
||||
|
||||
## Want to chat about CloudFox?
|
||||
Join us on the [RedSec discord server](https://discord.gg/redsec)
|
||||
|
||||
|
||||
## Quick Start
|
||||
CloudFox is modular (you can run one command at a time), but there is an `aws all-checks` command that will run the other aws commands for you with sane defaults:
|
||||
|
||||
`cloudfox aws --profile [profile-name] all-checks`
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
CloudFox is designed to be executed by a principal with limited read-only permissions, but it's purpose is to help you find attack paths that can be exploited in simulated compromise scenarios (aka, objective based penetration testing).
|
||||
### White Box Enumeration
|
||||
CloudFox was designed to be executed by a principal with limited read-only permissions, but it's purpose is to help you find attack paths that can be exploited in simulated compromise scenarios (aka, objective based penetration testing).
|
||||
|
||||
### Black Box Enumeration
|
||||
CloudFox can be used with "found" credentials, similar to how you would use [weirdAAL](https://github.com/carnal0wnage/weirdAAL) or [enumerate-iam](https://github.com/andresriancho/enumerate-iam). Checks that fail, do so silently, so any data returned means your "found" creds have the access needed to retrieve it.
|
||||
|
||||
### Documentation
|
||||
For the full documentation please refer to our [wiki](https://github.com/BishopFox/CloudFox/wiki).
|
||||
|
||||
|
||||
@@ -27,63 +48,108 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
|
||||
| Provider| CloudFox Commands |
|
||||
| - | - |
|
||||
| AWS | 15 |
|
||||
| Azure | 2 |
|
||||
| GCP | Support Planned |
|
||||
| AWS | 33 |
|
||||
| Azure | 4 |
|
||||
| GCP | Support Planned |
|
||||
| Kubernetes | Support Planned |
|
||||
|
||||
|
||||
# Install
|
||||
|
||||
**Option 1:** Download the [latest binary release](https://github.com/BishopFox/cloudfox/releases) for your platform.
|
||||
|
||||
**Option 2:** [Install Go](https://golang.org/doc/install), clone the cloudfox repository and compile from source
|
||||
**Option 2:** If you use homebrew: `brew install cloudfox`
|
||||
|
||||
**Option 3:** [Install Go](https://golang.org/doc/install), use `go install github.com/BishopFox/cloudfox@latest` to install from the remote source
|
||||
|
||||
**Option 4:** Developer mode:
|
||||
|
||||
[Install Go](https://golang.org/doc/install), clone the CloudFox repository and compile from source
|
||||
```
|
||||
# git clone https://github.com/BishopFox/cloudfox.git
|
||||
...omitted for brevity...
|
||||
# cd ./cloudfox
|
||||
# Make any changes necessary
|
||||
# go build .
|
||||
# ./cloudfox
|
||||
```
|
||||
|
||||
**Option 5:** Testing a bug fix
|
||||
```
|
||||
git clone [email protected]:BishopFox/cloudfox.git
|
||||
git checkout seth-dev
|
||||
go build .
|
||||
./cloudfox [rest of the command options]
|
||||
```
|
||||
|
||||
# Prerequisites
|
||||
|
||||
|
||||
### AWS
|
||||
* AWS CLI installed
|
||||
* Supports AWS profiles, AWS environment variables, or metadata retrieval (on an ec2 instance)
|
||||
* A principal with one recommended policies attacehd (described below)
|
||||
* Reccommended attached policies: **`SecurityAudit` + [CloudFox custom policy](./misc/aws/cloudfox-policy.json)**
|
||||
* Additional policy notes (as of 09/2022):
|
||||
* [The cloudfox policy we created](./misc/aws/cloudfox-policy.json) has a complete list of every permission cloudfox uses and nothing else
|
||||
* `arn:aws:iam::aws:policy/SecurityAudit` - Covers most cloudfox checks but is missing newer services or permissions like apprunner:\*, grafana:\*, lambda:GetFunctionURL, lightsail:GetContainerServices
|
||||
* `arn:aws:iam::aws:policy/job-function/ViewOnlyAccessetcchecks` - Covers most cloudfox checks but is missing newer services or permissions like AppRunner:\*, grafana:\*, lambda:GetFunctionURL, lightsail:GetContainerServices - and is also missing iam:SimulatePrincipalPolicy.
|
||||
* `arn:aws:iam::aws:policy/ReadOnlyAccess` - Only missing AppRunner, but also grants things like "s3:Get*" which can be overly permissive.
|
||||
* `arn:aws:iam::aws:policy/AdministratorAccess` - This will work just fine with CloudFox, but if you were given this level of access as a penetration tester, that should probably be a finding in itself :)
|
||||
* To run commands on multiple profiles at once, you can specify the path to a file with a list of profile names separated by a new line using the `-l` flag or pass all stored profiles with the `-a` flag.
|
||||
* A principal with one recommended policies attached (described below)
|
||||
* Recommended attached policies: **`SecurityAudit` + [CloudFox custom policy](./misc/aws/cloudfox-policy.json)**
|
||||
|
||||
Additional policy notes (as of 09/2022):
|
||||
|
||||
| Policy | Notes |
|
||||
| - | - |
|
||||
| [CloudFox custom policy](./misc/aws/cloudfox-policy.json) | Has a complete list of every permission cloudfox uses and nothing else |
|
||||
| `arn:aws:iam::aws:policy/SecurityAudit` | Covers most cloudfox checks but is missing newer services or permissions like apprunner:\*, grafana:\*, lambda:GetFunctionURL, lightsail:GetContainerServices |
|
||||
| `arn:aws:iam::aws:policy/job-function/ViewOnlyAccess` | Covers most cloudfox checks but is missing newer services or permissions like AppRunner:\*, grafana:\*, lambda:GetFunctionURL, lightsail:GetContainerServices - and is also missing iam:SimulatePrincipalPolicy.
|
||||
| `arn:aws:iam::aws:policy/ReadOnlyAccess` | Only missing AppRunner, but also grants things like "s3:Get*" which can be overly permissive. |
|
||||
| `arn:aws:iam::aws:policy/AdministratorAccess` | This will work just fine with CloudFox, but if you were handed this level of access as a penetration tester, that should probably be a finding in itself :) |
|
||||
|
||||
### Azure
|
||||
* Viewer or similar permissions applied.
|
||||
|
||||
# Supported Commands
|
||||
# AWS Commands
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| AWS | [all-checks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#all-checks) | Run all of the other commands using reasonable defaults. You'll still want to check out the non-default options of each command, but this is a great place to start. |
|
||||
| AWS | [access-keys](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#access-keys) | Lists active access keys for all users. Useful for cross referencing a key you found with which in-scope account it belongs to. |
|
||||
| AWS | [api-gw](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#api-gw) | Lists API gateway endpoints and gives you custom curl commands including API tokens if they are stored in metadata. |
|
||||
| AWS | [buckets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Lists the buckets in the account and gives you handy commands for inspecting them further. |
|
||||
| AWS | [cloudformation](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#cloudformation) | Lists the cloudformation stacks in the account. Generates loot file with stack details, stack parameters, and stack output - look for secrets. |
|
||||
| AWS | [codebuild](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#codebuild) | Enumerate CodeBuild projects |
|
||||
| AWS | [databases](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#databases) | Enumerate RDS databases. Get a loot file with connection strings. |
|
||||
| AWS | [ecr](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecr) | List the most recently pushed image URI from all repositories. Use the loot file to pull selected images down with docker/nerdctl for inspection. |
|
||||
| AWS | [ecs-tasks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ecs-tasks) | List all ecs tasks. This returns a list of ecs tasks and associated cluster, task definition, container instance, launch type, and associated IAM principal. |
|
||||
| AWS | [eks](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#eks) | List all EKS clusters, see if they expose their endpoint publicly, and check the associated IAM roles attached to reach cluster or node group. Generates a loot file with the `aws eks udpate-kubeconfig` command needed to connect to each cluster. |
|
||||
| AWS | [elastic-network-interfaces](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#eni) | List all eni information. This returns a list of eni ID, type, external IP, private IP, VPCID, attached instance and a description. |
|
||||
| AWS | [endpoints](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#endpoints) | Enumerates endpoints from various services. Scan these endpoints from both an internal and external position to look for things that don't require authentication, are misconfigured, etc. |
|
||||
| AWS | [env-vars](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#env-vars) | Grabs the environment variables from services that have them (App Runner, ECS, Lambda, Lightsail containers, Sagemaker are supported. If you find a sensitive secret, use `cloudfox iam-simulator` AND `pmapper` to see who has access to them. |
|
||||
| AWS | [filesystems](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#filesystems) | Enumerate the EFS and FSx filesystems that you might be able to mount without creds (if you have the right network access). For example, this is useful when you have `ec:RunInstance` but not `iam:PassRole`. |
|
||||
| AWS | [iam-simulator](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#iam-simulator) | Like pmapper, but uses the IAM policy simulator. It uses AWS's evaluation logic, but notably, it doesn't consider transitive access via privesc, which is why you should also always also use pmapper. |
|
||||
| AWS | [instances](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#instances) | Enumerates useful information for EC2 Instances in all regions like name, public/private IPs, and instance profiles. Generates loot files you can feed to nmap and other tools for service enumeration. |
|
||||
| AWS | [inventory](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#inventory) | Gain a rough understanding of size of the account and preferred regions. |
|
||||
| AWS | [outbound-assumed-roles](#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [lambda](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#lambda) | Lists the lambda functions in the account, including which one's have admin roles attached. Also gives you handy commands for downloading each function. |
|
||||
| AWS | [network-ports](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#network-ports) | Enumerates AWS services that are potentially exposing a network service. The security groups and the network ACLs are parsed for each resource to determine what ports are potentially exposed. |
|
||||
| AWS | [orgs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#orgs) | Enumerate accounts in an organization |
|
||||
| AWS | [outbound-assumed-roles](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#outbound-assumed-roles) | List the roles that have been assumed by principals in this account. This is an excellent way to find outbound attack paths that lead into other accounts. |
|
||||
| AWS | [permissions](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#permissions) | Enumerates IAM permissions associated with all users and roles. Grep this output to figure out what permissions a particular principal has rather than logging into the AWS console and painstakingly expanding each policy attached to the principal you are investigating. |
|
||||
| AWS | [pmapper](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#pmapper) | Looks for pmapper data stored on the local filesystem, [in the locations defined here](https://github.com/nccgroup/PMapper/wiki/Frequently-Asked-Questions#where-does-pmapper-store-its-data). If pmapper data has been found (you already ran `pmapper graph create`), then this command will use this data to build a graph in cloudfox memory let you know who can privesc to admin.
|
||||
| AWS | [principals](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#principals) | Enumerates IAM users and Roles so you have the data at your fingertips. |
|
||||
| AWS | [ram](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#ram) | List all resources in this account that are shared with other accounts, or resources from other accounts that are shared with this account. Useful for cross-account attack paths. |
|
||||
| AWS | [resource-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#resource-trusts) | Looks through multiple services that support resource policies and helps you find any overly permissive resource trusts.|
|
||||
| AWS | [role-trusts](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#role-trusts) | Enumerates IAM role trust policies so you can look for overly permissive role trusts or find roles that trust a specific service. |
|
||||
| AWS | [route53](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#route53) | Enumerate all records from all route53 managed zones. Use this for application and service enumeration. |
|
||||
| AWS | [secrets](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#secrets) | List secrets from SecretsManager and SSM. Look for interesting secrets in the list and then see who has access to them using use `cloudfox iam-simulator` and/or `pmapper`. |
|
||||
| Azure | [instances-map](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#instances-map) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
|
||||
| Azure | [rbac-map](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac-map) | Enumerates Role Assignments for all tenants |
|
||||
| AWS | [sns](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sns) | This command enumerates all of the sns topics and gives you the commands to subscribe to a topic or send messages to a topic (if you have the permissions needed). This command only deals with topics, and not the SMS functionality. This command also attempts to summarize topic resource policies if they exist.|
|
||||
| AWS | [sqs](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#sqs) | This command enumerates all of the sqs queues and gives you the commands to receive messages from a queue and send messages to a queue (if you have the permissions needed). This command also attempts to summarize queue resource policies if they exist.|
|
||||
| AWS | [tags](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#tags) | List all resources with tags, and all of the tags. This can be used similar to inventory as another method to identify what types of resources exist in an account. |
|
||||
| AWS | [workloads](https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#workloads) | List all of the compute workloads and what role they have. Tells you if any of the roles are admin (bad) and if you have pmapper data locally, it will tell you if any of the roles can privesc to admin (also bad) |
|
||||
|
||||
|
||||
# Azure Commands
|
||||
| Provider | Command Name | Description
|
||||
| - | - | - |
|
||||
| Azure | [whoami](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#whoami) | Displays information on the tenant, subscriptions and resource groups available to your current Azure CLI session. This is useful to provide situation awareness on what tenant and subscription IDs to use with the other sub commands. |
|
||||
| Azure | [inventory](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#inventory) | Display an inventory table of all resources per location. |
|
||||
| Azure | [rbac](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#rbac) | Lists Azure RBAC role assignments at subscription or tenant level |
|
||||
| Azure | [storage](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#storage) | The storage command is still under development. Currently it only displays limited data about the storage accounts |
|
||||
| Azure | [vms](https://github.com/BishopFox/cloudfox/wiki/Azure-Commands#vms) | Enumerates useful information for Compute instances in all available resource groups and subscriptions |
|
||||
|
||||
# Authors
|
||||
* [Carlos Vendramini](https://github.com/carlosvendramini-bf)
|
||||
@@ -92,10 +158,19 @@ For the full documentation please refer to our [wiki](https://github.com/BishopF
|
||||
# Contributing
|
||||
[Wiki - How to Contribute](https://github.com/BishopFox/cloudfox/wiki#how-to-contribute)
|
||||
|
||||
# TODO
|
||||
* AWS - Add support for GovCloud and China regions
|
||||
* AWS - Add support for hardcoded region (which would override the default of looking in every region)
|
||||
|
||||
# FAQ
|
||||
|
||||
**How does CloudFox compare with ScoutSuite, Prowler, Steampipe's AWS Compliance Module, AWS Security Hub, etc.**
|
||||
|
||||
CloudFox doesn't create any alerts or findings, and doesn't check your environment for compliance to a baseline or benchmark. Instead, it simply enables you to be more efficient during your manual penetration testing activities. If gives you the information you'll likely need to validate whether an attack path is possible or not.
|
||||
|
||||
**Why do I see errors in some CloudFox commands?**
|
||||
|
||||
* Services that don't exist in all regions - CloudFox tries a few ways to figure out what services are supported in each region. However some services don't support the methods CloudFox uses, so CloudFox defaults to just asking every region about the service. Regions that don't support the service will return errors.
|
||||
* You don't have permission - Another reason you might see errors if you don't have permissions to make calls that CloudFox is making. Either because the policy doesn't allow it (e.g., SecurityAudit doesn't allow all of the permissions CloudFox needs. Or, it might be an SCP that is blocking you.
|
||||
|
||||
You can always look in the ~/.cloudfox/cloudfox-error.log file to get more information on errors.
|
||||
|
||||
# Prior work and other related projects
|
||||
* [SmogCloud](https://github.com/BishopFox/smogcloud) - Inspiration for the `endpoints` command
|
||||
|
||||
+87
-62
@@ -1,33 +1,35 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type AccessKeysModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSProfile string
|
||||
OutputFormat string
|
||||
CommandCounter console.CommandCounter
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
// Main module data
|
||||
AnalyzedUsers []UserKeys
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -36,24 +38,26 @@ type UserKeys struct {
|
||||
Key string
|
||||
}
|
||||
|
||||
func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *AccessKeysModule) PrintAccessKeys(filter string, outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "access-keys"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
},
|
||||
)
|
||||
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Mapping user access keys for account: %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Mapping user access keys for account: %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
m.getAccessKeysForAllUsers()
|
||||
|
||||
// Variables used to draw table output
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"User Name",
|
||||
"Access Key ID",
|
||||
}
|
||||
@@ -64,6 +68,7 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
key.Username,
|
||||
key.Key,
|
||||
},
|
||||
@@ -74,23 +79,63 @@ func (m *AccessKeysModule) PrintAccessKeys(filter string, outputFormat string, o
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
// Pretty prints output
|
||||
fmt.Printf("[%s] Only active access keys are shown.\n", cyan(m.output.CallingModule))
|
||||
//fmt.Printf("[%s] Preparing output.\n\n")
|
||||
fmt.Printf("[%s][%s] Only active access keys are shown.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
//fmt.Printf("[%s][%s] Preparing output.\n\n")
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
m.writeLoot(outputDirectory, verbosity)
|
||||
fmt.Printf("[%s] %s access keys found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"User Name",
|
||||
"Access Key ID",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"User Name",
|
||||
"Access Key ID",
|
||||
}
|
||||
}
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
//m.writeLoot(m.output.FilePath, verbosity)
|
||||
fmt.Printf("[%s][%s] %s access keys found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s] No access keys found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
|
||||
fmt.Printf("[%s][%s] No access keys found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *AccessKeysModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -107,56 +152,36 @@ func (m *AccessKeysModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), f)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
}
|
||||
|
||||
func (m *AccessKeysModule) getAccessKeysForAllUsers() {
|
||||
|
||||
// The "PaginationControl" value remains true until all data is received (default is 100 results per page).
|
||||
// "" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
PaginationControl := true
|
||||
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for PaginationControl {
|
||||
// added this to break out if there no users
|
||||
if len(ListUsers) != 0 {
|
||||
for _, user := range ListUsers {
|
||||
|
||||
IAMQuery, err := m.IAMClient.ListUsers(context.TODO(), &iam.ListUsersInput{Marker: PaginationMarker})
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
results, err := sdk.CachedIamListAccessKeys(m.IAMClient, aws.ToString(m.Caller.Account), aws.ToString(user.UserName))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// added this to break out if there no users
|
||||
if len(IAMQuery.Users) == 0 {
|
||||
break
|
||||
}
|
||||
// This for loop extracts relevant information for each access key and adds to output array
|
||||
for _, key := range results {
|
||||
|
||||
// This for loop iterates through all users.
|
||||
for PaginationControl {
|
||||
PaginationMarker = nil
|
||||
for _, user := range IAMQuery.Users {
|
||||
|
||||
results, err := m.IAMClient.ListAccessKeys(context.TODO(), &iam.ListAccessKeysInput{UserName: user.UserName, Marker: PaginationMarker})
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
if key.Status == "Active" {
|
||||
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
|
||||
}
|
||||
|
||||
// This for loop extracts relevant information for each access key and adds to output array
|
||||
for _, key := range results.AccessKeyMetadata {
|
||||
|
||||
if key.Status == "Active" {
|
||||
m.AnalyzedUsers = append(m.AnalyzedUsers, UserKeys{Username: *user.UserName, Key: *key.AccessKeyId})
|
||||
}
|
||||
}
|
||||
PaginationControl = results.IsTruncated
|
||||
PaginationMarker = results.Marker
|
||||
}
|
||||
}
|
||||
|
||||
PaginationControl = IAMQuery.IsTruncated
|
||||
PaginationMarker = IAMQuery.Marker
|
||||
}
|
||||
}
|
||||
+681
@@ -0,0 +1,681 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
apigatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
|
||||
apigatewayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var CURL_COMMAND string = "curl -X %s %s"
|
||||
|
||||
type ApiGwModule struct {
|
||||
// General configuration data
|
||||
APIGatewayClient sdk.APIGatewayClientInterface
|
||||
APIGatewayv2Client sdk.APIGatewayv2ClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Gateways []ApiGateway
|
||||
CommandCounter internal.CommandCounter
|
||||
Errors []string
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type ApiGateway struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Endpoint string
|
||||
ApiKey string
|
||||
Public string
|
||||
Method string
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) PrintApiGws(outputDirectory string, verbosity int) {
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "api-gw"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating api-gateways for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan ApiGateway)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
//execute regional checks
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
sort.Slice(m.Gateways, func(i, j int) bool {
|
||||
return m.Gateways[i].AWSService < m.Gateways[j].AWSService
|
||||
})
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Method",
|
||||
"Endpoint",
|
||||
"ApiKey",
|
||||
"Public",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Gateways {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Gateways[i].AWSService,
|
||||
m.Gateways[i].Region,
|
||||
m.Gateways[i].Name,
|
||||
m.Gateways[i].Method,
|
||||
m.Gateways[i].Endpoint,
|
||||
m.Gateways[i].ApiKey,
|
||||
m.Gateways[i].Public,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
Loot: internal.LootClient{
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
loot := m.writeLoot(filepath, verbosity)
|
||||
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
|
||||
Name: m.output.CallingModule,
|
||||
Contents: loot,
|
||||
})
|
||||
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
|
||||
|
||||
fmt.Printf("[%s][%s] %s API gateways found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No API gateways found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) Receiver(receiver chan ApiGateway, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Gateways = append(m.Gateways, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
|
||||
defer wg.Done()
|
||||
// check the concurrency semaphore
|
||||
// semaphore <- struct{}{}
|
||||
// defer func() {
|
||||
// <-semaphore
|
||||
// }()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("apigateway", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayAPIsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayVIPsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayv2APIsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAPIGatewayv2VIPsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) writeLoot(outputDirectory string, verbosity int) string {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
f := filepath.Join(path, "api-gws.txt")
|
||||
|
||||
var out string
|
||||
|
||||
for _, endpoint := range m.Gateways {
|
||||
method := endpoint.Method
|
||||
// Write a GET and POST for ANY
|
||||
if endpoint.Method == "ANY" {
|
||||
line := fmt.Sprintf(CURL_COMMAND, "GET", endpoint.Endpoint)
|
||||
if endpoint.ApiKey != "" {
|
||||
line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey)
|
||||
}
|
||||
|
||||
out += line + "\n"
|
||||
|
||||
method = "POST"
|
||||
}
|
||||
|
||||
line := fmt.Sprintf(CURL_COMMAND, method, endpoint.Endpoint)
|
||||
if endpoint.ApiKey != "" {
|
||||
line += fmt.Sprintf(" -H 'X-Api-Key: %s'", endpoint.ApiKey)
|
||||
}
|
||||
|
||||
if method == "DELETE" || method == "PATCH" || method == "POST" || method == "PUT" {
|
||||
line += " -H 'Content-Type: application/json' -d '{}'"
|
||||
}
|
||||
|
||||
out += line + "\n"
|
||||
}
|
||||
|
||||
// err = os.WriteFile(f, []byte(out), 0644)
|
||||
// if err != nil {
|
||||
// m.modLog.Error(err.Error())
|
||||
// m.CommandCounter.Error++
|
||||
// panic(err.Error())
|
||||
// }
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Send these requests through your favorite interception proxy"))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getAPIGatewayAPIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
|
||||
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, api := range Items {
|
||||
m.CommandCounter.Total++
|
||||
for _, endpoint := range m.getEndpointsPerAPIGateway(r, api) {
|
||||
dataReceiver <- endpoint
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getAPIGatewayVIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
|
||||
Items, err := sdk.CachedApiGatewayGetRestAPIs(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
GetDomainNames, err := sdk.CachedApiGatewayGetDomainNames(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, item := range GetDomainNames {
|
||||
|
||||
domain := aws.ToString(item.DomainName)
|
||||
GetBasePathMappings, err := sdk.CachedApiGatewayGetBasePathMappings(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, item.DomainName)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, mapping := range GetBasePathMappings {
|
||||
stage := aws.ToString(mapping.Stage)
|
||||
basePath := aws.ToString(mapping.BasePath)
|
||||
if basePath == "(none)" {
|
||||
basePath = "" // Empty string since '/' is already prepended
|
||||
}
|
||||
|
||||
for _, api := range Items {
|
||||
if api.Id != nil && aws.ToString(api.Id) == aws.ToString(mapping.RestApiId) {
|
||||
m.CommandCounter.Total++
|
||||
|
||||
endpoints := m.getEndpointsPerAPIGateway(r, api)
|
||||
for _, endpoint := range endpoints {
|
||||
old := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.RestApiId), r, stage)
|
||||
|
||||
if strings.HasPrefix(endpoint.Endpoint, old) {
|
||||
var new string
|
||||
if basePath == "" {
|
||||
new = fmt.Sprintf("https://%s/", domain)
|
||||
} else {
|
||||
new = fmt.Sprintf("https://%s/%s/", domain, basePath)
|
||||
}
|
||||
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
|
||||
endpoint.Name = domain
|
||||
dataReceiver <- endpoint
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getEndpointsPerAPIGateway(r string, api apigatewayTypes.RestApi) []ApiGateway {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
var gateways []ApiGateway
|
||||
|
||||
//var PaginationControl2 *string
|
||||
awsService := "APIGateway"
|
||||
var public string
|
||||
|
||||
name := aws.ToString(api.Name)
|
||||
id := aws.ToString(api.Id)
|
||||
raw_endpoint := fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com", id, r)
|
||||
|
||||
endpointType := *api.EndpointConfiguration
|
||||
//fmt.Println(endpointType)
|
||||
if endpointType.Types[0] == "PRIVATE" {
|
||||
public = "False"
|
||||
} else {
|
||||
public = "True"
|
||||
}
|
||||
|
||||
GetStages, err := sdk.CachedApiGatewayGetStages(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return gateways
|
||||
}
|
||||
|
||||
resources, err := sdk.CachedApiGatewayGetResources(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, id)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, stage := range GetStages.Item {
|
||||
stageName := aws.ToString(stage.StageName)
|
||||
for _, resource := range resources {
|
||||
if len(resource.ResourceMethods) != 0 {
|
||||
for method := range resource.ResourceMethods {
|
||||
|
||||
// Check if API Key is required for endpoint
|
||||
apiKey := ""
|
||||
if m.ApiGatewayApiKeyRequired(r, api.Id, resource.Id, method) {
|
||||
apiKey, err = m.GetApiGatewayApiKey(r, id, stageName)
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
path := aws.ToString(resource.Path)
|
||||
|
||||
endpoint := fmt.Sprintf("%s/%s%s", raw_endpoint, stageName, path)
|
||||
|
||||
gateways = append(gateways, ApiGateway{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Method: method,
|
||||
Public: public,
|
||||
ApiKey: apiKey,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return gateways
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getAPIGatewayv2APIsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
|
||||
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
for _, api := range Items {
|
||||
m.CommandCounter.Total++
|
||||
for _, endpoint := range m.getEndpointsPerAPIGatewayv2(r, api) {
|
||||
dataReceiver <- endpoint
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getAPIGatewayv2VIPsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan ApiGateway) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
|
||||
Items, err := sdk.CachedAPIGatewayv2GetAPIs(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
GetDomainNames, err := sdk.CachedAPIGatewayv2GetDomainNames(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, item := range GetDomainNames {
|
||||
|
||||
domain := aws.ToString(item.DomainName)
|
||||
GetApiMappings, err := sdk.CachedAPIGatewayv2GetApiMappings(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, domain)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, mapping := range GetApiMappings {
|
||||
stage := aws.ToString(mapping.Stage)
|
||||
if stage == "$default" {
|
||||
stage = ""
|
||||
}
|
||||
path := aws.ToString(mapping.ApiMappingKey)
|
||||
|
||||
for _, api := range Items {
|
||||
if api.ApiId != nil && aws.ToString(api.ApiId) == aws.ToString(mapping.ApiId) {
|
||||
m.CommandCounter.Total++
|
||||
endpoints := m.getEndpointsPerAPIGatewayv2(r, api)
|
||||
for _, endpoint := range endpoints {
|
||||
var old string
|
||||
if stage == "" {
|
||||
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/", aws.ToString(mapping.ApiId), r)
|
||||
} else {
|
||||
old = fmt.Sprintf("https://%s.execute-api.%s.amazonaws.com/%s/", aws.ToString(mapping.ApiId), r, stage)
|
||||
}
|
||||
if strings.HasPrefix(endpoint.Endpoint, old) {
|
||||
var new string
|
||||
if path == "" {
|
||||
new = fmt.Sprintf("https://%s/", domain)
|
||||
} else {
|
||||
new = fmt.Sprintf("https://%s/%s/", domain, path)
|
||||
}
|
||||
endpoint.Endpoint = strings.Replace(endpoint.Endpoint, old, new, 1)
|
||||
endpoint.Name = domain
|
||||
dataReceiver <- endpoint
|
||||
}
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) getEndpointsPerAPIGatewayv2(r string, api apigatewayV2Types.Api) []ApiGateway {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}()
|
||||
|
||||
var gateways []ApiGateway
|
||||
|
||||
awsService := "APIGatewayv2"
|
||||
|
||||
var public string
|
||||
|
||||
name := aws.ToString(api.Name)
|
||||
raw_endpoint := aws.ToString(api.ApiEndpoint)
|
||||
id := aws.ToString(api.ApiId)
|
||||
|
||||
var stages []string
|
||||
GetStages, err := sdk.CachedAPIGatewayv2GetStages(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, stage := range GetStages {
|
||||
s := aws.ToString(stage.StageName)
|
||||
if s == "$default" {
|
||||
s = ""
|
||||
}
|
||||
stages = append(stages, s)
|
||||
}
|
||||
GetRoutes, err := sdk.CachedAPIGatewayv2GetRoutes(m.APIGatewayv2Client, aws.ToString(m.Caller.Account), r, id)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, stage := range stages {
|
||||
for _, route := range GetRoutes {
|
||||
routeKey := route.RouteKey
|
||||
var method string
|
||||
var path string
|
||||
if len(strings.Fields(*routeKey)) == 2 {
|
||||
method = strings.Fields(*routeKey)[0]
|
||||
path = strings.Fields(*routeKey)[1]
|
||||
}
|
||||
var endpoint string
|
||||
if stage == "" {
|
||||
endpoint = fmt.Sprintf("%s%s", raw_endpoint, path)
|
||||
} else {
|
||||
endpoint = fmt.Sprintf("%s/%s%s", raw_endpoint, stage, path)
|
||||
}
|
||||
public = "True"
|
||||
|
||||
gateways = append(gateways, ApiGateway{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Method: method,
|
||||
Endpoint: endpoint,
|
||||
Public: public,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
return gateways
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) ApiGatewayApiKeyRequired(r string, ApiId *string, ResourceId *string, method string) bool {
|
||||
GetMethod, err := sdk.CachedApiGatewayGetMethod(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, aws.ToString(ApiId), aws.ToString(ResourceId), method)
|
||||
|
||||
if err != nil {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s", r))
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
} else {
|
||||
return aws.ToBool(GetMethod.ApiKeyRequired)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *ApiGwModule) GetApiGatewayApiKey(r string, ApiId string, Stage string) (string, error) {
|
||||
var items []apigatewayTypes.UsagePlan
|
||||
|
||||
GetUsagePlans, err := sdk.CachedApiGatewayGetUsagePlans(m.APIGatewayClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, item := range GetUsagePlans {
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
for _, item := range items {
|
||||
for _, apiStage := range item.ApiStages {
|
||||
if aws.ToString(apiStage.ApiId) == ApiId && aws.ToString(apiStage.Stage) == Stage {
|
||||
GetUsagePlanKeys, err := sdk.CachedApiGatewayGetUsagePlanKeys(m.APIGatewayClient, aws.ToString(m.Caller.Account), r, aws.ToString(item.Id))
|
||||
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
for _, i := range GetUsagePlanKeys {
|
||||
if aws.ToString(i.Type) == "API_KEY" {
|
||||
return aws.ToString(i.Value), nil
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "", nil
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestApiGw(t *testing.T) {
|
||||
|
||||
m := ApiGwModule{
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
WrapTable: false,
|
||||
APIGatewayClient: &sdk.MockedAWSAPIGatewayClient{},
|
||||
APIGatewayv2Client: &sdk.MockedAWSAPIGatewayv2Client{},
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
//tmpDir := "~/.cloudfox/"
|
||||
tmpDir := ptr.ToString(internal.GetLogDirPath())
|
||||
|
||||
m.PrintApiGws(tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/api-gw.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
//print the results file to the screen
|
||||
//fmt.Println(string(resultsFile))
|
||||
|
||||
// I want a test that runs the main function and checks the output to see if the following items are in the output: "https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1", "https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2"
|
||||
|
||||
expectedResults := []string{
|
||||
"https://qwerty.execute-api.us-east-1.amazonaws.com/stage1/path1",
|
||||
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage1/route2",
|
||||
"https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com/stage2/route1",
|
||||
"23oieuwefo3rfs",
|
||||
}
|
||||
|
||||
for _, expected := range expectedResults {
|
||||
if !strings.Contains(string(resultsFile), expected) {
|
||||
t.Errorf("Expected %s to be in the results file", expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
+234
-60
@@ -1,7 +1,6 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,106 +9,185 @@ import (
|
||||
"sync"
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type BucketsModule struct {
|
||||
// General configuration data
|
||||
S3Client *s3.Client
|
||||
//BucketsS3Client CloudFoxS3Client
|
||||
CheckBucketPolicies bool
|
||||
S3Client sdk.AWSS3ClientInterface
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSTableCols string
|
||||
AWSOutputType string
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Buckets []Bucket
|
||||
CommandCounter console.CommandCounter
|
||||
Buckets []BucketRow
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Bucket struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
type BucketRow struct {
|
||||
Arn string
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
Access string
|
||||
IsPublic string
|
||||
IsConditionallyPublic string
|
||||
Statement string
|
||||
Actions string
|
||||
ConditionText string
|
||||
ResourcePolicySummary string
|
||||
}
|
||||
|
||||
func (m *BucketsModule) PrintBuckets(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *BucketsModule) PrintBuckets(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "buckets"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating buckets for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating buckets for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Bucket)
|
||||
dataReceiver := make(chan BucketRow)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(wg, dataReceiver)
|
||||
go m.executeChecks(wg, semaphore, dataReceiver)
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{"Service", "Region", "Name"}
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"Region",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Buckets {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Buckets[i].AWSService,
|
||||
m.Buckets[i].Region,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Buckets[i].Name,
|
||||
m.Buckets[i].Region,
|
||||
m.Buckets[i].IsPublic,
|
||||
m.Buckets[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
m.writeLoot(outputDirectory, verbosity, m.AWSProfile)
|
||||
fmt.Printf("[%s] %s buckets found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"Region",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Name",
|
||||
"Region",
|
||||
"Public?",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the Public? and Resource Policy Summary columns if the user did not specify CheckBucketPolicies
|
||||
if !m.CheckBucketPolicies {
|
||||
tableCols = removeStringFromSlice(tableCols, "Public?")
|
||||
tableCols = removeStringFromSlice(tableCols, "Resource Policy Summary")
|
||||
}
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
TableCols: tableCols,
|
||||
Body: m.output.Body,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity, m.AWSProfile)
|
||||
|
||||
fmt.Printf("[%s][%s] %s buckets found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
fmt.Printf("[%s][%s] Bucket policies written to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No buckets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No buckets found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
|
||||
func (m *BucketsModule) Receiver(receiver chan BucketRow, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
@@ -122,18 +200,18 @@ func (m *BucketsModule) Receiver(receiver chan Bucket, receiverDone chan bool) {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, dataReceiver chan Bucket) {
|
||||
func (m *BucketsModule) executeChecks(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
|
||||
defer wg.Done()
|
||||
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getBuckets(m.output.Verbosity, dataReceiver)
|
||||
wg.Add(1)
|
||||
m.createBucketsRows(m.output.Verbosity, wg, semaphore, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
|
||||
func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile string) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -167,36 +245,132 @@ func (m *BucketsModule) writeLoot(outputDirectory string, verbosity int, profile
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Use the commands below to manually inspect certain buckets of interest."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to manually inspect certain buckets of interest."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), pullFile)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getBuckets(verbosity int, dataReceiver chan Bucket) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var r string = "Global"
|
||||
func (m *BucketsModule) createBucketsRows(verbosity int, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan BucketRow) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
var region string = "Global"
|
||||
var name string
|
||||
ListBuckets, err := m.S3Client.ListBuckets(
|
||||
context.TODO(),
|
||||
&s3.ListBucketsInput{},
|
||||
)
|
||||
|
||||
ListBuckets, err := sdk.CachedListBuckets(m.S3Client, aws.ToString(m.Caller.Account))
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, bucket := range ListBuckets.Buckets {
|
||||
name = aws.ToString(bucket.Name)
|
||||
// Send Bucket object through the channel to the receiver
|
||||
dataReceiver <- Bucket{
|
||||
for _, b := range ListBuckets {
|
||||
bucket := &BucketRow{
|
||||
Name: aws.ToString(b.Name),
|
||||
AWSService: "S3",
|
||||
Name: name,
|
||||
Region: r,
|
||||
}
|
||||
region, err = sdk.CachedGetBucketLocation(m.S3Client, aws.ToString(m.Caller.Account), aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
region = "Unknown"
|
||||
|
||||
}
|
||||
bucket.Region = region
|
||||
|
||||
if m.CheckBucketPolicies {
|
||||
|
||||
policyJSON, err := sdk.CachedGetBucketPolicy(m.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
} else {
|
||||
bucket.PolicyJSON = policyJSON
|
||||
}
|
||||
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
|
||||
} else {
|
||||
bucket.Policy = policy
|
||||
}
|
||||
|
||||
bucket.IsPublic = "No"
|
||||
if !bucket.Policy.IsEmpty() {
|
||||
m.analyseBucketPolicy(bucket, dataReceiver)
|
||||
} else {
|
||||
bucket.Access = "No resource policy"
|
||||
dataReceiver <- *bucket
|
||||
}
|
||||
} else {
|
||||
|
||||
// Send Bucket object through the channel to the receiver
|
||||
bucket.Access = "Skipped"
|
||||
bucket.IsPublic = "Skipped"
|
||||
dataReceiver <- *bucket
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BucketsModule) isPublicAccessBlocked(bucketName string, r string) bool {
|
||||
publicAccessBlock, err := sdk.CachedGetPublicAccessBlock(m.S3Client, aws.ToString(m.Caller.Account), r, bucketName)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
return aws.ToBool(publicAccessBlock.IgnorePublicAcls) && aws.ToBool(publicAccessBlock.BlockPublicPolicy) && aws.ToBool(publicAccessBlock.RestrictPublicBuckets)
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) analyseBucketPolicy(bucket *BucketRow, dataReceiver chan BucketRow) {
|
||||
m.storeAccessPolicy(bucket)
|
||||
|
||||
if bucket.Policy.IsPublic() && !bucket.Policy.IsConditionallyPublic() && !m.isPublicAccessBlocked(bucket.Name, bucket.Region) {
|
||||
bucket.IsPublic = "YES"
|
||||
}
|
||||
|
||||
for i, statement := range bucket.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(bucket.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
bucket.ResourcePolicySummary = bucket.ResourcePolicySummary + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
} else {
|
||||
bucket.ResourcePolicySummary = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
//bucket.ResourcePolicySummary = strings.TrimSuffix(bucket.ResourcePolicySummary, "\n")
|
||||
|
||||
}
|
||||
dataReceiver <- *bucket
|
||||
|
||||
}
|
||||
|
||||
func (m *BucketsModule) storeAccessPolicy(bucket *BucketRow) {
|
||||
f := filepath.Join(m.getLootDir(), fmt.Sprintf("%s.json", bucket.Name))
|
||||
|
||||
if err := m.storeFile(f, bucket.PolicyJSON); err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *BucketsModule) getLootDir() string {
|
||||
return filepath.Join(m.output.FilePath, "loot", "bucket-policies")
|
||||
}
|
||||
|
||||
func (m *BucketsModule) storeFile(filename string, policy string) error {
|
||||
err := os.MkdirAll(filepath.Dir(filename), 0750)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return fmt.Errorf("creating parent dirs: %s", err)
|
||||
}
|
||||
|
||||
return os.WriteFile(filename, []byte(policy), 0644)
|
||||
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type MockedS3Client struct {
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) ListBuckets(ctx context.Context, params *s3.ListBucketsInput, optFns ...func(*s3.Options)) (*s3.ListBucketsOutput, error) {
|
||||
buckets := make([]types.Bucket, 1)
|
||||
mockBucket := types.Bucket{
|
||||
CreationDate: aws.Time(time.Now()),
|
||||
Name: aws.String("mockBucket123"),
|
||||
}
|
||||
buckets[0] = mockBucket
|
||||
output := &s3.ListBucketsOutput{
|
||||
Buckets: buckets,
|
||||
}
|
||||
return output, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) GetBucketPolicy(ctx context.Context, params *s3.GetBucketPolicyInput, optFns ...func(*s3.Options)) (*s3.GetBucketPolicyOutput, error) {
|
||||
bucketPolicy := `{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AddPerm",
|
||||
"Effect": "Allow",
|
||||
"Principal": "*",
|
||||
"Action": "s3:GetObject",
|
||||
"Resource": "arn:aws:s3:::examplebucket/*"
|
||||
}
|
||||
]
|
||||
}`
|
||||
output := &s3.GetBucketPolicyOutput{
|
||||
Policy: aws.String(bucketPolicy),
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) GetBucketLocation(ctx context.Context, params *s3.GetBucketLocationInput, optFns ...func(*s3.Options)) (*s3.GetBucketLocationOutput, error) {
|
||||
locationConstraint := types.BucketLocationConstraint("us-east-2")
|
||||
output := &s3.GetBucketLocationOutput{
|
||||
LocationConstraint: locationConstraint,
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func (m *MockedS3Client) GetPublicAccessBlock(ctx context.Context, params *s3.GetPublicAccessBlockInput, optFns ...func(*s3.Options)) (*s3.GetPublicAccessBlockOutput, error) {
|
||||
output := &s3.GetPublicAccessBlockOutput{
|
||||
PublicAccessBlockConfiguration: &types.PublicAccessBlockConfiguration{
|
||||
BlockPublicAcls: aws.Bool(true),
|
||||
BlockPublicPolicy: aws.Bool(true),
|
||||
IgnorePublicAcls: aws.Bool(false),
|
||||
RestrictPublicBuckets: aws.Bool(true),
|
||||
},
|
||||
}
|
||||
return output, nil
|
||||
}
|
||||
|
||||
func TestListBuckets(t *testing.T) {
|
||||
|
||||
m := BucketsModule{
|
||||
|
||||
S3Client: &MockedS3Client{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
AWSRegions: []string{"us-east-1", "us-west-1", "us-west-2"},
|
||||
AWSProfile: "unittesting",
|
||||
AWSOutputType: "",
|
||||
CheckBucketPolicies: true,
|
||||
Goroutines: 3,
|
||||
}
|
||||
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule BucketsModule
|
||||
expectedResult []BucketRow
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: m,
|
||||
expectedResult: []BucketRow{{
|
||||
Name: "mockBucket123",
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintBuckets(subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedBucket := range subtest.expectedResult {
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/buckets.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭───────────────┬───────────┬─────────┬───────────────────────────╮
|
||||
│ Name │ Region │ Public? │ Resource Policy Summary │
|
||||
├───────────────┼───────────┼─────────┼───────────────────────────┤
|
||||
│ mockBucket123 │ us-east-2 │ YES │ Everyone can s3:GetObject │
|
||||
╰───────────────┴───────────┴─────────┴───────────────────────────╯
|
||||
`, "\n")
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
|
||||
if expectedBucket.Name != subtest.testModule.Buckets[index].Name {
|
||||
log.Fatal("Bucket name does not match expected name")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/fsx"
|
||||
"github.com/aws/aws-sdk-go-v2/service/glue"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations"
|
||||
"github.com/aws/aws-sdk-go-v2/service/s3"
|
||||
"github.com/aws/aws-sdk-go-v2/service/secretsmanager"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sns"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sqs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
func initIAMSimClient(iamSimPPClient sdk.AWSIAMClientInterface, caller sts.GetCallerIdentityOutput, AWSProfile string, Goroutines int) IamSimulatorModule {
|
||||
|
||||
iamSimMod := IamSimulatorModule{
|
||||
IAMClient: iamSimPPClient,
|
||||
Caller: caller,
|
||||
AWSProfileProvided: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return iamSimMod
|
||||
|
||||
}
|
||||
|
||||
func InitCloudFoxSNSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSWrapTable bool, AWSMFAToken string) SNSModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
cloudFoxSNSClient := SNSModule{
|
||||
SNSClient: sns.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
Goroutines: Goroutines,
|
||||
WrapTable: AWSWrapTable,
|
||||
}
|
||||
return cloudFoxSNSClient
|
||||
|
||||
}
|
||||
|
||||
func initCloudFoxS3Client(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, AWSMFAToken string) BucketsModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
cloudFoxS3Client := BucketsModule{
|
||||
S3Client: s3.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return cloudFoxS3Client
|
||||
|
||||
}
|
||||
|
||||
func InitSQSClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) SQSModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
sqsClient := SQSModule{
|
||||
SQSClient: sqs.NewFromConfig(AWSConfig),
|
||||
|
||||
Caller: caller,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
AWSProfile: AWSProfile,
|
||||
Goroutines: Goroutines,
|
||||
}
|
||||
|
||||
return sqsClient
|
||||
|
||||
}
|
||||
|
||||
func InitLambdaClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) LambdasModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
lambdaClient := LambdasModule{
|
||||
LambdaClient: lambda.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return lambdaClient
|
||||
}
|
||||
|
||||
func InitCodeBuildClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) CodeBuildModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
codeBuildClient := CodeBuildModule{
|
||||
CodeBuildClient: codebuild.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return codeBuildClient
|
||||
}
|
||||
|
||||
func InitECRClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) ECRModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
ecrClient := ECRModule{
|
||||
ECRClient: ecr.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return ecrClient
|
||||
}
|
||||
|
||||
func InitFileSystemsClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) FilesystemsModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
fileSystemsClient := FilesystemsModule{
|
||||
EFSClient: efs.NewFromConfig(AWSConfig),
|
||||
FSxClient: fsx.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return fileSystemsClient
|
||||
}
|
||||
|
||||
func InitOrgClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) OrgModule {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
orgClient := OrgModule{
|
||||
OrganizationsClient: organizations.NewFromConfig(AWSConfig),
|
||||
Caller: caller,
|
||||
AWSProfile: AWSProfile,
|
||||
AWSRegions: internal.GetEnabledRegions(AWSProfile, cfVersion, AWSMFAToken),
|
||||
}
|
||||
return orgClient
|
||||
}
|
||||
|
||||
func InitSecretsManagerClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *secretsmanager.Client {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
return secretsmanager.NewFromConfig(AWSConfig)
|
||||
}
|
||||
|
||||
func InitGlueClient(caller sts.GetCallerIdentityOutput, AWSProfile string, cfVersion string, Goroutines int, AWSMFAToken string) *glue.Client {
|
||||
var AWSConfig = internal.AWSConfigFileLoader(AWSProfile, cfVersion, AWSMFAToken)
|
||||
return glue.NewFromConfig(AWSConfig)
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CloudformationModule struct {
|
||||
// General configuration data
|
||||
CloudFormationClient sdk.CloudFormationClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
// Main module data
|
||||
CFStacks []CFStack
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type CFStack struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Role string
|
||||
Outputs []types.Output
|
||||
Parameters []types.Parameter
|
||||
Template string
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) PrintCloudformationStacks(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "cloudformation"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating cloudformation stacks for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan CFStack)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"Parameters",
|
||||
"Outputs",
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.CFStacks {
|
||||
var hasParameters string
|
||||
var hasOutputs string
|
||||
if m.CFStacks[i].Parameters != nil {
|
||||
hasParameters = "Y"
|
||||
} else {
|
||||
hasParameters = "N"
|
||||
}
|
||||
if m.CFStacks[i].Outputs != nil {
|
||||
hasOutputs = "Y"
|
||||
} else {
|
||||
hasOutputs = "N"
|
||||
}
|
||||
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.CFStacks[i].AWSService,
|
||||
m.CFStacks[i].Region,
|
||||
m.CFStacks[i].Name,
|
||||
m.CFStacks[i].Role,
|
||||
hasParameters,
|
||||
hasOutputs,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"Parameters",
|
||||
"Outputs",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
}
|
||||
}
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s cloudformation stacks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No cloudformation stacks found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
serviceRegions, err := servicemap.GetRegionsForService("cloudformation")
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
}
|
||||
for _, serviceRegion := range serviceRegions {
|
||||
if r == serviceRegion {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.createCFStackRowsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) Receiver(receiver chan CFStack, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.CFStacks = append(m.CFStacks, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
pullFile := filepath.Join(path, "cloudformation-data.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# Look for secrets. Use something like trufflehog")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
for _, stack := range m.CFStacks {
|
||||
out = out + fmt.Sprintln("=============================================")
|
||||
out = out + fmt.Sprintf("Stack Name: %s\n\n", stack.Name)
|
||||
out = out + fmt.Sprintln("Stack Outputs:")
|
||||
out = out + fmt.Sprintln()
|
||||
for _, output := range stack.Outputs {
|
||||
outputDescription := aws.ToString(output.Description)
|
||||
outputExport := aws.ToString(output.ExportName)
|
||||
outputKey := aws.ToString(output.OutputKey)
|
||||
outputValue := aws.ToString(output.OutputValue)
|
||||
out = out + fmt.Sprintf("Stack Output Description: %s\n", outputDescription)
|
||||
out = out + fmt.Sprintf("Stack Output Name: %s\n", outputExport)
|
||||
out = out + fmt.Sprintf("Stack Output Key: %s\n", outputKey)
|
||||
out = out + fmt.Sprintf("Stack Output Value: %s\n\n", outputValue)
|
||||
}
|
||||
out = out + "Stack Parameters:\n\n"
|
||||
for _, param := range stack.Parameters {
|
||||
paramKey := aws.ToString(param.ParameterKey)
|
||||
paramValue := aws.ToString(param.ParameterValue)
|
||||
out = out + fmt.Sprintf("Stack Parameter Key: %s\n", paramKey)
|
||||
out = out + fmt.Sprintf("Stack Parameter Value: %s\n\n", paramValue)
|
||||
}
|
||||
//out = out + fmt.Sprintf("Stack Parameters:\n %s\n", stack.Parameters)
|
||||
out = out + fmt.Sprintf("Stack Template:\n %s\n", stack.Template)
|
||||
out = out + fmt.Sprintln("=============================================")
|
||||
|
||||
}
|
||||
err = os.WriteFile(pullFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Look for secrets. Use something like trufflehog"))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Look for secrets. Use something like trufflehog"))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *CloudformationModule) createCFStackRowsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan CFStack) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
var stackTemplateBody string = ""
|
||||
|
||||
DescribeStacks, err := sdk.CachedCloudFormationDescribeStacks(m.CloudFormationClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, stack := range DescribeStacks {
|
||||
stackName := aws.ToString(stack.StackName)
|
||||
stackRole := aws.ToString(stack.RoleARN)
|
||||
stackOutputs := stack.Outputs
|
||||
stackParameters := stack.Parameters
|
||||
|
||||
stackTemplateBody, err = sdk.CachedCloudFormationGetTemplate(m.CloudFormationClient, aws.ToString(m.Caller.Account), r, stackName)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
dataReceiver <- CFStack{
|
||||
AWSService: "cloudformation",
|
||||
Name: stackName,
|
||||
Region: r,
|
||||
Role: stackRole,
|
||||
Outputs: stackOutputs,
|
||||
Parameters: stackParameters,
|
||||
Template: stackTemplateBody,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
func TestCloudFormation(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule CloudformationModule
|
||||
expectedResult []CFStack
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: CloudformationModule{
|
||||
CloudFormationClient: &sdk.MockedCloudformationClient{},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
},
|
||||
expectedResult: []CFStack{{
|
||||
Name: "myteststack",
|
||||
Role: "role123",
|
||||
}},
|
||||
},
|
||||
}
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintCloudformationStacks(subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedStack := range subtest.expectedResult {
|
||||
if expectedStack.Name != subtest.testModule.CFStacks[index].Name {
|
||||
log.Fatal("Stack name does not match expected name")
|
||||
}
|
||||
if expectedStack.Role != subtest.testModule.CFStacks[index].Role {
|
||||
log.Fatal("Stack role does not match expected name")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,267 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type CodeBuildModule struct {
|
||||
// General configuration data
|
||||
CodeBuildClient sdk.CodeBuildClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Main module data
|
||||
Projects []Project
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Project struct {
|
||||
Region string
|
||||
Name string
|
||||
Arn string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) PrintCodeBuildProjects(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "codebuild"
|
||||
localAdminMap := make(map[string]bool)
|
||||
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating CodeBuild projects for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Project)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// Perform role analysis
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.Projects {
|
||||
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Projects[i].Role)
|
||||
}
|
||||
} else {
|
||||
for i := range m.Projects {
|
||||
m.Projects[i].Admin, m.Projects[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Projects[i].Role, m.iamSimClient, localAdminMap)
|
||||
}
|
||||
}
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
// Table rows
|
||||
|
||||
for i := range m.Projects {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Projects[i].Region,
|
||||
m.Projects[i].Name,
|
||||
m.Projects[i].Role,
|
||||
m.Projects[i].Admin,
|
||||
m.Projects[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var seen []string
|
||||
for _, project := range m.Projects {
|
||||
if !internal.Contains(project.Name, seen) {
|
||||
seen = append(seen, project.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %d projects found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No projects found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("codebuild", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getcodeBuildProjectsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) Receiver(receiver chan Project, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Projects = append(m.Projects, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *CodeBuildModule) getcodeBuildProjectsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Project) {
|
||||
defer wg.Done()
|
||||
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
projects, err := sdk.CachedCodeBuildListProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
}
|
||||
|
||||
for _, project := range projects {
|
||||
details, err := sdk.CachedCodeBuildBatchGetProjects(m.CodeBuildClient, aws.ToString(m.Caller.Account), r, project)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
}
|
||||
|
||||
dataReceiver <- Project{
|
||||
Name: aws.ToString(details.Name),
|
||||
Region: r,
|
||||
Role: aws.ToString(details.ServiceRole),
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
type mockedCodeBuildClient struct {
|
||||
}
|
||||
|
||||
func (m *mockedCodeBuildClient) ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
|
||||
out := codebuild.ListProjectsOutput{
|
||||
Projects: []string{"test1", "test2"},
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func (m *mockedCodeBuildClient) BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error) {
|
||||
project1out := codebuild.BatchGetProjectsOutput{
|
||||
Projects: []types.Project{
|
||||
{
|
||||
Name: aws.String("test1"),
|
||||
Environment: &types.ProjectEnvironment{
|
||||
Image: aws.String("aws/codebuild/standard:4.0"),
|
||||
},
|
||||
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test1"),
|
||||
Source: &types.ProjectSource{
|
||||
Type: "CODECOMMIT",
|
||||
Location: aws.String("https://git-codecommit.us-east-1.amazonaws.com/v1/repos/test1"),
|
||||
},
|
||||
Artifacts: &types.ProjectArtifacts{
|
||||
Type: "S3",
|
||||
Location: aws.String("s3://test1"),
|
||||
},
|
||||
TimeoutInMinutes: aws.Int32(60),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
project2out := codebuild.BatchGetProjectsOutput{
|
||||
Projects: []types.Project{
|
||||
{
|
||||
Name: aws.String("test2"),
|
||||
Environment: &types.ProjectEnvironment{
|
||||
Image: aws.String("aws/codebuild/standard:4.0"),
|
||||
},
|
||||
ServiceRole: aws.String("arn:aws:iam::123456789012:role/test2"),
|
||||
Source: &types.ProjectSource{
|
||||
Type: "GITHUB",
|
||||
Location: aws.String("https://test.github.com/test2"),
|
||||
},
|
||||
Artifacts: &types.ProjectArtifacts{
|
||||
Type: "S3",
|
||||
Location: aws.String("s3://test2"),
|
||||
},
|
||||
TimeoutInMinutes: aws.Int32(60),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
if params.Names[0] == "test1" {
|
||||
return &project1out, nil
|
||||
} else if params.Names[0] == "test2" {
|
||||
return &project2out, nil
|
||||
} else {
|
||||
return nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockedCodeBuildClient) GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
|
||||
out := codebuild.GetResourcePolicyOutput{
|
||||
Policy: aws.String(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": "*"
|
||||
},
|
||||
"Action": [
|
||||
"codebuild:StartBuild",
|
||||
"codebuild:BatchGetBuilds",
|
||||
"codebuild:StopBuild",
|
||||
"codebuild:ListProjects",
|
||||
"codebuild:BatchGetProjects"
|
||||
],
|
||||
"Resource": "*"
|
||||
}
|
||||
]
|
||||
}`),
|
||||
}
|
||||
|
||||
return &out, nil
|
||||
}
|
||||
|
||||
func TestCodeBuildProjects(t *testing.T) {
|
||||
|
||||
m := CodeBuildModule{
|
||||
CodeBuildClient: &mockedCodeBuildClient{},
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-west-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
WrapTable: true,
|
||||
SkipAdminCheck: true,
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := "."
|
||||
|
||||
// execute the module with verbosity set to 2
|
||||
m.PrintCodeBuildProjects(tmpDir, 2)
|
||||
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/codebuild.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFilePath, err)
|
||||
}
|
||||
|
||||
expectedResults := strings.TrimLeft(`
|
||||
╭───────────┬───────┬──────────────────────────────────────┬──────────────╮
|
||||
│ Region │ Name │ Role │ IsAdminRole? │
|
||||
├───────────┼───────┼──────────────────────────────────────┼──────────────┤
|
||||
│ us-west-1 │ test1 │ arn:aws:iam::123456789012:role/test1 │ Skipped │
|
||||
│ us-west-1 │ test2 │ arn:aws:iam::123456789012:role/test2 │ Skipped │
|
||||
╰───────────┴───────┴──────────────────────────────────────┴──────────────╯
|
||||
`, "\n")
|
||||
fmt.Println(expectedResults)
|
||||
fmt.Println("results file")
|
||||
fmt.Println(string(resultsFile))
|
||||
|
||||
if string(resultsFile) != expectedResults {
|
||||
t.Fatalf("Unexpected results:\n%s\n", resultsFile)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,500 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type DatabasesModule struct {
|
||||
RDSClient sdk.RDSClientInterface
|
||||
RedshiftClient sdk.AWSRedShiftClientInterface
|
||||
DynamoDBClient sdk.DynamoDBClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
Databases []Database
|
||||
CommandCounter internal.CommandCounter
|
||||
Errors []string
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Database struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Engine string
|
||||
Name string
|
||||
Arn string
|
||||
UserName string
|
||||
Endpoint string
|
||||
Port int32
|
||||
Protocol string
|
||||
Public string
|
||||
Size string
|
||||
Roles string
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) PrintDatabases(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "databases"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating databases for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: RDS, Redshift, DynamoDB, DocumentDB, Neptune\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Database)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
//execute regional checks
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
sort.Slice(m.Databases, func(i, j int) bool {
|
||||
return m.Databases[i].AWSService < m.Databases[j].AWSService
|
||||
})
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Engine",
|
||||
"Region",
|
||||
"Name",
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
"Port",
|
||||
"Roles",
|
||||
//"Protocol",
|
||||
//"Public",
|
||||
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Engine",
|
||||
"Region",
|
||||
"Name",
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
"Port",
|
||||
"Roles",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Service",
|
||||
"Engine",
|
||||
"Region",
|
||||
"Name",
|
||||
"Size",
|
||||
"UserName",
|
||||
"Endpoint",
|
||||
"Port",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Databases {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Databases[i].AWSService,
|
||||
m.Databases[i].Engine,
|
||||
m.Databases[i].Region,
|
||||
m.Databases[i].Name,
|
||||
m.Databases[i].Size,
|
||||
m.Databases[i].UserName,
|
||||
m.Databases[i].Endpoint,
|
||||
strconv.Itoa(int(m.Databases[i].Port)),
|
||||
m.Databases[i].Roles,
|
||||
// m.Databases[i].Protocol,
|
||||
// m.Databases[i].Public,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
filepath := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
Loot: internal.LootClient{
|
||||
DirectoryName: filepath,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
loot := m.writeLoot(filepath, verbosity)
|
||||
//o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
o.Loot.LootFiles = append(o.Loot.LootFiles, internal.LootFile{
|
||||
Name: "databases-UrlsOnly.txt",
|
||||
Contents: loot,
|
||||
})
|
||||
o.WriteFullOutput(o.Table.TableFiles, o.Loot.LootFiles)
|
||||
|
||||
fmt.Printf("[%s][%s] %s databases found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No databases found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) Receiver(receiver chan Database, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Databases = append(m.Databases, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer wg.Done()
|
||||
|
||||
serviceMap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
m.executeRdsCheck(r, wg, semaphore, dataReceiver, serviceMap) // Also returns Neptune and DocDB
|
||||
m.executeRedshiftCheck(r, wg, semaphore, dataReceiver, serviceMap)
|
||||
m.executeDynamoDbCheck(r, wg, semaphore, dataReceiver, serviceMap)
|
||||
}
|
||||
|
||||
type check struct {
|
||||
region string
|
||||
wg *sync.WaitGroup
|
||||
semaphore chan struct{}
|
||||
dataReceiver chan Database
|
||||
serviceMap *awsservicemap.AwsServiceMap
|
||||
service string
|
||||
executor func(string, *sync.WaitGroup, chan struct{}, chan Database)
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeCheck(check check) {
|
||||
res, err := check.serviceMap.IsServiceInRegion(check.service, check.region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
check.wg.Add(1)
|
||||
go check.executor(check.region, check.wg, check.semaphore, check.dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeRdsCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "rds",
|
||||
executor: m.getRdsClustersPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeRedshiftCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "redshift",
|
||||
executor: m.getRedshiftDatabasesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) executeDynamoDbCheck(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database, servicemap *awsservicemap.AwsServiceMap) {
|
||||
m.executeCheck(check{
|
||||
region: r,
|
||||
wg: wg,
|
||||
semaphore: semaphore,
|
||||
dataReceiver: dataReceiver,
|
||||
serviceMap: servicemap,
|
||||
service: "dynamodb",
|
||||
executor: m.getDynamoDBTablesPerRegion,
|
||||
})
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) writeLoot(outputDirectory string, verbosity int) string {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
f := filepath.Join(path, "databases-UrlsOnly.txt")
|
||||
|
||||
var out string
|
||||
|
||||
for _, database := range m.Databases {
|
||||
out = out + fmt.Sprintln(database.Endpoint)
|
||||
}
|
||||
|
||||
// err = os.WriteFile(f, []byte(out), 0644)
|
||||
// if err != nil {
|
||||
// m.modLog.Error(err.Error())
|
||||
// m.CommandCounter.Error++
|
||||
// panic(err.Error())
|
||||
// }
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed this databases into nmap and something like gowitness/aquatone for screenshots."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
|
||||
return out
|
||||
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRdsClustersPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
|
||||
DBClusters, err := sdk.CachedRDSDescribeDBClusters(m.RDSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, cluster := range DBClusters {
|
||||
var public string
|
||||
var service string
|
||||
var roles string
|
||||
if cluster.Endpoint == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
name := aws.ToString(cluster.DBClusterIdentifier)
|
||||
port := cluster.Port
|
||||
endpoint := aws.ToString(cluster.Endpoint)
|
||||
engine := aws.ToString(cluster.Engine)
|
||||
|
||||
if aws.ToBool(cluster.PubliclyAccessible) {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
|
||||
if isNeptune(cluster.Engine) {
|
||||
service = "Neptune"
|
||||
} else if isDocDB(cluster.Engine) {
|
||||
service = "DocsDB"
|
||||
} else {
|
||||
service = "RDS"
|
||||
}
|
||||
|
||||
associatedRoles := cluster.AssociatedRoles
|
||||
for _, role := range associatedRoles {
|
||||
roles = roles + aws.ToString(role.RoleArn) + " "
|
||||
}
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: service,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Engine: engine,
|
||||
Endpoint: endpoint,
|
||||
UserName: aws.ToString(cluster.MasterUsername),
|
||||
Port: aws.ToInt32(port),
|
||||
Protocol: aws.ToString(cluster.Engine),
|
||||
Public: public,
|
||||
Roles: roles,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getRedshiftDatabasesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "Redshift"
|
||||
protocol := "https"
|
||||
|
||||
Clusters, err := sdk.CachedRedShiftDescribeClusters(m.RedshiftClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
if errors.As(err, &oe) {
|
||||
m.Errors = append(m.Errors, fmt.Sprintf(" Error: Region: %s, Service: %s, Operation: %s", r, oe.Service(), oe.Operation()))
|
||||
}
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
var public string
|
||||
for _, cluster := range Clusters {
|
||||
name := aws.ToString(cluster.DBName)
|
||||
//id := workspace.Id
|
||||
endpoint := aws.ToString(cluster.Endpoint.Address)
|
||||
port := aws.ToInt32(cluster.Endpoint.Port)
|
||||
|
||||
if aws.ToBool(cluster.PubliclyAccessible) {
|
||||
public = "True"
|
||||
} else {
|
||||
public = "False"
|
||||
}
|
||||
dataReceiver <- Database{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: name,
|
||||
Endpoint: endpoint,
|
||||
Port: port,
|
||||
Protocol: protocol,
|
||||
Public: public,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *DatabasesModule) getDynamoDBTablesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Database) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "DynamoDB"
|
||||
|
||||
Tables, err := sdk.CachedDynamoDBListTables(m.DynamoDBClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, table := range Tables {
|
||||
//get size of dynamodb table
|
||||
TableOutput, err := sdk.CachedDynamoDBDescribeTable(m.DynamoDBClient, aws.ToString(m.Caller.Account), r, table)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
size := aws.ToInt64(TableOutput.TableSizeBytes)
|
||||
|
||||
dataReceiver <- Database{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: table,
|
||||
Size: strconv.Itoa(int(size)),
|
||||
UserName: "N/A",
|
||||
Endpoint: "N/A",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isNeptune(engine *string) bool {
|
||||
return *engine == "neptune"
|
||||
}
|
||||
|
||||
func isDocDB(engine *string) bool {
|
||||
return *engine == "docdb"
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/aws/smithy-go/ptr"
|
||||
"github.com/spf13/afero"
|
||||
)
|
||||
|
||||
func TestDatabasesCommand(t *testing.T) {
|
||||
|
||||
m := DatabasesModule{
|
||||
AWSProfile: "unittesting",
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/Alice"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
Goroutines: 3,
|
||||
WrapTable: false,
|
||||
RDSClient: &sdk.MockedRDSClient{},
|
||||
DynamoDBClient: &sdk.MockedAWSDynamoDBClient{},
|
||||
RedshiftClient: &sdk.MockedRedshiftClient{},
|
||||
}
|
||||
|
||||
fs := internal.MockFileSystem(true)
|
||||
defer internal.MockFileSystem(false)
|
||||
tmpDir := ptr.ToString(internal.GetLogDirPath())
|
||||
|
||||
m.PrintDatabases(tmpDir, 2)
|
||||
//resultsFile, err := afero.ReadFile(fs, "table/databases.txt")
|
||||
resultsFilePath := filepath.Join(tmpDir, "cloudfox-output/aws/unittesting-123456789012/table/databases.txt")
|
||||
resultsFile, err := afero.ReadFile(fs, resultsFilePath)
|
||||
if err != nil {
|
||||
t.Fatalf("Cannot read output file at %s: %s", resultsFile, err)
|
||||
}
|
||||
|
||||
expectedResults := []string{
|
||||
"db1.cluster-123456789012.us-west-2.rds.amazonaws.com",
|
||||
"db2.cluster-123456789012.us-west-2.rds.amazonaws.com",
|
||||
"db3.cluster-123456789012.us-west-2.neptune.amazonaws.com",
|
||||
"db4.cluster-123456789012.us-west-2.docdb.amazonaws.com",
|
||||
}
|
||||
|
||||
for _, expected := range expectedResults {
|
||||
if !strings.Contains(string(resultsFile), expected) {
|
||||
t.Errorf("Expected %s to be in the results file", expected)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+210
-133
@@ -1,7 +1,6 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -10,29 +9,33 @@ import (
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ECRModule struct {
|
||||
// General configuration data
|
||||
ECRClient *ecr.Client
|
||||
ECRClient sdk.AWSECRClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Repositories []Repository
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -44,53 +47,59 @@ type Repository struct {
|
||||
PushedAt string
|
||||
ImageTags string
|
||||
ImageSize int64
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
}
|
||||
|
||||
func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *ECRModule) PrintECR(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "ecr"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating container repositories for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Enumerating container repositories for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Repository)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
// This is the complete list of potential table columns
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
@@ -100,11 +109,53 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
|
||||
"ImageSize",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"URI",
|
||||
"PushedAt",
|
||||
"ImageTags",
|
||||
"ImageSize",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"URI",
|
||||
"PushedAt",
|
||||
"ImageTags",
|
||||
"ImageSize",
|
||||
}
|
||||
}
|
||||
|
||||
// sort the table by Name
|
||||
sort.Slice(m.Repositories, func(i, j int) bool {
|
||||
return m.Repositories[i].Name < m.Repositories[j].Name
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Repositories {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Repositories[i].AWSService,
|
||||
m.Repositories[i].Region,
|
||||
m.Repositories[i].Name,
|
||||
@@ -117,25 +168,47 @@ func (m *ECRModule) PrintECR(outputFormat string, outputDirectory string, verbos
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
m.writeLoot(outputDirectory, verbosity)
|
||||
fmt.Printf("[%s] %s repositories found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s] No repositories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s repositories found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No repositories found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Repository) {
|
||||
func (m *ECRModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getECRRecordsPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ecr", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECRModule) Receiver(receiver chan Repository, receiverDone chan bool) {
|
||||
@@ -152,7 +225,7 @@ func (m *ECRModule) Receiver(receiver chan Repository, receiverDone chan bool) {
|
||||
}
|
||||
|
||||
func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -186,119 +259,123 @@ func (m *ECRModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Use the commands below to authenticate to ECR and download the images that look interesting"))
|
||||
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("You will need the ecr:GetAuthorizationToken on the registry to authenticate and this is not part of the SecurityAudit permissions policy"))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to authenticate to ECR and download the images that look interesting"))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("You will need the ecr:GetAuthorizationToken on the registry to authenticate and this is not part of the SecurityAudit permissions policy"))
|
||||
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), pullFile)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *ECRModule) getECRRecordsPerRegion(r string, dataReceiver chan Repository) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var PaginationControl2 *string
|
||||
func (m *ECRModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Repository) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
for {
|
||||
DescribeRepositories, err := m.ECRClient.DescribeRepositories(
|
||||
context.TODO(),
|
||||
&ecr.DescribeRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
|
||||
Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, repo := range Repositories {
|
||||
repoName := aws.ToString(repo.RepositoryName)
|
||||
repoURI := aws.ToString(repo.RepositoryUri)
|
||||
//created := *repo.CreatedAt
|
||||
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
|
||||
images, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
continue
|
||||
}
|
||||
|
||||
for _, repo := range DescribeRepositories.Repositories {
|
||||
repoName := aws.ToString(repo.RepositoryName)
|
||||
repoURI := aws.ToString(repo.RepositoryUri)
|
||||
//created := *repo.CreatedAt
|
||||
//fmt.Printf("%s, %s, %s", repoName, repoURI, created)
|
||||
var images []types.ImageDetail
|
||||
for {
|
||||
DescribeImages, err := m.ECRClient.DescribeImages(
|
||||
context.TODO(),
|
||||
&ecr.DescribeImagesInput{
|
||||
RepositoryName: &repoName,
|
||||
NextToken: PaginationControl2,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
sort.Slice(images, func(i, j int) bool {
|
||||
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
})
|
||||
|
||||
//images := DescribeImages.ImageDetails
|
||||
images = append(images, DescribeImages.ImageDetails...)
|
||||
var image types.ImageDetail
|
||||
var imageTags string
|
||||
|
||||
if DescribeImages.NextToken != nil {
|
||||
PaginationControl2 = DescribeImages.NextToken
|
||||
} else {
|
||||
|
||||
// not sure if this is the right way to do this, but adding this code here was the only way i could
|
||||
// sort the results from all pages to look for the latest push.
|
||||
PaginationControl2 = nil
|
||||
|
||||
sort.Slice(images, func(i, j int) bool {
|
||||
return images[i].ImagePushedAt.Format("2006-01-02 15:04:05") < images[j].ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
})
|
||||
|
||||
var image types.ImageDetail
|
||||
var imageTags string
|
||||
|
||||
if len(images) > 1 {
|
||||
image = images[len(images)-1]
|
||||
} else if len(images) == 1 {
|
||||
image = images[0]
|
||||
} else {
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
if len(image.ImageTags) > 0 {
|
||||
imageTags = image.ImageTags[0]
|
||||
}
|
||||
//imageTags := image.ImageTags[0]
|
||||
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
imageSize := aws.ToInt64(image.ImageSizeInBytes)
|
||||
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
|
||||
|
||||
dataReceiver <- Repository{
|
||||
AWSService: "ECR",
|
||||
Name: repoName,
|
||||
Region: r,
|
||||
URI: pullURI,
|
||||
PushedAt: pushedAt,
|
||||
ImageTags: imageTags,
|
||||
ImageSize: imageSize,
|
||||
}
|
||||
|
||||
// }
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeRepositories.NextToken != nil {
|
||||
PaginationControl = DescribeRepositories.NextToken
|
||||
if len(images) > 1 {
|
||||
image = images[len(images)-1]
|
||||
} else if len(images) == 1 {
|
||||
image = images[0]
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
continue
|
||||
}
|
||||
|
||||
if len(image.ImageTags) > 0 {
|
||||
imageTags = image.ImageTags[0]
|
||||
} else {
|
||||
imageTags = "No tags"
|
||||
}
|
||||
|
||||
//imageTags := image.ImageTags[0]
|
||||
pushedAt := image.ImagePushedAt.Format("2006-01-02 15:04:05")
|
||||
imageSize := aws.ToInt64(image.ImageSizeInBytes)
|
||||
pullURI := fmt.Sprintf("%s:%s", repoURI, imageTags)
|
||||
|
||||
dataReceiver <- Repository{
|
||||
AWSService: "ECR",
|
||||
Name: repoName,
|
||||
Region: r,
|
||||
URI: pullURI,
|
||||
PushedAt: pushedAt,
|
||||
ImageTags: imageTags,
|
||||
ImageSize: imageSize,
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// func (m *ECRModule) describeRepositories(r string) ([]types.Repository, error) {
|
||||
|
||||
// var repositories []types.Repository
|
||||
// Repositories, err := sdk.CachedECRDescribeRepositories(m.ECRClient, aws.ToString(m.Caller.Account), r)
|
||||
// if err != nil {
|
||||
// m.CommandCounter.Error++
|
||||
// return nil, err
|
||||
// }
|
||||
|
||||
// repositories = append(repositories, Repositories...)
|
||||
|
||||
// return repositories, nil
|
||||
// }
|
||||
|
||||
// func (m *ECRModule) describeImages(r string, repoName string) ([]types.ImageDetail, error) {
|
||||
// var images []types.ImageDetail
|
||||
|
||||
// ImageDetails, err := sdk.CachedECRDescribeImages(m.ECRClient, aws.ToString(m.Caller.Account), r, repoName)
|
||||
// if err != nil {
|
||||
// m.CommandCounter.Error++
|
||||
// return nil, err
|
||||
// }
|
||||
// images = append(images, ImageDetails...)
|
||||
// return images, nil
|
||||
// }
|
||||
|
||||
func (m *ECRModule) getECRRepositoryPolicy(r string, repository string) (policy.Policy, error) {
|
||||
var repoPolicy policy.Policy
|
||||
Policy, err := sdk.CachedECRGetRepositoryPolicy(m.ECRClient, aws.ToString(m.Caller.Account), r, repository)
|
||||
if err != nil {
|
||||
m.CommandCounter.Error++
|
||||
return repoPolicy, err
|
||||
}
|
||||
repoPolicy, err = policy.ParseJSONPolicy([]byte(Policy))
|
||||
if err != nil {
|
||||
return repoPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", repository, err)
|
||||
}
|
||||
return repoPolicy, nil
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
func TestDescribeRepos(t *testing.T) {
|
||||
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule ECRModule
|
||||
expectedResult []Repository
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: ECRModule{
|
||||
ECRClient: &sdk.MockedECRClient{},
|
||||
Caller: sts.GetCallerIdentityOutput{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests"),
|
||||
Account: aws.String("123456789012"),
|
||||
},
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: []string{"us-east-1"},
|
||||
},
|
||||
expectedResult: []Repository{
|
||||
{
|
||||
Name: "repo1",
|
||||
URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1",
|
||||
PushedAt: "2022-10-25 15:14:00",
|
||||
ImageTags: "customtag, tag2",
|
||||
ImageSize: 123456,
|
||||
},
|
||||
{
|
||||
Name: "repo2",
|
||||
URI: "11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2",
|
||||
PushedAt: "2021-10-15 11:14:00",
|
||||
ImageTags: "latest",
|
||||
ImageSize: 2222222,
|
||||
}},
|
||||
},
|
||||
}
|
||||
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.PrintECR(subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedRepo := range subtest.expectedResult {
|
||||
if expectedRepo.Name != subtest.testModule.Repositories[index].Name {
|
||||
log.Fatal("Repo name does not match expected name")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,539 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ECSTasksModule struct {
|
||||
ECSClient sdk.AWSECSClientInterface
|
||||
EC2Client sdk.AWSEC2ClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
AWSProfile string
|
||||
Goroutines int
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
MappedECSTasks []MappedECSTask
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type MappedECSTask struct {
|
||||
Cluster string
|
||||
TaskDefinitionName string
|
||||
TaskDefinitionContent string
|
||||
LaunchType string
|
||||
ID string
|
||||
ExternalIP string
|
||||
PrivateIP string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) ECSTasks(outputDirectory string, verbosity int) {
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "ecs-tasks"
|
||||
localAdminMap := make(map[string]bool)
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating ECS tasks in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
spinnerDone := make(chan bool)
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
dataReceiver := make(chan MappedECSTask)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Perform role analysis
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.MappedECSTasks {
|
||||
m.MappedECSTasks[i].Admin, m.MappedECSTasks[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedECSTasks[i].Role)
|
||||
}
|
||||
} else {
|
||||
for i := range m.MappedECSTasks {
|
||||
m.MappedECSTasks[i].Admin, m.MappedECSTasks[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.MappedECSTasks[i].Role, m.iamSimClient, localAdminMap)
|
||||
}
|
||||
}
|
||||
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
m.printECSTaskData(outputDirectory, dataReceiver, verbosity)
|
||||
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) Receiver(receiver chan MappedECSTask, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.MappedECSTasks = append(m.MappedECSTasks, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) printECSTaskData(outputDirectory string, dataReceiver chan MappedECSTask, verbosity int) {
|
||||
// This is the complete list of potential table columns
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"ID",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Cluster",
|
||||
"TaskDefinition",
|
||||
"LaunchType",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"RoleArn",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
for _, ecsTask := range m.MappedECSTasks {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
ecsTask.Cluster,
|
||||
ecsTask.TaskDefinitionName,
|
||||
ecsTask.LaunchType,
|
||||
ecsTask.ID,
|
||||
ecsTask.ExternalIP,
|
||||
ecsTask.PrivateIP,
|
||||
ecsTask.Role,
|
||||
ecsTask.Admin,
|
||||
ecsTask.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName)
|
||||
fmt.Printf("[%s][%s] %s ECS tasks found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No ECS tasks found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) writeLoot(outputDirectory string) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
privateIPsFilename := filepath.Join(path, "ecs-tasks-PrivateIPs.txt")
|
||||
publicIPsFilename := filepath.Join(path, "ecs-tasks-PublicIPs.txt")
|
||||
|
||||
var publicIPs string
|
||||
var privateIPs string
|
||||
|
||||
for _, task := range m.MappedECSTasks {
|
||||
if task.ExternalIP != "NoExternalIP" {
|
||||
publicIPs = publicIPs + fmt.Sprintln(task.ExternalIP)
|
||||
}
|
||||
if task.PrivateIP != "" {
|
||||
privateIPs = privateIPs + fmt.Sprintln(task.PrivateIP)
|
||||
}
|
||||
|
||||
}
|
||||
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, task := range m.MappedECSTasks {
|
||||
if task.TaskDefinitionContent != "" {
|
||||
path := filepath.Join(path, "task-definitions")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
taskDefinitionFilename := filepath.Join(path, task.TaskDefinitionName+".json")
|
||||
|
||||
err = os.WriteFile(taskDefinitionFilename, []byte(task.TaskDefinitionContent), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
|
||||
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedECSTask) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ecs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getListClusters(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) getListClusters(region string, dataReceiver chan MappedECSTask) {
|
||||
|
||||
ClusterArns, err := sdk.CachedECSListClusters(m.ECSClient, aws.ToString(m.Caller.Account), region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, clusterARN := range ClusterArns {
|
||||
m.getListTasks(clusterARN, region, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) getListTasks(clusterARN string, region string, dataReceiver chan MappedECSTask) {
|
||||
TaskArns, err := sdk.CachedECSListTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
batchSize := 100 // maximum value: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeTasks.html#API_DescribeTasks_RequestSyntax
|
||||
for i := 0; i < len(TaskArns); i += batchSize {
|
||||
j := i + batchSize
|
||||
if j > len(TaskArns) {
|
||||
j = len(TaskArns)
|
||||
}
|
||||
|
||||
m.loadTasksData(clusterARN, TaskArns[i:j], region, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) loadTasksData(clusterARN string, taskARNs []string, region string, dataReceiver chan MappedECSTask) {
|
||||
|
||||
if len(taskARNs) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
Tasks, err := sdk.CachedECSDescribeTasks(m.ECSClient, aws.ToString(m.Caller.Account), region, clusterARN, taskARNs)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
eniIDs := []string{}
|
||||
for _, task := range Tasks {
|
||||
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
|
||||
if eniID != "" {
|
||||
eniIDs = append(eniIDs, eniID)
|
||||
}
|
||||
}
|
||||
publicIPs, err := m.loadPublicIPs(eniIDs, region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, task := range Tasks {
|
||||
//taskDefinition, err := m.describeTaskDefinition(aws.ToString(task.TaskDefinitionArn), region)
|
||||
taskDefinition, err := sdk.CachedECSDescribeTaskDefinition(m.ECSClient, aws.ToString(m.Caller.Account), region, aws.ToString(task.TaskDefinitionArn))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
mappedTask := MappedECSTask{
|
||||
Cluster: getNameFromARN(clusterARN),
|
||||
TaskDefinitionName: getNameFromARN(aws.ToString(task.TaskDefinitionArn)),
|
||||
TaskDefinitionContent: getTaskDefinitionContent(taskDefinition),
|
||||
LaunchType: string(task.LaunchType),
|
||||
ID: getIDFromECSTask(aws.ToString(task.TaskArn)),
|
||||
PrivateIP: getPrivateIPv4AddressFromECSTask(task),
|
||||
Role: getTaskRole(taskDefinition),
|
||||
}
|
||||
|
||||
eniID := getElasticNetworkInterfaceIDOfECSTask(task)
|
||||
if eniID != "" {
|
||||
mappedTask.ExternalIP = publicIPs[eniID]
|
||||
}
|
||||
|
||||
dataReceiver <- mappedTask
|
||||
}
|
||||
}
|
||||
|
||||
func getTaskRole(taskDefinition types.TaskDefinition) string {
|
||||
return aws.ToString(taskDefinition.TaskRoleArn)
|
||||
}
|
||||
|
||||
func getTaskDefinitionContent(taskDefinition types.TaskDefinition) string {
|
||||
// return taskDefinition as a json string
|
||||
|
||||
taskDefinitionContent, err := json.Marshal(taskDefinition)
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return string(taskDefinitionContent)
|
||||
}
|
||||
|
||||
func (m *ECSTasksModule) describeTaskDefinition(taskDefinitionArn string, region string) (types.TaskDefinition, error) {
|
||||
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
|
||||
context.TODO(),
|
||||
&ecs.DescribeTaskDefinitionInput{
|
||||
TaskDefinition: &taskDefinitionArn,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return types.TaskDefinition{}, err
|
||||
}
|
||||
return *DescribeTaskDefinition.TaskDefinition, nil
|
||||
}
|
||||
|
||||
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF NOT NEEDED
|
||||
func (m *ECSTasksModule) loadAllPublicIPs(eniIDs []string, region string) (map[string]string, error) {
|
||||
eniPublicIPs := make(map[string]string)
|
||||
|
||||
batchSize := 1000 // seems to be maximum value: https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_DescribeNetworkInterfaces.html
|
||||
for i := 0; i < len(eniIDs); i += batchSize {
|
||||
j := i + batchSize
|
||||
if j > len(eniIDs) {
|
||||
j = len(eniIDs)
|
||||
}
|
||||
|
||||
publicIPs, err := m.loadPublicIPs(eniIDs[i:j], region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
|
||||
}
|
||||
|
||||
for eniID, publicIP := range publicIPs {
|
||||
eniPublicIPs[eniID] = publicIP
|
||||
}
|
||||
}
|
||||
|
||||
return eniPublicIPs, nil
|
||||
}
|
||||
*/
|
||||
|
||||
func (m *ECSTasksModule) loadPublicIPs(eniIDs []string, region string) (map[string]string, error) {
|
||||
eniPublicIPs := make(map[string]string)
|
||||
|
||||
if len(eniIDs) == 0 {
|
||||
return eniPublicIPs, nil
|
||||
}
|
||||
|
||||
NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("getting elastic network interfaces: %s", err)
|
||||
}
|
||||
|
||||
for _, eni := range NetworkInterfaces {
|
||||
eniPublicIPs[aws.ToString(eni.NetworkInterfaceId)] = getPublicIPOfElasticNetworkInterface(eni)
|
||||
}
|
||||
|
||||
return eniPublicIPs, nil
|
||||
}
|
||||
|
||||
func getNameFromARN(arn string) string {
|
||||
tokens := strings.SplitN(arn, "/", 2)
|
||||
if len(tokens) != 2 {
|
||||
return arn
|
||||
}
|
||||
|
||||
return tokens[1]
|
||||
}
|
||||
|
||||
func getIDFromECSTask(arn string) string {
|
||||
tokens := strings.SplitN(arn, "/", 3)
|
||||
if len(tokens) != 3 {
|
||||
return arn
|
||||
}
|
||||
|
||||
return tokens[2]
|
||||
}
|
||||
|
||||
func getPrivateIPv4AddressFromECSTask(task types.Task) string {
|
||||
ips := []string{}
|
||||
|
||||
for _, attachment := range task.Attachments {
|
||||
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, kvp := range attachment.Details {
|
||||
if aws.ToString(kvp.Name) == "privateIPv4Address" {
|
||||
ips = append(ips, aws.ToString(kvp.Value))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return strings.Join(ips, "|")
|
||||
}
|
||||
|
||||
func getElasticNetworkInterfaceIDOfECSTask(task types.Task) string {
|
||||
for _, attachment := range task.Attachments {
|
||||
if aws.ToString(attachment.Type) != "ElasticNetworkInterface" || aws.ToString(attachment.Status) != "ATTACHED" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, kvp := range attachment.Details {
|
||||
if aws.ToString(kvp.Name) == "networkInterfaceId" {
|
||||
return aws.ToString(kvp.Value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
func TestECSTasks(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule ECSTasksModule
|
||||
expectedResult []MappedECSTask
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: ECSTasksModule{
|
||||
|
||||
AWSProfile: "default",
|
||||
AWSRegions: []string{"us-east-1", "us-west-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
|
||||
SkipAdminCheck: true,
|
||||
Goroutines: 30,
|
||||
EC2Client: &sdk.MockedEC2Client2{},
|
||||
ECSClient: &sdk.MockedECSClient{},
|
||||
},
|
||||
expectedResult: []MappedECSTask{{
|
||||
Cluster: "MyCluster",
|
||||
ID: "74de0355a10a4f979ac495c14EXAMPLE",
|
||||
ExternalIP: "203.0.113.12",
|
||||
Role: "test123",
|
||||
}},
|
||||
},
|
||||
}
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.ECSTasks(subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedTask := range subtest.expectedResult {
|
||||
if expectedTask.Cluster != subtest.testModule.MappedECSTasks[index].Cluster {
|
||||
log.Fatal("Cluster name does not match expected value")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+407
@@ -0,0 +1,407 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type EKSModule struct {
|
||||
// General configuration data
|
||||
// These interfaces are used for unit testing
|
||||
EKSClient sdk.EKSClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
// Main module data
|
||||
Clusters []Cluster
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Cluster struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Name string
|
||||
Endpoint string
|
||||
Public string
|
||||
OIDC string
|
||||
NodeGroup string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *EKSModule) EKS(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "eks"
|
||||
localAdminMap := make(map[string]bool)
|
||||
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating EKS clusters for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Cluster)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// Perform role analysis
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.Clusters {
|
||||
m.Clusters[i].Admin, m.Clusters[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Clusters[i].Role)
|
||||
}
|
||||
} else {
|
||||
for i := range m.Clusters {
|
||||
m.Clusters[i].Admin, m.Clusters[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Clusters[i].Role, m.iamSimClient, localAdminMap)
|
||||
}
|
||||
}
|
||||
|
||||
// This is the complete list of potential table columns
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"Endpoint",
|
||||
"Public",
|
||||
"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Region",
|
||||
"Name",
|
||||
//"Endpoint",
|
||||
"Public",
|
||||
//"OIDC",
|
||||
"NodeGroup",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
// Table rows
|
||||
|
||||
for i := range m.Clusters {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Clusters[i].Region,
|
||||
m.Clusters[i].Name,
|
||||
//m.Clusters[i].Endpoint,
|
||||
m.Clusters[i].Public,
|
||||
//m.Clusters[i].OIDC,
|
||||
m.Clusters[i].NodeGroup,
|
||||
m.Clusters[i].Role,
|
||||
m.Clusters[i].Admin,
|
||||
m.Clusters[i].CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
var seen []string
|
||||
for _, cluster := range m.Clusters {
|
||||
if !internal.Contains(cluster.Name, seen) {
|
||||
seen = append(seen, cluster.Name)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %d clusters with a total of %d node groups found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(seen), len(m.output.Body))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No clusters found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *EKSModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("eks", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getEKSRecordsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EKSModule) Receiver(receiver chan Cluster, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Clusters = append(m.Clusters, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EKSModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
pullFile := filepath.Join(path, "eks-kubeconfig-commands.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
|
||||
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the repositories.")
|
||||
out = out + fmt.Sprintln("# E.g., export profile=found_creds")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
var seen []string
|
||||
for _, cluster := range m.Clusters {
|
||||
|
||||
if !internal.Contains(cluster.Name, seen) {
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s eks update-kubeconfig --name %s\n", cluster.Region, cluster.Name)
|
||||
seen = append(seen, cluster.Name)
|
||||
}
|
||||
|
||||
}
|
||||
err = os.WriteFile(pullFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to authenticate to EKS and set up your kubeconfig"))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Note: Just because you have the eks:updatekubeconfig permission, this does not"))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green(" mean your IAM user has permissions in the cluster."))
|
||||
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *EKSModule) getEKSRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Cluster) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
var clusters []string
|
||||
|
||||
clusters, err := sdk.CachedEKSListClusters(m.EKSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, clusterName := range clusters {
|
||||
var role string
|
||||
var oidc string = ""
|
||||
var publicEndpoint = ""
|
||||
clusterDetails, err := sdk.CachedEKSDescribeCluster(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
//nodeGroups = append(nodeGroups, DescribeCluster.Cluster.)
|
||||
endpoint := aws.ToString(clusterDetails.Endpoint)
|
||||
if clusterDetails.Identity != nil && clusterDetails.Identity.Oidc != nil {
|
||||
oidc = aws.ToString(clusterDetails.Identity.Oidc.Issuer)
|
||||
|
||||
}
|
||||
if clusterDetails.ResourcesVpcConfig != nil {
|
||||
publicEndpoint = strconv.FormatBool(clusterDetails.ResourcesVpcConfig.EndpointPublicAccess)
|
||||
}
|
||||
// if DescribeCluster.Cluster.ResourcesVpcConfig.PublicAccessCidrs[0] == "0.0.0.0/0" {
|
||||
// publicCIDRs := "0.0.0.0/0"
|
||||
// } else {
|
||||
// publicCIDRs := "specific IPs"
|
||||
// }
|
||||
|
||||
ListNodeGroups, err := sdk.CachedEKSListNodeGroups(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, r)
|
||||
|
||||
if len(ListNodeGroups) > 0 {
|
||||
for _, nodeGroup := range ListNodeGroups {
|
||||
|
||||
nodeGroupDetails, err := sdk.CachedEKSDescribeNodeGroup(m.EKSClient, aws.ToString(m.Caller.Account), clusterName, nodeGroup, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
role = aws.ToString(nodeGroupDetails.NodeRole)
|
||||
|
||||
dataReceiver <- Cluster{
|
||||
AWSService: "EKS",
|
||||
Name: clusterName,
|
||||
Region: r,
|
||||
Endpoint: endpoint,
|
||||
Public: publicEndpoint,
|
||||
OIDC: oidc,
|
||||
NodeGroup: nodeGroup,
|
||||
Role: role,
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
}
|
||||
} else {
|
||||
role = aws.ToString(clusterDetails.RoleArn)
|
||||
dataReceiver <- Cluster{
|
||||
AWSService: "EKS",
|
||||
Name: clusterName,
|
||||
Region: r,
|
||||
Endpoint: endpoint,
|
||||
Public: publicEndpoint,
|
||||
OIDC: oidc,
|
||||
NodeGroup: "N/A",
|
||||
Role: role,
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+127
@@ -0,0 +1,127 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log"
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
type MockedEKSClientInterface struct {
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientInterface) ListClusters(ctx context.Context, params *eks.ListClustersInput, optFns ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
|
||||
testClusters := []string{}
|
||||
testClusters = append(testClusters, "test1")
|
||||
|
||||
testListClustersOutput := &eks.ListClustersOutput{
|
||||
Clusters: testClusters,
|
||||
}
|
||||
|
||||
return testListClustersOutput, nil
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientInterface) DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
|
||||
testIdentity := types.Identity{
|
||||
Oidc: &types.OIDC{
|
||||
Issuer: aws.String("abc123"),
|
||||
},
|
||||
}
|
||||
testRVPC := types.VpcConfigResponse{
|
||||
EndpointPublicAccess: true,
|
||||
}
|
||||
|
||||
testCluster := types.Cluster{
|
||||
Identity: &testIdentity,
|
||||
Endpoint: aws.String("http://endpoint.com"),
|
||||
ResourcesVpcConfig: &testRVPC,
|
||||
}
|
||||
|
||||
testDescribeClusterOutput := &eks.DescribeClusterOutput{
|
||||
Cluster: &testCluster,
|
||||
}
|
||||
|
||||
return testDescribeClusterOutput, nil
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientInterface) ListNodegroups(ctx context.Context, params *eks.ListNodegroupsInput, optFns ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
|
||||
testNodegroups := []string{}
|
||||
testNodegroups = append(testNodegroups, "test1")
|
||||
|
||||
testListNodegroupsOutput := &eks.ListNodegroupsOutput{
|
||||
Nodegroups: testNodegroups,
|
||||
}
|
||||
|
||||
return testListNodegroupsOutput, nil
|
||||
}
|
||||
|
||||
func (m *MockedEKSClientInterface) DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
|
||||
testNodegroup := types.Nodegroup{}
|
||||
testNodegroup.NodeRole = aws.String("roleABC")
|
||||
testDescribeNodegroupOutput := &eks.DescribeNodegroupOutput{
|
||||
Nodegroup: &testNodegroup,
|
||||
}
|
||||
|
||||
return testDescribeNodegroupOutput, nil
|
||||
}
|
||||
|
||||
func TestEks(t *testing.T) {
|
||||
subtests := []struct {
|
||||
name string
|
||||
outputDirectory string
|
||||
verbosity int
|
||||
testModule EKSModule
|
||||
expectedResult []Cluster
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
outputDirectory: ".",
|
||||
verbosity: 2,
|
||||
testModule: EKSModule{
|
||||
EKSClient: &MockedEKSClientInterface{},
|
||||
//IAMSimulatePrincipalPolicyClient: iam.SimulatePrincipalPolicyAPIClient,
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("test")},
|
||||
AWSProfile: "test",
|
||||
Goroutines: 30,
|
||||
AWSRegions: AWSRegions,
|
||||
SkipAdminCheck: true,
|
||||
},
|
||||
expectedResult: []Cluster{{
|
||||
Name: "test1",
|
||||
Endpoint: "http://endpoint.com",
|
||||
NodeGroup: "test1",
|
||||
OIDC: "abc123",
|
||||
Public: "true",
|
||||
}},
|
||||
},
|
||||
}
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.EKS(subtest.outputDirectory, subtest.verbosity)
|
||||
for index, expectedCluster := range subtest.expectedResult {
|
||||
if expectedCluster.Name != subtest.testModule.Clusters[index].Name {
|
||||
log.Fatal("Cluster name does not match expected value")
|
||||
}
|
||||
if expectedCluster.Endpoint != subtest.testModule.Clusters[index].Endpoint {
|
||||
log.Fatal("Cluster endpoint does not match expected value")
|
||||
}
|
||||
if expectedCluster.OIDC != subtest.testModule.Clusters[index].OIDC {
|
||||
log.Fatal("Cluster OIDC does not match expected value")
|
||||
}
|
||||
if expectedCluster.Public != subtest.testModule.Clusters[index].Public {
|
||||
log.Fatal("Cluster public does not match expected value")
|
||||
}
|
||||
if expectedCluster.NodeGroup != subtest.testModule.Clusters[index].NodeGroup {
|
||||
log.Fatal("Cluster NodeGroup does not match expected value")
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,309 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ElasticNetworkInterfacesModule struct {
|
||||
EC2Client sdk.AWSEC2ClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
MappedENIs []MappedENI
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type MappedENI struct {
|
||||
ID string
|
||||
Type string
|
||||
ExternalIP string
|
||||
PrivateIP string
|
||||
VPCID string
|
||||
AttachedInstance string
|
||||
Description string
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) ElasticNetworkInterfaces(outputDirectory string, verbosity int) {
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "elastic-network-interfaces"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating elastic network interfaces in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
spinnerDone := make(chan bool)
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
dataReceiver := make(chan MappedENI)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
m.printENIsData(outputDirectory, dataReceiver, verbosity)
|
||||
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) Receiver(receiver chan MappedENI, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.MappedENIs = append(m.MappedENIs, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) printENIsData(outputDirectory string, dataReceiver chan MappedENI, verbosity int) {
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"ID",
|
||||
"Type",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"VPC ID",
|
||||
"Attached Instance",
|
||||
"Description",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"ID",
|
||||
"Type",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"VPC ID",
|
||||
"Attached Instance",
|
||||
"Description",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"ID",
|
||||
"Type",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"VPC ID",
|
||||
"Attached Instance",
|
||||
"Description",
|
||||
}
|
||||
}
|
||||
|
||||
for _, eni := range m.MappedENIs {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
eni.ID,
|
||||
eni.Type,
|
||||
eni.ExternalIP,
|
||||
eni.PrivateIP,
|
||||
eni.VPCID,
|
||||
eni.AttachedInstance,
|
||||
eni.Description,
|
||||
},
|
||||
)
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName)
|
||||
fmt.Printf("[%s][%s] %s elastic network interfaces found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No elastic network interfaces found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) writeLoot(outputDirectory string) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
privateIPsFilename := filepath.Join(path, "elastic-network-interfaces-PrivateIPs.txt")
|
||||
publicIPsFilename := filepath.Join(path, "elastic-network-interfaces-PublicIPs.txt")
|
||||
|
||||
var publicIPs string
|
||||
var privateIPs string
|
||||
|
||||
for _, eni := range m.MappedENIs {
|
||||
if eni.ExternalIP != "NoExternalIP" {
|
||||
publicIPs = publicIPs + fmt.Sprintln(eni.ExternalIP)
|
||||
}
|
||||
if eni.PrivateIP != "" {
|
||||
privateIPs = privateIPs + fmt.Sprintln(eni.PrivateIP)
|
||||
}
|
||||
|
||||
}
|
||||
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
err = os.WriteFile(publicIPsFilename, []byte(publicIPs), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
|
||||
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan MappedENI) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getDescribeNetworkInterfaces(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ElasticNetworkInterfacesModule) getDescribeNetworkInterfaces(region string, dataReceiver chan MappedENI) {
|
||||
|
||||
NetworkInterfaces, err := sdk.CachedEC2DescribeNetworkInterfaces(m.EC2Client, aws.ToString(m.Caller.Account), region)
|
||||
|
||||
// var PaginationControl *string
|
||||
// for {
|
||||
// DescribeNetworkInterfaces, err := m.DescribeNetworkInterfacesClient.DescribeNetworkInterfaces(
|
||||
// context.TODO(),
|
||||
// &(ec2.DescribeNetworkInterfacesInput{
|
||||
// NextToken: PaginationControl,
|
||||
// }),
|
||||
// func(o *ec2.Options) {
|
||||
// o.Region = region
|
||||
// },
|
||||
// )
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, eni := range NetworkInterfaces {
|
||||
status := string(eni.Status)
|
||||
if status == "available" {
|
||||
continue // unused ENI
|
||||
}
|
||||
|
||||
mappedENI := MappedENI{
|
||||
ID: aws.ToString(eni.NetworkInterfaceId),
|
||||
Type: string(eni.InterfaceType),
|
||||
ExternalIP: getPublicIPOfElasticNetworkInterface(eni),
|
||||
PrivateIP: aws.ToString(eni.PrivateIpAddress),
|
||||
VPCID: aws.ToString(eni.VpcId),
|
||||
AttachedInstance: getAttachmentInstanceOfElasticNetworkInterface(eni),
|
||||
Description: aws.ToString(eni.Description),
|
||||
}
|
||||
|
||||
dataReceiver <- mappedENI
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func getPublicIPOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string {
|
||||
if elasticNetworkInterface.Association != nil {
|
||||
return aws.ToString(elasticNetworkInterface.Association.PublicIp)
|
||||
}
|
||||
|
||||
return "NoExternalIP"
|
||||
}
|
||||
|
||||
func getAttachmentInstanceOfElasticNetworkInterface(elasticNetworkInterface types.NetworkInterface) string {
|
||||
if elasticNetworkInterface.Attachment == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
return aws.ToString(elasticNetworkInterface.Attachment.InstanceId)
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
)
|
||||
|
||||
func TestElasticNetworkInterfaces(t *testing.T) {
|
||||
|
||||
m := ElasticNetworkInterfacesModule{
|
||||
AWSProfile: "default",
|
||||
AWSRegions: []string{"us-east-1", "us-west-1"},
|
||||
Caller: sts.GetCallerIdentityOutput{Arn: aws.String("arn:aws:iam::123456789012:user/cloudfox_unit_tests")},
|
||||
EC2Client: &sdk.MockedEC2Client2{},
|
||||
}
|
||||
|
||||
//m.ElasticNetworkInterfaces("table", ".", 3)
|
||||
subtests := []struct {
|
||||
name string
|
||||
testModule ElasticNetworkInterfacesModule
|
||||
expectedResult []MappedENI
|
||||
}{
|
||||
{
|
||||
name: "Test ElasticNetworkInterfaces",
|
||||
testModule: m,
|
||||
expectedResult: []MappedENI{
|
||||
{
|
||||
PrivateIP: "10.0.1.17",
|
||||
ExternalIP: "203.0.113.12",
|
||||
},
|
||||
{
|
||||
PrivateIP: "10.0.1.149",
|
||||
ExternalIP: "198.51.100.0",
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
internal.MockFileSystem(true)
|
||||
for _, subtest := range subtests {
|
||||
t.Run(subtest.name, func(t *testing.T) {
|
||||
subtest.testModule.ElasticNetworkInterfaces(".", 3)
|
||||
for index, expectedTask := range subtest.expectedResult {
|
||||
if expectedTask.ExternalIP != subtest.testModule.MappedENIs[index].ExternalIP {
|
||||
t.Errorf("expected %s, got %s", expectedTask.ExternalIP, subtest.testModule.MappedENIs[index].ExternalIP)
|
||||
}
|
||||
if expectedTask.PrivateIP != subtest.testModule.MappedENIs[index].PrivateIP {
|
||||
t.Errorf("expected %s, got %s", expectedTask.PrivateIP, subtest.testModule.MappedENIs[index].PrivateIP)
|
||||
}
|
||||
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+951
-851
File diff suppressed because it is too large.
Load diff
+292
-164
@@ -6,10 +6,11 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apprunner"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
@@ -19,6 +20,7 @@ import (
|
||||
"github.com/aws/aws-sdk-go-v2/service/lightsail"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sagemaker"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -33,10 +35,14 @@ import (
|
||||
|
||||
type EnvsModule struct {
|
||||
// General configuration data
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
OutputFormat string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSProfile string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
|
||||
// Service Clients
|
||||
ECSClient *ecs.Client
|
||||
@@ -47,11 +53,11 @@ type EnvsModule struct {
|
||||
|
||||
// Main module data
|
||||
EnvironmentVariables []EnvironmentVariable
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
modLog *logrus.Entry
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
}
|
||||
|
||||
type EnvironmentVariable struct {
|
||||
@@ -60,36 +66,39 @@ type EnvironmentVariable struct {
|
||||
region string
|
||||
environmentVarName string
|
||||
environmentVarValue string
|
||||
interesting bool
|
||||
}
|
||||
|
||||
func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *EnvsModule) PrintEnvs(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "env-vars"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating environment variables in all regions for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s] Supported Services: App Runner, Elastic Container Service, Lambda, Lightsail Containers, Sagemaker \n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] Enumerating environment variables in all regions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: App Runner, Elastic Container Service, Lambda, Lightsail Containers, Sagemaker \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, 50)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan EnvironmentVariable)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
@@ -100,10 +109,11 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
@@ -113,6 +123,7 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
|
||||
|
||||
// Table headers
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
@@ -120,28 +131,128 @@ func (m *EnvsModule) PrintEnvs(outputFormat string, outputDirectory string, verb
|
||||
"Value",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Key",
|
||||
"Value",
|
||||
}
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Key",
|
||||
"Value",
|
||||
}
|
||||
}
|
||||
|
||||
m.determineIfInteresting()
|
||||
//Table rows
|
||||
for _, envVar := range m.EnvironmentVariables {
|
||||
m.output.Body = append(
|
||||
m.output.Body, []string{
|
||||
envVar.service,
|
||||
envVar.region,
|
||||
envVar.name,
|
||||
envVar.environmentVarName,
|
||||
envVar.environmentVarValue,
|
||||
},
|
||||
)
|
||||
|
||||
if envVar.interesting {
|
||||
m.output.Body = append(
|
||||
m.output.Body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
envVar.service,
|
||||
envVar.region,
|
||||
envVar.name,
|
||||
magenta(envVar.environmentVarName),
|
||||
magenta(envVar.environmentVarValue),
|
||||
},
|
||||
)
|
||||
} else {
|
||||
m.output.Body = append(
|
||||
m.output.Body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
envVar.service,
|
||||
envVar.region,
|
||||
envVar.name,
|
||||
envVar.environmentVarName,
|
||||
envVar.environmentVarValue,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
fmt.Printf("[%s] %s environment variables found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
|
||||
// Create another table file that only contains the environment variables that seem to be secrets.
|
||||
body := m.interestingEnvVarsOnly()
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: body,
|
||||
TableCols: tableCols,
|
||||
Name: fmt.Sprintf("%s-interesting", m.output.CallingModule),
|
||||
SkipPrintToScreen: true,
|
||||
})
|
||||
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
fmt.Printf("[%s][%s] %s environment variables found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No environment variables found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No environment variables found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *EnvsModule) determineIfInteresting() {
|
||||
for i, envVar := range m.EnvironmentVariables {
|
||||
// If the environment variable name contains the word secret, key, token, or password, we'll make it magenta to make it stand out.
|
||||
lowerEnvVar := strings.ToLower(envVar.environmentVarName)
|
||||
if (strings.Contains(lowerEnvVar, "secret") && !strings.Contains(lowerEnvVar, "arn")) ||
|
||||
strings.Contains(lowerEnvVar, "key") ||
|
||||
strings.Contains(lowerEnvVar, "token") ||
|
||||
strings.Contains(lowerEnvVar, "user") ||
|
||||
strings.Contains(lowerEnvVar, "pass") {
|
||||
m.EnvironmentVariables[i].interesting = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func EnvVarsContains(element EnvironmentVariable, array []EnvironmentVariable) bool {
|
||||
for _, v := range array {
|
||||
if v == element {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone chan bool) {
|
||||
@@ -149,7 +260,9 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.EnvironmentVariables = append(m.EnvironmentVariables, data)
|
||||
if !EnvVarsContains(data, m.EnvironmentVariables) {
|
||||
m.EnvironmentVariables = append(m.EnvironmentVariables, data)
|
||||
}
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
@@ -159,26 +272,50 @@ func (m *EnvsModule) Receiver(receiver chan EnvironmentVariable, receiverDone ch
|
||||
|
||||
func (m *EnvsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan EnvironmentVariable) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, _ := servicemap.IsServiceInRegion("ecs", r)
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getECSEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getECSEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLambdaEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLambdaEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
// AppRunner is not supported in the aws service region catalog so we have to run it in all regions
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getAppRunnerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
res, err = servicemap.IsServiceInRegion("lightsail", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getLightsailEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getSagemakerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
res, err = servicemap.IsServiceInRegion("sagemaker", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
go m.getSagemakerEnvironmentVariablesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -197,13 +334,41 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
//var PaginationMarker *string
|
||||
|
||||
// This new approach takes one active task from each task family and grabs the container envs from that. Ran into a case where every version
|
||||
// of a task was listed as active and it brought cloudfox to a halt for a really long time.
|
||||
for _, familyName := range m.getTaskDefinitionFamilies(region) {
|
||||
|
||||
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
|
||||
context.TODO(),
|
||||
&ecs.DescribeTaskDefinitionInput{
|
||||
TaskDefinition: &familyName,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
|
||||
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EnvsModule) getTaskDefinitionFamilies(region string) []string {
|
||||
var allFamilyNames []string
|
||||
var PaginationMarker *string
|
||||
|
||||
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
ListTaskDefinitions, err := m.ECSClient.ListTaskDefinitions(
|
||||
|
||||
ListTaskDefinitionFamilies, err := m.ECSClient.ListTaskDefinitionFamilies(
|
||||
context.TODO(),
|
||||
&ecs.ListTaskDefinitionsInput{
|
||||
&ecs.ListTaskDefinitionFamiliesInput{
|
||||
NextToken: PaginationMarker,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
@@ -216,32 +381,17 @@ func (m *EnvsModule) getECSEnvironmentVariablesPerRegion(region string, wg *sync
|
||||
break
|
||||
}
|
||||
|
||||
for _, taskDefinitionArn := range ListTaskDefinitions.TaskDefinitionArns {
|
||||
DescribeTaskDefinition, err := m.ECSClient.DescribeTaskDefinition(
|
||||
context.TODO(),
|
||||
&ecs.DescribeTaskDefinitionInput{
|
||||
TaskDefinition: &taskDefinitionArn,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, containerDefinition := range DescribeTaskDefinition.TaskDefinition.ContainerDefinitions {
|
||||
m.getECSEnvironmentVariablesPerDefinition(containerDefinition, region, dataReceiver)
|
||||
}
|
||||
allFamilyNames = append(allFamilyNames, ListTaskDefinitionFamilies.Families...)
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListTaskDefinitions.NextToken != nil {
|
||||
PaginationMarker = ListTaskDefinitions.NextToken
|
||||
if ListTaskDefinitionFamilies.NextToken != nil {
|
||||
PaginationMarker = ListTaskDefinitionFamilies.NextToken
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return allFamilyNames
|
||||
|
||||
}
|
||||
|
||||
func (m *EnvsModule) getECSEnvironmentVariablesPerDefinition(containerDefinition ecsTypes.ContainerDefinition, region string, dataReceiver chan EnvironmentVariable) {
|
||||
@@ -273,38 +423,18 @@ func (m *EnvsModule) getLambdaEnvironmentVariablesPerRegion(region string, wg *s
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
|
||||
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
functions, err := m.LambdaClient.ListFunctions(
|
||||
context.TODO(),
|
||||
&lambda.ListFunctionsInput{
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
func(o *lambda.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, function := range functions.Functions {
|
||||
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if functions.NextMarker != nil {
|
||||
PaginationMarker = functions.NextMarker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
Functions, err := sdk.CachedLambdaListFunctions(m.LambdaClient, aws.ToString(m.Caller.Account), region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, function := range Functions {
|
||||
m.getLambdaEnvironmentVariablesPerFunction(function, region, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *EnvsModule) getLambdaEnvironmentVariablesPerFunction(function lambdaTypes.FunctionConfiguration, region string, dataReceiver chan EnvironmentVariable) {
|
||||
@@ -335,69 +465,51 @@ func (m *EnvsModule) getAppRunnerEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
// m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
|
||||
for {
|
||||
ListServices, err := m.AppRunnerClient.ListServices(
|
||||
context.TODO(),
|
||||
&(apprunner.ListServicesInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
if len(ListServices.ServiceSummaryList) > 0 {
|
||||
ServiceSummaryList, err := sdk.CachedAppRunnerListServices(m.AppRunnerClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
for _, service := range ListServices.ServiceSummaryList {
|
||||
name := aws.ToString(service.ServiceName)
|
||||
arn := aws.ToString(service.ServiceArn)
|
||||
awsService := "App Runner"
|
||||
if err != nil {
|
||||
//modLog.Error(err.Error())
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
if len(ServiceSummaryList) > 0 {
|
||||
|
||||
DescribeService, err := m.AppRunnerClient.DescribeService(
|
||||
context.TODO(),
|
||||
&apprunner.DescribeServiceInput{
|
||||
ServiceArn: &arn,
|
||||
},
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
for _, service := range ServiceSummaryList {
|
||||
name := aws.ToString(service.ServiceName)
|
||||
arn := aws.ToString(service.ServiceArn)
|
||||
awsService := "App Runner"
|
||||
|
||||
}
|
||||
|
||||
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
|
||||
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
|
||||
//fmt.Printf("%s - %s", k, v)
|
||||
dataReceiver <- EnvironmentVariable{
|
||||
service: awsService,
|
||||
name: name,
|
||||
region: r,
|
||||
environmentVarName: k,
|
||||
environmentVarValue: v,
|
||||
}
|
||||
}
|
||||
}
|
||||
DescribeService, err := m.AppRunnerClient.DescribeService(
|
||||
context.TODO(),
|
||||
&apprunner.DescribeServiceInput{
|
||||
ServiceArn: &arn,
|
||||
},
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListServices.NextToken != nil {
|
||||
PaginationControl = ListServices.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
if len(DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables) > 0 {
|
||||
for k, v := range DescribeService.Service.SourceConfiguration.ImageRepository.ImageConfiguration.RuntimeEnvironmentVariables {
|
||||
//fmt.Printf("%s - %s", k, v)
|
||||
dataReceiver <- EnvironmentVariable{
|
||||
service: awsService,
|
||||
name: name,
|
||||
region: r,
|
||||
environmentVarName: k,
|
||||
environmentVarValue: v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -418,13 +530,7 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
m.CommandCounter.Executing++
|
||||
awsService := "Lightsail [Container]"
|
||||
|
||||
GetContainerServices, err := m.LightsailClient.GetContainerServices(
|
||||
context.TODO(),
|
||||
&(lightsail.GetContainerServicesInput{}),
|
||||
func(o *lightsail.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
ContainerServices, err := sdk.CachedLightsailGetContainerServices(m.LightsailClient, aws.ToString(m.Caller.Account), r)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -434,9 +540,9 @@ func (m *EnvsModule) getLightsailEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
|
||||
if err == nil {
|
||||
|
||||
if len(GetContainerServices.ContainerServices) > 0 {
|
||||
if len(ContainerServices) > 0 {
|
||||
|
||||
for _, containerService := range GetContainerServices.ContainerServices {
|
||||
for _, containerService := range ContainerServices {
|
||||
for _, container := range containerService.CurrentDeployment.Containers {
|
||||
for k, v := range container.Environment {
|
||||
name := aws.ToString(containerService.ContainerServiceName)
|
||||
@@ -695,7 +801,10 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
if err == nil {
|
||||
|
||||
// find the number of environment variables in the model in a pointer safe way
|
||||
|
||||
if DescribeModel.PrimaryContainer != nil {
|
||||
|
||||
if len(DescribeModel.PrimaryContainer.Environment) > 0 {
|
||||
name := fmt.Sprintf("[Model] %s", aws.ToString(DescribeModel.ModelName))
|
||||
@@ -721,3 +830,22 @@ func (m *EnvsModule) getSagemakerEnvironmentVariablesPerRegion(r string, wg *syn
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (m *EnvsModule) interestingEnvVarsOnly() [][]string {
|
||||
var interestingBody [][]string
|
||||
for _, envVar := range m.EnvironmentVariables {
|
||||
if envVar.interesting {
|
||||
interestingBody = append(
|
||||
interestingBody, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
envVar.service,
|
||||
envVar.region,
|
||||
envVar.name,
|
||||
magenta(envVar.environmentVarName),
|
||||
magenta(envVar.environmentVarValue),
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
return interestingBody
|
||||
}
|
||||
+184
-118
@@ -7,39 +7,43 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/fsx"
|
||||
fsxTypes "github.com/aws/aws-sdk-go-v2/service/fsx/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/fatih/color"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var green = color.New(color.FgGreen).SprintFunc()
|
||||
|
||||
type FilesystemsModule struct {
|
||||
// General configuration data
|
||||
|
||||
EFSClient *efs.Client
|
||||
FSxClient *fsx.Client
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Filesystems []FilesystemObject
|
||||
|
||||
Regions [30]FilesystemObject
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -51,52 +55,53 @@ type FilesystemObject struct {
|
||||
IP string
|
||||
Policy string
|
||||
MountTarget string
|
||||
Permissions string
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *FilesystemsModule) PrintFilesystems(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "filesystems"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
}
|
||||
//populate region in the filesystems module struct
|
||||
for i, region := range m.AWSRegions {
|
||||
m.Regions[i].Region = region
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] Enumerating filesystems for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: EFS, FSx \n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan FilesystemObject)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
//execute regional checks
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.executeChecks(region, wg, dataReceiver)
|
||||
m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
@@ -105,13 +110,50 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
})
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"DNS Name",
|
||||
//"IP",
|
||||
"Mount Target",
|
||||
"Policy",
|
||||
"Permissions",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"DNS Name",
|
||||
//"IP",
|
||||
"Mount Target",
|
||||
"Policy",
|
||||
"Permissions",
|
||||
}
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Region",
|
||||
"Name",
|
||||
"DNS Name",
|
||||
//"IP",
|
||||
"Mount Target",
|
||||
"Policy",
|
||||
"Permissions",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Table rows
|
||||
@@ -119,32 +161,49 @@ func (m *FilesystemsModule) PrintFilesystems(outputFormat string, outputDirector
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Filesystems[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Filesystems[i].Region,
|
||||
m.Filesystems[i].Name,
|
||||
m.Filesystems[i].DnsName,
|
||||
//m.Filesystems[i].IP,
|
||||
m.Filesystems[i].MountTarget,
|
||||
m.Filesystems[i].Policy,
|
||||
m.Filesystems[i].Permissions,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
m.writeLoot(outputDirectory, verbosity)
|
||||
fmt.Printf("[%s] %s filesystems found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s filesystems found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No filesystems found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No filesystems found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDone chan bool) {
|
||||
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
@@ -157,19 +216,30 @@ func (m *FilesystemsModule) Receiver(receiver chan FilesystemObject, receiverDon
|
||||
}
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan FilesystemObject) {
|
||||
func (m *FilesystemsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer wg.Done()
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("efs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
wg.Add(1)
|
||||
go m.getEFSSharesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
//each fsx type has different supported regions so easier to just run this function against all enabled regions.
|
||||
wg.Add(1)
|
||||
go m.getEFSSharesPerRegion(r, wg, dataReceiver)
|
||||
wg.Add(1)
|
||||
go m.getFSxSharesPerRegion(r, wg, dataReceiver)
|
||||
go m.getFSxSharesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
f := filepath.Join(path, "filesystems-mount-commands.txt")
|
||||
@@ -180,8 +250,8 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
switch m.Filesystems[i].AWSService {
|
||||
case "EFS":
|
||||
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
|
||||
out = out + fmt.Sprintf("mkdir -p /efs/%s/\n", m.Filesystems[i].MountTarget)
|
||||
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs/%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
|
||||
out = out + fmt.Sprintf("mkdir -p /efs%s/\n", m.Filesystems[i].MountTarget)
|
||||
out = out + fmt.Sprintf("sudo mount -t nfs -o nfsvers=4.1,rsize=1048576,wsize=1048576,hard,timeo=600,retrans=2,noresvport %s:/ /efs%s\n\n", m.Filesystems[i].DnsName, m.Filesystems[i].MountTarget)
|
||||
case "FSx [LUSTRE]":
|
||||
out = out + fmt.Sprintf("########## Mount instructions for %s - %s ##########\n", m.Filesystems[i].AWSService, m.Filesystems[i].Name)
|
||||
out = out + fmt.Sprintln("#sudo amazon-linux-extras install -y lustre2.10")
|
||||
@@ -206,20 +276,21 @@ func (m *FilesystemsModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
|
||||
err = os.WriteFile(f, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), f)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), f)
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Use the commands below to try and mount the identified filesystems."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Use the commands below to try and mount the identified filesystems."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, dataReceiver chan FilesystemObject) {
|
||||
func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
@@ -229,100 +300,81 @@ func (m *FilesystemsModule) getEFSSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
var PaginationMarker2 *string
|
||||
|
||||
var policy string
|
||||
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
DescribeFileSystems, err := m.EFSClient.DescribeFileSystems(
|
||||
|
||||
DescribeFileSystems, err := sdk.CachedDescribeFileSystems(m.EFSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, filesystem := range DescribeFileSystems {
|
||||
name := filesystem.Name
|
||||
id := aws.ToString(filesystem.FileSystemId)
|
||||
|
||||
_, err := m.EFSClient.DescribeFileSystemPolicy(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemsInput{
|
||||
Marker: PaginationMarker,
|
||||
&efs.DescribeFileSystemPolicyInput{
|
||||
FileSystemId: filesystem.FileSystemId,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
policy = "Default (No IAM auth)"
|
||||
} else {
|
||||
policy = "IAM Authenticated"
|
||||
}
|
||||
|
||||
DescribeMountTargets, err := sdk.CachedDescribeMountTargets(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, filesystem := range DescribeFileSystems.FileSystems {
|
||||
name := filesystem.Name
|
||||
id := filesystem.FileSystemId
|
||||
//dnsName := fmt.Sprintf("%s.efs.%s.amazonaws.com", *id, r)
|
||||
for _, mountTarget := range DescribeMountTargets {
|
||||
ip := *mountTarget.IpAddress
|
||||
mountTarget := aws.ToString(mountTarget.MountTargetId)
|
||||
awsService := "EFS"
|
||||
|
||||
_, err := m.EFSClient.DescribeFileSystemPolicy(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemPolicyInput{
|
||||
FileSystemId: id,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
accessPoints, err := sdk.CachedDescribeAccessPoints(m.EFSClient, aws.ToString(m.Caller.Account), r, id)
|
||||
if err != nil {
|
||||
policy = "Default (No IAM auth)"
|
||||
}
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
for {
|
||||
DescribeMountTargets, err := m.EFSClient.DescribeMountTargets(
|
||||
context.TODO(),
|
||||
&efs.DescribeMountTargetsInput{
|
||||
FileSystemId: id,
|
||||
Marker: PaginationMarker2,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, mountTarget := range DescribeMountTargets.MountTargets {
|
||||
mountTargetId := mountTarget.MountTargetId
|
||||
ip := *mountTarget.IpAddress
|
||||
awsService := "EFS"
|
||||
|
||||
dataReceiver <- FilesystemObject{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: aws.ToString(name),
|
||||
DnsName: ip,
|
||||
Policy: policy,
|
||||
MountTarget: aws.ToString(mountTargetId),
|
||||
}
|
||||
}
|
||||
if DescribeMountTargets.NextMarker != nil {
|
||||
PaginationMarker2 = DescribeMountTargets.NextMarker
|
||||
} else {
|
||||
PaginationMarker2 = nil
|
||||
break
|
||||
m.CommandCounter.Error++
|
||||
dataReceiver <- FilesystemObject{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: aws.ToString(name),
|
||||
DnsName: ip,
|
||||
Policy: policy,
|
||||
MountTarget: mountTarget,
|
||||
Permissions: "access denied",
|
||||
}
|
||||
}
|
||||
|
||||
//awsService := fmt.Sprintf("EFS [%s]", fsType)
|
||||
for _, accessPoint := range accessPoints {
|
||||
path, permissions := m.getEFSfilesystemPermissions(accessPoint)
|
||||
dataReceiver <- FilesystemObject{
|
||||
AWSService: awsService,
|
||||
Region: r,
|
||||
Name: aws.ToString(name),
|
||||
DnsName: ip,
|
||||
Policy: policy,
|
||||
MountTarget: path,
|
||||
Permissions: permissions,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeFileSystems.NextMarker != nil {
|
||||
PaginationMarker = DescribeFileSystems.NextMarker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, dataReceiver chan FilesystemObject) {
|
||||
func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan FilesystemObject) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
@@ -351,7 +403,8 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
sharedLogger.Error(err.Error())
|
||||
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
@@ -403,7 +456,7 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
context.TODO(),
|
||||
&fsx.DescribeVolumesInput{
|
||||
Filters: []fsxTypes.VolumeFilter{
|
||||
fsxTypes.VolumeFilter{
|
||||
{
|
||||
Name: "file-system-id",
|
||||
Values: []string{id},
|
||||
}},
|
||||
@@ -466,3 +519,16 @@ func (m *FilesystemsModule) getFSxSharesPerRegion(r string, wg *sync.WaitGroup,
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *FilesystemsModule) getEFSfilesystemPermissions(accessPoint types.AccessPointDescription) (string, string) {
|
||||
var path string
|
||||
var permissions string
|
||||
|
||||
if accessPoint.AccessPointId != nil && accessPoint.RootDirectory != nil && accessPoint.RootDirectory.CreationInfo != nil {
|
||||
path = aws.ToString(accessPoint.RootDirectory.Path)
|
||||
permissions = aws.ToString(accessPoint.RootDirectory.CreationInfo.Permissions)
|
||||
return path, permissions
|
||||
}
|
||||
return "", ""
|
||||
|
||||
}
|
||||
+216
-169
@@ -7,11 +7,12 @@ import (
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
@@ -20,18 +21,22 @@ import (
|
||||
|
||||
type IamSimulatorModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Goroutines int
|
||||
AWSProfileProvided string
|
||||
AWSProfileStub string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
SimulatorResults []SimulatorResult
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -50,101 +55,133 @@ var (
|
||||
"ssm:GetParameter",
|
||||
"s3:ListBucket",
|
||||
"s3:GetObject",
|
||||
"ssm:sSendCommand",
|
||||
"ssm:SendCommand",
|
||||
"ssm:StartSession",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:GetAuthorizationToken",
|
||||
"eks:UpdateClusterConfig",
|
||||
"lambda:ListFunctions",
|
||||
"ecs:DescribeTaskDefinition",
|
||||
"apprunner:DescribeService",
|
||||
"ec2:DescribeInstanceAttributeInput",
|
||||
"sns:Subscribe",
|
||||
"sqs:SendMessage",
|
||||
}
|
||||
adminActionNames = []string{
|
||||
"iam:PutUserPolicy",
|
||||
"iam:AttachUserPolicy",
|
||||
"iam:PutRolePolicy",
|
||||
"iam:AttachRolePolicy",
|
||||
"secretsmanager:GetSecretValue",
|
||||
"ssm:GetDocument",
|
||||
}
|
||||
TxtLogger = internal.TxtLogger()
|
||||
)
|
||||
|
||||
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string, resource string, outputDirectory string, verbosity int) {
|
||||
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "iam-simulator"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
var filename string
|
||||
var actionList []string
|
||||
var pmapperCommands []string
|
||||
var pmapperOutFileName string
|
||||
var inputArn string
|
||||
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
if m.AWSProfileProvided == "" {
|
||||
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
|
||||
} else {
|
||||
m.AWSProfileStub = m.AWSProfileProvided
|
||||
}
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan SimulatorResult)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
// This double if/else section is here to handle the cases where --principal or --action (or both) are specified.
|
||||
if principal != "" {
|
||||
if action != "" {
|
||||
// The user specified a specific --principal and a specific --action
|
||||
fmt.Printf("[%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), principal, action)
|
||||
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
fmt.Printf("[%s][%s] Checking to see if %s can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal, action)
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
actionList = append(actionList, action)
|
||||
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
|
||||
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
// try as a role
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
|
||||
m.getPolicySimulatorResult((&inputArn), actionList, resource, dataReceiver)
|
||||
// try as a user
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
m.getPolicySimulatorResult((&inputArn), actionList, resource, dataReceiver)
|
||||
} else {
|
||||
// the arn was supplied so just run it
|
||||
m.getPolicySimulatorResult((&principal), actionList, resource, dataReceiver)
|
||||
}
|
||||
|
||||
} else {
|
||||
// The user specified a specific --principal, but --action was empty
|
||||
fmt.Printf("[%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), principal)
|
||||
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
|
||||
fmt.Printf("[%s][%s] Checking to see if %s can do any actions of interest.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), principal)
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
|
||||
// if user supplied a principal name without the arn, try to create the arn as a user and as a role and run both
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
// try as a role
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
|
||||
m.getPolicySimulatorResult((&inputArn), defaultActionNames, resource, dataReceiver)
|
||||
// try as a user
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
m.getPolicySimulatorResult((&inputArn), defaultActionNames, resource, dataReceiver)
|
||||
} else {
|
||||
// the arn was supplied so just run it
|
||||
m.getPolicySimulatorResult((&principal), defaultActionNames, resource, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
} else {
|
||||
if action != "" {
|
||||
// The did not specify a specific --principal, but they did specify an --action
|
||||
fmt.Printf("[%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), action)
|
||||
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
fmt.Printf("[%s][%s] Checking to see if any principal can do %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), action)
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
actionList = append(actionList, action)
|
||||
wg.Add(1)
|
||||
m.getIAMUsers(wg, actionList, resource, dataReceiver)
|
||||
wg.Add(1)
|
||||
m.getIAMRoles(wg, actionList, resource, dataReceiver)
|
||||
pmapperOutFileName = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
|
||||
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
if m.AWSProfileProvided != "" {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
|
||||
} else {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
|
||||
}
|
||||
} else {
|
||||
// Both --principal and --action are empty. Run in default mode!
|
||||
fmt.Printf("[%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
m.output.FullFilename = m.output.CallingModule
|
||||
fmt.Printf("[%s][%s] Running multiple iam-simulator queries for account %s. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
|
||||
filename = m.output.CallingModule
|
||||
m.executeChecks(wg, resource, dataReceiver)
|
||||
for _, action := range defaultActionNames {
|
||||
pmapperOutFileName = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfile, action, resource, pmapperOutFileName))
|
||||
pmapperOutFileName = filepath.Join(filename, "loot", fmt.Sprintf("pmapper-output-%s.txt", action))
|
||||
if m.AWSProfileProvided != "" {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper --profile %s query \"who can do %s with %s\" | tee %s\n", m.AWSProfileProvided, action, resource, pmapperOutFileName))
|
||||
} else {
|
||||
pmapperCommands = append(pmapperCommands, fmt.Sprintf("pmapper query \"who can do %s with %s\" | tee %s\n", action, resource, pmapperOutFileName))
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
@@ -153,11 +190,36 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
|
||||
// Regardless of what options were selected, for now at least, we will always print the data using the output module (table/csv mode)
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Account",
|
||||
"Principal",
|
||||
"Query",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Principal",
|
||||
"Query",
|
||||
}
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Principal",
|
||||
"Query",
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(m.SimulatorResults, func(i, j int) bool {
|
||||
return m.SimulatorResults[i].Query < m.SimulatorResults[j].Query
|
||||
})
|
||||
@@ -167,7 +229,7 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.SimulatorResults[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.SimulatorResults[i].Principal,
|
||||
m.SimulatorResults[i].Query,
|
||||
},
|
||||
@@ -175,23 +237,39 @@ func (m *IamSimulatorModule) PrintIamSimulator(principal string, action string,
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
fmt.Printf("[%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule))
|
||||
// fmt.Printf("[%s]\t\tpmapper --profile %s graph create\n", cyan(m.output.CallingModule), m.AWSProfile)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: filename,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfileStub
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
fmt.Printf("[%s][%s] We suggest running the pmapper commands in the loot file to get the same information but taking privesc paths into account.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
// fmt.Printf("[%s]\t\tpmapper --profile %s graph create\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
// for _, line := range pmapperCommands {
|
||||
// fmt.Printf("[%s]\t\t%s", cyan(m.output.CallingModule), line)
|
||||
// fmt.Printf("[%s]\t\t%s", cyan(m.output.CallingModule), cyan(m.AWSProfile), line)
|
||||
// }
|
||||
m.writeLoot(outputDirectory, verbosity, pmapperCommands)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity, pmapperCommands)
|
||||
|
||||
} else if principal != "" || action != "" {
|
||||
fmt.Printf("[%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No allowed permissions identified, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pmapperCommands []string) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -200,7 +278,11 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
|
||||
|
||||
outFile := filepath.Join(path, "iam-simulator-pmapper-commands.txt")
|
||||
var out string
|
||||
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfile)
|
||||
if m.AWSProfileProvided != "" {
|
||||
out = out + fmt.Sprintf("pmapper --profile %s graph create\n", m.AWSProfileProvided)
|
||||
} else {
|
||||
out = out + fmt.Sprintf("pmapper graph create\n")
|
||||
}
|
||||
for _, line := range pmapperCommands {
|
||||
out = out + line
|
||||
}
|
||||
@@ -212,12 +294,12 @@ func (m *IamSimulatorModule) writeLoot(outputDirectory string, verbosity int, pm
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("We suggest running these pmapper commands in the loot file to get the same information but taking privesc paths into account."))
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s] %s \n\n", cyan(m.output.CallingModule), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), outFile)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), outFile)
|
||||
|
||||
}
|
||||
|
||||
@@ -250,40 +332,31 @@ func (m *IamSimulatorModule) getIAMUsers(wg *sync.WaitGroup, actions []string, r
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var adminCheckResult bool
|
||||
|
||||
for {
|
||||
ListUsers, err := m.IAMClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
|
||||
for _, user := range ListUsers.Users {
|
||||
//name := user.UserName
|
||||
principal := user.Arn
|
||||
adminCheckResult = m.policySimulatorAdminCheck(principal, adminActionNames, resource, dataReceiver)
|
||||
if !adminCheckResult {
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, user := range ListUsers {
|
||||
//name := user.UserName
|
||||
principal := user.Arn
|
||||
adminCheckResult := m.isPrincipalAnAdmin(principal)
|
||||
if adminCheckResult {
|
||||
query := "Appears to be an administrator"
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -298,113 +371,96 @@ func (m *IamSimulatorModule) getIAMRoles(wg *sync.WaitGroup, actions []string, r
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var adminCheckResult bool
|
||||
|
||||
for {
|
||||
ListRoles, err := m.IAMClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
|
||||
for _, role := range ListRoles.Roles {
|
||||
//name := user.UserName
|
||||
principal := role.Arn
|
||||
adminCheckResult = m.policySimulatorAdminCheck(principal, adminActionNames, resource, dataReceiver)
|
||||
if !adminCheckResult {
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
for _, role := range ListRoles {
|
||||
//name := user.UserName
|
||||
principal := role.Arn
|
||||
adminCheckResult := m.isPrincipalAnAdmin(principal)
|
||||
if adminCheckResult {
|
||||
query := "Appears to be an administrator"
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
} else {
|
||||
m.getPolicySimulatorResult(principal, actions, resource, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamSimulatorModule) getPolicySimulatorResult(principal *string, actionNames []string, resource string, dataReceiver chan SimulatorResult) {
|
||||
var PaginationControl2 *string
|
||||
|
||||
//var policySourceArn = "*"
|
||||
//var arn *string
|
||||
var resourceArns []string
|
||||
resourceArns = append(resourceArns, resource)
|
||||
//var resourceArns = []string{"*"}
|
||||
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
ActionNames: actionNames,
|
||||
PolicySourceArn: principal,
|
||||
ResourceArns: resourceArns,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
|
||||
EvaluationResults, err := sdk.CachedIamSimulatePrincipalPolicy(m.IAMClient, aws.ToString(m.Caller.Account), principal, actionNames, resourceArns)
|
||||
|
||||
break
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
m.modLog.Error(fmt.Sprintf("Failed to query actions for %s\n\n", aws.ToString(principal)))
|
||||
return
|
||||
}
|
||||
|
||||
for _, result := range SimulatePrincipalPolicy.EvaluationResults {
|
||||
evalName := result.EvalActionName
|
||||
query := fmt.Sprintf("can %s on %s", *evalName, resource)
|
||||
decision := result.EvalDecision
|
||||
if decision == "allowed" {
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: string(decision),
|
||||
}
|
||||
for _, result := range EvaluationResults {
|
||||
evalName := result.EvalActionName
|
||||
query := fmt.Sprintf("can %s on %s", *evalName, resource)
|
||||
decision := result.EvalDecision
|
||||
if decision == "allowed" {
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: string(decision),
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if SimulatePrincipalPolicy.Marker != nil {
|
||||
PaginationControl2 = SimulatePrincipalPolicy.Marker
|
||||
} else {
|
||||
PaginationControl2 = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamSimulatorModule) policySimulatorAdminCheck(principal *string, actionNames []string, resource string, dataReceiver chan SimulatorResult) bool {
|
||||
func (m *IamSimulatorModule) isPrincipalAnAdmin(principal *string) bool {
|
||||
var PaginationControl2 *string
|
||||
var resourceArns []string
|
||||
resourceArns = append(resourceArns, resource)
|
||||
|
||||
resourceArns = append(resourceArns, "*")
|
||||
var adminActionNames = []string{
|
||||
"iam:PutUserPolicy",
|
||||
"iam:AttachUserPolicy",
|
||||
"iam:PutRolePolicy",
|
||||
"iam:AttachRolePolicy",
|
||||
"secretsmanager:GetSecretValue",
|
||||
"ssm:GetDocument",
|
||||
}
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := m.IAMClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
ActionNames: actionNames,
|
||||
ActionNames: adminActionNames,
|
||||
PolicySourceArn: principal,
|
||||
ResourceArns: resourceArns,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
//m.modLog.Error(err.Error())
|
||||
TxtLogger.Println(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
m.modLog.Error(fmt.Sprintf("Failed admin check on %s\n\n", aws.ToString(principal)))
|
||||
//m.modLog.Error(fmt.Sprintf("Failed admin check on %s\n\n", aws.ToString(principal)))
|
||||
TxtLogger.Printf("Failed admin check on %s\n\n", aws.ToString(principal))
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -428,14 +484,5 @@ func (m *IamSimulatorModule) policySimulatorAdminCheck(principal *string, action
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
query := fmt.Sprintf("Appears to be an administrator")
|
||||
dataReceiver <- SimulatorResult{
|
||||
AWSService: "IAM",
|
||||
Principal: aws.ToString(principal),
|
||||
Query: query,
|
||||
Decision: "",
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
+332
-130
@@ -4,36 +4,49 @@ import (
|
||||
"context"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type InstancesModule struct {
|
||||
// General configuration data
|
||||
EC2Client *ec2.Client
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
UserDataAttributesOnly bool
|
||||
AWSProfile string
|
||||
EC2Client sdk.AWSEC2ClientInterface
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
UserDataAttributesOnly bool
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
InstanceProfileToRolesMap map[string][]iamTypes.Role
|
||||
SkipAdminCheck bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Module's Results
|
||||
MappedInstances []MappedInstance
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -46,56 +59,65 @@ type MappedInstance struct {
|
||||
ExternalIP string
|
||||
PrivateIP string
|
||||
Profile string
|
||||
Admin string
|
||||
Role string
|
||||
Region string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *InstancesModule) Instances(filter string, outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *InstancesModule) Instances(filter string, outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "instances"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
localAdminMap := make(map[string]bool)
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
// regions, errtemp := m.EC2Client.DescribeRegions(
|
||||
// context.TODO(),
|
||||
// &ec2.DescribeRegionsInput{})
|
||||
|
||||
// if errtemp != nil {
|
||||
// m.modLog.Error(errtemp.Error())
|
||||
// }
|
||||
// for _, reg := range regions.Regions {
|
||||
// fmt.Println(*reg.RegionName)
|
||||
// }
|
||||
// Populate the instance profile to roles map. You can't use getInstanceProfile by name, so the only way to do this is to
|
||||
// list all of the profiles, save the data, and then do the lookup yourself.
|
||||
|
||||
// Parses the type of filter being used (file with instances or single instance id)
|
||||
var instancesToSearch []string
|
||||
if filter == "all" {
|
||||
instancesToSearch = []string{"all"}
|
||||
} else {
|
||||
instancesToSearch = utils.LoadFileLinesIntoArray(filter)
|
||||
instancesToSearch = internal.LoadFileLinesIntoArray(filter)
|
||||
}
|
||||
|
||||
//Connects to EC2 service and maps instances
|
||||
fmt.Printf("[%s] Enumerating EC2 instances in all regions for account %s\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Enumerating EC2 instances in all regions for account %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
// Initialized the tools we'll need to check if any workload roles are admin or can privesc to admin
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan MappedInstance)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
m.getRolesFromInstanceProfiles()
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
@@ -105,19 +127,31 @@ func (m *InstancesModule) Instances(filter string, outputFormat string, outputDi
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Perform role analysis
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.MappedInstances {
|
||||
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.MappedInstances[i].Role)
|
||||
}
|
||||
} else {
|
||||
for i := range m.MappedInstances {
|
||||
m.MappedInstances[i].Admin, m.MappedInstances[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.MappedInstances[i].Role, m.iamSimClient, localAdminMap)
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// This conditional block will either dump the userData attribute content or the general instances data, depending on what you select via command line.
|
||||
//fmt.Printf("\n[*] Preparing output...\n\n")
|
||||
if m.UserDataAttributesOnly {
|
||||
m.printInstancesUserDataAttributesOnly(outputFormat, outputDirectory, dataReceiver)
|
||||
m.printInstancesUserDataAttributesOnly(outputDirectory, dataReceiver)
|
||||
} else {
|
||||
m.printGeneralInstanceData(outputFormat, outputDirectory, dataReceiver)
|
||||
m.printGeneralInstanceData(outputDirectory, dataReceiver, verbosity)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -135,14 +169,13 @@ func (m *InstancesModule) Receiver(receiver chan MappedInstance, receiverDone ch
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
|
||||
func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputDirectory string, dataReceiver chan MappedInstance) {
|
||||
defer func() {
|
||||
m.output.CallingModule = "instances"
|
||||
}()
|
||||
|
||||
m.output.CallingModule = "instance-userdata"
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)), "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -150,7 +183,7 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
}
|
||||
userDataFileName := filepath.Join(path, fmt.Sprintf("%s.txt", m.output.CallingModule))
|
||||
|
||||
var userDataOut string = fmt.Sprintf("=============================================\n")
|
||||
var userDataOut string = fmt.Sprintln("=============================================")
|
||||
|
||||
for _, instance := range m.MappedInstances {
|
||||
userData, err := m.getInstanceUserDataAttribute(aws.String(instance.ID), instance.Region)
|
||||
@@ -166,12 +199,13 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
userDataOut = userDataOut + fmt.Sprintf("Region: %s\n", instance.Region)
|
||||
userDataOut = userDataOut + fmt.Sprintf("Instance Profile: %s\n\n", instance.Profile)
|
||||
userDataOut = userDataOut + fmt.Sprintf("User Data: \n%s\n", aws.ToString(userData))
|
||||
userDataOut = userDataOut + fmt.Sprintf("=============================================\n\n")
|
||||
userDataOut = userDataOut + "=============================================\n\n"
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(m.MappedInstances) > 0 {
|
||||
if m.output.Verbosity > 1 {
|
||||
// only create a file if if there is at least one instance AND at least one instance had user-data.
|
||||
if (len(m.MappedInstances) > 0) && (userDataOut != "=============================================\n") {
|
||||
if m.output.Verbosity > 2 {
|
||||
fmt.Printf("%s", userDataOut)
|
||||
}
|
||||
err = os.WriteFile(userDataFileName, []byte(userDataOut), 0644)
|
||||
@@ -179,55 +213,132 @@ func (m *InstancesModule) printInstancesUserDataAttributesOnly(outputFormat stri
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), userDataFileName)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), userDataFileName)
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No user data found, skipping the creation of an output file\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) printGeneralInstanceData(outputFormat string, outputDirectory string, dataReceiver chan MappedInstance) {
|
||||
func (m *InstancesModule) printGeneralInstanceData(outputDirectory string, dataReceiver chan MappedInstance, verbosity int) {
|
||||
// Prepare Table headers
|
||||
//m.output.Headers = table.Row{
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
//"ID",
|
||||
"Name",
|
||||
"Arn",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Profile",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
//"ID",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"ID",
|
||||
"Zone",
|
||||
"State",
|
||||
"External IP",
|
||||
"Internal IP",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
//Table rows
|
||||
for _, instance := range m.MappedInstances {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
//table.Row{
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
//instance.ID,
|
||||
instance.Name,
|
||||
instance.Arn,
|
||||
//instance.Arn,
|
||||
instance.ID,
|
||||
instance.AvailabilityZone,
|
||||
instance.State,
|
||||
instance.ExternalIP,
|
||||
instance.PrivateIP,
|
||||
instance.Profile,
|
||||
instance.Role,
|
||||
instance.Admin,
|
||||
instance.CanPrivEsc,
|
||||
},
|
||||
)
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
|
||||
m.writeLoot(outputDirectory)
|
||||
fmt.Printf("[%s] %s instances found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
//m.writeLoot(m.output.FilePath)
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s instances found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No instances found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No instances found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
func (m *InstancesModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -235,9 +346,14 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
}
|
||||
privateIPsFilename := filepath.Join(path, "instances-ec2PrivateIPs.txt")
|
||||
publicIPsFilename := filepath.Join(path, "instances-ec2PublicIPs.txt")
|
||||
ssmCommandsFilename := filepath.Join(path, "instances-ssmCommands.txt")
|
||||
ec2InstanceConnectCommandsFilename := filepath.Join(path, "instances-ec2InstanceConnectCommands.txt")
|
||||
|
||||
var publicIPs string
|
||||
var privateIPs string
|
||||
var ssmCommands string
|
||||
var ec2InstanceConnectCommands string
|
||||
var headlineName string
|
||||
|
||||
for _, instance := range m.MappedInstances {
|
||||
if instance.ExternalIP != "NoExternalIP" {
|
||||
@@ -247,6 +363,32 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
privateIPs = privateIPs + fmt.Sprintln(instance.PrivateIP)
|
||||
}
|
||||
|
||||
if instance.Name != "" {
|
||||
headlineName = fmt.Sprintf("%s/%s", instance.Name, instance.ID)
|
||||
} else {
|
||||
headlineName = fmt.Sprintf("%s", instance.ID)
|
||||
}
|
||||
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("### SSM start-session to %s ###\n", headlineName)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("# You'll need the AWS CLI session manager plugin installed: https://docs.aws.amazon.com/systems-manager/latest/userguide/session-manager-working-with-install-plugin.html\n")
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm start-session --target %s\n\n", instance.Region, instance.ID)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("### SSM send-command to %s ###\n", headlineName)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("# If you just want to run one command you can use send-command, but really, start-session is way easier\n")
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm send-command --instance-ids %s --document-name AWS-RunShellScript --parameters commands=\"aws sts get-caller-identity\" \n", instance.Region, instance.ID)
|
||||
ssmCommands = ssmCommands + fmt.Sprintf("aws --profile $profile --region %s ssm get-command-invocation --output text --instance-id %s --command-id <command-id-from-previous-command>\n\n", instance.Region, instance.ID)
|
||||
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("############## Instance: %s ##############\n", headlineName)
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("-----------------------------------------------------------------------\n")
|
||||
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("### EC2 Instance Connect to %s ###\n", instance.ID)
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("# You'll need to change the --instance-os-user and --ssh-public-key parameters to match your own setup\n")
|
||||
ec2InstanceConnectCommands = ec2InstanceConnectCommands + fmt.Sprintf("aws --profile $profile --region %s ec2-instance-connect send-ssh-public-key --instance-id %s --instance-os-user ec2-user --ssh-public-key file://~/.ssh/id_rsa.pub\n\n", instance.Region, instance.ID)
|
||||
|
||||
}
|
||||
err = os.WriteFile(privateIPsFilename, []byte(privateIPs), 0644)
|
||||
if err != nil {
|
||||
@@ -258,44 +400,69 @@ func (m *InstancesModule) writeLoot(outputDirectory string) {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
err = os.WriteFile(ssmCommandsFilename, []byte(ssmCommands), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
err = os.WriteFile(ec2InstanceConnectCommandsFilename, []byte(ec2InstanceConnectCommands), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), privateIPsFilename)
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), publicIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), privateIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), publicIPsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ssmCommandsFilename)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), ec2InstanceConnectCommandsFilename)
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Loot file for instance command:"))
|
||||
fmt.Printf("Private IPs:\n\n")
|
||||
fmt.Print(privateIPs)
|
||||
fmt.Printf("Public IPs:\n\n")
|
||||
fmt.Print(publicIPs)
|
||||
fmt.Printf("SSM Commands:\n\n")
|
||||
fmt.Print(ssmCommands)
|
||||
fmt.Printf("EC2 Instance Connect Commands:\n\n")
|
||||
fmt.Print(ec2InstanceConnectCommands)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *InstancesModule) executeChecks(instancesToSearch []string, r string, wg *sync.WaitGroup, dataReceiver chan MappedInstance) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ec2", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getDescribeInstances(instancesToSearch, r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, region string) (userData *string, err error) {
|
||||
|
||||
Attributes, err := m.EC2Client.DescribeInstanceAttribute(
|
||||
context.TODO(),
|
||||
&ec2.DescribeInstanceAttributeInput{
|
||||
InstanceId: instanceID,
|
||||
Attribute: types.InstanceAttributeName("userData"),
|
||||
},
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
UserData, err := sdk.CachedEC2DescribeInstanceAttributeUserData(m.EC2Client, aws.ToString(m.Caller.Account), region, aws.ToString(instanceID))
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return nil, err
|
||||
} else {
|
||||
if Attributes.UserData.Value == nil {
|
||||
if UserData == "" {
|
||||
return aws.String("NoUserData"), nil
|
||||
} else {
|
||||
data, _ := base64.StdEncoding.DecodeString(*Attributes.UserData.Value)
|
||||
data, _ := base64.StdEncoding.DecodeString(UserData)
|
||||
return aws.String(string(data)), nil
|
||||
}
|
||||
}
|
||||
@@ -305,51 +472,27 @@ func (m *InstancesModule) getInstanceUserDataAttribute(instanceID *string, regio
|
||||
func (m *InstancesModule) getDescribeInstances(instancesToSearch []string, region string, dataReceiver chan MappedInstance) {
|
||||
|
||||
// The "PaginationControl" value is nil when there's no more data to return.
|
||||
var PaginationControl *string
|
||||
for {
|
||||
|
||||
DescribeInstances, err := m.EC2Client.DescribeInstances(
|
||||
context.TODO(),
|
||||
&(ec2.DescribeInstancesInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
Instances, err := sdk.CachedEC2DescribeInstances(m.EC2Client, aws.ToString(m.Caller.Account), region)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
for _, instance := range Instances {
|
||||
if instancesToSearch[0] == "all" || internal.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
|
||||
m.loadInstanceData(instance, region, dataReceiver)
|
||||
}
|
||||
|
||||
for _, reservation := range DescribeInstances.Reservations {
|
||||
accountId := reservation.OwnerId
|
||||
|
||||
for _, instance := range reservation.Instances {
|
||||
|
||||
if instancesToSearch[0] == "all" || utils.Contains(aws.ToString(instance.InstanceId), instancesToSearch) {
|
||||
m.loadInstanceData(instance, region, accountId, dataReceiver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if DescribeInstances.NextToken != nil {
|
||||
PaginationControl = DescribeInstances.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, accountId *string, dataReceiver chan MappedInstance) {
|
||||
func (m *InstancesModule) loadInstanceData(instance types.Instance, region string, dataReceiver chan MappedInstance) {
|
||||
|
||||
var profile string
|
||||
var externalIP string
|
||||
var name string = ""
|
||||
var adminRole string = ""
|
||||
var profileArn, profileName, name string
|
||||
|
||||
// The name is in a tag so we have to do this to grab the value from the right tag
|
||||
for _, tag := range instance.Tags {
|
||||
@@ -359,15 +502,7 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
|
||||
|
||||
}
|
||||
|
||||
arn := fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId))
|
||||
|
||||
if instance.IamInstanceProfile == nil {
|
||||
profile = "NoInstanceProfile"
|
||||
} else {
|
||||
// This returns only the role name without the preceding forward slash.
|
||||
profileARN := aws.ToString(instance.IamInstanceProfile.Arn)
|
||||
profile = strings.Split(profileARN, "/")[len(strings.Split(profileARN, "/"))-1]
|
||||
}
|
||||
//arn := fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(accountId), aws.ToString(instance.InstanceId))
|
||||
|
||||
if instance.PublicIpAddress == nil {
|
||||
externalIP = "NoExternalIP"
|
||||
@@ -375,16 +510,83 @@ func (m *InstancesModule) loadInstanceData(instance types.Instance, region strin
|
||||
externalIP = aws.ToString(instance.PublicIpAddress)
|
||||
}
|
||||
|
||||
dataReceiver <- MappedInstance{
|
||||
aws.ToString(instance.InstanceId),
|
||||
aws.ToString(&name),
|
||||
arn,
|
||||
aws.ToString(instance.Placement.AvailabilityZone),
|
||||
string(instance.State.Name),
|
||||
externalIP,
|
||||
aws.ToString(instance.PrivateIpAddress),
|
||||
profile,
|
||||
region,
|
||||
if instance.IamInstanceProfile == nil {
|
||||
profile = "NoInstanceProfile"
|
||||
dataReceiver <- MappedInstance{
|
||||
ID: aws.ToString(instance.InstanceId),
|
||||
Name: aws.ToString(&name),
|
||||
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
|
||||
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
|
||||
State: string(instance.State.Name),
|
||||
ExternalIP: externalIP,
|
||||
PrivateIP: aws.ToString(instance.PrivateIpAddress),
|
||||
Profile: profile,
|
||||
Role: "",
|
||||
Region: region,
|
||||
Admin: adminRole,
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
|
||||
} else {
|
||||
profileArn = aws.ToString(instance.IamInstanceProfile.Arn)
|
||||
|
||||
// Extracting instance profile name from ARN
|
||||
profileName = strings.Split(profileArn, "/")[len(strings.Split(profileArn, "/"))-1]
|
||||
|
||||
// Describe the IAM instance profile
|
||||
profileOutput, err := sdk.CachedIamGetInstanceProfile(m.IAMClient, aws.ToString(m.Caller.Account), profileName)
|
||||
if err != nil {
|
||||
log.Printf("failed to get instance profile for %s, %v", profileArn, err)
|
||||
}
|
||||
|
||||
for _, role := range profileOutput.Roles {
|
||||
dataReceiver <- MappedInstance{
|
||||
ID: aws.ToString(instance.InstanceId),
|
||||
Name: aws.ToString(&name),
|
||||
Arn: fmt.Sprintf("arn:aws:ec2:%s:%s:instance/%s", region, aws.ToString(m.Caller.Account), aws.ToString(instance.InstanceId)),
|
||||
AvailabilityZone: aws.ToString(instance.Placement.AvailabilityZone),
|
||||
State: string(instance.State.Name),
|
||||
ExternalIP: externalIP,
|
||||
PrivateIP: aws.ToString(instance.PrivateIpAddress),
|
||||
Profile: profile,
|
||||
Role: aws.ToString(role.Arn),
|
||||
Region: region,
|
||||
Admin: adminRole,
|
||||
CanPrivEsc: "",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *InstancesModule) getRolesFromInstanceProfiles() {
|
||||
|
||||
// The "PaginationControl" value is nil when there's no more data to return.
|
||||
var PaginationMarker *string
|
||||
PaginationControl := true
|
||||
m.InstanceProfileToRolesMap = map[string][]iamTypes.Role{}
|
||||
|
||||
for PaginationControl {
|
||||
ListInstanceProfiles, err := m.IAMClient.ListInstanceProfiles(
|
||||
context.TODO(),
|
||||
&(iam.ListInstanceProfilesInput{
|
||||
Marker: PaginationMarker,
|
||||
}),
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, instanceProfile := range ListInstanceProfiles.InstanceProfiles {
|
||||
m.InstanceProfileToRolesMap[aws.ToString(instanceProfile.InstanceProfileId)] = instanceProfile.Roles
|
||||
}
|
||||
if aws.ToString(ListInstanceProfiles.Marker) != "" {
|
||||
PaginationMarker = ListInstanceProfiles.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+2575
-808
File diff suppressed because it is too large.
Load diff
+395
@@ -0,0 +1,395 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda"
|
||||
"github.com/aws/aws-sdk-go-v2/service/lambda/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type LambdasModule struct {
|
||||
// General configuration data
|
||||
LambdaClient *lambda.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Main module data
|
||||
Lambdas []Lambda
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Lambda struct {
|
||||
AWSService string
|
||||
Region string
|
||||
Type string
|
||||
Name string
|
||||
Arn string
|
||||
Role string
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
Public string
|
||||
}
|
||||
|
||||
func (m *LambdasModule) PrintLambdas(outputDirectory string, verbosity int) {
|
||||
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "lambda"
|
||||
localAdminMap := make(map[string]bool)
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating lambdas for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
//fmt.Printf("[%s][%s] Attempting to build a PrivEsc graph in memory using local pmapper data if it exists on the filesystem.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Lambda)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Perform role analysis
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.Lambdas {
|
||||
m.Lambdas[i].Admin, m.Lambdas[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, &m.Lambdas[i].Role)
|
||||
}
|
||||
} else {
|
||||
for i := range m.Lambdas {
|
||||
m.Lambdas[i].Admin, m.Lambdas[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, &m.Lambdas[i].Role, m.iamSimClient, localAdminMap)
|
||||
}
|
||||
}
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"Arn",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Arn",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Name",
|
||||
"Role",
|
||||
"IsAdminRole?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
sort.Slice(m.Lambdas, func(i, j int) bool {
|
||||
return m.Lambdas[i].AWSService < m.Lambdas[j].AWSService
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Lambdas {
|
||||
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Lambdas[i].Region,
|
||||
//m.Lambdas[i].Type,
|
||||
m.Lambdas[i].Name,
|
||||
m.Lambdas[i].Arn,
|
||||
m.Lambdas[i].Role,
|
||||
m.Lambdas[i].Admin,
|
||||
m.Lambdas[i].CanPrivEsc,
|
||||
//m.Lambdas[i].Public,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s lambdas found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No lambdas found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *LambdasModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getLambdasPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LambdasModule) Receiver(receiver chan Lambda, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Lambdas = append(m.Lambdas, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *LambdasModule) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
pullFile := filepath.Join(path, "lambda-get-function-commands.txt")
|
||||
|
||||
var out string
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("# The profile you will use to perform these commands is most likely not the profile you used to run CloudFox")
|
||||
out = out + fmt.Sprintln("# Set the $profile environment variable to the profile you are going to use to inspect the buckets.")
|
||||
out = out + fmt.Sprintln("# E.g., export profile=dev-prod.")
|
||||
out = out + fmt.Sprintln("#############################################")
|
||||
out = out + fmt.Sprintln("")
|
||||
|
||||
for _, function := range m.Lambdas {
|
||||
out = out + fmt.Sprintln("=============================================")
|
||||
out = out + fmt.Sprintf("# Lambda Name: %s\n\n", function.Name)
|
||||
out = out + "# Get function metadata including download location\n"
|
||||
out = out + fmt.Sprintf("aws --profile $profile --region %s lambda get-function --function-name %s\n", function.Region, function.Name)
|
||||
out = out + "# Download function code to to disk (requires jq and curl) \n"
|
||||
out = out + fmt.Sprintf("mkdir -p ./lambdas/%s\n", function.Name)
|
||||
out = out + fmt.Sprintf("url=`aws --profile $profile lambda get-function --region %s --function-name %s | jq .Code.Location | sed s/\\\"//g` && curl \"$url\" -o ./lambdas/%s.zip\n", function.Region, function.Name, function.Name)
|
||||
}
|
||||
err = os.WriteFile(pullFile, []byte(out), 0644)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
if verbosity > 2 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Beginning of loot file."))
|
||||
|
||||
fmt.Print(out)
|
||||
fmt.Printf("[%s][%s] %s \n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), pullFile)
|
||||
|
||||
}
|
||||
|
||||
func (m *LambdasModule) getLambdasPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Lambda) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
|
||||
functions, err := m.listFunctions(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, function := range functions {
|
||||
arn := aws.ToString(function.FunctionArn)
|
||||
name := aws.ToString(function.FunctionName)
|
||||
role := aws.ToString(function.Role)
|
||||
|
||||
dataReceiver <- Lambda{
|
||||
AWSService: "Lambda",
|
||||
Name: name,
|
||||
Arn: arn,
|
||||
Region: r,
|
||||
Type: "",
|
||||
Role: role,
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
Public: "",
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *LambdasModule) listFunctions(r string) ([]types.FunctionConfiguration, error) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var functions []types.FunctionConfiguration
|
||||
|
||||
for {
|
||||
ListFunctions, err := m.LambdaClient.ListFunctions(
|
||||
context.TODO(),
|
||||
&lambda.ListFunctionsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *lambda.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return functions, err
|
||||
}
|
||||
functions = append(functions, ListFunctions.Functions...)
|
||||
|
||||
if aws.ToString(ListFunctions.NextMarker) != "" {
|
||||
PaginationControl = ListFunctions.NextMarker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
return functions, nil
|
||||
}
|
||||
|
||||
func (m *LambdasModule) getResourcePolicy(r string, functionName string) (policy.Policy, error) {
|
||||
var projectPolicy policy.Policy
|
||||
var policyJSON string
|
||||
Policy, err := m.LambdaClient.GetPolicy(
|
||||
context.TODO(),
|
||||
&lambda.GetPolicyInput{
|
||||
FunctionName: &functionName,
|
||||
},
|
||||
func(options *lambda.Options) {
|
||||
options.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return projectPolicy, err
|
||||
}
|
||||
|
||||
policyJSON = aws.ToString(Policy.Policy)
|
||||
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return projectPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", functionName, err)
|
||||
}
|
||||
return projectPolicy, nil
|
||||
}
|
||||
File diff suppressed because it is too large.
Load diff
+325
@@ -0,0 +1,325 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/organizations/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/kyokomi/emoji"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type OrgModule struct {
|
||||
OrganizationsClient sdk.OrganizationsClientInterface
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
DescribeOrgOutput *types.Organization
|
||||
|
||||
// Main module data
|
||||
Accounts []Account
|
||||
Orgs map[string]Org
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Org struct {
|
||||
OrgId string
|
||||
MgmtAccount string
|
||||
ChildAccounts []Account
|
||||
}
|
||||
|
||||
type Account struct {
|
||||
ProfileName string
|
||||
isManagementAccount bool
|
||||
Name string
|
||||
Id string
|
||||
Email string
|
||||
Arn string
|
||||
Status string
|
||||
OrgId string
|
||||
}
|
||||
|
||||
func (m *OrgModule) PrintOrgAccounts(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
var err error
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "org"
|
||||
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] Checking if account %s is the management account in an organization.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
m.DescribeOrgOutput, err = sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Errorf("Failed to describe organization: %s", err)
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
m.modLog.Errorf("Failed to describe organization: %s", err)
|
||||
fmt.Printf("[%s][%s] Account %s is either not associated with an organization, or you do not have the organizations:DescribeOrganization permission.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
} else {
|
||||
|
||||
if m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account)) {
|
||||
m.addOrgAccounts()
|
||||
} else {
|
||||
m.addOrgAccount()
|
||||
//fmt.Printf("[%s][%s] Account %s is not the management account in an organization %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
}
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"ID",
|
||||
"isManagementAccount?",
|
||||
"Status",
|
||||
"Email",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"ID",
|
||||
"isManagementAccount?",
|
||||
"Status",
|
||||
"Email",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Name",
|
||||
"ID",
|
||||
"isManagementAccount?",
|
||||
"Status",
|
||||
"Email",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
|
||||
for i := range m.Accounts {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Accounts[i].Name,
|
||||
m.Accounts[i].Id,
|
||||
strconv.FormatBool(m.Accounts[i].isManagementAccount),
|
||||
m.Accounts[i].Status,
|
||||
m.Accounts[i].Email,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %d accounts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), len(m.output.Body))
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) ProcessMultipleAccounts(AWSProfiles []string, version string) {
|
||||
var seenOrgs []string
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Account)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, profile := range AWSProfiles {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.FindMgmtAccounts(profile, version, wg, semaphore, dataReceiver)
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
orgId := aws.ToString(m.DescribeOrgOutput.Id)
|
||||
// if this orgId is not in seenOrgs, add it to seenOrgs
|
||||
if !internal.Contains(orgId, seenOrgs) {
|
||||
seenOrgs = append(seenOrgs, orgId)
|
||||
// add this org to the Orgs map
|
||||
Org := Org{
|
||||
OrgId: orgId,
|
||||
}
|
||||
m.Orgs[orgId] = Org
|
||||
|
||||
} else {
|
||||
continue
|
||||
}
|
||||
|
||||
isMgmtAccount := m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(m.Caller.Account))
|
||||
if isMgmtAccount {
|
||||
mgmtAccount := aws.ToString(m.Caller.Account)
|
||||
fmt.Printf("[%s] Found an Organization Management Account: %s\n", cyan(emoji.Sprintf(":fox:cloudfox v%s :fox:", version)), mgmtAccount)
|
||||
//mgmtAccounts[mgmtAccount] = append(mgmtAccounts[mgmtAccount], profile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) Receiver(receiver chan Account, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Accounts = append(m.Accounts, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) FindMgmtAccounts(profile string, version string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Account) {
|
||||
var err error
|
||||
|
||||
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Errorf("Failed to describe organization: %s", err)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
dataReceiver <- Account{
|
||||
isManagementAccount: m.IsManagementAccount(DescribeOrganization, aws.ToString(m.Caller.Account)),
|
||||
Name: aws.ToString(m.Caller.Account),
|
||||
Id: aws.ToString(m.Caller.Account),
|
||||
Status: "ACTIVE",
|
||||
Email: aws.ToString(m.Caller.Account),
|
||||
OrgId: aws.ToString(DescribeOrganization.Id),
|
||||
Arn: aws.ToString(m.Caller.Account),
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *OrgModule) IsManagementAccount(Organization *types.Organization, account string) bool {
|
||||
// Check if the account is the management account
|
||||
// https://docs.aws.amazon.com/organizations/latest/APIReference/API_DescribeOrganization.html
|
||||
if aws.ToString(Organization.MasterAccountId) == account {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *OrgModule) addOrgAccounts() {
|
||||
accounts, err := sdk.CachedOrganizationsListAccounts(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
for _, account := range accounts {
|
||||
m.Accounts = append(m.Accounts, Account{
|
||||
isManagementAccount: m.IsManagementAccount(m.DescribeOrgOutput, aws.ToString(account.Id)),
|
||||
Name: aws.ToString(account.Name),
|
||||
Id: aws.ToString(account.Id),
|
||||
Email: aws.ToString(account.Email),
|
||||
Arn: aws.ToString(account.Arn),
|
||||
Status: string(account.Status),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OrgModule) addOrgAccount() {
|
||||
DescribeOrganization, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Errorf("Failed to describe organization: %s", err)
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
m.Accounts = append(m.Accounts, Account{
|
||||
isManagementAccount: true,
|
||||
Name: "Mgmt Account",
|
||||
Id: aws.ToString(DescribeOrganization.MasterAccountId),
|
||||
Email: aws.ToString(DescribeOrganization.MasterAccountEmail),
|
||||
Arn: aws.ToString(DescribeOrganization.MasterAccountArn),
|
||||
Status: "ACTIVE",
|
||||
})
|
||||
m.Accounts = append(m.Accounts, Account{
|
||||
isManagementAccount: false,
|
||||
Name: "This account",
|
||||
Id: aws.ToString(m.Caller.Account),
|
||||
Email: "Unknown",
|
||||
Arn: aws.ToString(m.Caller.Arn),
|
||||
Status: "ACTIVE",
|
||||
})
|
||||
}
|
||||
|
||||
func (m *OrgModule) IsCallerAccountPartOfAnOrg() bool {
|
||||
DescribeOrgOutput, err := sdk.CachedOrganizationsDescribeOrganization(m.OrganizationsClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Errorf("Failed to describe organization: %s", err)
|
||||
return false
|
||||
} else {
|
||||
m.DescribeOrgOutput = DescribeOrgOutput
|
||||
}
|
||||
return true
|
||||
}
|
||||
+108
-35
@@ -6,15 +6,16 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudtrail"
|
||||
cloudtrailTypes "github.com/aws/aws-sdk-go-v2/service/cloudtrail/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -22,17 +23,21 @@ type OutboundAssumedRolesModule struct {
|
||||
// General configuration data
|
||||
CloudTrailClient *cloudtrail.Client
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
OutboundAssumeRoleEntries []OutboundAssumeRoleEntry
|
||||
Days int
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
@@ -112,41 +117,42 @@ type CloudTrailEvent struct {
|
||||
} `json:"tlsDetails"`
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "outbound-assumed-roles"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
m.Days = days
|
||||
|
||||
fmt.Printf("[%s] Enumerating outbound assumed role entries in cloudtrail for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s] Going back through %d days of cloudtrail events. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), days)
|
||||
fmt.Printf("[%s][%s] Enumerating outbound assumed role entries in cloudtrail for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Going back through %d days of cloudtrail events. (This command can be pretty slow, FYI)\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), days)
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan OutboundAssumeRoleEntry)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, dataReceiver)
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
|
||||
@@ -154,12 +160,11 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Account",
|
||||
"Region",
|
||||
"Type",
|
||||
//"Source Account",
|
||||
@@ -169,12 +174,48 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
|
||||
"Log Entry Timestamp",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Type",
|
||||
//"Source Account",
|
||||
"Source Principal",
|
||||
//"Destination Account",
|
||||
"Destination Principal",
|
||||
"Log Entry Timestamp",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
|
||||
tableCols = []string{
|
||||
"Region",
|
||||
"Type",
|
||||
//"Source Account",
|
||||
"Source Principal",
|
||||
//"Destination Account",
|
||||
"Destination Principal",
|
||||
"Log Entry Timestamp",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.OutboundAssumeRoleEntries {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.OutboundAssumeRoleEntries[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.OutboundAssumeRoleEntries[i].Region,
|
||||
m.OutboundAssumeRoleEntries[i].Type,
|
||||
//m.OutboundAssumeRoleEntries[i].SourceAccount,
|
||||
@@ -188,16 +229,31 @@ func (m *OutboundAssumedRolesModule) PrintOutboundRoleTrusts(days int, outputFor
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
fmt.Printf("[%s] %s log entries found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s log entries found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
//m.writeLoot()
|
||||
} else {
|
||||
fmt.Printf("[%s] No matching log entries found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No matching log entries found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEntry, receiverDone chan bool) {
|
||||
@@ -213,17 +269,34 @@ func (m *OutboundAssumedRolesModule) Receiver(receiver chan OutboundAssumeRoleEn
|
||||
}
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
func (m *OutboundAssumedRolesModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getAssumeRoleLogEntriesPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("cloudtrail", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Total++
|
||||
m.getAssumeRoleLogEntriesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan OutboundAssumeRoleEntry) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() {
|
||||
<-semaphore
|
||||
}()
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
//var LookupAttributes []types.LookupAttributes
|
||||
@@ -240,7 +313,7 @@ func (m *OutboundAssumedRolesModule) getAssumeRoleLogEntriesPerRegion(r string,
|
||||
EndTime: endTime,
|
||||
StartTime: &startTime,
|
||||
LookupAttributes: []cloudtrailTypes.LookupAttribute{
|
||||
cloudtrailTypes.LookupAttribute{
|
||||
{
|
||||
AttributeKey: cloudtrailTypes.LookupAttributeKeyEventName,
|
||||
AttributeValue: aws.String("AssumeRole"),
|
||||
},
|
||||
|
||||
+387
-239
@@ -1,19 +1,17 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
@@ -21,12 +19,16 @@ import (
|
||||
|
||||
type IamPermissionsModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
|
||||
@@ -35,9 +37,9 @@ type IamPermissionsModule struct {
|
||||
Roles []GAADRole
|
||||
Groups []GAADGroup
|
||||
Rows []PermissionsRow
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -76,43 +78,47 @@ type PermissionsRow struct {
|
||||
Arn string
|
||||
PolicyType string
|
||||
PolicyName string
|
||||
PolicyArn string
|
||||
Effect string
|
||||
Action string
|
||||
Resource string
|
||||
Condition string
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDirectory string, verbosity int, principal string) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *IamPermissionsModule) PrintIamPermissions(outputDirectory string, verbosity int, principal string) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "permissions"
|
||||
m.output.FullFilename = m.output.CallingModule
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
filename := m.output.CallingModule
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
fmt.Printf("[%s] Enumerating IAM permissions for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
fmt.Printf("[%s][%s] Enumerating IAM permissions for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
if principal != "" {
|
||||
m.output.FullFilename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
filename = filepath.Join(fmt.Sprintf("%s-custom-%s", m.output.CallingModule, strconv.FormatInt((time.Now().Unix()), 10)))
|
||||
}
|
||||
|
||||
m.getGAAD(principal)
|
||||
m.parsePermissions()
|
||||
m.getGAAD()
|
||||
m.parsePermissions(principal)
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Principal Type",
|
||||
"Account",
|
||||
"Type",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"Policy Type",
|
||||
"Arn",
|
||||
"Policy",
|
||||
"Policy Name",
|
||||
"Policy Arn",
|
||||
"Effect",
|
||||
"Action",
|
||||
"Resource",
|
||||
"Condition",
|
||||
}
|
||||
|
||||
//Table rows
|
||||
@@ -120,219 +126,359 @@ func (m *IamPermissionsModule) PrintIamPermissions(outputFormat string, outputDi
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Rows[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Rows[i].Type,
|
||||
m.Rows[i].Name,
|
||||
//m.Rows[i].Arn,
|
||||
m.Rows[i].Arn,
|
||||
m.Rows[i].PolicyType,
|
||||
m.Rows[i].PolicyName,
|
||||
m.Rows[i].PolicyArn,
|
||||
m.Rows[i].Effect,
|
||||
m.Rows[i].Action,
|
||||
m.Rows[i].Resource,
|
||||
m.Rows[i].Condition,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector3(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] %s unique permissions identified.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Type",
|
||||
//"Name",
|
||||
"Arn",
|
||||
"Policy",
|
||||
"Policy Name",
|
||||
"Policy Arn",
|
||||
"Effect",
|
||||
"Action",
|
||||
"Resource",
|
||||
"Condition"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Type",
|
||||
"Name",
|
||||
//"Arn",
|
||||
"Policy",
|
||||
"Policy Name",
|
||||
"Effect",
|
||||
"Action",
|
||||
"Resource",
|
||||
"Condition",
|
||||
}
|
||||
}
|
||||
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
TableCols: tableCols,
|
||||
Body: m.output.Body,
|
||||
Name: filename,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s unique permissions identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No IAM permissions found. skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No IAM permissions found. skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) getGAAD() {
|
||||
GAAD, err := sdk.CachedIAMGetAccountAuthorizationDetails(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
// // if user supplied a principal name without the arn, try to create the arn
|
||||
// if !strings.Contains(principal, "arn:") {
|
||||
// principal = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
// }
|
||||
|
||||
for _, policy := range GAAD.Policies {
|
||||
//var IAMtype = "Role"
|
||||
arn := aws.ToString(policy.Arn)
|
||||
name := aws.ToString(policy.PolicyName)
|
||||
|
||||
m.Policies = append(m.Policies, GAADPolicy{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
PolicyVersionList: policy.PolicyVersionList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, role := range GAAD.RoleDetailList {
|
||||
arn := aws.ToString(role.Arn)
|
||||
name := aws.ToString(role.RoleName)
|
||||
m.Roles = append(m.Roles, GAADRole{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: role.AttachedManagedPolicies,
|
||||
InlinePolicies: role.RolePolicyList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, user := range GAAD.UserDetailList {
|
||||
//var IAMtype = "User"
|
||||
arn := aws.ToString(user.Arn)
|
||||
name := aws.ToString(user.UserName)
|
||||
groupList := user.GroupList
|
||||
m.Users = append(m.Users, GAADUser{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: user.AttachedManagedPolicies,
|
||||
InlinePolicies: user.UserPolicyList,
|
||||
GroupList: groupList,
|
||||
})
|
||||
}
|
||||
|
||||
for _, group := range GAAD.GroupDetailList {
|
||||
arn := aws.ToString(group.Arn)
|
||||
name := aws.ToString(group.GroupName)
|
||||
|
||||
m.Groups = append(m.Groups, GAADGroup{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: group.AttachedManagedPolicies,
|
||||
InlinePolicies: group.GroupPolicyList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) getGAAD(principal string) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
//var totalRoles int
|
||||
|
||||
// var attachedPolicies []types.AttachedPolicy
|
||||
// var inlinePolicies []types.PolicyDetail
|
||||
|
||||
for {
|
||||
GAAD, err := m.IAMClient.GetAccountAuthorizationDetails(
|
||||
context.TODO(),
|
||||
&iam.GetAccountAuthorizationDetailsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, policy := range GAAD.Policies {
|
||||
//var IAMtype = "Role"
|
||||
arn := aws.ToString(policy.Arn)
|
||||
name := aws.ToString(policy.PolicyName)
|
||||
|
||||
m.Policies = append(m.Policies, GAADPolicy{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
PolicyVersionList: policy.PolicyVersionList,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
for _, role := range GAAD.RoleDetailList {
|
||||
arn := aws.ToString(role.Arn)
|
||||
name := aws.ToString(role.RoleName)
|
||||
if principal == "" {
|
||||
m.Roles = append(m.Roles, GAADRole{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: role.AttachedManagedPolicies,
|
||||
InlinePolicies: role.RolePolicyList,
|
||||
})
|
||||
} else {
|
||||
if arn == principal {
|
||||
m.Roles = append(m.Roles, GAADRole{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: role.AttachedManagedPolicies,
|
||||
InlinePolicies: role.RolePolicyList,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// i think the error here is pagination!!
|
||||
|
||||
for _, user := range GAAD.UserDetailList {
|
||||
//var IAMtype = "User"
|
||||
arn := aws.ToString(user.Arn)
|
||||
name := aws.ToString(user.UserName)
|
||||
groupList := user.GroupList
|
||||
if principal == "" {
|
||||
m.Users = append(m.Users, GAADUser{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: user.AttachedManagedPolicies,
|
||||
InlinePolicies: user.UserPolicyList,
|
||||
GroupList: groupList,
|
||||
})
|
||||
} else {
|
||||
if arn == principal {
|
||||
m.Users = append(m.Users, GAADUser{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: user.AttachedManagedPolicies,
|
||||
InlinePolicies: user.UserPolicyList,
|
||||
GroupList: groupList,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range GAAD.GroupDetailList {
|
||||
arn := aws.ToString(group.Arn)
|
||||
name := aws.ToString(group.GroupName)
|
||||
if principal == "" {
|
||||
m.Groups = append(m.Groups, GAADGroup{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: group.AttachedManagedPolicies,
|
||||
InlinePolicies: group.GroupPolicyList,
|
||||
})
|
||||
} else {
|
||||
if arn == principal {
|
||||
m.Groups = append(m.Groups, GAADGroup{
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
AttachedPolicies: group.AttachedManagedPolicies,
|
||||
InlinePolicies: group.GroupPolicyList,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if GAAD.Marker != nil {
|
||||
PaginationControl = GAAD.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
// create a function that will take a principal name and try to see if it is a role, user, or group and return the arn of the principal
|
||||
func (m *IamPermissionsModule) getPrincipalArn(principal string) string {
|
||||
var arn string
|
||||
for _, role := range m.Roles {
|
||||
if role.Name == principal {
|
||||
arn = role.Arn
|
||||
}
|
||||
}
|
||||
|
||||
for _, user := range m.Users {
|
||||
if user.Name == principal {
|
||||
arn = user.Arn
|
||||
}
|
||||
}
|
||||
|
||||
for _, group := range m.Groups {
|
||||
if group.Name == principal {
|
||||
arn = group.Arn
|
||||
}
|
||||
}
|
||||
|
||||
return arn
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) parsePermissions() {
|
||||
|
||||
func (m *IamPermissionsModule) parsePermissions(principal string) {
|
||||
var inputArn string
|
||||
for i := range m.Roles {
|
||||
if principal == "" {
|
||||
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
|
||||
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
|
||||
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
} else {
|
||||
// if user supplied a principal name without the arn, try to create the arn
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:role/%s", aws.ToString(m.Caller.Account), principal)
|
||||
} else {
|
||||
inputArn = principal
|
||||
}
|
||||
|
||||
if strings.ToLower(m.Roles[i].Arn) == strings.ToLower(inputArn) {
|
||||
for _, attachedPolicy := range m.Roles[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Roles[i].Arn, attachedPolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Roles[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Roles[i].Arn, inlinePolicy, "Role", m.Roles[i].Name)
|
||||
}
|
||||
}
|
||||
|
||||
for i := range m.Users {
|
||||
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
if principal == "" {
|
||||
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Users[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
} else {
|
||||
// if user supplied a principal name without the arn, try to create the arn
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
} else {
|
||||
inputArn = principal
|
||||
}
|
||||
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
|
||||
for _, attachedPolicy := range m.Users[i].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Users[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, inlinePolicy := range m.Users[i].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
|
||||
for j, group := range m.Users[i].GroupList {
|
||||
for _, gaadGroup := range m.Groups {
|
||||
if gaadGroup.Name == group {
|
||||
for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
// for each group in the user's group list, get the attached and inline policy names, and then get the permissions from those policies
|
||||
for g := range m.Users[i].GroupList {
|
||||
if principal == "" {
|
||||
for _, group := range m.Groups {
|
||||
if group.Name == m.Users[i].GroupList[g] {
|
||||
for _, attachedPolicy := range group.AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
for _, inlinePolicy := range group.InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
}
|
||||
for _, inlinePolicy := range m.Groups[j].InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
} else {
|
||||
// if user supplied a principal name without the arn, try to create the arn
|
||||
if !strings.Contains(principal, "arn:") {
|
||||
inputArn = fmt.Sprintf("arn:aws:iam::%s:user/%s", aws.ToString(m.Caller.Account), principal)
|
||||
} else {
|
||||
inputArn = principal
|
||||
}
|
||||
if strings.ToLower(m.Users[i].Arn) == strings.ToLower(inputArn) {
|
||||
for _, group := range m.Groups {
|
||||
if group.Name == m.Users[i].GroupList[g] {
|
||||
for _, attachedPolicy := range group.AttachedPolicies {
|
||||
m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
for _, inlinePolicy := range group.InlinePolicies {
|
||||
m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// for group := range m.Users[i].GroupList {
|
||||
// for _, gaadGroup := range m.Groups {
|
||||
// if gaadGroup.Name == group {
|
||||
// for _, attachedPolicy := range m.Groups[j].AttachedPolicies {
|
||||
// m.getPermissionsFromAttachedPolicy(m.Users[i].Arn, attachedPolicy, "User", m.Users[i].Name)
|
||||
// }
|
||||
// for _, inlinePolicy := range m.Groups[j].InlinePolicies {
|
||||
// m.getPermissionsFromInlinePolicy(m.Users[i].Arn, inlinePolicy, "User", m.Users[i].Name)
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, attachedPolicy types.AttachedPolicy, IAMtype string, name string) {
|
||||
//var policies []types.ManagedPolicyDetail
|
||||
var s StatementEntry
|
||||
var s policy.PolicyStatement
|
||||
var AWSService = "IAM"
|
||||
var hasConditions string
|
||||
|
||||
for _, p := range m.Policies {
|
||||
if p.Name == aws.ToString(attachedPolicy.PolicyName) {
|
||||
for _, d := range p.PolicyVersionList {
|
||||
if d.IsDefaultVersion {
|
||||
parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
|
||||
//parsedPolicyDocument, _ := parsePolicyDocument(d.Document)
|
||||
document, _ := url.QueryUnescape(aws.ToString(d.Document))
|
||||
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
if s.Action != nil {
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
PolicyArn: p.Arn,
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if s.NotAction != nil {
|
||||
for _, action := range s.NotAction {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Managed",
|
||||
PolicyName: p.Name,
|
||||
PolicyArn: p.Arn,
|
||||
Effect: effect,
|
||||
Action: "[NotAction] " + action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -342,66 +488,68 @@ func (m *IamPermissionsModule) getPermissionsFromAttachedPolicy(arn string, atta
|
||||
|
||||
func (m *IamPermissionsModule) getPermissionsFromInlinePolicy(arn string, inlinePolicy types.PolicyDetail, IAMtype string, name string) {
|
||||
//var policies []types.ManagedPolicyDetail
|
||||
var s StatementEntry
|
||||
var s policy.PolicyStatement
|
||||
var AWSService = "IAM"
|
||||
var hasConditions string
|
||||
|
||||
//parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
|
||||
document, _ := url.QueryUnescape(aws.ToString(inlinePolicy.PolicyDocument))
|
||||
parsedPolicyDocument, _ := policy.ParseJSONPolicy([]byte(document))
|
||||
|
||||
parsedPolicyDocument, _ := parsePolicyDocument(inlinePolicy.PolicyDocument)
|
||||
for _, s = range parsedPolicyDocument.Statement {
|
||||
//version := parsedPolicyDocument.Version
|
||||
effect := s.Effect
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
})
|
||||
if s.Action != nil {
|
||||
for _, action := range s.Action {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
PolicyArn: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
if s.NotAction != nil {
|
||||
for _, action := range s.NotAction {
|
||||
for _, resource := range s.Resource {
|
||||
if s.Condition != nil {
|
||||
hasConditions = "Yes"
|
||||
} else {
|
||||
hasConditions = "No"
|
||||
}
|
||||
m.Rows = append(
|
||||
m.Rows,
|
||||
PermissionsRow{
|
||||
AWSService: AWSService,
|
||||
Arn: arn,
|
||||
Name: name,
|
||||
Type: IAMtype,
|
||||
PolicyType: "Inline",
|
||||
PolicyName: aws.ToString(inlinePolicy.PolicyName),
|
||||
PolicyArn: aws.ToString(inlinePolicy.PolicyName),
|
||||
Effect: effect,
|
||||
Action: "[NotAction] " + action,
|
||||
Resource: resource,
|
||||
Condition: hasConditions,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
type policyDocument struct {
|
||||
Version string `json:"Version"`
|
||||
Statement []StatementEntry `json:"Statement"`
|
||||
}
|
||||
|
||||
type StatementEntry struct {
|
||||
Effect string `json:"Effect"`
|
||||
Action ListOfItems `json:"Action"`
|
||||
Resource ListOfItems `json:"Resource"`
|
||||
Condition ListOfItems `json:"Condition"`
|
||||
}
|
||||
|
||||
func parsePolicyDocument(doc *string) (policyDocument, error) {
|
||||
document, _ := url.QueryUnescape(aws.ToString(doc))
|
||||
var parsedDocumentToJSON policyDocument
|
||||
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
|
||||
return parsedDocumentToJSON, nil
|
||||
}
|
||||
|
||||
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
|
||||
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
|
||||
type ListOfItems []string
|
||||
|
||||
func (r *ListOfItems) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
*r = append(*r, s)
|
||||
return nil
|
||||
}
|
||||
var ss []string
|
||||
if err := json.Unmarshal(b, &ss); err == nil {
|
||||
*r = ss
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
|
||||
}
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/dominikbraun/graph"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type PmapperModule struct {
|
||||
// General configuration data
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
pmapperGraph graph.Graph[string, string]
|
||||
Nodes []Node
|
||||
Edges []Edge
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Edge struct {
|
||||
Source string `json:"source"`
|
||||
Destination string `json:"destination"`
|
||||
Reason string `json:"reason"`
|
||||
ShortReason string `json:"short_reason"`
|
||||
}
|
||||
|
||||
type Node struct {
|
||||
Arn string `json:"arn"`
|
||||
IDValue string `json:"id_value"`
|
||||
AttachedPolicies []AttachedPolicies `json:"attached_policies"`
|
||||
GroupMemberships []interface{} `json:"group_memberships"`
|
||||
TrustPolicy interface{} `json:"trust_policy"`
|
||||
InstanceProfile interface{} `json:"instance_profile"`
|
||||
ActivePassword bool `json:"active_password"`
|
||||
AccessKeys int `json:"access_keys"`
|
||||
IsAdmin bool `json:"is_admin"`
|
||||
PermissionsBoundary interface{} `json:"permissions_boundary"`
|
||||
HasMfa bool `json:"has_mfa"`
|
||||
Tags Tags `json:"tags"`
|
||||
PathToAdmin bool
|
||||
}
|
||||
|
||||
type AttachedPolicies struct {
|
||||
Arn string `json:"arn"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
type Tags struct {
|
||||
}
|
||||
|
||||
func (m *PmapperModule) initPmapperGraph() error {
|
||||
// Parse mapper nodes and edges and populate the m.Nodes and m.Edges slices in the method struct
|
||||
err := m.readPmapperData(m.Caller.Account)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
m.pmapperGraph = m.createAndPopulateGraph()
|
||||
|
||||
for i := range m.Nodes {
|
||||
if m.doesNodeHavePathToAdmin(m.Nodes[i]) {
|
||||
m.Nodes[i].PathToAdmin = true
|
||||
//fmt.Println(m.Nodes[i].Arn, m.Nodes[i].IsAdmin, m.Nodes[i].PathToAdmin)
|
||||
} else {
|
||||
m.Nodes[i].PathToAdmin = false
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *PmapperModule) createAndPopulateGraph() graph.Graph[string, string] {
|
||||
|
||||
pmapperGraph := graph.New(graph.StringHash, graph.Directed())
|
||||
|
||||
// having issues with caching the graph. will have to swing back and try again later
|
||||
|
||||
// gob.Register(pmapperGraph)
|
||||
// cacheKey := fmt.Sprintf("%s-pmapperGraph", aws.ToString(m.Caller.Account))
|
||||
// cached, found := internal.Cache.Get(cacheKey)
|
||||
// if found {
|
||||
// return cached.(graph.Graph[string, string])
|
||||
// }
|
||||
|
||||
for _, node := range m.Nodes {
|
||||
_ = pmapperGraph.AddVertex(node.Arn)
|
||||
|
||||
}
|
||||
|
||||
for _, edge := range m.Edges {
|
||||
_ = pmapperGraph.AddEdge(edge.Source, edge.Destination)
|
||||
}
|
||||
|
||||
//internal.Cache.Set(cacheKey, pmapperGraph, cache.DefaultExpiration)
|
||||
return pmapperGraph
|
||||
|
||||
}
|
||||
|
||||
func (m *PmapperModule) DoesPrincipalHavePathToAdmin(principal string) bool {
|
||||
for i := range m.Nodes {
|
||||
if m.Nodes[i].Arn == principal {
|
||||
if m.Nodes[i].PathToAdmin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *PmapperModule) DoesPrincipalHaveAdmin(principal string) bool {
|
||||
for i := range m.Nodes {
|
||||
if m.Nodes[i].Arn == principal {
|
||||
if m.Nodes[i].IsAdmin {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *PmapperModule) PrintPmapperData(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "pmapper"
|
||||
m.output.FullFilename = m.output.CallingModule
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
pmapperError := m.initPmapperGraph()
|
||||
if pmapperError != nil {
|
||||
fmt.Printf("[%s][%s] No pmapper data found for this account. \n\t\t\t1. Generate pmapper data by running `pmapper --profile %s graph create`\n\t\t\t2. After that completes, cloudfox will attempt to enrich this command and others with pmapper privesc data\n\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.AWSProfile)
|
||||
fmt.Printf("[%s][%s] For more info and troubleshooting steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
m.modLog.Error(pmapperError)
|
||||
return
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] Parsing pmapper data for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
}
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"Principal Arn",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Principal Arn",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Principal Arn",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
}
|
||||
|
||||
//Table rows
|
||||
var isAdmin, pathToAdmin string
|
||||
for i := range m.Nodes {
|
||||
if m.Nodes[i].PathToAdmin || m.Nodes[i].IsAdmin {
|
||||
if m.Nodes[i].IsAdmin {
|
||||
isAdmin = "YES"
|
||||
} else {
|
||||
isAdmin = "No"
|
||||
}
|
||||
if m.Nodes[i].PathToAdmin {
|
||||
pathToAdmin = "YES"
|
||||
} else {
|
||||
pathToAdmin = "No"
|
||||
}
|
||||
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Nodes[i].Arn,
|
||||
isAdmin,
|
||||
pathToAdmin,
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
|
||||
fmt.Printf("[%s][%s] %s principals who are admin or have a path to admin identified.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No principals who are admin or have a path to admin identified. skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *PmapperModule) doesNodeHavePathToAdmin(startNode Node) bool {
|
||||
if startNode.IsAdmin {
|
||||
return true
|
||||
} else {
|
||||
for _, destNode := range m.Nodes {
|
||||
if destNode.IsAdmin {
|
||||
path, _ := graph.ShortestPath(m.pmapperGraph, startNode.Arn, destNode.Arn)
|
||||
for _, p := range path {
|
||||
if p != "" {
|
||||
if startNode.Arn != destNode.Arn {
|
||||
// if we got here there is a path
|
||||
//fmt.Printf("%s has a path %s who is an admin.\n", startNode.Arn, destNode.Arn)
|
||||
//fmt.Println(path)
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *PmapperModule) readPmapperData(accountID *string) error {
|
||||
|
||||
e, n := generatePmapperDataBasePaths(accountID)
|
||||
|
||||
nodesFile, err := os.Open(n)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer nodesFile.Close()
|
||||
nodesByteValue, _ := ioutil.ReadAll(nodesFile)
|
||||
json.Unmarshal([]byte(nodesByteValue), &m.Nodes)
|
||||
|
||||
edgesFile, err := os.Open(e)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer edgesFile.Close()
|
||||
edgesByteValue, _ := ioutil.ReadAll(edgesFile)
|
||||
json.Unmarshal([]byte(edgesByteValue), &m.Edges)
|
||||
|
||||
return nil
|
||||
|
||||
}
|
||||
+125
-112
@@ -1,36 +1,38 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"sync"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type IamPrincipalsModule struct {
|
||||
// General configuration data
|
||||
IAMClient *iam.Client
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Users []User
|
||||
Roles []Role
|
||||
Groups []Group
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -62,24 +64,24 @@ type Role struct {
|
||||
InlinePolicies []string
|
||||
}
|
||||
|
||||
func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *IamPrincipalsModule) PrintIamPrincipals(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "principals"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Enumerating IAM Users and Roles for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
// wg := new(sync.WaitGroup)
|
||||
|
||||
// done := make(chan bool)
|
||||
// go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, done)
|
||||
// go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, done)
|
||||
// wg.Add(1)
|
||||
// m.CommandCounter.Pending++
|
||||
//m.executeChecks(wg)
|
||||
@@ -87,19 +89,50 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
|
||||
// done <- true
|
||||
// <-done
|
||||
|
||||
m.getIAMUsers()
|
||||
m.getIAMRoles()
|
||||
m.addIAMUsersToTable()
|
||||
m.addIAMRolesToTable()
|
||||
|
||||
//fmt.Printf("\nAnalyzed Resources by Region\n\n")
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Account",
|
||||
"Type",
|
||||
"Name",
|
||||
"Arn",
|
||||
"AttachedPolicies",
|
||||
"InlinePolicies",
|
||||
}
|
||||
|
||||
// "AttachedPolicies",
|
||||
// "InlinePolicies",
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Type",
|
||||
"Name",
|
||||
"Arn",
|
||||
//"AttachedPolicies",
|
||||
//"InlinePolicies",
|
||||
}
|
||||
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Type",
|
||||
"Name",
|
||||
"Arn",
|
||||
// "AttachedPolicies",
|
||||
// "InlinePolicies",
|
||||
}
|
||||
}
|
||||
|
||||
//Table rows
|
||||
@@ -107,13 +140,12 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Users[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Users[i].Type,
|
||||
m.Users[i].Name,
|
||||
m.Users[i].Arn,
|
||||
|
||||
// m.Users[i].AttachedPolicies,
|
||||
// m.Users[i].InlinePolicies,
|
||||
strings.Join(m.Users[i].AttachedPolicies, " , "),
|
||||
strings.Join(m.Users[i].InlinePolicies, " , "),
|
||||
},
|
||||
)
|
||||
|
||||
@@ -123,29 +155,45 @@ func (m *IamPrincipalsModule) PrintIamPrincipals(outputFormat string, outputDire
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Roles[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Roles[i].Type,
|
||||
m.Roles[i].Name,
|
||||
m.Roles[i].Arn,
|
||||
|
||||
// m.Roles[i].AttachedPolicies,
|
||||
// m.Roles[i].InlinePolicies,
|
||||
strings.Join(m.Roles[i].AttachedPolicies, " , "),
|
||||
strings.Join(m.Roles[i].InlinePolicies, " , "),
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
fmt.Printf("[%s] %s IAM principals found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s IAM principals found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No IAM principals found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No IAM principals found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
/* UNUSED CODE BLOCK - PLEASE REVIEW AND DELETE IF APPLICABLE
|
||||
func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
@@ -156,101 +204,66 @@ func (m *IamPrincipalsModule) executeChecks(wg *sync.WaitGroup) {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
*/
|
||||
|
||||
func (m *IamPrincipalsModule) getIAMUsers() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
func (m *IamPrincipalsModule) addIAMUsersToTable() {
|
||||
var AWSService = "IAM"
|
||||
|
||||
var IAMtype = "User"
|
||||
var attachedPolicies []string
|
||||
var inlinePolicies []string
|
||||
|
||||
for {
|
||||
ListUsers, err := m.IAMClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
ListUsers, err := sdk.CachedIamListUsers(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
for _, user := range ListUsers.Users {
|
||||
arn := user.Arn
|
||||
name := user.UserName
|
||||
for _, user := range ListUsers {
|
||||
arn := user.Arn
|
||||
name := user.UserName
|
||||
|
||||
m.Users = append(
|
||||
m.Users,
|
||||
User{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
m.Users = append(
|
||||
m.Users,
|
||||
User{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *IamPrincipalsModule) getIAMRoles() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
func (m *IamPrincipalsModule) addIAMRolesToTable() {
|
||||
|
||||
//var totalRoles int
|
||||
var AWSService = "IAM"
|
||||
var IAMtype = "Role"
|
||||
var attachedPolicies []string
|
||||
var inlinePolicies []string
|
||||
|
||||
for {
|
||||
ListRoles, err := m.IAMClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, role := range ListRoles.Roles {
|
||||
arn := role.Arn
|
||||
name := role.RoleName
|
||||
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
m.Roles = append(
|
||||
m.Roles,
|
||||
Role{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
for _, role := range ListRoles {
|
||||
arn := role.Arn
|
||||
name := role.RoleName
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
//fmt.Printf("IAM Roles: %d\n\n", totalRoles)
|
||||
break
|
||||
}
|
||||
m.Roles = append(
|
||||
m.Roles,
|
||||
Role{
|
||||
AWSService: AWSService,
|
||||
Arn: aws.ToString(arn),
|
||||
Name: aws.ToString(name),
|
||||
Type: IAMtype,
|
||||
AttachedPolicies: attachedPolicies,
|
||||
InlinePolicies: inlinePolicies,
|
||||
})
|
||||
}
|
||||
|
||||
}
|
||||
+181
-69
@@ -5,13 +5,15 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ram"
|
||||
ramTypes "github.com/aws/aws-sdk-go-v2/service/ram/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
@@ -19,16 +21,20 @@ type RAMModule struct {
|
||||
// General configuration data
|
||||
RAMClient *ram.Client
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
AWSProfile string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
WrapTable bool
|
||||
|
||||
// Main module data
|
||||
Resources []Resource
|
||||
CommandCounter console.CommandCounter
|
||||
CommandCounter internal.CommandCounter
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
@@ -37,34 +43,37 @@ type Resource struct {
|
||||
Region string
|
||||
Name string
|
||||
Owner string
|
||||
Type string
|
||||
ShareType string
|
||||
}
|
||||
|
||||
func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbosity int) {
|
||||
// These stuct values are used by the output module
|
||||
func (m *RAMModule) PrintRAM(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "ram"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating shared resources for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Enumerating shared resources for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
wg := new(sync.WaitGroup)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go console.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "regions")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Resource)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
@@ -75,22 +84,54 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
//time.Sleep(time.Second * 2)
|
||||
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Account",
|
||||
"Region",
|
||||
"Name",
|
||||
"URI",
|
||||
"PushedAt",
|
||||
"ImageTags",
|
||||
"ImageSize",
|
||||
"Share Name",
|
||||
"Type",
|
||||
"Owner",
|
||||
"Share Type",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Region",
|
||||
"Share Name",
|
||||
"Type",
|
||||
"Owner",
|
||||
"Share Type",
|
||||
}
|
||||
// Otherwise, use the default columns.
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Service",
|
||||
"Region",
|
||||
"Share Name",
|
||||
"Type",
|
||||
"Owner",
|
||||
"Share Type",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
@@ -98,33 +139,60 @@ func (m *RAMModule) PrintRAM(outputFormat string, outputDirectory string, verbos
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Resources[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Resources[i].Region,
|
||||
m.Resources[i].Name,
|
||||
m.Resources[i].Type,
|
||||
m.Resources[i].Owner,
|
||||
m.Resources[i].ShareType,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
fmt.Printf("[%s] %s resources found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s] No resources found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s resources found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No resources found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *RAMModule) executeChecks(r string, wg *sync.WaitGroup, dataReceiver chan Resource) {
|
||||
defer wg.Done()
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getRAMResourcesPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("ram", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
m.CommandCounter.Pending--
|
||||
m.CommandCounter.Executing++
|
||||
m.getRAMResourcesPerRegion(r, dataReceiver)
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
}
|
||||
}
|
||||
|
||||
func (m *RAMModule) Receiver(receiver chan Resource, receiverDone chan bool) {
|
||||
@@ -143,44 +211,88 @@ func (m *RAMModule) Receiver(receiver chan Resource, receiverDone chan bool) {
|
||||
func (m *RAMModule) getRAMResourcesPerRegion(r string, dataReceiver chan Resource) {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var PaginationControl2 *string
|
||||
var shareName string
|
||||
var resourceType string
|
||||
shareTypes := []ramTypes.ResourceOwner{"SELF", "OTHER-ACCOUNTS"}
|
||||
|
||||
for {
|
||||
GetResourceShares, err := m.RAMClient.GetResourceShares(
|
||||
context.TODO(),
|
||||
&ram.GetResourceSharesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ram.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, resource := range GetResourceShares.ResourceShares {
|
||||
name := aws.ToString(resource.Name)
|
||||
ownerID := aws.ToString(resource.OwningAccountId)
|
||||
|
||||
dataReceiver <- Resource{
|
||||
AWSService: "RAM",
|
||||
Name: name,
|
||||
Region: r,
|
||||
Owner: ownerID,
|
||||
for _, shareType := range shareTypes {
|
||||
for {
|
||||
GetResourceShares, err := m.RAMClient.GetResourceShares(
|
||||
context.TODO(),
|
||||
&ram.GetResourceSharesInput{
|
||||
NextToken: PaginationControl,
|
||||
ResourceOwner: shareType,
|
||||
},
|
||||
func(o *ram.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
// }
|
||||
break
|
||||
}
|
||||
for _, resourceShare := range GetResourceShares.ResourceShares {
|
||||
var resourceShareArns []string
|
||||
shareName = aws.ToString(resourceShare.Name)
|
||||
resourceShareArns = append(resourceShareArns, aws.ToString(resourceShare.ResourceShareArn))
|
||||
ownerID := aws.ToString(resourceShare.OwningAccountId)
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if GetResourceShares.NextToken != nil {
|
||||
PaginationControl = GetResourceShares.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
for {
|
||||
ListResources, err := m.RAMClient.ListResources(
|
||||
context.TODO(),
|
||||
&ram.ListResourcesInput{
|
||||
NextToken: PaginationControl2,
|
||||
ResourceOwner: shareType,
|
||||
ResourceShareArns: resourceShareArns,
|
||||
},
|
||||
func(o *ram.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, resource := range ListResources.Resources {
|
||||
resourceType = aws.ToString(resource.Type)
|
||||
var shareDirection string
|
||||
if string(shareType) == "OTHER-ACCOUNTS" {
|
||||
shareDirection = "Inbound share (Another account shared this with me)"
|
||||
} else {
|
||||
shareDirection = "Outbound share (I've shared this resource with others)"
|
||||
}
|
||||
|
||||
dataReceiver <- Resource{
|
||||
AWSService: "RAM",
|
||||
Name: shareName,
|
||||
Type: resourceType,
|
||||
Region: r,
|
||||
Owner: ownerID,
|
||||
ShareType: shareDirection,
|
||||
}
|
||||
}
|
||||
if ListResources.NextToken != nil {
|
||||
PaginationControl2 = ListResources.NextToken
|
||||
} else {
|
||||
PaginationControl2 = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if GetResourceShares.NextToken != nil {
|
||||
PaginationControl = GetResourceShares.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,925 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/awsservicemap"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type ResourceTrustsModule struct {
|
||||
// General configuration data
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
Goroutines int
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
AWSMFAToken string
|
||||
AWSConfig aws.Config
|
||||
AWSProfileProvided string
|
||||
AWSProfileStub string
|
||||
CloudFoxVersion string
|
||||
|
||||
Resources2 []Resource2
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
output internal.OutputData2
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
type Resource2 struct {
|
||||
AccountID string
|
||||
Name string
|
||||
ARN string
|
||||
Region string
|
||||
Policy policy.Policy
|
||||
PolicyJSON string
|
||||
ResourcePolicySummary string
|
||||
Public string
|
||||
Interesting string
|
||||
TrustedPrincipals string
|
||||
TrustsCrossAccount string
|
||||
TrustsAllAccounts string
|
||||
HasConditions string
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) PrintResources(outputDirectory string, verbosity int) {
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "resource-trusts"
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
|
||||
if m.AWSProfileProvided == "" {
|
||||
m.AWSProfileStub = internal.BuildAWSPath(m.Caller)
|
||||
} else {
|
||||
m.AWSProfileStub = m.AWSProfileProvided
|
||||
}
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileProvided, aws.ToString(m.Caller.Account)))
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating Resources with resource policies for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Supported Services: CodeBuild, ECR, EFS, Glue, Lambda, SecretsManager, S3, SNS, SQS\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
wg := new(sync.WaitGroup)
|
||||
semaphore := make(chan struct{}, m.Goroutines)
|
||||
|
||||
// Create a channel to signal the spinner aka task status goroutine to finish
|
||||
spinnerDone := make(chan bool)
|
||||
//fire up the the task status spinner/updated
|
||||
go internal.SpinUntil(m.output.CallingModule, &m.CommandCounter, spinnerDone, "tasks")
|
||||
|
||||
//create a channel to receive the objects
|
||||
dataReceiver := make(chan Resource2)
|
||||
|
||||
// Create a channel to signal to stop
|
||||
receiverDone := make(chan bool)
|
||||
go m.Receiver(dataReceiver, receiverDone)
|
||||
|
||||
for _, region := range m.AWSRegions {
|
||||
wg.Add(1)
|
||||
m.CommandCounter.Pending++
|
||||
go m.executeChecks(region, wg, semaphore, dataReceiver)
|
||||
|
||||
}
|
||||
wg.Add(1)
|
||||
go m.getS3Buckets(wg, semaphore, dataReceiver)
|
||||
|
||||
wg.Wait()
|
||||
// Send a message to the spinner goroutine to close the channel and stop
|
||||
spinnerDone <- true
|
||||
<-spinnerDone
|
||||
// Send a message to the data receiver goroutine to close the channel and stop
|
||||
receiverDone <- true
|
||||
<-receiverDone
|
||||
|
||||
// add - if struct is not empty do this. otherwise, dont write anything.
|
||||
m.output.Headers = []string{
|
||||
"Account",
|
||||
"ARN",
|
||||
"Public",
|
||||
"Interesting",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"ARN",
|
||||
"Public",
|
||||
"Interesting",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
} else {
|
||||
tableCols = []string{
|
||||
//"Account ID",
|
||||
"ARN",
|
||||
"Public",
|
||||
"Interesting",
|
||||
"Resource Policy Summary",
|
||||
}
|
||||
}
|
||||
|
||||
// sort the table roles by Interesting
|
||||
sort.Slice(m.Resources2, func(i, j int) bool {
|
||||
return m.Resources2[j].Interesting > m.Resources2[i].Interesting
|
||||
})
|
||||
|
||||
// Table rows
|
||||
for i := range m.Resources2 {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Resources2[i].ARN,
|
||||
m.Resources2[i].Public,
|
||||
m.Resources2[i].Interesting,
|
||||
m.Resources2[i].ResourcePolicySummary,
|
||||
},
|
||||
)
|
||||
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfileStub
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfileStub, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
//m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s resource policies found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), strconv.Itoa(len(m.output.Body)))
|
||||
//fmt.Printf("[%s][%s] Resource policies stored to: %s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.getLootDir())
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No resource policies found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub))
|
||||
}
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfileStub), m.output.CallingModule)
|
||||
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) executeChecks(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer wg.Done()
|
||||
|
||||
servicemap := &awsservicemap.AwsServiceMap{
|
||||
JsonFileSource: "DOWNLOAD_FROM_AWS",
|
||||
}
|
||||
res, err := servicemap.IsServiceInRegion("sns", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSNSTopicsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("sqs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSQSQueuesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("ecr", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getECRRecordsPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
res, err = servicemap.IsServiceInRegion("codebuild", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getCodeBuildResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("lambda", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getLambdaPolicyPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("efs", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getEFSfilesystemPoliciesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("secretsmanager", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getSecretsManagerSecretsPoliciesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
res, err = servicemap.IsServiceInRegion("glue", r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err)
|
||||
}
|
||||
if res {
|
||||
m.CommandCounter.Total++
|
||||
wg.Add(1)
|
||||
m.getGlueResourcePoliciesPerRegion(r, wg, semaphore, dataReceiver)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) Receiver(receiver chan Resource2, receiverDone chan bool) {
|
||||
defer close(receiverDone)
|
||||
for {
|
||||
select {
|
||||
case data := <-receiver:
|
||||
m.Resources2 = append(m.Resources2, data)
|
||||
case <-receiverDone:
|
||||
receiverDone <- true
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getSNSTopicsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSNSClient := InitCloudFoxSNSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.WrapTable, m.AWSMFAToken)
|
||||
|
||||
ListTopics, err := cloudFoxSNSClient.listTopics(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, t := range ListTopics {
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
topic, err := cloudFoxSNSClient.getTopicWithAttributes(aws.ToString(t.TopicArn), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
parsedArn, err := arn.Parse(aws.ToString(t.TopicArn))
|
||||
if err != nil {
|
||||
topic.Name = aws.ToString(t.TopicArn)
|
||||
}
|
||||
topic.Name = parsedArn.Resource
|
||||
topic.Region = parsedArn.Region
|
||||
|
||||
// check if topic is public or not
|
||||
if topic.Policy.IsPublic() {
|
||||
topic.IsPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
} else {
|
||||
topic.IsPublic = "No"
|
||||
}
|
||||
|
||||
// If there is a resource policy, convert the resource policy into plain english
|
||||
if !topic.Policy.IsEmpty() {
|
||||
|
||||
for i, statement := range topic.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(topic.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = statementSummaryInEnglish + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(t.TopicArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: topic.IsPublic,
|
||||
Region: parsedArn.Region,
|
||||
Name: topic.Name,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getS3Buckets(wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxS3Client := initCloudFoxS3Client(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.AWSMFAToken)
|
||||
|
||||
ListBuckets, err := sdk.CachedListBuckets(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, b := range ListBuckets {
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
bucket := &BucketRow{
|
||||
Arn: fmt.Sprintf("arn:aws:s3:::%s", aws.ToString(b.Name)),
|
||||
}
|
||||
name := aws.ToString(b.Name)
|
||||
region, err := sdk.CachedGetBucketLocation(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account), name)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
policyJSON, err := sdk.CachedGetBucketPolicy(cloudFoxS3Client.S3Client, aws.ToString(m.Caller.Account), region, aws.ToString(b.Name))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
continue
|
||||
} else {
|
||||
bucket.PolicyJSON = policyJSON
|
||||
}
|
||||
|
||||
policy, err := policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
m.modLog.Error(fmt.Sprintf("parsing bucket access policy (%s) as JSON: %s", name, err))
|
||||
} else {
|
||||
bucket.Policy = policy
|
||||
}
|
||||
// easier to just set the default state to be no and only flip it to yes if we have a case that matches
|
||||
if policy.IsPublic() {
|
||||
bucket.IsPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
} else {
|
||||
bucket.IsPublic = "No"
|
||||
}
|
||||
|
||||
// If there is a resource policy, convert the resource policy into plain english
|
||||
if !bucket.Policy.IsEmpty() {
|
||||
for i, statement := range bucket.Policy.Statement {
|
||||
var prefix string = ""
|
||||
if len(bucket.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = statementSummaryInEnglish + prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: bucket.Arn,
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: bucket.IsPublic,
|
||||
Region: region,
|
||||
Name: name,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getSQSQueuesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSQSClient := InitSQSClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListQueues, err := cloudFoxSQSClient.listQueues(r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, q := range ListQueues {
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
var isPublic string = "No"
|
||||
queue, err := cloudFoxSQSClient.getQueueWithAttributes(q, r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if !queue.Policy.IsEmpty() {
|
||||
if queue.Policy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
for i, statement := range queue.Policy.Statement {
|
||||
prefix := ""
|
||||
if len(queue.Policy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(&queue.Arn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: q,
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getECRRecordsPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxECRClient := InitECRClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
DescribeRepositories, err := sdk.CachedECRDescribeRepositories(cloudFoxECRClient.ECRClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, repo := range DescribeRepositories {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
repoPolicy, err := cloudFoxECRClient.getECRRepositoryPolicy(r, aws.ToString(repo.RepositoryName))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if !repoPolicy.IsEmpty() {
|
||||
if repoPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
for i, statement := range repoPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(repoPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(repo.RepositoryArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(repo.RepositoryName),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getCodeBuildResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxCodeBuildClient := InitCodeBuildClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListProjects, err := sdk.CachedCodeBuildListProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, p := range ListProjects {
|
||||
var projectPolicy policy.Policy
|
||||
var statementSummaryInEnglish string
|
||||
var isPublic string
|
||||
var isInteresting string = "No"
|
||||
project, err := sdk.CachedCodeBuildBatchGetProjects(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
policyJSON, err := sdk.CachedCodeBuildGetResourcePolicy(cloudFoxCodeBuildClient.CodeBuildClient, aws.ToString(cloudFoxCodeBuildClient.Caller.Account), r, p)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
continue
|
||||
}
|
||||
|
||||
projectPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
|
||||
if !projectPolicy.IsEmpty() {
|
||||
if projectPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
for i, statement := range projectPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(projectPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(project.Arn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(project.Name),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getLambdaPolicyPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxLambdaClient := InitLambdaClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListFunctions, err := cloudFoxLambdaClient.listFunctions(r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, f := range ListFunctions {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
functionPolicy, err := cloudFoxLambdaClient.getResourcePolicy(r, aws.ToString(f.FunctionName))
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if !functionPolicy.IsEmpty() {
|
||||
if functionPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
for i, statement := range functionPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(functionPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(f.FunctionArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(f.FunctionName),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getEFSfilesystemPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxEFSClient := InitFileSystemsClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListFileSystems, err := sdk.CachedDescribeFileSystems(cloudFoxEFSClient.EFSClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, fs := range ListFileSystems {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
fsPolicy, err := sdk.CachedDescribeFileSystemPolicy(cloudFoxEFSClient.EFSClient, aws.ToString(fs.FileSystemId), r, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if fsPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
|
||||
if !fsPolicy.IsEmpty() {
|
||||
for i, statement := range fsPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(fsPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(fs.FileSystemArn),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(fs.Name),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// getSecretsManagerSecretsPoliciesPerRegion retrieves the resource policies for all Secrets Manager secrets in the specified region.
|
||||
// It sends the resulting Resource2 objects to the dataReceiver channel.
|
||||
// It uses a semaphore to limit the number of concurrent requests and a WaitGroup to wait for all requests to complete.
|
||||
// It takes the region to search in, the WaitGroup to use, the semaphore to use, and the dataReceiver channel to send results to.
|
||||
func (m *ResourceTrustsModule) getSecretsManagerSecretsPoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxSecretsManagerClient := InitSecretsManagerClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ListSecrets, err := sdk.CachedSecretsManagerListSecrets(cloudFoxSecretsManagerClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, s := range ListSecrets {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
secretPolicy, err := sdk.CachedSecretsManagerGetResourcePolicy(cloudFoxSecretsManagerClient, aws.ToString(s.ARN), r, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
continue
|
||||
}
|
||||
|
||||
if secretPolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
|
||||
if !secretPolicy.IsEmpty() {
|
||||
for i, statement := range secretPolicy.Statement {
|
||||
prefix := ""
|
||||
if len(secretPolicy.Statement) > 1 {
|
||||
prefix = fmt.Sprintf("Statement %d says: ", i)
|
||||
statementSummaryInEnglish = prefix + statement.GetStatementSummaryInEnglish(*m.Caller.Account) + "\n"
|
||||
} else {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
}
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: aws.ToString(s.ARN),
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: aws.ToString(s.Name),
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ResourceTrustsModule) getGlueResourcePoliciesPerRegion(r string, wg *sync.WaitGroup, semaphore chan struct{}, dataReceiver chan Resource2) {
|
||||
defer func() {
|
||||
m.CommandCounter.Executing--
|
||||
m.CommandCounter.Complete++
|
||||
wg.Done()
|
||||
|
||||
}()
|
||||
semaphore <- struct{}{}
|
||||
defer func() { <-semaphore }()
|
||||
|
||||
cloudFoxGlueClient := InitGlueClient(m.Caller, m.AWSProfileProvided, m.CloudFoxVersion, m.Goroutines, m.AWSMFAToken)
|
||||
|
||||
ResourcePolicies, err := sdk.CachedGlueGetResourcePolicies(cloudFoxGlueClient, aws.ToString(m.Caller.Account), r)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for _, resourcePolicy := range ResourcePolicies {
|
||||
var isPublic string
|
||||
var statementSummaryInEnglish string
|
||||
var isInteresting string = "No"
|
||||
|
||||
if resourcePolicy.IsPublic() {
|
||||
isPublic = magenta("Yes")
|
||||
isInteresting = magenta("Yes")
|
||||
|
||||
} else {
|
||||
isPublic = "No"
|
||||
}
|
||||
|
||||
if !resourcePolicy.IsEmpty() {
|
||||
for _, statement := range resourcePolicy.Statement {
|
||||
statementSummaryInEnglish = statement.GetStatementSummaryInEnglish(*m.Caller.Account)
|
||||
resources := statement.Resource
|
||||
for _, resource := range resources {
|
||||
|
||||
statementSummaryInEnglish = strings.TrimSuffix(statementSummaryInEnglish, "\n")
|
||||
if isResourcePolicyInteresting(statementSummaryInEnglish) {
|
||||
//magenta(statementSummaryInEnglish)
|
||||
isInteresting = magenta("Yes")
|
||||
}
|
||||
|
||||
dataReceiver <- Resource2{
|
||||
AccountID: aws.ToString(m.Caller.Account),
|
||||
ARN: resource,
|
||||
ResourcePolicySummary: statementSummaryInEnglish,
|
||||
Public: isPublic,
|
||||
Name: resource,
|
||||
Region: r,
|
||||
Interesting: isInteresting,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func isResourcePolicyInteresting(statementSummaryInEnglish string) bool {
|
||||
// check if the statement has any of the following items, but make sure the check is case insensitive
|
||||
// if it does, then return true
|
||||
// if it doesn't, then return false
|
||||
|
||||
if //(strings.Contains(strings.ToLower(statementSummaryInEnglish), "*") && !strings.Contains(strings.ToLower(statementSummaryInEnglish), "denied")) ||
|
||||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("Everyone")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), "denied")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalOrgID")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalAccount")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:PrincipalOrgPaths")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("role")) ||
|
||||
strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("root")) ||
|
||||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:arn")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("SourceArn"))) ||
|
||||
(strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("aws:arn")) && !strings.Contains(strings.ToLower(statementSummaryInEnglish), strings.ToLower("SourceAccount"))) {
|
||||
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsResourcePolicyInteresting(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
expected bool
|
||||
}{
|
||||
{
|
||||
name: "test1",
|
||||
input: "Everyone can sqs:SendMessage & can sqs:ReceiveMessage",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "PrincipalOrgPaths",
|
||||
input: "aws:PrincipalOrgPaths",
|
||||
expected: true,
|
||||
},
|
||||
{
|
||||
name: "Empty",
|
||||
input: "",
|
||||
expected: false,
|
||||
},
|
||||
{
|
||||
name: "NotInteresting",
|
||||
input: "sns.amazonaws.com can lambda:InvokeFunction",
|
||||
expected: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
actual := isResourcePolicyInteresting(tc.input)
|
||||
if actual != tc.expected {
|
||||
t.Errorf("Expected %v but got %v for input %s", tc.expected, actual, tc.input)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
+511
-126
@@ -1,122 +1,565 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/bishopfox/knownawsaccountslookup"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type RoleTrustsModule struct {
|
||||
// General configuration data
|
||||
IAMClient iam.ListRolesAPIClient
|
||||
IAMClient sdk.AWSIAMClientInterface
|
||||
IAMSimulatePrincipalPolicyClient iam.SimulatePrincipalPolicyAPIClient
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSProfile string
|
||||
CommandCounter console.CommandCounter
|
||||
Goroutines int
|
||||
CommandCounter internal.CommandCounter
|
||||
SkipAdminCheck bool
|
||||
WrapTable bool
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
pmapperMod PmapperModule
|
||||
pmapperError error
|
||||
iamSimClient IamSimulatorModule
|
||||
|
||||
// Main module data
|
||||
AnalyzedRoles []AnalyzedRole
|
||||
AnalyzedRoles []AnalyzedRole
|
||||
RoleTrustTable []RoleTrustRow
|
||||
|
||||
vendors *knownawsaccountslookup.Vendors
|
||||
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) PrintRoleTrusts(outputFormat string, outputDirectory string, verbosity int) {
|
||||
type RoleTrustRow struct {
|
||||
RoleARN string
|
||||
RoleName string
|
||||
TrustedPrincipal string
|
||||
TrustedService string
|
||||
TrustedFederatedProvider string
|
||||
TrustedFederatedSubject string
|
||||
ExternalID string
|
||||
IsAdmin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
type AnalyzedRole struct {
|
||||
roleARN *string
|
||||
trustsDoc policy.TrustPolicyDocument
|
||||
// trustType string // UNUSED FIELD, PLEASE REVIEW
|
||||
Admin string
|
||||
CanPrivEsc string
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) PrintRoleTrusts(outputDirectory string, verbosity int) {
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
fmt.Printf("[%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
localAdminMap := make(map[string]bool)
|
||||
m.vendors = knownawsaccountslookup.NewVendorMap()
|
||||
m.vendors.PopulateKnownAWSAccounts()
|
||||
|
||||
fmt.Printf("[%s][%s] Enumerating role trusts for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
//fmt.Printf("[%s][%s] Looking for pmapper data for this account and building a PrivEsc graph in golang if it exists.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
m.pmapperMod, m.pmapperError = initPmapperGraph(m.Caller, m.AWSProfile, m.Goroutines)
|
||||
m.iamSimClient = initIAMSimClient(m.IAMClient, m.Caller, m.AWSProfile, m.Goroutines)
|
||||
// if m.pmapperError != nil {
|
||||
// fmt.Printf("[%s][%s] No pmapper data found for this account. Using cloudfox's iam-simulator for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// } else {
|
||||
// fmt.Printf("[%s][%s] Found pmapper data for this account. Using it for role analysis\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
// }
|
||||
m.getAllRoleTrusts()
|
||||
m.printPrincipalTrusts(outputFormat, outputDirectory)
|
||||
m.printServiceTrusts(outputFormat, outputDirectory)
|
||||
|
||||
if m.pmapperError == nil {
|
||||
for i := range m.AnalyzedRoles {
|
||||
m.AnalyzedRoles[i].Admin, m.AnalyzedRoles[i].CanPrivEsc = GetPmapperResults(m.SkipAdminCheck, m.pmapperMod, m.AnalyzedRoles[i].roleARN)
|
||||
}
|
||||
} else {
|
||||
for i := range m.AnalyzedRoles {
|
||||
m.AnalyzedRoles[i].Admin, m.AnalyzedRoles[i].CanPrivEsc = GetIamSimResult(m.SkipAdminCheck, m.AnalyzedRoles[i].roleARN, m.iamSimClient, localAdminMap)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
principalsHeader, principalsBody, principalTableCols := m.printPrincipalTrusts(outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: principalsHeader,
|
||||
Body: principalsBody,
|
||||
TableCols: principalTableCols,
|
||||
Name: "role-trusts-principals",
|
||||
})
|
||||
rootPrincipalsHeader, rootPrincipalsBody, rootPrincipalTableCols := m.printPrincipalTrustsRootOnly(outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: rootPrincipalsHeader,
|
||||
Body: rootPrincipalsBody,
|
||||
TableCols: rootPrincipalTableCols,
|
||||
Name: "role-trusts-principals-root-trusts-without-external-id",
|
||||
SkipPrintToScreen: true,
|
||||
})
|
||||
servicesHeader, servicesBody, serviceTableCols := m.printServiceTrusts(outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: servicesHeader,
|
||||
Body: servicesBody,
|
||||
TableCols: serviceTableCols,
|
||||
Name: "role-trusts-services",
|
||||
})
|
||||
|
||||
federatedHeader, federatedBody, federatedTableCols := m.printFederatedTrusts(outputDirectory)
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: federatedHeader,
|
||||
Body: federatedBody,
|
||||
TableCols: federatedTableCols,
|
||||
Name: "role-trusts-federated",
|
||||
})
|
||||
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
if len(principalsBody) > 0 {
|
||||
fmt.Printf("[%s][%s] %s principal role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(principalsBody)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
if len(servicesBody) > 0 {
|
||||
fmt.Printf("[%s][%s] %s service role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(servicesBody)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
if len(federatedBody) > 0 {
|
||||
fmt.Printf("[%s][%s] %s federated role trusts found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(federatedBody)))
|
||||
} else {
|
||||
fmt.Printf("[%s][%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printPrincipalTrusts(outputFormat string, outputDirectory string) {
|
||||
func (m *RoleTrustsModule) printPrincipalTrusts(outputDirectory string) ([]string, [][]string, []string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-principals"
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
header = []string{
|
||||
"Account",
|
||||
"Role Arn",
|
||||
"Role Name",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
}
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
if len(role.trustsDoc.Statement[0].Principal.AWS) > 0 {
|
||||
for _, principal := range role.trustsDoc.Statement[0].Principal.AWS {
|
||||
column1 := aws.ToString(role.roleARN)
|
||||
column2 := principal
|
||||
column3 := role.trustsDoc.Statement[0].Condition.StringEquals.StsExternalID
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3})
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
//check to see if the accountID is known
|
||||
if strings.Contains(principal, "arn:aws:iam::") || strings.Contains(principal, "root") {
|
||||
accountID := strings.Split(principal, ":")[4]
|
||||
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
|
||||
if vendorName != "" {
|
||||
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
|
||||
}
|
||||
}
|
||||
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
|
||||
TrustedPrincipal: principal,
|
||||
ExternalID: statement.Condition.StringEquals.StsExternalID,
|
||||
IsAdmin: role.Admin,
|
||||
CanPrivEsc: role.CanPrivEsc,
|
||||
}
|
||||
body = append(body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
RoleTrustRow.RoleARN,
|
||||
RoleTrustRow.RoleName,
|
||||
RoleTrustRow.TrustedPrincipal,
|
||||
RoleTrustRow.ExternalID,
|
||||
RoleTrustRow.IsAdmin,
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
return header, body, tableCols
|
||||
|
||||
}
|
||||
|
||||
// printPrincipalTrusts but only those that have a trusted principal that contains :root and also does not have an external ID
|
||||
func (m *RoleTrustsModule) printPrincipalTrustsRootOnly(outputDirectory string) ([]string, [][]string, []string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-principals-root-trusts-without-external-id"
|
||||
header = []string{
|
||||
"Account",
|
||||
"Role Arn",
|
||||
"Role Name",
|
||||
"Trusted Principal",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{"Account", "Role Arn", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{"Role Name", "Trusted Principal", "ExternalID", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
}
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, principal := range statement.Principal.AWS {
|
||||
if strings.Contains(principal, ":root") && statement.Condition.StringEquals.StsExternalID == "" {
|
||||
accountID := strings.Split(principal, ":")[4]
|
||||
vendorName := m.vendors.GetVendorNameFromAccountID(accountID)
|
||||
if vendorName != "" {
|
||||
principal = fmt.Sprintf("%s (%s)", principal, vendorName)
|
||||
}
|
||||
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
|
||||
TrustedPrincipal: principal,
|
||||
ExternalID: statement.Condition.StringEquals.StsExternalID,
|
||||
IsAdmin: role.Admin,
|
||||
CanPrivEsc: role.CanPrivEsc,
|
||||
}
|
||||
body = append(body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
RoleTrustRow.RoleARN,
|
||||
RoleTrustRow.RoleName,
|
||||
RoleTrustRow.TrustedPrincipal,
|
||||
RoleTrustRow.ExternalID,
|
||||
RoleTrustRow.IsAdmin,
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
////m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
fmt.Printf("[%s] %s role trusts found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
}
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
return header, body, tableCols
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printServiceTrusts(outputFormat string, outputDirectory string) {
|
||||
func (m *RoleTrustsModule) printServiceTrusts(outputDirectory string) ([]string, [][]string, []string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-services"
|
||||
m.output.Headers = []string{
|
||||
"Role",
|
||||
header = []string{
|
||||
"Account",
|
||||
"Role Arn",
|
||||
"Role Name",
|
||||
"Trusted Service",
|
||||
"ExternalID",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{"Account", "Role Arn", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{"Role Name", "Trusted Service", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
}
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
if len(role.trustsDoc.Statement[0].Principal.Service) > 0 {
|
||||
for _, service := range role.trustsDoc.Statement[0].Principal.Service {
|
||||
column1 := aws.ToString(role.roleARN)
|
||||
column2 := service
|
||||
column3 := role.trustsDoc.Statement[0].Condition.StringEquals.StsExternalID
|
||||
m.output.Body = append(m.output.Body, []string{column1, column2, column3})
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
for _, service := range statement.Principal.Service {
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
|
||||
TrustedService: service,
|
||||
IsAdmin: role.Admin,
|
||||
CanPrivEsc: role.CanPrivEsc,
|
||||
}
|
||||
body = append(body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
RoleTrustRow.RoleARN,
|
||||
RoleTrustRow.RoleName,
|
||||
RoleTrustRow.TrustedService,
|
||||
RoleTrustRow.IsAdmin,
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
if len(m.output.Body) > 0 {
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(m.output.Verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.FullFilename, m.output.CallingModule)
|
||||
fmt.Printf("[%s] %s role trusts found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
// sort the rows based on column 2 (service)
|
||||
sort.SliceStable(body, func(i, j int) bool {
|
||||
return body[i][3] < body[j][3]
|
||||
})
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No role trusts found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
return header, body, tableCols
|
||||
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) printFederatedTrusts(outputDirectory string) ([]string, [][]string, []string) {
|
||||
var header []string
|
||||
var body [][]string
|
||||
m.output.FullFilename = ""
|
||||
m.output.Body = nil
|
||||
m.output.CallingModule = "role-trusts"
|
||||
m.output.FullFilename = "role-trusts-federated"
|
||||
header = []string{
|
||||
"Account",
|
||||
"Role Arn",
|
||||
"Role Name",
|
||||
"Trusted Provider",
|
||||
"Trusted Subject",
|
||||
"IsAdmin?",
|
||||
"CanPrivEscToAdmin?",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{"Account", "Role Arn", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
// Otherwise, use the default columns for this module (brief)
|
||||
} else {
|
||||
tableCols = []string{"Role Name", "Trusted Provider", "Trusted Subject", "IsAdmin?", "CanPrivEscToAdmin?"}
|
||||
}
|
||||
// Remove the pmapper row if there is no pmapper data
|
||||
if m.pmapperError != nil {
|
||||
sharedLogger.Errorf("%s - %s - No pmapper data found for this account. Skipping the pmapper column in the output table.", m.output.CallingModule, m.AWSProfile)
|
||||
tableCols = removeStringFromSlice(tableCols, "CanPrivEscToAdmin?")
|
||||
}
|
||||
for _, role := range m.AnalyzedRoles {
|
||||
for _, statement := range role.trustsDoc.Statement {
|
||||
if len(statement.Principal.Federated) > 0 {
|
||||
provider, subjects := parseFederatedTrustPolicy(statement)
|
||||
for _, subject := range subjects {
|
||||
RoleTrustRow := RoleTrustRow{
|
||||
RoleARN: aws.ToString(role.roleARN),
|
||||
RoleName: GetResourceNameFromArn(aws.ToString(role.roleARN)),
|
||||
TrustedFederatedProvider: provider,
|
||||
TrustedFederatedSubject: subject,
|
||||
IsAdmin: role.Admin,
|
||||
CanPrivEsc: role.CanPrivEsc,
|
||||
}
|
||||
body = append(body, []string{
|
||||
aws.ToString(m.Caller.Account),
|
||||
RoleTrustRow.RoleARN,
|
||||
RoleTrustRow.RoleName,
|
||||
RoleTrustRow.TrustedFederatedProvider,
|
||||
RoleTrustRow.TrustedFederatedSubject,
|
||||
RoleTrustRow.IsAdmin,
|
||||
RoleTrustRow.CanPrivEsc})
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
m.sortTrustsTablePerTrustedPrincipal()
|
||||
return header, body, tableCols
|
||||
|
||||
}
|
||||
|
||||
func parseFederatedTrustPolicy(statement policy.RoleTrustStatementEntry) (string, []string) {
|
||||
var provider string
|
||||
var subjects []string
|
||||
if len(statement.Principal.Federated) > 1 {
|
||||
sharedLogger.Warnf("Multiple federated providers found in the trust policy. This is not currently supported. Please review the trust policy for specifics.")
|
||||
provider = "Multiple Federated Providers"
|
||||
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
|
||||
}
|
||||
|
||||
switch {
|
||||
// lets use the Federated field to determine the provider, then based on the provider we can grab the list of subjects
|
||||
case strings.Contains(statement.Principal.Federated[0], "token.actions.githubusercontent.com"):
|
||||
provider = "GitHub"
|
||||
if len(statement.Condition.StringLike.TokenActionsGithubusercontentComSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.TokenActionsGithubusercontentComSub...)
|
||||
} else if len(statement.Condition.StringEquals.TokenActionsGithubusercontentComSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.TokenActionsGithubusercontentComSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL REPOS!!!")
|
||||
}
|
||||
case strings.Contains(statement.Principal.Federated[0], "oidc.eks"):
|
||||
provider = "EKS"
|
||||
if len(statement.Condition.StringLike.OidcEksSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.OidcEksSub...)
|
||||
} else if len(statement.Condition.StringEquals.OidcEksSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.OidcEksSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL SERVICE ACCOUNTS!!!")
|
||||
}
|
||||
// terraform case
|
||||
case strings.Contains(statement.Principal.Federated[0], "app.terraform.io"):
|
||||
provider = "Terraform Cloud"
|
||||
if len(statement.Condition.StringLike.TerraformSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.TerraformSub...)
|
||||
} else if len(statement.Condition.StringEquals.TerraformSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.TerraformSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL WORKSPACES")
|
||||
}
|
||||
// Azure AD case
|
||||
case strings.Contains(statement.Principal.Federated[0], "http://sts.windows.net"):
|
||||
provider = "Azure AD"
|
||||
if len(statement.Condition.StringLike.AzureADIss) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.AzureADIss...)
|
||||
} else if len(statement.Condition.StringEquals.AzureADIss) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.AzureADIss...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL ISSUERS")
|
||||
}
|
||||
|
||||
// AWS SSO case
|
||||
case strings.Contains(statement.Principal.Federated[0], "AWSSSO"):
|
||||
provider = "AWS SSO"
|
||||
subjects = append(subjects, "Not applicable")
|
||||
|
||||
// okta case
|
||||
case strings.Contains(statement.Principal.Federated[0], "Okta"):
|
||||
provider = "Okta"
|
||||
subjects = append(subjects, "Not applicable")
|
||||
|
||||
// cognito case
|
||||
case statement.Principal.Federated[0] == "cognito-identity.amazonaws.com":
|
||||
provider = "Cognito"
|
||||
if statement.Condition.ForAnyValueStringLike.CognitoAMR != "" {
|
||||
subjects = append(subjects, statement.Condition.ForAnyValueStringLike.CognitoAMR)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL IDENTITIES")
|
||||
}
|
||||
// google workspace case
|
||||
case strings.Contains(statement.Principal.Federated[0], "workspace.google.com"):
|
||||
provider = "Google Workspace"
|
||||
if len(statement.Condition.StringLike.GoogleWorkspaceSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.GoogleWorkspaceSub...)
|
||||
} else if len(statement.Condition.StringEquals.GoogleWorkspaceSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.GoogleWorkspaceSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL USERS")
|
||||
}
|
||||
// GCP case
|
||||
case strings.Contains(statement.Principal.Federated[0], "accounts.google.com"):
|
||||
provider = "GCP"
|
||||
if len(statement.Condition.StringLike.GCPSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.GCPSub...)
|
||||
} else if len(statement.Condition.StringEquals.GCPSub) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.GCPSub...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL USERS")
|
||||
}
|
||||
// auth0 case
|
||||
//not ready yet
|
||||
// case strings.Contains(statement.Principal.Federated[0], "auth0.com"):
|
||||
// provider = "Auth0"
|
||||
// if len(statement.Condition.ForAnyValueStringLike.Auth0Amr) > 0 {
|
||||
// subjects = append(subjects, statement.Condition.ForAnyValueStringLike.Auth0Amr...)
|
||||
// } else {
|
||||
// subjects = append(subjects, "ALL GROUPS")
|
||||
// }
|
||||
// circleci case
|
||||
case strings.Contains(statement.Principal.Federated[0], "oidc.circleci.com"):
|
||||
provider = "CircleCI"
|
||||
if len(statement.Condition.StringLike.CircleCIAud) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringLike.CircleCIAud...)
|
||||
} else if len(statement.Condition.StringEquals.CircleCIAud) > 0 {
|
||||
subjects = append(subjects, statement.Condition.StringEquals.CircleCIAud...)
|
||||
} else {
|
||||
subjects = append(subjects, "ALL PROJECTS")
|
||||
}
|
||||
|
||||
default:
|
||||
provider = "Unknown Federated Provider"
|
||||
subjects = append(subjects, "Review policy for specifics\nand submit issue to cloudfox repo.")
|
||||
|
||||
}
|
||||
return provider, subjects
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
|
||||
@@ -129,88 +572,30 @@ func (m *RoleTrustsModule) sortTrustsTablePerTrustedPrincipal() {
|
||||
}
|
||||
|
||||
func (m *RoleTrustsModule) getAllRoleTrusts() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationMarker *string
|
||||
ListRoles, err := sdk.CachedIamListRoles(m.IAMClient, aws.ToString(m.Caller.Account))
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
}
|
||||
|
||||
// This for loop exits at the end dependeding on whether the output hits its last page (see pagination control block at the end of the loop).
|
||||
for {
|
||||
results, err := m.IAMClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
)
|
||||
for _, role := range ListRoles {
|
||||
trustsdoc, err := policy.ParseRoleTrustPolicyDocument(role)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
|
||||
for _, role := range results.Roles {
|
||||
trustsdoc, err := parseRoleTrustPolicyDocument(role)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{roleARN: role.Arn, trustsDoc: trustsdoc})
|
||||
if role.Arn != nil {
|
||||
m.AnalyzedRoles = append(m.AnalyzedRoles, AnalyzedRole{
|
||||
roleARN: role.Arn,
|
||||
trustsDoc: trustsdoc,
|
||||
Admin: "",
|
||||
CanPrivEsc: "",
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if results.IsTruncated {
|
||||
PaginationMarker = results.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
type AnalyzedRole struct {
|
||||
roleARN *string
|
||||
trustsDoc trustPolicyDocument
|
||||
trustType string
|
||||
}
|
||||
|
||||
type trustPolicyDocument struct {
|
||||
Version string `json:"Version"`
|
||||
Statement []struct {
|
||||
Sid string `json:"Sid"`
|
||||
Effect string `json:"Effect"`
|
||||
Principal struct {
|
||||
AWS ListOfPrincipals `json:"AWS"`
|
||||
Service ListOfPrincipals `json:"Service"`
|
||||
} `json:"Principal"`
|
||||
Action string `json:"Action"`
|
||||
Condition struct {
|
||||
StringEquals struct {
|
||||
StsExternalID string `json:"sts:ExternalId"`
|
||||
} `json:"StringEquals"`
|
||||
} `json:"Condition"`
|
||||
} `json:"Statement"`
|
||||
}
|
||||
|
||||
func parseRoleTrustPolicyDocument(role types.Role) (trustPolicyDocument, error) {
|
||||
document, _ := url.QueryUnescape(aws.ToString(role.AssumeRolePolicyDocument))
|
||||
var parsedDocumentToJSON trustPolicyDocument
|
||||
_ = json.Unmarshal([]byte(document), &parsedDocumentToJSON)
|
||||
return parsedDocumentToJSON, nil
|
||||
}
|
||||
|
||||
// A custom unmarshaller is necessary because the list of principals can be an array of strings or a string.
|
||||
// https://stackoverflow.com/questions/65854778/parsing-arn-from-iam-policy-using-regex
|
||||
type ListOfPrincipals []string
|
||||
|
||||
func (r *ListOfPrincipals) UnmarshalJSON(b []byte) error {
|
||||
var s string
|
||||
if err := json.Unmarshal(b, &s); err == nil {
|
||||
*r = append(*r, s)
|
||||
return nil
|
||||
}
|
||||
var ss []string
|
||||
if err := json.Unmarshal(b, &ss); err == nil {
|
||||
*r = ss
|
||||
return nil
|
||||
}
|
||||
return errors.New("cannot unmarshal neither to a string nor a slice of strings")
|
||||
}
|
||||
+112
-88
@@ -1,37 +1,37 @@
|
||||
package aws
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/console"
|
||||
"github.com/BishopFox/cloudfox/utils"
|
||||
"github.com/BishopFox/cloudfox/aws/sdk"
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/route53"
|
||||
"github.com/aws/aws-sdk-go-v2/service/sts"
|
||||
"github.com/fatih/color"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
var cyan = color.New(color.FgCyan).SprintFunc()
|
||||
|
||||
type Route53Module struct {
|
||||
// General configuration data
|
||||
Route53Client *route53.Client
|
||||
Route53Client sdk.AWSRoute53ClientInterface
|
||||
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
OutputFormat string
|
||||
Caller sts.GetCallerIdentityOutput
|
||||
AWSRegions []string
|
||||
AWSOutputType string
|
||||
AWSTableCols string
|
||||
|
||||
Goroutines int
|
||||
AWSProfile string
|
||||
CommandCounter console.CommandCounter
|
||||
WrapTable bool
|
||||
CommandCounter internal.CommandCounter
|
||||
|
||||
// Main module data
|
||||
Records []Record
|
||||
// Used to store output data for pretty printing
|
||||
output utils.OutputData2
|
||||
output internal.OutputData2
|
||||
|
||||
modLog *logrus.Entry
|
||||
}
|
||||
@@ -44,37 +44,66 @@ type Record struct {
|
||||
PrivateZone string
|
||||
}
|
||||
|
||||
func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string, verbosity int) {
|
||||
func (m *Route53Module) PrintRoute53(outputDirectory string, verbosity int) {
|
||||
|
||||
// These stuct values are used by the output module
|
||||
// These struct values are used by the output module
|
||||
m.output.Verbosity = verbosity
|
||||
m.output.Directory = outputDirectory
|
||||
m.output.CallingModule = "route53"
|
||||
m.modLog = utils.TxtLogger.WithFields(logrus.Fields{
|
||||
m.modLog = internal.TxtLog.WithFields(logrus.Fields{
|
||||
"module": m.output.CallingModule,
|
||||
})
|
||||
if m.AWSProfile == "" {
|
||||
m.AWSProfile = fmt.Sprintf("%s-%s", aws.ToString(m.Caller.Account), aws.ToString(m.Caller.UserId))
|
||||
m.AWSProfile = internal.BuildAWSPath(m.Caller)
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Enumerating Route53 for account %s.\n", cyan(m.output.CallingModule), aws.ToString(m.Caller.Account))
|
||||
fmt.Printf("[%s][%s] Enumerating Route53 for account %s.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), aws.ToString(m.Caller.Account))
|
||||
|
||||
m.getRoute53Records()
|
||||
|
||||
m.output.Headers = []string{
|
||||
"Service",
|
||||
"Account",
|
||||
"Name",
|
||||
"Type",
|
||||
"Value",
|
||||
"PrivateZone",
|
||||
}
|
||||
|
||||
// If the user specified table columns, use those.
|
||||
// If the user specified -o wide, use the wide default cols for this module.
|
||||
// Otherwise, use the hardcoded default cols for this module.
|
||||
var tableCols []string
|
||||
// If the user specified table columns, use those.
|
||||
if m.AWSTableCols != "" {
|
||||
// If the user specified wide as the output format, use these columns.
|
||||
// remove any spaces between any commas and the first letter after the commas
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
m.AWSTableCols = strings.ReplaceAll(m.AWSTableCols, ", ", ",")
|
||||
tableCols = strings.Split(m.AWSTableCols, ",")
|
||||
} else if m.AWSOutputType == "wide" {
|
||||
tableCols = []string{
|
||||
"Account",
|
||||
"Name",
|
||||
"Type",
|
||||
"Value",
|
||||
"PrivateZone",
|
||||
}
|
||||
|
||||
} else {
|
||||
tableCols = []string{
|
||||
"Name",
|
||||
"Type",
|
||||
"Value",
|
||||
"PrivateZone",
|
||||
}
|
||||
}
|
||||
|
||||
// Table rows
|
||||
for i := range m.Records {
|
||||
m.output.Body = append(
|
||||
m.output.Body,
|
||||
[]string{
|
||||
m.Records[i].AWSService,
|
||||
aws.ToString(m.Caller.Account),
|
||||
m.Records[i].Name,
|
||||
m.Records[i].Type,
|
||||
m.Records[i].Value,
|
||||
@@ -85,20 +114,35 @@ func (m *Route53Module) PrintRoute53(outputFormat string, outputDirectory string
|
||||
}
|
||||
if len(m.output.Body) > 0 {
|
||||
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile)
|
||||
//m.output.OutputSelector(outputFormat)
|
||||
utils.OutputSelector(verbosity, outputFormat, m.output.Headers, m.output.Body, m.output.FilePath, m.output.CallingModule, m.output.CallingModule)
|
||||
m.writeLoot(outputDirectory, verbosity)
|
||||
fmt.Printf("[%s] %s DNS records found.\n", cyan(m.output.CallingModule), strconv.Itoa(len(m.output.Body)))
|
||||
m.output.FilePath = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
|
||||
o := internal.OutputClient{
|
||||
Verbosity: verbosity,
|
||||
CallingModule: m.output.CallingModule,
|
||||
Table: internal.TableClient{
|
||||
Wrap: m.WrapTable,
|
||||
},
|
||||
}
|
||||
o.Table.TableFiles = append(o.Table.TableFiles, internal.TableFile{
|
||||
Header: m.output.Headers,
|
||||
Body: m.output.Body,
|
||||
TableCols: tableCols,
|
||||
Name: m.output.CallingModule,
|
||||
})
|
||||
o.PrefixIdentifier = m.AWSProfile
|
||||
o.Table.DirectoryName = filepath.Join(outputDirectory, "cloudfox-output", "aws", fmt.Sprintf("%s-%s", m.AWSProfile, aws.ToString(m.Caller.Account)))
|
||||
o.WriteFullOutput(o.Table.TableFiles, nil)
|
||||
m.writeLoot(o.Table.DirectoryName, verbosity)
|
||||
fmt.Printf("[%s][%s] %s DNS records found.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), strconv.Itoa(len(m.output.Body)))
|
||||
|
||||
} else {
|
||||
fmt.Printf("[%s] No DNS records found, skipping the creation of an output file.\n", cyan(m.output.CallingModule))
|
||||
fmt.Printf("[%s][%s] No DNS records found, skipping the creation of an output file.\n", cyan(m.output.CallingModule), cyan(m.AWSProfile))
|
||||
}
|
||||
|
||||
fmt.Printf("[%s][%s] For context and next steps: https://github.com/BishopFox/cloudfox/wiki/AWS-Commands#%s\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), m.output.CallingModule)
|
||||
}
|
||||
|
||||
func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
|
||||
path := filepath.Join(outputDirectory, "cloudfox-output", "aws", m.AWSProfile, "loot")
|
||||
path := filepath.Join(outputDirectory, "loot")
|
||||
err := os.MkdirAll(path, os.ModePerm)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
@@ -112,7 +156,7 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
|
||||
var route53APublicRecords string
|
||||
|
||||
for _, record := range m.Records {
|
||||
if record.Type == "A" || record.Type == "AAAA" {
|
||||
if record.Type == "A" || record.Type == "AAAA" || record.Type == "CNAME" {
|
||||
if record.PrivateZone == "True" {
|
||||
route53APrivateRecords = route53APrivateRecords + fmt.Sprintln(record.Name)
|
||||
} else {
|
||||
@@ -131,13 +175,13 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
|
||||
if verbosity > 2 {
|
||||
if len(route53APublicRecords) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Feed these public zone A records into nmap and something like gowitness or aquatone."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed these public zone A records into nmap and something like gowitness or aquatone."))
|
||||
fmt.Print(route53APublicRecords)
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), route53ARecordsPublicZonesFileName)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), route53ARecordsPublicZonesFileName)
|
||||
|
||||
err = os.WriteFile(route53ARecordsPrivateZonesFileName, []byte(route53APrivateRecords), 0644)
|
||||
if err != nil {
|
||||
@@ -149,83 +193,63 @@ func (m *Route53Module) writeLoot(outputDirectory string, verbosity int) {
|
||||
if verbosity > 2 {
|
||||
if len(route53APrivateRecords) > 0 {
|
||||
fmt.Println()
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("Feed these private zone A records into nmap and something like gowitness or aquatone."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("Feed these private zone A records into nmap and something like gowitness or aquatone."))
|
||||
fmt.Print(route53APrivateRecords)
|
||||
fmt.Printf("[%s] %s \n", cyan(m.output.CallingModule), green("End of loot file."))
|
||||
fmt.Printf("[%s][%s] %s \n", cyan(m.output.CallingModule), cyan(m.AWSProfile), green("End of loot file."))
|
||||
}
|
||||
}
|
||||
|
||||
fmt.Printf("[%s] Loot written to [%s]\n", cyan(m.output.CallingModule), route53ARecordsPrivateZonesFileName)
|
||||
fmt.Printf("[%s][%s] Loot written to [%s]\n", cyan(m.output.CallingModule), cyan(m.AWSProfile), route53ARecordsPrivateZonesFileName)
|
||||
|
||||
}
|
||||
|
||||
func (m *Route53Module) getRoute53Records() {
|
||||
// "PaginationMarker" is a control variable used for output continuity, as AWS return the output in pages.
|
||||
var PaginationControl *string
|
||||
var recordName string
|
||||
var recordType string
|
||||
|
||||
for {
|
||||
ListHostedZones, err := m.Route53Client.ListHostedZones(
|
||||
context.TODO(),
|
||||
&route53.ListHostedZonesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
HostedZones, err := sdk.CachedRoute53ListHostedZones(m.Route53Client, aws.ToString(m.Caller.Account))
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
return
|
||||
}
|
||||
|
||||
var privateZone string
|
||||
for _, zone := range HostedZones {
|
||||
if zone.Config.PrivateZone {
|
||||
privateZone = "True"
|
||||
} else {
|
||||
privateZone = "False"
|
||||
}
|
||||
|
||||
Records, err := sdk.CachedRoute53ListResourceRecordSets(m.Route53Client, aws.ToString(m.Caller.Account), aws.ToString(zone.Id))
|
||||
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, record := range Records {
|
||||
recordName = aws.ToString(record.Name)
|
||||
recordType = string(record.Type)
|
||||
|
||||
for _, resourceRecord := range record.ResourceRecords {
|
||||
recordValue := resourceRecord.Value
|
||||
m.Records = append(
|
||||
m.Records,
|
||||
Record{
|
||||
AWSService: "Route53",
|
||||
Name: recordName,
|
||||
Type: recordType,
|
||||
Value: aws.ToString(recordValue),
|
||||
PrivateZone: privateZone,
|
||||
})
|
||||
|
||||
var privateZone string
|
||||
for _, zone := range ListHostedZones.HostedZones {
|
||||
id := aws.ToString(zone.Id)
|
||||
if zone.Config.PrivateZone {
|
||||
privateZone = "True"
|
||||
} else {
|
||||
privateZone = "False"
|
||||
}
|
||||
|
||||
ListResourceRecordSets, err := m.Route53Client.ListResourceRecordSets(
|
||||
context.TODO(),
|
||||
&route53.ListResourceRecordSetsInput{
|
||||
HostedZoneId: &id,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
m.modLog.Error(err.Error())
|
||||
m.CommandCounter.Error++
|
||||
break
|
||||
}
|
||||
for _, record := range ListResourceRecordSets.ResourceRecordSets {
|
||||
recordName = aws.ToString(record.Name)
|
||||
recordType = string(record.Type)
|
||||
|
||||
for _, resourceRecord := range record.ResourceRecords {
|
||||
recordValue := resourceRecord.Value
|
||||
m.Records = append(
|
||||
m.Records,
|
||||
Record{
|
||||
AWSService: "Route53",
|
||||
Name: recordName,
|
||||
Type: recordType,
|
||||
Value: aws.ToString(recordValue),
|
||||
PrivateZone: privateZone,
|
||||
})
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// The "NextToken" value is nil when there's no more data to return.
|
||||
if ListHostedZones.NextMarker != nil {
|
||||
PaginationControl = ListHostedZones.NextMarker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigateway"
|
||||
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type APIGatewayClientInterface interface {
|
||||
GetRestApis(context.Context, *apigateway.GetRestApisInput, ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error)
|
||||
GetStages(context.Context, *apigateway.GetStagesInput, ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error)
|
||||
GetResources(context.Context, *apigateway.GetResourcesInput, ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error)
|
||||
GetDomainNames(context.Context, *apigateway.GetDomainNamesInput, ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error)
|
||||
GetBasePathMappings(context.Context, *apigateway.GetBasePathMappingsInput, ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error)
|
||||
GetMethod(context.Context, *apigateway.GetMethodInput, ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error)
|
||||
GetUsagePlans(context.Context, *apigateway.GetUsagePlansInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error)
|
||||
GetUsagePlanKeys(context.Context, *apigateway.GetUsagePlanKeysInput, ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error)
|
||||
}
|
||||
|
||||
type CachedGetMethodOutput struct {
|
||||
ApiKeyRequired *bool
|
||||
AuthorizationScopes []string
|
||||
AuthorizationType *string
|
||||
AuthorizerId *string
|
||||
HttpMethod *string
|
||||
MethodIntegration *apiGatewayTypes.Integration
|
||||
MethodResponses map[string]apiGatewayTypes.MethodResponse
|
||||
OperationName *string
|
||||
RequestModels map[string]string
|
||||
RequestParameters map[string]bool
|
||||
RequestValidatorId *string
|
||||
ResourceId *string
|
||||
}
|
||||
|
||||
type CachedGetStagesOutput struct {
|
||||
Item []apiGatewayTypes.Stage
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]apiGatewayTypes.RestApi{})
|
||||
gob.Register(CachedGetStagesOutput{})
|
||||
gob.Register([]apiGatewayTypes.Resource{})
|
||||
gob.Register([]apiGatewayTypes.DomainName{})
|
||||
gob.Register([]apiGatewayTypes.BasePathMapping{})
|
||||
gob.Register(CachedGetMethodOutput{})
|
||||
gob.Register([]apiGatewayTypes.UsagePlan{})
|
||||
gob.Register([]apiGatewayTypes.UsagePlanKey{})
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetRestAPIs function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination
|
||||
func CachedApiGatewayGetRestAPIs(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.RestApi, error) {
|
||||
var PaginationControl *string
|
||||
var restAPIs []apiGatewayTypes.RestApi
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetRestApis-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.RestApi), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetRestApis, err := client.GetRestApis(
|
||||
context.TODO(),
|
||||
&apigateway.GetRestApisInput{
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return restAPIs, err
|
||||
}
|
||||
|
||||
restAPIs = append(restAPIs, GetRestApis.Items...)
|
||||
|
||||
//pagination
|
||||
if GetRestApis.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetRestApis.Position
|
||||
}
|
||||
internal.Cache.Set(cacheKey, restAPIs, cache.DefaultExpiration)
|
||||
return restAPIs, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetStages function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
|
||||
func CachedApiGatewayGetStages(client APIGatewayClientInterface, accountID string, region string, restAPIID string) (*apigateway.GetStagesOutput, error) {
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetStages-%s-%s", accountID, region, restAPIID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
// Convert cached data back to GetStagesOutput before returning
|
||||
cachedOutput := cached.(*CachedGetStagesOutput) // Ensure this type assertion matches your caching logic
|
||||
return fromCachedGetStagesOutput(cachedOutput), nil
|
||||
}
|
||||
|
||||
GetStages, err := client.GetStages(
|
||||
context.TODO(),
|
||||
&apigateway.GetStagesInput{
|
||||
RestApiId: &restAPIID,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return &apigateway.GetStagesOutput{}, err
|
||||
}
|
||||
|
||||
// Convert GetStagesOutput to CachedGetStagesOutput before caching
|
||||
cachedVersion := toCachedGetStagesOutput(GetStages)
|
||||
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
|
||||
return GetStages, err
|
||||
}
|
||||
|
||||
// Convert from AWS SDK type to custom type for caching
|
||||
func toCachedGetStagesOutput(gso *apigateway.GetStagesOutput) *CachedGetStagesOutput {
|
||||
return &CachedGetStagesOutput{
|
||||
Item: gso.Item,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to AWS SDK type after fetching from cache
|
||||
func fromCachedGetStagesOutput(cgso *CachedGetStagesOutput) *apigateway.GetStagesOutput {
|
||||
return &apigateway.GetStagesOutput{
|
||||
Item: cgso.Item,
|
||||
// Initialize ResultMetadata or leave it as zero value if it's not required for your use case
|
||||
}
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetResources function that accepts a client, account id, region, and rest api id. Make sure it handles caching, the region option and pagination
|
||||
func CachedApiGatewayGetResources(client APIGatewayClientInterface, accountID string, region string, restAPIID string) ([]apiGatewayTypes.Resource, error) {
|
||||
var PaginationControl *string
|
||||
var resources []apiGatewayTypes.Resource
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetResources-%s-%s", accountID, region, restAPIID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.Resource), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetResources, err := client.GetResources(
|
||||
context.TODO(),
|
||||
&apigateway.GetResourcesInput{
|
||||
RestApiId: &restAPIID,
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return resources, err
|
||||
}
|
||||
|
||||
resources = append(resources, GetResources.Items...)
|
||||
|
||||
//pagination
|
||||
if GetResources.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetResources.Position
|
||||
}
|
||||
internal.Cache.Set(cacheKey, resources, cache.DefaultExpiration)
|
||||
return resources, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetDomainNames function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetDomainNames(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.DomainName, error) {
|
||||
var PaginationControl *string
|
||||
var domainNames []apiGatewayTypes.DomainName
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetDomainNames-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.DomainName), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetDomainNames, err := client.GetDomainNames(
|
||||
context.TODO(),
|
||||
&apigateway.GetDomainNamesInput{
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return domainNames, err
|
||||
}
|
||||
|
||||
domainNames = append(domainNames, GetDomainNames.Items...)
|
||||
|
||||
//pagination
|
||||
if GetDomainNames.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetDomainNames.Position
|
||||
}
|
||||
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
|
||||
return domainNames, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetBasePathMappings function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetBasePathMappings(client APIGatewayClientInterface, accountID string, region string, domain *string) ([]apiGatewayTypes.BasePathMapping, error) {
|
||||
var PaginationControl *string
|
||||
var basePathMappings []apiGatewayTypes.BasePathMapping
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetBasePathMappings-%s-%s", accountID, region, aws.ToString(domain))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.BasePathMapping), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetBasePathMappings, err := client.GetBasePathMappings(
|
||||
context.TODO(),
|
||||
&apigateway.GetBasePathMappingsInput{
|
||||
DomainName: domain,
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return basePathMappings, err
|
||||
}
|
||||
|
||||
basePathMappings = append(basePathMappings, GetBasePathMappings.Items...)
|
||||
|
||||
//pagination
|
||||
if GetBasePathMappings.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetBasePathMappings.Position
|
||||
}
|
||||
internal.Cache.Set(cacheKey, basePathMappings, cache.DefaultExpiration)
|
||||
return basePathMappings, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetMethod function that accepts a client, account id, region, rest api id, and resource id. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetMethod(client APIGatewayClientInterface, accountID string, region string, restAPIID string, resourceID string, method string) (*apigateway.GetMethodOutput, error) {
|
||||
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetMethod-%s-%s-%s-%s", accountID, region, restAPIID, resourceID, method)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
// Convert cached data back to GetMethodOutput before returning
|
||||
cachedOutput := cached.(*CachedGetMethodOutput) // Ensure this type assertion matches your caching logic
|
||||
return fromCachedGetMethodOutput(cachedOutput), nil
|
||||
}
|
||||
|
||||
GetMethod, err := client.GetMethod(
|
||||
context.TODO(),
|
||||
&apigateway.GetMethodInput{
|
||||
RestApiId: &restAPIID,
|
||||
ResourceId: &resourceID,
|
||||
HttpMethod: &method,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return &apigateway.GetMethodOutput{}, err
|
||||
}
|
||||
|
||||
// Convert GetMethodOutput to CachedGetMethodOutput before caching
|
||||
cachedVersion := toCachedGetMethodOutput(GetMethod)
|
||||
internal.Cache.Set(cacheKey, cachedVersion, cache.DefaultExpiration)
|
||||
return GetMethod, nil
|
||||
|
||||
}
|
||||
|
||||
// Convert from AWS SDK type to custom type for caching
|
||||
func toCachedGetMethodOutput(gmo *apigateway.GetMethodOutput) *CachedGetMethodOutput {
|
||||
return &CachedGetMethodOutput{
|
||||
ApiKeyRequired: gmo.ApiKeyRequired,
|
||||
AuthorizationScopes: gmo.AuthorizationScopes,
|
||||
AuthorizationType: gmo.AuthorizationType,
|
||||
AuthorizerId: gmo.AuthorizerId,
|
||||
HttpMethod: gmo.HttpMethod,
|
||||
MethodIntegration: gmo.MethodIntegration,
|
||||
MethodResponses: gmo.MethodResponses,
|
||||
OperationName: gmo.OperationName,
|
||||
RequestModels: gmo.RequestModels,
|
||||
RequestParameters: gmo.RequestParameters,
|
||||
RequestValidatorId: gmo.RequestValidatorId,
|
||||
}
|
||||
}
|
||||
|
||||
// Convert back to AWS SDK type after fetching from cache
|
||||
func fromCachedGetMethodOutput(cgmo *CachedGetMethodOutput) *apigateway.GetMethodOutput {
|
||||
return &apigateway.GetMethodOutput{
|
||||
ApiKeyRequired: cgmo.ApiKeyRequired,
|
||||
AuthorizationScopes: cgmo.AuthorizationScopes,
|
||||
AuthorizationType: cgmo.AuthorizationType,
|
||||
AuthorizerId: cgmo.AuthorizerId,
|
||||
HttpMethod: cgmo.HttpMethod,
|
||||
MethodIntegration: cgmo.MethodIntegration,
|
||||
MethodResponses: cgmo.MethodResponses,
|
||||
OperationName: cgmo.OperationName,
|
||||
RequestModels: cgmo.RequestModels,
|
||||
RequestParameters: cgmo.RequestParameters,
|
||||
RequestValidatorId: cgmo.RequestValidatorId,
|
||||
// ResultMetadata: This will be missing or zero value; handle accordingly
|
||||
}
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetUsagePlans function that accepts a client, account id, region. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetUsagePlans(client APIGatewayClientInterface, accountID string, region string) ([]apiGatewayTypes.UsagePlan, error) {
|
||||
var PaginationControl *string
|
||||
var usagePlans []apiGatewayTypes.UsagePlan
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetUsagePlans-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.UsagePlan), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetUsagePlans, err := client.GetUsagePlans(
|
||||
context.TODO(),
|
||||
&apigateway.GetUsagePlansInput{
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return usagePlans, err
|
||||
}
|
||||
|
||||
usagePlans = append(usagePlans, GetUsagePlans.Items...)
|
||||
|
||||
//pagination
|
||||
if GetUsagePlans.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetUsagePlans.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, usagePlans, cache.DefaultExpiration)
|
||||
return usagePlans, nil
|
||||
}
|
||||
|
||||
// create a CachedApiGatewayGetUsagePlanKeys function that accepts a client, account id, region, and usage plan id. Make sure it handles caching, the region option and pagination if needed
|
||||
func CachedApiGatewayGetUsagePlanKeys(client APIGatewayClientInterface, accountID string, region string, usagePlanID string) ([]apiGatewayTypes.UsagePlanKey, error) {
|
||||
var PaginationControl *string
|
||||
var usagePlanKeys []apiGatewayTypes.UsagePlanKey
|
||||
cacheKey := fmt.Sprintf("%s-apigateway-GetUsagePlanKeys-%s-%s", accountID, region, usagePlanID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatewayTypes.UsagePlanKey), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetUsagePlanKeys, err := client.GetUsagePlanKeys(
|
||||
context.TODO(),
|
||||
&apigateway.GetUsagePlanKeysInput{
|
||||
UsagePlanId: &usagePlanID,
|
||||
Position: PaginationControl,
|
||||
},
|
||||
func(o *apigateway.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return usagePlanKeys, err
|
||||
}
|
||||
|
||||
usagePlanKeys = append(usagePlanKeys, GetUsagePlanKeys.Items...)
|
||||
|
||||
//pagination
|
||||
if GetUsagePlanKeys.Position == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetUsagePlanKeys.Position
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, usagePlanKeys, cache.DefaultExpiration)
|
||||
return usagePlanKeys, nil
|
||||
}
|
||||
@@ -0,0 +1,142 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigateway"
|
||||
apiGatewayTypes "github.com/aws/aws-sdk-go-v2/service/apigateway/types"
|
||||
)
|
||||
|
||||
type MockedAWSAPIGatewayClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetRestApis(ctx context.Context, input *apigateway.GetRestApisInput, options ...func(*apigateway.Options)) (*apigateway.GetRestApisOutput, error) {
|
||||
return &apigateway.GetRestApisOutput{
|
||||
Items: []apiGatewayTypes.RestApi{
|
||||
{
|
||||
Id: aws.String("abcdefg"),
|
||||
Name: aws.String("api1"),
|
||||
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
|
||||
Types: []apiGatewayTypes.EndpointType{
|
||||
apiGatewayTypes.EndpointTypePrivate,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: aws.String("qwerty"),
|
||||
Name: aws.String("api2"),
|
||||
EndpointConfiguration: &apiGatewayTypes.EndpointConfiguration{
|
||||
Types: []apiGatewayTypes.EndpointType{
|
||||
apiGatewayTypes.EndpointTypeRegional,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetStages(ctx context.Context, input *apigateway.GetStagesInput, options ...func(*apigateway.Options)) (*apigateway.GetStagesOutput, error) {
|
||||
return &apigateway.GetStagesOutput{
|
||||
Item: []apiGatewayTypes.Stage{
|
||||
{
|
||||
StageName: aws.String("stage1"),
|
||||
},
|
||||
{
|
||||
StageName: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetResources(ctx context.Context, input *apigateway.GetResourcesInput, options ...func(*apigateway.Options)) (*apigateway.GetResourcesOutput, error) {
|
||||
return &apigateway.GetResourcesOutput{
|
||||
Items: []apiGatewayTypes.Resource{
|
||||
{
|
||||
Id: aws.String("resource1"),
|
||||
Path: aws.String("/path1"),
|
||||
ResourceMethods: map[string]apiGatewayTypes.Method{
|
||||
"GET": {
|
||||
ApiKeyRequired: aws.Bool(true),
|
||||
AuthorizerId: aws.String("authorizer1"),
|
||||
OperationName: aws.String("operation1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Id: aws.String("resource2"),
|
||||
Path: aws.String("/path2"),
|
||||
ResourceMethods: map[string]apiGatewayTypes.Method{
|
||||
"ANY": {
|
||||
ApiKeyRequired: aws.Bool(true),
|
||||
AuthorizerId: aws.String("authorizer2"),
|
||||
OperationName: aws.String("operation2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetDomainNames(ctx context.Context, input *apigateway.GetDomainNamesInput, options ...func(*apigateway.Options)) (*apigateway.GetDomainNamesOutput, error) {
|
||||
return &apigateway.GetDomainNamesOutput{
|
||||
Items: []apiGatewayTypes.DomainName{},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetBasePathMappings(ctx context.Context, input *apigateway.GetBasePathMappingsInput, options ...func(*apigateway.Options)) (*apigateway.GetBasePathMappingsOutput, error) {
|
||||
return &apigateway.GetBasePathMappingsOutput{
|
||||
Items: []apiGatewayTypes.BasePathMapping{
|
||||
{
|
||||
BasePath: aws.String("basepath1"),
|
||||
RestApiId: aws.String("abcdefg"),
|
||||
Stage: aws.String("stage1"),
|
||||
},
|
||||
{
|
||||
BasePath: aws.String("basepath2"),
|
||||
RestApiId: aws.String("qwerty"),
|
||||
Stage: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetMethod(ctx context.Context, input *apigateway.GetMethodInput, options ...func(*apigateway.Options)) (*apigateway.GetMethodOutput, error) {
|
||||
return &apigateway.GetMethodOutput{
|
||||
ApiKeyRequired: aws.Bool(true),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetUsagePlans(ctx context.Context, input *apigateway.GetUsagePlansInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlansOutput, error) {
|
||||
return &apigateway.GetUsagePlansOutput{
|
||||
Items: []apiGatewayTypes.UsagePlan{
|
||||
{
|
||||
Id: aws.String("usageplan1"),
|
||||
Name: aws.String("usageplan1"),
|
||||
ApiStages: []apiGatewayTypes.ApiStage{
|
||||
{
|
||||
ApiId: aws.String("abcdefg"),
|
||||
Stage: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayClient) GetUsagePlanKeys(ctx context.Context, input *apigateway.GetUsagePlanKeysInput, options ...func(*apigateway.Options)) (*apigateway.GetUsagePlanKeysOutput, error) {
|
||||
return &apigateway.GetUsagePlanKeysOutput{
|
||||
Items: []apiGatewayTypes.UsagePlanKey{
|
||||
{
|
||||
Id: aws.String("usageplankey1"),
|
||||
Type: aws.String("API_KEY"),
|
||||
Value: aws.String("23oieuwefo3rfs"),
|
||||
},
|
||||
{
|
||||
Id: aws.String("usageplankey2"),
|
||||
Type: aws.String("API_KEY"),
|
||||
Value: aws.String("982yf98fdv8dlds"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
|
||||
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type APIGatewayv2ClientInterface interface {
|
||||
GetApis(context.Context, *apigatewayv2.GetApisInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error)
|
||||
GetDomainNames(context.Context, *apigatewayv2.GetDomainNamesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error)
|
||||
GetApiMappings(context.Context, *apigatewayv2.GetApiMappingsInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error)
|
||||
GetStages(context.Context, *apigatewayv2.GetStagesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error)
|
||||
GetRoutes(context.Context, *apigatewayv2.GetRoutesInput, ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error)
|
||||
}
|
||||
|
||||
type domainName apiGatwayV2Types.DomainName
|
||||
type stage apiGatwayV2Types.Stage
|
||||
|
||||
func init() {
|
||||
gob.Register([]apiGatwayV2Types.Api{})
|
||||
//need to do this to avoid conflicts with the Instance type in the ec2 package
|
||||
gob.Register([]domainName{})
|
||||
gob.Register([]apiGatwayV2Types.ApiMapping{})
|
||||
gob.Register([]stage{})
|
||||
gob.Register([]apiGatwayV2Types.Route{})
|
||||
}
|
||||
|
||||
func CachedAPIGatewayv2GetAPIs(client APIGatewayv2ClientInterface, accountID string, region string) ([]apiGatwayV2Types.Api, error) {
|
||||
var PaginationControl *string
|
||||
var apis []apiGatwayV2Types.Api
|
||||
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApis-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatwayV2Types.Api), nil
|
||||
}
|
||||
for {
|
||||
GetApis, err := client.GetApis(
|
||||
context.TODO(),
|
||||
&apigatewayv2.GetApisInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apigatewayv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return apis, err
|
||||
}
|
||||
|
||||
apis = append(apis, GetApis.Items...)
|
||||
|
||||
//pagination
|
||||
if GetApis.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetApis.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, apis, cache.DefaultExpiration)
|
||||
return apis, nil
|
||||
}
|
||||
|
||||
func CachedAPIGatewayv2GetDomainNames(client APIGatewayv2ClientInterface, accountID string, region string) ([]domainName, error) {
|
||||
var PaginationControl *string
|
||||
var domainNames []domainName
|
||||
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetDomainNames-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]domainName), nil
|
||||
}
|
||||
for {
|
||||
GetDomainNames, err := client.GetDomainNames(
|
||||
context.TODO(),
|
||||
&apigatewayv2.GetDomainNamesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apigatewayv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return domainNames, err
|
||||
}
|
||||
// convert []apiGatwayV2Types.DomainName to []domainName
|
||||
|
||||
// Convert each element to domainName before appending to avoid conflicting with apigateway's domainname type
|
||||
for _, item := range GetDomainNames.Items {
|
||||
domainNames = append(domainNames, domainName(item))
|
||||
}
|
||||
|
||||
//pagination
|
||||
if GetDomainNames.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetDomainNames.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, domainNames, cache.DefaultExpiration)
|
||||
return domainNames, nil
|
||||
}
|
||||
|
||||
func CachedAPIGatewayv2GetApiMappings(client APIGatewayv2ClientInterface, accountID string, region string, domain string) ([]apiGatwayV2Types.ApiMapping, error) {
|
||||
var PaginationControl *string
|
||||
var apiMappings []apiGatwayV2Types.ApiMapping
|
||||
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetApiMappings-%s-%s", accountID, region, domain)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatwayV2Types.ApiMapping), nil
|
||||
}
|
||||
for {
|
||||
GetApiMappings, err := client.GetApiMappings(
|
||||
context.TODO(),
|
||||
&apigatewayv2.GetApiMappingsInput{
|
||||
DomainName: &domain,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apigatewayv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return apiMappings, err
|
||||
}
|
||||
|
||||
apiMappings = append(apiMappings, GetApiMappings.Items...)
|
||||
|
||||
//pagination
|
||||
if GetApiMappings.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetApiMappings.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, apiMappings, cache.DefaultExpiration)
|
||||
return apiMappings, nil
|
||||
}
|
||||
|
||||
func CachedAPIGatewayv2GetStages(client APIGatewayv2ClientInterface, accountID string, region string, apiID string) ([]apiGatwayV2Types.Stage, error) {
|
||||
var PaginationControl *string
|
||||
var stages []apiGatwayV2Types.Stage
|
||||
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetStages-%s-%s", accountID, region, apiID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatwayV2Types.Stage), nil
|
||||
}
|
||||
for {
|
||||
GetStages, err := client.GetStages(
|
||||
context.TODO(),
|
||||
&apigatewayv2.GetStagesInput{
|
||||
ApiId: &apiID,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apigatewayv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stages, err
|
||||
}
|
||||
|
||||
stages = append(stages, GetStages.Items...)
|
||||
|
||||
//pagination
|
||||
if GetStages.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetStages.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, stages, cache.DefaultExpiration)
|
||||
return stages, nil
|
||||
}
|
||||
|
||||
func CachedAPIGatewayv2GetRoutes(client APIGatewayv2ClientInterface, accountID string, region string, apiID string) ([]apiGatwayV2Types.Route, error) {
|
||||
var PaginationControl *string
|
||||
var routes []apiGatwayV2Types.Route
|
||||
cacheKey := fmt.Sprintf("%s-apigatewayv2-GetRoutes-%s-%s", accountID, region, apiID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apiGatwayV2Types.Route), nil
|
||||
}
|
||||
for {
|
||||
GetRoutes, err := client.GetRoutes(
|
||||
context.TODO(),
|
||||
&apigatewayv2.GetRoutesInput{
|
||||
ApiId: &apiID,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apigatewayv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return routes, err
|
||||
}
|
||||
|
||||
routes = append(routes, GetRoutes.Items...)
|
||||
|
||||
//pagination
|
||||
if GetRoutes.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = GetRoutes.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, routes, cache.DefaultExpiration)
|
||||
return routes, nil
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apigatewayv2"
|
||||
apiGatwayV2Types "github.com/aws/aws-sdk-go-v2/service/apigatewayv2/types"
|
||||
)
|
||||
|
||||
type MockedAWSAPIGatewayv2Client struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetApis(ctx context.Context, input *apigatewayv2.GetApisInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApisOutput, error) {
|
||||
return &apigatewayv2.GetApisOutput{
|
||||
Items: []apiGatwayV2Types.Api{
|
||||
{
|
||||
ApiId: aws.String("asdfsdfasdf"),
|
||||
Name: aws.String("api1"),
|
||||
ApiEndpoint: aws.String("https://asdfsdfasdf.execute-api.us-east-1.amazonaws.com"),
|
||||
},
|
||||
{
|
||||
ApiId: aws.String("qwertyqwerty"),
|
||||
Name: aws.String("api2"),
|
||||
ApiEndpoint: aws.String("https://qwertyqwerty.execute-api.us-east-1.amazonaws.com"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetDomainNames(ctx context.Context, input *apigatewayv2.GetDomainNamesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetDomainNamesOutput, error) {
|
||||
return &apigatewayv2.GetDomainNamesOutput{
|
||||
Items: []apiGatwayV2Types.DomainName{
|
||||
{
|
||||
DomainName: aws.String("domain1"),
|
||||
},
|
||||
{
|
||||
DomainName: aws.String("domain2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetApiMappings(ctx context.Context, input *apigatewayv2.GetApiMappingsInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetApiMappingsOutput, error) {
|
||||
return &apigatewayv2.GetApiMappingsOutput{
|
||||
Items: []apiGatwayV2Types.ApiMapping{
|
||||
{
|
||||
ApiMappingId: aws.String("apiMapping1"),
|
||||
},
|
||||
{
|
||||
ApiMappingId: aws.String("apiMapping2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetStages(ctx context.Context, input *apigatewayv2.GetStagesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetStagesOutput, error) {
|
||||
return &apigatewayv2.GetStagesOutput{
|
||||
Items: []apiGatwayV2Types.Stage{
|
||||
{
|
||||
StageName: aws.String("stage1"),
|
||||
},
|
||||
{
|
||||
StageName: aws.String("stage2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAPIGatewayv2Client) GetRoutes(ctx context.Context, input *apigatewayv2.GetRoutesInput, options ...func(*apigatewayv2.Options)) (*apigatewayv2.GetRoutesOutput, error) {
|
||||
return &apigatewayv2.GetRoutesOutput{
|
||||
Items: []apiGatwayV2Types.Route{
|
||||
{
|
||||
RouteId: aws.String("route1"),
|
||||
RouteKey: aws.String("POST /route1"),
|
||||
},
|
||||
{
|
||||
RouteId: aws.String("route2"),
|
||||
RouteKey: aws.String("GET /route2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apprunner"
|
||||
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AppRunnerClientInterface interface {
|
||||
ListServices(context.Context, *apprunner.ListServicesInput, ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]apprunnerTypes.Service{})
|
||||
gob.Register([]apprunnerTypes.ServiceSummary{})
|
||||
}
|
||||
|
||||
func CachedAppRunnerListServices(client AppRunnerClientInterface, accountID string, region string) ([]apprunnerTypes.ServiceSummary, error) {
|
||||
var PaginationControl *string
|
||||
var services []apprunnerTypes.ServiceSummary
|
||||
cacheKey := fmt.Sprintf("%s-apprunner-ListServices-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]apprunnerTypes.ServiceSummary), nil
|
||||
}
|
||||
for {
|
||||
ListServices, err := client.ListServices(
|
||||
context.TODO(),
|
||||
&apprunner.ListServicesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *apprunner.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return services, err
|
||||
}
|
||||
|
||||
services = append(services, ListServices.ServiceSummaryList...)
|
||||
|
||||
//pagination
|
||||
if ListServices.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListServices.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
|
||||
return services, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/apprunner"
|
||||
apprunnerTypes "github.com/aws/aws-sdk-go-v2/service/apprunner/types"
|
||||
)
|
||||
|
||||
type MockedAppRunnerClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAppRunnerClient) ListServices(ctx context.Context, input *apprunner.ListServicesInput, options ...func(*apprunner.Options)) (*apprunner.ListServicesOutput, error) {
|
||||
return &apprunner.ListServicesOutput{
|
||||
ServiceSummaryList: []apprunnerTypes.ServiceSummary{
|
||||
{
|
||||
ServiceName: aws.String("service1"),
|
||||
},
|
||||
{
|
||||
ServiceName: aws.String("service2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/athena"
|
||||
athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSAthenaClientInterface interface {
|
||||
ListDataCatalogs(context.Context, *athena.ListDataCatalogsInput, ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error)
|
||||
ListDatabases(context.Context, *athena.ListDatabasesInput, ...func(*athena.Options)) (*athena.ListDatabasesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]athenaTypes.DataCatalogSummary{})
|
||||
}
|
||||
|
||||
func CachedAthenaListDataCatalogs(client AWSAthenaClientInterface, accountID string, region string) ([]athenaTypes.DataCatalogSummary, error) {
|
||||
var PaginationControl *string
|
||||
var dataCatalogs []athenaTypes.DataCatalogSummary
|
||||
cacheKey := fmt.Sprintf("%s-athena-ListDataCatalogs-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]athenaTypes.DataCatalogSummary), nil
|
||||
}
|
||||
for {
|
||||
ListDataCatalogs, err := client.ListDataCatalogs(
|
||||
context.TODO(),
|
||||
&athena.ListDataCatalogsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *athena.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dataCatalogs, err
|
||||
}
|
||||
|
||||
dataCatalogs = append(dataCatalogs, ListDataCatalogs.DataCatalogsSummary...)
|
||||
|
||||
//pagination
|
||||
if ListDataCatalogs.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListDataCatalogs.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, dataCatalogs, cache.DefaultExpiration)
|
||||
return dataCatalogs, nil
|
||||
}
|
||||
|
||||
func CachedAthenaListDatabases(client AWSAthenaClientInterface, accountID string, region string, catalogName string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var databases []string
|
||||
cacheKey := fmt.Sprintf("%s-athena-ListDatabases-%s-%s", accountID, region, catalogName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListDatabases, err := client.ListDatabases(
|
||||
context.TODO(),
|
||||
&athena.ListDatabasesInput{
|
||||
CatalogName: &catalogName,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *athena.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return databases, err
|
||||
}
|
||||
|
||||
for _, database := range ListDatabases.DatabaseList {
|
||||
databases = append(databases, *database.Name)
|
||||
}
|
||||
|
||||
//pagination
|
||||
if ListDatabases.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListDatabases.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, databases, cache.DefaultExpiration)
|
||||
return databases, nil
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/athena"
|
||||
athenaTypes "github.com/aws/aws-sdk-go-v2/service/athena/types"
|
||||
)
|
||||
|
||||
type MockedAWSAthenaClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSAthenaClient) ListDatabases(ctx context.Context, input *athena.ListDatabasesInput, options ...func(*athena.Options)) (*athena.ListDatabasesOutput, error) {
|
||||
return &athena.ListDatabasesOutput{
|
||||
DatabaseList: []athenaTypes.Database{
|
||||
{
|
||||
Name: aws.String("db1"),
|
||||
},
|
||||
{
|
||||
Name: aws.String("db2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSAthenaClient) ListDataCatalogs(ctx context.Context, input *athena.ListDataCatalogsInput, options ...func(*athena.Options)) (*athena.ListDataCatalogsOutput, error) {
|
||||
return &athena.ListDataCatalogsOutput{
|
||||
DataCatalogsSummary: []athenaTypes.DataCatalogSummary{
|
||||
{
|
||||
CatalogName: aws.String("catalog1"),
|
||||
},
|
||||
{
|
||||
CatalogName: aws.String("catalog2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloud9"
|
||||
cloud9Types "github.com/aws/aws-sdk-go-v2/service/cloud9/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSCloud9ClientInterface interface {
|
||||
ListEnvironments(context.Context, *cloud9.ListEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error)
|
||||
DescribeEnvironments(context.Context, *cloud9.DescribeEnvironmentsInput, ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
}
|
||||
|
||||
func CachedCloud9ListEnvironments(client AWSCloud9ClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var environments []string
|
||||
cacheKey := "cloud9-ListEnvironments-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListEnvironments, err := client.ListEnvironments(
|
||||
context.TODO(),
|
||||
&cloud9.ListEnvironmentsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloud9.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return environments, err
|
||||
}
|
||||
|
||||
environments = append(environments, ListEnvironments.EnvironmentIds...)
|
||||
|
||||
//pagination
|
||||
if ListEnvironments.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListEnvironments.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration)
|
||||
return environments, nil
|
||||
}
|
||||
|
||||
func CachedCloud9DescribeEnvironments(client AWSCloud9ClientInterface, accountID string, region string, environmentIDs []string) ([]cloud9Types.Environment, error) {
|
||||
var environments []cloud9Types.Environment
|
||||
cacheKey := "cloud9-DescribeEnvironments-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]cloud9Types.Environment), nil
|
||||
}
|
||||
for _, environmentID := range environmentIDs {
|
||||
DescribeEnvironments, err := client.DescribeEnvironments(
|
||||
context.TODO(),
|
||||
&cloud9.DescribeEnvironmentsInput{
|
||||
EnvironmentIds: []string{environmentID},
|
||||
},
|
||||
func(o *cloud9.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return environments, err
|
||||
}
|
||||
|
||||
environments = append(environments, DescribeEnvironments.Environments...)
|
||||
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, environments, cache.DefaultExpiration)
|
||||
return environments, nil
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloud9"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloud9/types"
|
||||
)
|
||||
|
||||
type MockedAWSCloud9Client struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSCloud9Client) ListEnvironments(ctx context.Context, input *cloud9.ListEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.ListEnvironmentsOutput, error) {
|
||||
return &cloud9.ListEnvironmentsOutput{
|
||||
EnvironmentIds: []string{
|
||||
"env1",
|
||||
"env2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSCloud9Client) DescribeEnvironments(ctx context.Context, input *cloud9.DescribeEnvironmentsInput, options ...func(*cloud9.Options)) (*cloud9.DescribeEnvironmentsOutput, error) {
|
||||
return &cloud9.DescribeEnvironmentsOutput{
|
||||
Environments: []types.Environment{
|
||||
{
|
||||
Name: aws.String("env1"),
|
||||
Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env1"),
|
||||
},
|
||||
{
|
||||
Name: aws.String("env2"),
|
||||
Arn: aws.String("arn:aws:cloud9:us-east-1:123456789012:environment/env2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
|
||||
cloudFormationTypes "github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type CloudFormationClientInterface interface {
|
||||
GetTemplate(context.Context, *cloudformation.GetTemplateInput, ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error)
|
||||
DescribeStacks(context.Context, *cloudformation.DescribeStacksInput, ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error)
|
||||
ListStacks(context.Context, *cloudformation.ListStacksInput, ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]cloudFormationTypes.Stack{})
|
||||
gob.Register([]cloudFormationTypes.StackSummary{})
|
||||
}
|
||||
|
||||
func CachedCloudFormationDescribeStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.Stack, error) {
|
||||
var PaginationControl *string
|
||||
var stacks []cloudFormationTypes.Stack
|
||||
cacheKey := fmt.Sprintf("%s-cloudformation-DescribeStacks-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]cloudFormationTypes.Stack), nil
|
||||
}
|
||||
for {
|
||||
DescribeStacks, err := client.DescribeStacks(
|
||||
context.TODO(),
|
||||
&cloudformation.DescribeStacksInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
stacks = append(stacks, DescribeStacks.Stacks...)
|
||||
//pagination
|
||||
if DescribeStacks.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeStacks.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
|
||||
return stacks, nil
|
||||
}
|
||||
|
||||
func CachedCloudFormationGetTemplate(client CloudFormationClientInterface, accountID string, region string, stackName string) (string, error) {
|
||||
var stackTemplateBody string
|
||||
cacheKey := fmt.Sprintf("%s-cloudformation-GetTemplate-%s-%s", accountID, region, stackName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
GetTemplate, err := client.GetTemplate(
|
||||
context.TODO(),
|
||||
&cloudformation.GetTemplateInput{
|
||||
StackName: &stackName,
|
||||
}, func(o *cloudformation.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stackTemplateBody, err
|
||||
}
|
||||
stackTemplateBody = aws.ToString(GetTemplate.TemplateBody)
|
||||
internal.Cache.Set(cacheKey, *GetTemplate.TemplateBody, cache.DefaultExpiration)
|
||||
return stackTemplateBody, nil
|
||||
}
|
||||
|
||||
func CachedCloudFormationListStacks(client CloudFormationClientInterface, accountID string, region string) ([]cloudFormationTypes.StackSummary, error) {
|
||||
var PaginationControl *string
|
||||
var stacks []cloudFormationTypes.StackSummary
|
||||
cacheKey := fmt.Sprintf("%s-cloudformation-ListStacks-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]cloudFormationTypes.StackSummary), nil
|
||||
}
|
||||
for {
|
||||
ListStacks, err := client.ListStacks(
|
||||
context.TODO(),
|
||||
&cloudformation.ListStacksInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *cloudformation.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return stacks, err
|
||||
}
|
||||
|
||||
stacks = append(stacks, ListStacks.StackSummaries...)
|
||||
//pagination
|
||||
if ListStacks.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListStacks.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, stacks, cache.DefaultExpiration)
|
||||
return stacks, nil
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudformation/types"
|
||||
)
|
||||
|
||||
const DESCRIBE_STACKS_TEST_FILE = "./test-data/cloudformation-describestacks.json"
|
||||
const TEMPLATE_BODY_TEST_FILE = "./test-data/cloudformation-getTemplate.json"
|
||||
|
||||
type Stacks struct {
|
||||
Stacks []struct {
|
||||
StackID string `json:"StackId"`
|
||||
Description string `json:"Description"`
|
||||
Tags []interface{} `json:"Tags"`
|
||||
Outputs []types.Output `json:"Outputs"`
|
||||
Parameters []types.Parameter `json:"Parameters"`
|
||||
StackStatusReason interface{} `json:"StackStatusReason"`
|
||||
CreationTime time.Time `json:"CreationTime"`
|
||||
Capabilities []interface{} `json:"Capabilities"`
|
||||
StackName string `json:"StackName"`
|
||||
RoleArn string `json:"RoleArn"`
|
||||
StackStatus string `json:"StackStatus"`
|
||||
DisableRollback bool `json:"DisableRollback"`
|
||||
} `json:"Stacks"`
|
||||
}
|
||||
|
||||
type TemplateBody struct {
|
||||
TemplateBody string `json:"TemplateBody"`
|
||||
}
|
||||
|
||||
type MockedCloudformationClient struct {
|
||||
describeStacks Stacks
|
||||
getTemplateBody TemplateBody
|
||||
}
|
||||
|
||||
func (m *MockedCloudformationClient) DescribeStacks(ctx context.Context, params *cloudformation.DescribeStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.DescribeStacksOutput, error) {
|
||||
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.describeStacks)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_STACKS_TEST_FILE)
|
||||
}
|
||||
var stacks []types.Stack
|
||||
for _, stack := range m.describeStacks.Stacks {
|
||||
stacks = append(stacks, types.Stack{
|
||||
StackName: &stack.StackName,
|
||||
RoleARN: &stack.RoleArn,
|
||||
Outputs: stack.Outputs,
|
||||
Parameters: stack.Parameters,
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
return &cloudformation.DescribeStacksOutput{Stacks: stacks}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCloudformationClient) GetTemplate(ctx context.Context, params *cloudformation.GetTemplateInput, optFns ...func(*cloudformation.Options)) (*cloudformation.GetTemplateOutput, error) {
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_STACKS_TEST_FILE), &m.getTemplateBody)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", TEMPLATE_BODY_TEST_FILE)
|
||||
}
|
||||
|
||||
return &cloudformation.GetTemplateOutput{TemplateBody: &m.getTemplateBody.TemplateBody}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCloudformationClient) ListStacks(ctx context.Context, params *cloudformation.ListStacksInput, optFns ...func(*cloudformation.Options)) (*cloudformation.ListStacksOutput, error) {
|
||||
return &cloudformation.ListStacksOutput{}, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
|
||||
cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSCloudFrontClientInterface interface {
|
||||
ListDistributions(ctx context.Context, params *cloudfront.ListDistributionsInput, optFns ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]cloudfrontTypes.DistributionSummary{})
|
||||
gob.Register(cloudfrontTypes.DistributionSummary{})
|
||||
}
|
||||
|
||||
func CachedCloudFrontListDistributions(CloudFrontClient AWSCloudFrontClientInterface, accountID string) ([]cloudfrontTypes.DistributionSummary, error) {
|
||||
var PaginationControl *string
|
||||
var distributions []cloudfrontTypes.DistributionSummary
|
||||
cacheKey := "cloudfront-ListDistributions-" + accountID
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached CloudFront distributions data")
|
||||
return cached.([]cloudfrontTypes.DistributionSummary), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListDistributions, err := CloudFrontClient.ListDistributions(
|
||||
context.TODO(),
|
||||
&cloudfront.ListDistributionsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
distributions = append(distributions, ListDistributions.DistributionList.Items...)
|
||||
|
||||
// Pagination control.
|
||||
if ListDistributions.DistributionList.NextMarker != nil {
|
||||
PaginationControl = ListDistributions.DistributionList.NextMarker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, distributions, cache.DefaultExpiration)
|
||||
return distributions, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/cloudfront"
|
||||
cloudfrontTypes "github.com/aws/aws-sdk-go-v2/service/cloudfront/types"
|
||||
)
|
||||
|
||||
type MockedAWSCloudFrontClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSCloudFrontClient) ListDistributions(ctx context.Context, input *cloudfront.ListDistributionsInput, options ...func(*cloudfront.Options)) (*cloudfront.ListDistributionsOutput, error) {
|
||||
return &cloudfront.ListDistributionsOutput{
|
||||
DistributionList: &cloudfrontTypes.DistributionList{
|
||||
Items: []cloudfrontTypes.DistributionSummary{
|
||||
{
|
||||
Id: aws.String("distribution1"),
|
||||
},
|
||||
{
|
||||
Id: aws.String("distribution2"),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codeartifact"
|
||||
codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSCodeArtifactClientInterface interface {
|
||||
ListDomains(context.Context, *codeartifact.ListDomainsInput, ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error)
|
||||
ListRepositories(context.Context, *codeartifact.ListRepositoriesInput, ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]codeArtifactTypes.DomainSummary{})
|
||||
gob.Register([]codeArtifactTypes.RepositorySummary{})
|
||||
}
|
||||
|
||||
func CachedCodeArtifactListDomains(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.DomainSummary, error) {
|
||||
var PaginationControl *string
|
||||
var domains []codeArtifactTypes.DomainSummary
|
||||
cacheKey := fmt.Sprintf("%s-codeartifact-ListDomains-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]codeArtifactTypes.DomainSummary), nil
|
||||
}
|
||||
for {
|
||||
ListDomains, err := client.ListDomains(
|
||||
context.TODO(),
|
||||
&codeartifact.ListDomainsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codeartifact.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return domains, err
|
||||
}
|
||||
|
||||
domains = append(domains, ListDomains.Domains...)
|
||||
|
||||
//pagination
|
||||
if ListDomains.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListDomains.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, domains, cache.DefaultExpiration)
|
||||
return domains, nil
|
||||
}
|
||||
|
||||
func CachedCodeArtifactListRepositories(client AWSCodeArtifactClientInterface, accountID string, region string) ([]codeArtifactTypes.RepositorySummary, error) {
|
||||
var PaginationControl *string
|
||||
var repositories []codeArtifactTypes.RepositorySummary
|
||||
cacheKey := fmt.Sprintf("%s-codeartifact-ListRepositories-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]codeArtifactTypes.RepositorySummary), nil
|
||||
}
|
||||
for {
|
||||
ListRepositories, err := client.ListRepositories(
|
||||
context.TODO(),
|
||||
&codeartifact.ListRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codeartifact.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return repositories, err
|
||||
}
|
||||
|
||||
repositories = append(repositories, ListRepositories.Repositories...)
|
||||
|
||||
//pagination
|
||||
if ListRepositories.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListRepositories.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
|
||||
return repositories, nil
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codeartifact"
|
||||
codeArtifactTypes "github.com/aws/aws-sdk-go-v2/service/codeartifact/types"
|
||||
)
|
||||
|
||||
type MockedAWSCodeArtifactClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSCodeArtifactClient) ListRepositories(ctx context.Context, input *codeartifact.ListRepositoriesInput, options ...func(*codeartifact.Options)) (*codeartifact.ListRepositoriesOutput, error) {
|
||||
return &codeartifact.ListRepositoriesOutput{
|
||||
NextToken: nil,
|
||||
Repositories: []codeArtifactTypes.RepositorySummary{
|
||||
{
|
||||
Name: aws.String("repo1"),
|
||||
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo1"),
|
||||
DomainName: aws.String("domain1"),
|
||||
},
|
||||
{
|
||||
Name: aws.String("repo2"),
|
||||
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:repository/repo2"),
|
||||
DomainName: aws.String("domain1"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSCodeArtifactClient) ListDomains(ctx context.Context, input *codeartifact.ListDomainsInput, options ...func(*codeartifact.Options)) (*codeartifact.ListDomainsOutput, error) {
|
||||
return &codeartifact.ListDomainsOutput{
|
||||
NextToken: nil,
|
||||
Domains: []codeArtifactTypes.DomainSummary{
|
||||
{
|
||||
Name: aws.String("domain1"),
|
||||
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain1"),
|
||||
},
|
||||
{
|
||||
Name: aws.String("domain2"),
|
||||
Arn: aws.String("arn:aws:codeartifact:us-east-1:123456789012:domain/domain2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
codeBuildTypes "github.com/aws/aws-sdk-go-v2/service/codebuild/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type CodeBuildClientInterface interface {
|
||||
ListProjects(ctx context.Context, params *codebuild.ListProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error)
|
||||
BatchGetProjects(ctx context.Context, params *codebuild.BatchGetProjectsInput, optFns ...func(*codebuild.Options)) (*codebuild.BatchGetProjectsOutput, error)
|
||||
GetResourcePolicy(ctx context.Context, params *codebuild.GetResourcePolicyInput, optFns ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register(codeBuildTypes.Project{})
|
||||
|
||||
}
|
||||
|
||||
// create a CachedCodeBuildListProjects function that accepts a codebuild client, account id, and region. Make sure it handles the region option and pagination
|
||||
func CachedCodeBuildListProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var projects []string
|
||||
cacheKey := fmt.Sprintf("%s-codebuild-ListProjects", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListProjects, err := CodeBuildClient.ListProjects(
|
||||
context.TODO(),
|
||||
&codebuild.ListProjectsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codebuild.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
projects = append(projects, ListProjects.Projects...)
|
||||
|
||||
// Pagination control.
|
||||
if ListProjects.NextToken != nil {
|
||||
PaginationControl = ListProjects.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, projects, cache.DefaultExpiration)
|
||||
return projects, nil
|
||||
|
||||
}
|
||||
|
||||
// create a CachedCodeBuildBatchGetProjects function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
|
||||
func CachedCodeBuildBatchGetProjects(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (codeBuildTypes.Project, error) {
|
||||
project := codeBuildTypes.Project{}
|
||||
cacheKey := fmt.Sprintf("%s-codebuild-BatchGetProjects-%s-%s", accountID, region, projectID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(codeBuildTypes.Project), nil
|
||||
}
|
||||
|
||||
BatchGetProjects, err := CodeBuildClient.BatchGetProjects(
|
||||
context.TODO(),
|
||||
&codebuild.BatchGetProjectsInput{
|
||||
Names: []string{projectID},
|
||||
},
|
||||
func(o *codebuild.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return project, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, BatchGetProjects.Projects, cache.DefaultExpiration)
|
||||
return BatchGetProjects.Projects[0], nil
|
||||
}
|
||||
|
||||
// create a CachedCodeBuildGetResourcePolicy function that accepts a codebuild client, account id, region, and a single projectId. Make sure it handles the region option and pagination
|
||||
func CachedCodeBuildGetResourcePolicy(CodeBuildClient CodeBuildClientInterface, accountID string, region string, projectID string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s-codebuild-GetResourcePolicy-%s-%s", accountID, region, projectID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
GetResourcePolicy, err := CodeBuildClient.GetResourcePolicy(
|
||||
context.TODO(),
|
||||
&codebuild.GetResourcePolicyInput{
|
||||
ResourceArn: aws.String("arn:aws:codebuild:" + region + ":" + accountID + ":project/" + projectID),
|
||||
},
|
||||
func(o *codebuild.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, GetResourcePolicy, cache.DefaultExpiration)
|
||||
return aws.ToString(GetResourcePolicy.Policy), nil
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codebuild"
|
||||
)
|
||||
|
||||
type MockedCodeBuildClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedCodeBuildClient) ListProjects(ctx context.Context, input *codebuild.ListProjectsInput, options ...func(*codebuild.Options)) (*codebuild.ListProjectsOutput, error) {
|
||||
return &codebuild.ListProjectsOutput{
|
||||
Projects: []string{
|
||||
"project1",
|
||||
"project2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCodeBuildClient) GetResourcePolicy(ctx context.Context, input *codebuild.GetResourcePolicyInput, options ...func(*codebuild.Options)) (*codebuild.GetResourcePolicyOutput, error) {
|
||||
return &codebuild.GetResourcePolicyOutput{
|
||||
Policy: aws.String(`{
|
||||
"Version": "2012-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Effect": "Allow",
|
||||
"Action": "codebuild:BatchGetProjects",
|
||||
"Resource": "*",
|
||||
"Principal": {
|
||||
"AWS": "arn:aws:iam::123456789012:root"
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
`),
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codecommit"
|
||||
codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSCodeCommitClientInterface interface {
|
||||
ListRepositories(context.Context, *codecommit.ListRepositoriesInput, ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]codeCommitTypes.RepositoryNameIdPair{})
|
||||
}
|
||||
|
||||
func CachedCodeCommitListRepositories(client AWSCodeCommitClientInterface, accountID string, region string) ([]codeCommitTypes.RepositoryNameIdPair, error) {
|
||||
var PaginationControl *string
|
||||
var repositories []codeCommitTypes.RepositoryNameIdPair
|
||||
cacheKey := fmt.Sprintf("%s-codecommit-ListRepositories-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]codeCommitTypes.RepositoryNameIdPair), nil
|
||||
}
|
||||
for {
|
||||
ListRepositories, err := client.ListRepositories(
|
||||
context.TODO(),
|
||||
&codecommit.ListRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codecommit.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return repositories, err
|
||||
}
|
||||
|
||||
repositories = append(repositories, ListRepositories.Repositories...)
|
||||
|
||||
//pagination
|
||||
if ListRepositories.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListRepositories.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
|
||||
return repositories, nil
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codecommit"
|
||||
codeCommitTypes "github.com/aws/aws-sdk-go-v2/service/codecommit/types"
|
||||
)
|
||||
|
||||
// create mocks for codecommit ListRepositories
|
||||
|
||||
type MockedAWSCodeCommitClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSCodeCommitClient) ListRepositories(ctx context.Context, input *codecommit.ListRepositoriesInput, options ...func(*codecommit.Options)) (*codecommit.ListRepositoriesOutput, error) {
|
||||
return &codecommit.ListRepositoriesOutput{
|
||||
NextToken: nil,
|
||||
Repositories: []codeCommitTypes.RepositoryNameIdPair{
|
||||
{
|
||||
RepositoryId: aws.String("repo1"),
|
||||
RepositoryName: aws.String("repo1"),
|
||||
},
|
||||
{
|
||||
RepositoryId: aws.String("repo2"),
|
||||
RepositoryName: aws.String("repo2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
|
||||
codeDeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSCodeDeployClientInterface interface {
|
||||
ListApplications(context.Context, *codedeploy.ListApplicationsInput, ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error)
|
||||
GetApplication(context.Context, *codedeploy.GetApplicationInput, ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error)
|
||||
ListDeployments(context.Context, *codedeploy.ListDeploymentsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error)
|
||||
GetDeployment(context.Context, *codedeploy.GetDeploymentInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error)
|
||||
ListDeploymentConfigs(context.Context, *codedeploy.ListDeploymentConfigsInput, ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error)
|
||||
GetDeploymentConfig(context.Context, *codedeploy.GetDeploymentConfigInput, ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
gob.Register([]codeDeployTypes.ApplicationInfo{})
|
||||
gob.Register([]codeDeployTypes.DeploymentConfigInfo{})
|
||||
gob.Register([]codeDeployTypes.DeploymentInfo{})
|
||||
|
||||
}
|
||||
|
||||
func CachedCodeDeployListApplications(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var applications []string
|
||||
cacheKey := fmt.Sprintf("%s-codedeploy-ListApplications-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListApplications, err := client.ListApplications(
|
||||
context.TODO(),
|
||||
&codedeploy.ListApplicationsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codedeploy.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return applications, err
|
||||
}
|
||||
|
||||
applications = append(applications, ListApplications.Applications...)
|
||||
|
||||
//pagination
|
||||
if ListApplications.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListApplications.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, applications, cache.DefaultExpiration)
|
||||
return applications, nil
|
||||
}
|
||||
|
||||
func CachedCodeDeployListDeploymentConfigs(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var deploymentConfigs []string
|
||||
cacheKey := fmt.Sprintf("%s-codedeploy-ListDeploymentConfigs-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListDeploymentConfigs, err := client.ListDeploymentConfigs(
|
||||
context.TODO(),
|
||||
&codedeploy.ListDeploymentConfigsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codedeploy.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return deploymentConfigs, err
|
||||
}
|
||||
|
||||
deploymentConfigs = append(deploymentConfigs, ListDeploymentConfigs.DeploymentConfigsList...)
|
||||
|
||||
//pagination
|
||||
if ListDeploymentConfigs.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListDeploymentConfigs.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, deploymentConfigs, cache.DefaultExpiration)
|
||||
return deploymentConfigs, nil
|
||||
}
|
||||
|
||||
func CachedCodeDeployListDeployments(client AWSCodeDeployClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var deployments []string
|
||||
cacheKey := fmt.Sprintf("%s-codedeploy-ListDeployments-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListDeployments, err := client.ListDeployments(
|
||||
context.TODO(),
|
||||
&codedeploy.ListDeploymentsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *codedeploy.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return deployments, err
|
||||
}
|
||||
|
||||
deployments = append(deployments, ListDeployments.Deployments...)
|
||||
|
||||
//pagination
|
||||
if ListDeployments.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListDeployments.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, deployments, cache.DefaultExpiration)
|
||||
return deployments, nil
|
||||
}
|
||||
|
||||
func CachedCodeDeployGetApplication(client AWSCodeDeployClientInterface, accountID string, region string, applicationName string) (codeDeployTypes.ApplicationInfo, error) {
|
||||
cacheKey := fmt.Sprintf("%s-codedeploy-GetApplication-%s-%s", accountID, region, applicationName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(codeDeployTypes.ApplicationInfo), nil
|
||||
}
|
||||
GetApplication, err := client.GetApplication(
|
||||
context.TODO(),
|
||||
&codedeploy.GetApplicationInput{
|
||||
ApplicationName: &applicationName,
|
||||
},
|
||||
func(o *codedeploy.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return *GetApplication.Application, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, *GetApplication.Application, cache.DefaultExpiration)
|
||||
|
||||
return *GetApplication.Application, nil
|
||||
}
|
||||
|
||||
func CachedCodeDeployGetDeploymentConfig(client AWSCodeDeployClientInterface, accountID string, region string, deploymentConfigName string) (codeDeployTypes.DeploymentConfigInfo, error) {
|
||||
cacheKey := fmt.Sprintf("%s-codedeploy-GetDeploymentConfig-%s-%s", accountID, region, deploymentConfigName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(codeDeployTypes.DeploymentConfigInfo), nil
|
||||
}
|
||||
GetDeploymentConfig, err := client.GetDeploymentConfig(
|
||||
context.TODO(),
|
||||
&codedeploy.GetDeploymentConfigInput{
|
||||
DeploymentConfigName: &deploymentConfigName,
|
||||
},
|
||||
func(o *codedeploy.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return *GetDeploymentConfig.DeploymentConfigInfo, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, *GetDeploymentConfig.DeploymentConfigInfo, cache.DefaultExpiration)
|
||||
|
||||
return *GetDeploymentConfig.DeploymentConfigInfo, nil
|
||||
}
|
||||
|
||||
func CachedCodeDeployGetDeployment(client AWSCodeDeployClientInterface, accountID string, region string, deploymentID string) (codeDeployTypes.DeploymentInfo, error) {
|
||||
cacheKey := fmt.Sprintf("%s-codedeploy-GetDeployment-%s-%s", accountID, region, deploymentID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(codeDeployTypes.DeploymentInfo), nil
|
||||
}
|
||||
GetDeployment, err := client.GetDeployment(
|
||||
context.TODO(),
|
||||
&codedeploy.GetDeploymentInput{
|
||||
DeploymentId: &deploymentID,
|
||||
},
|
||||
func(o *codedeploy.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return *GetDeployment.DeploymentInfo, err
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, *GetDeployment.DeploymentInfo, cache.DefaultExpiration)
|
||||
|
||||
return *GetDeployment.DeploymentInfo, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/codedeploy"
|
||||
codedeployTypes "github.com/aws/aws-sdk-go-v2/service/codedeploy/types"
|
||||
)
|
||||
|
||||
type MockedCodedeployClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedCodedeployClient) ListApplications(ctx context.Context, input *codedeploy.ListApplicationsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListApplicationsOutput, error) {
|
||||
return &codedeploy.ListApplicationsOutput{
|
||||
Applications: []string{
|
||||
"app1",
|
||||
"app2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCodedeployClient) ListDeployments(ctx context.Context, input *codedeploy.ListDeploymentsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentsOutput, error) {
|
||||
return &codedeploy.ListDeploymentsOutput{
|
||||
Deployments: []string{
|
||||
"deployment1",
|
||||
"deployment2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCodedeployClient) ListDeploymentConfigs(ctx context.Context, input *codedeploy.ListDeploymentConfigsInput, options ...func(*codedeploy.Options)) (*codedeploy.ListDeploymentConfigsOutput, error) {
|
||||
return &codedeploy.ListDeploymentConfigsOutput{
|
||||
DeploymentConfigsList: []string{
|
||||
"deploymentConfig1",
|
||||
"deploymentConfig2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCodedeployClient) GetApplication(ctx context.Context, input *codedeploy.GetApplicationInput, options ...func(*codedeploy.Options)) (*codedeploy.GetApplicationOutput, error) {
|
||||
return &codedeploy.GetApplicationOutput{
|
||||
Application: &codedeployTypes.ApplicationInfo{
|
||||
ApplicationName: aws.String("application1"),
|
||||
ApplicationId: aws.String("application1"),
|
||||
CreateTime: aws.Time(time.Now()),
|
||||
GitHubAccountName: aws.String("github"),
|
||||
LinkedToGitHub: true,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCodedeployClient) GetDeployment(ctx context.Context, input *codedeploy.GetDeploymentInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentOutput, error) {
|
||||
return &codedeploy.GetDeploymentOutput{
|
||||
DeploymentInfo: &codedeployTypes.DeploymentInfo{
|
||||
ApplicationName: aws.String("application1"),
|
||||
CreateTime: aws.Time(time.Now()),
|
||||
DeploymentId: aws.String("deployment1"),
|
||||
DeploymentOverview: &codedeployTypes.DeploymentOverview{
|
||||
Failed: 0,
|
||||
InProgress: 0,
|
||||
Pending: 0,
|
||||
Ready: 0,
|
||||
Skipped: 0,
|
||||
Succeeded: 0,
|
||||
},
|
||||
DeploymentConfigName: aws.String("deploymentConfig1"),
|
||||
Status: codedeployTypes.DeploymentStatusSucceeded,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedCodedeployClient) GetDeploymentConfig(ctx context.Context, input *codedeploy.GetDeploymentConfigInput, options ...func(*codedeploy.Options)) (*codedeploy.GetDeploymentConfigOutput, error) {
|
||||
return &codedeploy.GetDeploymentConfigOutput{
|
||||
DeploymentConfigInfo: &codedeployTypes.DeploymentConfigInfo{
|
||||
DeploymentConfigName: aws.String("deploymentConfig1"),
|
||||
ComputePlatform: codedeployTypes.ComputePlatformServer,
|
||||
MinimumHealthyHosts: &codedeployTypes.MinimumHealthyHosts{
|
||||
Type: codedeployTypes.MinimumHealthyHostsTypeFleetPercent,
|
||||
Value: 100,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
|
||||
dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSDataPipelineClientInterface interface {
|
||||
ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, opts ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]dataPipelineTypes.PipelineIdName{})
|
||||
}
|
||||
|
||||
func CachedDataPipelineListPipelines(client AWSDataPipelineClientInterface, accountID string, region string) ([]dataPipelineTypes.PipelineIdName, error) {
|
||||
var PaginationControl *string
|
||||
var pipelines []dataPipelineTypes.PipelineIdName
|
||||
cacheKey := fmt.Sprintf("%s-datapipeline-ListPipelines-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]dataPipelineTypes.PipelineIdName), nil
|
||||
}
|
||||
for {
|
||||
ListPipelines, err := client.ListPipelines(
|
||||
context.TODO(),
|
||||
&datapipeline.ListPipelinesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *datapipeline.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return pipelines, err
|
||||
}
|
||||
|
||||
pipelines = append(pipelines, ListPipelines.PipelineIdList...)
|
||||
|
||||
//pagination
|
||||
if ListPipelines.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListPipelines.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, pipelines, cache.DefaultExpiration)
|
||||
return pipelines, nil
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/datapipeline"
|
||||
dataPipelineTypes "github.com/aws/aws-sdk-go-v2/service/datapipeline/types"
|
||||
)
|
||||
|
||||
type MockedDataPipelineClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedDataPipelineClient) ListPipelines(ctx context.Context, input *datapipeline.ListPipelinesInput, options ...func(*datapipeline.Options)) (*datapipeline.ListPipelinesOutput, error) {
|
||||
return &datapipeline.ListPipelinesOutput{
|
||||
PipelineIdList: []dataPipelineTypes.PipelineIdName{
|
||||
{
|
||||
Id: aws.String("pipeline1"),
|
||||
Name: aws.String("pipeline1"),
|
||||
},
|
||||
{
|
||||
Id: aws.String("pipeline2"),
|
||||
Name: aws.String("pipeline2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/docdb"
|
||||
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
|
||||
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type DocDBClientInterface interface {
|
||||
DescribeGlobalClusters(context.Context, *docdb.DescribeGlobalClustersInput, ...func(*docdb.Options)) (*docdb.DescribeGlobalClustersOutput, error)
|
||||
DescribeDBClusters(context.Context, *docdb.DescribeDBClustersInput, ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error)
|
||||
DescribeDBInstances(context.Context, *docdb.DescribeDBInstancesInput, ...func(*docdb.Options)) (*docdb.DescribeDBInstancesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]docdbTypes.GlobalCluster{})
|
||||
//gob.Register([]docdbTypes.DBCluster{})
|
||||
//gob.Register([]docdbTypes.DBInstance{})
|
||||
|
||||
}
|
||||
|
||||
func CachedDocDBDescribeGlobalClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.GlobalCluster, error) {
|
||||
var PaginationControl *string
|
||||
var globalClusters []docdbTypes.GlobalCluster
|
||||
cacheKey := fmt.Sprintf("%s-docdb-DescribeGlobalClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]docdbTypes.GlobalCluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeGlobalClusters, err := client.DescribeGlobalClusters(
|
||||
context.TODO(),
|
||||
&docdb.DescribeGlobalClustersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *docdb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return globalClusters, err
|
||||
}
|
||||
|
||||
globalClusters = append(globalClusters, DescribeGlobalClusters.GlobalClusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeGlobalClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeGlobalClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, globalClusters, cache.DefaultExpiration)
|
||||
return globalClusters, nil
|
||||
}
|
||||
|
||||
func CachedDocDBDescribeDBClusters(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBCluster, error) {
|
||||
var PaginationControl *string
|
||||
var dbClusters []docdbTypes.DBCluster
|
||||
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]docdbTypes.DBCluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeDBClusters, err := client.DescribeDBClusters(
|
||||
context.TODO(),
|
||||
&docdb.DescribeDBClustersInput{
|
||||
Marker: PaginationControl,
|
||||
Filters: []docdbTypes.Filter{
|
||||
{
|
||||
Name: aws.String("engine"),
|
||||
Values: []string{"docdb"},
|
||||
},
|
||||
},
|
||||
},
|
||||
func(o *docdb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dbClusters, err
|
||||
}
|
||||
|
||||
dbClusters = append(dbClusters, DescribeDBClusters.DBClusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeDBClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDBClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, dbClusters, cache.DefaultExpiration)
|
||||
return dbClusters, nil
|
||||
}
|
||||
|
||||
func CachedDocDBDescribeDBInstances(client DocDBClientInterface, accountID string, region string) ([]docdbTypes.DBInstance, error) {
|
||||
var PaginationControl *string
|
||||
var dbInstances []docdbTypes.DBInstance
|
||||
cacheKey := fmt.Sprintf("%s-docdb-DescribeDBInstances-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]docdbTypes.DBInstance), nil
|
||||
}
|
||||
for {
|
||||
DescribeDBInstances, err := client.DescribeDBInstances(
|
||||
context.TODO(),
|
||||
&docdb.DescribeDBInstancesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *docdb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return dbInstances, err
|
||||
}
|
||||
|
||||
dbInstances = append(dbInstances, DescribeDBInstances.DBInstances...)
|
||||
|
||||
//pagination
|
||||
if DescribeDBInstances.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeDBInstances.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, dbInstances, cache.DefaultExpiration)
|
||||
return dbInstances, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/docdb"
|
||||
docdbTypes "github.com/aws/aws-sdk-go-v2/service/docdb/types"
|
||||
)
|
||||
|
||||
type MockedAWSDocsDBClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSDocsDBClient) DescribeDBClusters(ctx context.Context, input *docdb.DescribeDBClustersInput, options ...func(*docdb.Options)) (*docdb.DescribeDBClustersOutput, error) {
|
||||
return &docdb.DescribeDBClustersOutput{
|
||||
DBClusters: []docdbTypes.DBCluster{
|
||||
{
|
||||
DBClusterIdentifier: aws.String("cluster1"),
|
||||
},
|
||||
{
|
||||
DBClusterIdentifier: aws.String("cluster2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
dynamoDBTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type DynamoDBClientInterface interface {
|
||||
ListTables(context.Context, *dynamodb.ListTablesInput, ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error)
|
||||
DescribeTable(context.Context, *dynamodb.DescribeTableInput, ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
gob.Register(dynamoDBTypes.TableDescription{})
|
||||
}
|
||||
|
||||
func CachedDynamoDBListTables(client DynamoDBClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var tables []string
|
||||
cacheKey := fmt.Sprintf("%s-dynamodb-ListTables-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListTables, err := client.ListTables(
|
||||
context.TODO(),
|
||||
&dynamodb.ListTablesInput{
|
||||
ExclusiveStartTableName: PaginationControl,
|
||||
},
|
||||
func(o *dynamodb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return tables, err
|
||||
}
|
||||
|
||||
tables = append(tables, ListTables.TableNames...)
|
||||
|
||||
//pagination
|
||||
if ListTables.LastEvaluatedTableName == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListTables.LastEvaluatedTableName
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, tables, cache.DefaultExpiration)
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func CachedDynamoDBDescribeTable(client DynamoDBClientInterface, accountID string, region string, tableName string) (dynamoDBTypes.TableDescription, error) {
|
||||
var tableDescription dynamoDBTypes.TableDescription
|
||||
cacheKey := fmt.Sprintf("%s-dynamodb-DescribeTable-%s-%s", accountID, region, tableName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(dynamoDBTypes.TableDescription), nil
|
||||
}
|
||||
|
||||
DescribeTable, err := client.DescribeTable(
|
||||
context.TODO(),
|
||||
&dynamodb.DescribeTableInput{
|
||||
TableName: &tableName,
|
||||
},
|
||||
func(o *dynamodb.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return tableDescription, err
|
||||
}
|
||||
tableDescription = *DescribeTable.Table
|
||||
|
||||
internal.Cache.Set(cacheKey, tableDescription, cache.DefaultExpiration)
|
||||
return tableDescription, nil
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/dynamodb"
|
||||
dynamodbTypes "github.com/aws/aws-sdk-go-v2/service/dynamodb/types"
|
||||
)
|
||||
|
||||
type MockedAWSDynamoDBClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSDynamoDBClient) DescribeTable(ctx context.Context, input *dynamodb.DescribeTableInput, options ...func(*dynamodb.Options)) (*dynamodb.DescribeTableOutput, error) {
|
||||
return &dynamodb.DescribeTableOutput{
|
||||
Table: &dynamodbTypes.TableDescription{
|
||||
TableName: input.TableName,
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSDynamoDBClient) ListTables(ctx context.Context, input *dynamodb.ListTablesInput, options ...func(*dynamodb.Options)) (*dynamodb.ListTablesOutput, error) {
|
||||
return &dynamodb.ListTablesOutput{
|
||||
TableNames: []string{"table1", "table2"},
|
||||
}, nil
|
||||
}
|
||||
+228
@@ -0,0 +1,228 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
ec2Types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSEC2ClientInterface interface {
|
||||
DescribeInstances(context.Context, *ec2.DescribeInstancesInput, ...func(*ec2.Options)) (*ec2.DescribeInstancesOutput, error)
|
||||
DescribeNetworkInterfaces(context.Context, *ec2.DescribeNetworkInterfacesInput, ...func(*ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error)
|
||||
DescribeSnapshots(context.Context, *ec2.DescribeSnapshotsInput, ...func(*ec2.Options)) (*ec2.DescribeSnapshotsOutput, error)
|
||||
DescribeVolumes(context.Context, *ec2.DescribeVolumesInput, ...func(*ec2.Options)) (*ec2.DescribeVolumesOutput, error)
|
||||
DescribeImages(context.Context, *ec2.DescribeImagesInput, ...func(*ec2.Options)) (*ec2.DescribeImagesOutput, error)
|
||||
DescribeInstanceAttribute(context.Context, *ec2.DescribeInstanceAttributeInput, ...func(*ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]ec2Types.Instance{})
|
||||
gob.Register([]ec2Types.NetworkInterface{})
|
||||
gob.Register([]ec2Types.Snapshot{})
|
||||
gob.Register([]ec2Types.Volume{})
|
||||
gob.Register([]ec2Types.Image{})
|
||||
|
||||
}
|
||||
|
||||
func CachedEC2DescribeInstances(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Instance, error) {
|
||||
var PaginationControl *string
|
||||
var instances []ec2Types.Instance
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstances-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]ec2Types.Instance), nil
|
||||
}
|
||||
for {
|
||||
DescribeInstances, err := client.DescribeInstances(
|
||||
context.TODO(),
|
||||
&ec2.DescribeInstancesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return instances, err
|
||||
}
|
||||
|
||||
for _, reservation := range DescribeInstances.Reservations {
|
||||
instances = append(instances, reservation.Instances...)
|
||||
}
|
||||
|
||||
//pagination
|
||||
if DescribeInstances.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeInstances.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
func CachedEC2DescribeInstanceAttributeUserData(client AWSEC2ClientInterface, accountID string, region string, instanceID string) (string, error) {
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeInstanceAttributeUserData-%s-%s", accountID, region, instanceID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(string), nil
|
||||
}
|
||||
DescribeInstanceAttribute, err := client.DescribeInstanceAttribute(
|
||||
context.TODO(),
|
||||
&ec2.DescribeInstanceAttributeInput{
|
||||
Attribute: ec2Types.InstanceAttributeNameUserData,
|
||||
InstanceId: &instanceID,
|
||||
},
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
internal.Cache.Set(cacheKey, aws.ToString(DescribeInstanceAttribute.UserData.Value), cache.DefaultExpiration)
|
||||
|
||||
return aws.ToString(DescribeInstanceAttribute.UserData.Value), nil
|
||||
}
|
||||
|
||||
func CachedEC2DescribeNetworkInterfaces(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.NetworkInterface, error) {
|
||||
var PaginationControl *string
|
||||
var NetworkInterfaces []ec2Types.NetworkInterface
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeNetworkInterfaces-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]ec2Types.NetworkInterface), nil
|
||||
}
|
||||
for {
|
||||
DescribeNetworkInterfaces, err := client.DescribeNetworkInterfaces(
|
||||
context.TODO(),
|
||||
&(ec2.DescribeNetworkInterfacesInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return NetworkInterfaces, err
|
||||
}
|
||||
NetworkInterfaces = append(NetworkInterfaces, DescribeNetworkInterfaces.NetworkInterfaces...)
|
||||
|
||||
if DescribeNetworkInterfaces.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeNetworkInterfaces.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, NetworkInterfaces, cache.DefaultExpiration)
|
||||
return NetworkInterfaces, nil
|
||||
}
|
||||
|
||||
func CachedEC2DescribeSnapshots(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Snapshot, error) {
|
||||
var PaginationControl *string
|
||||
var Snapshots []ec2Types.Snapshot
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeSnapshots-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]ec2Types.Snapshot), nil
|
||||
}
|
||||
for {
|
||||
DescribeSnapshots, err := client.DescribeSnapshots(
|
||||
context.TODO(),
|
||||
&(ec2.DescribeSnapshotsInput{
|
||||
NextToken: PaginationControl,
|
||||
OwnerIds: []string{accountID},
|
||||
}),
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return Snapshots, err
|
||||
}
|
||||
Snapshots = append(Snapshots, DescribeSnapshots.Snapshots...)
|
||||
|
||||
if DescribeSnapshots.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeSnapshots.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, Snapshots, cache.DefaultExpiration)
|
||||
return Snapshots, nil
|
||||
}
|
||||
|
||||
func CachedEC2DescribeVolumes(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Volume, error) {
|
||||
var PaginationControl *string
|
||||
var Volumes []ec2Types.Volume
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeVolumes-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]ec2Types.Volume), nil
|
||||
}
|
||||
for {
|
||||
DescribeVolumes, err := client.DescribeVolumes(
|
||||
context.TODO(),
|
||||
&(ec2.DescribeVolumesInput{
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return Volumes, err
|
||||
}
|
||||
Volumes = append(Volumes, DescribeVolumes.Volumes...)
|
||||
|
||||
if DescribeVolumes.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeVolumes.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, Volumes, cache.DefaultExpiration)
|
||||
return Volumes, nil
|
||||
}
|
||||
|
||||
func CachedEC2DescribeImages(client AWSEC2ClientInterface, accountID string, region string) ([]ec2Types.Image, error) {
|
||||
var PaginationControl *string
|
||||
var Images []ec2Types.Image
|
||||
cacheKey := fmt.Sprintf("%s-ec2-DescribeImages-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]ec2Types.Image), nil
|
||||
}
|
||||
for {
|
||||
DescribeImages, err := client.DescribeImages(
|
||||
context.TODO(),
|
||||
&(ec2.DescribeImagesInput{
|
||||
Owners: []string{accountID},
|
||||
NextToken: PaginationControl,
|
||||
}),
|
||||
func(o *ec2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return Images, err
|
||||
}
|
||||
Images = append(Images, DescribeImages.Images...)
|
||||
|
||||
if DescribeImages.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeImages.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, Images, cache.DefaultExpiration)
|
||||
return Images, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,388 @@
|
||||
package sdk
|
||||
|
||||
// create test for the Cached EC2 functions. Create a mocked client and mocked functions for each of the methods used in the ec2.go file
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ec2"
|
||||
ec2types "github.com/aws/aws-sdk-go-v2/service/ec2/types"
|
||||
)
|
||||
|
||||
const DESCRIBE_NETWORK_INTEFACES_TEST_FILE = "./test-data/describe-network-interfaces.json"
|
||||
const DESCRIBE_INSTANCES_TEST_FILE = "./test-data/ec2-describeInstances.json"
|
||||
const DESCRIBE_SNAPSHOTS_TEST_FILE = "./test-data/ec2-describeSnapshots.json"
|
||||
const DESCRIBE_VOLUMES_TEST_FILE = "./test-data/ec2-describeVolumes.json"
|
||||
const DESCRIBE_IMAGES_TEST_FILE = "./test-data/ec2-describeImages.json"
|
||||
|
||||
type MockedEC2Client2 struct {
|
||||
describeNetworkInterfaces DescribeNetworkInterfaces
|
||||
describeInstances DescribeInstances
|
||||
describeSnapshots DescribeSnapshots
|
||||
describeVolumes DescribeVolumes
|
||||
describeImages DescribeImages
|
||||
}
|
||||
|
||||
type DescribeNetworkInterfaces struct {
|
||||
NetworkInterfaces []struct {
|
||||
Status string `json:"Status"`
|
||||
MacAddress string `json:"MacAddress"`
|
||||
SourceDestCheck bool `json:"SourceDestCheck"`
|
||||
VpcID string `json:"VpcId"`
|
||||
Description string `json:"Description"`
|
||||
Association struct {
|
||||
PublicIP string `json:"PublicIp"`
|
||||
AssociationID string `json:"AssociationId"`
|
||||
PublicDNSName string `json:"PublicDnsName"`
|
||||
IPOwnerID string `json:"IpOwnerId"`
|
||||
} `json:"Association"`
|
||||
NetworkInterfaceID string `json:"NetworkInterfaceId"`
|
||||
PrivateIPAddresses []struct {
|
||||
PrivateDNSName string `json:"PrivateDnsName"`
|
||||
Association struct {
|
||||
PublicIP string `json:"PublicIp"`
|
||||
AssociationID string `json:"AssociationId"`
|
||||
PublicDNSName string `json:"PublicDnsName"`
|
||||
IPOwnerID string `json:"IpOwnerId"`
|
||||
} `json:"Association"`
|
||||
Primary bool `json:"Primary"`
|
||||
PrivateIPAddress string `json:"PrivateIpAddress"`
|
||||
} `json:"PrivateIpAddresses"`
|
||||
RequesterManaged bool `json:"RequesterManaged"`
|
||||
Ipv6Addresses []interface{} `json:"Ipv6Addresses"`
|
||||
PrivateDNSName string `json:"PrivateDnsName,omitempty"`
|
||||
AvailabilityZone string `json:"AvailabilityZone"`
|
||||
Attachment struct {
|
||||
Status string `json:"Status"`
|
||||
DeviceIndex int `json:"DeviceIndex"`
|
||||
AttachTime time.Time `json:"AttachTime"`
|
||||
InstanceID string `json:"InstanceId"`
|
||||
DeleteOnTermination bool `json:"DeleteOnTermination"`
|
||||
AttachmentID string `json:"AttachmentId"`
|
||||
InstanceOwnerID string `json:"InstanceOwnerId"`
|
||||
} `json:"Attachment"`
|
||||
Groups []struct {
|
||||
GroupName string `json:"GroupName"`
|
||||
GroupID string `json:"GroupId"`
|
||||
} `json:"Groups"`
|
||||
SubnetID string `json:"SubnetId"`
|
||||
OwnerID string `json:"OwnerId"`
|
||||
TagSet []interface{} `json:"TagSet"`
|
||||
PrivateIPAddress string `json:"PrivateIpAddress"`
|
||||
} `json:"NetworkInterfaces"`
|
||||
}
|
||||
|
||||
type DescribeInstances struct {
|
||||
Reservations []struct {
|
||||
Instances []struct {
|
||||
AmiLaunchIndex int `json:"AmiLaunchIndex"`
|
||||
ImageID string `json:"ImageId"`
|
||||
InstanceID string `json:"InstanceId"`
|
||||
InstanceType string `json:"InstanceType"`
|
||||
KernelID string `json:"KernelId"`
|
||||
KeyName string `json:"KeyName"`
|
||||
LaunchTime string `json:"LaunchTime"`
|
||||
Monitoring struct {
|
||||
State string `json:"State"`
|
||||
} `json:"Monitoring"`
|
||||
Placement struct {
|
||||
AvailabilityZone string `json:"AvailabilityZone"`
|
||||
GroupName string `json:"GroupName"`
|
||||
Tenancy string `json:"Tenancy"`
|
||||
} `json:"Placement"`
|
||||
Platform string `json:"Platform"`
|
||||
PrivateDNS string `json:"PrivateDnsName"`
|
||||
PrivateIP string `json:"PrivateIpAddress"`
|
||||
PublicDNS string `json:"PublicDnsName"`
|
||||
PublicIP string `json:"PublicIpAddress"`
|
||||
State string `json:"State"`
|
||||
SubnetID string `json:"SubnetId"`
|
||||
VpcID string `json:"VpcId"`
|
||||
Architecture string `json:"Architecture"`
|
||||
BlockDeviceMappings []struct {
|
||||
DeviceName string `json:"DeviceName"`
|
||||
Ebs struct {
|
||||
AttachTime time.Time `json:"AttachTime"`
|
||||
DeleteOnTermination bool `json:"DeleteOnTermination"`
|
||||
Status string `json:"Status"`
|
||||
VolumeID string `json:"VolumeId"`
|
||||
} `json:"Ebs"`
|
||||
} `json:"BlockDeviceMappings"`
|
||||
ClientToken string `json:"ClientToken"`
|
||||
EbsOptimized bool `json:"EbsOptimized"`
|
||||
Hypervisor string `json:"Hypervisor"`
|
||||
IamInstanceProfile struct {
|
||||
Arn string `json:"Arn"`
|
||||
ID string `json:"Id"`
|
||||
} `json:"IamInstanceProfile"`
|
||||
NetworkInterfaces []struct {
|
||||
Attachment struct {
|
||||
AttachTime time.Time `json:"AttachTime"`
|
||||
AttachmentID string `json:"AttachmentId"`
|
||||
DeleteOnTermination bool `json:"DeleteOnTermination"`
|
||||
DeviceIndex int `json:"DeviceIndex"`
|
||||
Status string `json:"Status"`
|
||||
} `json:"Attachment"`
|
||||
Description string `json:"Description"`
|
||||
Groups []struct {
|
||||
GroupName string `json:"GroupName"`
|
||||
GroupID string `json:"GroupId"`
|
||||
} `json:"Groups"`
|
||||
MacAddress string `json:"MacAddress"`
|
||||
NetworkInterfaceID string `json:"NetworkInterfaceId"`
|
||||
OwnerID string `json:"OwnerId"`
|
||||
PrivateDNSName string `json:"PrivateDnsName"`
|
||||
PrivateIPAddress string `json:"PrivateIpAddress"`
|
||||
PrivateIPAddresses []struct {
|
||||
Association struct {
|
||||
IPOwnerID string `json:"IpOwnerId"`
|
||||
PublicDNSName string `json:"PublicDnsName"`
|
||||
PublicIP string `json:"PublicIp"`
|
||||
} `json:"Association"`
|
||||
Primary bool `json:"Primary"`
|
||||
PrivateDNSName string `json:"PrivateDnsName"`
|
||||
PrivateIPAddress string `json:"PrivateIpAddress"`
|
||||
} `json:"PrivateIpAddresses"`
|
||||
SourceDestCheck bool `json:"SourceDestCheck"`
|
||||
Status string `json:"Status"`
|
||||
SubnetID string `json:"SubnetId"`
|
||||
VpcID string `json:"VpcId"`
|
||||
} `json:"NetworkInterfaces"`
|
||||
RootDeviceName string `json:"RootDeviceName"`
|
||||
RootDeviceType string `json:"RootDeviceType"`
|
||||
SecurityGroups []struct {
|
||||
GroupName string `json:"GroupName"`
|
||||
GroupID string `json:"GroupId"`
|
||||
} `json:"SecurityGroups"`
|
||||
SourceDestCheck bool `json:"SourceDestCheck"`
|
||||
StateReason struct {
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
} `json:"StateReason"`
|
||||
Tags []struct {
|
||||
Key string `json:"Key"`
|
||||
Value string `json:"Value"`
|
||||
} `json:"Tags"`
|
||||
VirtualizationType string `json:"VirtualizationType"`
|
||||
} `json:"Instances"`
|
||||
OwnerID string `json:"OwnerId"`
|
||||
RequesterID string `json:"RequesterId"`
|
||||
ReservationID string `json:"ReservationId"`
|
||||
} `json:"Reservations"`
|
||||
}
|
||||
|
||||
type DescribeImages struct {
|
||||
Images []struct {
|
||||
Architecture string `json:"Architecture"`
|
||||
BlockDeviceMappings []struct {
|
||||
DeviceName string `json:"DeviceName"`
|
||||
Ebs struct {
|
||||
DeleteOnTermination bool `json:"DeleteOnTermination"`
|
||||
SnapshotID string `json:"SnapshotId"`
|
||||
VolumeSize int `json:"VolumeSize"`
|
||||
VolumeType string `json:"VolumeType"`
|
||||
} `json:"Ebs"`
|
||||
} `json:"BlockDeviceMappings"`
|
||||
CreationDate string `json:"CreationDate"`
|
||||
Description string `json:"Description"`
|
||||
EnaSupport bool `json:"EnaSupport"`
|
||||
Hypervisor string `json:"Hypervisor"`
|
||||
ImageID string `json:"ImageId"`
|
||||
ImageLocation string `json:"ImageLocation"`
|
||||
ImageType string `json:"ImageType"`
|
||||
KernelID string `json:"KernelId"`
|
||||
Name string `json:"Name"`
|
||||
OwnerAlias string `json:"OwnerAlias"`
|
||||
OwnerID string `json:"OwnerId"`
|
||||
Platform string `json:"Platform"`
|
||||
ProductCodes []struct {
|
||||
ProductCodeID string `json:"ProductCodeId"`
|
||||
ProductCodeType string `json:"ProductCodeType"`
|
||||
} `json:"ProductCodes"`
|
||||
Public bool `json:"Public"`
|
||||
RamdiskID string `json:"RamdiskId"`
|
||||
RootDeviceName string `json:"RootDeviceName"`
|
||||
RootDeviceType string `json:"RootDeviceType"`
|
||||
SriovNetSupport string `json:"SriovNetSupport"`
|
||||
State string `json:"State"`
|
||||
StateReason struct {
|
||||
Code string `json:"Code"`
|
||||
Message string `json:"Message"`
|
||||
} `json:"StateReason"`
|
||||
Tags []struct {
|
||||
Key string `json:"Key"`
|
||||
Value string `json:"Value"`
|
||||
} `json:"Tags"`
|
||||
VirtualizationType string `json:"VirtualizationType"`
|
||||
} `json:"Images"`
|
||||
NextToken string `json:"NextToken"`
|
||||
}
|
||||
|
||||
type DescribeSnapshots struct {
|
||||
Snapshots []struct {
|
||||
DataEncryptionKeyID string `json:"DataEncryptionKeyId"`
|
||||
Description string `json:"Description"`
|
||||
Encrypted bool `json:"Encrypted"`
|
||||
KMSKeyID string `json:"KmsKeyId"`
|
||||
OwnerAlias string `json:"OwnerAlias"`
|
||||
OwnerID string `json:"OwnerId"`
|
||||
Progress string `json:"Progress"`
|
||||
SnapshotID string `json:"SnapshotId"`
|
||||
StartTime time.Time `json:"StartTime"`
|
||||
State string `json:"State"`
|
||||
StateMessage string `json:"StateMessage"`
|
||||
TagSet []struct {
|
||||
Key string `json:"Key"`
|
||||
Value string `json:"Value"`
|
||||
} `json:"Tags"`
|
||||
VolumeID string `json:"VolumeId"`
|
||||
VolumeSize int `json:"VolumeSize"`
|
||||
} `json:"Snapshots"`
|
||||
}
|
||||
|
||||
type DescribeVolumes struct {
|
||||
Volumes []struct {
|
||||
Attachments []struct {
|
||||
AttachTime time.Time `json:"AttachTime"`
|
||||
DeleteOnTermination bool `json:"DeleteOnTermination"`
|
||||
Device string `json:"Device"`
|
||||
InstanceID string `json:"InstanceId"`
|
||||
State string `json:"State"`
|
||||
VolumeID string `json:"VolumeId"`
|
||||
} `json:"Attachments"`
|
||||
AvailabilityZone string `json:"AvailabilityZone"`
|
||||
CreateTime string `json:"CreateTime"`
|
||||
Encrypted bool `json:"Encrypted"`
|
||||
KMSKeyID string `json:"KmsKeyId"`
|
||||
Size int `json:"Size"`
|
||||
SnapshotID string `json:"SnapshotId"`
|
||||
State string `json:"State"`
|
||||
VolumeID string `json:"VolumeId"`
|
||||
} `json:"Volumes"`
|
||||
}
|
||||
|
||||
// DescribeInstanceAttribute is a struct for the DescribeInstanceAttribute function - include userdata
|
||||
type DescribeInstanceAttribute struct {
|
||||
UserData string `json:"UserData"`
|
||||
InstanceID string `json:"InstanceId"`
|
||||
}
|
||||
|
||||
func (c *MockedEC2Client2) DescribeNetworkInterfaces(ctx context.Context, input *ec2.DescribeNetworkInterfacesInput, f ...func(o *ec2.Options)) (*ec2.DescribeNetworkInterfacesOutput, error) {
|
||||
var nics []ec2types.NetworkInterface
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_NETWORK_INTEFACES_TEST_FILE), &c.describeNetworkInterfaces)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_NETWORK_INTEFACES_TEST_FILE)
|
||||
}
|
||||
for _, mockednic := range c.describeNetworkInterfaces.NetworkInterfaces {
|
||||
nics = append(nics, ec2types.NetworkInterface{
|
||||
Association: &ec2types.NetworkInterfaceAssociation{
|
||||
PublicIp: aws.String(mockednic.Association.PublicIP),
|
||||
},
|
||||
NetworkInterfaceId: aws.String(mockednic.NetworkInterfaceID),
|
||||
PrivateIpAddress: aws.String(mockednic.PrivateIPAddress),
|
||||
VpcId: aws.String(mockednic.VpcID),
|
||||
Attachment: &ec2types.NetworkInterfaceAttachment{InstanceId: aws.String(mockednic.Attachment.InstanceID)},
|
||||
Description: aws.String(mockednic.Description),
|
||||
})
|
||||
}
|
||||
return &ec2.DescribeNetworkInterfacesOutput{NetworkInterfaces: nics}, nil
|
||||
}
|
||||
|
||||
func (c *MockedEC2Client2) DescribeInstances(ctx context.Context, input *ec2.DescribeInstancesInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstancesOutput, error) {
|
||||
var instances []ec2types.Instance
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_INSTANCES_TEST_FILE), &c.describeInstances)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_INSTANCES_TEST_FILE)
|
||||
}
|
||||
for _, mockedReservation := range c.describeInstances.Reservations {
|
||||
for _, mockedInstance := range mockedReservation.Instances {
|
||||
for _, inputInstanceID := range input.InstanceIds {
|
||||
if mockedInstance.InstanceID == inputInstanceID {
|
||||
instances = append(instances, ec2types.Instance{
|
||||
InstanceId: aws.String(mockedInstance.InstanceID),
|
||||
InstanceType: ec2types.InstanceType(mockedInstance.InstanceType),
|
||||
ImageId: aws.String(mockedInstance.ImageID),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ec2.DescribeInstancesOutput{Reservations: []ec2types.Reservation{
|
||||
{Instances: instances},
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c *MockedEC2Client2) DescribeVolumes(ctx context.Context, input *ec2.DescribeVolumesInput, f ...func(o *ec2.Options)) (*ec2.DescribeVolumesOutput, error) {
|
||||
var volumes []ec2types.Volume
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_VOLUMES_TEST_FILE), &c.describeVolumes)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_VOLUMES_TEST_FILE)
|
||||
}
|
||||
for _, mockedVolume := range c.describeVolumes.Volumes {
|
||||
for _, inputVolumeID := range input.VolumeIds {
|
||||
if mockedVolume.VolumeID == inputVolumeID {
|
||||
volumes = append(volumes, ec2types.Volume{
|
||||
VolumeId: aws.String(mockedVolume.VolumeID),
|
||||
Size: aws.Int32(int32(mockedVolume.Size)),
|
||||
Attachments: []ec2types.VolumeAttachment{
|
||||
{
|
||||
InstanceId: aws.String(mockedVolume.Attachments[0].InstanceID),
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ec2.DescribeVolumesOutput{Volumes: volumes}, nil
|
||||
}
|
||||
|
||||
func (c *MockedEC2Client2) DescribeSnapshots(ctx context.Context, input *ec2.DescribeSnapshotsInput, f ...func(o *ec2.Options)) (*ec2.DescribeSnapshotsOutput, error) {
|
||||
var snapshots []ec2types.Snapshot
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_SNAPSHOTS_TEST_FILE), &c.describeSnapshots)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_SNAPSHOTS_TEST_FILE)
|
||||
}
|
||||
for _, mockedSnapshot := range c.describeSnapshots.Snapshots {
|
||||
for _, inputSnapshotID := range input.SnapshotIds {
|
||||
if mockedSnapshot.SnapshotID == inputSnapshotID {
|
||||
snapshots = append(snapshots, ec2types.Snapshot{
|
||||
SnapshotId: aws.String(mockedSnapshot.SnapshotID),
|
||||
VolumeId: aws.String(mockedSnapshot.VolumeID),
|
||||
State: ec2types.SnapshotState(mockedSnapshot.State),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ec2.DescribeSnapshotsOutput{Snapshots: snapshots}, nil
|
||||
}
|
||||
|
||||
func (c *MockedEC2Client2) DescribeImages(ctx context.Context, input *ec2.DescribeImagesInput, f ...func(o *ec2.Options)) (*ec2.DescribeImagesOutput, error) {
|
||||
var images []ec2types.Image
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_IMAGES_TEST_FILE), &c.describeImages)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_IMAGES_TEST_FILE)
|
||||
}
|
||||
for _, mockedImage := range c.describeImages.Images {
|
||||
for _, inputImageID := range input.ImageIds {
|
||||
if mockedImage.ImageID == inputImageID {
|
||||
images = append(images, ec2types.Image{
|
||||
ImageId: aws.String(mockedImage.ImageID),
|
||||
Name: aws.String(mockedImage.Name),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ec2.DescribeImagesOutput{Images: images}, nil
|
||||
}
|
||||
|
||||
func (c *MockedEC2Client2) DescribeInstanceAttribute(ctx context.Context, input *ec2.DescribeInstanceAttributeInput, f ...func(o *ec2.Options)) (*ec2.DescribeInstanceAttributeOutput, error) {
|
||||
return &ec2.DescribeInstanceAttributeOutput{
|
||||
UserData: &ec2types.AttributeValue{
|
||||
Value: aws.String("userdata"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+137
@@ -0,0 +1,137 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSECRClientInterface interface {
|
||||
DescribeRepositories(ctx context.Context, params *ecr.DescribeRepositoriesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error)
|
||||
DescribeImages(ctx context.Context, params *ecr.DescribeImagesInput, optFns ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error)
|
||||
GetRepositoryPolicy(ctx context.Context, params *ecr.GetRepositoryPolicyInput, optFns ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]ecrTypes.Repository{})
|
||||
gob.Register([]ecrTypes.ImageDetail{})
|
||||
gob.Register(ecrTypes.Repository{})
|
||||
|
||||
}
|
||||
|
||||
// create a CachedECRDescribeRepositories function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
|
||||
func CachedECRDescribeRepositories(ECRClient AWSECRClientInterface, accountID string, region string) ([]ecrTypes.Repository, error) {
|
||||
var PaginationControl *string
|
||||
var repositories []ecrTypes.Repository
|
||||
cacheKey := fmt.Sprintf("%s-ecr-DescribeRepositories-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECR repositories data")
|
||||
return cached.([]ecrTypes.Repository), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeRepositories, err := ECRClient.DescribeRepositories(
|
||||
context.TODO(),
|
||||
&ecr.DescribeRepositoriesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
repositories = append(repositories, DescribeRepositories.Repositories...)
|
||||
|
||||
// Pagination control.
|
||||
if DescribeRepositories.NextToken != nil {
|
||||
PaginationControl = DescribeRepositories.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, repositories, cache.DefaultExpiration)
|
||||
return repositories, nil
|
||||
}
|
||||
|
||||
// create a CachedECRDescribeImages function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
|
||||
func CachedECRDescribeImages(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) ([]ecrTypes.ImageDetail, error) {
|
||||
var PaginationControl *string
|
||||
var images []ecrTypes.ImageDetail
|
||||
cacheKey := fmt.Sprintf("%s-ecr-DescribeImages-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Images data")
|
||||
return cached.([]ecrTypes.ImageDetail), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeImages, err := ECRClient.DescribeImages(
|
||||
context.TODO(),
|
||||
&ecr.DescribeImagesInput{
|
||||
NextToken: PaginationControl,
|
||||
RepositoryName: &repositoryName,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
images = append(images, DescribeImages.ImageDetails...)
|
||||
|
||||
// Pagination control.
|
||||
if DescribeImages.NextToken != nil {
|
||||
PaginationControl = DescribeImages.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, images, cache.DefaultExpiration)
|
||||
return images, nil
|
||||
}
|
||||
|
||||
// create a CachedECRGetRepositoryPolicy function that uses go-cache line the other Cached* functions. It should accept a ecr client, account id, and region. Make sure it handles the region option and pagination if needed
|
||||
func CachedECRGetRepositoryPolicy(ECRClient AWSECRClientInterface, accountID string, region string, repositoryName string) (string, error) {
|
||||
// in repositoryName, replace "/" with "-"
|
||||
cacheKey := fmt.Sprintf("%s-ecr-GetRepositoryPolicy-%s-%s", accountID, region, strings.ReplaceAll(repositoryName, "/", "-"))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECR repository policy data")
|
||||
return cached.(string), nil
|
||||
}
|
||||
|
||||
GetRepositoryPolicy, err := ECRClient.GetRepositoryPolicy(
|
||||
context.TODO(),
|
||||
&ecr.GetRepositoryPolicyInput{
|
||||
RepositoryName: &repositoryName,
|
||||
},
|
||||
func(o *ecr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return "", err
|
||||
}
|
||||
PolicyText := aws.ToString(GetRepositoryPolicy.PolicyText)
|
||||
|
||||
internal.Cache.Set(cacheKey, PolicyText, cache.DefaultExpiration)
|
||||
return PolicyText, nil
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecr"
|
||||
ecrTypes "github.com/aws/aws-sdk-go-v2/service/ecr/types"
|
||||
)
|
||||
|
||||
type MockedECRClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedECRClient) DescribeRepositories(ctx context.Context, input *ecr.DescribeRepositoriesInput, options ...func(*ecr.Options)) (*ecr.DescribeRepositoriesOutput, error) {
|
||||
return &ecr.DescribeRepositoriesOutput{
|
||||
Repositories: []ecrTypes.Repository{
|
||||
{
|
||||
RepositoryName: aws.String("repo1"),
|
||||
RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo1"),
|
||||
},
|
||||
{
|
||||
RepositoryName: aws.String("repo2"),
|
||||
RepositoryUri: aws.String("11111111111111.dkr.ecr.us-east-1.amazonaws.com/repo2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedECRClient) DescribeImages(ctx context.Context, input *ecr.DescribeImagesInput, options ...func(*ecr.Options)) (*ecr.DescribeImagesOutput, error) {
|
||||
if aws.ToString(input.RepositoryName) == "repo1" {
|
||||
return &ecr.DescribeImagesOutput{
|
||||
ImageDetails: []ecrTypes.ImageDetail{
|
||||
{
|
||||
ImageTags: []string{
|
||||
"customtag",
|
||||
"tag2",
|
||||
},
|
||||
ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)),
|
||||
ImageSizeInBytes: aws.Int64(123456),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
} else if aws.ToString(input.RepositoryName) == "repo2" {
|
||||
return &ecr.DescribeImagesOutput{
|
||||
ImageDetails: []ecrTypes.ImageDetail{
|
||||
{
|
||||
ImageTags: []string{
|
||||
"latest",
|
||||
},
|
||||
ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)),
|
||||
ImageSizeInBytes: aws.Int64(2222222),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
} else {
|
||||
return &ecr.DescribeImagesOutput{
|
||||
ImageDetails: []ecrTypes.ImageDetail{
|
||||
{
|
||||
ImageTags: []string{
|
||||
"customtag",
|
||||
"tag2",
|
||||
},
|
||||
ImagePushedAt: aws.Time(time.Date(2022, 10, 25, 15, 14, 0, 0, time.UTC)),
|
||||
ImageSizeInBytes: aws.Int64(111),
|
||||
},
|
||||
{
|
||||
ImageTags: []string{
|
||||
"latest",
|
||||
},
|
||||
ImagePushedAt: aws.Time(time.Date(2021, 10, 15, 11, 14, 0, 0, time.UTC)),
|
||||
ImageSizeInBytes: aws.Int64(333),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedECRClient) GetRepositoryPolicy(ctx context.Context, input *ecr.GetRepositoryPolicyInput, options ...func(*ecr.Options)) (*ecr.GetRepositoryPolicyOutput, error) {
|
||||
return &ecr.GetRepositoryPolicyOutput{
|
||||
PolicyText: aws.String(`{
|
||||
"Version": "2008-10-17",
|
||||
"Statement": [
|
||||
{
|
||||
"Sid": "AllowPushPull",
|
||||
"Effect": "Allow",
|
||||
"Principal": {
|
||||
"AWS": [
|
||||
"arn:aws:iam::123456789012:root",
|
||||
"arn:aws:iam::123456789012:user/MyUser"
|
||||
]
|
||||
},
|
||||
"Action": [
|
||||
|
||||
"ecr:GetDownloadUrlForLayer",
|
||||
"ecr:BatchGetImage",
|
||||
"ecr:BatchCheckLayerAvailability",
|
||||
"ecr:PutImage",
|
||||
"ecr:InitiateLayerUpload",
|
||||
"ecr:UploadLayerPart",
|
||||
"ecr:CompleteLayerUpload"
|
||||
]
|
||||
|
||||
}
|
||||
]
|
||||
}`),
|
||||
}, nil
|
||||
}
|
||||
+222
@@ -0,0 +1,222 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSECSClientInterface interface {
|
||||
ListClusters(ctx context.Context, params *ecs.ListClustersInput, optFns ...func(*ecs.Options)) (*ecs.ListClustersOutput, error)
|
||||
ListTasks(ctx context.Context, params *ecs.ListTasksInput, optFns ...func(*ecs.Options)) (*ecs.ListTasksOutput, error)
|
||||
ListServices(ctx context.Context, params *ecs.ListServicesInput, optFns ...func(*ecs.Options)) (*ecs.ListServicesOutput, error)
|
||||
DescribeTasks(ctx context.Context, params *ecs.DescribeTasksInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error)
|
||||
DescribeTaskDefinition(ctx context.Context, params *ecs.DescribeTaskDefinitionInput, optFns ...func(*ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
gob.Register(ecsTypes.Task{})
|
||||
gob.Register([]ecsTypes.Task{})
|
||||
gob.Register(ecsTypes.TaskDefinition{})
|
||||
|
||||
}
|
||||
|
||||
func CachedECSListClusters(ECSClient AWSECSClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []string
|
||||
cacheKey := fmt.Sprintf("%s-ecs-ListClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECS clusters data")
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListClusters, err := ECSClient.ListClusters(
|
||||
context.TODO(),
|
||||
&ecs.ListClustersInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
clusters = append(clusters, ListClusters.ClusterArns...)
|
||||
|
||||
// Pagination control.
|
||||
if ListClusters.NextToken != nil {
|
||||
PaginationControl = ListClusters.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func CachedECSListTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var tasks []string
|
||||
//grab cluster name from AWS ARN
|
||||
clusterName := cluster[strings.LastIndex(cluster, "/")+1:]
|
||||
|
||||
cacheKey := fmt.Sprintf("%s-ecs-ListTasks-%s-%s", accountID, region, clusterName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECS tasks data")
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListTasks, err := ECSClient.ListTasks(
|
||||
context.TODO(),
|
||||
&ecs.ListTasksInput{
|
||||
Cluster: &cluster,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
tasks = append(tasks, ListTasks.TaskArns...)
|
||||
|
||||
// Pagination control.
|
||||
if ListTasks.NextToken != nil {
|
||||
PaginationControl = ListTasks.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, tasks, cache.DefaultExpiration)
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func CachedECSDescribeTasks(ECSClient AWSECSClientInterface, accountID string, region string, cluster string, tasks []string) ([]ecsTypes.Task, error) {
|
||||
var taskDetails []ecsTypes.Task
|
||||
//replace semi-colons with underscores in task definition name
|
||||
clusterFileSystemSafe := strings.ReplaceAll(cluster, ":", "_")
|
||||
clusterFileSystemSafe = strings.ReplaceAll(clusterFileSystemSafe, "/", "_")
|
||||
cacheKey := fmt.Sprintf("%s-ecs-DescribeTasks-%s-%s", accountID, region, clusterFileSystemSafe)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECS task details data")
|
||||
return cached.([]ecsTypes.Task), nil
|
||||
}
|
||||
|
||||
DescribeTasks, err := ECSClient.DescribeTasks(
|
||||
context.TODO(),
|
||||
&ecs.DescribeTasksInput{
|
||||
Cluster: &cluster,
|
||||
Tasks: tasks,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return []ecsTypes.Task{}, err
|
||||
}
|
||||
|
||||
taskDetails = append(taskDetails, DescribeTasks.Tasks...)
|
||||
|
||||
internal.Cache.Set(cacheKey, taskDetails, cache.DefaultExpiration)
|
||||
return taskDetails, nil
|
||||
}
|
||||
|
||||
func CachedECSDescribeTaskDefinition(ECSClient AWSECSClientInterface, accountID string, region string, taskDefinition string) (ecsTypes.TaskDefinition, error) {
|
||||
var taskDefinitionDetails ecsTypes.TaskDefinition
|
||||
//replace semi-colons with underscores in task definition name
|
||||
taskDefinitionFileSystemSafe := strings.ReplaceAll(taskDefinition, ":", "_")
|
||||
taskDefinitionFileSystemSafe = strings.ReplaceAll(taskDefinitionFileSystemSafe, "/", "_")
|
||||
cacheKey := fmt.Sprintf("%s-ecs-DescribeTaskDefinition-%s-%s", accountID, region, taskDefinitionFileSystemSafe)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECS task definition details data")
|
||||
return cached.(ecsTypes.TaskDefinition), nil
|
||||
}
|
||||
|
||||
DescribeTaskDefinition, err := ECSClient.DescribeTaskDefinition(
|
||||
context.TODO(),
|
||||
&ecs.DescribeTaskDefinitionInput{
|
||||
TaskDefinition: &taskDefinition,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return ecsTypes.TaskDefinition{}, err
|
||||
}
|
||||
|
||||
taskDefinitionDetails = *DescribeTaskDefinition.TaskDefinition
|
||||
|
||||
internal.Cache.Set(cacheKey, taskDefinitionDetails, cache.DefaultExpiration)
|
||||
return taskDefinitionDetails, nil
|
||||
}
|
||||
|
||||
func CachedECSListServices(ECSClient AWSECSClientInterface, accountID string, region string, cluster string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var services []string
|
||||
//grab cluster name from AWS ARN
|
||||
clusterName := cluster[strings.LastIndex(cluster, "/")+1:]
|
||||
|
||||
cacheKey := fmt.Sprintf("%s-ecs-ListServices-%s-%s", accountID, region, clusterName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached ECS services data")
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListServices, err := ECSClient.ListServices(
|
||||
context.TODO(),
|
||||
&ecs.ListServicesInput{
|
||||
Cluster: &cluster,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *ecs.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
services = append(services, ListServices.ServiceArns...)
|
||||
|
||||
// Pagination control.
|
||||
if ListServices.NextToken != nil {
|
||||
PaginationControl = ListServices.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, services, cache.DefaultExpiration)
|
||||
return services, nil
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/ecs"
|
||||
ecsTypes "github.com/aws/aws-sdk-go-v2/service/ecs/types"
|
||||
)
|
||||
|
||||
const DESCRIBE_TASKS_TEST_FILE = "./test-data/describe-tasks.json"
|
||||
|
||||
type MockedECSClient struct {
|
||||
describeTasks DescribeTasks
|
||||
}
|
||||
|
||||
type ListTasks struct {
|
||||
TaskArns []string `json:"taskArns"`
|
||||
}
|
||||
|
||||
type DescribeTasks struct {
|
||||
Tasks []struct {
|
||||
Attachments []struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Status string `json:"status"`
|
||||
Details []struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
} `json:"details"`
|
||||
} `json:"attachments"`
|
||||
Attributes []struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
} `json:"attributes"`
|
||||
AvailabilityZone string `json:"availabilityZone"`
|
||||
ClusterArn string `json:"clusterArn"`
|
||||
Connectivity string `json:"connectivity"`
|
||||
ConnectivityAt string `json:"connectivityAt"`
|
||||
Containers []struct {
|
||||
ContainerArn string `json:"containerArn"`
|
||||
TaskArn string `json:"taskArn"`
|
||||
Name string `json:"name"`
|
||||
Image string `json:"image"`
|
||||
RuntimeID string `json:"runtimeId"`
|
||||
LastStatus string `json:"lastStatus"`
|
||||
NetworkBindings []interface{} `json:"networkBindings"`
|
||||
NetworkInterfaces []struct {
|
||||
AttachmentID string `json:"attachmentId"`
|
||||
PrivateIpv4Address string `json:"privateIpv4Address"`
|
||||
} `json:"networkInterfaces"`
|
||||
HealthStatus string `json:"healthStatus"`
|
||||
CPU string `json:"cpu"`
|
||||
Memory string `json:"memory"`
|
||||
} `json:"containers"`
|
||||
CPU string `json:"cpu"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
DesiredStatus string `json:"desiredStatus"`
|
||||
EnableExecuteCommand bool `json:"enableExecuteCommand"`
|
||||
Group string `json:"group"`
|
||||
HealthStatus string `json:"healthStatus"`
|
||||
LastStatus string `json:"lastStatus"`
|
||||
LaunchType string `json:"launchType"`
|
||||
Memory string `json:"memory"`
|
||||
Overrides struct {
|
||||
ContainerOverrides []struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"containerOverrides"`
|
||||
InferenceAcceleratorOverrides []interface{} `json:"inferenceAcceleratorOverrides"`
|
||||
} `json:"overrides"`
|
||||
PlatformVersion string `json:"platformVersion"`
|
||||
PlatformFamily string `json:"platformFamily"`
|
||||
PullStartedAt string `json:"pullStartedAt"`
|
||||
PullStoppedAt string `json:"pullStoppedAt"`
|
||||
StartedAt string `json:"startedAt"`
|
||||
StartedBy string `json:"startedBy"`
|
||||
Tags []interface{} `json:"tags"`
|
||||
TaskArn string `json:"taskArn"`
|
||||
TaskDefinitionArn string `json:"taskDefinitionArn"`
|
||||
Version int `json:"version"`
|
||||
EphemeralStorage struct {
|
||||
SizeInGiB int `json:"sizeInGiB"`
|
||||
} `json:"ephemeralStorage"`
|
||||
} `json:"tasks"`
|
||||
Failures []interface{} `json:"failures"`
|
||||
}
|
||||
|
||||
func (c *MockedECSClient) ListClusters(context.Context, *ecs.ListClustersInput, ...func(*ecs.Options)) (*ecs.ListClustersOutput, error) {
|
||||
return &ecs.ListClustersOutput{ClusterArns: []string{
|
||||
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster",
|
||||
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster2",
|
||||
"arn:aws:ecs:us-east-1:123456789012:cluster/MyCluster3",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c *MockedECSClient) ListTasks(ctx context.Context, input *ecs.ListTasksInput, f ...func(*ecs.Options)) (*ecs.ListTasksOutput, error) {
|
||||
return &ecs.ListTasksOutput{TaskArns: []string{
|
||||
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/74de0355a10a4f979ac495c14EXAMPLE",
|
||||
"arn:aws:ecs:us-east-1:123456789012:task/MyCluster/d789e94343414c25b9f6bd59eEXAMPLE",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c *MockedECSClient) ListServices(ctx context.Context, input *ecs.ListServicesInput, f ...func(*ecs.Options)) (*ecs.ListServicesOutput, error) {
|
||||
return &ecs.ListServicesOutput{ServiceArns: []string{
|
||||
"arn:aws:ecs:us-east-1:123456789012:service/MyService",
|
||||
"arn:aws:ecs:us-east-1:123456789012:service/MyService2",
|
||||
"arn:aws:ecs:us-east-1:123456789012:service/MyService3",
|
||||
}}, nil
|
||||
}
|
||||
|
||||
func (c *MockedECSClient) DescribeTasks(ctx context.Context, input *ecs.DescribeTasksInput, f ...func(*ecs.Options)) (*ecs.DescribeTasksOutput, error) {
|
||||
err := json.Unmarshal(readTestFile(DESCRIBE_TASKS_TEST_FILE), &c.describeTasks)
|
||||
if err != nil {
|
||||
log.Fatalf("can't unmarshall file %s", DESCRIBE_TASKS_TEST_FILE)
|
||||
}
|
||||
var tasks []ecsTypes.Task
|
||||
for _, mockedTask := range c.describeTasks.Tasks {
|
||||
if mockedTask.ClusterArn == aws.ToString(input.Cluster) {
|
||||
for _, inputTask := range input.Tasks {
|
||||
if mockedTask.TaskArn == inputTask {
|
||||
var attachments []ecsTypes.Attachment
|
||||
for _, a := range mockedTask.Attachments {
|
||||
var deets []ecsTypes.KeyValuePair
|
||||
for _, detail := range a.Details {
|
||||
deets = append(deets, ecsTypes.KeyValuePair{
|
||||
Name: aws.String(detail.Name),
|
||||
Value: aws.String(detail.Value)})
|
||||
}
|
||||
attachments = append(attachments, ecsTypes.Attachment{
|
||||
Type: aws.String(a.Type),
|
||||
Details: deets,
|
||||
Id: aws.String(a.ID),
|
||||
Status: aws.String(a.Status),
|
||||
})
|
||||
}
|
||||
tasks = append(tasks, ecsTypes.Task{
|
||||
ClusterArn: aws.String(mockedTask.ClusterArn),
|
||||
TaskDefinitionArn: aws.String(mockedTask.TaskDefinitionArn),
|
||||
LaunchType: ecsTypes.LaunchType(*aws.String(mockedTask.LaunchType)),
|
||||
TaskArn: aws.String(mockedTask.TaskArn),
|
||||
Attachments: attachments,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return &ecs.DescribeTasksOutput{Tasks: tasks}, nil
|
||||
}
|
||||
|
||||
func (c *MockedECSClient) DescribeTaskDefinition(ctx context.Context, input *ecs.DescribeTaskDefinitionInput, f ...func(o *ecs.Options)) (*ecs.DescribeTaskDefinitionOutput, error) {
|
||||
testTaskDefinition := ecsTypes.TaskDefinition{}
|
||||
testTaskDefinition.TaskRoleArn = aws.String("test123")
|
||||
return &ecs.DescribeTaskDefinitionOutput{TaskDefinition: &testTaskDefinition}, nil
|
||||
}
|
||||
+178
@@ -0,0 +1,178 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSEFSClientInterface interface {
|
||||
DescribeFileSystems(ctx context.Context, params *efs.DescribeFileSystemsInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error)
|
||||
DescribeMountTargets(ctx context.Context, params *efs.DescribeMountTargetsInput, optFns ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error)
|
||||
DescribeAccessPoints(ctx context.Context, params *efs.DescribeAccessPointsInput, optFns ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error)
|
||||
DescribeFileSystemPolicy(ctx context.Context, params *efs.DescribeFileSystemPolicyInput, optFns ...func(*efs.Options)) (*efs.DescribeFileSystemPolicyOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]efsTypes.FileSystemDescription{})
|
||||
gob.Register([]efsTypes.MountTargetDescription{})
|
||||
gob.Register([]efsTypes.AccessPointDescription{})
|
||||
gob.Register(policy.Policy{})
|
||||
}
|
||||
|
||||
func CachedDescribeFileSystems(EFSClient AWSEFSClientInterface, accountID string, r string) ([]efsTypes.FileSystemDescription, error) {
|
||||
var PaginationMarker *string
|
||||
var filesystems []efsTypes.FileSystemDescription
|
||||
var err error
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribeFileSystems-%s", accountID, r)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]efsTypes.FileSystemDescription), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeFileSystems, err := EFSClient.DescribeFileSystems(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemsInput{
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
filesystems = append(filesystems, DescribeFileSystems.FileSystems...)
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeFileSystems.Marker != nil {
|
||||
PaginationMarker = DescribeFileSystems.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, filesystems, cache.DefaultExpiration)
|
||||
return filesystems, err
|
||||
}
|
||||
|
||||
func CachedDescribeMountTargets(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.MountTargetDescription, error) {
|
||||
var PaginationMarker *string
|
||||
var mountTargets []efsTypes.MountTargetDescription
|
||||
var err error
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribeMountTargets-%s-%s", accountID, r, filesystemId)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]efsTypes.MountTargetDescription), nil
|
||||
}
|
||||
|
||||
for {
|
||||
DescribeMountTargets, err := EFSClient.DescribeMountTargets(
|
||||
context.TODO(),
|
||||
&efs.DescribeMountTargetsInput{
|
||||
FileSystemId: aws.String(filesystemId),
|
||||
Marker: PaginationMarker,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
mountTargets = append(mountTargets, DescribeMountTargets.MountTargets...)
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeMountTargets.Marker != nil {
|
||||
PaginationMarker = DescribeMountTargets.Marker
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, mountTargets, cache.DefaultExpiration)
|
||||
return mountTargets, err
|
||||
}
|
||||
|
||||
func CachedDescribeAccessPoints(EFSClient AWSEFSClientInterface, accountID string, r string, filesystemId string) ([]efsTypes.AccessPointDescription, error) {
|
||||
var PaginationMarker *string
|
||||
var accessPoints []efsTypes.AccessPointDescription
|
||||
var err error
|
||||
cacheKey := fmt.Sprintf("%s-efs-DescribeAccessPoints-%s-%s", accountID, r, filesystemId)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]efsTypes.AccessPointDescription), nil
|
||||
}
|
||||
for {
|
||||
DescribeAccessPoints, err := EFSClient.DescribeAccessPoints(
|
||||
context.TODO(),
|
||||
&efs.DescribeAccessPointsInput{
|
||||
FileSystemId: aws.String(filesystemId),
|
||||
NextToken: PaginationMarker,
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return nil, err
|
||||
}
|
||||
|
||||
accessPoints = append(accessPoints, DescribeAccessPoints.AccessPoints...)
|
||||
|
||||
// Pagination control. After the last page of output, the for loop exits.
|
||||
if DescribeAccessPoints.NextToken != nil {
|
||||
PaginationMarker = DescribeAccessPoints.NextToken
|
||||
} else {
|
||||
PaginationMarker = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
internal.Cache.Set(cacheKey, accessPoints, cache.DefaultExpiration)
|
||||
return accessPoints, err
|
||||
}
|
||||
|
||||
func CachedDescribeFileSystemPolicy(EFSClient AWSEFSClientInterface, filesystemId string, r string, accountID string) (policy.Policy, error) {
|
||||
var efsPolicy policy.Policy
|
||||
var policyJSON string
|
||||
cacheKey := fmt.Sprintf("efs-%s-DescribeFileSystemPolicy-%s-%s", accountID, r, filesystemId)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(policy.Policy), nil
|
||||
}
|
||||
|
||||
Policy, err := EFSClient.DescribeFileSystemPolicy(
|
||||
context.TODO(),
|
||||
&efs.DescribeFileSystemPolicyInput{
|
||||
FileSystemId: aws.String(filesystemId),
|
||||
},
|
||||
func(o *efs.Options) {
|
||||
o.Region = r
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
return efsPolicy, err
|
||||
}
|
||||
|
||||
policyJSON = aws.ToString(Policy.Policy)
|
||||
efsPolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return efsPolicy, fmt.Errorf("parsing policy (%s) as JSON: %s", filesystemId, err)
|
||||
}
|
||||
internal.Cache.Set(cacheKey, efsPolicy, cache.DefaultExpiration)
|
||||
return efsPolicy, nil
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/efs"
|
||||
efsTypes "github.com/aws/aws-sdk-go-v2/service/efs/types"
|
||||
)
|
||||
|
||||
type MockedEfsClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedEfsClient) DescribeFileSystems(ctx context.Context, input *efs.DescribeFileSystemsInput, options ...func(*efs.Options)) (*efs.DescribeFileSystemsOutput, error) {
|
||||
return &efs.DescribeFileSystemsOutput{
|
||||
FileSystems: []efsTypes.FileSystemDescription{
|
||||
{
|
||||
FileSystemId: aws.String("fs-12345678"),
|
||||
},
|
||||
{
|
||||
FileSystemId: aws.String("fs-87654321"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedEfsClient) DescribeMountTargets(ctx context.Context, input *efs.DescribeMountTargetsInput, options ...func(*efs.Options)) (*efs.DescribeMountTargetsOutput, error) {
|
||||
return &efs.DescribeMountTargetsOutput{
|
||||
MountTargets: []efsTypes.MountTargetDescription{
|
||||
{
|
||||
MountTargetId: aws.String("fsmt-12345678"),
|
||||
FileSystemId: aws.String("fs-12345678"),
|
||||
IpAddress: aws.String("10.1.1.1"),
|
||||
},
|
||||
{
|
||||
MountTargetId: aws.String("fsmt-87654321"),
|
||||
FileSystemId: aws.String("fs-87654321"),
|
||||
IpAddress: aws.String("10.2.2.2.2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedEfsClient) DescribeAccessPoints(ctx context.Context, input *efs.DescribeAccessPointsInput, options ...func(*efs.Options)) (*efs.DescribeAccessPointsOutput, error) {
|
||||
return &efs.DescribeAccessPointsOutput{
|
||||
AccessPoints: []efsTypes.AccessPointDescription{
|
||||
{
|
||||
AccessPointId: aws.String("fsap-12345678"),
|
||||
FileSystemId: aws.String("fs-12345678"),
|
||||
Name: aws.String("fsap-12345678"),
|
||||
PosixUser: &efsTypes.PosixUser{
|
||||
Gid: aws.Int64(1000),
|
||||
Uid: aws.Int64(1000),
|
||||
},
|
||||
},
|
||||
{
|
||||
AccessPointId: aws.String("fsap-87654321"),
|
||||
FileSystemId: aws.String("fs-87654321"),
|
||||
Name: aws.String("fsap-12345679"),
|
||||
PosixUser: &efsTypes.PosixUser{
|
||||
Gid: aws.Int64(1000),
|
||||
Uid: aws.Int64(1000),
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+152
@@ -0,0 +1,152 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type EKSClientInterface interface {
|
||||
DescribeCluster(context.Context, *eks.DescribeClusterInput, ...func(*eks.Options)) (*eks.DescribeClusterOutput, error)
|
||||
ListClusters(context.Context, *eks.ListClustersInput, ...func(*eks.Options)) (*eks.ListClustersOutput, error)
|
||||
DescribeNodegroup(context.Context, *eks.DescribeNodegroupInput, ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error)
|
||||
ListNodegroups(context.Context, *eks.ListNodegroupsInput, ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
gob.Register(eksTypes.Cluster{})
|
||||
gob.Register(eksTypes.Nodegroup{})
|
||||
|
||||
}
|
||||
|
||||
func CachedEKSDescribeCluster(client EKSClientInterface, accountID string, clusterName string, region string) (eksTypes.Cluster, error) {
|
||||
var cluster eksTypes.Cluster
|
||||
cacheKey := fmt.Sprintf("%s-eks-DescribeCluster-%s-%s", accountID, region, clusterName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(eksTypes.Cluster), nil
|
||||
}
|
||||
clusterOutput, err := client.DescribeCluster(
|
||||
context.TODO(),
|
||||
&eks.DescribeClusterInput{
|
||||
Name: &clusterName,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return cluster, err
|
||||
}
|
||||
cluster = *clusterOutput.Cluster
|
||||
|
||||
internal.Cache.Set(cacheKey, cluster, cache.DefaultExpiration)
|
||||
return cluster, nil
|
||||
}
|
||||
|
||||
func CachedEKSListClusters(client EKSClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []string
|
||||
cacheKey := fmt.Sprintf("%s-eks-ListClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListClusters, err := client.ListClusters(
|
||||
context.TODO(),
|
||||
&eks.ListClustersInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, ListClusters.Clusters...)
|
||||
|
||||
//pagination
|
||||
if ListClusters.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListClusters.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func CachedEKSListNodeGroups(client EKSClientInterface, accountID string, region string, clusterName string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var nodeGroups []string
|
||||
cacheKey := fmt.Sprintf("%s-eks-ListNodeGroups-%s-%s", accountID, region, clusterName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListNodeGroups, err := client.ListNodegroups(
|
||||
context.TODO(),
|
||||
&eks.ListNodegroupsInput{
|
||||
ClusterName: &clusterName,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return nodeGroups, err
|
||||
}
|
||||
|
||||
nodeGroups = append(nodeGroups, ListNodeGroups.Nodegroups...)
|
||||
if ListNodeGroups.NextToken != nil {
|
||||
PaginationControl = ListNodeGroups.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
internal.Cache.Set(cacheKey, nodeGroups, cache.DefaultExpiration)
|
||||
|
||||
return nodeGroups, nil
|
||||
}
|
||||
|
||||
func CachedEKSDescribeNodeGroup(client EKSClientInterface, accountID string, region string, clusterName string, nodeGroupName string) (eksTypes.Nodegroup, error) {
|
||||
var nodeGroup eksTypes.Nodegroup
|
||||
cacheKey := fmt.Sprintf("%s-eks-DescribeNodeGroup-%s-%s-%s", accountID, region, clusterName, nodeGroupName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(eksTypes.Nodegroup), nil
|
||||
}
|
||||
nodeGroupOutput, err := client.DescribeNodegroup(
|
||||
context.TODO(),
|
||||
&eks.DescribeNodegroupInput{
|
||||
ClusterName: &clusterName,
|
||||
NodegroupName: &nodeGroupName,
|
||||
},
|
||||
func(o *eks.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return nodeGroup, err
|
||||
}
|
||||
nodeGroup = *nodeGroupOutput.Nodegroup
|
||||
|
||||
internal.Cache.Set(cacheKey, nodeGroup, cache.DefaultExpiration)
|
||||
return nodeGroup, nil
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/eks"
|
||||
eksTypes "github.com/aws/aws-sdk-go-v2/service/eks/types"
|
||||
)
|
||||
|
||||
type MockedAWSEksClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedAWSEksClient) ListClusters(ctx context.Context, input *eks.ListClustersInput, options ...func(*eks.Options)) (*eks.ListClustersOutput, error) {
|
||||
return &eks.ListClustersOutput{
|
||||
Clusters: []string{
|
||||
"cluster1",
|
||||
"cluster2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSEksClient) DescribeCluster(ctx context.Context, input *eks.DescribeClusterInput, options ...func(*eks.Options)) (*eks.DescribeClusterOutput, error) {
|
||||
return &eks.DescribeClusterOutput{
|
||||
Cluster: &eksTypes.Cluster{
|
||||
Arn: aws.String("arn:aws:eks:us-east-1:123456789012:cluster/cluster1"),
|
||||
Endpoint: aws.String("https://cluster1.us-east-1.eks.amazonaws.com"),
|
||||
Name: aws.String("cluster1"),
|
||||
PlatformVersion: aws.String("eks.1"),
|
||||
Version: aws.String("1.18"),
|
||||
RoleArn: aws.String("arn:aws:iam::123456789012:role/eks-role"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSEksClient) ListNodegroups(ctx context.Context, input *eks.ListNodegroupsInput, options ...func(*eks.Options)) (*eks.ListNodegroupsOutput, error) {
|
||||
return &eks.ListNodegroupsOutput{
|
||||
Nodegroups: []string{
|
||||
"nodegroup1",
|
||||
"nodegroup2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedAWSEksClient) DescribeNodegroup(ctx context.Context, input *eks.DescribeNodegroupInput, options ...func(*eks.Options)) (*eks.DescribeNodegroupOutput, error) {
|
||||
return &eks.DescribeNodegroupOutput{
|
||||
Nodegroup: &eksTypes.Nodegroup{
|
||||
ClusterName: aws.String("cluster1"),
|
||||
NodeRole: aws.String("arn:aws:iam::123456789012:role/eks-role"),
|
||||
NodegroupName: aws.String("nodegroup1"),
|
||||
NodegroupArn: aws.String("arn:aws:eks:us-east-1:123456789012:nodegroup/cluster1/nodegroup1"),
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticache"
|
||||
elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSElastiCacheClientInterface interface {
|
||||
DescribeCacheClusters(context.Context, *elasticache.DescribeCacheClustersInput, ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]elasticacheTypes.CacheCluster{})
|
||||
|
||||
}
|
||||
|
||||
func CachedElastiCacheDescribeCacheClusters(client AWSElastiCacheClientInterface, accountID string, region string) ([]elasticacheTypes.CacheCluster, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []elasticacheTypes.CacheCluster
|
||||
cacheKey := fmt.Sprintf("%s-elasticache-DescribeCacheClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]elasticacheTypes.CacheCluster), nil
|
||||
}
|
||||
for {
|
||||
DescribeCacheClusters, err := client.DescribeCacheClusters(
|
||||
context.TODO(),
|
||||
&elasticache.DescribeCacheClustersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *elasticache.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, DescribeCacheClusters.CacheClusters...)
|
||||
|
||||
//pagination
|
||||
if DescribeCacheClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeCacheClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticache"
|
||||
elasticacheTypes "github.com/aws/aws-sdk-go-v2/service/elasticache/types"
|
||||
)
|
||||
|
||||
type MockedElasticacheClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedElasticacheClient) DescribeCacheClusters(ctx context.Context, input *elasticache.DescribeCacheClustersInput, options ...func(*elasticache.Options)) (*elasticache.DescribeCacheClustersOutput, error) {
|
||||
return &elasticache.DescribeCacheClustersOutput{
|
||||
CacheClusters: []elasticacheTypes.CacheCluster{
|
||||
{
|
||||
CacheClusterId: aws.String("test"),
|
||||
ARN: aws.String("arn:aws:elasticache:us-east-1:123456789012:cluster:myCluster"),
|
||||
Engine: aws.String("redis"),
|
||||
EngineVersion: aws.String("6.x"),
|
||||
CacheNodeType: aws.String("cache.t3.micro"),
|
||||
NumCacheNodes: aws.Int32(1),
|
||||
CacheClusterStatus: aws.String("available"),
|
||||
PreferredAvailabilityZone: aws.String("us-east-1a"),
|
||||
CacheSubnetGroupName: aws.String("default"),
|
||||
ReplicationGroupId: aws.String("test"),
|
||||
SecurityGroups: []elasticacheTypes.SecurityGroupMembership{
|
||||
{
|
||||
SecurityGroupId: aws.String("test"),
|
||||
Status: aws.String("active"),
|
||||
},
|
||||
},
|
||||
|
||||
AutoMinorVersionUpgrade: aws.Bool(true),
|
||||
PreferredMaintenanceWindow: aws.String("sun:05:00-sun:06:00"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk"
|
||||
elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types"
|
||||
)
|
||||
|
||||
type AWSElasticBeanstalkClientInterface interface {
|
||||
DescribeApplications(context.Context, *elasticbeanstalk.DescribeApplicationsInput, ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]elasticbeanstalkTypes.ApplicationDescription{})
|
||||
}
|
||||
|
||||
func CachedElasticBeanstalkDescribeApplications(client AWSElasticBeanstalkClientInterface, accountID string, region string) ([]elasticbeanstalkTypes.ApplicationDescription, error) {
|
||||
var applications []elasticbeanstalkTypes.ApplicationDescription
|
||||
DescribeApplications, err := client.DescribeApplications(
|
||||
context.TODO(),
|
||||
&elasticbeanstalk.DescribeApplicationsInput{},
|
||||
func(o *elasticbeanstalk.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return applications, err
|
||||
}
|
||||
|
||||
applications = append(applications, DescribeApplications.Applications...)
|
||||
return applications, nil
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk"
|
||||
elasticbeanstalkTypes "github.com/aws/aws-sdk-go-v2/service/elasticbeanstalk/types"
|
||||
)
|
||||
|
||||
type MockedElasticBeanstalkClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedElasticBeanstalkClient) DescribeApplications(ctx context.Context, input *elasticbeanstalk.DescribeApplicationsInput, options ...func(*elasticbeanstalk.Options)) (*elasticbeanstalk.DescribeApplicationsOutput, error) {
|
||||
return &elasticbeanstalk.DescribeApplicationsOutput{
|
||||
Applications: []elasticbeanstalkTypes.ApplicationDescription{
|
||||
{
|
||||
ApplicationName: aws.String("app1"),
|
||||
ApplicationArn: aws.String("arn:aws:elasticbeanstalk:us-east-1:123456789012:application/app1"),
|
||||
ConfigurationTemplates: []string{
|
||||
"template1",
|
||||
},
|
||||
},
|
||||
{
|
||||
ApplicationName: aws.String("app2"),
|
||||
ApplicationArn: aws.String("arn:aws:elasticbeanstalk:us-east-1:123456789012:application/app2"),
|
||||
ConfigurationTemplates: []string{
|
||||
"template2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
|
||||
elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type ELBClientInterface interface {
|
||||
DescribeLoadBalancers(context.Context, *elasticloadbalancing.DescribeLoadBalancersInput, ...func(*elasticloadbalancing.Options)) (*elasticloadbalancing.DescribeLoadBalancersOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]elbTypes.LoadBalancerDescription{})
|
||||
}
|
||||
|
||||
func CachedELBDescribeLoadBalancers(client ELBClientInterface, accountID string, region string) ([]elbTypes.LoadBalancerDescription, error) {
|
||||
var PaginationControl *string
|
||||
var loadbalancers []elbTypes.LoadBalancerDescription
|
||||
cacheKey := fmt.Sprintf("%s-elb-DescribeLoadBalancers-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]elbTypes.LoadBalancerDescription), nil
|
||||
}
|
||||
for {
|
||||
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
|
||||
context.TODO(),
|
||||
&elasticloadbalancing.DescribeLoadBalancersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *elasticloadbalancing.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return loadbalancers, err
|
||||
}
|
||||
|
||||
loadbalancers = append(loadbalancers, DescribeLoadBalancers.LoadBalancerDescriptions...)
|
||||
|
||||
//pagination
|
||||
if DescribeLoadBalancers.NextMarker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeLoadBalancers.NextMarker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, loadbalancers, cache.DefaultExpiration)
|
||||
return loadbalancers, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing"
|
||||
elbTypes "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancing/types"
|
||||
)
|
||||
|
||||
type MockedElbClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedElbClient) DescribeLoadBalancers(ctx context.Context, input *elasticloadbalancing.DescribeLoadBalancersInput, options ...func(*elasticloadbalancing.Options)) (*elasticloadbalancing.DescribeLoadBalancersOutput, error) {
|
||||
return &elasticloadbalancing.DescribeLoadBalancersOutput{
|
||||
LoadBalancerDescriptions: []elbTypes.LoadBalancerDescription{
|
||||
{
|
||||
LoadBalancerName: aws.String("elb1"),
|
||||
DNSName: aws.String("elb1"),
|
||||
Instances: []elbTypes.Instance{
|
||||
{
|
||||
InstanceId: aws.String("i-1234567890abcdef0"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
LoadBalancerName: aws.String("elb2"),
|
||||
DNSName: aws.String("elb2"),
|
||||
Instances: []elbTypes.Instance{
|
||||
{
|
||||
InstanceId: aws.String("i-1234567890abcdef1"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
|
||||
elbV2Types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type ELBv2ClientInterface interface {
|
||||
DescribeLoadBalancers(context.Context, *elasticloadbalancingv2.DescribeLoadBalancersInput, ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]elbV2Types.LoadBalancer{})
|
||||
}
|
||||
|
||||
func CachedELBv2DescribeLoadBalancers(client ELBv2ClientInterface, accountID string, region string) ([]elbV2Types.LoadBalancer, error) {
|
||||
var PaginationControl *string
|
||||
var loadbalancers []elbV2Types.LoadBalancer
|
||||
cacheKey := fmt.Sprintf("%s-elbv2-DescribeLoadBalancers-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]elbV2Types.LoadBalancer), nil
|
||||
}
|
||||
for {
|
||||
DescribeLoadBalancers, err := client.DescribeLoadBalancers(
|
||||
context.TODO(),
|
||||
&elasticloadbalancingv2.DescribeLoadBalancersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *elasticloadbalancingv2.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return loadbalancers, err
|
||||
}
|
||||
|
||||
loadbalancers = append(loadbalancers, DescribeLoadBalancers.LoadBalancers...)
|
||||
|
||||
//pagination
|
||||
if DescribeLoadBalancers.NextMarker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = DescribeLoadBalancers.NextMarker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, loadbalancers, cache.DefaultExpiration)
|
||||
return loadbalancers, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2"
|
||||
elbv2Types "github.com/aws/aws-sdk-go-v2/service/elasticloadbalancingv2/types"
|
||||
)
|
||||
|
||||
type MockedElbv2Client struct {
|
||||
}
|
||||
|
||||
func (m *MockedElbv2Client) DescribeLoadBalancers(ctx context.Context, input *elasticloadbalancingv2.DescribeLoadBalancersInput, options ...func(*elasticloadbalancingv2.Options)) (*elasticloadbalancingv2.DescribeLoadBalancersOutput, error) {
|
||||
return &elasticloadbalancingv2.DescribeLoadBalancersOutput{
|
||||
LoadBalancers: []elbv2Types.LoadBalancer{
|
||||
{
|
||||
LoadBalancerArn: aws.String("arn:aws:elasticloadbalancing:us-east-1:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188"),
|
||||
DNSName: aws.String("my-load-balancer-424835706.us-east-1.elb.amazonaws.com"),
|
||||
CanonicalHostedZoneId: aws.String("Z2P70J7EXAMPLE"),
|
||||
CreatedTime: aws.Time(time.Now()),
|
||||
LoadBalancerName: aws.String("my-load-balancer"),
|
||||
Scheme: elbv2Types.LoadBalancerSchemeEnumInternetFacing,
|
||||
VpcId: aws.String("vpc-3ac0fb5f"),
|
||||
State: &elbv2Types.LoadBalancerState{
|
||||
Code: elbv2Types.LoadBalancerStateEnumActive,
|
||||
Reason: aws.String(""),
|
||||
},
|
||||
Type: elbv2Types.LoadBalancerTypeEnumApplication,
|
||||
AvailabilityZones: []elbv2Types.AvailabilityZone{
|
||||
{
|
||||
ZoneName: aws.String("us-east-1a"),
|
||||
SubnetId: aws.String("subnet-8360a9e7"),
|
||||
LoadBalancerAddresses: []elbv2Types.LoadBalancerAddress{
|
||||
{
|
||||
IpAddress: aws.String("1.2.3.4"),
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
ZoneName: aws.String("us-east-1b"),
|
||||
SubnetId: aws.String("subnet-b7d581c0"),
|
||||
LoadBalancerAddresses: []elbv2Types.LoadBalancerAddress{
|
||||
{
|
||||
IpAddress: aws.String("2.3.4.5"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
SecurityGroups: []string{
|
||||
"sg-5943793c",
|
||||
},
|
||||
IpAddressType: elbv2Types.IpAddressTypeDualstack,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,98 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/emr"
|
||||
emrTypes "github.com/aws/aws-sdk-go-v2/service/emr/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSEMRClientInterface interface {
|
||||
ListClusters(context.Context, *emr.ListClustersInput, ...func(*emr.Options)) (*emr.ListClustersOutput, error)
|
||||
ListInstances(context.Context, *emr.ListInstancesInput, ...func(*emr.Options)) (*emr.ListInstancesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]emrTypes.ClusterSummary{})
|
||||
|
||||
//need to do this to avoid conflicts with the Instance type in the ec2 package
|
||||
type EMRInstance emrTypes.Instance
|
||||
gob.Register([]EMRInstance{})
|
||||
}
|
||||
|
||||
func CachedEMRListClusters(client AWSEMRClientInterface, accountID string, region string) ([]emrTypes.ClusterSummary, error) {
|
||||
var PaginationControl *string
|
||||
var clusters []emrTypes.ClusterSummary
|
||||
cacheKey := fmt.Sprintf("%s-emr-ListClusters-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]emrTypes.ClusterSummary), nil
|
||||
}
|
||||
for {
|
||||
ListClusters, err := client.ListClusters(
|
||||
context.TODO(),
|
||||
&emr.ListClustersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *emr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return clusters, err
|
||||
}
|
||||
|
||||
clusters = append(clusters, ListClusters.Clusters...)
|
||||
|
||||
//pagination
|
||||
if ListClusters.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListClusters.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, clusters, cache.DefaultExpiration)
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
func CachedEMRListInstances(client AWSEMRClientInterface, accountID string, region string, clusterID string) ([]emrTypes.Instance, error) {
|
||||
var PaginationControl *string
|
||||
var instances []emrTypes.Instance
|
||||
cacheKey := fmt.Sprintf("%s-emr-ListInstances-%s-%s", accountID, region, clusterID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]emrTypes.Instance), nil
|
||||
}
|
||||
for {
|
||||
ListInstances, err := client.ListInstances(
|
||||
context.TODO(),
|
||||
&emr.ListInstancesInput{
|
||||
ClusterId: &clusterID,
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
func(o *emr.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return instances, err
|
||||
}
|
||||
|
||||
instances = append(instances, ListInstances.Instances...)
|
||||
|
||||
//pagination
|
||||
if ListInstances.Marker == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListInstances.Marker
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, instances, cache.DefaultExpiration)
|
||||
return instances, nil
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/emr"
|
||||
emrTypes "github.com/aws/aws-sdk-go-v2/service/emr/types"
|
||||
)
|
||||
|
||||
type MockedEMRClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedEMRClient) ListClusters(ctx context.Context, input *emr.ListClustersInput, options ...func(*emr.Options)) (*emr.ListClustersOutput, error) {
|
||||
return &emr.ListClustersOutput{
|
||||
Clusters: []emrTypes.ClusterSummary{
|
||||
{
|
||||
Id: aws.String("cluster1"),
|
||||
},
|
||||
{
|
||||
Id: aws.String("cluster2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedEMRClient) ListInstances(ctx context.Context, input *emr.ListInstancesInput, options ...func(*emr.Options)) (*emr.ListInstancesOutput, error) {
|
||||
return &emr.ListInstancesOutput{
|
||||
Instances: []emrTypes.Instance{
|
||||
{
|
||||
Id: aws.String("instance1"),
|
||||
InstanceType: aws.String("m5.xlarge"),
|
||||
Ec2InstanceId: aws.String("i-1234567890"),
|
||||
PrivateDnsName: aws.String("ip-10-0-0-1.ec2.internal"),
|
||||
PublicDnsName: aws.String("ec2-1-2-3-4.compute-1.amazonaws.com"),
|
||||
PrivateIpAddress: aws.String("10.0.0.1"),
|
||||
PublicIpAddress: aws.String("1.2.3.4"),
|
||||
},
|
||||
{
|
||||
Id: aws.String("instance2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+248
@@ -0,0 +1,248 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/BishopFox/cloudfox/internal/aws/policy"
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/glue"
|
||||
glueTypes "github.com/aws/aws-sdk-go-v2/service/glue/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSGlueClientInterface interface {
|
||||
ListDevEndpoints(ctx context.Context, params *glue.ListDevEndpointsInput, optFns ...func(*glue.Options)) (*glue.ListDevEndpointsOutput, error)
|
||||
ListJobs(ctx context.Context, params *glue.ListJobsInput, optFns ...func(*glue.Options)) (*glue.ListJobsOutput, error)
|
||||
GetTables(ctx context.Context, params *glue.GetTablesInput, optFns ...func(*glue.Options)) (*glue.GetTablesOutput, error)
|
||||
GetDatabases(ctx context.Context, params *glue.GetDatabasesInput, optFns ...func(*glue.Options)) (*glue.GetDatabasesOutput, error)
|
||||
GetResourcePolicies(ctx context.Context, params *glue.GetResourcePoliciesInput, optFns ...func(*glue.Options)) (*glue.GetResourcePoliciesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
gob.Register(glueTypes.DevEndpoint{})
|
||||
gob.Register(glueTypes.Job{})
|
||||
gob.Register([]glueTypes.Table{})
|
||||
gob.Register([]glueTypes.Database{})
|
||||
gob.Register([]policy.Policy{})
|
||||
}
|
||||
|
||||
func CachedGlueListDevEndpoints(GlueClient AWSGlueClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var devEndpoints []string
|
||||
cacheKey := "glue-ListDevEndpoints-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Glue dev endpoints data")
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListDevEndpoints, err := GlueClient.ListDevEndpoints(
|
||||
context.TODO(),
|
||||
&glue.ListDevEndpointsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *glue.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
devEndpoints = append(devEndpoints, ListDevEndpoints.DevEndpointNames...)
|
||||
|
||||
// Pagination control.
|
||||
if aws.ToString(ListDevEndpoints.NextToken) != "" {
|
||||
PaginationControl = ListDevEndpoints.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, devEndpoints, cache.DefaultExpiration)
|
||||
|
||||
return devEndpoints, nil
|
||||
}
|
||||
|
||||
func CachedGlueListJobs(GlueClient AWSGlueClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var jobs []string
|
||||
cacheKey := "glue-ListJobs-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Glue jobs data")
|
||||
return cached.([]string), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListJobs, err := GlueClient.ListJobs(
|
||||
context.TODO(),
|
||||
&glue.ListJobsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *glue.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
jobs = append(jobs, ListJobs.JobNames...)
|
||||
|
||||
// Pagination control.
|
||||
if ListJobs.NextToken != nil {
|
||||
PaginationControl = ListJobs.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, jobs, cache.DefaultExpiration)
|
||||
|
||||
return jobs, nil
|
||||
}
|
||||
|
||||
func CachedGlueGetTables(GlueClient AWSGlueClientInterface, accountID string, region string, dbName string) ([]glueTypes.Table, error) {
|
||||
var PaginationControl *string
|
||||
var tables []glueTypes.Table
|
||||
cacheKey := "glue-GetTables-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Glue tables data")
|
||||
return cached.([]glueTypes.Table), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetTables, err := GlueClient.GetTables(
|
||||
context.TODO(),
|
||||
&glue.GetTablesInput{
|
||||
DatabaseName: &dbName,
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *glue.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
tables = append(tables, GetTables.TableList...)
|
||||
|
||||
// Pagination control.
|
||||
if GetTables.NextToken != nil {
|
||||
PaginationControl = GetTables.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, tables, cache.DefaultExpiration)
|
||||
|
||||
return tables, nil
|
||||
}
|
||||
|
||||
func CachedGlueGetDatabases(GlueClient AWSGlueClientInterface, accountID string, region string) ([]glueTypes.Database, error) {
|
||||
var PaginationControl *string
|
||||
var databases []glueTypes.Database
|
||||
cacheKey := "glue-GetDatabases-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Glue databases data")
|
||||
return cached.([]glueTypes.Database), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetDatabases, err := GlueClient.GetDatabases(
|
||||
context.TODO(),
|
||||
&glue.GetDatabasesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *glue.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
databases = append(databases, GetDatabases.DatabaseList...)
|
||||
|
||||
// Pagination control.
|
||||
if GetDatabases.NextToken != nil {
|
||||
PaginationControl = GetDatabases.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, databases, cache.DefaultExpiration)
|
||||
|
||||
return databases, nil
|
||||
}
|
||||
|
||||
func CachedGlueGetResourcePolicies(GlueClient AWSGlueClientInterface, accountID string, region string) ([]policy.Policy, error) {
|
||||
var PaginationControl *string
|
||||
var GluePolicy policy.Policy
|
||||
var policies []policy.Policy
|
||||
var policyJSON string
|
||||
cacheKey := "glue-GetResourcePolicies-" + accountID + "-" + region
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
sharedLogger.Debug("Using cached Glue resource policies data")
|
||||
return cached.([]policy.Policy), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetResourcePolicies, err := GlueClient.GetResourcePolicies(
|
||||
context.TODO(),
|
||||
&glue.GetResourcePoliciesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *glue.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
for _, policyPointer := range GetResourcePolicies.GetResourcePoliciesResponseList {
|
||||
policyJSON = aws.ToString(policyPointer.PolicyInJson)
|
||||
GluePolicy, err = policy.ParseJSONPolicy([]byte(policyJSON))
|
||||
if err != nil {
|
||||
return policies, errors.New("error parsing Glue policy")
|
||||
}
|
||||
policies = append(policies, GluePolicy)
|
||||
}
|
||||
|
||||
// Pagination control.
|
||||
if GetResourcePolicies.NextToken != nil {
|
||||
PaginationControl = GetResourcePolicies.NextToken
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, policies, cache.DefaultExpiration)
|
||||
|
||||
return policies, nil
|
||||
}
|
||||
|
||||
// in the resource trust command, need to parse the actual resource policies to determine the resources for the cloudfox table. crazy pants
|
||||
@@ -0,0 +1,80 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/glue"
|
||||
glueTypes "github.com/aws/aws-sdk-go-v2/service/glue/types"
|
||||
)
|
||||
|
||||
type MockedGlueClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedGlueClient) ListDevEndpoints(ctx context.Context, input *glue.ListDevEndpointsInput, options ...func(*glue.Options)) (*glue.ListDevEndpointsOutput, error) {
|
||||
return &glue.ListDevEndpointsOutput{
|
||||
DevEndpointNames: []string{
|
||||
"devendpoint1",
|
||||
"devendpoint2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedGlueClient) ListJobs(ctx context.Context, input *glue.ListJobsInput, options ...func(*glue.Options)) (*glue.ListJobsOutput, error) {
|
||||
return &glue.ListJobsOutput{
|
||||
JobNames: []string{
|
||||
"job1",
|
||||
"job2",
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedGlueClient) GetTables(ctx context.Context, input *glue.GetTablesInput, options ...func(*glue.Options)) (*glue.GetTablesOutput, error) {
|
||||
return &glue.GetTablesOutput{
|
||||
TableList: []glueTypes.Table{
|
||||
{
|
||||
Name: aws.String("table1"),
|
||||
DatabaseName: aws.String("database1"),
|
||||
Description: aws.String("description1"),
|
||||
Parameters: map[string]string{
|
||||
"param1": "value1",
|
||||
"param2": "value2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("table2"),
|
||||
DatabaseName: aws.String("database2"),
|
||||
Description: aws.String("description2"),
|
||||
Parameters: map[string]string{
|
||||
"param1": "value1",
|
||||
"param2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedGlueClient) GetDatabases(ctx context.Context, input *glue.GetDatabasesInput, options ...func(*glue.Options)) (*glue.GetDatabasesOutput, error) {
|
||||
return &glue.GetDatabasesOutput{
|
||||
DatabaseList: []glueTypes.Database{
|
||||
{
|
||||
Name: aws.String("database1"),
|
||||
Description: aws.String("description1"),
|
||||
LocationUri: aws.String("s3://bucket1"),
|
||||
Parameters: map[string]string{
|
||||
"param1": "value1",
|
||||
"param2": "value2",
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: aws.String("database2"),
|
||||
Description: aws.String("description2"),
|
||||
LocationUri: aws.String("s3://bucket2"),
|
||||
Parameters: map[string]string{
|
||||
"param1": "value1",
|
||||
"param2": "value2",
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/grafana"
|
||||
grafanaTypes "github.com/aws/aws-sdk-go-v2/service/grafana/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type GrafanaClientInterface interface {
|
||||
ListWorkspaces(context.Context, *grafana.ListWorkspacesInput, ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]grafanaTypes.WorkspaceSummary{})
|
||||
}
|
||||
|
||||
func CachedGrafanaListWorkspaces(client GrafanaClientInterface, accountID string, region string) ([]grafanaTypes.WorkspaceSummary, error) {
|
||||
var PaginationControl *string
|
||||
var workspaces []grafanaTypes.WorkspaceSummary
|
||||
cacheKey := fmt.Sprintf("%s-grafana-ListWorkspaces-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]grafanaTypes.WorkspaceSummary), nil
|
||||
}
|
||||
|
||||
for {
|
||||
|
||||
ListWorkspaces, err := client.ListWorkspaces(
|
||||
context.TODO(),
|
||||
&grafana.ListWorkspacesInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *grafana.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return workspaces, err
|
||||
|
||||
}
|
||||
|
||||
workspaces = append(workspaces, ListWorkspaces.Workspaces...)
|
||||
|
||||
//pagination
|
||||
if ListWorkspaces.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListWorkspaces.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, workspaces, cache.DefaultExpiration)
|
||||
return workspaces, nil
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/grafana"
|
||||
grafanaTypes "github.com/aws/aws-sdk-go-v2/service/grafana/types"
|
||||
)
|
||||
|
||||
type MockedGrafanaClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedGrafanaClient) ListWorkspaces(ctx context.Context, input *grafana.ListWorkspacesInput, options ...func(*grafana.Options)) (*grafana.ListWorkspacesOutput, error) {
|
||||
return &grafana.ListWorkspacesOutput{
|
||||
Workspaces: []grafanaTypes.WorkspaceSummary{
|
||||
{
|
||||
Authentication: &grafanaTypes.AuthenticationSummary{
|
||||
Providers: []grafanaTypes.AuthenticationProviderTypes{
|
||||
grafanaTypes.AuthenticationProviderTypesAwsSso,
|
||||
},
|
||||
},
|
||||
Created: aws.Time(time.Now()),
|
||||
Id: aws.String("workspace1"),
|
||||
Name: aws.String("workspace1"),
|
||||
Status: grafanaTypes.WorkspaceStatusActive,
|
||||
},
|
||||
{
|
||||
Authentication: &grafanaTypes.AuthenticationSummary{
|
||||
Providers: []grafanaTypes.AuthenticationProviderTypes{
|
||||
grafanaTypes.AuthenticationProviderTypesAwsSso,
|
||||
},
|
||||
},
|
||||
Created: aws.Time(time.Now()),
|
||||
Id: aws.String("workspace2"),
|
||||
Name: aws.String("workspace2"),
|
||||
Status: grafanaTypes.WorkspaceStatusActive,
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
+317
@@ -0,0 +1,317 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/md5"
|
||||
"encoding/gob"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/aws/arn"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSIAMClientInterface interface {
|
||||
ListUsers(ctx context.Context, params *iam.ListUsersInput, optFns ...func(*iam.Options)) (*iam.ListUsersOutput, error)
|
||||
ListAccessKeys(ctx context.Context, params *iam.ListAccessKeysInput, optFns ...func(*iam.Options)) (*iam.ListAccessKeysOutput, error)
|
||||
ListRoles(ctx context.Context, params *iam.ListRolesInput, optFns ...func(*iam.Options)) (*iam.ListRolesOutput, error)
|
||||
GetAccountAuthorizationDetails(ctx context.Context, params *iam.GetAccountAuthorizationDetailsInput, optFns ...func(*iam.Options)) (*iam.GetAccountAuthorizationDetailsOutput, error)
|
||||
SimulatePrincipalPolicy(ctx context.Context, params *iam.SimulatePrincipalPolicyInput, optFns ...func(*iam.Options)) (*iam.SimulatePrincipalPolicyOutput, error)
|
||||
ListInstanceProfiles(ctx context.Context, params *iam.ListInstanceProfilesInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error)
|
||||
ListGroups(ctx context.Context, params *iam.ListGroupsInput, optFns ...func(*iam.Options)) (*iam.ListGroupsOutput, error)
|
||||
GetInstanceProfile(ctx context.Context, params *iam.GetInstanceProfileInput, optFns ...func(*iam.Options)) (*iam.GetInstanceProfileOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]iamTypes.User{})
|
||||
gob.Register([]iamTypes.AccessKeyMetadata{})
|
||||
gob.Register([]iamTypes.Role{})
|
||||
gob.Register([]iamTypes.Group{})
|
||||
gob.Register([]iamTypes.PolicyDetail{})
|
||||
gob.Register([]iamTypes.InstanceProfile{})
|
||||
gob.Register([]iamTypes.EvaluationResult{})
|
||||
gob.Register(customGAADOutput{})
|
||||
gob.Register(iamTypes.InstanceProfile{})
|
||||
}
|
||||
|
||||
func CachedIamListUsers(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.User, error) {
|
||||
var PaginationControl *string
|
||||
var users []iamTypes.User
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListUsers", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.User), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListUsers, err := IAMClient.ListUsers(
|
||||
context.TODO(),
|
||||
&iam.ListUsersInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
users = append(users, ListUsers.Users...)
|
||||
|
||||
// Pagination control.
|
||||
if ListUsers.Marker != nil {
|
||||
PaginationControl = ListUsers.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, users, cache.DefaultExpiration)
|
||||
return users, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamListRoles(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.Role, error) {
|
||||
var PaginationControl *string
|
||||
var roles []iamTypes.Role
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListRoles", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.Role), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListRoles, err := IAMClient.ListRoles(
|
||||
context.TODO(),
|
||||
&iam.ListRolesInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
sharedLogger.Error(err.Error())
|
||||
break
|
||||
}
|
||||
|
||||
roles = append(roles, ListRoles.Roles...)
|
||||
|
||||
// Pagination control.
|
||||
if ListRoles.Marker != nil {
|
||||
PaginationControl = ListRoles.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, roles, cache.DefaultExpiration)
|
||||
return roles, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamListAccessKeys(IAMClient AWSIAMClientInterface, accountID string, userName string) ([]iamTypes.AccessKeyMetadata, error) {
|
||||
var PaginationControl *string
|
||||
var accessKeys []iamTypes.AccessKeyMetadata
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListAccessKeys-%s", accountID, userName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.AccessKeyMetadata), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListAccessKeys, err := IAMClient.ListAccessKeys(
|
||||
context.TODO(),
|
||||
&iam.ListAccessKeysInput{
|
||||
UserName: &userName,
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return accessKeys, err
|
||||
}
|
||||
|
||||
accessKeys = append(accessKeys, ListAccessKeys.AccessKeyMetadata...)
|
||||
|
||||
// Pagination control.
|
||||
if ListAccessKeys.Marker != nil {
|
||||
PaginationControl = ListAccessKeys.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, accessKeys, cache.DefaultExpiration)
|
||||
return accessKeys, nil
|
||||
|
||||
}
|
||||
|
||||
type customGAADOutput struct {
|
||||
GroupDetailList []iamTypes.GroupDetail
|
||||
UserDetailList []iamTypes.UserDetail
|
||||
RoleDetailList []iamTypes.RoleDetail
|
||||
Policies []iamTypes.ManagedPolicyDetail
|
||||
}
|
||||
|
||||
func CachedIAMGetAccountAuthorizationDetails(IAMClient AWSIAMClientInterface, accountID string) (customGAADOutput, error) {
|
||||
var PaginationControl *string
|
||||
var GroupDetailList []iamTypes.GroupDetail
|
||||
var UserDetailList []iamTypes.UserDetail
|
||||
var RoleDetailList []iamTypes.RoleDetail
|
||||
var Policies []iamTypes.ManagedPolicyDetail
|
||||
var AllAccountAuthorizationDetails = customGAADOutput{
|
||||
GroupDetailList: GroupDetailList,
|
||||
UserDetailList: UserDetailList,
|
||||
RoleDetailList: RoleDetailList,
|
||||
Policies: Policies,
|
||||
}
|
||||
|
||||
cacheKey := fmt.Sprintf("%s-iam-GetAccountAuthorizationDetails", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(customGAADOutput), nil
|
||||
}
|
||||
|
||||
for {
|
||||
GetAccountAuthorizationDetails, err := IAMClient.GetAccountAuthorizationDetails(
|
||||
context.TODO(),
|
||||
&iam.GetAccountAuthorizationDetailsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return AllAccountAuthorizationDetails, err
|
||||
}
|
||||
|
||||
AllAccountAuthorizationDetails.GroupDetailList = append(AllAccountAuthorizationDetails.GroupDetailList, GetAccountAuthorizationDetails.GroupDetailList...)
|
||||
AllAccountAuthorizationDetails.UserDetailList = append(AllAccountAuthorizationDetails.UserDetailList, GetAccountAuthorizationDetails.UserDetailList...)
|
||||
AllAccountAuthorizationDetails.RoleDetailList = append(AllAccountAuthorizationDetails.RoleDetailList, GetAccountAuthorizationDetails.RoleDetailList...)
|
||||
AllAccountAuthorizationDetails.Policies = append(AllAccountAuthorizationDetails.Policies, GetAccountAuthorizationDetails.Policies...)
|
||||
|
||||
if GetAccountAuthorizationDetails.Marker != nil {
|
||||
PaginationControl = GetAccountAuthorizationDetails.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, AllAccountAuthorizationDetails, cache.DefaultExpiration)
|
||||
return AllAccountAuthorizationDetails, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamSimulatePrincipalPolicy(IAMClient AWSIAMClientInterface, accountID string, principal *string, actionNames []string, resourceArns []string) ([]iamTypes.EvaluationResult, error) {
|
||||
var PaginationControl2 *string
|
||||
var EvaluationResults []iamTypes.EvaluationResult
|
||||
md5hashedActionNames := md5.Sum([]byte(strings.Join(actionNames, "")))
|
||||
md5hashedResourceArns := md5.Sum([]byte(strings.Join(resourceArns, "")))
|
||||
// process arn and get the name of the resource
|
||||
arn, err := arn.Parse(*principal)
|
||||
if err != nil {
|
||||
return EvaluationResults, err
|
||||
}
|
||||
name := arn.Resource
|
||||
// if name has muliplte / then get the last one
|
||||
if strings.Contains(arn.Resource, "/") {
|
||||
name = arn.Resource[strings.LastIndex(arn.Resource, "/")+1:]
|
||||
}
|
||||
|
||||
truncatedHashLength := 8
|
||||
cacheKey := fmt.Sprintf("%s-iamSimulator-%s-%s-%s", accountID, name, hex.EncodeToString(md5hashedActionNames[:truncatedHashLength]), hex.EncodeToString(md5hashedResourceArns[:truncatedHashLength]))
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.EvaluationResult), nil
|
||||
}
|
||||
|
||||
for {
|
||||
SimulatePrincipalPolicy, err := IAMClient.SimulatePrincipalPolicy(
|
||||
context.TODO(),
|
||||
&iam.SimulatePrincipalPolicyInput{
|
||||
Marker: PaginationControl2,
|
||||
ActionNames: actionNames,
|
||||
PolicySourceArn: principal,
|
||||
ResourceArns: resourceArns,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return EvaluationResults, err
|
||||
}
|
||||
|
||||
EvaluationResults = append(EvaluationResults, SimulatePrincipalPolicy.EvaluationResults...)
|
||||
|
||||
if SimulatePrincipalPolicy.Marker != nil {
|
||||
PaginationControl2 = SimulatePrincipalPolicy.Marker
|
||||
} else {
|
||||
PaginationControl2 = nil
|
||||
break
|
||||
}
|
||||
|
||||
}
|
||||
internal.Cache.Set(cacheKey, EvaluationResults, cache.DefaultExpiration)
|
||||
return EvaluationResults, nil
|
||||
}
|
||||
|
||||
func CachedIamListGroups(IAMClient AWSIAMClientInterface, accountID string) ([]iamTypes.Group, error) {
|
||||
var PaginationControl *string
|
||||
var Groups []iamTypes.Group
|
||||
cacheKey := fmt.Sprintf("%s-iam-ListGroups", accountID)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]iamTypes.Group), nil
|
||||
}
|
||||
|
||||
for {
|
||||
ListGroups, err := IAMClient.ListGroups(
|
||||
context.TODO(),
|
||||
&iam.ListGroupsInput{
|
||||
Marker: PaginationControl,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return Groups, err
|
||||
}
|
||||
|
||||
Groups = append(Groups, ListGroups.Groups...)
|
||||
|
||||
// Pagination control.
|
||||
if ListGroups.Marker != nil {
|
||||
PaginationControl = ListGroups.Marker
|
||||
} else {
|
||||
PaginationControl = nil
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, Groups, cache.DefaultExpiration)
|
||||
return Groups, nil
|
||||
|
||||
}
|
||||
|
||||
func CachedIamGetInstanceProfile(IAMClient AWSIAMClientInterface, accountID string, instanceProfileName string) (iamTypes.InstanceProfile, error) {
|
||||
var InstanceProfile iamTypes.InstanceProfile
|
||||
cacheKey := fmt.Sprintf("%s-iam-GetInstanceProfile-%s", accountID, instanceProfileName)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.(iamTypes.InstanceProfile), nil
|
||||
}
|
||||
|
||||
GetInstanceProfile, err := IAMClient.GetInstanceProfile(
|
||||
context.TODO(),
|
||||
&iam.GetInstanceProfileInput{
|
||||
InstanceProfileName: &instanceProfileName,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return InstanceProfile, err
|
||||
}
|
||||
|
||||
InstanceProfile = *GetInstanceProfile.InstanceProfile
|
||||
|
||||
internal.Cache.Set(cacheKey, InstanceProfile, cache.DefaultExpiration)
|
||||
return InstanceProfile, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,222 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go-v2/aws"
|
||||
"github.com/aws/aws-sdk-go-v2/service/iam"
|
||||
iamTypes "github.com/aws/aws-sdk-go-v2/service/iam/types"
|
||||
)
|
||||
|
||||
type MockedIAMClient struct {
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) ListUsers(ctx context.Context, input *iam.ListUsersInput, options ...func(*iam.Options)) (*iam.ListUsersOutput, error) {
|
||||
return &iam.ListUsersOutput{
|
||||
Users: []iamTypes.User{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/user1"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
Path: aws.String("/"),
|
||||
UserId: aws.String("123456789012"),
|
||||
UserName: aws.String("user1"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/user2"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
Path: aws.String("/"),
|
||||
UserId: aws.String("123456789012"),
|
||||
UserName: aws.String("user2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) ListAccessKeys(ctx context.Context, input *iam.ListAccessKeysInput, options ...func(*iam.Options)) (*iam.ListAccessKeysOutput, error) {
|
||||
return &iam.ListAccessKeysOutput{
|
||||
AccessKeyMetadata: []iamTypes.AccessKeyMetadata{
|
||||
{
|
||||
AccessKeyId: aws.String("accesskey1"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
Status: iamTypes.StatusTypeActive,
|
||||
UserName: aws.String("user1"),
|
||||
},
|
||||
{
|
||||
AccessKeyId: aws.String("accesskey2"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
Status: iamTypes.StatusTypeActive,
|
||||
UserName: aws.String("user2"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) ListGroups(ctx context.Context, input *iam.ListGroupsInput, options ...func(*iam.Options)) (*iam.ListGroupsOutput, error) {
|
||||
return &iam.ListGroupsOutput{
|
||||
Groups: []iamTypes.Group{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:group/group1"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
GroupId: aws.String("123456789012"),
|
||||
GroupName: aws.String("group1"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:group/group2"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
GroupId: aws.String("123456789012"),
|
||||
GroupName: aws.String("group2"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) ListRoles(ctx context.Context, input *iam.ListRolesInput, options ...func(*iam.Options)) (*iam.ListRolesOutput, error) {
|
||||
return &iam.ListRolesOutput{
|
||||
Roles: []iamTypes.Role{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role1"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
RoleName: aws.String("role1"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role2"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
RoleName: aws.String("role2"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role3"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::123456789012:root\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
RoleName: aws.String("role3"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) GetAccountAuthorizationDetails(ctx context.Context, params *iam.GetAccountAuthorizationDetailsInput, optFns ...func(*iam.Options)) (*iam.GetAccountAuthorizationDetailsOutput, error) {
|
||||
return &iam.GetAccountAuthorizationDetailsOutput{
|
||||
GroupDetailList: []iamTypes.GroupDetail{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:group/group1"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
GroupId: aws.String("123456789012"),
|
||||
GroupName: aws.String("group1"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:group/group2"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
GroupId: aws.String("123456789012"),
|
||||
GroupName: aws.String("group2"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
},
|
||||
RoleDetailList: []iamTypes.RoleDetail{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role1"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
RoleName: aws.String("role1"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role2"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
RoleName: aws.String("role2"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role3"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"AWS\":\"arn:aws:iam::123456789012:root\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
|
||||
RoleName: aws.String("role3"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
},
|
||||
UserDetailList: []iamTypes.UserDetail{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/user1"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
|
||||
UserId: aws.String("123456789012"),
|
||||
UserName: aws.String("user1"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:user/user2"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
|
||||
UserId: aws.String("123456789012"),
|
||||
UserName: aws.String("user2"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) SimulatePrincipalPolicy(ctx context.Context, params *iam.SimulatePrincipalPolicyInput, optFns ...func(*iam.Options)) (*iam.SimulatePrincipalPolicyOutput, error) {
|
||||
return &iam.SimulatePrincipalPolicyOutput{
|
||||
EvaluationResults: []iamTypes.EvaluationResult{
|
||||
{
|
||||
EvalActionName: aws.String("sts:AssumeRole"),
|
||||
EvalDecision: iamTypes.PolicyEvaluationDecisionTypeAllowed,
|
||||
EvalResourceName: aws.String("arn:aws:iam::123456789012:role/role1"),
|
||||
MatchedStatements: []iamTypes.Statement{
|
||||
{
|
||||
SourcePolicyId: aws.String("PolicyForRole1"),
|
||||
SourcePolicyType: iamTypes.PolicySourceTypeUser,
|
||||
StartPosition: &iamTypes.Position{
|
||||
Column: 0,
|
||||
Line: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
|
||||
func (m *MockedIAMClient) ListInstanceProfiles(ctx context.Context, params *iam.ListInstanceProfilesInput, optFns ...func(*iam.Options)) (*iam.ListInstanceProfilesOutput, error) {
|
||||
return &iam.ListInstanceProfilesOutput{
|
||||
InstanceProfiles: []iamTypes.InstanceProfile{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:instance-profile/instance-profile1"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
InstanceProfileId: aws.String("123456789012"),
|
||||
InstanceProfileName: aws.String("instance-profile1"),
|
||||
Path: aws.String("/"),
|
||||
Roles: []iamTypes.Role{
|
||||
{
|
||||
Arn: aws.String("arn:aws:iam::123456789012:role/role1"),
|
||||
AssumeRolePolicyDocument: aws.String("{\"Version\":\"2012-10-17\",\"Statement\":[{\"Effect\":\"Allow\",\"Principal\":{\"Service\":\"ec2.amazonaws.com\"},\"Action\":\"sts:AssumeRole\"}]}"),
|
||||
CreateDate: aws.Time(time.Now()),
|
||||
RoleId: aws.String("123456789012"),
|
||||
RoleName: aws.String("role1"),
|
||||
Path: aws.String("/"),
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}, nil
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package sdk
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
|
||||
"github.com/BishopFox/cloudfox/internal"
|
||||
"github.com/aws/aws-sdk-go-v2/service/kinesis"
|
||||
"github.com/patrickmn/go-cache"
|
||||
)
|
||||
|
||||
type AWSKinesisClientInterface interface {
|
||||
ListStreams(context.Context, *kinesis.ListStreamsInput, ...func(*kinesis.Options)) (*kinesis.ListStreamsOutput, error)
|
||||
}
|
||||
|
||||
func init() {
|
||||
gob.Register([]string{})
|
||||
}
|
||||
|
||||
func CachedKinesisListStreams(client AWSKinesisClientInterface, accountID string, region string) ([]string, error) {
|
||||
var PaginationControl *string
|
||||
var streams []string
|
||||
cacheKey := fmt.Sprintf("%s-kinesis-ListStreams-%s", accountID, region)
|
||||
cached, found := internal.Cache.Get(cacheKey)
|
||||
if found {
|
||||
return cached.([]string), nil
|
||||
}
|
||||
for {
|
||||
ListStreams, err := client.ListStreams(
|
||||
context.TODO(),
|
||||
&kinesis.ListStreamsInput{
|
||||
NextToken: PaginationControl,
|
||||
},
|
||||
func(o *kinesis.Options) {
|
||||
o.Region = region
|
||||
},
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return streams, err
|
||||
}
|
||||
|
||||
streams = append(streams, ListStreams.StreamNames...)
|
||||
|
||||
//pagination
|
||||
if ListStreams.NextToken == nil {
|
||||
break
|
||||
}
|
||||
PaginationControl = ListStreams.NextToken
|
||||
}
|
||||
|
||||
internal.Cache.Set(cacheKey, streams, cache.DefaultExpiration)
|
||||
return streams, nil
|
||||
}
|
||||
Loaded 100 of 234 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user