mirror of
https://github.com/prowler-cloud/prowler.git
synced 2026-03-26 13:59:55 +00:00
Compare commits
3352 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5d41c6a0a5 | ||
|
|
29dad4e8aa | ||
|
|
a1e53ef0fc | ||
|
|
dfed6ac248 | ||
|
|
c930416260 | ||
|
|
83ffd78e63 | ||
|
|
1045ffe489 | ||
|
|
5af81b9b6d | ||
|
|
f95394bec0 | ||
|
|
0a865f8950 | ||
|
|
68d7f140ff | ||
|
|
6ed237b49c | ||
|
|
51c2158563 | ||
|
|
dbb348fb09 | ||
|
|
405dc9c507 | ||
|
|
40004ebb99 | ||
|
|
0556f30670 | ||
|
|
1723ac6a6a | ||
|
|
7b308bf5f4 | ||
|
|
d4e9940beb | ||
|
|
8558034eae | ||
|
|
a6b4c27262 | ||
|
|
159aa8b464 | ||
|
|
293c822c3d | ||
|
|
649ec19012 | ||
|
|
e04e5d3b18 | ||
|
|
52723eda6e | ||
|
|
4a4636571e | ||
|
|
32d8da2131 | ||
|
|
bb34a932ff | ||
|
|
50796bea7a | ||
|
|
d678946044 | ||
|
|
fdafb8b0d3 | ||
|
|
c8b84163c9 | ||
|
|
ab489befe6 | ||
|
|
67f3adbe4c | ||
|
|
9b018ff885 | ||
|
|
3c2b0a58a1 | ||
|
|
2a13301d35 | ||
|
|
333f74dba0 | ||
|
|
ffaa267b5e | ||
|
|
ff80a47123 | ||
|
|
17c31c64d9 | ||
|
|
add2134274 | ||
|
|
3547153c0a | ||
|
|
76b8ac157d | ||
|
|
e09a04d593 | ||
|
|
f6187ee9ca | ||
|
|
1fbf72cb6b | ||
|
|
bcb2987f60 | ||
|
|
75b6d376c4 | ||
|
|
9794b5cf27 | ||
|
|
89a7128236 | ||
|
|
c1d6021a3a | ||
|
|
d5bb5e9287 | ||
|
|
466ec0e66c | ||
|
|
f0ebfcdd69 | ||
|
|
fb15329aee | ||
|
|
c35dc7ea4a | ||
|
|
6dea923866 | ||
|
|
bcf1ef1d31 | ||
|
|
9bf3171cfa | ||
|
|
70e327a3c1 | ||
|
|
af815287ed | ||
|
|
d5187b3099 | ||
|
|
fd8d34e8bc | ||
|
|
4ba1c0259f | ||
|
|
17a39f3305 | ||
|
|
b69a0d5137 | ||
|
|
f576b24fc8 | ||
|
|
f9864eeda0 | ||
|
|
03db9d3f74 | ||
|
|
677e20a1a4 | ||
|
|
4a8150d613 | ||
|
|
afd152c073 | ||
|
|
d57db6c39e | ||
|
|
0b2e1f1917 | ||
|
|
9a666891fd | ||
|
|
9c383baff3 | ||
|
|
3e9b4d34bd | ||
|
|
122ddd3e72 | ||
|
|
f61d800147 | ||
|
|
901806e98b | ||
|
|
920d6a8692 | ||
|
|
8eb2fbeb18 | ||
|
|
96e91c4d70 | ||
|
|
94c6253c70 | ||
|
|
04d99f1928 | ||
|
|
94a174c405 | ||
|
|
2e26750006 | ||
|
|
e7e80944e9 | ||
|
|
ff6c1e4127 | ||
|
|
a67e3f4c58 | ||
|
|
a4c92ea0ea | ||
|
|
f4ffb42c91 | ||
|
|
0ec9f37d2f | ||
|
|
e4ecc98aae | ||
|
|
15f500f91a | ||
|
|
5120c1d869 | ||
|
|
725fcf80aa | ||
|
|
6fe8c81312 | ||
|
|
befcdd3dfa | ||
|
|
766fcf75cd | ||
|
|
d2a1433ff8 | ||
|
|
cfd4339c41 | ||
|
|
365c3fe3ad | ||
|
|
f8af960909 | ||
|
|
121b24b7d1 | ||
|
|
c7b463d61e | ||
|
|
520a5fc756 | ||
|
|
f45edc18a9 | ||
|
|
53a4befb01 | ||
|
|
fee0bf3ea1 | ||
|
|
6811a22651 | ||
|
|
fe2dd69b08 | ||
|
|
26a9748700 | ||
|
|
cef0a54bc3 | ||
|
|
553a51ddc2 | ||
|
|
61dc09d15d | ||
|
|
38f0f9a84d | ||
|
|
9b91ba2b91 | ||
|
|
1c6d42e60d | ||
|
|
18d60c98d7 | ||
|
|
00054b5cd9 | ||
|
|
24fc86cbb3 | ||
|
|
861fb22257 | ||
|
|
7e14204be8 | ||
|
|
09ea6ba6c4 | ||
|
|
a83725fbed | ||
|
|
34210cfc06 | ||
|
|
2e20d52030 | ||
|
|
9b0b61ef02 | ||
|
|
0203aec9e0 | ||
|
|
6cdfddd2ff | ||
|
|
a1074f1a81 | ||
|
|
a90a3f12e7 | ||
|
|
47d74a7742 | ||
|
|
862a4ad76c | ||
|
|
4b7883c464 | ||
|
|
2bf835d3d2 | ||
|
|
09733eb298 | ||
|
|
7fd53c1bc3 | ||
|
|
ad949632b4 | ||
|
|
096749a455 | ||
|
|
ebc96bed06 | ||
|
|
c4a3a1e0b5 | ||
|
|
07beb094fb | ||
|
|
280a4df4f2 | ||
|
|
ccc2aecbd4 | ||
|
|
a2cc3e913d | ||
|
|
b06e549d81 | ||
|
|
b28cfede8c | ||
|
|
a5f5967bb2 | ||
|
|
7a4f5f34f7 | ||
|
|
e33b081dc6 | ||
|
|
c8fdaa3923 | ||
|
|
8a491bcf7d | ||
|
|
f5e71db5e0 | ||
|
|
73c5764495 | ||
|
|
e84fd1fd65 | ||
|
|
456f79d80c | ||
|
|
9f728833a7 | ||
|
|
f01ce849dc | ||
|
|
572d5a1f2e | ||
|
|
c69571abcd | ||
|
|
8ddb9fbb84 | ||
|
|
193b79c221 | ||
|
|
a25a6148f2 | ||
|
|
0a63e707c2 | ||
|
|
f53a887291 | ||
|
|
ca35510d74 | ||
|
|
776b41e866 | ||
|
|
985efc67cc | ||
|
|
5d7c8d9cd2 | ||
|
|
0d01790b22 | ||
|
|
223073e3df | ||
|
|
783db5c3dc | ||
|
|
eb40369c30 | ||
|
|
e92bbffc53 | ||
|
|
d1424b3c9c | ||
|
|
1d0cc950a1 | ||
|
|
01bc745478 | ||
|
|
aedc8de964 | ||
|
|
3f5f50fe38 | ||
|
|
4fd5d868c6 | ||
|
|
e21386c1d5 | ||
|
|
78b518e22b | ||
|
|
17af724995 | ||
|
|
aa8c46d232 | ||
|
|
c9898d6d01 | ||
|
|
259538d5e4 | ||
|
|
4785feae0e | ||
|
|
8be83fc632 | ||
|
|
005d251106 | ||
|
|
b6c8adfc64 | ||
|
|
7a711095cd | ||
|
|
b0bb348480 | ||
|
|
c1b050b8b9 | ||
|
|
28c7e803ac | ||
|
|
7a57922891 | ||
|
|
919acfd548 | ||
|
|
1586cdae5e | ||
|
|
cb74dae296 | ||
|
|
58068b34bf | ||
|
|
3608aa3536 | ||
|
|
1dc4bd313a | ||
|
|
c59b08c40b | ||
|
|
73361a1cea | ||
|
|
794268cec5 | ||
|
|
06b41cf8e6 | ||
|
|
a419b4b898 | ||
|
|
890bd12e99 | ||
|
|
bf04261af6 | ||
|
|
f3dce4f7a7 | ||
|
|
29dfd303db | ||
|
|
521b3ded9c | ||
|
|
622bc48688 | ||
|
|
c0659f712a | ||
|
|
796983a530 | ||
|
|
e4395ddd55 | ||
|
|
6d05ad9815 | ||
|
|
0290b837f2 | ||
|
|
833bf0520c | ||
|
|
239826ce1f | ||
|
|
8dc042e594 | ||
|
|
e881a0f274 | ||
|
|
b1547a6d28 | ||
|
|
4603e6b46d | ||
|
|
26050bad5b | ||
|
|
810cc6c2f8 | ||
|
|
8fb6f5b11d | ||
|
|
db36cdf379 | ||
|
|
5641160177 | ||
|
|
dca49b1972 | ||
|
|
b8b60e6bc5 | ||
|
|
9d65b49cb4 | ||
|
|
f1334190d8 | ||
|
|
c434181dfd | ||
|
|
f3cfacae9a | ||
|
|
3efdfad37d | ||
|
|
77c7986797 | ||
|
|
2ac716d6db | ||
|
|
daee5fb4d2 | ||
|
|
7fc06a2740 | ||
|
|
d587d40451 | ||
|
|
f0cd88bd0e | ||
|
|
65c197d9ae | ||
|
|
a3060ed295 | ||
|
|
aca17904fa | ||
|
|
0157802ac1 | ||
|
|
10766d708d | ||
|
|
f231d8b080 | ||
|
|
590a7b2697 | ||
|
|
3c3421644f | ||
|
|
f1f68da25d | ||
|
|
48df7fdebf | ||
|
|
f2e8691bf4 | ||
|
|
2378b01ea9 | ||
|
|
60c2c409b0 | ||
|
|
344d54155a | ||
|
|
1c84ceda2e | ||
|
|
1a6f8fc504 | ||
|
|
8ecffa3039 | ||
|
|
39fbdab93c | ||
|
|
efbbfc1c68 | ||
|
|
dc68c1b955 | ||
|
|
5de13bdd8a | ||
|
|
5d0f498425 | ||
|
|
716558ffcb | ||
|
|
23929b3e68 | ||
|
|
a5612abc8c | ||
|
|
78dddc1e03 | ||
|
|
76020d4d47 | ||
|
|
1802caf25f | ||
|
|
7c2cd453eb | ||
|
|
a07a0b05bc | ||
|
|
b0af1390b5 | ||
|
|
d0d8de9028 | ||
|
|
30ed31cebe | ||
|
|
bc3cd43126 | ||
|
|
bec7644798 | ||
|
|
327b4f4bba | ||
|
|
39f1796da6 | ||
|
|
fdb644fc6d | ||
|
|
df73234234 | ||
|
|
95dc87a91b | ||
|
|
087dae07d8 | ||
|
|
5801857883 | ||
|
|
0baf4fb224 | ||
|
|
1c37b58177 | ||
|
|
0f8ea48f2f | ||
|
|
ec207c50ce | ||
|
|
b59b40b822 | ||
|
|
aa51045329 | ||
|
|
d8d831c2a0 | ||
|
|
1a9f854063 | ||
|
|
6bdcb509e1 | ||
|
|
260f007e5b | ||
|
|
ce1e9de104 | ||
|
|
2471bc569a | ||
|
|
d0ef75d8d9 | ||
|
|
aa79a289ce | ||
|
|
0340ab9570 | ||
|
|
a2929f2efb | ||
|
|
bf4db86dec | ||
|
|
a339dafcc6 | ||
|
|
f376516aad | ||
|
|
816b49fac5 | ||
|
|
6851350093 | ||
|
|
69528cbe66 | ||
|
|
c268e0613c | ||
|
|
714e96cc6e | ||
|
|
89dd56a0ff | ||
|
|
0271fe5ca0 | ||
|
|
89d7189a0f | ||
|
|
fca3d138c5 | ||
|
|
354bd90cfa | ||
|
|
c1f86cb502 | ||
|
|
fd2fdbe2f9 | ||
|
|
d5873c0437 | ||
|
|
a2dba30869 | ||
|
|
0662dff13f | ||
|
|
0ae26bddfc | ||
|
|
43efabef6c | ||
|
|
58b5d3cf83 | ||
|
|
87fb26d271 | ||
|
|
05271bc110 | ||
|
|
6f1aa6a1b1 | ||
|
|
c7a8a62cf2 | ||
|
|
2448f9b029 | ||
|
|
e90e10587b | ||
|
|
b11a33d3da | ||
|
|
73f7167b63 | ||
|
|
05e3be418d | ||
|
|
b09fd48d61 | ||
|
|
e73fc14f62 | ||
|
|
c62ab62bf9 | ||
|
|
44b0208846 | ||
|
|
e444e39fd0 | ||
|
|
89fe8fa8e2 | ||
|
|
76c6065a80 | ||
|
|
634ef2e599 | ||
|
|
4efb70a508 | ||
|
|
c3ae0aa873 | ||
|
|
a109cd2816 | ||
|
|
78fb540bbb | ||
|
|
5b543bf058 | ||
|
|
f96777bcf9 | ||
|
|
4a3ff78636 | ||
|
|
9802fc141a | ||
|
|
4ab119d6c9 | ||
|
|
ea038085ba | ||
|
|
f2d207d1d4 | ||
|
|
6ff1c436a0 | ||
|
|
4bab3e262c | ||
|
|
e0c2720d31 | ||
|
|
1b50fdba28 | ||
|
|
230d2571f9 | ||
|
|
6c818cbcc3 | ||
|
|
694cee1afb | ||
|
|
bc89f4383e | ||
|
|
84d4e4a604 | ||
|
|
5fbf8ddfe9 | ||
|
|
e3ae44d033 | ||
|
|
ddcd06d9be | ||
|
|
5214a37d6d | ||
|
|
a1f4ae73cf | ||
|
|
d0bc37c281 | ||
|
|
85393e6f78 | ||
|
|
e3104ae5ee | ||
|
|
be523c11c8 | ||
|
|
797b627695 | ||
|
|
5ac670ed4f | ||
|
|
e04ba94ace | ||
|
|
9a9481a88e | ||
|
|
3609043e4c | ||
|
|
bf9111397b | ||
|
|
3de2c47c56 | ||
|
|
17dd9de6d8 | ||
|
|
8ca21bb92e | ||
|
|
258d18112c | ||
|
|
ff9d5442ab | ||
|
|
4a3b767002 | ||
|
|
ee2d7ca79e | ||
|
|
89c441ba58 | ||
|
|
c3c775786c | ||
|
|
33ae08be65 | ||
|
|
593bce5155 | ||
|
|
31c035eb52 | ||
|
|
e4400ecf10 | ||
|
|
fc19fbac68 | ||
|
|
c188028de5 | ||
|
|
43f9a5b1d0 | ||
|
|
c81cb04bd0 | ||
|
|
d7452238d6 | ||
|
|
fb99733a1e | ||
|
|
7c4f34bb6c | ||
|
|
cbba5acc31 | ||
|
|
9882cd53cf | ||
|
|
052b882195 | ||
|
|
3a8053c3c6 | ||
|
|
046f1b2e5f | ||
|
|
9e8f88c889 | ||
|
|
2d73b9b8f4 | ||
|
|
9a7190c9c2 | ||
|
|
a2b6bdc461 | ||
|
|
d0b5992146 | ||
|
|
37343750cd | ||
|
|
056d482023 | ||
|
|
239b248935 | ||
|
|
5bd394dffe | ||
|
|
1195b75acc | ||
|
|
fee70bc9b4 | ||
|
|
f1a951b2e4 | ||
|
|
01716d9020 | ||
|
|
b87e6d20d7 | ||
|
|
11592634f2 | ||
|
|
bc308de571 | ||
|
|
6783da028c | ||
|
|
ee7ba35068 | ||
|
|
886e3aefb0 | ||
|
|
ccc80d5ce4 | ||
|
|
e468a91468 | ||
|
|
4bee4d482a | ||
|
|
82ec3e8779 | ||
|
|
85777546e8 | ||
|
|
ec69d8073a | ||
|
|
e6053ce218 | ||
|
|
f01910e4f2 | ||
|
|
8848cadc0a | ||
|
|
2c7d71a0d9 | ||
|
|
dcd1b1121a | ||
|
|
8a6e222f7a | ||
|
|
01045c973f | ||
|
|
5a8d6087f9 | ||
|
|
a4c39c25f1 | ||
|
|
628d50cf0d | ||
|
|
f0c663aca8 | ||
|
|
0a801d29cd | ||
|
|
52526800f9 | ||
|
|
f70e3deade | ||
|
|
14f06d6497 | ||
|
|
3c6e06837c | ||
|
|
e778444d1d | ||
|
|
a4cca188ef | ||
|
|
76ee608ef8 | ||
|
|
7af5c82371 | ||
|
|
98ec0532b2 | ||
|
|
172530153c | ||
|
|
0114d0462f | ||
|
|
6502330512 | ||
|
|
9bf9ebe4fd | ||
|
|
406d5864ee | ||
|
|
674a38e80f | ||
|
|
0f9ebecbb7 | ||
|
|
753c128357 | ||
|
|
0331af02ac | ||
|
|
64fb823276 | ||
|
|
33f2c80a78 | ||
|
|
84ce7a8b52 | ||
|
|
1a6b2eaa7d | ||
|
|
df373279e9 | ||
|
|
6a09171851 | ||
|
|
93d257941b | ||
|
|
28f8915f6f | ||
|
|
fef99fd5fb | ||
|
|
1e1c7cc1ce | ||
|
|
7e7d86f14a | ||
|
|
41cdc2bcc7 | ||
|
|
c41866db38 | ||
|
|
f36d23c9a7 | ||
|
|
8ac28fbcfd | ||
|
|
7f41ae7385 | ||
|
|
4c5f3a212c | ||
|
|
a4b16dd1e9 | ||
|
|
13ff0e08bb | ||
|
|
9a9a6410e1 | ||
|
|
ffa29f2f6e | ||
|
|
af267fede4 | ||
|
|
2ef9e27ee3 | ||
|
|
d4b93d79b5 | ||
|
|
d00afbdc87 | ||
|
|
5b0868e26c | ||
|
|
415c319208 | ||
|
|
1aca7a754c | ||
|
|
147c3c455b | ||
|
|
cc0923b3c7 | ||
|
|
5f7a3d0bcf | ||
|
|
d997ebb2cc | ||
|
|
50cb79ee2f | ||
|
|
2b34fd39f6 | ||
|
|
0c82137834 | ||
|
|
413b86e7cf | ||
|
|
ed427c1352 | ||
|
|
23a20a582e | ||
|
|
8411fcb5fc | ||
|
|
41e585643b | ||
|
|
aca5824240 | ||
|
|
e65b346afd | ||
|
|
98cb954f74 | ||
|
|
778edd5fec | ||
|
|
06deda7e5f | ||
|
|
a8825c385b | ||
|
|
26a00a14df | ||
|
|
12abea371d | ||
|
|
6d69a192f3 | ||
|
|
a17cf1bbb6 | ||
|
|
5d51942768 | ||
|
|
3122d727a5 | ||
|
|
e5f89d5bc7 | ||
|
|
efc60d2bf4 | ||
|
|
f7fd355dc1 | ||
|
|
7bd402bf4e | ||
|
|
b69962efb6 | ||
|
|
2b8b223403 | ||
|
|
a024ab31a0 | ||
|
|
9969e271ed | ||
|
|
f1449b66d6 | ||
|
|
3c0f360244 | ||
|
|
6e3c008a89 | ||
|
|
a694b422cf | ||
|
|
9d97b1a7ee | ||
|
|
d07f1e982a | ||
|
|
402e0e3107 | ||
|
|
c5716bf9b6 | ||
|
|
60c75b4814 | ||
|
|
bfdff563e6 | ||
|
|
4be83f240a | ||
|
|
efd2805602 | ||
|
|
b3c905c95a | ||
|
|
868615fa89 | ||
|
|
08937a9a66 | ||
|
|
ce205dc95d | ||
|
|
45c32abcdf | ||
|
|
c0ac4c7c30 | ||
|
|
c90cb3712b | ||
|
|
23c3884ab7 | ||
|
|
a491e39a18 | ||
|
|
78d2fb9fd5 | ||
|
|
aac6038565 | ||
|
|
0449d6372c | ||
|
|
bc1e6c0626 | ||
|
|
c1d061ef70 | ||
|
|
9788fe4236 | ||
|
|
7fd0798b7c | ||
|
|
82ab439e9a | ||
|
|
54280ee2dc | ||
|
|
434460b978 | ||
|
|
808fa96407 | ||
|
|
2c0c1f7d09 | ||
|
|
53b04879a0 | ||
|
|
91e7906a0b | ||
|
|
7f73e26016 | ||
|
|
d0b54d1950 | ||
|
|
da9429351f | ||
|
|
037e40f8e4 | ||
|
|
e0ed891fc4 | ||
|
|
dfc8e3e38f | ||
|
|
aef4a68c46 | ||
|
|
c0a9bd14aa | ||
|
|
0585428029 | ||
|
|
bfb591977e | ||
|
|
3c929bd68f | ||
|
|
444d820f98 | ||
|
|
304bb27502 | ||
|
|
a6db526eec | ||
|
|
3ace44979a | ||
|
|
1fff7ef1d3 | ||
|
|
351132fb5b | ||
|
|
f29e87f45b | ||
|
|
493d6a9210 | ||
|
|
3762d70ba3 | ||
|
|
03a26ec507 | ||
|
|
69a1468c18 | ||
|
|
c3e3381c63 | ||
|
|
f8a8266c9d | ||
|
|
d9c2933dc5 | ||
|
|
cad99c5e0f | ||
|
|
9f2de7d2f9 | ||
|
|
0a8c352194 | ||
|
|
ab29373537 | ||
|
|
b304f11b18 | ||
|
|
4cf7a3244f | ||
|
|
bd46196fd0 | ||
|
|
d79e1d6c94 | ||
|
|
5b51653d78 | ||
|
|
5246d84599 | ||
|
|
9409ea75e5 | ||
|
|
970cb97f73 | ||
|
|
4181ca56be | ||
|
|
d45750b042 | ||
|
|
16191a7b15 | ||
|
|
0c149461b3 | ||
|
|
a1585142b7 | ||
|
|
3ee39cff2a | ||
|
|
41ba118cc4 | ||
|
|
ba106ac8f3 | ||
|
|
558d83c957 | ||
|
|
e0587fe0cf | ||
|
|
7b38950f3c | ||
|
|
67333c00b9 | ||
|
|
7a6ab5b7c7 | ||
|
|
a149458593 | ||
|
|
fe27a32dcb | ||
|
|
a6095f7aa1 | ||
|
|
50481665ce | ||
|
|
a49c744e08 | ||
|
|
aa32634105 | ||
|
|
b27898de1d | ||
|
|
b703357027 | ||
|
|
8791b7e3f1 | ||
|
|
27cd9b22df | ||
|
|
5bf85366e0 | ||
|
|
e843ef6ffc | ||
|
|
b3c2f3a3fc | ||
|
|
3d533b56ef | ||
|
|
b43832fa8f | ||
|
|
30bc971f4b | ||
|
|
a5332b31f1 | ||
|
|
fa604af6ea | ||
|
|
dbb0d506af | ||
|
|
785bdb5bb3 | ||
|
|
343754061a | ||
|
|
7572136cc8 | ||
|
|
3950d7eba8 | ||
|
|
2f8a3d2ef8 | ||
|
|
6b7fe81cf8 | ||
|
|
3b64bbd3a8 | ||
|
|
09d099891a | ||
|
|
f5e53e814b | ||
|
|
b8b05b923f | ||
|
|
22bacfdcb3 | ||
|
|
d138c4eeb8 | ||
|
|
f0f4e85f06 | ||
|
|
e2261af59f | ||
|
|
ff74edcc04 | ||
|
|
735f830251 | ||
|
|
abcf37ea92 | ||
|
|
a6b10a8611 | ||
|
|
c239ede3f9 | ||
|
|
66f2754017 | ||
|
|
9138ecdce9 | ||
|
|
2b66368cf2 | ||
|
|
aa3425a7de | ||
|
|
8da95c7102 | ||
|
|
a31b15c26c | ||
|
|
f2301d5ed6 | ||
|
|
df10253056 | ||
|
|
d5acdc766a | ||
|
|
72d875aa4f | ||
|
|
8130880f2d | ||
|
|
d98b716dfc | ||
|
|
6bd8a17a5f | ||
|
|
e389e0136f | ||
|
|
8bb3bd0dcb | ||
|
|
4d4bf3fa11 | ||
|
|
ded28baa2f | ||
|
|
5c0ee0cfb3 | ||
|
|
c7d6484eb8 | ||
|
|
e99c58405c | ||
|
|
42ebf91a67 | ||
|
|
d8c9720723 | ||
|
|
2177704b4b | ||
|
|
2ffe7f3ef7 | ||
|
|
158263a8bf | ||
|
|
469986dd28 | ||
|
|
ff101087bf | ||
|
|
b2151e2e9c | ||
|
|
8e7dfcaa76 | ||
|
|
2c4244b1fb | ||
|
|
260cdf575a | ||
|
|
ab4190c215 | ||
|
|
7f97b0a57f | ||
|
|
2c2dd82d0c | ||
|
|
a72b33597d | ||
|
|
2511df1732 | ||
|
|
f955dd76d9 | ||
|
|
a08cc769c8 | ||
|
|
6e37d8d850 | ||
|
|
ce51108f7f | ||
|
|
9e56a4a10d | ||
|
|
77ac5e3b91 | ||
|
|
2da8f2b1eb | ||
|
|
76b1c83add | ||
|
|
650b95c4f1 | ||
|
|
ceebfc9aca | ||
|
|
2e443db362 | ||
|
|
e15690781f | ||
|
|
35f7c90c19 | ||
|
|
717f9765e1 | ||
|
|
607cd5d1e0 | ||
|
|
4e5bb81906 | ||
|
|
24163b2644 | ||
|
|
38e024216c | ||
|
|
8e4847ec89 | ||
|
|
c6d34e8089 | ||
|
|
880523076d | ||
|
|
3d2f1a3aa7 | ||
|
|
c9ff96144d | ||
|
|
234f8c2958 | ||
|
|
54bb034cac | ||
|
|
7c2f7d7eeb | ||
|
|
fcd1aa5d76 | ||
|
|
1f5ee1ee3f | ||
|
|
bbbcc4a185 | ||
|
|
da87c0d81e | ||
|
|
7732ec7d34 | ||
|
|
f8c5f4f1cc | ||
|
|
78f8badddd | ||
|
|
5223cf3763 | ||
|
|
39b7fca11f | ||
|
|
904a4a61e9 | ||
|
|
f146946319 | ||
|
|
a1b9b2171f | ||
|
|
30e3fd9e46 | ||
|
|
3db541a42a | ||
|
|
d5abe16180 | ||
|
|
564b18c388 | ||
|
|
db9faa2f4b | ||
|
|
d9ec74b149 | ||
|
|
ba1f8c9a3a | ||
|
|
f496896884 | ||
|
|
13e40eb03e | ||
|
|
b402ced402 | ||
|
|
6bbb9d04a6 | ||
|
|
6616657c91 | ||
|
|
853b833cfb | ||
|
|
1ea8addb04 | ||
|
|
c047b29140 | ||
|
|
f7df63e2af | ||
|
|
c4a39662ae | ||
|
|
2807fc2b8e | ||
|
|
fbb5ede272 | ||
|
|
8e1c8304d8 | ||
|
|
dbfc11e822 | ||
|
|
0235f37faa | ||
|
|
ef7272cf80 | ||
|
|
66e804f212 | ||
|
|
840df1dab6 | ||
|
|
9d4fa55c13 | ||
|
|
ff05ce4da1 | ||
|
|
0474c7995c | ||
|
|
1a679f371f | ||
|
|
05f7170add | ||
|
|
19acb873af | ||
|
|
0b566f9666 | ||
|
|
67bf89537a | ||
|
|
d0681a9e20 | ||
|
|
31bff99b3d | ||
|
|
48c7e65a39 | ||
|
|
1f75d70d4e | ||
|
|
ede597d02d | ||
|
|
8db20eb2ba | ||
|
|
a70fcf488d | ||
|
|
c544a069a2 | ||
|
|
4b74a8a008 | ||
|
|
1b407639f0 | ||
|
|
4d7d5718d5 | ||
|
|
7955048e79 | ||
|
|
8e0b715f12 | ||
|
|
1d81261d97 | ||
|
|
114a3088a4 | ||
|
|
bc8f3eba4d | ||
|
|
8e087196c9 | ||
|
|
744e7ff5ac | ||
|
|
90b84b57d3 | ||
|
|
0a2b7cf152 | ||
|
|
ebbccd04f1 | ||
|
|
4c83351b26 | ||
|
|
2b431fc79f | ||
|
|
fe7c3e7548 | ||
|
|
0e5f929044 | ||
|
|
47a6e28d71 | ||
|
|
de5742433b | ||
|
|
3fcccd0bcd | ||
|
|
00938cadb1 | ||
|
|
e67f4e5f29 | ||
|
|
9fb26643ba | ||
|
|
e4890f9d9d | ||
|
|
980b9b4770 | ||
|
|
348cea67c0 | ||
|
|
f4d89066d9 | ||
|
|
b26dc899be | ||
|
|
25327d618d | ||
|
|
3951295c0c | ||
|
|
ff9c3b52d6 | ||
|
|
af8c18eb4e | ||
|
|
087ffcbb95 | ||
|
|
6fbfcc7f5f | ||
|
|
b860e35408 | ||
|
|
7c7132f9c4 | ||
|
|
62e30f929c | ||
|
|
ddaafd5876 | ||
|
|
94eba806e3 | ||
|
|
fa77455c3e | ||
|
|
1f43e6eff9 | ||
|
|
aa118c05c5 | ||
|
|
cca17b9378 | ||
|
|
4a881fd2fd | ||
|
|
14ed19e3a8 | ||
|
|
8caf8f794c | ||
|
|
cba9ad61e4 | ||
|
|
e64a0eff0f | ||
|
|
4f7d6a8402 | ||
|
|
4ca95b08e2 | ||
|
|
23c65b8fde | ||
|
|
a7c93f3237 | ||
|
|
7b9402f3d0 | ||
|
|
0d5be65879 | ||
|
|
3b96d14f84 | ||
|
|
1dfde958bf | ||
|
|
cb20f595ac | ||
|
|
720256968e | ||
|
|
4badcca4f8 | ||
|
|
c6daa60f26 | ||
|
|
7fb6250029 | ||
|
|
f9aa2bb8be | ||
|
|
66ac395705 | ||
|
|
16a251254e | ||
|
|
751958907c | ||
|
|
60012ab19d | ||
|
|
65d7ba020b | ||
|
|
9456c6198a | ||
|
|
45ce1a0650 | ||
|
|
4c5db5295c | ||
|
|
a2ad0cdf30 | ||
|
|
0c70a64e84 | ||
|
|
73c96f8346 | ||
|
|
0974c5f333 | ||
|
|
7db0746416 | ||
|
|
8f0bf5e896 | ||
|
|
57abe1c839 | ||
|
|
3cac32ac78 | ||
|
|
a5fb1205af | ||
|
|
41e7dce861 | ||
|
|
10f68a4630 | ||
|
|
94090f6997 | ||
|
|
43183962ad | ||
|
|
87948b458e | ||
|
|
ab5c3eb4f8 | ||
|
|
320a2a2c77 | ||
|
|
dbc8e140e3 | ||
|
|
f50f1680df | ||
|
|
fd1832243e | ||
|
|
52e8ba702d | ||
|
|
ed9bbd30a3 | ||
|
|
035d06bbfe | ||
|
|
39c6fa9e55 | ||
|
|
21ac395d4c | ||
|
|
8a8c2b5097 | ||
|
|
3bea772c6b | ||
|
|
34679c98d6 | ||
|
|
2b41445d57 | ||
|
|
796c87bc93 | ||
|
|
a83e08aa9e | ||
|
|
489ac20141 | ||
|
|
ae794c7c32 | ||
|
|
edc78bfd6b | ||
|
|
9263adeb78 | ||
|
|
bfdc87723b | ||
|
|
8d23e81b1c | ||
|
|
f0cd924016 | ||
|
|
9ffde34198 | ||
|
|
0100b805ee | ||
|
|
c425e8249b | ||
|
|
1ece8bbcd6 | ||
|
|
5fb2d7c3ce | ||
|
|
64aebe84fe | ||
|
|
de831b0abe | ||
|
|
68af4f6c73 | ||
|
|
52981b54b9 | ||
|
|
a366594714 | ||
|
|
1fb36f316b | ||
|
|
30ffa8f00b | ||
|
|
5855918ade | ||
|
|
f9005c875f | ||
|
|
91bf99ca45 | ||
|
|
8176063fef | ||
|
|
3373822240 | ||
|
|
7e16702b2f | ||
|
|
f54b64f1f8 | ||
|
|
2c337ab3f6 | ||
|
|
5279d937d7 | ||
|
|
48c31a1616 | ||
|
|
917a2ad0fe | ||
|
|
8cfc4c56cf | ||
|
|
99e9e42a17 | ||
|
|
6a341b88f0 | ||
|
|
13c95ba131 | ||
|
|
600a8c7804 | ||
|
|
64fb52fc5e | ||
|
|
92b6e7230d | ||
|
|
cc8bc781c1 | ||
|
|
edbe463d73 | ||
|
|
8ace8c01cf | ||
|
|
8f37252676 | ||
|
|
1cef6f0db7 | ||
|
|
c0c59968bf | ||
|
|
9f5a909be3 | ||
|
|
90975bdadc | ||
|
|
7d1fad9eb7 | ||
|
|
983c79ad3b | ||
|
|
96e73fcb63 | ||
|
|
70a3736073 | ||
|
|
f7645e8f25 | ||
|
|
1e8e8ba65c | ||
|
|
0adb8c142b | ||
|
|
359a1f2c8e | ||
|
|
e7eb57375e | ||
|
|
2e4f8cbfc7 | ||
|
|
482aee0d9d | ||
|
|
0ae3374e81 | ||
|
|
ddc088859e | ||
|
|
d264a16065 | ||
|
|
67f572285b | ||
|
|
5e3da2d687 | ||
|
|
1af7f658a8 | ||
|
|
d5d76e248f | ||
|
|
67fcc8ac67 | ||
|
|
ceca5dd0c4 | ||
|
|
1298620da8 | ||
|
|
75c48cfaa3 | ||
|
|
3406a07ae5 | ||
|
|
cc9e1c5af8 | ||
|
|
0343f01cca | ||
|
|
cad7985c28 | ||
|
|
71030f6f42 | ||
|
|
6883467d2f | ||
|
|
2c6944176f | ||
|
|
2420aedde9 | ||
|
|
1ef15f0b24 | ||
|
|
f5b0583df5 | ||
|
|
db225e9d2a | ||
|
|
c9ae9df87f | ||
|
|
159a090c02 | ||
|
|
605c6770e5 | ||
|
|
5163bcb72c | ||
|
|
ae950484ed | ||
|
|
c54b815b90 | ||
|
|
457c845af8 | ||
|
|
7a937c7708 | ||
|
|
d62e74853e | ||
|
|
3a87b30140 | ||
|
|
73330ecb1a | ||
|
|
bab59bc86e | ||
|
|
b605316560 | ||
|
|
ed116b688f | ||
|
|
c3346ff605 | ||
|
|
39e8485fc1 | ||
|
|
412d25db30 | ||
|
|
1ed670cf40 | ||
|
|
b9f46cafff | ||
|
|
ec67fc12e0 | ||
|
|
09ef68e1c5 | ||
|
|
3cc9910f61 | ||
|
|
3d120b3505 | ||
|
|
f5462c9b27 | ||
|
|
48377ca865 | ||
|
|
4d902e02bb | ||
|
|
e146491d4b | ||
|
|
a30c6520d4 | ||
|
|
5326ffbcc9 | ||
|
|
ff0ba89a3f | ||
|
|
bc7c3bd74b | ||
|
|
4eed5c7a99 | ||
|
|
f169599a56 | ||
|
|
a9ff875a3a | ||
|
|
95768baa9e | ||
|
|
4e8aae4f9e | ||
|
|
1bc6ac06a4 | ||
|
|
122dddea9e | ||
|
|
97616213db | ||
|
|
d8d348f609 | ||
|
|
2e09667bab | ||
|
|
bb32af93b2 | ||
|
|
bd336250ee | ||
|
|
a975e96a45 | ||
|
|
ac93672752 | ||
|
|
3933440a08 | ||
|
|
36e7bf0912 | ||
|
|
897e25dd3c | ||
|
|
a1021fbca7 | ||
|
|
f4a8059f9b | ||
|
|
71d844c101 | ||
|
|
45f398bf30 | ||
|
|
c2b2754926 | ||
|
|
cfd4019281 | ||
|
|
81743c9c29 | ||
|
|
989fce300d | ||
|
|
d8ae2bf455 | ||
|
|
2d07186eb1 | ||
|
|
70fdc2693e | ||
|
|
9797c11152 | ||
|
|
007c1febf7 | ||
|
|
d1cd8848eb | ||
|
|
0acfb6040e | ||
|
|
24857eaa7f | ||
|
|
c281f85742 | ||
|
|
163027a49d | ||
|
|
aa44572be5 | ||
|
|
631885e364 | ||
|
|
80c4802b36 | ||
|
|
285eb45673 | ||
|
|
5c2f2ee3b3 | ||
|
|
1f83e4fe7b | ||
|
|
fed489f9d9 | ||
|
|
b29f99441a | ||
|
|
82c065bff4 | ||
|
|
8183207802 | ||
|
|
168d44d14b | ||
|
|
7c76d4efa1 | ||
|
|
910a72140b | ||
|
|
0a090b5694 | ||
|
|
c0a20b0f5d | ||
|
|
d988877173 | ||
|
|
4fd673fd7c | ||
|
|
b5a40d07cf | ||
|
|
1985b16824 | ||
|
|
1bff2451e5 | ||
|
|
0921daf18b | ||
|
|
7ff80dbb8f | ||
|
|
f487bda1fe | ||
|
|
06d05ec860 | ||
|
|
11af5e1429 | ||
|
|
440e95515a | ||
|
|
d61e999b8f | ||
|
|
bcb63d0b2d | ||
|
|
71f50422ad | ||
|
|
2b49aa8e89 | ||
|
|
921b6b1e85 | ||
|
|
fc155e8368 | ||
|
|
4cf5d9cb43 | ||
|
|
c910167ff6 | ||
|
|
79f1cf89cf | ||
|
|
496d4daf01 | ||
|
|
559c0d4e0b | ||
|
|
2fda2388bb | ||
|
|
0f79312c33 | ||
|
|
472aea6a91 | ||
|
|
0d18406f80 | ||
|
|
05da5d1796 | ||
|
|
fb449cede8 | ||
|
|
61df2ce0c2 | ||
|
|
b7e20344a8 | ||
|
|
c2552ee508 | ||
|
|
57f1fa5bfa | ||
|
|
ed0d975e43 | ||
|
|
0b238243b1 | ||
|
|
df405254c6 | ||
|
|
460acf2860 | ||
|
|
dec3e652c5 | ||
|
|
fc03188bfb | ||
|
|
ff244138d9 | ||
|
|
903f9c576f | ||
|
|
0005f86a5f | ||
|
|
a2144ad353 | ||
|
|
5f075b296d | ||
|
|
0c7b960e08 | ||
|
|
c65e91f834 | ||
|
|
5876fea163 | ||
|
|
8f2bd45872 | ||
|
|
063de00e45 | ||
|
|
a557d62d84 | ||
|
|
4b18397e69 | ||
|
|
52dd08883f | ||
|
|
f25319f3f6 | ||
|
|
8a2cfea677 | ||
|
|
4e104194bc | ||
|
|
1e02b05d2d | ||
|
|
78042063cb | ||
|
|
80d05c276f | ||
|
|
8129b174f1 | ||
|
|
f676ca9078 | ||
|
|
3f78fb4220 | ||
|
|
e11bb478d6 | ||
|
|
967158f216 | ||
|
|
3b621e73f6 | ||
|
|
357c9b0813 | ||
|
|
dec5fb6428 | ||
|
|
256ccfea79 | ||
|
|
1a8bc14587 | ||
|
|
8483486095 | ||
|
|
7aaecbabab | ||
|
|
5cc9554c23 | ||
|
|
5d42ae6e6f | ||
|
|
38b73fb0c0 | ||
|
|
84a76f4535 | ||
|
|
a126fd82b3 | ||
|
|
71a56031e2 | ||
|
|
d714213cc2 | ||
|
|
33a5556b8a | ||
|
|
a864c76955 | ||
|
|
109a477f9e | ||
|
|
c159fb1dac | ||
|
|
52e21a020e | ||
|
|
9296e0cc0d | ||
|
|
f61ed5ddf5 | ||
|
|
f236d2087a | ||
|
|
bf139138e0 | ||
|
|
441945e075 | ||
|
|
0fcf4243f5 | ||
|
|
bbb0248bc1 | ||
|
|
e6581255c2 | ||
|
|
717932ae26 | ||
|
|
3f56731e6d | ||
|
|
0f837f658e | ||
|
|
b70977163e | ||
|
|
98fc624010 | ||
|
|
ccb755340f | ||
|
|
49ff901195 | ||
|
|
e7d0d49809 | ||
|
|
47bb97961c | ||
|
|
1178317567 | ||
|
|
edd0dd1080 | ||
|
|
26ab6513a2 | ||
|
|
b0ec7a2a82 | ||
|
|
caa5e7dd96 | ||
|
|
75f4f0d43a | ||
|
|
6ea3057b23 | ||
|
|
1724e25c3b | ||
|
|
5af439d926 | ||
|
|
40991c4b7e | ||
|
|
614548f58a | ||
|
|
d7fe3595d3 | ||
|
|
088b4fa4fe | ||
|
|
ca3da473d7 | ||
|
|
ae1b114a13 | ||
|
|
3c9c28f351 | ||
|
|
11518a0806 | ||
|
|
93e6751e35 | ||
|
|
ebd3bb386c | ||
|
|
afc4189577 | ||
|
|
680781656b | ||
|
|
21382efd07 | ||
|
|
097e61ab9d | ||
|
|
4c0a14b96c | ||
|
|
8dba9a7d9e | ||
|
|
891c171247 | ||
|
|
308f52c6f9 | ||
|
|
52d83bd83b | ||
|
|
49cfe15abc | ||
|
|
0ef30c655a | ||
|
|
e2d211c188 | ||
|
|
62a1d91869 | ||
|
|
8c1347323e | ||
|
|
092ad10c56 | ||
|
|
cb807e4aed | ||
|
|
c492d25f4c | ||
|
|
bcc8d5f1fe | ||
|
|
59acd303fb | ||
|
|
0675cc8fdb | ||
|
|
ed27491118 | ||
|
|
4f99518d52 | ||
|
|
abb28af68e | ||
|
|
18885d0cd7 | ||
|
|
ca56ac4e77 | ||
|
|
8f2b39b3ce | ||
|
|
a2ab216531 | ||
|
|
7ab46d61b5 | ||
|
|
b5928be876 | ||
|
|
abc2a3fa72 | ||
|
|
10fc131e13 | ||
|
|
761eebac1e | ||
|
|
5bb3c012c9 | ||
|
|
8bdff0d681 | ||
|
|
55e0656375 | ||
|
|
b5b2e225ce | ||
|
|
bed2b1e7f7 | ||
|
|
6d48265618 | ||
|
|
43c9e70a65 | ||
|
|
e2fc83c81b | ||
|
|
e666b66ec0 | ||
|
|
cdb4f73803 | ||
|
|
b4c7345124 | ||
|
|
af8cc37eea | ||
|
|
ae8098d53e | ||
|
|
857edd9aa6 | ||
|
|
28bed98ee4 | ||
|
|
3d39eb7db6 | ||
|
|
2c5f2e9f5c | ||
|
|
5ce54e5605 | ||
|
|
6c029a9d7d | ||
|
|
96f893c3ec | ||
|
|
f0047cf5a7 | ||
|
|
1b18aef0f0 | ||
|
|
ca3d076607 | ||
|
|
80e13bffa2 | ||
|
|
384d16749c | ||
|
|
9c4ba1183b | ||
|
|
40a88e07d1 | ||
|
|
692ed760e0 | ||
|
|
35128b89b1 | ||
|
|
157c475f5c | ||
|
|
4483baae19 | ||
|
|
3511cd977a | ||
|
|
d69c35fa3c | ||
|
|
6c3e451f32 | ||
|
|
24f511b567 | ||
|
|
dee7e621de | ||
|
|
0ae248926d | ||
|
|
89c6652bd6 | ||
|
|
8aca456285 | ||
|
|
824a465667 | ||
|
|
086c203e6b | ||
|
|
4371ad1535 | ||
|
|
4137eaec6d | ||
|
|
14ff4282c0 | ||
|
|
b7d324f1b0 | ||
|
|
81bf3fc15f | ||
|
|
22ebe00cf6 | ||
|
|
3ae00cadb9 | ||
|
|
f746a9e742 | ||
|
|
90810d9098 | ||
|
|
75b3f52309 | ||
|
|
8ecb4696d4 | ||
|
|
7b22c9c97b | ||
|
|
84f0542b98 | ||
|
|
8faa40dfb6 | ||
|
|
47f7555d05 | ||
|
|
96d9cbd8af | ||
|
|
c8bc54aa48 | ||
|
|
fad0b8995a | ||
|
|
1992ef050a | ||
|
|
d4b6fa27e2 | ||
|
|
a37723fd32 | ||
|
|
e7f79589d4 | ||
|
|
fc5eefe532 | ||
|
|
ff3f90ac94 | ||
|
|
ffd9b2a2f6 | ||
|
|
112f48ac08 | ||
|
|
95ec3d91b4 | ||
|
|
b0709d08cd | ||
|
|
a0e3cb87a4 | ||
|
|
1b9cc9e3db | ||
|
|
d9fb67bc43 | ||
|
|
dff3462113 | ||
|
|
a2172d12f4 | ||
|
|
ffb91d2733 | ||
|
|
485482c868 | ||
|
|
b16a7150fa | ||
|
|
00613cdda3 | ||
|
|
32ecc5dbad | ||
|
|
e1a12bcb14 | ||
|
|
0283b34190 | ||
|
|
26cfbeb3a8 | ||
|
|
b95d48e2ad | ||
|
|
a79022dce8 | ||
|
|
0a2ce690f4 | ||
|
|
2bfa37ca2e | ||
|
|
bbc51114b0 | ||
|
|
6a7b6f3e6b | ||
|
|
a9462da78e | ||
|
|
02f2043a8c | ||
|
|
acfbdc6405 | ||
|
|
25ec271a7f | ||
|
|
a3555af684 | ||
|
|
de55eeb183 | ||
|
|
8fc9204946 | ||
|
|
32da86f393 | ||
|
|
74d02e1da6 | ||
|
|
5b31ce8484 | ||
|
|
e5a328e9ea | ||
|
|
8ec6e89e5c | ||
|
|
14a10fc6f0 | ||
|
|
17012ec1a4 | ||
|
|
941bdfb2e1 | ||
|
|
d431516270 | ||
|
|
92e88674f6 | ||
|
|
89d15c40da | ||
|
|
8461257428 | ||
|
|
26a5ffaf82 | ||
|
|
563ddb3707 | ||
|
|
2c11c3d6f9 | ||
|
|
e050f44d63 | ||
|
|
4fd3405bbf | ||
|
|
a1c2caa745 | ||
|
|
f639dc8bf4 | ||
|
|
35325d9f40 | ||
|
|
ddf9a3ef2d | ||
|
|
0a0a08b97d | ||
|
|
71503b553a | ||
|
|
d91a240ea8 | ||
|
|
3fa614341f | ||
|
|
b112202f41 | ||
|
|
b9b5f66073 | ||
|
|
9d66a7ec4a | ||
|
|
e3f66840aa | ||
|
|
0d6c529a46 | ||
|
|
5237658047 | ||
|
|
48f633889a | ||
|
|
fd9cff9392 | ||
|
|
86a4938b5f | ||
|
|
c00f61ac10 | ||
|
|
2cd840a2b5 | ||
|
|
9fd642fe0e | ||
|
|
0035c8c08e | ||
|
|
151fca146e | ||
|
|
1bea55c5e8 | ||
|
|
8ce28dd311 | ||
|
|
7e630ebe27 | ||
|
|
2f1c0facfd | ||
|
|
603bb03f35 | ||
|
|
54b3fc3ae6 | ||
|
|
b8de713497 | ||
|
|
0ee60efaa7 | ||
|
|
b7af1a06e8 | ||
|
|
02fc034b1f | ||
|
|
40522cdc62 | ||
|
|
dc11d85451 | ||
|
|
13c50086eb | ||
|
|
f7729381e0 | ||
|
|
d244475578 | ||
|
|
10dcbaea7b | ||
|
|
c91bbdcf2b | ||
|
|
c7dbcb17d6 | ||
|
|
1244cdb73e | ||
|
|
4b63fc4757 | ||
|
|
5a8a9286db | ||
|
|
2476a1275a | ||
|
|
b65159dd43 | ||
|
|
842608afa0 | ||
|
|
ac680c58cd | ||
|
|
68f0916ce4 | ||
|
|
57f5fd51e6 | ||
|
|
6a135cb47c | ||
|
|
dc896fc0af | ||
|
|
76af71d2df | ||
|
|
1ac3ab48f2 | ||
|
|
7f104bdc91 | ||
|
|
e927413e11 | ||
|
|
b4adacd9e0 | ||
|
|
04bd613fc9 | ||
|
|
dd2c92d805 | ||
|
|
96f761e4ef | ||
|
|
044c8dbb3a | ||
|
|
9e16e477e9 | ||
|
|
2038e30d3e | ||
|
|
a4dc6975b0 | ||
|
|
a4a89fa581 | ||
|
|
1c5859d93c | ||
|
|
8388aad831 | ||
|
|
de97b9f298 | ||
|
|
fc449bfd7b | ||
|
|
db30c0253d | ||
|
|
2477948ae9 | ||
|
|
ca98584ded | ||
|
|
0590c00c9b | ||
|
|
489830f01a | ||
|
|
6ab0a42f67 | ||
|
|
bd56ca2979 | ||
|
|
04483a9a4f | ||
|
|
684f63d398 | ||
|
|
b528dd44cd | ||
|
|
dfdeac0a46 | ||
|
|
b52b67fd4b | ||
|
|
5cf7d89aab | ||
|
|
7cb3a4e16e | ||
|
|
b2d3f492ec | ||
|
|
f5e6b1e438 | ||
|
|
aa44bde940 | ||
|
|
ddc927a4ad | ||
|
|
fbc99259e2 | ||
|
|
43f79663d9 | ||
|
|
e6d84cb245 | ||
|
|
28f6f0abcc | ||
|
|
0933a04239 | ||
|
|
5185f3a41e | ||
|
|
6d20b11394 | ||
|
|
a01635e9ea | ||
|
|
3bf9cd3db1 | ||
|
|
e15f0b2d0f | ||
|
|
0403c1f1b5 | ||
|
|
f2de059ca1 | ||
|
|
8c8ac95d9c | ||
|
|
89159c2111 | ||
|
|
70eb59185b | ||
|
|
f97af19860 | ||
|
|
5ccd8af2a2 | ||
|
|
b53e8abc87 | ||
|
|
91eb26dac2 | ||
|
|
db4c4fdaeb | ||
|
|
44afe2db3e | ||
|
|
204d548cd0 | ||
|
|
3faf80c0d7 | ||
|
|
5078e4a823 | ||
|
|
d1b57ebd75 | ||
|
|
93a8f91eb1 | ||
|
|
7093261f84 | ||
|
|
ec7df134b4 | ||
|
|
fdab3a737a | ||
|
|
b6f01b92dd | ||
|
|
c92537c791 | ||
|
|
3e7cc2e0a2 | ||
|
|
bfa98646c1 | ||
|
|
3bd84a0efd | ||
|
|
b8cfdb590b | ||
|
|
577afbd521 | ||
|
|
d01cc51b6d | ||
|
|
cd7d7c303a | ||
|
|
8f41b38bbf | ||
|
|
0bdfa1a3b9 | ||
|
|
ffa60b4ccd | ||
|
|
d6dd0f7244 | ||
|
|
4df0dc4904 | ||
|
|
386a1e1d1a | ||
|
|
db9d7a4439 | ||
|
|
9ae201bddf | ||
|
|
5725035e29 | ||
|
|
96a49e97d2 | ||
|
|
2a95750525 | ||
|
|
7773858340 | ||
|
|
b868d1a7fe | ||
|
|
93e44a6019 | ||
|
|
3edb2ea9f2 | ||
|
|
37ade2a722 | ||
|
|
c67032e07f | ||
|
|
0de8ef032a | ||
|
|
027aa9796d | ||
|
|
a505776227 | ||
|
|
3be9de376a | ||
|
|
725dbd2979 | ||
|
|
a61554bd04 | ||
|
|
fe0d005f97 | ||
|
|
c4074d842d | ||
|
|
e7d4143f47 | ||
|
|
08059e3a32 | ||
|
|
0bef1a157b | ||
|
|
c427878820 | ||
|
|
23cd6553a9 | ||
|
|
bd26d74b28 | ||
|
|
ca27854ff0 | ||
|
|
9df759da60 | ||
|
|
f31a92ea98 | ||
|
|
79966db251 | ||
|
|
abd18dc14d | ||
|
|
297f506fd3 | ||
|
|
443e6b6bee | ||
|
|
157a54f30c | ||
|
|
746b427943 | ||
|
|
78ca4b93a5 | ||
|
|
c80d51b585 | ||
|
|
86df1fd98e | ||
|
|
cf9b23c302 | ||
|
|
ef4b9e8d6a | ||
|
|
f0a276773e | ||
|
|
d4b21cbe6a | ||
|
|
a5a8c2a769 | ||
|
|
64b21ae2b9 | ||
|
|
3da4824a1d | ||
|
|
2247296cf9 | ||
|
|
615127f790 | ||
|
|
160990f979 | ||
|
|
42f21a52c9 | ||
|
|
e9442b2f89 | ||
|
|
6336b1c0d9 | ||
|
|
ee640da9e7 | ||
|
|
a0603b972e | ||
|
|
4d43a6bdd6 | ||
|
|
d80622ca69 | ||
|
|
4beff6e62f | ||
|
|
f319884532 | ||
|
|
6138c7da9d | ||
|
|
cf49641d5c | ||
|
|
d49139c4f4 | ||
|
|
046c82232d | ||
|
|
027aafd9ea | ||
|
|
215d5dabd7 | ||
|
|
f5e2ac7486 | ||
|
|
6fc24b5435 | ||
|
|
457801f752 | ||
|
|
f7c7b6a5ba | ||
|
|
2337b203d0 | ||
|
|
e10bb9e3f2 | ||
|
|
b63d1f1292 | ||
|
|
3d99e6ea28 | ||
|
|
b23aefadc1 | ||
|
|
a29c9bf563 | ||
|
|
f19adde4e5 | ||
|
|
721aea945a | ||
|
|
01a0d07151 | ||
|
|
15c9edd49f | ||
|
|
b585a31a14 | ||
|
|
9c817ae8a9 | ||
|
|
cd7f19c00e | ||
|
|
3a502c5b3d | ||
|
|
30775373dc | ||
|
|
7e194407f6 | ||
|
|
8caae5996e | ||
|
|
0664032ef7 | ||
|
|
67c6a12be4 | ||
|
|
d1a7d19799 | ||
|
|
d7dffbc44b | ||
|
|
6b028142ee | ||
|
|
d4eabf2d7e | ||
|
|
c7abc37671 | ||
|
|
1637325625 | ||
|
|
0402cc7e2d | ||
|
|
bf83f38c89 | ||
|
|
673619c8a1 | ||
|
|
2345a7384b | ||
|
|
e387c591c3 | ||
|
|
47a37c7d0d | ||
|
|
7b359cf1eb | ||
|
|
35d525b903 | ||
|
|
b5b193427d | ||
|
|
e6ae539323 | ||
|
|
a69a155679 | ||
|
|
541b907038 | ||
|
|
7ff6d860ce | ||
|
|
040e1eaa5e | ||
|
|
e23a674277 | ||
|
|
e73cefdf1a | ||
|
|
9ed4e89c60 | ||
|
|
da547b2bbe | ||
|
|
ca033745c9 | ||
|
|
b440be717c | ||
|
|
fb49fb83ae | ||
|
|
76e0b23365 | ||
|
|
d8752719c1 | ||
|
|
737a0ff9cb | ||
|
|
1c8e676822 | ||
|
|
7b98f0fc92 | ||
|
|
45865f2e71 | ||
|
|
eded2df687 | ||
|
|
766d3f6670 | ||
|
|
3f2d0a13af | ||
|
|
690957e1c3 | ||
|
|
3092b56fd6 | ||
|
|
82ccdc45d2 | ||
|
|
de777a6417 | ||
|
|
87d8cda745 | ||
|
|
64abd0a6d0 | ||
|
|
096d7c6304 | ||
|
|
4908e06544 | ||
|
|
d42cc66d9f | ||
|
|
7a5318b936 | ||
|
|
ffb494f9a4 | ||
|
|
f515b2b53b | ||
|
|
a3cf7665ac | ||
|
|
dbaf72958e | ||
|
|
169d1686d2 | ||
|
|
ba726b205d | ||
|
|
630d980861 | ||
|
|
7d81040eae | ||
|
|
4009d96f8a | ||
|
|
cee5064b11 | ||
|
|
e5c911abef | ||
|
|
c000aa2602 | ||
|
|
ff5c41f363 | ||
|
|
cf84875355 | ||
|
|
ccfc46d743 | ||
|
|
fc23eccc7b | ||
|
|
385eb5cc18 | ||
|
|
2ff7d81a9b | ||
|
|
644c4fd3a4 | ||
|
|
d0a931bae8 | ||
|
|
5583714c7a | ||
|
|
c5fb11e815 | ||
|
|
fdab1edd3e | ||
|
|
ea74d82c48 | ||
|
|
093738c65f | ||
|
|
bae224c891 | ||
|
|
32cded949d | ||
|
|
6463dcdde0 | ||
|
|
0b16dab2ad | ||
|
|
825c620e6f | ||
|
|
819a5597a3 | ||
|
|
4bae3d2600 | ||
|
|
131cb82751 | ||
|
|
029caf3b10 | ||
|
|
9ee23a39b5 | ||
|
|
4837df4352 | ||
|
|
d173d58a93 | ||
|
|
af29570fe9 | ||
|
|
9253cd42dd | ||
|
|
836b4ba2cc | ||
|
|
f28c0578aa | ||
|
|
536f0df9d3 | ||
|
|
465261e1df | ||
|
|
3667370604 | ||
|
|
9ca64e7bdb | ||
|
|
95a9f1c458 | ||
|
|
9fbd627f9a | ||
|
|
7203fcf4f1 | ||
|
|
f10bb343a6 | ||
|
|
9147a45e2f | ||
|
|
5353d515b6 | ||
|
|
e8a94733bf | ||
|
|
625be45742 | ||
|
|
ecb6cb897f | ||
|
|
f07bd79442 | ||
|
|
b7c1fabae1 | ||
|
|
59d3b2f33e | ||
|
|
6c098e98e3 | ||
|
|
380011fd1e | ||
|
|
e97bf32a90 | ||
|
|
ed18ea0ec4 | ||
|
|
dc897986bc | ||
|
|
e296d6e5c1 | ||
|
|
1252e6163b | ||
|
|
8ad14c7833 | ||
|
|
61b9ecc214 | ||
|
|
f8f2c19454 | ||
|
|
922438a7a0 | ||
|
|
920f98c9ef | ||
|
|
9b1ad5dd2e | ||
|
|
d7a97b6e1d | ||
|
|
07db051d14 | ||
|
|
6fec85589d | ||
|
|
f82aa1c3e1 | ||
|
|
ee9faedbbe | ||
|
|
e5dec1251d | ||
|
|
692a39b08f | ||
|
|
60b3523def | ||
|
|
e1428bc1ff | ||
|
|
0ff8b7e02a | ||
|
|
7b84008046 | ||
|
|
30a092e2aa | ||
|
|
11a7ff2977 | ||
|
|
12ba978361 | ||
|
|
42182a2b70 | ||
|
|
26eaec3101 | ||
|
|
daf6194dee | ||
|
|
e28300a1db | ||
|
|
1a225c334f | ||
|
|
1d64ca4372 | ||
|
|
2a139e3dc7 | ||
|
|
89d1712ff1 | ||
|
|
45ea9e1e79 | ||
|
|
4b46fe9788 | ||
|
|
28b9e269b7 | ||
|
|
0a41ec4746 | ||
|
|
e6472f9bfc | ||
|
|
c033af6194 | ||
|
|
4d662dc446 | ||
|
|
0de10c4742 | ||
|
|
f7b7ce3b95 | ||
|
|
7b43b3d31e | ||
|
|
84b9c442fe | ||
|
|
a890895e8b | ||
|
|
f3c6720a1c | ||
|
|
8c29bbfe4e | ||
|
|
910c969473 | ||
|
|
2795673ebc | ||
|
|
dc510e0683 | ||
|
|
070edc1693 | ||
|
|
8645ee20c3 | ||
|
|
8d4abd7638 | ||
|
|
f4106f4b72 | ||
|
|
4087aaf6cf | ||
|
|
c3ef0d4ca8 | ||
|
|
a1aed37482 | ||
|
|
d05a15ef5a | ||
|
|
ef9d3b902e | ||
|
|
366bb91a1e | ||
|
|
0c01cf28c4 | ||
|
|
f895e4df6a | ||
|
|
2affed81ad | ||
|
|
b33b529e74 | ||
|
|
0bbb762c74 | ||
|
|
ec5fb035b1 | ||
|
|
e45a189422 | ||
|
|
b2b66bd080 | ||
|
|
b905d73b82 | ||
|
|
6ed3167e17 | ||
|
|
3a2fea7136 | ||
|
|
212ff2439e | ||
|
|
7b2a7faf6b | ||
|
|
2725d476a4 | ||
|
|
dfa940440c | ||
|
|
862bc8cae8 | ||
|
|
a51bdef083 | ||
|
|
52955f9c6e | ||
|
|
581cfcc917 | ||
|
|
4ee29225bc | ||
|
|
095b6bc463 | ||
|
|
bd1fcdd68a | ||
|
|
98f6003069 | ||
|
|
583c3c6ca7 | ||
|
|
a5378b58f7 | ||
|
|
98b7df643a | ||
|
|
533f7cbd5a | ||
|
|
f4a1130c03 | ||
|
|
38c9187a5e | ||
|
|
c7827cdc80 | ||
|
|
33246a4dab | ||
|
|
7bc09fb1c8 | ||
|
|
950adb109f | ||
|
|
a98d095be0 | ||
|
|
a029296811 | ||
|
|
3e6c682fa1 | ||
|
|
ab06627ee8 | ||
|
|
5fe85aa2a5 | ||
|
|
ceac9eee60 | ||
|
|
24d8c05ae0 | ||
|
|
e6e7303640 | ||
|
|
a6b2ec42b8 | ||
|
|
d51fd0e997 | ||
|
|
9c8280d980 | ||
|
|
b27155790e | ||
|
|
ca554ad3ff | ||
|
|
b72e4a657c | ||
|
|
7371104194 | ||
|
|
96fc4c3383 | ||
|
|
ee178c2305 | ||
|
|
4dc2070853 | ||
|
|
e9670d7291 | ||
|
|
3aa28329d2 | ||
|
|
aa425077b7 | ||
|
|
eb7f56f512 | ||
|
|
a591f07bdf | ||
|
|
90e4bf7d69 | ||
|
|
a590ef52da | ||
|
|
011c6c4571 | ||
|
|
6c54e305d9 | ||
|
|
c7550d8902 | ||
|
|
cdd10a49f6 | ||
|
|
374567a858 | ||
|
|
c118e34ada | ||
|
|
d1632d71c2 | ||
|
|
d007555a64 | ||
|
|
0e71756db3 | ||
|
|
69166a0352 | ||
|
|
9923845f20 | ||
|
|
05d4338d83 | ||
|
|
db504965a1 | ||
|
|
a8c6d29679 | ||
|
|
9e934b8e87 | ||
|
|
248c7c51d6 | ||
|
|
ea4a3b4e11 | ||
|
|
2f57f1f594 | ||
|
|
716d38814f | ||
|
|
1971d19a5d | ||
|
|
3eb95a349e | ||
|
|
921cbb14d6 | ||
|
|
a9b7fc5e48 | ||
|
|
b0d33ce20c | ||
|
|
06a338f5fb | ||
|
|
f4eaf2d909 | ||
|
|
41a4750b45 | ||
|
|
114921ef8e | ||
|
|
8570493ff7 | ||
|
|
7fc19510a4 | ||
|
|
bf1616d705 | ||
|
|
db29c758ef | ||
|
|
6c632ddcf3 | ||
|
|
12f9f8a044 | ||
|
|
73b3484ce8 | ||
|
|
0f7c301896 | ||
|
|
6f3eca7249 | ||
|
|
7da7726fe9 | ||
|
|
53cfcff68e | ||
|
|
e3015c6af4 | ||
|
|
5cf4b638d5 | ||
|
|
4aedba71fd | ||
|
|
416e406394 | ||
|
|
378e1599ed | ||
|
|
c33c3e3e21 | ||
|
|
c6786881fb | ||
|
|
32c28572a4 | ||
|
|
d77fb51795 | ||
|
|
03530d3e0d | ||
|
|
4628b823cf | ||
|
|
8423e328ce | ||
|
|
923176796a | ||
|
|
d7c4a1c789 | ||
|
|
e73a533f41 | ||
|
|
4fbddd5b42 | ||
|
|
45ccd7e793 | ||
|
|
bc80edd586 | ||
|
|
5d2af9b9f7 | ||
|
|
6601b4231d | ||
|
|
6e88b260d0 | ||
|
|
ebe3c5db54 | ||
|
|
1df93b62df | ||
|
|
225e12be91 | ||
|
|
73b7d76219 | ||
|
|
e226cb06e0 | ||
|
|
d35fd463a2 | ||
|
|
c197aa8594 | ||
|
|
6f0dc44975 | ||
|
|
d9cf113882 | ||
|
|
b776a6414d | ||
|
|
4cfd4b3e31 | ||
|
|
1b083eec67 | ||
|
|
b4c04c7cfc | ||
|
|
5d1f40e104 | ||
|
|
7f105e4d7a | ||
|
|
c183a47637 | ||
|
|
9fd29ca5e4 | ||
|
|
b5d153948d | ||
|
|
1f49d6d74c | ||
|
|
d23c2a9be5 | ||
|
|
a03a5d147b | ||
|
|
a54a0dd7c5 | ||
|
|
b60354ec4d | ||
|
|
d4a079a559 | ||
|
|
eb05d637a2 | ||
|
|
b19b80008d | ||
|
|
5c263db5d4 | ||
|
|
808d87a0dd | ||
|
|
3162f6cd92 | ||
|
|
2fbb47d839 | ||
|
|
f26f5d3c72 | ||
|
|
eb35f60d6b | ||
|
|
cd0253e477 | ||
|
|
6ceb2c1e56 | ||
|
|
c67c23dd42 | ||
|
|
8b0bae1c57 | ||
|
|
c873f95743 | ||
|
|
ddd94e6f64 | ||
|
|
722554ad3f | ||
|
|
484cf6f49d | ||
|
|
e4154ed4a2 | ||
|
|
86cb9f5838 | ||
|
|
1622d0aa35 | ||
|
|
b54ecb50bf | ||
|
|
f16857fdf1 | ||
|
|
ab109c935c | ||
|
|
8e7e456431 | ||
|
|
46114cd5f4 | ||
|
|
275e509c8d | ||
|
|
12f135669f | ||
|
|
f004df673d | ||
|
|
3ed24b5d7a | ||
|
|
77eade01a2 | ||
|
|
a2158983f7 | ||
|
|
c0d57c9498 | ||
|
|
35c8ea5e3f | ||
|
|
b36152484d | ||
|
|
768ca3f0ce | ||
|
|
bedd05c075 | ||
|
|
721f73fdbe | ||
|
|
34c2128d88 | ||
|
|
14de3acdaa | ||
|
|
899b2f8eb6 | ||
|
|
27bb05fedc | ||
|
|
e1909b8ad9 | ||
|
|
0ed7a247b6 | ||
|
|
ee46bf3809 | ||
|
|
469254094b | ||
|
|
acac3fc693 | ||
|
|
022b7ef756 | ||
|
|
69d4f55734 | ||
|
|
a0bff4b859 | ||
|
|
23df599a03 | ||
|
|
c8d74ca350 | ||
|
|
8d6ba43ad0 | ||
|
|
44ca2f7a66 | ||
|
|
ec0be1c7fe | ||
|
|
fd732db91b | ||
|
|
67f45b7767 | ||
|
|
396e6a1c36 | ||
|
|
326c46defd | ||
|
|
7a1762be51 | ||
|
|
b466b476a3 | ||
|
|
e4652d4339 | ||
|
|
f1e4cd3938 | ||
|
|
e192a98079 | ||
|
|
833dc83922 | ||
|
|
ab1751c595 | ||
|
|
fff06f971e | ||
|
|
a138d2964e | ||
|
|
e6d7965453 | ||
|
|
ab714f0fc7 | ||
|
|
465b0f6a16 | ||
|
|
bd87351ea7 | ||
|
|
d79ec44e4c | ||
|
|
a2f84a12ea | ||
|
|
6fd71356ee | ||
|
|
a0a305d9b1 | ||
|
|
6396d90fa6 | ||
|
|
e324750ec2 | ||
|
|
5d99f020fa | ||
|
|
b82e928f58 | ||
|
|
da871897e6 | ||
|
|
81778f73e4 | ||
|
|
2623728518 | ||
|
|
97f1d1b476 | ||
|
|
2f6a837bc0 | ||
|
|
5e22c2d9a5 | ||
|
|
99bd637de4 | ||
|
|
b9177e5580 | ||
|
|
fc7ec184d9 | ||
|
|
7a6ca342af | ||
|
|
30b6e5e5c6 | ||
|
|
f8476decf7 | ||
|
|
49e238577c | ||
|
|
026fff79c6 | ||
|
|
36c3870c2f | ||
|
|
54c309dbda | ||
|
|
f00dd35f93 | ||
|
|
e040efb3c8 | ||
|
|
805d50586b | ||
|
|
a289a807c5 | ||
|
|
e9117f95ee | ||
|
|
82bd4e940f | ||
|
|
ad3b0b33f2 | ||
|
|
b2b664a5b0 | ||
|
|
571f3ebe1d | ||
|
|
c7f09df4e7 | ||
|
|
8758ecae97 | ||
|
|
f13c843ba6 | ||
|
|
e95f7dd540 | ||
|
|
693329b87e | ||
|
|
f1ad521f64 | ||
|
|
82fbba6513 | ||
|
|
66fba8e4cd | ||
|
|
417131fa36 | ||
|
|
9c9d270053 | ||
|
|
f7fab165ba | ||
|
|
93bdf43c95 | ||
|
|
b3866b5b71 | ||
|
|
2308084dee | ||
|
|
6eb5496c27 | ||
|
|
c5514fdb63 | ||
|
|
c78c3058fd | ||
|
|
10d9ef9906 | ||
|
|
43426041ef | ||
|
|
125eb9ac53 | ||
|
|
681407e0a2 | ||
|
|
082f3a8fe8 | ||
|
|
397cc26b2a | ||
|
|
331ae92843 | ||
|
|
06843cd41a | ||
|
|
28b5ef9ee9 | ||
|
|
63dcc057d3 | ||
|
|
0bc16ee5ff | ||
|
|
abcc9c2c80 | ||
|
|
daf2ad38bd | ||
|
|
3dc418df39 | ||
|
|
00aaafbc12 | ||
|
|
bd49a55f3d | ||
|
|
013975b7a6 | ||
|
|
392026286a | ||
|
|
29ef974565 | ||
|
|
06c8216092 | ||
|
|
03f04d24a5 | ||
|
|
7b45ed63cc | ||
|
|
6e4dd1d69c | ||
|
|
185b4cba0c | ||
|
|
8198ea4a2c | ||
|
|
aaf3e8a5cf | ||
|
|
ecef56fa8f | ||
|
|
349ce3f2d0 | ||
|
|
e3d4741213 | ||
|
|
9d6d5f1d76 | ||
|
|
3152d67f58 | ||
|
|
cb41c8d15b | ||
|
|
06590842d6 | ||
|
|
d4c22a0ca5 | ||
|
|
c6f9936292 | ||
|
|
eaa8900758 | ||
|
|
e1e95d8879 | ||
|
|
ef3a0f4878 | ||
|
|
64cc36e7e2 | ||
|
|
1e001bb0fd | ||
|
|
6ba123a003 | ||
|
|
36d0f2c23f | ||
|
|
63412e3645 | ||
|
|
191cf276c3 | ||
|
|
45978bd0bb | ||
|
|
9666652d18 | ||
|
|
ad2716d7c9 | ||
|
|
0a7939bea3 | ||
|
|
b8c50a7b45 | ||
|
|
175e8d2b05 | ||
|
|
046069a656 | ||
|
|
f9522da48f | ||
|
|
c03f959005 | ||
|
|
522aeebe5e | ||
|
|
5312f487f9 | ||
|
|
d9b6624d65 | ||
|
|
1506da54fc | ||
|
|
245512d320 | ||
|
|
487190b379 | ||
|
|
74aaeaa95c | ||
|
|
28e8f0de2b | ||
|
|
f60b5017e2 | ||
|
|
fe80821596 | ||
|
|
628a3c4e7b | ||
|
|
3d59c34ec9 | ||
|
|
35043c2dd6 | ||
|
|
ab815123c9 | ||
|
|
69ab84efe1 | ||
|
|
77823afa54 | ||
|
|
63cd6c1290 | ||
|
|
cab32d2f94 | ||
|
|
1f4316e9dd | ||
|
|
ade762a85e | ||
|
|
bda5d62c72 | ||
|
|
2176fff8c3 | ||
|
|
87893bd54b | ||
|
|
b539a888b1 | ||
|
|
d6b2b0ca13 | ||
|
|
58ee45b702 | ||
|
|
c62d97f23a | ||
|
|
d618c5ea12 | ||
|
|
d8e27f0d33 | ||
|
|
38496ff646 | ||
|
|
da1084907e | ||
|
|
3385b630e7 | ||
|
|
fc59183045 | ||
|
|
33242079f7 | ||
|
|
086148819c | ||
|
|
5df9fd881c | ||
|
|
bd17d36e7f | ||
|
|
be55fa22fd | ||
|
|
b48b3a5e2e | ||
|
|
fc03dd37f1 | ||
|
|
d8bb384689 | ||
|
|
0b32a10bb8 | ||
|
|
f0c027f54e | ||
|
|
b0f2f34d3b | ||
|
|
3e6b76df76 | ||
|
|
6197cf792d | ||
|
|
3c4e5a14f7 | ||
|
|
effc743b6e | ||
|
|
364a945d28 | ||
|
|
07b9354d18 | ||
|
|
8b1e537ca5 | ||
|
|
6a20e850bc | ||
|
|
636892bc9a | ||
|
|
b40f32ab57 | ||
|
|
14bab496b5 | ||
|
|
3cc367e0a3 | ||
|
|
36fc575e40 | ||
|
|
24efb34d91 | ||
|
|
c08e244c95 | ||
|
|
c2f8980f1f | ||
|
|
0ef85b3dee | ||
|
|
93a2431211 | ||
|
|
1fe74937c1 | ||
|
|
6ee016e577 | ||
|
|
f7248dfb1c | ||
|
|
856afb3966 | ||
|
|
bf315261af | ||
|
|
6e83afb580 | ||
|
|
1a5742d4f5 | ||
|
|
0e22458e86 | ||
|
|
cd8d1b8a8f | ||
|
|
141a142742 | ||
|
|
a59b344d20 | ||
|
|
f666711a2a | ||
|
|
1014d64828 | ||
|
|
a126a99853 | ||
|
|
082390a7f0 | ||
|
|
a994553c16 | ||
|
|
3fd2ae954d | ||
|
|
e17c5642ca | ||
|
|
fa7968cb1b | ||
|
|
57c3183b15 | ||
|
|
1fd6471cb1 | ||
|
|
1827230514 | ||
|
|
06dc3d3361 | ||
|
|
a7a2e24d42 | ||
|
|
bb543cb5db | ||
|
|
373ce0ad04 | ||
|
|
fcb979aae1 | ||
|
|
fcc56ad6f7 | ||
|
|
5be8570c8c | ||
|
|
d471442422 | ||
|
|
4070c923fc | ||
|
|
3ca38fe92d | ||
|
|
55ebadfe28 | ||
|
|
9bd2519c83 | ||
|
|
4bfe145be3 | ||
|
|
41085049e2 | ||
|
|
f7312db0c7 | ||
|
|
008534d839 | ||
|
|
8533714cb2 | ||
|
|
b822c19d2c | ||
|
|
2aa3126eb0 | ||
|
|
4c5e85f7ba | ||
|
|
2b41da4543 | ||
|
|
f8dc88df6e | ||
|
|
534033874e | ||
|
|
0851b923fd | ||
|
|
fd4bed65a0 | ||
|
|
4746b8b835 | ||
|
|
d24eafe6a6 | ||
|
|
f3b81edf67 | ||
|
|
976d0da26e | ||
|
|
5113b83bc4 | ||
|
|
a88877bf7c | ||
|
|
a46d7b2ed9 | ||
|
|
170241649d | ||
|
|
1ac22bddd6 | ||
|
|
54fe10ae86 | ||
|
|
33647786e6 | ||
|
|
eb3cb97115 | ||
|
|
236f57ab0e | ||
|
|
c88054107e | ||
|
|
c03c7c35d8 | ||
|
|
b5455215a5 | ||
|
|
85e12e9479 | ||
|
|
f3b7f841fb | ||
|
|
92547bfdb6 | ||
|
|
3739801ed4 | ||
|
|
a6778a6e27 | ||
|
|
f1fc3c63ea | ||
|
|
b2a80775a8 | ||
|
|
1f7f68f6af | ||
|
|
388678f822 | ||
|
|
1230a3323d | ||
|
|
02a3c750f8 | ||
|
|
cbdb9ce614 | ||
|
|
be98ea52d7 | ||
|
|
b6cf63bb0c | ||
|
|
04410033e7 | ||
|
|
e6c6df1334 | ||
|
|
91b06a4297 | ||
|
|
640ad7bd60 | ||
|
|
08b2ea01ab | ||
|
|
236dea9d26 | ||
|
|
f281f3791b | ||
|
|
aff2b80d55 | ||
|
|
e69949c336 | ||
|
|
5f7f36ecd4 | ||
|
|
9212478148 | ||
|
|
dec0ee1001 | ||
|
|
e610c2514d | ||
|
|
3955450245 | ||
|
|
49a437dc0d | ||
|
|
bf37be5013 | ||
|
|
9793de1e96 | ||
|
|
4c15318f28 | ||
|
|
a4d3e78eb1 | ||
|
|
436166c255 | ||
|
|
bbce2c5e35 | ||
|
|
0745a57f52 | ||
|
|
9974c84440 | ||
|
|
3c396e76f6 | ||
|
|
e701aca64b | ||
|
|
26ad482b90 | ||
|
|
d8fd3ef506 | ||
|
|
43016d75e8 | ||
|
|
39b6ce3352 | ||
|
|
1e3ec10a1a | ||
|
|
c4e13eef3f | ||
|
|
6558aedee3 | ||
|
|
a2dfb60466 | ||
|
|
c158dcf2ef | ||
|
|
40318b87bf | ||
|
|
64f06b11b8 | ||
|
|
583194085c | ||
|
|
2d89f57644 | ||
|
|
f4ed01444a | ||
|
|
a7980a202d | ||
|
|
3a6c93dd37 | ||
|
|
6cd272da37 | ||
|
|
a7056b66c7 | ||
|
|
4d6d58ef91 | ||
|
|
93a88ec2c7 | ||
|
|
b679df4fbe | ||
|
|
ba2c7347f9 | ||
|
|
f8b4e6e8f0 | ||
|
|
7ecb4d7b00 | ||
|
|
1697e6ad62 | ||
|
|
6687f76736 | ||
|
|
35e5bbdaf1 | ||
|
|
5c5e7d9509 | ||
|
|
b0c0a9d98c | ||
|
|
7c246f7be4 | ||
|
|
bfc2a41699 | ||
|
|
081a7ead4c | ||
|
|
70fbf1676a | ||
|
|
87ddb6b171 | ||
|
|
c0d45d730f | ||
|
|
6b97a04643 | ||
|
|
2a5a07bae0 | ||
|
|
18e34c670e | ||
|
|
d6a35485d2 | ||
|
|
6204f6cdc8 | ||
|
|
50bc5309f5 | ||
|
|
725e2e92ab | ||
|
|
0b07326e36 | ||
|
|
e86d194f11 | ||
|
|
6949656d0e | ||
|
|
a2c62bab47 | ||
|
|
3dd8aeac7c | ||
|
|
2c342a5c5f | ||
|
|
adef1afdfa | ||
|
|
a980b2606b | ||
|
|
ed83927486 | ||
|
|
e745885b09 | ||
|
|
16ddbfde9f | ||
|
|
bc11537350 | ||
|
|
ab4de79168 | ||
|
|
8134897e91 | ||
|
|
693d22ed25 | ||
|
|
b1dab2466f | ||
|
|
d2b09f39e7 | ||
|
|
4475801a96 | ||
|
|
126ff8cf0d | ||
|
|
a536a785de | ||
|
|
ed89ef74eb | ||
|
|
f1bea27e44 | ||
|
|
7305e53439 | ||
|
|
b08c0e8150 | ||
|
|
8606a4579a | ||
|
|
1dfb72a1d1 | ||
|
|
f09b55b893 | ||
|
|
30ba6029f5 | ||
|
|
9f0c830511 | ||
|
|
973e3138fe | ||
|
|
c996a562e6 | ||
|
|
f2bba4d1ee | ||
|
|
8017a95413 | ||
|
|
26d209daff | ||
|
|
44b979b4a4 | ||
|
|
03ad61abc6 | ||
|
|
fe425f89a4 | ||
|
|
11ad66fb79 | ||
|
|
ca5734a2c6 | ||
|
|
e5414e87c7 | ||
|
|
8142f8f62f | ||
|
|
74cf4076fa | ||
|
|
dbd29c0ce1 | ||
|
|
38a7dc1a93 | ||
|
|
2891bc0b96 | ||
|
|
8846ae6664 | ||
|
|
2e3c3a55aa | ||
|
|
7e44116d51 | ||
|
|
46f85e6395 | ||
|
|
94a384fd81 | ||
|
|
af6acefb53 | ||
|
|
94fd7d252f | ||
|
|
4767e38f5b | ||
|
|
276f6f9fb1 | ||
|
|
2386c71c4f | ||
|
|
21c52db66b | ||
|
|
13cfa02f80 | ||
|
|
eedfbe3e7a | ||
|
|
fe03eb4436 | ||
|
|
d8e45d5c3f | ||
|
|
12e9fb5eeb | ||
|
|
957ffaabae | ||
|
|
cb76e5a23c | ||
|
|
b17cc563ff | ||
|
|
06a0b12efb | ||
|
|
d5bd5ebb7d | ||
|
|
0a9a1c26db | ||
|
|
83bfd8a2d4 | ||
|
|
e5d2c0c700 | ||
|
|
590a5669d6 | ||
|
|
e042740f67 | ||
|
|
dab2ecaa6b | ||
|
|
f9f4133b48 | ||
|
|
33dd21897d | ||
|
|
cb2ef23a29 | ||
|
|
e70e01196f | ||
|
|
f70b9e6eb4 | ||
|
|
d186c69473 | ||
|
|
4d817c48a8 | ||
|
|
c13cab792b | ||
|
|
80aa463aa2 | ||
|
|
bd28b17ad9 | ||
|
|
223119e303 | ||
|
|
7c45cb45ae | ||
|
|
ac11c6729b | ||
|
|
1677654dea | ||
|
|
bc5a7a961b | ||
|
|
c10462223d | ||
|
|
54a9f412e8 | ||
|
|
5a107c58bb | ||
|
|
8f091e7548 | ||
|
|
8cdc7b18c7 | ||
|
|
9f2e87e9fb | ||
|
|
e119458048 | ||
|
|
c2983faf1d | ||
|
|
a09855207e | ||
|
|
1e1859ba6f | ||
|
|
a3937e48a8 | ||
|
|
d2aa53a2ec | ||
|
|
b0bdeea60f | ||
|
|
465e64b9ac | ||
|
|
fc53b28997 | ||
|
|
72e701a4b5 | ||
|
|
2298d5356d | ||
|
|
54137be92b | ||
|
|
7ffb12268d | ||
|
|
790fff460a | ||
|
|
9055dbafe3 | ||
|
|
4454d9115e | ||
|
|
0d74dec446 | ||
|
|
0313dba7b4 | ||
|
|
3fafac75ef | ||
|
|
6b24b46f3d | ||
|
|
474e39a4c9 | ||
|
|
e652298b6a | ||
|
|
9340ae43f3 | ||
|
|
552024c53e | ||
|
|
3aba71ad2f | ||
|
|
ade511df28 | ||
|
|
fc650214d4 | ||
|
|
8266fd0c6f | ||
|
|
f4308032c3 | ||
|
|
1e1f445ade | ||
|
|
d41b0332ac | ||
|
|
7258466572 | ||
|
|
76db92ea14 | ||
|
|
ad3cd66e08 | ||
|
|
22f8855ad7 | ||
|
|
36e095c830 | ||
|
|
887cac1264 | ||
|
|
13059e0568 | ||
|
|
9e8023d716 | ||
|
|
c54ba5fd8c | ||
|
|
db80e063d4 | ||
|
|
b6aa12706a | ||
|
|
c1caf6717d | ||
|
|
513fd9f532 | ||
|
|
bf77f817cb | ||
|
|
e0bfef2ece | ||
|
|
4a87f908a8 | ||
|
|
16d95e5155 | ||
|
|
1797b54259 | ||
|
|
f289c8fb2e | ||
|
|
e4ad881a69 | ||
|
|
138bca38e7 | ||
|
|
44f7af3580 | ||
|
|
2d832bca15 | ||
|
|
efa75a62e3 | ||
|
|
5763bca317 | ||
|
|
c335334402 | ||
|
|
5bf3f70717 | ||
|
|
92c8a440ea | ||
|
|
b92d8a014c | ||
|
|
aced44f051 | ||
|
|
49c9d2b077 | ||
|
|
61beacf085 | ||
|
|
02f432238e | ||
|
|
864d178e01 | ||
|
|
78f0b823a9 | ||
|
|
26cdc7a0ee | ||
|
|
5e773f1eee | ||
|
|
4a7ac7df22 | ||
|
|
5250670d5d | ||
|
|
de4a825db8 | ||
|
|
c256419144 | ||
|
|
7bdca0420e | ||
|
|
3aa1fbced9 | ||
|
|
dbbb70027a | ||
|
|
b4e78d28f8 | ||
|
|
e3d4e38a59 | ||
|
|
386f558eae | ||
|
|
e08424d3a3 | ||
|
|
03ad403e7a | ||
|
|
4a674aae99 | ||
|
|
8ee3744027 | ||
|
|
965327e801 | ||
|
|
f82ea43324 | ||
|
|
a5c63845b4 | ||
|
|
034faa72cf | ||
|
|
9bcd617964 | ||
|
|
0db975dc7b | ||
|
|
a51fa7703b | ||
|
|
69fad0009d | ||
|
|
e721251936 | ||
|
|
2fe767e3e5 | ||
|
|
6328ef4444 | ||
|
|
50b8e084e7 | ||
|
|
3d88544feb | ||
|
|
62e602c32e | ||
|
|
47a82560ea | ||
|
|
f7bbcc98b3 | ||
|
|
98a587aa15 | ||
|
|
d2e34c42fd | ||
|
|
605b07901e | ||
|
|
18f02fac68 | ||
|
|
28ea37f367 | ||
|
|
65a737bb58 | ||
|
|
7423cd2f93 | ||
|
|
babd026351 | ||
|
|
dd6e5a9029 | ||
|
|
02519a4429 | ||
|
|
6575121b7a | ||
|
|
5b66368f0d | ||
|
|
971c6720e4 | ||
|
|
3afccc279f | ||
|
|
8f015d0672 | ||
|
|
f33b96861c | ||
|
|
9832ce2ff9 | ||
|
|
490cbbaa48 | ||
|
|
d1c91093e2 | ||
|
|
66fe101ccd | ||
|
|
7ab8c6b154 | ||
|
|
73017b14c3 | ||
|
|
f55495cd6a | ||
|
|
e97146b5a3 | ||
|
|
58f056c76d | ||
|
|
338bbc7a1f | ||
|
|
4ba54738a9 | ||
|
|
235fd2adc4 | ||
|
|
b15d518c94 | ||
|
|
021e1c122c | ||
|
|
014b0dd6f6 | ||
|
|
f9f68f9b86 | ||
|
|
11a8ba131a | ||
|
|
858de64f8e | ||
|
|
676e60afb7 | ||
|
|
b1968f3f8b | ||
|
|
d2d077afaa | ||
|
|
7097ca401d | ||
|
|
73e9a1eb9e | ||
|
|
0439d455fb | ||
|
|
d57f665a78 | ||
|
|
859c731a13 | ||
|
|
2e7613ddec | ||
|
|
57e9436783 | ||
|
|
2f153fda2e | ||
|
|
cbcb5905a3 | ||
|
|
6a2fb37615 | ||
|
|
6403feaff9 | ||
|
|
47736910ca | ||
|
|
ead592a0bf | ||
|
|
d5bdba9244 | ||
|
|
4f033cec8d | ||
|
|
a58f4b2498 | ||
|
|
01522ed8c7 | ||
|
|
fa99ee9d5b | ||
|
|
6efe634850 | ||
|
|
60a1497eaf | ||
|
|
1d0cbc08df | ||
|
|
4d4280033b | ||
|
|
fd58775cae | ||
|
|
ccb0e93da2 | ||
|
|
c2a05da908 | ||
|
|
e1da9e60fc | ||
|
|
d044e535e0 | ||
|
|
293560dcd4 | ||
|
|
90ebb815d5 | ||
|
|
3d3d418ee6 | ||
|
|
f875cd05be | ||
|
|
435911489f | ||
|
|
5fcfcd53aa | ||
|
|
bc09215aad | ||
|
|
5f7e109e3d | ||
|
|
b75a5050d7 | ||
|
|
be497f7083 | ||
|
|
0ccae3e15b | ||
|
|
d736c32aec | ||
|
|
8ea5ba5d3f | ||
|
|
60c341befd | ||
|
|
be4f58ed8f | ||
|
|
d82d1abab6 | ||
|
|
0d81bd457c | ||
|
|
af2b19436f | ||
|
|
51beb3c7e4 | ||
|
|
5061456735 | ||
|
|
b01eb3af95 | ||
|
|
328bebc168 | ||
|
|
fc63fffa15 | ||
|
|
707584b2ef | ||
|
|
561459d93b | ||
|
|
25e48ae546 | ||
|
|
513bb3e8d0 | ||
|
|
04710ca908 | ||
|
|
fcf0fcf20c | ||
|
|
2ff40d8e37 | ||
|
|
1bab5b06a4 | ||
|
|
01cd4bcb47 | ||
|
|
49b2a559ae | ||
|
|
9212d24685 | ||
|
|
eb43b11202 | ||
|
|
5c4cae8c9d | ||
|
|
cfd7099743 | ||
|
|
19ae237d29 | ||
|
|
9cda78e561 | ||
|
|
cc31872a7f | ||
|
|
3c2c896708 | ||
|
|
b73da9c54c | ||
|
|
414a45bfb0 | ||
|
|
2a6f808bca | ||
|
|
cdf2a13bbd | ||
|
|
3e3e8a14ee | ||
|
|
37e180827a | ||
|
|
b047b54545 | ||
|
|
b7bb4bbd57 | ||
|
|
86cf2cd233 | ||
|
|
ab12c201b4 | ||
|
|
a8f03d859c | ||
|
|
3c7580f024 | ||
|
|
277833e388 | ||
|
|
eb16d7e6f9 | ||
|
|
1418068d2b | ||
|
|
774346f5f8 | ||
|
|
1aab88e6ca | ||
|
|
613f49b8bb | ||
|
|
5c95dc6e20 | ||
|
|
cbc2713bee | ||
|
|
2955975793 | ||
|
|
f8299d7f40 | ||
|
|
e855d44523 | ||
|
|
64e7715480 | ||
|
|
2e9a74f609 | ||
|
|
11a1230738 | ||
|
|
298373742e | ||
|
|
dc7aeecd85 | ||
|
|
15a7de7b24 | ||
|
|
714d0d4092 | ||
|
|
225d7f39d1 | ||
|
|
0005798c83 | ||
|
|
1d9078f9be | ||
|
|
510ac7005a | ||
|
|
c049b968a5 | ||
|
|
858698f7cd | ||
|
|
d104f6f8fc | ||
|
|
3ecf0d3230 | ||
|
|
6e4131fee4 | ||
|
|
41fa6bc8ed | ||
|
|
58a29bf058 | ||
|
|
7dac17de18 | ||
|
|
799d7de182 | ||
|
|
735af02f59 | ||
|
|
ad3f3799fa | ||
|
|
5f97df015e | ||
|
|
ff18fd2c38 | ||
|
|
3ab0cd02df | ||
|
|
c31072f42f | ||
|
|
c01c59023a | ||
|
|
4329aac377 | ||
|
|
c10b31e9d0 | ||
|
|
71a789c0b4 | ||
|
|
deb9847e2b | ||
|
|
9e9e7e1e96 | ||
|
|
d34e0341e2 | ||
|
|
aec254b05a | ||
|
|
f8b420047a | ||
|
|
7e6e4c0bc6 | ||
|
|
71fb59943c | ||
|
|
34419d0ca1 | ||
|
|
475a36f0d7 | ||
|
|
1234c1e7e2 | ||
|
|
a4a400facf | ||
|
|
ed2ca4d896 | ||
|
|
ce42e4d1cd | ||
|
|
b048128e77 | ||
|
|
635c257502 | ||
|
|
58a38c08d7 | ||
|
|
8fbee7737b | ||
|
|
e84f5f184e | ||
|
|
0bd26b19d7 | ||
|
|
64f82d5d51 | ||
|
|
f63ff994ce | ||
|
|
a10ee43271 | ||
|
|
54ed29e08d | ||
|
|
cc097e7a3f | ||
|
|
5de92ada43 | ||
|
|
0c546211cf | ||
|
|
4dc5a3a67c | ||
|
|
c51b226ceb | ||
|
|
0a5ca6cf74 | ||
|
|
96957219e4 | ||
|
|
32b7620db3 | ||
|
|
347f65e089 | ||
|
|
16628a427e | ||
|
|
ed16034a25 | ||
|
|
0c5f144e41 | ||
|
|
acc7d6e7dc | ||
|
|
84b4139052 | ||
|
|
9943643958 | ||
|
|
9ceaefb663 | ||
|
|
ec03ea5bc1 | ||
|
|
5855633c1f | ||
|
|
a53bc2bc2e | ||
|
|
88445820ed | ||
|
|
044ed3ae98 | ||
|
|
6f48012234 | ||
|
|
d344318dd4 | ||
|
|
6273dd3d83 | ||
|
|
0f3f3cbffd | ||
|
|
3244123b21 | ||
|
|
cba2ee3622 | ||
|
|
25ed925df5 | ||
|
|
8c5bd60bab | ||
|
|
c5510556a7 | ||
|
|
bbcfca84ef | ||
|
|
1260e94c2a | ||
|
|
8a02574303 | ||
|
|
c930f08348 | ||
|
|
5204acb5d0 | ||
|
|
784aaa98c9 | ||
|
|
745e2494bc | ||
|
|
c00792519d | ||
|
|
142fe5a12c | ||
|
|
5b127f232e | ||
|
|
c22bf01003 | ||
|
|
05e4911d6f | ||
|
|
9b551ef0ba | ||
|
|
56a8bb2349 | ||
|
|
8503c6a64d | ||
|
|
820f18da4d | ||
|
|
51a2432ebf | ||
|
|
6639534e97 | ||
|
|
0621577c7d | ||
|
|
26a507e3db | ||
|
|
244b540fe0 | ||
|
|
030ca4c173 | ||
|
|
88a2810f29 | ||
|
|
9164ee363a | ||
|
|
4cd47fdcc5 | ||
|
|
708852a3cb | ||
|
|
4a93bdf3ea | ||
|
|
22e7d2a811 | ||
|
|
93eca1dff2 | ||
|
|
9afe7408cd | ||
|
|
5dc2347a25 | ||
|
|
e3a0124b10 | ||
|
|
16af89c281 | ||
|
|
621e4258c8 | ||
|
|
ac6272e739 | ||
|
|
6e84f517a9 | ||
|
|
fdbdb3ad86 | ||
|
|
7adcf5ca46 | ||
|
|
fe6716cf76 | ||
|
|
3c2096db68 | ||
|
|
58cad1a6b3 | ||
|
|
662e67ff16 | ||
|
|
8d577b872f | ||
|
|
b55290f3cb | ||
|
|
e8d3eb7393 | ||
|
|
47fa16e35f | ||
|
|
a87f769b85 | ||
|
|
8e63fa4594 | ||
|
|
63501a0d59 | ||
|
|
828fb37ca8 | ||
|
|
40f513d3b6 | ||
|
|
f0b8b66a75 | ||
|
|
d51cdc068b | ||
|
|
f8b382e480 | ||
|
|
1995f43b67 | ||
|
|
69e0392a8b | ||
|
|
1f6319442e | ||
|
|
559c4c0c2c | ||
|
|
feeb5b58d9 | ||
|
|
7a00f79a56 | ||
|
|
10d744704a | ||
|
|
eee35f9cc3 | ||
|
|
b3656761eb | ||
|
|
7b5fe34316 | ||
|
|
4536780a19 | ||
|
|
05d866e6b3 | ||
|
|
0d138cf473 | ||
|
|
dbe539ac80 | ||
|
|
665a39d179 | ||
|
|
5fd5d8c8c5 | ||
|
|
2832b4564c | ||
|
|
d4369a64ee | ||
|
|
81fa1630b7 | ||
|
|
a1c4b35205 | ||
|
|
5e567f3e37 | ||
|
|
c4757684c1 | ||
|
|
a55a6bf94b | ||
|
|
fa1792eb77 | ||
|
|
93a8f6e759 | ||
|
|
4a614855d4 | ||
|
|
8bdd47f912 | ||
|
|
f9e82abadc | ||
|
|
428fda81e2 | ||
|
|
29c9ad602d | ||
|
|
44458e2a97 | ||
|
|
861fb1f54b | ||
|
|
02534f4d55 | ||
|
|
5532cb95a2 | ||
|
|
9176e43fc9 | ||
|
|
cb190f54fc | ||
|
|
4be2539bc2 | ||
|
|
291e2adffa | ||
|
|
fa2ec63f45 | ||
|
|
946c943457 | ||
|
|
0e50766d6e | ||
|
|
58a1610ae0 | ||
|
|
06dc21168a | ||
|
|
305b67fbed | ||
|
|
4da6d152c3 | ||
|
|
25630f1ef5 | ||
|
|
9b01e3f1c9 | ||
|
|
99450400eb | ||
|
|
2f8a8988d7 | ||
|
|
9104d2e89e | ||
|
|
e75022763c | ||
|
|
f0f3fb337d | ||
|
|
f7f01a34c2 | ||
|
|
f9f9ff0cb8 | ||
|
|
522ba05ba8 | ||
|
|
f4f4093466 | ||
|
|
2e16ab0c2c | ||
|
|
6f02606fb7 | ||
|
|
df40142b51 | ||
|
|
cc290d488b | ||
|
|
64328218fc | ||
|
|
8d1356a085 | ||
|
|
4f39dd0f73 | ||
|
|
54ffc8ae45 | ||
|
|
78ab1944bd | ||
|
|
434cf94657 | ||
|
|
dcb893e230 | ||
|
|
ce4fadc378 | ||
|
|
5683d1b1bd | ||
|
|
0eb88d0c10 | ||
|
|
eb1367e54d | ||
|
|
33a4786206 | ||
|
|
8c6606ad95 | ||
|
|
cde9519a76 | ||
|
|
7b2e0d79cb | ||
|
|
5b0da8e92a | ||
|
|
0126d2f77c | ||
|
|
0b436014c9 | ||
|
|
2cb7f223ed | ||
|
|
eca551ed98 | ||
|
|
608fd92861 | ||
|
|
e37d8fe45f | ||
|
|
4cce91ec97 | ||
|
|
72fdde35dc | ||
|
|
d425187778 | ||
|
|
e419aa1f1a | ||
|
|
5506547f7f | ||
|
|
568ed72b3e | ||
|
|
e8cc0e6684 | ||
|
|
4331f69395 | ||
|
|
7cc67ae7cb | ||
|
|
244b3438fc | ||
|
|
1a741f7ca0 | ||
|
|
1447800e2b | ||
|
|
f968fe7512 | ||
|
|
0a2349fad7 | ||
|
|
941b8cbc1e | ||
|
|
3b7b16acfd | ||
|
|
fbc7bb68fc | ||
|
|
0d16880596 | ||
|
|
3b5218128f | ||
|
|
cb731bf1db | ||
|
|
7c4d6eb02d | ||
|
|
c14e7fb17a | ||
|
|
fe57811bc5 | ||
|
|
e073b48f7d | ||
|
|
a9df609593 | ||
|
|
6c3db9646e | ||
|
|
ff9c4c717e | ||
|
|
182374b46f | ||
|
|
0871cda526 | ||
|
|
1b47cba37a | ||
|
|
e5bef36905 | ||
|
|
706d723703 | ||
|
|
51eacbfac5 | ||
|
|
5c2a411982 | ||
|
|
08d65cbc41 | ||
|
|
9d2bf429c1 | ||
|
|
d34f863bd4 | ||
|
|
b4abf1c2c7 | ||
|
|
68baaf589e | ||
|
|
be74e41d84 | ||
|
|
848122b0ec | ||
|
|
0edcb7c0d9 | ||
|
|
cc58e06b5e | ||
|
|
0d6ca606ea | ||
|
|
75ee93789f | ||
|
|
05daddafbf | ||
|
|
7bbce6725d | ||
|
|
789b211586 | ||
|
|
826a043748 | ||
|
|
6761048298 | ||
|
|
738fc9acad | ||
|
|
43c0540de7 | ||
|
|
2d1c3d8121 | ||
|
|
f48a5c650d | ||
|
|
66c18eddb8 | ||
|
|
fdd2ee6365 | ||
|
|
c207f60ad8 | ||
|
|
0eaa95c8c0 | ||
|
|
df2fca5935 | ||
|
|
dcaf5d9c7d | ||
|
|
0112969a97 | ||
|
|
3ec0f3d69c | ||
|
|
5555d300a1 | ||
|
|
8155ef4b60 | ||
|
|
a12402f6c8 | ||
|
|
cf28b814cb | ||
|
|
b05f67db19 | ||
|
|
260f4659d5 | ||
|
|
9e700f298c | ||
|
|
56510734c4 | ||
|
|
3938a4d14e | ||
|
|
fa3b9eeeaf | ||
|
|
eb9d6fa25c | ||
|
|
b53307c1c2 | ||
|
|
c3fc708a66 | ||
|
|
b34ffbe6d0 | ||
|
|
f364315e48 | ||
|
|
3ddb5a13a5 | ||
|
|
a24cc399a4 | ||
|
|
305f4b2688 | ||
|
|
9823171d65 | ||
|
|
4761bd8fda | ||
|
|
9c22698723 | ||
|
|
e3892bbcc6 | ||
|
|
629b156f52 | ||
|
|
c45dd47d34 | ||
|
|
ef8831f784 | ||
|
|
c5a42cf5de | ||
|
|
90ebbfc20f | ||
|
|
17cd0dc91d | ||
|
|
fa1f42af59 | ||
|
|
f45ea1ab53 | ||
|
|
0dde3fe483 | ||
|
|
277dc7dd09 | ||
|
|
3215d0b856 | ||
|
|
0167d5efcd | ||
|
|
b48ac808a6 | ||
|
|
616524775c | ||
|
|
5832849b11 | ||
|
|
467c5d01e9 | ||
|
|
24711a2f39 | ||
|
|
24e8286f35 | ||
|
|
e8a1378ad0 | ||
|
|
76bb418ea9 | ||
|
|
cd8770a3e3 | ||
|
|
da834c0935 | ||
|
|
024ffb1117 | ||
|
|
eed7ab9793 | ||
|
|
032feb343f | ||
|
|
eabccba3fa | ||
|
|
d86d656316 | ||
|
|
fa73c91b0b | ||
|
|
2eee50832d | ||
|
|
b40736918b | ||
|
|
ffb1a2e30f | ||
|
|
d6c3c0c6c1 | ||
|
|
ee251721ac | ||
|
|
fdbb9195d5 | ||
|
|
c68b08d9af | ||
|
|
3653bbfca0 | ||
|
|
05c7cc7277 | ||
|
|
5670bf099b | ||
|
|
0c324b0f09 | ||
|
|
968557e38e | ||
|
|
882cdebacb | ||
|
|
07753e1774 | ||
|
|
5b984507fc | ||
|
|
27df481967 | ||
|
|
0943031f23 | ||
|
|
2d95168de0 | ||
|
|
97cae8f92c | ||
|
|
eb213bac92 | ||
|
|
8187788b2c | ||
|
|
c80e08abce | ||
|
|
42fd851e5c | ||
|
|
70e4ebccab | ||
|
|
140f87c741 | ||
|
|
b0d756123e | ||
|
|
6188c92916 | ||
|
|
34c6f96728 | ||
|
|
50fd047c0b | ||
|
|
5bcc05b536 | ||
|
|
ce7d6c8dd5 | ||
|
|
d87a1e28b4 | ||
|
|
227306c572 | ||
|
|
45c2691f89 | ||
|
|
d0c81245b8 | ||
|
|
e494afb1aa | ||
|
|
ecc3c1cf3b | ||
|
|
228b16416a | ||
|
|
17eb74842a | ||
|
|
c01ff74c73 | ||
|
|
f88613b26d | ||
|
|
3464f4241f | ||
|
|
849b703828 | ||
|
|
4b935a40b6 | ||
|
|
5873a23ccb | ||
|
|
eae2786825 | ||
|
|
6407386de5 | ||
|
|
3fe950723f | ||
|
|
52bf6acd46 | ||
|
|
9590e7d7e0 | ||
|
|
7a08140a2d | ||
|
|
d1491cfbd1 | ||
|
|
695b80549d | ||
|
|
11c60a637f | ||
|
|
844ad70bb9 | ||
|
|
5ac7cde577 | ||
|
|
ce3ef0550f | ||
|
|
813f3e7d42 | ||
|
|
d03f97af6b | ||
|
|
019ab0286d | ||
|
|
c6647b4706 | ||
|
|
f913536d88 | ||
|
|
640d1bd176 | ||
|
|
66baccf528 | ||
|
|
6e6dacbace | ||
|
|
cdbb10fb26 | ||
|
|
c34ba3918c | ||
|
|
fa228c876c | ||
|
|
2f4d0af7d7 | ||
|
|
2d3e5235a9 | ||
|
|
8e91ccaa54 | ||
|
|
6955658b36 | ||
|
|
dbb44401fd | ||
|
|
b42ed70c84 | ||
|
|
a28276d823 | ||
|
|
fa4b27dd0e | ||
|
|
0be44d5c49 | ||
|
|
2514596276 | ||
|
|
7008d2a953 | ||
|
|
2539fedfc4 | ||
|
|
b453df7591 | ||
|
|
9e5d5edcba | ||
|
|
2d5de6ff99 | ||
|
|
259e9f1c17 | ||
|
|
daeb53009e | ||
|
|
f12d271ca5 | ||
|
|
965185ca3b | ||
|
|
9c484f6a78 | ||
|
|
de18c3c722 | ||
|
|
9be753b281 | ||
|
|
d6ae122de1 | ||
|
|
c6b90044f2 | ||
|
|
14898b6422 | ||
|
|
26294b0759 | ||
|
|
6da45b5c2b | ||
|
|
674332fddd | ||
|
|
ab8942d05a | ||
|
|
29790b8a5c | ||
|
|
4a4c26ffeb | ||
|
|
25c9bc07b2 | ||
|
|
d22d4c4c83 | ||
|
|
d88640fd20 | ||
|
|
57a2fca3a4 | ||
|
|
f796688c84 | ||
|
|
d6bbf8b7cc | ||
|
|
37ec460f64 | ||
|
|
004b9c95e4 | ||
|
|
86e27b465a | ||
|
|
5e9afddc3a | ||
|
|
de281535b1 | ||
|
|
9df7def14e | ||
|
|
5b9db9795d | ||
|
|
7d2ce7e6ab | ||
|
|
3e807af2b2 | ||
|
|
4c64dc7885 | ||
|
|
e7a7874b34 | ||
|
|
c78a47788b | ||
|
|
922698c5d9 | ||
|
|
8e8a490936 | ||
|
|
231bc0605f | ||
|
|
0298ff9478 | ||
|
|
33a25dcf0e | ||
|
|
54c16e3cdb | ||
|
|
28a978acc2 | ||
|
|
bea26a461f | ||
|
|
ed54c5b8b9 | ||
|
|
13316b68aa | ||
|
|
043986f35b | ||
|
|
2dc4421dd6 | ||
|
|
6c16e2bca2 | ||
|
|
c2b4a8e115 | ||
|
|
63b7bc8794 | ||
|
|
f41ae74ae2 | ||
|
|
98689d223e | ||
|
|
f19cf21146 | ||
|
|
24e19e6b18 | ||
|
|
08376cb15e | ||
|
|
5f6e4663c0 | ||
|
|
9b91c00fcc | ||
|
|
229ab88c2f | ||
|
|
8863d13578 | ||
|
|
e07fc9fbb9 | ||
|
|
0164574fdd | ||
|
|
98eec332d8 | ||
|
|
3d2986fc64 | ||
|
|
29e7f8581e | ||
|
|
4ee3f6c87a | ||
|
|
b8c7440e1f | ||
|
|
d49ff8d9a4 | ||
|
|
07198042bd | ||
|
|
c7a9492e96 | ||
|
|
360c6f3c1c | ||
|
|
89aab4acd5 | ||
|
|
d9b3e842d9 | ||
|
|
3ac4dc8392 | ||
|
|
0d1a5318ec | ||
|
|
94b7a219fd | ||
|
|
ba3eb71abd | ||
|
|
bbc9e11205 | ||
|
|
75571e4266 | ||
|
|
4e879271a0 | ||
|
|
552e0fefc3 | ||
|
|
cb7439a831 | ||
|
|
35d6b8bbc6 | ||
|
|
48b9220ffc | ||
|
|
5537981877 | ||
|
|
711f24a5b2 | ||
|
|
5d2b8bc8aa | ||
|
|
f6ea10db2d | ||
|
|
fc38ba3acb | ||
|
|
0830ad268f | ||
|
|
e633664c2a | ||
|
|
d4c7d9a60a | ||
|
|
5ee0d964f3 | ||
|
|
ba5e0f145f | ||
|
|
34eb9cc063 | ||
|
|
a795fdc40d | ||
|
|
24cba4c4ca | ||
|
|
3d13f4bb9b | ||
|
|
e713d0d321 | ||
|
|
4e34be87a1 | ||
|
|
07307d37a1 | ||
|
|
81463181bc | ||
|
|
02e57927fc | ||
|
|
36925f0dbd | ||
|
|
f9b985e03d | ||
|
|
598ad62b92 | ||
|
|
ea929ab713 | ||
|
|
04e56ced58 | ||
|
|
2278565b86 | ||
|
|
afd0c56b44 | ||
|
|
5ebdf66d22 | ||
|
|
177d8a72a7 | ||
|
|
03ef80dd8e | ||
|
|
6f9825362a | ||
|
|
2167154064 | ||
|
|
f88b35bd80 | ||
|
|
6b9520338e | ||
|
|
438c087856 | ||
|
|
2a43274b06 | ||
|
|
20a9336867 | ||
|
|
c921782714 | ||
|
|
776ac9e3d4 | ||
|
|
d02bd9b717 | ||
|
|
50070e8fe7 | ||
|
|
e3e3b3e279 | ||
|
|
38fba297e8 | ||
|
|
52d65ee4e8 | ||
|
|
9ad2f33dd8 | ||
|
|
02ae23b11d | ||
|
|
70c6d6e7ae | ||
|
|
8efebf992f | ||
|
|
b9be94bcc5 | ||
|
|
e6310c32ac | ||
|
|
654b4702d0 | ||
|
|
262b5a7ee5 | ||
|
|
ef0d4fe34b | ||
|
|
c08342f40c | ||
|
|
e7796268b5 | ||
|
|
0cbe80d2ab | ||
|
|
11d3ba70a0 | ||
|
|
c30e4c4867 | ||
|
|
d1e5087c18 | ||
|
|
618dd442e3 | ||
|
|
7f26fdf2d0 | ||
|
|
64090474e1 | ||
|
|
a69c28713a | ||
|
|
1d4b3095af | ||
|
|
ff75125af8 | ||
|
|
aa0025abbe | ||
|
|
c9436da235 | ||
|
|
12f1eaace7 | ||
|
|
09ef8aba0f | ||
|
|
08c094b8a5 | ||
|
|
e9fb4410cd | ||
|
|
cbdda22a33 | ||
|
|
fe906477da | ||
|
|
b03df619df | ||
|
|
53d89d8d17 | ||
|
|
1e5a1f3e1f | ||
|
|
6efe2979c6 | ||
|
|
92cc2c8e69 | ||
|
|
50dd2e4179 | ||
|
|
7a8fd9c3d3 | ||
|
|
d5a3fc490b | ||
|
|
13f948062b | ||
|
|
b965fda226 | ||
|
|
f9d67f0e9d | ||
|
|
4dfa20e40b | ||
|
|
d5edbaa3a9 | ||
|
|
0cd5ce8c29 | ||
|
|
1c50a87ca2 | ||
|
|
efa83e05e4 | ||
|
|
76a694d043 | ||
|
|
571280f0cd | ||
|
|
c2fc01608e | ||
|
|
2ba144843a | ||
|
|
458dadc9b6 | ||
|
|
6ed0c59762 | ||
|
|
54fbaa808e | ||
|
|
f0db63da35 | ||
|
|
9b8c80b74d | ||
|
|
0c23b6af84 | ||
|
|
1189177079 | ||
|
|
794402e92d | ||
|
|
0de6d87af5 | ||
|
|
567c150eaa | ||
|
|
7ea9225277 | ||
|
|
df25ead15a | ||
|
|
5227d57a55 | ||
|
|
8db86992aa | ||
|
|
79c09e613b | ||
|
|
99d1cea537 | ||
|
|
98bc3f18fe | ||
|
|
b007d01057 | ||
|
|
ea85e0824b | ||
|
|
d75b48877d | ||
|
|
94bda8c17d | ||
|
|
f05cb2859e | ||
|
|
3c6254f086 | ||
|
|
d9dc6c0a49 | ||
|
|
3cfe1b8376 | ||
|
|
83275c5fd0 | ||
|
|
e4698b5843 | ||
|
|
c4b134c0b5 | ||
|
|
5065cdb9e6 | ||
|
|
f72be9a1e4 | ||
|
|
a53f9eb294 | ||
|
|
44e0eedac2 | ||
|
|
d894556191 | ||
|
|
00cac892a7 | ||
|
|
167d332257 | ||
|
|
258abf6fe3 | ||
|
|
451b362c52 | ||
|
|
ff6b433661 | ||
|
|
3af2a44c70 | ||
|
|
7f712e4d72 | ||
|
|
28dee33e4f | ||
|
|
2d0b503f9f | ||
|
|
b0b706e2f4 | ||
|
|
0391fad32b | ||
|
|
167902616c | ||
|
|
ea42a6274b | ||
|
|
65e72d6937 | ||
|
|
bb5ba8c37c | ||
|
|
f5e5921abc | ||
|
|
80a8cfb6a6 | ||
|
|
4e34040e62 | ||
|
|
ba2620d91d | ||
|
|
c2ae4a5efd | ||
|
|
62c1ce73bb | ||
|
|
bab6380d68 | ||
|
|
9502355d22 | ||
|
|
a82d9591ab | ||
|
|
d8fe11f393 | ||
|
|
df5963082c | ||
|
|
c3980e4f27 | ||
|
|
a7155300d3 | ||
|
|
b622fe7229 | ||
|
|
2ddf3c8881 | ||
|
|
38ba009794 | ||
|
|
a55649b3e1 | ||
|
|
fdf80ed89d | ||
|
|
2da27d59b6 | ||
|
|
b67e718412 | ||
|
|
b05286f455 | ||
|
|
2a5f032a52 | ||
|
|
27a79d9c8c | ||
|
|
7ff72c048a | ||
|
|
388c0b2b9f | ||
|
|
bb09267f2a | ||
|
|
0cd13b90f4 | ||
|
|
fbb39a364e | ||
|
|
7bffe6b2d5 | ||
|
|
df4b89366c | ||
|
|
05075d6508 | ||
|
|
5e40d93d63 | ||
|
|
c2f5177afa | ||
|
|
e5e01e51a9 | ||
|
|
8f802f1241 | ||
|
|
a54372e05e | ||
|
|
f964439a15 | ||
|
|
309c1e004b | ||
|
|
9d91250f05 | ||
|
|
1f7262aaaa | ||
|
|
9a5e433489 | ||
|
|
d1f5d58eeb | ||
|
|
e3d118f5bc | ||
|
|
1a11f5777a | ||
|
|
b3e57ca3e5 | ||
|
|
1a70a45805 | ||
|
|
989638a42d | ||
|
|
9204142eaf | ||
|
|
af1d85ae75 | ||
|
|
25d92ca4b0 | ||
|
|
52a3e990c6 | ||
|
|
1370e0dec4 | ||
|
|
f99a89eae2 | ||
|
|
9954763356 | ||
|
|
538496ed6b | ||
|
|
7d80a9d048 | ||
|
|
a0ef56f245 | ||
|
|
e016fb2d6b | ||
|
|
62081cb399 | ||
|
|
bfc8c90abb | ||
|
|
967990b76d | ||
|
|
6ff9f30473 | ||
|
|
025b0547cd | ||
|
|
3370475fe9 | ||
|
|
12896cceaa | ||
|
|
62ffe26b42 | ||
|
|
c83c4d0892 | ||
|
|
9ff9b68d91 | ||
|
|
daa299c7a6 | ||
|
|
67b5de205b | ||
|
|
5a9c064943 | ||
|
|
24ca19d502 | ||
|
|
d2d2c75967 | ||
|
|
684b7fe0b8 | ||
|
|
2c5320a0b0 | ||
|
|
30738d7810 | ||
|
|
5281d521f4 | ||
|
|
58bdbadb11 | ||
|
|
e9b2f1d2fb | ||
|
|
8c8763a620 | ||
|
|
6497f7bfe8 | ||
|
|
9b035230ac | ||
|
|
9d3bff9e54 | ||
|
|
3b86b3ac77 | ||
|
|
c87327bb77 | ||
|
|
c9880b953f | ||
|
|
b187bf12c2 | ||
|
|
19ab29628f | ||
|
|
bbecd505eb | ||
|
|
69d3a9e363 | ||
|
|
f5873fe0d7 | ||
|
|
4762e1cc4c | ||
|
|
8ae989cce8 | ||
|
|
c6adf3a6d8 | ||
|
|
976e07c125 | ||
|
|
7c1dc1c977 | ||
|
|
3e749dd652 | ||
|
|
adf04ba632 | ||
|
|
f7842fdcdd | ||
|
|
b2976984d3 | ||
|
|
7e1b0d13c7 | ||
|
|
8487777f96 | ||
|
|
2d86254549 | ||
|
|
e77486f771 | ||
|
|
53f8a9698f | ||
|
|
bd6eb723dd | ||
|
|
5c78e6b171 | ||
|
|
44ce95979b | ||
|
|
44ce00d6e9 | ||
|
|
df0925394b | ||
|
|
5b5b0b0405 | ||
|
|
6e73321a95 | ||
|
|
d09020d144 | ||
|
|
e2a8fa8738 | ||
|
|
1119ee54af | ||
|
|
e6cd7c838f | ||
|
|
2b59068e50 | ||
|
|
5cc3888022 | ||
|
|
78975c286a | ||
|
|
7a40d9c44b | ||
|
|
460b71e3d9 | ||
|
|
107070e6e2 | ||
|
|
fb176f56d0 | ||
|
|
f67dc57384 | ||
|
|
dc7c0cd981 | ||
|
|
5cda2ad19f | ||
|
|
470b2ae369 | ||
|
|
14ee08ce6d | ||
|
|
c85b2567f7 | ||
|
|
ef110128f2 | ||
|
|
1fc249e772 | ||
|
|
7388cb33d4 | ||
|
|
f40c8f2dc5 | ||
|
|
eb914d03ce | ||
|
|
e087f2e1b6 | ||
|
|
f0c24d5152 | ||
|
|
44f514f02c | ||
|
|
a63c42f59c | ||
|
|
65185943ca | ||
|
|
de1f707434 | ||
|
|
0d0e00a8bd | ||
|
|
5054b82030 | ||
|
|
182d0381c3 | ||
|
|
fb0429b2a5 | ||
|
|
c7a43b09ce | ||
|
|
d18b430c16 | ||
|
|
9b4415f7b3 | ||
|
|
6c36c599a5 | ||
|
|
a6fb000266 | ||
|
|
92024e2b0e | ||
|
|
b229c01450 | ||
|
|
15867d3ef6 | ||
|
|
5abd7817af | ||
|
|
fa0fdbf0d1 | ||
|
|
f30245bb15 | ||
|
|
bc5df671dd | ||
|
|
a796545da5 | ||
|
|
6e58991986 | ||
|
|
85a6634a56 | ||
|
|
5541ec0763 | ||
|
|
a9aabd0082 | ||
|
|
cbd375f5d0 | ||
|
|
de96894a4d | ||
|
|
5e40fc28c9 | ||
|
|
0b34940e20 | ||
|
|
f93dfe5e78 | ||
|
|
b59042d9e9 | ||
|
|
0c2ed53c54 | ||
|
|
fe474ae9df | ||
|
|
6f0d42a881 | ||
|
|
5e479a5050 | ||
|
|
dfbc618d44 | ||
|
|
9f82a8a6d6 | ||
|
|
476d93b33e | ||
|
|
9895f9f595 | ||
|
|
510cca6b29 | ||
|
|
66d2b7b4d9 | ||
|
|
da76f69e51 | ||
|
|
ed1572d2d9 | ||
|
|
7d0a95e98f | ||
|
|
67834c3f8b | ||
|
|
a5e58ad9ce | ||
|
|
5cb363c389 | ||
|
|
b80c7222ea | ||
|
|
611bd909ef | ||
|
|
db3de2d69e | ||
|
|
7b9fae5605 | ||
|
|
d47bb09b2a | ||
|
|
b2899bda69 | ||
|
|
11652838e2 | ||
|
|
a1dcc1310a | ||
|
|
7e2303a732 | ||
|
|
0d7214a4a6 | ||
|
|
cbd23c7fb1 | ||
|
|
a2b40caeda | ||
|
|
66d57a3d36 | ||
|
|
2288702d26 | ||
|
|
cdbf62a9e5 | ||
|
|
25dc6c4a20 | ||
|
|
af2bdc37ea | ||
|
|
438ef9f348 | ||
|
|
6ac6ef359f | ||
|
|
b07b7f3f26 | ||
|
|
ecefda11c7 | ||
|
|
21f8f56c18 | ||
|
|
e52ab12696 | ||
|
|
b89b883741 | ||
|
|
f694a6d12a | ||
|
|
8abcc5988d | ||
|
|
9d5e43e6a2 | ||
|
|
162852634e | ||
|
|
33c6801501 | ||
|
|
eb679f50f1 | ||
|
|
36fcab17f3 | ||
|
|
b22faa01ea | ||
|
|
9b05a9c334 | ||
|
|
0f39ee9b34 | ||
|
|
9a0088c84e | ||
|
|
c533d48cf5 | ||
|
|
6a3ceb6bc0 | ||
|
|
5ad517ce83 |
14
.backportrc.json
Normal file
14
.backportrc.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"repoOwner": "prowler-cloud",
|
||||
"repoName": "prowler",
|
||||
"targetPRLabels": [
|
||||
"backport"
|
||||
],
|
||||
"sourcePRLabels": [
|
||||
"was-backported"
|
||||
],
|
||||
"copySourcePRLabels": false,
|
||||
"copySourcePRReviewers": true,
|
||||
"prTitle": "{{sourcePullRequest.title}}",
|
||||
"commitConflicts": true
|
||||
}
|
||||
@@ -1,17 +0,0 @@
|
||||
# Ignore git files
|
||||
.git/
|
||||
.github/
|
||||
|
||||
# Ignore Dodckerfile
|
||||
Dockerfile
|
||||
|
||||
# Ignore hidden files
|
||||
.pre-commit-config.yaml
|
||||
.dockerignore
|
||||
.gitignore
|
||||
.pytest*
|
||||
.DS_Store
|
||||
|
||||
# Ignore output directories
|
||||
output/
|
||||
junit-reports/
|
||||
93
.env
Normal file
93
.env
Normal file
@@ -0,0 +1,93 @@
|
||||
#### Important Note ####
|
||||
# This file is used to store environment variables for the Prowler App.
|
||||
# For production, it is recommended to use a secure method to store these variables and change the default secret keys.
|
||||
|
||||
#### Prowler UI Configuration ####
|
||||
PROWLER_UI_VERSION="latest"
|
||||
SITE_URL=http://localhost:3000
|
||||
API_BASE_URL=http://prowler-api:8080/api/v1
|
||||
AUTH_TRUST_HOST=true
|
||||
UI_PORT=3000
|
||||
# openssl rand -base64 32
|
||||
AUTH_SECRET="N/c6mnaS5+SWq81+819OrzQZlmx1Vxtp/orjttJSmw8="
|
||||
|
||||
#### Prowler API Configuration ####
|
||||
PROWLER_API_VERSION="latest"
|
||||
# PostgreSQL settings
|
||||
# If running Django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
POSTGRES_HOST=postgres-db
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_ADMIN_USER=prowler_admin
|
||||
POSTGRES_ADMIN_PASSWORD=postgres
|
||||
POSTGRES_USER=prowler
|
||||
POSTGRES_PASSWORD=postgres
|
||||
POSTGRES_DB=prowler_db
|
||||
|
||||
# Valkey settings
|
||||
# If running Valkey and celery on host, use localhost, else use 'valkey'
|
||||
VALKEY_HOST=valkey
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_DB=0
|
||||
|
||||
# Django settings
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1,prowler-api
|
||||
DJANGO_BIND_ADDRESS=0.0.0.0
|
||||
DJANGO_PORT=8080
|
||||
DJANGO_DEBUG=False
|
||||
DJANGO_SETTINGS_MODULE=config.django.production
|
||||
# Select one of [ndjson|human_readable]
|
||||
DJANGO_LOGGING_FORMATTER=human_readable
|
||||
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
|
||||
# Applies to both Django and Celery Workers
|
||||
DJANGO_LOGGING_LEVEL=INFO
|
||||
# Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_WORKERS=4
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_MANAGE_DB_PARTITIONS=True
|
||||
# openssl genrsa -out private.pem 2048
|
||||
DJANGO_TOKEN_SIGNING_KEY="-----BEGIN PRIVATE KEY-----
|
||||
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDs4e+kt7SnUJek
|
||||
6V5r9zMGzXCoU5qnChfPiqu+BgANyawz+MyVZPs6RCRfeo6tlCknPQtOziyXYM2I
|
||||
7X+qckmuzsjqp8+u+o1mw3VvUuJew5k2SQLPYwsiTzuFNVJEOgRo3hywGiGwS2iv
|
||||
/5nh2QAl7fq2qLqZEXQa5+/xJlQggS1CYxOJgggvLyra50QZlBvPve/AxKJ/EV/Q
|
||||
irWTZU5lLNI8sH2iZR05vQeBsxZ0dCnGMT+vGl+cGkqrvzQzKsYbDmabMcfTYhYi
|
||||
78fpv6A4uharJFHayypYBjE39PwhMyyeycrNXlpm1jpq+03HgmDuDMHydk1tNwuT
|
||||
nEC7m7iNAgMBAAECggEAA2m48nJcJbn9SVi8bclMwKkWmbJErOnyEGEy2sTK3Of+
|
||||
NWx9BB0FmqAPNxn0ss8K7cANKOhDD7ZLF9E2MO4/HgfoMKtUzHRbM7MWvtEepldi
|
||||
nnvcUMEgULD8Dk4HnqiIVjt3BdmGiTv46OpBnRWrkSBV56pUL+7msZmMZTjUZvh2
|
||||
ZWv0+I3gtDIjo2Zo/FiwDV7CfwRjJarRpYUj/0YyuSA4FuOUYl41WAX1I301FKMH
|
||||
xo3jiAYi1s7IneJ16OtPpOA34Wg5F6ebm/UO0uNe+iD4kCXKaZmxYQPh5tfB0Qa3
|
||||
qj1T7GNpFNyvtG7VVdauhkb8iu8X/wl6PCwbg0RCKQKBgQD9HfpnpH0lDlHMRw9K
|
||||
X7Vby/1fSYy1BQtlXFEIPTN/btJ/asGxLmAVwJ2HAPXWlrfSjVAH7CtVmzN7v8oj
|
||||
HeIHfeSgoWEu1syvnv2AMaYSo03UjFFlfc/GUxF7DUScRIhcJUPCP8jkAROz9nFv
|
||||
DByNjUL17Q9r43DmDiRsy0IFqQKBgQDvlJ9Uhl+Sp7gRgKYwa/IG0+I4AduAM+Gz
|
||||
Dxbm52QrMGMTjaJFLmLHBUZ/ot+pge7tZZGws8YR8ufpyMJbMqPjxhIvRRa/p1Tf
|
||||
E3TQPW93FMsHUvxAgY3MV5MzXFPhlNAKb+akP/RcXUhetGAuZKLubtDCWa55ZQuL
|
||||
wj2OS+niRQKBgE7K8zUqNi6/22S8xhy/2GPgB1qPObbsABUofK0U6CAGLo6te+gc
|
||||
6Jo84IyzFtQbDNQFW2Fr+j1m18rw9AqkdcUhQndiZS9AfG07D+zFB86LeWHt4DS4
|
||||
ymIRX8Kvaak/iDcu/n3Mf0vCrhB6aetImObTj4GgrwlFvtJOmrYnO8EpAoGAIXXP
|
||||
Xt25gWD9OyyNiVu6HKwA/zN7NYeJcRmdaDhO7B1A6R0x2Zml4AfjlbXoqOLlvLAf
|
||||
zd79vcoAC82nH1eOPiSOq51plPDI0LMF8IN0CtyTkn1Lj7LIXA6rF1RAvtOqzppc
|
||||
SvpHpZK9pcRpXnFdtBE0BMDDtl6fYzCIqlP94UUCgYEAnhXbAQMF7LQifEm34Dx8
|
||||
BizRMOKcqJGPvbO2+Iyt50O5X6onU2ITzSV1QHtOvAazu+B1aG9pEuBFDQ+ASxEu
|
||||
L9ruJElkOkb/o45TSF6KCsHd55ReTZ8AqnRjf5R+lyzPqTZCXXb8KTcRvWT4zQa3
|
||||
VxyT2PnaSqEcexWUy4+UXoQ=
|
||||
-----END PRIVATE KEY-----"
|
||||
# openssl rsa -in private.pem -pubout -out public.pem
|
||||
DJANGO_TOKEN_VERIFYING_KEY="-----BEGIN PUBLIC KEY-----
|
||||
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA7OHvpLe0p1CXpOlea/cz
|
||||
Bs1wqFOapwoXz4qrvgYADcmsM/jMlWT7OkQkX3qOrZQpJz0LTs4sl2DNiO1/qnJJ
|
||||
rs7I6qfPrvqNZsN1b1LiXsOZNkkCz2MLIk87hTVSRDoEaN4csBohsEtor/+Z4dkA
|
||||
Je36tqi6mRF0Gufv8SZUIIEtQmMTiYIILy8q2udEGZQbz73vwMSifxFf0Iq1k2VO
|
||||
ZSzSPLB9omUdOb0HgbMWdHQpxjE/rxpfnBpKq780MyrGGw5mmzHH02IWIu/H6b+g
|
||||
OLoWqyRR2ssqWAYxN/T8ITMsnsnKzV5aZtY6avtNx4Jg7gzB8nZNbTcLk5xAu5u4
|
||||
jQIDAQAB
|
||||
-----END PUBLIC KEY-----"
|
||||
# openssl rand -base64 32
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY="oE/ltOhp/n1TdbHjVmzcjDPLcLA41CVI/4Rk+UB5ESc="
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
7
.github/CODEOWNERS
vendored
7
.github/CODEOWNERS
vendored
@@ -1 +1,6 @@
|
||||
* @prowler-cloud/prowler-team
|
||||
/* @prowler-cloud/sdk
|
||||
/.github/ @prowler-cloud/sdk
|
||||
prowler @prowler-cloud/sdk @prowler-cloud/detection-and-remediation
|
||||
tests @prowler-cloud/sdk @prowler-cloud/detection-and-remediation
|
||||
api @prowler-cloud/api
|
||||
ui @prowler-cloud/ui
|
||||
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
50
.github/ISSUE_TEMPLATE/bug_report.md
vendored
@@ -1,50 +0,0 @@
|
||||
---
|
||||
name: Bug report
|
||||
about: Create a report to help us improve
|
||||
title: "[Bug]: "
|
||||
labels: bug, status/needs-triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
<!--
|
||||
Please use this template to create your bug report. By providing as much info as possible you help us understand the issue, reproduce it and resolve it for you quicker. Therefore, take a couple of extra minutes to make sure you have provided all info needed.
|
||||
|
||||
PROTIP: record your screen and attach it as a gif to showcase the issue.
|
||||
|
||||
- How to record and attach gif: https://bit.ly/2Mi8T6K
|
||||
-->
|
||||
|
||||
**What happened?**
|
||||
A clear and concise description of what the bug is or what is not working as expected
|
||||
|
||||
|
||||
**How to reproduce it**
|
||||
Steps to reproduce the behavior:
|
||||
1. What command are you running?
|
||||
2. Environment you have, like single account, multi-account, organizations, etc.
|
||||
3. See error
|
||||
|
||||
|
||||
**Expected behavior**
|
||||
A clear and concise description of what you expected to happen.
|
||||
|
||||
|
||||
**Screenshots or Logs**
|
||||
If applicable, add screenshots to help explain your problem.
|
||||
Also, you can add logs (anonymize them first!). Here a command that may help to share a log
|
||||
`bash -x ./prowler -options > debug.log 2>&1` then attach here `debug.log`
|
||||
|
||||
|
||||
**From where are you running Prowler?**
|
||||
Please, complete the following information:
|
||||
- Resource: [e.g. EC2 instance, Fargate task, Docker container manually, EKS, Cloud9, CodeBuild, workstation, etc.)
|
||||
- OS: [e.g. Amazon Linux 2, Mac, Alpine, Windows, etc. ]
|
||||
- AWS-CLI Version [`aws --version`]:
|
||||
- Prowler Version [`./prowler -V`]:
|
||||
- Shell and version:
|
||||
- Others:
|
||||
|
||||
|
||||
**Additional context**
|
||||
Add any other context about the problem here.
|
||||
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
96
.github/ISSUE_TEMPLATE/bug_report.yml
vendored
Normal file
@@ -0,0 +1,96 @@
|
||||
name: 🐞 Bug Report
|
||||
description: Create a report to help us improve
|
||||
labels: ["bug", "status/needs-triage"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: reproduce
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Steps to reproduce the behavior
|
||||
placeholder: |-
|
||||
1. What command are you running?
|
||||
2. Cloud provider you are launching
|
||||
3. Environment you have, like single account, multi-account, organizations, multi or single subscription, etc.
|
||||
4. See error
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Expected behavior
|
||||
description: A clear and concise description of what you expected to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual Result with Screenshots or Logs
|
||||
description: If applicable, add screenshots to help explain your problem. Also, you can add logs (anonymize them first!). Here a command that may help to share a log `prowler <your arguments> --log-level ERROR --log-file $(date +%F)_error.log` then attach here the log file.
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: type
|
||||
attributes:
|
||||
label: How did you install Prowler?
|
||||
options:
|
||||
- Cloning the repository from github.com (git clone)
|
||||
- From pip package (pip install prowler)
|
||||
- From brew (brew install prowler)
|
||||
- Docker (docker pull toniblyx/prowler)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: environment
|
||||
attributes:
|
||||
label: Environment Resource
|
||||
description: From where are you running Prowler?
|
||||
placeholder: |-
|
||||
1. EC2 instance
|
||||
2. Fargate task
|
||||
3. Docker container locally
|
||||
4. EKS
|
||||
5. Cloud9
|
||||
6. CodeBuild
|
||||
7. Workstation
|
||||
8. Other(please specify)
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: os
|
||||
attributes:
|
||||
label: OS used
|
||||
description: Which OS are you using?
|
||||
placeholder: |-
|
||||
1. Amazon Linux 2
|
||||
2. MacOS
|
||||
3. Alpine Linux
|
||||
4. Windows
|
||||
5. Other(please specify)
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: prowler-version
|
||||
attributes:
|
||||
label: Prowler version
|
||||
description: Which Prowler version are you using?
|
||||
placeholder: |-
|
||||
prowler --version
|
||||
validations:
|
||||
required: true
|
||||
- type: input
|
||||
id: pip-version
|
||||
attributes:
|
||||
label: Pip version
|
||||
description: Which pip version are you using?
|
||||
placeholder: |-
|
||||
pip --version
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: additional
|
||||
attributes:
|
||||
description: Additional context
|
||||
label: Context
|
||||
validations:
|
||||
required: false
|
||||
4
.github/ISSUE_TEMPLATE/config.yml
vendored
4
.github/ISSUE_TEMPLATE/config.yml
vendored
@@ -1,5 +1 @@
|
||||
blank_issues_enabled: false
|
||||
contact_links:
|
||||
- name: Questions & Help
|
||||
url: https://github.com/prowler-cloud/prowler/discussions/categories/q-a
|
||||
about: Please ask and answer questions here.
|
||||
|
||||
35
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
35
.github/ISSUE_TEMPLATE/feature-request.yml
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
name: 💡 Feature Request
|
||||
description: Suggest an idea for this project
|
||||
labels: ["feature-request", "status/needs-triage"]
|
||||
|
||||
body:
|
||||
- type: textarea
|
||||
id: Problem
|
||||
attributes:
|
||||
label: New feature motivation
|
||||
description: Is your feature request related to a problem? Please describe
|
||||
placeholder: |-
|
||||
1. A clear and concise description of what the problem is. Ex. I'm always frustrated when
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: Solution
|
||||
attributes:
|
||||
label: Solution Proposed
|
||||
description: A clear and concise description of what you want to happen.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: Alternatives
|
||||
attributes:
|
||||
label: Describe alternatives you've considered
|
||||
description: A clear and concise description of any alternative solutions or features you've considered.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: Context
|
||||
attributes:
|
||||
label: Additional context
|
||||
description: Add any other context or screenshots about the feature request here.
|
||||
validations:
|
||||
required: false
|
||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
@@ -1,20 +0,0 @@
|
||||
---
|
||||
name: Feature request
|
||||
about: Suggest an idea for this project
|
||||
title: ''
|
||||
labels: enhancement, status/needs-triage
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Is your feature request related to a problem? Please describe.**
|
||||
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||
|
||||
**Describe the solution you'd like**
|
||||
A clear and concise description of what you want to happen.
|
||||
|
||||
**Describe alternatives you've considered**
|
||||
A clear and concise description of any alternative solutions or features you've considered.
|
||||
|
||||
**Additional context**
|
||||
Add any other context or screenshots about the feature request here.
|
||||
3
.github/codeql/api-codeql-config.yml
vendored
Normal file
3
.github/codeql/api-codeql-config.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
name: "API - CodeQL Config"
|
||||
paths:
|
||||
- "api/"
|
||||
4
.github/codeql/sdk-codeql-config.yml
vendored
Normal file
4
.github/codeql/sdk-codeql-config.yml
vendored
Normal file
@@ -0,0 +1,4 @@
|
||||
name: "SDK - CodeQL Config"
|
||||
paths-ignore:
|
||||
- "api/"
|
||||
- "ui/"
|
||||
3
.github/codeql/ui-codeql-config.yml
vendored
Normal file
3
.github/codeql/ui-codeql-config.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
name: "UI - CodeQL Config"
|
||||
paths:
|
||||
- "ui/"
|
||||
46
.github/dependabot.yml
vendored
Normal file
46
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,46 @@
|
||||
# To get started with Dependabot version updates, you'll need to specify which
|
||||
# package ecosystems to update and where the package manifests are located.
|
||||
# Please see the documentation for all configuration options:
|
||||
# https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
|
||||
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: master
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
|
||||
- package-ecosystem: "pip"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "pip"
|
||||
- "v3"
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
interval: "daily"
|
||||
open-pull-requests-limit: 10
|
||||
target-branch: v3
|
||||
labels:
|
||||
- "dependencies"
|
||||
- "github_actions"
|
||||
- "v3"
|
||||
89
.github/labeler.yml
vendored
Normal file
89
.github/labeler.yml
vendored
Normal file
@@ -0,0 +1,89 @@
|
||||
documentation:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "docs/**"
|
||||
|
||||
provider/aws:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/aws/**"
|
||||
- any-glob-to-any-file: "tests/providers/aws/**"
|
||||
|
||||
provider/azure:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/azure/**"
|
||||
- any-glob-to-any-file: "tests/providers/azure/**"
|
||||
|
||||
provider/gcp:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/gcp/**"
|
||||
- any-glob-to-any-file: "tests/providers/gcp/**"
|
||||
|
||||
provider/kubernetes:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/kubernetes/**"
|
||||
- any-glob-to-any-file: "tests/providers/kubernetes/**"
|
||||
|
||||
github_actions:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: ".github/workflows/*"
|
||||
|
||||
cli:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "cli/**"
|
||||
|
||||
mutelist:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/aws/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/azure/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/gcp/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "prowler/providers/kubernetes/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/aws/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/azure/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/gcp/lib/mutelist/**"
|
||||
- any-glob-to-any-file: "tests/providers/kubernetes/lib/mutelist/**"
|
||||
|
||||
integration/s3:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/aws/lib/s3/**"
|
||||
- any-glob-to-any-file: "tests/providers/aws/lib/s3/**"
|
||||
|
||||
integration/slack:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/slack/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/slack/**"
|
||||
|
||||
integration/security-hub:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/providers/aws/lib/security_hub/**"
|
||||
- any-glob-to-any-file: "tests/providers/aws/lib/security_hub/**"
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/asff/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/asff/**"
|
||||
|
||||
output/html:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/html/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/html/**"
|
||||
|
||||
output/asff:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/asff/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/asff/**"
|
||||
|
||||
output/ocsf:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/ocsf/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/ocsf/**"
|
||||
|
||||
output/csv:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "prowler/lib/outputs/csv/**"
|
||||
- any-glob-to-any-file: "tests/lib/outputs/csv/**"
|
||||
|
||||
component/api:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "api/**"
|
||||
|
||||
component/ui:
|
||||
- changed-files:
|
||||
- any-glob-to-any-file: "ui/**"
|
||||
12
.github/pull_request_template.md
vendored
12
.github/pull_request_template.md
vendored
@@ -1,13 +1,21 @@
|
||||
### Context
|
||||
### Context
|
||||
|
||||
Please include relevant motivation and context for this PR.
|
||||
|
||||
If fixes an issue please add it with `Fix #XXXX`
|
||||
|
||||
### Description
|
||||
|
||||
Please include a summary of the change and which issue is fixed. List any dependencies that are required for this change.
|
||||
|
||||
### Checklist
|
||||
|
||||
- Are there new checks included in this PR? Yes / No
|
||||
- If so, do we need to update permissions for the provider? Please review this carefully.
|
||||
- [ ] Review if the code is being covered by tests.
|
||||
- [ ] Review if code is being documented following this specification https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings
|
||||
- [ ] Review if backport is needed.
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
63
.github/workflows/api-codeql.yml
vendored
Normal file
63
.github/workflows/api-codeql.yml
vendored
Normal file
@@ -0,0 +1,63 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: API - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "api/**"
|
||||
schedule:
|
||||
- cron: '00 12 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/api-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
156
.github/workflows/api-pull-request.yml
vendored
Normal file
156
.github/workflows/api-pull-request.yml
vendored
Normal file
@@ -0,0 +1,156 @@
|
||||
name: API - Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "api/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
paths:
|
||||
- "api/**"
|
||||
|
||||
|
||||
env:
|
||||
POSTGRES_HOST: localhost
|
||||
POSTGRES_PORT: 5432
|
||||
POSTGRES_ADMIN_USER: prowler
|
||||
POSTGRES_ADMIN_PASSWORD: S3cret
|
||||
POSTGRES_USER: prowler_user
|
||||
POSTGRES_PASSWORD: prowler
|
||||
POSTGRES_DB: postgres-db
|
||||
VALKEY_HOST: localhost
|
||||
VALKEY_PORT: 6379
|
||||
VALKEY_DB: 0
|
||||
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.12"]
|
||||
|
||||
# Service containers to run with `test`
|
||||
services:
|
||||
# Label used to access the service container
|
||||
postgres:
|
||||
image: postgres
|
||||
env:
|
||||
POSTGRES_HOST: ${{ env.POSTGRES_HOST }}
|
||||
POSTGRES_PORT: ${{ env.POSTGRES_PORT }}
|
||||
POSTGRES_USER: ${{ env.POSTGRES_USER }}
|
||||
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
|
||||
POSTGRES_DB: ${{ env.POSTGRES_DB }}
|
||||
# Set health checks to wait until postgres has started
|
||||
ports:
|
||||
- 5432:5432
|
||||
options: >-
|
||||
--health-cmd pg_isready
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
valkey:
|
||||
image: valkey/valkey:7-alpine3.19
|
||||
env:
|
||||
VALKEY_HOST: ${{ env.VALKEY_HOST }}
|
||||
VALKEY_PORT: ${{ env.VALKEY_PORT }}
|
||||
VALKEY_DB: ${{ env.VALKEY_DB }}
|
||||
# Set health checks to wait until postgres has started
|
||||
ports:
|
||||
- 6379:6379
|
||||
options: >-
|
||||
--health-cmd "valkey-cli ping"
|
||||
--health-interval 10s
|
||||
--health-timeout 5s
|
||||
--health-retries 5
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: api/**
|
||||
files_ignore: |
|
||||
api/.github/**
|
||||
api/docs/**
|
||||
api/permissions/**
|
||||
api/README.md
|
||||
api/mkdocs.yml
|
||||
- name: Install poetry
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
- name: Install dependencies
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry install
|
||||
poetry run pip list
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/hadolint/hadolint/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||
) && curl -L -o /tmp/hadolint "https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64" \
|
||||
&& chmod +x /tmp/hadolint
|
||||
|
||||
- name: Poetry check
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry lock --check
|
||||
- name: Lint with ruff
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run ruff check . --exclude contrib
|
||||
- name: Check Format with ruff
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run ruff format --check . --exclude contrib
|
||||
- name: Lint with pylint
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/
|
||||
- name: Bandit
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run bandit -q -lll -x '*_test.py,./contrib/' -r .
|
||||
- name: Safety
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run safety check --ignore 70612,66963
|
||||
- name: Vulture
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run vulture --exclude "contrib,tests,conftest.py" --min-confidence 100 .
|
||||
- name: Hadolint
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
/tmp/hadolint Dockerfile --ignore=DL3013
|
||||
- name: Test with pytest
|
||||
working-directory: ./api
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest --cov=./src/backend --cov-report=xml src/backend
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
42
.github/workflows/backport.yml
vendored
Normal file
42
.github/workflows/backport.yml
vendored
Normal file
@@ -0,0 +1,42 @@
|
||||
name: Prowler - Automatic Backport
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches: ['master']
|
||||
types: ['labeled', 'closed']
|
||||
|
||||
jobs:
|
||||
backport:
|
||||
name: Backport PR
|
||||
if: github.event.pull_request.merged == true && !(contains(github.event.pull_request.labels.*.name, 'backport'))
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
steps:
|
||||
# Workaround not to fail the workflow if the PR does not need a backport
|
||||
# https://github.com/sorenlouv/backport-github-action/issues/127#issuecomment-2258561266
|
||||
- name: Check for backport labels
|
||||
id: check_labels
|
||||
run: |-
|
||||
labels='${{ toJSON(github.event.pull_request.labels.*.name) }}'
|
||||
echo "$labels"
|
||||
matched=$(echo "${labels}" | jq '. | map(select(startswith("backport-to-"))) | length')
|
||||
echo "matched=$matched"
|
||||
echo "matched=$matched" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Backport Action
|
||||
if: fromJSON(steps.check_labels.outputs.matched) > 0
|
||||
uses: sorenlouv/backport-github-action@v9.5.1
|
||||
with:
|
||||
github_token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
auto_backport_label_prefix: backport-to-
|
||||
|
||||
- name: Info log
|
||||
if: ${{ success() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
|
||||
run: cat ~/.backport/backport.info.log
|
||||
|
||||
- name: Debug log
|
||||
if: ${{ failure() && fromJSON(steps.check_labels.outputs.matched) > 0 }}
|
||||
run: cat ~/.backport/backport.debug.log
|
||||
24
.github/workflows/build-documentation-on-pr.yml
vendored
Normal file
24
.github/workflows/build-documentation-on-pr.yml
vendored
Normal file
@@ -0,0 +1,24 @@
|
||||
name: Prowler - Pull Request Documentation Link
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- 'master'
|
||||
- 'v3'
|
||||
paths:
|
||||
- 'docs/**'
|
||||
|
||||
env:
|
||||
PR_NUMBER: ${{ github.event.pull_request.number }}
|
||||
|
||||
jobs:
|
||||
documentation-link:
|
||||
name: Documentation Link
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Leave PR comment with the Prowler Documentation URI
|
||||
uses: peter-evans/create-or-update-comment@v4
|
||||
with:
|
||||
issue-number: ${{ env.PR_NUMBER }}
|
||||
body: |
|
||||
You can check the documentation for this PR here -> [Prowler Documentation](https://prowler-prowler-docs--${{ env.PR_NUMBER }}.com.readthedocs.build/projects/prowler-open-source/en/${{ env.PR_NUMBER }}/)
|
||||
209
.github/workflows/build-lint-push-containers.yml
vendored
209
.github/workflows/build-lint-push-containers.yml
vendored
@@ -1,209 +0,0 @@
|
||||
name: build-lint-push-containers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'master'
|
||||
paths-ignore:
|
||||
- '.github/**'
|
||||
- 'README.md'
|
||||
|
||||
release:
|
||||
types: [published, edited]
|
||||
|
||||
env:
|
||||
AWS_REGION_STG: eu-west-1
|
||||
AWS_REGION_PLATFORM: eu-west-1
|
||||
AWS_REGION_PRO: us-east-1
|
||||
IMAGE_NAME: prowler
|
||||
LATEST_TAG: latest
|
||||
STABLE_TAG: stable
|
||||
TEMPORARY_TAG: temporary
|
||||
DOCKERFILE_PATH: ./Dockerfile
|
||||
|
||||
jobs:
|
||||
# Lint Dockerfile using Hadolint
|
||||
# dockerfile-linter:
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# -
|
||||
# name: Checkout
|
||||
# uses: actions/checkout@v3
|
||||
# -
|
||||
# name: Install Hadolint
|
||||
# run: |
|
||||
# VERSION=$(curl --silent "https://api.github.com/repos/hadolint/hadolint/releases/latest" | \
|
||||
# grep '"tag_name":' | \
|
||||
# sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||
# ) && curl -L -o /tmp/hadolint https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64 \
|
||||
# && chmod +x /tmp/hadolint
|
||||
# -
|
||||
# name: Run Hadolint
|
||||
# run: |
|
||||
# /tmp/hadolint util/Dockerfile
|
||||
|
||||
# Build Prowler OSS container
|
||||
container-build:
|
||||
# needs: dockerfile-linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v2
|
||||
-
|
||||
name: Build
|
||||
uses: docker/build-push-action@v2
|
||||
with:
|
||||
# Without pushing to registries
|
||||
push: false
|
||||
tags: ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }}
|
||||
file: ${{ env.DOCKERFILE_PATH }}
|
||||
outputs: type=docker,dest=/tmp/${{ env.IMAGE_NAME }}.tar
|
||||
-
|
||||
name: Share image between jobs
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: ${{ env.IMAGE_NAME }}.tar
|
||||
path: /tmp/${{ env.IMAGE_NAME }}.tar
|
||||
|
||||
# Lint Prowler OSS container using Dockle
|
||||
# container-linter:
|
||||
# needs: container-build
|
||||
# runs-on: ubuntu-latest
|
||||
# steps:
|
||||
# -
|
||||
# name: Get container image from shared
|
||||
# uses: actions/download-artifact@v2
|
||||
# with:
|
||||
# name: ${{ env.IMAGE_NAME }}.tar
|
||||
# path: /tmp
|
||||
# -
|
||||
# name: Load Docker image
|
||||
# run: |
|
||||
# docker load --input /tmp/${{ env.IMAGE_NAME }}.tar
|
||||
# docker image ls -a
|
||||
# -
|
||||
# name: Install Dockle
|
||||
# run: |
|
||||
# VERSION=$(curl --silent "https://api.github.com/repos/goodwithtech/dockle/releases/latest" | \
|
||||
# grep '"tag_name":' | \
|
||||
# sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||
# ) && curl -L -o dockle.deb https://github.com/goodwithtech/dockle/releases/download/v${VERSION}/dockle_${VERSION}_Linux-64bit.deb \
|
||||
# && sudo dpkg -i dockle.deb && rm dockle.deb
|
||||
# -
|
||||
# name: Run Dockle
|
||||
# run: dockle ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }}
|
||||
|
||||
# Push Prowler OSS container to registries
|
||||
container-push:
|
||||
# needs: container-linter
|
||||
needs: container-build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
contents: read # This is required for actions/checkout
|
||||
steps:
|
||||
-
|
||||
name: Get container image from shared
|
||||
uses: actions/download-artifact@v2
|
||||
with:
|
||||
name: ${{ env.IMAGE_NAME }}.tar
|
||||
path: /tmp
|
||||
-
|
||||
name: Load Docker image
|
||||
run: |
|
||||
docker load --input /tmp/${{ env.IMAGE_NAME }}.tar
|
||||
docker image ls -a
|
||||
-
|
||||
name: Login to DockerHub
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Login to Public ECR
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.PUBLIC_ECR_AWS_SECRET_ACCESS_KEY }}
|
||||
env:
|
||||
AWS_REGION: ${{ env.AWS_REGION_PRO }}
|
||||
-
|
||||
name: Configure AWS Credentials -- STG
|
||||
if: github.event_name == 'push'
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_STG }}
|
||||
role-to-assume: ${{ secrets.STG_IAM_ROLE_ARN }}
|
||||
role-session-name: build-lint-containers-stg
|
||||
-
|
||||
name: Login to ECR -- STG
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.STG_ECR }}
|
||||
-
|
||||
name: Configure AWS Credentials -- PLATFORM
|
||||
if: github.event_name == 'release'
|
||||
uses: aws-actions/configure-aws-credentials@v1
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_PLATFORM }}
|
||||
role-to-assume: ${{ secrets.STG_IAM_ROLE_ARN }}
|
||||
role-session-name: build-lint-containers-pro
|
||||
-
|
||||
name: Login to ECR -- PLATFORM
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/login-action@v2
|
||||
with:
|
||||
registry: ${{ secrets.PLATFORM_ECR }}
|
||||
-
|
||||
# Push to master branch - push "latest" tag
|
||||
name: Tag (latest)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.PLATFORM_ECR }}/${{ secrets.PLATFORM_ECR_REPOSITORY }}:${{ env.LATEST_TAG }}
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
|
||||
-
|
||||
# Push to master branch - push "latest" tag
|
||||
name: Push (latest)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
docker push ${{ secrets.PLATFORM_ECR }}/${{ secrets.PLATFORM_ECR_REPOSITORY }}:${{ env.LATEST_TAG }}
|
||||
docker push ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
|
||||
docker push ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
|
||||
-
|
||||
# Tag the new release (stable and release tag)
|
||||
name: Tag (release)
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.PLATFORM_ECR }}/${{ secrets.PLATFORM_ECR_REPOSITORY }}:${{ github.event.release.tag_name }}
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.PLATFORM_ECR }}/${{ secrets.PLATFORM_ECR_REPOSITORY }}:${{ env.STABLE_TAG }}
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
|
||||
docker tag ${{ env.IMAGE_NAME }}:${{ env.TEMPORARY_TAG }} ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
|
||||
|
||||
-
|
||||
# Push the new release (stable and release tag)
|
||||
name: Push (release)
|
||||
if: github.event_name == 'release'
|
||||
run: |
|
||||
docker push ${{ secrets.PLATFORM_ECR }}/${{ secrets.PLATFORM_ECR_REPOSITORY }}:${{ github.event.release.tag_name }}
|
||||
docker push ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
docker push ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ github.event.release.tag_name }}
|
||||
|
||||
docker push ${{ secrets.PLATFORM_ECR }}/${{ secrets.PLATFORM_ECR_REPOSITORY }}:${{ env.STABLE_TAG }}
|
||||
docker push ${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
|
||||
docker push ${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
|
||||
-
|
||||
name: Delete artifacts
|
||||
if: always()
|
||||
uses: geekyeggo/delete-artifact@v1
|
||||
with:
|
||||
name: ${{ env.IMAGE_NAME }}.tar
|
||||
7
.github/workflows/find-secrets.yml
vendored
7
.github/workflows/find-secrets.yml
vendored
@@ -1,4 +1,4 @@
|
||||
name: find-secrets
|
||||
name: Prowler - Find secrets
|
||||
|
||||
on: pull_request
|
||||
|
||||
@@ -7,12 +7,13 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
- name: TruffleHog OSS
|
||||
uses: trufflesecurity/trufflehog@v3.13.0
|
||||
uses: trufflesecurity/trufflehog@v3.84.1
|
||||
with:
|
||||
path: ./
|
||||
base: ${{ github.event.repository.default_branch }}
|
||||
head: HEAD
|
||||
extra_args: --only-verified
|
||||
|
||||
17
.github/workflows/labeler.yml
vendored
Normal file
17
.github/workflows/labeler.yml
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
name: Prowler - PR Labeler
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
|
||||
jobs:
|
||||
labeler:
|
||||
permissions:
|
||||
contents: read
|
||||
pull-requests: write
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v5
|
||||
41
.github/workflows/pull-request.yml
vendored
41
.github/workflows/pull-request.yml
vendored
@@ -1,41 +0,0 @@
|
||||
name: Lint & Test
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- 'prowler-3.0-dev'
|
||||
pull_request:
|
||||
branches:
|
||||
- 'prowler-3.0-dev'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v3
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install pipenv
|
||||
pipenv install
|
||||
- name: Bandit
|
||||
run: |
|
||||
pipenv run bandit -q -lll -x '*_test.py,./contrib/' -r .
|
||||
- name: Safety
|
||||
run: |
|
||||
pipenv run safety check
|
||||
- name: Vulture
|
||||
run: |
|
||||
pipenv run vulture --exclude "contrib" --min-confidence 100 .
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pipenv run pytest -n auto
|
||||
@@ -1,50 +0,0 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: Refresh regions of AWS services
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * *" #runs at 09:00 UTC everyday
|
||||
|
||||
env:
|
||||
GITHUB_BRANCH: "prowler-3.0-dev"
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v3
|
||||
with:
|
||||
ref: ${{ env.GITHUB_BRANCH }}
|
||||
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.9 #install the python needed
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: python3 util/update_aws_services_regions.py
|
||||
|
||||
# Create pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v4
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
commit-message: "feat(regions_update): Update regions for AWS services."
|
||||
branch: "aws-services-regions-updated"
|
||||
labels: "status/waiting-for-revision, severity/low"
|
||||
title: "feat(regions_update): Changes in regions for AWS services."
|
||||
body: |
|
||||
### Description
|
||||
|
||||
This PR updates the regions for AWS services.
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
182
.github/workflows/sdk-build-lint-push-containers.yml
vendored
Normal file
182
.github/workflows/sdk-build-lint-push-containers.yml
vendored
Normal file
@@ -0,0 +1,182 @@
|
||||
name: SDK - Build and Push containers
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "v3"
|
||||
- "master"
|
||||
paths-ignore:
|
||||
- ".github/**"
|
||||
- "README.md"
|
||||
- "docs/**"
|
||||
- "ui/**"
|
||||
- "api/**"
|
||||
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
# AWS Configuration
|
||||
AWS_REGION_STG: eu-west-1
|
||||
AWS_REGION_PLATFORM: eu-west-1
|
||||
AWS_REGION: us-east-1
|
||||
|
||||
# Container's configuration
|
||||
IMAGE_NAME: prowler
|
||||
DOCKERFILE_PATH: ./Dockerfile
|
||||
|
||||
# Tags
|
||||
LATEST_TAG: latest
|
||||
STABLE_TAG: stable
|
||||
# The RELEASE_TAG is set during runtime in releases
|
||||
RELEASE_TAG: ""
|
||||
# The PROWLER_VERSION and PROWLER_VERSION_MAJOR are set during runtime in releases
|
||||
PROWLER_VERSION: ""
|
||||
PROWLER_VERSION_MAJOR: ""
|
||||
# TEMPORARY_TAG: temporary
|
||||
|
||||
# Python configuration
|
||||
PYTHON_VERSION: 3.12
|
||||
|
||||
# Container Registries
|
||||
PROWLERCLOUD_DOCKERHUB_REPOSITORY: prowlercloud
|
||||
PROWLERCLOUD_DOCKERHUB_IMAGE: prowler
|
||||
|
||||
jobs:
|
||||
# Build Prowler OSS container
|
||||
container-build-push:
|
||||
# needs: dockerfile-linter
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
prowler_version_major: ${{ steps.get-prowler-version.outputs.PROWLER_VERSION_MAJOR }}
|
||||
prowler_version: ${{ steps.get-prowler-version.outputs.PROWLER_VERSION }}
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
|
||||
- name: Install Poetry
|
||||
run: |
|
||||
pipx install poetry
|
||||
pipx inject poetry poetry-bumpversion
|
||||
|
||||
- name: Get Prowler version
|
||||
id: get-prowler-version
|
||||
run: |
|
||||
PROWLER_VERSION="$(poetry version -s 2>/dev/null)"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_ENV}"
|
||||
echo "PROWLER_VERSION=${PROWLER_VERSION}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
# Store prowler version major just for the release
|
||||
PROWLER_VERSION_MAJOR="${PROWLER_VERSION%%.*}"
|
||||
echo "PROWLER_VERSION_MAJOR=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_ENV}"
|
||||
echo "PROWLER_VERSION_MAJOR=${PROWLER_VERSION_MAJOR}" >> "${GITHUB_OUTPUT}"
|
||||
|
||||
case ${PROWLER_VERSION_MAJOR} in
|
||||
3)
|
||||
echo "LATEST_TAG=v3-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v3-stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
|
||||
|
||||
4)
|
||||
echo "LATEST_TAG=v4-latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=v4-stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
|
||||
5)
|
||||
echo "LATEST_TAG=latest" >> "${GITHUB_ENV}"
|
||||
echo "STABLE_TAG=stable" >> "${GITHUB_ENV}"
|
||||
;;
|
||||
|
||||
*)
|
||||
# Fallback if any other version is present
|
||||
echo "Releasing another Prowler major version, aborting..."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- name: Login to DockerHub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Login to Public ECR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: public.ecr.aws
|
||||
username: ${{ secrets.PUBLIC_ECR_AWS_ACCESS_KEY_ID }}
|
||||
password: ${{ secrets.PUBLIC_ECR_AWS_SECRET_ACCESS_KEY }}
|
||||
env:
|
||||
AWS_REGION: ${{ env.AWS_REGION }}
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Build and push container image (latest)
|
||||
if: github.event_name == 'push'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
|
||||
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.LATEST_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.LATEST_TAG }}
|
||||
file: ${{ env.DOCKERFILE_PATH }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
- name: Build and push container image (release)
|
||||
if: github.event_name == 'release'
|
||||
uses: docker/build-push-action@v6
|
||||
with:
|
||||
# Use local context to get changes
|
||||
# https://github.com/docker/build-push-action#path-context
|
||||
context: .
|
||||
push: true
|
||||
tags: |
|
||||
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.PROWLER_VERSION }}
|
||||
${{ secrets.DOCKER_HUB_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
|
||||
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.PROWLER_VERSION }}
|
||||
${{ secrets.PUBLIC_ECR_REPOSITORY }}/${{ env.IMAGE_NAME }}:${{ env.STABLE_TAG }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.PROWLER_VERSION }}
|
||||
${{ env.PROWLERCLOUD_DOCKERHUB_REPOSITORY }}/${{ env.PROWLERCLOUD_DOCKERHUB_IMAGE }}:${{ env.STABLE_TAG }}
|
||||
file: ${{ env.DOCKERFILE_PATH }}
|
||||
cache-from: type=gha
|
||||
cache-to: type=gha,mode=max
|
||||
|
||||
dispatch-action:
|
||||
needs: container-build-push
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Get latest commit info (latest)
|
||||
if: github.event_name == 'push'
|
||||
run: |
|
||||
LATEST_COMMIT_HASH=$(echo ${{ github.event.after }} | cut -b -7)
|
||||
echo "LATEST_COMMIT_HASH=${LATEST_COMMIT_HASH}" >> $GITHUB_ENV
|
||||
|
||||
- name: Dispatch event (latest)
|
||||
if: github.event_name == 'push' && needs.container-build-push.outputs.prowler_version_major == '3'
|
||||
run: |
|
||||
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"v3-latest", "tag": "${{ env.LATEST_COMMIT_HASH }}"}}'
|
||||
|
||||
- name: Dispatch event (release)
|
||||
if: github.event_name == 'release' && needs.container-build-push.outputs.prowler_version_major == '3'
|
||||
run: |
|
||||
curl https://api.github.com/repos/${{ secrets.DISPATCH_OWNER }}/${{ secrets.DISPATCH_REPO }}/dispatches \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}" \
|
||||
-H "X-GitHub-Api-Version: 2022-11-28" \
|
||||
--data '{"event_type":"dispatch","client_payload":{"version":"release", "tag":"${{ needs.container-build-push.outputs.prowler_version }}"}}'
|
||||
65
.github/workflows/sdk-codeql.yml
vendored
Normal file
65
.github/workflows/sdk-codeql.yml
vendored
Normal file
@@ -0,0 +1,65 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: SDK - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths-ignore:
|
||||
- 'ui/**'
|
||||
- 'api/**'
|
||||
schedule:
|
||||
- cron: '00 12 * * *'
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: [ 'python' ]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/sdk-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
100
.github/workflows/sdk-pull-request.yml
vendored
Normal file
100
.github/workflows/sdk-pull-request.yml
vendored
Normal file
@@ -0,0 +1,100 @@
|
||||
name: SDK - Pull Request
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v3"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
python-version: ["3.9", "3.10", "3.11", "3.12"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Test if changes are in not ignored paths
|
||||
id: are-non-ignored-files-changed
|
||||
uses: tj-actions/changed-files@v45
|
||||
with:
|
||||
files: ./**
|
||||
files_ignore: |
|
||||
.github/**
|
||||
docs/**
|
||||
permissions/**
|
||||
api/**
|
||||
ui/**
|
||||
README.md
|
||||
mkdocs.yml
|
||||
.backportrc.json
|
||||
- name: Install poetry
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pipx install poetry
|
||||
- name: Set up Python ${{ matrix.python-version }}
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ matrix.python-version }}
|
||||
cache: "poetry"
|
||||
- name: Install dependencies
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry install
|
||||
poetry run pip list
|
||||
VERSION=$(curl --silent "https://api.github.com/repos/hadolint/hadolint/releases/latest" | \
|
||||
grep '"tag_name":' | \
|
||||
sed -E 's/.*"v([^"]+)".*/\1/' \
|
||||
) && curl -L -o /tmp/hadolint "https://github.com/hadolint/hadolint/releases/download/v${VERSION}/hadolint-Linux-x86_64" \
|
||||
&& chmod +x /tmp/hadolint
|
||||
- name: Poetry check
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry lock --check
|
||||
- name: Lint with flake8
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib,ui,api
|
||||
- name: Checking format with black
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run black --exclude api ui --check .
|
||||
- name: Lint with pylint
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pylint --disable=W,C,R,E -j 0 -rn -sn prowler/
|
||||
- name: Bandit
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run bandit -q -lll -x '*_test.py,./contrib/,./api/,./ui' -r .
|
||||
- name: Safety
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run safety check --ignore 70612 -r pyproject.toml
|
||||
- name: Vulture
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run vulture --exclude "contrib,api,ui" --min-confidence 100 .
|
||||
- name: Hadolint
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
/tmp/hadolint Dockerfile --ignore=DL3013
|
||||
- name: Test with pytest
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
run: |
|
||||
poetry run pytest -n auto --cov=./prowler --cov-report=xml tests
|
||||
- name: Upload coverage reports to Codecov
|
||||
if: steps.are-non-ignored-files-changed.outputs.any_changed == 'true'
|
||||
uses: codecov/codecov-action@v5
|
||||
env:
|
||||
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
|
||||
70
.github/workflows/sdk-pypi-release.yml
vendored
Normal file
70
.github/workflows/sdk-pypi-release.yml
vendored
Normal file
@@ -0,0 +1,70 @@
|
||||
name: SDK - PyPI release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
|
||||
env:
|
||||
RELEASE_TAG: ${{ github.event.release.tag_name }}
|
||||
PYTHON_VERSION: 3.11
|
||||
CACHE: "poetry"
|
||||
|
||||
jobs:
|
||||
release-prowler-job:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
POETRY_VIRTUALENVS_CREATE: "false"
|
||||
name: Release Prowler to PyPI
|
||||
steps:
|
||||
- name: Get Prowler version
|
||||
run: |
|
||||
PROWLER_VERSION="${{ env.RELEASE_TAG }}"
|
||||
|
||||
case ${PROWLER_VERSION%%.*} in
|
||||
3)
|
||||
echo "Releasing Prowler v3 with tag ${PROWLER_VERSION}"
|
||||
;;
|
||||
4)
|
||||
echo "Releasing Prowler v4 with tag ${PROWLER_VERSION}"
|
||||
;;
|
||||
5)
|
||||
echo "Releasing Prowler v5 with tag ${PROWLER_VERSION}"
|
||||
;;
|
||||
*)
|
||||
echo "Releasing another Prowler major version, aborting..."
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
pipx install poetry
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: ${{ env.PYTHON_VERSION }}
|
||||
cache: ${{ env.CACHE }}
|
||||
|
||||
- name: Build Prowler package
|
||||
run: |
|
||||
poetry build
|
||||
|
||||
- name: Publish Prowler package to PyPI
|
||||
run: |
|
||||
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
|
||||
poetry publish
|
||||
|
||||
- name: Replicate PyPI package
|
||||
run: |
|
||||
rm -rf ./dist && rm -rf ./build && rm -rf prowler.egg-info
|
||||
pip install toml
|
||||
python util/replicate_pypi_package.py
|
||||
poetry build
|
||||
|
||||
- name: Publish prowler-cloud package to PyPI
|
||||
run: |
|
||||
poetry config pypi-token.pypi ${{ secrets.PYPI_API_TOKEN }}
|
||||
poetry publish
|
||||
67
.github/workflows/sdk-refresh-aws-services-regions.yml
vendored
Normal file
67
.github/workflows/sdk-refresh-aws-services-regions.yml
vendored
Normal file
@@ -0,0 +1,67 @@
|
||||
# This is a basic workflow to help you get started with Actions
|
||||
|
||||
name: SDK - Refresh AWS services' regions
|
||||
|
||||
on:
|
||||
schedule:
|
||||
- cron: "0 9 * * *" #runs at 09:00 UTC everyday
|
||||
|
||||
env:
|
||||
GITHUB_BRANCH: "master"
|
||||
AWS_REGION_DEV: us-east-1
|
||||
|
||||
# A workflow run is made up of one or more jobs that can run sequentially or in parallel
|
||||
jobs:
|
||||
# This workflow contains a single job called "build"
|
||||
build:
|
||||
# The type of runner that the job will run on
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
id-token: write
|
||||
pull-requests: write
|
||||
contents: write
|
||||
# Steps represent a sequence of tasks that will be executed as part of the job
|
||||
steps:
|
||||
# Checks-out your repository under $GITHUB_WORKSPACE, so your job can access it
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
ref: ${{ env.GITHUB_BRANCH }}
|
||||
|
||||
- name: setup python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: 3.9 #install the python needed
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install boto3
|
||||
|
||||
- name: Configure AWS Credentials -- DEV
|
||||
uses: aws-actions/configure-aws-credentials@v4
|
||||
with:
|
||||
aws-region: ${{ env.AWS_REGION_DEV }}
|
||||
role-to-assume: ${{ secrets.DEV_IAM_ROLE_ARN }}
|
||||
role-session-name: refresh-AWS-regions-dev
|
||||
|
||||
# Runs a single command using the runners shell
|
||||
- name: Run a one-line script
|
||||
run: python3 util/update_aws_services_regions.py
|
||||
|
||||
# Create pull request
|
||||
- name: Create Pull Request
|
||||
uses: peter-evans/create-pull-request@v7
|
||||
with:
|
||||
token: ${{ secrets.PROWLER_BOT_ACCESS_TOKEN }}
|
||||
commit-message: "feat(regions_update): Update regions for AWS services"
|
||||
branch: "aws-services-regions-updated-${{ github.sha }}"
|
||||
labels: "status/waiting-for-revision, severity/low, provider/aws, backport-to-v3"
|
||||
title: "chore(regions_update): Changes in regions for AWS services"
|
||||
body: |
|
||||
### Description
|
||||
|
||||
This PR updates the regions for AWS services.
|
||||
|
||||
### License
|
||||
|
||||
By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.
|
||||
61
.github/workflows/ui-codeql.yml
vendored
Normal file
61
.github/workflows/ui-codeql.yml
vendored
Normal file
@@ -0,0 +1,61 @@
|
||||
# For most projects, this workflow file will not need changing; you simply need
|
||||
# to commit it to your repository.
|
||||
#
|
||||
# You may wish to alter this file to override the set of languages analyzed,
|
||||
# or to provide custom queries or build logic.
|
||||
#
|
||||
# ******** NOTE ********
|
||||
# We have attempted to detect the languages in your repository. Please check
|
||||
# the `language` matrix defined below to confirm you have the correct set of
|
||||
# supported CodeQL languages.
|
||||
#
|
||||
name: UI - CodeQL
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
pull_request:
|
||||
branches:
|
||||
- "master"
|
||||
- "v4.*"
|
||||
- "v5.*"
|
||||
paths:
|
||||
- "ui/**"
|
||||
schedule:
|
||||
- cron: "00 12 * * *"
|
||||
|
||||
jobs:
|
||||
analyze:
|
||||
name: Analyze
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
actions: read
|
||||
contents: read
|
||||
security-events: write
|
||||
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
language: ["javascript"]
|
||||
# Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
# Initializes the CodeQL tools for scanning.
|
||||
- name: Initialize CodeQL
|
||||
uses: github/codeql-action/init@v3
|
||||
with:
|
||||
languages: ${{ matrix.language }}
|
||||
config-file: ./.github/codeql/ui-codeql-config.yml
|
||||
|
||||
- name: Perform CodeQL Analysis
|
||||
uses: github/codeql-action/analyze@v3
|
||||
with:
|
||||
category: "/language:${{matrix.language}}"
|
||||
34
.github/workflows/ui-pull-request.yml
vendored
Normal file
34
.github/workflows/ui-pull-request.yml
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
name: UI - Pull Request
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- master
|
||||
paths:
|
||||
- 'ui/**'
|
||||
|
||||
jobs:
|
||||
test-and-coverage:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
node-version: [20.x]
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
persist-credentials: false
|
||||
- name: Setup Node.js ${{ matrix.node-version }}
|
||||
uses: actions/setup-node@v3
|
||||
with:
|
||||
node-version: ${{ matrix.node-version }}
|
||||
- name: Install dependencies
|
||||
working-directory: ./ui
|
||||
run: npm install
|
||||
- name: Run Healthcheck
|
||||
working-directory: ./ui
|
||||
run: npm run healthcheck
|
||||
- name: Build the application
|
||||
working-directory: ./ui
|
||||
run: npm run build
|
||||
28
.gitignore
vendored
28
.gitignore
vendored
@@ -5,6 +5,15 @@
|
||||
[._]ss[a-gi-z]
|
||||
[._]sw[a-p]
|
||||
|
||||
# Python code
|
||||
__pycache__
|
||||
venv/
|
||||
build/
|
||||
/dist/
|
||||
*.egg-info/
|
||||
*/__pycache__/*.pyc
|
||||
.idea/
|
||||
|
||||
# Session
|
||||
Session.vim
|
||||
Sessionx.vim
|
||||
@@ -33,8 +42,21 @@ junit-reports/
|
||||
# VSCode files
|
||||
.vscode/
|
||||
|
||||
terraform-kickstarter/.terraform.lock.hcl
|
||||
# Terraform
|
||||
.terraform*
|
||||
*.tfstate
|
||||
|
||||
terraform-kickstarter/.terraform/providers/registry.terraform.io/hashicorp/aws/3.56.0/darwin_amd64/terraform-provider-aws_v3.56.0_x5
|
||||
# .env
|
||||
ui/.env*
|
||||
api/.env*
|
||||
|
||||
terraform-kickstarter/terraform.tfstate
|
||||
# Coverage
|
||||
.coverage*
|
||||
.coverage
|
||||
coverage*
|
||||
|
||||
# Node
|
||||
node_modules
|
||||
|
||||
# Persistent data
|
||||
_data/
|
||||
|
||||
@@ -1,29 +1,109 @@
|
||||
repos:
|
||||
## GENERAL
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v3.3.0
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
args: ['--unsafe']
|
||||
args: ["--unsafe"]
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
exclude: 'README.md'
|
||||
- id: no-commit-to-branch
|
||||
- id: pretty-format-json
|
||||
args: ['--autofix']
|
||||
args: ["--autofix", --no-sort-keys, --no-ensure-ascii]
|
||||
|
||||
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||
rev: v0.8.0
|
||||
## TOML
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.13.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
- id: pretty-format-toml
|
||||
args: [--autofix]
|
||||
files: pyproject.toml
|
||||
|
||||
## BASH
|
||||
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||
rev: v0.10.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
exclude: contrib
|
||||
## PYTHON
|
||||
- repo: https://github.com/myint/autoflake
|
||||
rev: v2.3.1
|
||||
hooks:
|
||||
- id: autoflake
|
||||
args:
|
||||
[
|
||||
"--in-place",
|
||||
"--remove-all-unused-imports",
|
||||
"--remove-unused-variable",
|
||||
]
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black"]
|
||||
|
||||
- repo: https://github.com/psf/black
|
||||
rev: 24.4.2
|
||||
hooks:
|
||||
- id: black
|
||||
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
exclude: contrib
|
||||
args: ["--ignore=E266,W503,E203,E501,W605"]
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 1.8.0
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
- id: poetry-lock
|
||||
args: ["--no-update"]
|
||||
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.10.0
|
||||
rev: v2.13.0-beta
|
||||
hooks:
|
||||
- id: hadolint
|
||||
name: Lint Dockerfiles
|
||||
description: Runs hadolint to lint Dockerfiles
|
||||
args: ["--ignore=DL3013"]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: bash -c 'pylint --disable=W,C,R,E -j 0 -rn -sn prowler/'
|
||||
language: system
|
||||
types: ["dockerfile"]
|
||||
entry: hadolint
|
||||
files: '.*\.py'
|
||||
|
||||
- id: trufflehog
|
||||
name: TruffleHog
|
||||
description: Detect secrets in your data.
|
||||
entry: bash -c 'trufflehog --no-update git file://. --only-verified --fail'
|
||||
# For running trufflehog in docker, use the following entry instead:
|
||||
# entry: bash -c 'docker run -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --only-verified --fail'
|
||||
language: system
|
||||
stages: ["pre-commit", "pre-push"]
|
||||
|
||||
- id: bandit
|
||||
name: bandit
|
||||
description: "Bandit is a tool for finding common security issues in Python code"
|
||||
entry: bash -c 'bandit -q -lll -x '*_test.py,./contrib/' -r .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
- id: safety
|
||||
name: safety
|
||||
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
|
||||
entry: bash -c 'safety check --ignore 70612,66963'
|
||||
language: system
|
||||
|
||||
- id: vulture
|
||||
name: vulture
|
||||
description: "Vulture finds unused code in Python programs."
|
||||
entry: bash -c 'vulture --exclude "contrib" --min-confidence 100 .'
|
||||
exclude: 'api/src/backend/'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
25
.readthedocs.yaml
Normal file
25
.readthedocs.yaml
Normal file
@@ -0,0 +1,25 @@
|
||||
# .readthedocs.yaml
|
||||
# Read the Docs configuration file
|
||||
# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details
|
||||
|
||||
# Required
|
||||
version: 2
|
||||
|
||||
build:
|
||||
os: "ubuntu-22.04"
|
||||
tools:
|
||||
python: "3.11"
|
||||
jobs:
|
||||
post_create_environment:
|
||||
# Install poetry
|
||||
# https://python-poetry.org/docs/#installing-manually
|
||||
- python -m pip install poetry
|
||||
post_install:
|
||||
# Install dependencies with 'docs' dependency group
|
||||
# https://python-poetry.org/docs/managing-dependencies/#dependency-groups
|
||||
# VIRTUAL_ENV needs to be set manually for now.
|
||||
# See https://github.com/readthedocs/readthedocs.org/pull/11152/
|
||||
- VIRTUAL_ENV=${READTHEDOCS_VIRTUALENV_PATH} python -m poetry install --only=docs
|
||||
|
||||
mkdocs:
|
||||
configuration: mkdocs.yml
|
||||
@@ -55,7 +55,7 @@ further defined and clarified by project maintainers.
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be
|
||||
reported by contacting the project team at community@prowler.cloud. All
|
||||
reported by contacting the project team at [support.prowler.com](https://customer.support.prowler.com/servicedesk/customer/portals). All
|
||||
complaints will be reviewed and investigated and will result in a response that
|
||||
is deemed necessary and appropriate to the circumstances. The project team is
|
||||
obligated to maintain confidentiality with regard to the reporter of an incident.
|
||||
|
||||
13
CONTRIBUTING.md
Normal file
13
CONTRIBUTING.md
Normal file
@@ -0,0 +1,13 @@
|
||||
# Do you want to learn on how to...
|
||||
|
||||
- Contribute with your code or fixes to Prowler
|
||||
- Create a new check for a provider
|
||||
- Create a new security compliance framework
|
||||
- Add a custom output format
|
||||
- Add a new integration
|
||||
- Contribute with documentation
|
||||
|
||||
Want some swag as appreciation for your contribution?
|
||||
|
||||
# Prowler Developer Guide
|
||||
https://docs.prowler.com/projects/prowler-open-source/en/latest/developer-guide/introduction/
|
||||
82
Dockerfile
82
Dockerfile
@@ -1,64 +1,38 @@
|
||||
# Build command
|
||||
# docker build --platform=linux/amd64 --no-cache -t prowler:latest -f ./Dockerfile .
|
||||
|
||||
# hadolint ignore=DL3007
|
||||
FROM public.ecr.aws/amazonlinux/amazonlinux:latest
|
||||
FROM python:3.12.8-alpine3.20
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/prowler"
|
||||
|
||||
ARG USERNAME=prowler
|
||||
ARG USERID=34000
|
||||
|
||||
# Prepare image as root
|
||||
USER 0
|
||||
# System dependencies
|
||||
# hadolint ignore=DL3006,DL3013,DL3033
|
||||
RUN yum upgrade -y && \
|
||||
yum install -y python3 bash curl jq coreutils py3-pip which unzip shadow-utils && \
|
||||
yum clean all && \
|
||||
rm -rf /var/cache/yum
|
||||
|
||||
RUN amazon-linux-extras install -y epel postgresql14 && \
|
||||
yum clean all && \
|
||||
rm -rf /var/cache/yum
|
||||
# Update system dependencies and install essential tools
|
||||
#hadolint ignore=DL3018
|
||||
RUN apk --no-cache upgrade && apk --no-cache add curl git
|
||||
|
||||
# Create non-root user
|
||||
RUN useradd -l -s /bin/bash -U -u ${USERID} ${USERNAME}
|
||||
RUN mkdir -p /home/prowler && \
|
||||
echo 'prowler:x:1000:1000:prowler:/home/prowler:' > /etc/passwd && \
|
||||
echo 'prowler:x:1000:' > /etc/group && \
|
||||
chown -R prowler:prowler /home/prowler
|
||||
USER prowler
|
||||
|
||||
USER ${USERNAME}
|
||||
# Copy necessary files
|
||||
WORKDIR /home/prowler
|
||||
COPY prowler/ /home/prowler/prowler/
|
||||
COPY dashboard/ /home/prowler/dashboard/
|
||||
COPY pyproject.toml /home/prowler
|
||||
COPY README.md /home/prowler
|
||||
|
||||
# Python dependencies
|
||||
# hadolint ignore=DL3006,DL3013,DL3042
|
||||
RUN pip3 install --upgrade pip && \
|
||||
pip3 install --no-cache-dir boto3 detect-secrets==1.0.3 && \
|
||||
pip3 cache purge
|
||||
# Set Python PATH
|
||||
ENV PATH="/home/${USERNAME}/.local/bin:${PATH}"
|
||||
# Install Python dependencies
|
||||
ENV HOME='/home/prowler'
|
||||
ENV PATH="$HOME/.local/bin:$PATH"
|
||||
RUN pip install --no-cache-dir --upgrade pip setuptools wheel && \
|
||||
pip install --no-cache-dir .
|
||||
|
||||
# Remove deprecated dash dependencies
|
||||
RUN pip uninstall dash-html-components -y && \
|
||||
pip uninstall dash-core-components -y
|
||||
|
||||
# Remove Prowler directory and build files
|
||||
USER 0
|
||||
RUN rm -rf /home/prowler/prowler /home/prowler/pyproject.toml /home/prowler/README.md /home/prowler/build /home/prowler/prowler.egg-info
|
||||
|
||||
# Install AWS CLI
|
||||
RUN curl https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip -o awscliv2.zip && \
|
||||
unzip -q awscliv2.zip && \
|
||||
aws/install && \
|
||||
rm -rf aws awscliv2.zip
|
||||
|
||||
# Keep Python2 for yum
|
||||
RUN sed -i '1 s/python/python2.7/' /usr/bin/yum
|
||||
|
||||
# Set Python3
|
||||
RUN rm /usr/bin/python && \
|
||||
ln -s /usr/bin/python3 /usr/bin/python
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /prowler
|
||||
|
||||
# Copy all files
|
||||
COPY . ./
|
||||
|
||||
# Set files ownership
|
||||
RUN chown -R prowler .
|
||||
|
||||
USER ${USERNAME}
|
||||
|
||||
ENTRYPOINT ["./prowler"]
|
||||
USER prowler
|
||||
ENTRYPOINT ["prowler"]
|
||||
|
||||
4
LICENSE
4
LICENSE
@@ -186,7 +186,7 @@
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2018 Netflix, Inc.
|
||||
Copyright @ 2024 Toni de la Fuente
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
@@ -198,4 +198,4 @@ Copyright 2018 Netflix, Inc.
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
```
|
||||
./prowler -l # to see all available checks and their groups.
|
||||
./prowler -L # to see all available groups only.
|
||||
./prowler -l -g groupname # to see checks in a particular group
|
||||
```
|
||||
47
Makefile
Normal file
47
Makefile
Normal file
@@ -0,0 +1,47 @@
|
||||
.DEFAULT_GOAL:=help
|
||||
|
||||
##@ Testing
|
||||
test: ## Test with pytest
|
||||
rm -rf .coverage && \
|
||||
pytest -n auto -vvv -s --cov=./prowler --cov-report=xml tests
|
||||
|
||||
coverage: ## Show Test Coverage
|
||||
coverage run --skip-covered -m pytest -v && \
|
||||
coverage report -m && \
|
||||
rm -rf .coverage && \
|
||||
coverage report -m
|
||||
|
||||
coverage-html: ## Show Test Coverage
|
||||
rm -rf ./htmlcov && \
|
||||
coverage html && \
|
||||
open htmlcov/index.html
|
||||
|
||||
##@ Linting
|
||||
format: ## Format Code
|
||||
@echo "Running black..."
|
||||
black .
|
||||
|
||||
lint: ## Lint Code
|
||||
@echo "Running flake8..."
|
||||
flake8 . --ignore=E266,W503,E203,E501,W605,E128 --exclude contrib
|
||||
@echo "Running black... "
|
||||
black --check .
|
||||
@echo "Running pylint..."
|
||||
pylint --disable=W,C,R,E -j 0 prowler util
|
||||
|
||||
##@ PyPI
|
||||
pypi-clean: ## Delete the distribution files
|
||||
rm -rf ./dist && rm -rf ./build && rm -rf prowler.egg-info
|
||||
|
||||
pypi-build: ## Build package
|
||||
$(MAKE) pypi-clean && \
|
||||
poetry build
|
||||
|
||||
pypi-upload: ## Upload package
|
||||
python3 -m twine upload --repository pypi dist/*
|
||||
|
||||
|
||||
##@ Help
|
||||
help: ## Show this help.
|
||||
@echo "Prowler Makefile"
|
||||
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z_-]+:.*?##/ { printf " \033[36m%-15s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||
13
Pipfile
13
Pipfile
@@ -1,13 +0,0 @@
|
||||
[[source]]
|
||||
name = "pypi"
|
||||
url = "https://pypi.org/simple"
|
||||
verify_ssl = true
|
||||
|
||||
[dev-packages]
|
||||
|
||||
[packages]
|
||||
boto3 = ">=1.9.188"
|
||||
detect-secrets = "==1.0.3"
|
||||
|
||||
[requires]
|
||||
python_version = "3.7"
|
||||
23
SECURITY.md
Normal file
23
SECURITY.md
Normal file
@@ -0,0 +1,23 @@
|
||||
# Security Policy
|
||||
|
||||
## Software Security
|
||||
As an **AWS Partner** and we have passed the [AWS Foundation Technical Review (FTR)](https://aws.amazon.com/partners/foundational-technical-review/) and we use the following tools and automation to make sure our code is secure and dependencies up-to-dated:
|
||||
|
||||
- `bandit` for code security review.
|
||||
- `safety` and `dependabot` for dependencies.
|
||||
- `hadolint` and `dockle` for our containers security.
|
||||
- `snyk` in Docker Hub.
|
||||
- `clair` in Amazon ECR.
|
||||
- `vulture`, `flake8`, `black` and `pylint` for formatting and best practices.
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
If you would like to report a vulnerability or have a security concern regarding Prowler Open Source or ProwlerPro service, please submit the information by contacting to https://support.prowler.com.
|
||||
|
||||
The information you share with ProwlerPro as part of this process is kept confidential within ProwlerPro. We will only share this information with a third party if the vulnerability you report is found to affect a third-party product, in which case we will share this information with the third-party product's author or manufacturer. Otherwise, we will only share this information as permitted by you.
|
||||
|
||||
We will review the submitted report, and assign it a tracking number. We will then respond to you, acknowledging receipt of the report, and outline the next steps in the process.
|
||||
|
||||
You will receive a non-automated response to your initial contact within 24 hours, confirming receipt of your reported vulnerability.
|
||||
|
||||
We will coordinate public notification of any validated vulnerability with you. Where possible, we prefer that our respective public disclosures be posted simultaneously.
|
||||
@@ -1,29 +0,0 @@
|
||||
# Each line is a (checkid:item) tuple
|
||||
|
||||
# Example: Will not consider a myignoredbucket failures as full failure. (Still printed as a warning)
|
||||
check26:myignoredbucket
|
||||
|
||||
# Note that by default, this searches for the string appearing *anywhere* in the resource name.
|
||||
# For example:
|
||||
# extra718:ci-logs # Will block bucket "ci-logs" AND ALSO bucket "ci-logs-replica"
|
||||
# extra718:logs # Will block EVERY BUCKET containing the string "logs"
|
||||
|
||||
# line starting with # are ignored as comments
|
||||
# add a line per resource as here:
|
||||
#<checkid1>:<resource to ignore 1>
|
||||
#<checkid1>:<resource to ignore 2>
|
||||
# checkid2
|
||||
#<checkid2>:<resource to ignore 1>
|
||||
|
||||
# REGEXES
|
||||
# This allowlist works with regexes (ERE, the same style of regex as grep -E and bash's =~ use)
|
||||
# therefore:
|
||||
# extra718:[[:alnum:]]+-logs # will ignore all buckets containing the terms ci-logs, qa-logs, etc.
|
||||
|
||||
# EXAMPLE: CONTROL TOWER
|
||||
# When using Control Tower, guardrails prevent access to certain protected resources. The allowlist
|
||||
# below ensures that warnings instead of errors are reported for the affected resources.
|
||||
#extra734:aws-controltower-logs-[[:digit:]]+-[[:alpha:]\-]+
|
||||
#extra734:aws-controltower-s3-access-logs-[[:digit:]]+-[[:alpha:]\-]+
|
||||
#extra764:aws-controltower-logs-[[:digit:]]+-[[:alpha:]\-]+
|
||||
#extra764:aws-controltower-s3-access-logs-[[:digit:]]+-[[:alpha:]\-]+
|
||||
41
api/.env.example
Normal file
41
api/.env.example
Normal file
@@ -0,0 +1,41 @@
|
||||
# Django settings
|
||||
DJANGO_ALLOWED_HOSTS=localhost,127.0.0.1
|
||||
DJANGO_BIND_ADDRESS=0.0.0.0
|
||||
DJANGO_PORT=8000
|
||||
DJANGO_DEBUG=False
|
||||
# Select one of [production|devel]
|
||||
DJANGO_SETTINGS_MODULE=config.django.[production|devel]
|
||||
# Select one of [ndjson|human_readable]
|
||||
DJANGO_LOGGING_FORMATTER=[ndjson|human_readable]
|
||||
# Select one of [DEBUG|INFO|WARNING|ERROR|CRITICAL]
|
||||
# Applies to both Django and Celery Workers
|
||||
DJANGO_LOGGING_LEVEL=INFO
|
||||
DJANGO_WORKERS=4 # Defaults to the maximum available based on CPU cores if not set.
|
||||
DJANGO_TOKEN_SIGNING_KEY=""
|
||||
DJANGO_TOKEN_VERIFYING_KEY=""
|
||||
# Token lifetime is in minutes
|
||||
DJANGO_ACCESS_TOKEN_LIFETIME=30
|
||||
DJANGO_REFRESH_TOKEN_LIFETIME=1440
|
||||
DJANGO_CACHE_MAX_AGE=3600
|
||||
DJANGO_STALE_WHILE_REVALIDATE=60
|
||||
DJANGO_SECRETS_ENCRYPTION_KEY=""
|
||||
# Decide whether to allow Django manage database table partitions
|
||||
DJANGO_MANAGE_DB_PARTITIONS=[True|False]
|
||||
DJANGO_CELERY_DEADLOCK_ATTEMPTS=5
|
||||
DJANGO_BROKER_VISIBILITY_TIMEOUT=86400
|
||||
|
||||
# PostgreSQL settings
|
||||
# If running django and celery on host, use 'localhost', else use 'postgres-db'
|
||||
POSTGRES_HOST=[localhost|postgres-db]
|
||||
POSTGRES_PORT=5432
|
||||
POSTGRES_ADMIN_USER=prowler
|
||||
POSTGRES_ADMIN_PASSWORD=S3cret
|
||||
POSTGRES_USER=prowler_user
|
||||
POSTGRES_PASSWORD=S3cret
|
||||
POSTGRES_DB=prowler_db
|
||||
|
||||
# Valkey settings
|
||||
# If running django and celery on host, use localhost, else use 'valkey'
|
||||
VALKEY_HOST=[localhost|valkey]
|
||||
VALKEY_PORT=6379
|
||||
VALKEY_DB=0
|
||||
168
api/.gitignore
vendored
Normal file
168
api/.gitignore
vendored
Normal file
@@ -0,0 +1,168 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.pyc
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
/_data/
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
*.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
.idea/
|
||||
|
||||
# VSCode
|
||||
.vscode/
|
||||
91
api/.pre-commit-config.yaml
Normal file
91
api/.pre-commit-config.yaml
Normal file
@@ -0,0 +1,91 @@
|
||||
repos:
|
||||
## GENERAL
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
rev: v4.6.0
|
||||
hooks:
|
||||
- id: check-merge-conflict
|
||||
- id: check-yaml
|
||||
args: ["--unsafe"]
|
||||
- id: check-json
|
||||
- id: end-of-file-fixer
|
||||
- id: trailing-whitespace
|
||||
- id: no-commit-to-branch
|
||||
- id: pretty-format-json
|
||||
args: ["--autofix", "--no-sort-keys", "--no-ensure-ascii"]
|
||||
exclude: 'src/backend/api/fixtures/dev/.*\.json$'
|
||||
|
||||
## TOML
|
||||
- repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks
|
||||
rev: v2.13.0
|
||||
hooks:
|
||||
- id: pretty-format-toml
|
||||
args: [--autofix]
|
||||
files: pyproject.toml
|
||||
|
||||
## BASH
|
||||
- repo: https://github.com/koalaman/shellcheck-precommit
|
||||
rev: v0.10.0
|
||||
hooks:
|
||||
- id: shellcheck
|
||||
exclude: contrib
|
||||
## PYTHON
|
||||
- repo: https://github.com/astral-sh/ruff-pre-commit
|
||||
# Ruff version.
|
||||
rev: v0.5.0
|
||||
hooks:
|
||||
# Run the linter.
|
||||
- id: ruff
|
||||
args: [ --fix ]
|
||||
# Run the formatter.
|
||||
- id: ruff-format
|
||||
|
||||
- repo: https://github.com/python-poetry/poetry
|
||||
rev: 1.8.0
|
||||
hooks:
|
||||
- id: poetry-check
|
||||
args: ["--directory=src"]
|
||||
- id: poetry-lock
|
||||
args: ["--no-update", "--directory=src"]
|
||||
|
||||
- repo: https://github.com/hadolint/hadolint
|
||||
rev: v2.13.0-beta
|
||||
hooks:
|
||||
- id: hadolint
|
||||
args: ["--ignore=DL3013", "Dockerfile"]
|
||||
|
||||
- repo: local
|
||||
hooks:
|
||||
- id: pylint
|
||||
name: pylint
|
||||
entry: bash -c 'poetry run pylint --disable=W,C,R,E -j 0 -rn -sn src/'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
- id: trufflehog
|
||||
name: TruffleHog
|
||||
description: Detect secrets in your data.
|
||||
entry: bash -c 'trufflehog --no-update git file://. --only-verified --fail'
|
||||
# For running trufflehog in docker, use the following entry instead:
|
||||
# entry: bash -c 'docker run -v "$(pwd):/workdir" -i --rm trufflesecurity/trufflehog:latest git file:///workdir --only-verified --fail'
|
||||
language: system
|
||||
stages: ["commit", "push"]
|
||||
|
||||
- id: bandit
|
||||
name: bandit
|
||||
description: "Bandit is a tool for finding common security issues in Python code"
|
||||
entry: bash -c 'poetry run bandit -q -lll -x '*_test.py,./contrib/,./.venv/' -r .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
|
||||
- id: safety
|
||||
name: safety
|
||||
description: "Safety is a tool that checks your installed dependencies for known security vulnerabilities"
|
||||
entry: bash -c 'poetry run safety check --ignore 70612,66963'
|
||||
language: system
|
||||
|
||||
- id: vulture
|
||||
name: vulture
|
||||
description: "Vulture finds unused code in Python programs."
|
||||
entry: bash -c 'poetry run vulture --exclude "contrib,.venv,tests,conftest.py" --min-confidence 100 .'
|
||||
language: system
|
||||
files: '.*\.py'
|
||||
46
api/Dockerfile
Normal file
46
api/Dockerfile
Normal file
@@ -0,0 +1,46 @@
|
||||
FROM python:3.12-alpine AS build
|
||||
|
||||
LABEL maintainer="https://github.com/prowler-cloud/api"
|
||||
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk --no-cache add gcc python3-dev musl-dev linux-headers curl-dev
|
||||
|
||||
RUN apk --no-cache upgrade && \
|
||||
addgroup -g 1000 prowler && \
|
||||
adduser -D -u 1000 -G prowler prowler
|
||||
USER prowler
|
||||
|
||||
WORKDIR /home/prowler
|
||||
|
||||
COPY pyproject.toml ./
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir poetry
|
||||
|
||||
COPY src/backend/ ./backend/
|
||||
|
||||
ENV PATH="/home/prowler/.local/bin:$PATH"
|
||||
|
||||
RUN poetry install && \
|
||||
rm -rf ~/.cache/pip
|
||||
|
||||
COPY docker-entrypoint.sh ./docker-entrypoint.sh
|
||||
|
||||
WORKDIR /home/prowler/backend
|
||||
|
||||
# Development image
|
||||
# hadolint ignore=DL3006
|
||||
FROM build AS dev
|
||||
|
||||
USER 0
|
||||
# hadolint ignore=DL3018
|
||||
RUN apk --no-cache add curl vim
|
||||
|
||||
USER prowler
|
||||
|
||||
ENTRYPOINT ["../docker-entrypoint.sh", "dev"]
|
||||
|
||||
# Production image
|
||||
FROM build
|
||||
|
||||
ENTRYPOINT ["../docker-entrypoint.sh", "prod"]
|
||||
271
api/README.md
Normal file
271
api/README.md
Normal file
@@ -0,0 +1,271 @@
|
||||
# Description
|
||||
|
||||
This repository contains the JSON API and Task Runner components for Prowler, which facilitate a complete backend that interacts with the Prowler SDK and is used by the Prowler UI.
|
||||
|
||||
# Components
|
||||
The Prowler API is composed of the following components:
|
||||
|
||||
- The JSON API, which is an API built with Django Rest Framework.
|
||||
- The Celery worker, which is responsible for executing the background tasks that are defined in the JSON API.
|
||||
- The PostgreSQL database, which is used to store the data.
|
||||
- The Valkey database, which is an in-memory database which is used as a message broker for the Celery workers.
|
||||
|
||||
## Note about Valkey
|
||||
|
||||
[Valkey](https://valkey.io/) is an open source (BSD) high performance key/value datastore.
|
||||
|
||||
Valkey exposes a Redis 7.2 compliant API. Any service that exposes the Redis API can be used with Prowler API.
|
||||
|
||||
# Modify environment variables
|
||||
|
||||
Under the root path of the project, you can find a file called `.env.example`. This file shows all the environment variables that the project uses. You *must* create a new file called `.env` and set the values for the variables.
|
||||
|
||||
## Local deployment
|
||||
Keep in mind if you export the `.env` file to use it with local deployment that you will have to do it within the context of the Poetry interpreter, not before. Otherwise, variables will not be loaded properly.
|
||||
|
||||
To do this, you can run:
|
||||
|
||||
```console
|
||||
poetry shell
|
||||
set -a
|
||||
source .env
|
||||
```
|
||||
|
||||
# 🚀 Production deployment
|
||||
## Docker deployment
|
||||
|
||||
This method requires `docker` and `docker compose`.
|
||||
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
git clone https://github.com/prowler-cloud/api.git
|
||||
|
||||
# SSH
|
||||
git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
|
||||
### Build the base image
|
||||
|
||||
```console
|
||||
docker compose --profile prod build
|
||||
```
|
||||
|
||||
### Run the production service
|
||||
|
||||
This command will start the Django production server and the Celery worker and also the Valkey and PostgreSQL databases.
|
||||
|
||||
```console
|
||||
docker compose --profile prod up -d
|
||||
```
|
||||
|
||||
You can access the server in `http://localhost:8080`.
|
||||
|
||||
> **NOTE:** notice how the port is different. When developing using docker, the port will be `8080` to prevent conflicts.
|
||||
|
||||
### View the Production Server Logs
|
||||
|
||||
To view the logs for any component (e.g., Django, Celery worker), you can use the following command with a wildcard. This command will follow logs for any container that matches the specified pattern:
|
||||
|
||||
```console
|
||||
docker logs -f $(docker ps --format "{{.Names}}" | grep 'api-')
|
||||
|
||||
## Local deployment
|
||||
|
||||
To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `poetry` and `docker compose` are installed.
|
||||
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
git clone https://github.com/prowler-cloud/api.git
|
||||
|
||||
# SSH
|
||||
git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
### Install all dependencies with Poetry
|
||||
|
||||
```console
|
||||
poetry install
|
||||
poetry shell
|
||||
```
|
||||
|
||||
## Start the PostgreSQL Database and Valkey
|
||||
|
||||
The PostgreSQL database (version 16.3) and Valkey (version 7) are required for the development environment. To make development easier, we have provided a `docker-compose` file that will start these components for you.
|
||||
|
||||
**Note:** Make sure to use the specified versions, as there are features in our setup that may not be compatible with older versions of PostgreSQL and Valkey.
|
||||
|
||||
|
||||
```console
|
||||
docker compose up postgres valkey -d
|
||||
```
|
||||
|
||||
## Deploy Django and the Celery worker
|
||||
|
||||
### Run migrations
|
||||
|
||||
For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run:
|
||||
|
||||
```console
|
||||
cd src/backend
|
||||
python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
### Run the Celery worker
|
||||
|
||||
```console
|
||||
cd src/backend
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
### Run the Django server with Gunicorn
|
||||
|
||||
```console
|
||||
cd src/backend
|
||||
gunicorn -c config/guniconf.py config.wsgi:application
|
||||
```
|
||||
|
||||
> By default, the Gunicorn server will try to use as many workers as your machine can handle. You can manually change that in the `src/backend/config/guniconf.py` file.
|
||||
|
||||
# 🧪 Development guide
|
||||
|
||||
## Local deployment
|
||||
|
||||
To use this method, you'll need to set up a Python virtual environment (version ">=3.11,<3.13") and keep dependencies updated. Additionally, ensure that `poetry` and `docker compose` are installed.
|
||||
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
git clone https://github.com/prowler-cloud/api.git
|
||||
|
||||
# SSH
|
||||
git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
|
||||
### Start the PostgreSQL Database and Valkey
|
||||
|
||||
The PostgreSQL database (version 16.3) and Valkey (version 7) are required for the development environment. To make development easier, we have provided a `docker-compose` file that will start these components for you.
|
||||
|
||||
**Note:** Make sure to use the specified versions, as there are features in our setup that may not be compatible with older versions of PostgreSQL and Valkey.
|
||||
|
||||
|
||||
```console
|
||||
docker compose up postgres valkey -d
|
||||
```
|
||||
|
||||
### Install the Python dependencies
|
||||
|
||||
> You must have Poetry installed
|
||||
|
||||
```console
|
||||
poetry install
|
||||
poetry shell
|
||||
```
|
||||
|
||||
### Apply migrations
|
||||
|
||||
For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run:
|
||||
|
||||
```console
|
||||
cd src/backend
|
||||
python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
### Run the Django development server
|
||||
|
||||
```console
|
||||
cd src/backend
|
||||
python manage.py runserver
|
||||
```
|
||||
|
||||
You can access the server in `http://localhost:8000`.
|
||||
All changes in the code will be automatically reloaded in the server.
|
||||
|
||||
### Run the Celery worker
|
||||
|
||||
```console
|
||||
python -m celery -A config.celery worker -l info -E
|
||||
```
|
||||
|
||||
The Celery worker does not detect and reload changes in the code, so you need to restart it manually when you make changes.
|
||||
|
||||
## Docker deployment
|
||||
|
||||
This method requires `docker` and `docker compose`.
|
||||
|
||||
### Clone the repository
|
||||
|
||||
```console
|
||||
# HTTPS
|
||||
git clone https://github.com/prowler-cloud/api.git
|
||||
|
||||
# SSH
|
||||
git clone git@github.com:prowler-cloud/api.git
|
||||
|
||||
```
|
||||
|
||||
### Build the base image
|
||||
|
||||
```console
|
||||
docker compose --profile dev build
|
||||
```
|
||||
|
||||
### Run the development service
|
||||
|
||||
This command will start the Django development server and the Celery worker and also the Valkey and PostgreSQL databases.
|
||||
|
||||
```console
|
||||
docker compose --profile dev up -d
|
||||
```
|
||||
|
||||
You can access the server in `http://localhost:8080`.
|
||||
All changes in the code will be automatically reloaded in the server.
|
||||
|
||||
> **NOTE:** notice how the port is different. When developing using docker, the port will be `8080` to prevent conflicts.
|
||||
|
||||
### View the development server logs
|
||||
|
||||
To view the logs for any component (e.g., Django, Celery worker), you can use the following command with a wildcard. This command will follow logs for any container that matches the specified pattern:
|
||||
|
||||
```console
|
||||
docker logs -f $(docker ps --format "{{.Names}}" | grep 'api-')
|
||||
|
||||
## Applying migrations
|
||||
|
||||
For migrations, you need to force the `admin` database router. Assuming you have the correct environment variables and Python virtual environment, run:
|
||||
|
||||
```console
|
||||
poetry shell
|
||||
cd src/backend
|
||||
python manage.py migrate --database admin
|
||||
```
|
||||
|
||||
## Apply fixtures
|
||||
|
||||
Fixtures are used to populate the database with initial development data.
|
||||
|
||||
```console
|
||||
poetry shell
|
||||
cd src/backend
|
||||
python manage.py loaddata api/fixtures/0_dev_users.json --database admin
|
||||
```
|
||||
|
||||
> The default credentials are `dev@prowler.com:thisisapassword123` or `dev2@prowler.com:thisisapassword123`
|
||||
|
||||
## Run tests
|
||||
|
||||
Note that the tests will fail if you use the same `.env` file as the development environment.
|
||||
|
||||
For best results, run in a new shell with no environment variables set.
|
||||
|
||||
```console
|
||||
poetry shell
|
||||
cd src/backend
|
||||
pytest
|
||||
```
|
||||
125
api/docker-compose.yml
Normal file
125
api/docker-compose.yml
Normal file
@@ -0,0 +1,125 @@
|
||||
services:
|
||||
api:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
image: prowler-api
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
ports:
|
||||
- "${DJANGO_PORT:-8000}:${DJANGO_PORT:-8000}"
|
||||
profiles:
|
||||
- prod
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "prod"
|
||||
|
||||
api-dev:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
target: dev
|
||||
image: prowler-api-dev
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=config.django.devel
|
||||
- DJANGO_LOGGING_FORMATTER=human_readable
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
ports:
|
||||
- "${DJANGO_PORT:-8080}:${DJANGO_PORT:-8080}"
|
||||
volumes:
|
||||
- "./src/backend:/home/prowler/backend"
|
||||
- "./pyproject.toml:/home/prowler/pyproject.toml"
|
||||
profiles:
|
||||
- dev
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "dev"
|
||||
|
||||
postgres:
|
||||
image: postgres:16.3-alpine
|
||||
ports:
|
||||
- "${POSTGRES_PORT:-5432}:${POSTGRES_PORT:-5432}"
|
||||
hostname: "postgres-db"
|
||||
volumes:
|
||||
- ./_data/postgres:/var/lib/postgresql/data
|
||||
environment:
|
||||
- POSTGRES_USER=${POSTGRES_ADMIN_USER:-prowler}
|
||||
- POSTGRES_PASSWORD=${POSTGRES_ADMIN_PASSWORD:-S3cret}
|
||||
- POSTGRES_DB=${POSTGRES_DB:-prowler_db}
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "sh -c 'pg_isready -U ${POSTGRES_ADMIN_USER:-prowler} -d ${POSTGRES_DB:-prowler_db}'"]
|
||||
interval: 5s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
|
||||
valkey:
|
||||
image: valkey/valkey:7-alpine3.19
|
||||
ports:
|
||||
- "${VALKEY_PORT:-6379}:6379"
|
||||
hostname: "valkey"
|
||||
volumes:
|
||||
- ./_data/valkey:/data
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "sh -c 'valkey-cli ping'"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 3
|
||||
|
||||
worker:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
image: prowler-worker
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-config.django.production}
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
profiles:
|
||||
- dev
|
||||
- prod
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "worker"
|
||||
|
||||
worker-beat:
|
||||
build:
|
||||
dockerfile: Dockerfile
|
||||
image: prowler-worker
|
||||
environment:
|
||||
- DJANGO_SETTINGS_MODULE=${DJANGO_SETTINGS_MODULE:-config.django.production}
|
||||
env_file:
|
||||
- path: ./.env
|
||||
required: false
|
||||
profiles:
|
||||
- dev
|
||||
- prod
|
||||
depends_on:
|
||||
valkey:
|
||||
condition: service_healthy
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
entrypoint:
|
||||
- "../docker-entrypoint.sh"
|
||||
- "beat"
|
||||
71
api/docker-entrypoint.sh
Executable file
71
api/docker-entrypoint.sh
Executable file
@@ -0,0 +1,71 @@
|
||||
#!/bin/sh
|
||||
|
||||
|
||||
apply_migrations() {
|
||||
echo "Applying database migrations..."
|
||||
poetry run python manage.py migrate --database admin
|
||||
}
|
||||
|
||||
apply_fixtures() {
|
||||
echo "Applying Django fixtures..."
|
||||
for fixture in api/fixtures/dev/*.json; do
|
||||
if [ -f "$fixture" ]; then
|
||||
echo "Loading $fixture"
|
||||
poetry run python manage.py loaddata "$fixture" --database admin
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
start_dev_server() {
|
||||
echo "Starting the development server..."
|
||||
poetry run python manage.py runserver 0.0.0.0:"${DJANGO_PORT:-8080}"
|
||||
}
|
||||
|
||||
start_prod_server() {
|
||||
echo "Starting the Gunicorn server..."
|
||||
poetry run gunicorn -c config/guniconf.py config.wsgi:application
|
||||
}
|
||||
|
||||
start_worker() {
|
||||
echo "Starting the worker..."
|
||||
poetry run python -m celery -A config.celery worker -l "${DJANGO_LOGGING_LEVEL:-info}" -Q celery,scans -E
|
||||
}
|
||||
|
||||
start_worker_beat() {
|
||||
echo "Starting the worker-beat..."
|
||||
sleep 15
|
||||
poetry run python -m celery -A config.celery beat -l "${DJANGO_LOGGING_LEVEL:-info}" --scheduler django_celery_beat.schedulers:DatabaseScheduler
|
||||
}
|
||||
|
||||
manage_db_partitions() {
|
||||
if [ "${DJANGO_MANAGE_DB_PARTITIONS}" = "True" ]; then
|
||||
echo "Managing DB partitions..."
|
||||
# For now we skip the deletion of partitions until we define the data retention policy
|
||||
# --yes auto approves the operation without the need of an interactive terminal
|
||||
poetry run python manage.py pgpartition --using admin --skip-delete --yes
|
||||
fi
|
||||
}
|
||||
|
||||
case "$1" in
|
||||
dev)
|
||||
apply_migrations
|
||||
apply_fixtures
|
||||
manage_db_partitions
|
||||
start_dev_server
|
||||
;;
|
||||
prod)
|
||||
apply_migrations
|
||||
manage_db_partitions
|
||||
start_prod_server
|
||||
;;
|
||||
worker)
|
||||
start_worker
|
||||
;;
|
||||
beat)
|
||||
start_worker_beat
|
||||
;;
|
||||
*)
|
||||
echo "Usage: $0 {dev|prod|worker|beat}"
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
65
api/docs/partitions.md
Normal file
65
api/docs/partitions.md
Normal file
@@ -0,0 +1,65 @@
|
||||
# Partitions
|
||||
|
||||
## Overview
|
||||
|
||||
Partitions are used to split the data in a table into smaller chunks, allowing for more efficient querying and storage.
|
||||
|
||||
The Prowler API uses partitions to store findings. The partitions are created based on the UUIDv7 `id` field.
|
||||
|
||||
You can use the Prowler API without ever creating additional partitions. This documentation is only relevant if you want to manage partitions to gain additional query performance.
|
||||
|
||||
### Required Postgres Configuration
|
||||
|
||||
There are 3 configuration options that need to be set in the `postgres.conf` file to get the most performance out of the partitioning:
|
||||
|
||||
- `enable_partition_pruning = on` (default is on)
|
||||
- `enable_partitionwise_join = on` (default is off)
|
||||
- `enable_partitionwise_aggregate = on` (default is off)
|
||||
|
||||
For more information on these options, see the [Postgres documentation](https://www.postgresql.org/docs/current/runtime-config-query.html).
|
||||
|
||||
## Partitioning Strategy
|
||||
|
||||
The partitioning strategy is defined in the `api.partitions` module. The strategy is responsible for creating and deleting partitions based on the provided configuration.
|
||||
|
||||
## Managing Partitions
|
||||
|
||||
The application will run without any extra work on your part. If you want to add or delete partitions, you can use the following commands:
|
||||
|
||||
To manage the partitions, run `python manage.py pgpartition --using admin`
|
||||
|
||||
This command will generate a list of partitions to create and delete based on the provided configuration.
|
||||
|
||||
By default, the command will prompt you to accept the changes before applying them.
|
||||
|
||||
```shell
|
||||
Finding:
|
||||
+ 2024_nov
|
||||
name: 2024_nov
|
||||
from_values: 0192e505-9000-72c8-a47c-cce719d8fb93
|
||||
to_values: 01937f84-5418-7eb8-b2a6-e3be749e839d
|
||||
size_unit: months
|
||||
size_value: 1
|
||||
+ 2024_dec
|
||||
name: 2024_dec
|
||||
from_values: 01937f84-5800-7b55-879c-9cdb46f023f6
|
||||
to_values: 01941f29-7818-7f9f-b4be-20b05bb2f574
|
||||
size_unit: months
|
||||
size_value: 1
|
||||
|
||||
0 partitions will be deleted
|
||||
2 partitions will be created
|
||||
```
|
||||
|
||||
If you choose to apply the partitions, tables will be generated with the following format: `<table_name>_<year>_<month>`.
|
||||
|
||||
For more info on the partitioning manager, see https://github.com/SectorLabs/django-postgres-extra
|
||||
|
||||
### Changing the Partitioning Parameters
|
||||
|
||||
There are 4 environment variables that can be used to change the partitioning parameters:
|
||||
|
||||
- `DJANGO_MANAGE_DB_PARTITIONS`: Allow Django to manage database partitons. By default is set to `False`.
|
||||
- `FINDINGS_TABLE_PARTITION_MONTHS`: Set the months for each partition. Setting the partition monts to 1 will create partitions with a size of 1 natural month.
|
||||
- `FINDINGS_TABLE_PARTITION_COUNT`: Set the number of partitions to create
|
||||
- `FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS`: Set the number of months to keep partitions before deleting them. Setting this to `None` will keep partitions indefinitely.
|
||||
5074
api/poetry.lock
generated
Normal file
5074
api/poetry.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
55
api/pyproject.toml
Normal file
55
api/pyproject.toml
Normal file
@@ -0,0 +1,55 @@
|
||||
[build-system]
|
||||
build-backend = "poetry.core.masonry.api"
|
||||
requires = ["poetry-core"]
|
||||
|
||||
[tool.poetry]
|
||||
authors = ["Prowler Team"]
|
||||
description = "Prowler's API (Django/DRF)"
|
||||
license = "Apache-2.0"
|
||||
name = "prowler-api"
|
||||
package-mode = false
|
||||
version = "1.0.0"
|
||||
|
||||
[tool.poetry.dependencies]
|
||||
celery = {extras = ["pytest"], version = "^5.4.0"}
|
||||
django = "5.1.1"
|
||||
django-celery-beat = "^2.7.0"
|
||||
django-celery-results = "^2.5.1"
|
||||
django-cors-headers = "4.4.0"
|
||||
django-environ = "0.11.2"
|
||||
django-filter = "24.3"
|
||||
django-guid = "3.5.0"
|
||||
django-postgres-extra = "^2.0.8"
|
||||
djangorestframework = "3.15.2"
|
||||
djangorestframework-jsonapi = "7.0.2"
|
||||
djangorestframework-simplejwt = "^5.3.1"
|
||||
drf-nested-routers = "^0.94.1"
|
||||
drf-spectacular = "0.27.2"
|
||||
drf-spectacular-jsonapi = "0.5.1"
|
||||
gunicorn = "23.0.0"
|
||||
prowler = {git = "https://github.com/prowler-cloud/prowler.git", tag = "5.0.0"}
|
||||
psycopg2-binary = "2.9.9"
|
||||
pytest-celery = {extras = ["redis"], version = "^1.0.1"}
|
||||
# Needed for prowler compatibility
|
||||
python = ">=3.11,<3.13"
|
||||
uuid6 = "2024.7.10"
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
bandit = "1.7.9"
|
||||
coverage = "7.5.4"
|
||||
docker = "7.1.0"
|
||||
freezegun = "1.5.1"
|
||||
mypy = "1.10.1"
|
||||
pylint = "3.2.5"
|
||||
pytest = "8.2.2"
|
||||
pytest-cov = "5.0.0"
|
||||
pytest-django = "4.8.0"
|
||||
pytest-env = "1.1.3"
|
||||
pytest-randomly = "3.15.0"
|
||||
pytest-xdist = "3.6.1"
|
||||
ruff = "0.5.0"
|
||||
safety = "3.2.3"
|
||||
vulture = "2.11"
|
||||
|
||||
[tool.poetry.scripts]
|
||||
celery = "src.backend.config.settings.celery"
|
||||
0
api/src/__init__.py
Normal file
0
api/src/__init__.py
Normal file
0
api/src/backend/__init__.py
Normal file
0
api/src/backend/__init__.py
Normal file
0
api/src/backend/api/__init__.py
Normal file
0
api/src/backend/api/__init__.py
Normal file
3
api/src/backend/api/admin.py
Normal file
3
api/src/backend/api/admin.py
Normal file
@@ -0,0 +1,3 @@
|
||||
# from django.contrib import admin
|
||||
|
||||
# Register your models here.
|
||||
12
api/src/backend/api/apps.py
Normal file
12
api/src/backend/api/apps.py
Normal file
@@ -0,0 +1,12 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
default_auto_field = "django.db.models.BigAutoField"
|
||||
name = "api"
|
||||
|
||||
def ready(self):
|
||||
from api import signals # noqa: F401
|
||||
from api.compliance import load_prowler_compliance
|
||||
|
||||
load_prowler_compliance()
|
||||
104
api/src/backend/api/base_views.py
Normal file
104
api/src/backend/api/base_views.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from django.db import transaction
|
||||
from rest_framework import permissions
|
||||
from rest_framework.exceptions import NotAuthenticated
|
||||
from rest_framework.filters import SearchFilter
|
||||
from rest_framework_json_api import filters
|
||||
from rest_framework_json_api.views import ModelViewSet
|
||||
from rest_framework_simplejwt.authentication import JWTAuthentication
|
||||
|
||||
from api.db_utils import POSTGRES_USER_VAR, rls_transaction
|
||||
from api.filters import CustomDjangoFilterBackend
|
||||
|
||||
|
||||
class BaseViewSet(ModelViewSet):
|
||||
authentication_classes = [JWTAuthentication]
|
||||
permission_classes = [permissions.IsAuthenticated]
|
||||
filter_backends = [
|
||||
filters.QueryParameterValidationFilter,
|
||||
filters.OrderingFilter,
|
||||
CustomDjangoFilterBackend,
|
||||
SearchFilter,
|
||||
]
|
||||
|
||||
filterset_fields = []
|
||||
search_fields = []
|
||||
|
||||
ordering_fields = "__all__"
|
||||
ordering = ["id"]
|
||||
|
||||
def get_queryset(self):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseRLSViewSet(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# Ideally, this logic would be in the `.setup()` method but DRF view sets don't call it
|
||||
# https://docs.djangoproject.com/en/5.1/ref/class-based-views/base/#django.views.generic.base.View.setup
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
tenant_id = request.auth.get("tenant_id")
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
def get_serializer_context(self):
|
||||
context = super().get_serializer_context()
|
||||
context["tenant_id"] = self.request.tenant_id
|
||||
return context
|
||||
|
||||
|
||||
class BaseTenantViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
if (
|
||||
request.resolver_match.url_name != "tenant-detail"
|
||||
and request.method != "DELETE"
|
||||
):
|
||||
user_id = str(request.user.id)
|
||||
|
||||
with rls_transaction(value=user_id, parameter=POSTGRES_USER_VAR):
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
# TODO: DRY this when we have time
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
tenant_id = request.auth.get("tenant_id")
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
|
||||
|
||||
class BaseUserViewset(BaseViewSet):
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
with transaction.atomic():
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def initial(self, request, *args, **kwargs):
|
||||
# TODO refactor after improving RLS on users
|
||||
if request.stream is not None and request.stream.method == "POST":
|
||||
return super().initial(request, *args, **kwargs)
|
||||
if request.auth is None:
|
||||
raise NotAuthenticated
|
||||
|
||||
tenant_id = request.auth.get("tenant_id")
|
||||
if tenant_id is None:
|
||||
raise NotAuthenticated("Tenant ID is not present in token")
|
||||
|
||||
with rls_transaction(tenant_id):
|
||||
self.request.tenant_id = tenant_id
|
||||
return super().initial(request, *args, **kwargs)
|
||||
209
api/src/backend/api/compliance.py
Normal file
209
api/src/backend/api/compliance.py
Normal file
@@ -0,0 +1,209 @@
|
||||
from types import MappingProxyType
|
||||
|
||||
from prowler.lib.check.compliance_models import Compliance
|
||||
from prowler.lib.check.models import CheckMetadata
|
||||
|
||||
from api.models import Provider
|
||||
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE = {}
|
||||
PROWLER_CHECKS = {}
|
||||
|
||||
|
||||
def get_prowler_provider_checks(provider_type: Provider.ProviderChoices):
|
||||
"""
|
||||
Retrieve all check IDs for the specified provider type.
|
||||
|
||||
This function fetches the check metadata for the given cloud provider
|
||||
and returns an iterable of check IDs.
|
||||
|
||||
Args:
|
||||
provider_type (Provider.ProviderChoices): The provider type
|
||||
(e.g., 'aws', 'azure') for which to retrieve check IDs.
|
||||
|
||||
Returns:
|
||||
Iterable[str]: An iterable of check IDs associated with the specified provider type.
|
||||
"""
|
||||
return CheckMetadata.get_bulk(provider_type).keys()
|
||||
|
||||
|
||||
def get_prowler_provider_compliance(provider_type: Provider.ProviderChoices) -> dict:
|
||||
"""
|
||||
Retrieve the Prowler compliance data for a specified provider type.
|
||||
|
||||
This function fetches the compliance frameworks and their associated
|
||||
requirements for the given cloud provider.
|
||||
|
||||
Args:
|
||||
provider_type (Provider.ProviderChoices): The provider type
|
||||
(e.g., 'aws', 'azure') for which to retrieve compliance data.
|
||||
|
||||
Returns:
|
||||
dict: A dictionary mapping compliance framework names to their respective
|
||||
Compliance objects for the specified provider.
|
||||
"""
|
||||
return Compliance.get_bulk(provider_type)
|
||||
|
||||
|
||||
def load_prowler_compliance():
|
||||
"""
|
||||
Load and initialize the Prowler compliance data and checks for all provider types.
|
||||
|
||||
This function retrieves compliance data for all supported provider types,
|
||||
generates a compliance overview template, and populates the global variables
|
||||
`PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE` and `PROWLER_CHECKS` with read-only mappings
|
||||
of the compliance templates and checks, respectively.
|
||||
"""
|
||||
global PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE
|
||||
global PROWLER_CHECKS
|
||||
|
||||
prowler_compliance = {
|
||||
provider_type: get_prowler_provider_compliance(provider_type)
|
||||
for provider_type in Provider.ProviderChoices.values
|
||||
}
|
||||
template = generate_compliance_overview_template(prowler_compliance)
|
||||
PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE = MappingProxyType(template)
|
||||
PROWLER_CHECKS = MappingProxyType(load_prowler_checks(prowler_compliance))
|
||||
|
||||
|
||||
def load_prowler_checks(prowler_compliance):
|
||||
"""
|
||||
Generate a mapping of checks to the compliance frameworks that include them.
|
||||
|
||||
This function processes the provided compliance data and creates a dictionary
|
||||
mapping each provider type to a dictionary where each check ID maps to a set
|
||||
of compliance names that include that check.
|
||||
|
||||
Args:
|
||||
prowler_compliance (dict): The compliance data for all provider types,
|
||||
as returned by `get_prowler_provider_compliance`.
|
||||
|
||||
Returns:
|
||||
dict: A nested dictionary where the first-level keys are provider types,
|
||||
and the values are dictionaries mapping check IDs to sets of compliance names.
|
||||
"""
|
||||
checks = {}
|
||||
for provider_type in Provider.ProviderChoices.values:
|
||||
checks[provider_type] = {
|
||||
check_id: set() for check_id in get_prowler_provider_checks(provider_type)
|
||||
}
|
||||
for compliance_name, compliance_data in prowler_compliance[
|
||||
provider_type
|
||||
].items():
|
||||
for requirement in compliance_data.Requirements:
|
||||
for check in requirement.Checks:
|
||||
try:
|
||||
checks[provider_type][check].add(compliance_name)
|
||||
except KeyError:
|
||||
continue
|
||||
return checks
|
||||
|
||||
|
||||
def generate_scan_compliance(
|
||||
compliance_overview, provider_type: str, check_id: str, status: str
|
||||
):
|
||||
"""
|
||||
Update the compliance overview with the status of a specific check.
|
||||
|
||||
This function updates the compliance overview by setting the status of the given check
|
||||
within all compliance frameworks and requirements that include it. It then updates the
|
||||
requirement status to 'FAIL' if any of its checks have failed, and adjusts the counts
|
||||
of passed and failed requirements in the compliance overview.
|
||||
|
||||
Args:
|
||||
compliance_overview (dict): The compliance overview data structure to update.
|
||||
provider_type (str): The provider type (e.g., 'aws', 'azure') associated with the check.
|
||||
check_id (str): The identifier of the check whose status is being updated.
|
||||
status (str): The status of the check (e.g., 'PASS', 'FAIL', 'MUTED').
|
||||
|
||||
Returns:
|
||||
None: This function modifies the compliance_overview in place.
|
||||
"""
|
||||
for compliance_id in PROWLER_CHECKS[provider_type][check_id]:
|
||||
for requirement in compliance_overview[compliance_id]["requirements"].values():
|
||||
if check_id in requirement["checks"]:
|
||||
requirement["checks"][check_id] = status
|
||||
requirement["checks_status"][status.lower()] += 1
|
||||
|
||||
if requirement["status"] != "FAIL" and any(
|
||||
value == "FAIL" for value in requirement["checks"].values()
|
||||
):
|
||||
requirement["status"] = "FAIL"
|
||||
compliance_overview[compliance_id]["requirements_status"]["passed"] -= 1
|
||||
compliance_overview[compliance_id]["requirements_status"]["failed"] += 1
|
||||
|
||||
|
||||
def generate_compliance_overview_template(prowler_compliance: dict):
|
||||
"""
|
||||
Generate a compliance overview template for all provider types.
|
||||
|
||||
This function creates a nested dictionary structure representing the compliance
|
||||
overview template for each provider type, compliance framework, and requirement.
|
||||
It initializes the status of all checks and requirements, and calculates initial
|
||||
counts for requirements status.
|
||||
|
||||
Args:
|
||||
prowler_compliance (dict): The compliance data for all provider types,
|
||||
as returned by `get_prowler_provider_compliance`.
|
||||
|
||||
Returns:
|
||||
dict: A nested dictionary representing the compliance overview template,
|
||||
structured by provider type and compliance framework.
|
||||
"""
|
||||
template = {}
|
||||
for provider_type in Provider.ProviderChoices.values:
|
||||
provider_compliance = template.setdefault(provider_type, {})
|
||||
compliance_data_dict = prowler_compliance[provider_type]
|
||||
|
||||
for compliance_name, compliance_data in compliance_data_dict.items():
|
||||
compliance_requirements = {}
|
||||
requirements_status = {"passed": 0, "failed": 0, "manual": 0}
|
||||
total_requirements = 0
|
||||
|
||||
for requirement in compliance_data.Requirements:
|
||||
total_requirements += 1
|
||||
total_checks = len(requirement.Checks)
|
||||
checks_dict = {check: None for check in requirement.Checks}
|
||||
|
||||
# Build requirement dictionary
|
||||
requirement_dict = {
|
||||
"name": requirement.Name or requirement.Id,
|
||||
"description": requirement.Description,
|
||||
"attributes": [
|
||||
dict(attribute) for attribute in requirement.Attributes
|
||||
],
|
||||
"checks": checks_dict,
|
||||
"checks_status": {
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"manual": 0,
|
||||
"total": total_checks,
|
||||
},
|
||||
"status": "PASS",
|
||||
}
|
||||
|
||||
# Update requirements status
|
||||
if total_checks == 0:
|
||||
requirements_status["manual"] += 1
|
||||
|
||||
# Add requirement to compliance requirements
|
||||
compliance_requirements[requirement.Id] = requirement_dict
|
||||
|
||||
# Calculate pending requirements
|
||||
pending_requirements = total_requirements - requirements_status["manual"]
|
||||
requirements_status["passed"] = pending_requirements
|
||||
|
||||
# Build compliance dictionary
|
||||
compliance_dict = {
|
||||
"framework": compliance_data.Framework,
|
||||
"version": compliance_data.Version,
|
||||
"provider": provider_type,
|
||||
"description": compliance_data.Description,
|
||||
"requirements": compliance_requirements,
|
||||
"requirements_status": requirements_status,
|
||||
"total_requirements": total_requirements,
|
||||
}
|
||||
|
||||
# Add compliance to provider compliance
|
||||
provider_compliance[compliance_name] = compliance_dict
|
||||
|
||||
return template
|
||||
18
api/src/backend/api/db_router.py
Normal file
18
api/src/backend/api/db_router.py
Normal file
@@ -0,0 +1,18 @@
|
||||
class MainRouter:
|
||||
default_db = "default"
|
||||
admin_db = "admin"
|
||||
|
||||
def db_for_read(self, model, **hints): # noqa: F841
|
||||
model_table_name = model._meta.db_table
|
||||
if model_table_name.startswith("django_"):
|
||||
return self.admin_db
|
||||
return None
|
||||
|
||||
def db_for_write(self, model, **hints): # noqa: F841
|
||||
model_table_name = model._meta.db_table
|
||||
if model_table_name.startswith("django_"):
|
||||
return self.admin_db
|
||||
return None
|
||||
|
||||
def allow_migrate(self, db, app_label, model_name=None, **hints): # noqa: F841
|
||||
return db == self.admin_db
|
||||
318
api/src/backend/api/db_utils.py
Normal file
318
api/src/backend/api/db_utils.py
Normal file
@@ -0,0 +1,318 @@
|
||||
import secrets
|
||||
import uuid
|
||||
from contextlib import contextmanager
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import BaseUserManager
|
||||
from django.core.paginator import Paginator
|
||||
from django.db import connection, models, transaction
|
||||
from psycopg2 import connect as psycopg2_connect
|
||||
from psycopg2.extensions import AsIs, new_type, register_adapter, register_type
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
DB_USER = settings.DATABASES["default"]["USER"] if not settings.TESTING else "test"
|
||||
DB_PASSWORD = (
|
||||
settings.DATABASES["default"]["PASSWORD"] if not settings.TESTING else "test"
|
||||
)
|
||||
DB_PROWLER_USER = (
|
||||
settings.DATABASES["prowler_user"]["USER"] if not settings.TESTING else "test"
|
||||
)
|
||||
DB_PROWLER_PASSWORD = (
|
||||
settings.DATABASES["prowler_user"]["PASSWORD"] if not settings.TESTING else "test"
|
||||
)
|
||||
TASK_RUNNER_DB_TABLE = "django_celery_results_taskresult"
|
||||
POSTGRES_TENANT_VAR = "api.tenant_id"
|
||||
POSTGRES_USER_VAR = "api.user_id"
|
||||
|
||||
SET_CONFIG_QUERY = "SELECT set_config(%s, %s::text, TRUE);"
|
||||
|
||||
|
||||
@contextmanager
|
||||
def psycopg_connection(database_alias: str):
|
||||
psycopg2_connection = None
|
||||
try:
|
||||
admin_db = settings.DATABASES[database_alias]
|
||||
|
||||
psycopg2_connection = psycopg2_connect(
|
||||
dbname=admin_db["NAME"],
|
||||
user=admin_db["USER"],
|
||||
password=admin_db["PASSWORD"],
|
||||
host=admin_db["HOST"],
|
||||
port=admin_db["PORT"],
|
||||
)
|
||||
yield psycopg2_connection
|
||||
finally:
|
||||
if psycopg2_connection is not None:
|
||||
psycopg2_connection.close()
|
||||
|
||||
|
||||
@contextmanager
|
||||
def rls_transaction(value: str, parameter: str = POSTGRES_TENANT_VAR):
|
||||
"""
|
||||
Creates a new database transaction setting the given configuration value for Postgres RLS. It validates the
|
||||
if the value is a valid UUID.
|
||||
|
||||
Args:
|
||||
value (str): Database configuration parameter value.
|
||||
parameter (str): Database configuration parameter name, by default is 'api.tenant_id'.
|
||||
"""
|
||||
with transaction.atomic():
|
||||
with connection.cursor() as cursor:
|
||||
try:
|
||||
# just in case the value is an UUID object
|
||||
uuid.UUID(str(value))
|
||||
except ValueError:
|
||||
raise ValidationError("Must be a valid UUID")
|
||||
cursor.execute(SET_CONFIG_QUERY, [parameter, value])
|
||||
yield cursor
|
||||
|
||||
|
||||
class CustomUserManager(BaseUserManager):
|
||||
def create_user(self, email, password=None, **extra_fields):
|
||||
if not email:
|
||||
raise ValueError("The email field must be set")
|
||||
email = self.normalize_email(email)
|
||||
user = self.model(email=email, **extra_fields)
|
||||
user.set_password(password)
|
||||
user.save(using=self._db)
|
||||
return user
|
||||
|
||||
def get_by_natural_key(self, email):
|
||||
return self.get(email__iexact=email)
|
||||
|
||||
|
||||
def enum_to_choices(enum_class):
|
||||
"""
|
||||
This function converts a Python Enum to a list of tuples, where the first element is the value and the second element is the name.
|
||||
|
||||
It's for use with Django's `choices` attribute, which expects a list of tuples.
|
||||
"""
|
||||
return [(item.value, item.name.replace("_", " ").title()) for item in enum_class]
|
||||
|
||||
|
||||
def one_week_from_now():
|
||||
"""
|
||||
Return a datetime object with a date one week from now.
|
||||
"""
|
||||
return datetime.now(timezone.utc) + timedelta(days=7)
|
||||
|
||||
|
||||
def generate_random_token(length: int = 14, symbols: str | None = None) -> str:
|
||||
"""
|
||||
Generate a random token with the specified length.
|
||||
"""
|
||||
_symbols = "23456789ABCDEFGHJKMNPQRSTVWXYZ"
|
||||
return "".join(secrets.choice(symbols or _symbols) for _ in range(length))
|
||||
|
||||
|
||||
def batch_delete(queryset, batch_size=5000):
|
||||
"""
|
||||
Deletes objects in batches and returns the total number of deletions and a summary.
|
||||
|
||||
Args:
|
||||
queryset (QuerySet): The queryset of objects to delete.
|
||||
batch_size (int): The number of objects to delete in each batch.
|
||||
|
||||
Returns:
|
||||
tuple: (total_deleted, deletion_summary)
|
||||
"""
|
||||
total_deleted = 0
|
||||
deletion_summary = {}
|
||||
|
||||
paginator = Paginator(queryset.order_by("id").only("id"), batch_size)
|
||||
|
||||
for page_num in paginator.page_range:
|
||||
batch_ids = [obj.id for obj in paginator.page(page_num).object_list]
|
||||
|
||||
deleted_count, deleted_info = queryset.filter(id__in=batch_ids).delete()
|
||||
|
||||
total_deleted += deleted_count
|
||||
|
||||
for model_label, count in deleted_info.items():
|
||||
deletion_summary[model_label] = deletion_summary.get(model_label, 0) + count
|
||||
|
||||
return total_deleted, deletion_summary
|
||||
|
||||
|
||||
# Postgres Enums
|
||||
|
||||
|
||||
class PostgresEnumMigration:
|
||||
def __init__(self, enum_name: str, enum_values: tuple):
|
||||
self.enum_name = enum_name
|
||||
self.enum_values = enum_values
|
||||
|
||||
def create_enum_type(self, apps, schema_editor): # noqa: F841
|
||||
string_enum_values = ", ".join([f"'{value}'" for value in self.enum_values])
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute(
|
||||
f"CREATE TYPE {self.enum_name} AS ENUM ({string_enum_values});"
|
||||
)
|
||||
|
||||
def drop_enum_type(self, apps, schema_editor): # noqa: F841
|
||||
with schema_editor.connection.cursor() as cursor:
|
||||
cursor.execute(f"DROP TYPE {self.enum_name};")
|
||||
|
||||
|
||||
class PostgresEnumField(models.Field):
|
||||
def __init__(self, enum_type_name, *args, **kwargs):
|
||||
self.enum_type_name = enum_type_name
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def db_type(self, connection):
|
||||
return self.enum_type_name
|
||||
|
||||
def from_db_value(self, value, expression, connection): # noqa: F841
|
||||
return value
|
||||
|
||||
def to_python(self, value):
|
||||
if isinstance(value, EnumType):
|
||||
return value.value
|
||||
return value
|
||||
|
||||
def get_prep_value(self, value):
|
||||
if isinstance(value, EnumType):
|
||||
return value.value
|
||||
return value
|
||||
|
||||
|
||||
class EnumType:
|
||||
def __init__(self, value):
|
||||
self.value = value
|
||||
|
||||
def __str__(self):
|
||||
return self.value
|
||||
|
||||
|
||||
def enum_adapter(enum_obj):
|
||||
return AsIs(f"'{enum_obj.value}'::{enum_obj.__class__.enum_type_name}")
|
||||
|
||||
|
||||
def get_enum_oid(connection, enum_type_name: str):
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute("SELECT oid FROM pg_type WHERE typname = %s;", (enum_type_name,))
|
||||
result = cursor.fetchone()
|
||||
if result is None:
|
||||
raise ValueError(f"Enum type '{enum_type_name}' not found")
|
||||
return result[0]
|
||||
|
||||
|
||||
def register_enum(apps, schema_editor, enum_class): # noqa: F841
|
||||
with psycopg_connection(schema_editor.connection.alias) as connection:
|
||||
enum_oid = get_enum_oid(connection, enum_class.enum_type_name)
|
||||
enum_instance = new_type(
|
||||
(enum_oid,),
|
||||
enum_class.enum_type_name,
|
||||
lambda value, cur: value, # noqa: F841
|
||||
)
|
||||
register_type(enum_instance, connection)
|
||||
register_adapter(enum_class, enum_adapter)
|
||||
|
||||
|
||||
# Postgres enum definition for member role
|
||||
|
||||
|
||||
class MemberRoleEnum(EnumType):
|
||||
enum_type_name = "member_role"
|
||||
|
||||
|
||||
class MemberRoleEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("member_role", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Provider.provider
|
||||
|
||||
|
||||
class ProviderEnum(EnumType):
|
||||
enum_type_name = "provider"
|
||||
|
||||
|
||||
class ProviderEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("provider", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Scan.type
|
||||
|
||||
|
||||
class ScanTriggerEnum(EnumType):
|
||||
enum_type_name = "scan_trigger"
|
||||
|
||||
|
||||
class ScanTriggerEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("scan_trigger", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for state
|
||||
|
||||
|
||||
class StateEnum(EnumType):
|
||||
enum_type_name = "state"
|
||||
|
||||
|
||||
class StateEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("state", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Finding.Delta
|
||||
|
||||
|
||||
class FindingDeltaEnum(EnumType):
|
||||
enum_type_name = "finding_delta"
|
||||
|
||||
|
||||
class FindingDeltaEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("finding_delta", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Severity
|
||||
|
||||
|
||||
class SeverityEnum(EnumType):
|
||||
enum_type_name = "severity"
|
||||
|
||||
|
||||
class SeverityEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("severity", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Status
|
||||
|
||||
|
||||
class StatusEnum(EnumType):
|
||||
enum_type_name = "status"
|
||||
|
||||
|
||||
class StatusEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("status", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Provider secrets type
|
||||
|
||||
|
||||
class ProviderSecretTypeEnum(EnumType):
|
||||
enum_type_name = "provider_secret_type"
|
||||
|
||||
|
||||
class ProviderSecretTypeEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("provider_secret_type", *args, **kwargs)
|
||||
|
||||
|
||||
# Postgres enum definition for Provider secrets type
|
||||
|
||||
|
||||
class InvitationStateEnum(EnumType):
|
||||
enum_type_name = "invitation_state"
|
||||
|
||||
|
||||
class InvitationStateEnumField(PostgresEnumField):
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__("invitation_state", *args, **kwargs)
|
||||
59
api/src/backend/api/decorators.py
Normal file
59
api/src/backend/api/decorators.py
Normal file
@@ -0,0 +1,59 @@
|
||||
import uuid
|
||||
from functools import wraps
|
||||
|
||||
from django.db import connection, transaction
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
|
||||
|
||||
def set_tenant(func):
|
||||
"""
|
||||
Decorator to set the tenant context for a Celery task based on the provided tenant_id.
|
||||
|
||||
This decorator extracts the `tenant_id` from the task's keyword arguments,
|
||||
and uses it to set the tenant context for the current database session.
|
||||
The `tenant_id` is then removed from the kwargs before the task function
|
||||
is executed. If `tenant_id` is not provided, a KeyError is raised.
|
||||
|
||||
Args:
|
||||
func (function): The Celery task function to be decorated.
|
||||
|
||||
Raises:
|
||||
KeyError: If `tenant_id` is not found in the task's keyword arguments.
|
||||
|
||||
Returns:
|
||||
function: The wrapped function with tenant context set.
|
||||
|
||||
Example:
|
||||
# This decorator MUST be defined the last in the decorator chain
|
||||
|
||||
@shared_task
|
||||
@set_tenant
|
||||
def some_task(arg1, **kwargs):
|
||||
# Task logic here
|
||||
pass
|
||||
|
||||
# When calling the task
|
||||
some_task.delay(arg1, tenant_id="8db7ca86-03cc-4d42-99f6-5e480baf6ab5")
|
||||
|
||||
# The tenant context will be set before the task logic executes.
|
||||
"""
|
||||
|
||||
@wraps(func)
|
||||
@transaction.atomic
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
tenant_id = kwargs.pop("tenant_id")
|
||||
except KeyError:
|
||||
raise KeyError("This task requires the tenant_id")
|
||||
try:
|
||||
uuid.UUID(tenant_id)
|
||||
except ValueError:
|
||||
raise ValidationError("Tenant ID must be a valid UUID")
|
||||
with connection.cursor() as cursor:
|
||||
cursor.execute(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
|
||||
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
45
api/src/backend/api/exceptions.py
Normal file
45
api/src/backend/api/exceptions.py
Normal file
@@ -0,0 +1,45 @@
|
||||
from django.core.exceptions import ValidationError as django_validation_error
|
||||
from rest_framework import status
|
||||
from rest_framework.exceptions import APIException
|
||||
from rest_framework_json_api.exceptions import exception_handler
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from rest_framework_simplejwt.exceptions import TokenError, InvalidToken
|
||||
|
||||
|
||||
class ModelValidationError(ValidationError):
|
||||
def __init__(
|
||||
self,
|
||||
detail: str | None = None,
|
||||
code: str | None = None,
|
||||
pointer: str | None = None,
|
||||
status_code: int = 400,
|
||||
):
|
||||
super().__init__(
|
||||
detail=[
|
||||
{
|
||||
"detail": detail,
|
||||
"status": str(status_code),
|
||||
"source": {"pointer": pointer},
|
||||
"code": code,
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
|
||||
class InvitationTokenExpiredException(APIException):
|
||||
status_code = status.HTTP_410_GONE
|
||||
default_detail = "The invitation token has expired and is no longer valid."
|
||||
default_code = "token_expired"
|
||||
|
||||
|
||||
def custom_exception_handler(exc, context):
|
||||
if isinstance(exc, django_validation_error):
|
||||
if hasattr(exc, "error_dict"):
|
||||
exc = ValidationError(exc.message_dict)
|
||||
else:
|
||||
exc = ValidationError(detail=exc.messages[0], code=exc.code)
|
||||
elif isinstance(exc, (TokenError, InvalidToken)):
|
||||
exc.detail["messages"] = [
|
||||
message_item["message"] for message_item in exc.detail["messages"]
|
||||
]
|
||||
return exception_handler(exc, context)
|
||||
523
api/src/backend/api/filters.py
Normal file
523
api/src/backend/api/filters.py
Normal file
@@ -0,0 +1,523 @@
|
||||
from datetime import date, datetime, timezone
|
||||
|
||||
from django.conf import settings
|
||||
from django.db.models import Q
|
||||
from django_filters.rest_framework import (
|
||||
BaseInFilter,
|
||||
BooleanFilter,
|
||||
CharFilter,
|
||||
ChoiceFilter,
|
||||
DateFilter,
|
||||
FilterSet,
|
||||
UUIDFilter,
|
||||
)
|
||||
from rest_framework_json_api.django_filters.backends import DjangoFilterBackend
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
|
||||
from api.db_utils import (
|
||||
FindingDeltaEnumField,
|
||||
InvitationStateEnumField,
|
||||
ProviderEnumField,
|
||||
SeverityEnumField,
|
||||
StatusEnumField,
|
||||
)
|
||||
from api.models import (
|
||||
ComplianceOverview,
|
||||
Finding,
|
||||
Invitation,
|
||||
Membership,
|
||||
Provider,
|
||||
ProviderGroup,
|
||||
ProviderSecret,
|
||||
Resource,
|
||||
ResourceTag,
|
||||
Scan,
|
||||
ScanSummary,
|
||||
SeverityChoices,
|
||||
StateChoices,
|
||||
StatusChoices,
|
||||
Task,
|
||||
User,
|
||||
)
|
||||
from api.rls import Tenant
|
||||
from api.uuid_utils import (
|
||||
datetime_to_uuid7,
|
||||
transform_into_uuid7,
|
||||
uuid7_end,
|
||||
uuid7_range,
|
||||
uuid7_start,
|
||||
)
|
||||
from api.v1.serializers import TaskBase
|
||||
|
||||
|
||||
class CustomDjangoFilterBackend(DjangoFilterBackend):
|
||||
def to_html(self, _request, _queryset, _view):
|
||||
"""Override this method to use the Browsable API in dev environments.
|
||||
|
||||
This disables the HTML render for the default filter.
|
||||
"""
|
||||
return None
|
||||
|
||||
def get_filterset_class(self, view, queryset=None):
|
||||
# Check if the view has 'get_filterset_class' method
|
||||
if hasattr(view, "get_filterset_class"):
|
||||
return view.get_filterset_class()
|
||||
# Fallback to the default implementation
|
||||
return super().get_filterset_class(view, queryset)
|
||||
|
||||
|
||||
class UUIDInFilter(BaseInFilter, UUIDFilter):
|
||||
pass
|
||||
|
||||
|
||||
class CharInFilter(BaseInFilter, CharFilter):
|
||||
pass
|
||||
|
||||
|
||||
class ChoiceInFilter(BaseInFilter, ChoiceFilter):
|
||||
pass
|
||||
|
||||
|
||||
class TenantFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
|
||||
class Meta:
|
||||
model = Tenant
|
||||
fields = {
|
||||
"name": ["exact", "icontains"],
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"updated_at": ["gte", "lte"],
|
||||
}
|
||||
|
||||
|
||||
class MembershipFilter(FilterSet):
|
||||
date_joined = DateFilter(field_name="date_joined", lookup_expr="date")
|
||||
role = ChoiceFilter(choices=Membership.RoleChoices.choices)
|
||||
|
||||
class Meta:
|
||||
model = Membership
|
||||
fields = {
|
||||
"tenant": ["exact"],
|
||||
"role": ["exact"],
|
||||
"date_joined": ["date", "gte", "lte"],
|
||||
}
|
||||
|
||||
|
||||
class ProviderFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
connected = BooleanFilter()
|
||||
provider = ChoiceFilter(choices=Provider.ProviderChoices.choices)
|
||||
|
||||
class Meta:
|
||||
model = Provider
|
||||
fields = {
|
||||
"provider": ["exact", "in"],
|
||||
"id": ["exact", "in"],
|
||||
"uid": ["exact", "icontains", "in"],
|
||||
"alias": ["exact", "icontains", "in"],
|
||||
"inserted_at": ["gte", "lte"],
|
||||
"updated_at": ["gte", "lte"],
|
||||
}
|
||||
filter_overrides = {
|
||||
ProviderEnumField: {
|
||||
"filter_class": CharFilter,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class ProviderRelationshipFilterSet(FilterSet):
|
||||
provider_type = ChoiceFilter(
|
||||
choices=Provider.ProviderChoices.choices, field_name="provider__provider"
|
||||
)
|
||||
provider_type__in = ChoiceInFilter(
|
||||
choices=Provider.ProviderChoices.choices, field_name="provider__provider"
|
||||
)
|
||||
provider_uid = CharFilter(field_name="provider__uid", lookup_expr="exact")
|
||||
provider_uid__in = CharInFilter(field_name="provider__uid", lookup_expr="in")
|
||||
provider_uid__icontains = CharFilter(
|
||||
field_name="provider__uid", lookup_expr="icontains"
|
||||
)
|
||||
provider_alias = CharFilter(field_name="provider__alias", lookup_expr="exact")
|
||||
provider_alias__in = CharInFilter(field_name="provider__alias", lookup_expr="in")
|
||||
provider_alias__icontains = CharFilter(
|
||||
field_name="provider__alias", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
|
||||
class ProviderGroupFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
|
||||
class Meta:
|
||||
model = ProviderGroup
|
||||
fields = {
|
||||
"id": ["exact", "in"],
|
||||
"name": ["exact", "in"],
|
||||
"inserted_at": ["gte", "lte"],
|
||||
"updated_at": ["gte", "lte"],
|
||||
}
|
||||
|
||||
|
||||
class ScanFilter(ProviderRelationshipFilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
completed_at = DateFilter(field_name="completed_at", lookup_expr="date")
|
||||
started_at = DateFilter(field_name="started_at", lookup_expr="date")
|
||||
next_scan_at = DateFilter(field_name="next_scan_at", lookup_expr="date")
|
||||
trigger = ChoiceFilter(choices=Scan.TriggerChoices.choices)
|
||||
state = ChoiceFilter(choices=StateChoices.choices)
|
||||
state__in = ChoiceInFilter(
|
||||
field_name="state", choices=StateChoices.choices, lookup_expr="in"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Scan
|
||||
fields = {
|
||||
"provider": ["exact", "in"],
|
||||
"name": ["exact", "icontains"],
|
||||
"started_at": ["gte", "lte"],
|
||||
"next_scan_at": ["gte", "lte"],
|
||||
"trigger": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
class TaskFilter(FilterSet):
|
||||
name = CharFilter(field_name="task_runner_task__task_name", lookup_expr="exact")
|
||||
name__icontains = CharFilter(
|
||||
field_name="task_runner_task__task_name", lookup_expr="icontains"
|
||||
)
|
||||
state = ChoiceFilter(
|
||||
choices=StateChoices.choices, method="filter_state", lookup_expr="exact"
|
||||
)
|
||||
task_state_inverse_mapping_values = {
|
||||
v: k for k, v in TaskBase.state_mapping.items()
|
||||
}
|
||||
|
||||
def filter_state(self, queryset, name, value):
|
||||
if value not in StateChoices:
|
||||
raise ValidationError(
|
||||
f"Invalid provider value: '{value}'. Valid values are: "
|
||||
f"{', '.join(StateChoices)}"
|
||||
)
|
||||
|
||||
return queryset.filter(
|
||||
task_runner_task__status=self.task_state_inverse_mapping_values[value]
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = Task
|
||||
fields = []
|
||||
|
||||
|
||||
class ResourceTagFilter(FilterSet):
|
||||
class Meta:
|
||||
model = ResourceTag
|
||||
fields = {
|
||||
"key": ["exact", "icontains"],
|
||||
"value": ["exact", "icontains"],
|
||||
}
|
||||
search = ["text_search"]
|
||||
|
||||
|
||||
class ResourceFilter(ProviderRelationshipFilterSet):
|
||||
tag_key = CharFilter(method="filter_tag_key")
|
||||
tag_value = CharFilter(method="filter_tag_value")
|
||||
tag = CharFilter(method="filter_tag")
|
||||
tags = CharFilter(method="filter_tag")
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
|
||||
class Meta:
|
||||
model = Resource
|
||||
fields = {
|
||||
"provider": ["exact", "in"],
|
||||
"uid": ["exact", "icontains"],
|
||||
"name": ["exact", "icontains"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
"service": ["exact", "icontains", "in"],
|
||||
"type": ["exact", "icontains", "in"],
|
||||
"inserted_at": ["gte", "lte"],
|
||||
"updated_at": ["gte", "lte"],
|
||||
}
|
||||
|
||||
def filter_tag_key(self, queryset, name, value):
|
||||
return queryset.filter(Q(tags__key=value) | Q(tags__key__icontains=value))
|
||||
|
||||
def filter_tag_value(self, queryset, name, value):
|
||||
return queryset.filter(Q(tags__value=value) | Q(tags__value__icontains=value))
|
||||
|
||||
def filter_tag(self, queryset, name, value):
|
||||
# We won't know what the user wants to filter on just based on the value,
|
||||
# and we don't want to build special filtering logic for every possible
|
||||
# provider tag spec, so we'll just do a full text search
|
||||
return queryset.filter(tags__text_search=value)
|
||||
|
||||
|
||||
class FindingFilter(FilterSet):
|
||||
# We filter providers from the scan in findings
|
||||
provider = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
|
||||
provider__in = UUIDInFilter(field_name="scan__provider__id", lookup_expr="in")
|
||||
provider_type = ChoiceFilter(
|
||||
choices=Provider.ProviderChoices.choices, field_name="scan__provider__provider"
|
||||
)
|
||||
provider_type__in = ChoiceInFilter(
|
||||
choices=Provider.ProviderChoices.choices, field_name="scan__provider__provider"
|
||||
)
|
||||
provider_uid = CharFilter(field_name="scan__provider__uid", lookup_expr="exact")
|
||||
provider_uid__in = CharInFilter(field_name="scan__provider__uid", lookup_expr="in")
|
||||
provider_uid__icontains = CharFilter(
|
||||
field_name="scan__provider__uid", lookup_expr="icontains"
|
||||
)
|
||||
provider_alias = CharFilter(field_name="scan__provider__alias", lookup_expr="exact")
|
||||
provider_alias__in = CharInFilter(
|
||||
field_name="scan__provider__alias", lookup_expr="in"
|
||||
)
|
||||
provider_alias__icontains = CharFilter(
|
||||
field_name="scan__provider__alias", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
|
||||
uid = CharFilter(field_name="uid")
|
||||
delta = ChoiceFilter(choices=Finding.DeltaChoices.choices)
|
||||
status = ChoiceFilter(choices=StatusChoices.choices)
|
||||
severity = ChoiceFilter(choices=SeverityChoices)
|
||||
impact = ChoiceFilter(choices=SeverityChoices)
|
||||
|
||||
resources = UUIDInFilter(field_name="resource__id", lookup_expr="in")
|
||||
|
||||
region = CharFilter(field_name="resources__region")
|
||||
region__in = CharInFilter(field_name="resources__region", lookup_expr="in")
|
||||
region__icontains = CharFilter(
|
||||
field_name="resources__region", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
service = CharFilter(field_name="resources__service")
|
||||
service__in = CharInFilter(field_name="resources__service", lookup_expr="in")
|
||||
service__icontains = CharFilter(
|
||||
field_name="resources__service", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
resource_uid = CharFilter(field_name="resources__uid")
|
||||
resource_uid__in = CharInFilter(field_name="resources__uid", lookup_expr="in")
|
||||
resource_uid__icontains = CharFilter(
|
||||
field_name="resources__uid", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
resource_name = CharFilter(field_name="resources__name")
|
||||
resource_name__in = CharInFilter(field_name="resources__name", lookup_expr="in")
|
||||
resource_name__icontains = CharFilter(
|
||||
field_name="resources__name", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
resource_type = CharFilter(field_name="resources__type")
|
||||
resource_type__in = CharInFilter(field_name="resources__type", lookup_expr="in")
|
||||
resource_type__icontains = CharFilter(
|
||||
field_name="resources__type", lookup_expr="icontains"
|
||||
)
|
||||
|
||||
scan = UUIDFilter(method="filter_scan_id")
|
||||
scan__in = UUIDInFilter(method="filter_scan_id_in")
|
||||
|
||||
inserted_at = DateFilter(method="filter_inserted_at", lookup_expr="date")
|
||||
inserted_at__date = DateFilter(method="filter_inserted_at", lookup_expr="date")
|
||||
inserted_at__gte = DateFilter(method="filter_inserted_at_gte")
|
||||
inserted_at__lte = DateFilter(method="filter_inserted_at_lte")
|
||||
|
||||
class Meta:
|
||||
model = Finding
|
||||
fields = {
|
||||
"id": ["exact", "in"],
|
||||
"uid": ["exact", "in"],
|
||||
"scan": ["exact", "in"],
|
||||
"delta": ["exact", "in"],
|
||||
"status": ["exact", "in"],
|
||||
"severity": ["exact", "in"],
|
||||
"impact": ["exact", "in"],
|
||||
"check_id": ["exact", "in", "icontains"],
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"updated_at": ["gte", "lte"],
|
||||
}
|
||||
filter_overrides = {
|
||||
FindingDeltaEnumField: {
|
||||
"filter_class": CharFilter,
|
||||
},
|
||||
StatusEnumField: {
|
||||
"filter_class": CharFilter,
|
||||
},
|
||||
SeverityEnumField: {
|
||||
"filter_class": CharFilter,
|
||||
},
|
||||
}
|
||||
|
||||
# Convert filter values to UUIDv7 values for use with partitioning
|
||||
def filter_scan_id(self, queryset, name, value):
|
||||
try:
|
||||
value_uuid = transform_into_uuid7(value)
|
||||
start = uuid7_start(value_uuid)
|
||||
end = uuid7_end(value_uuid, settings.FINDINGS_TABLE_PARTITION_MONTHS)
|
||||
except ValidationError as validation_error:
|
||||
detail = str(validation_error.detail[0])
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"detail": detail,
|
||||
"status": 400,
|
||||
"source": {"pointer": "/data/relationships/scan"},
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
)
|
||||
|
||||
return (
|
||||
queryset.filter(id__gte=start)
|
||||
.filter(id__lt=end)
|
||||
.filter(scan__id=value_uuid)
|
||||
)
|
||||
|
||||
def filter_scan_id_in(self, queryset, name, value):
|
||||
try:
|
||||
uuid_list = [
|
||||
transform_into_uuid7(value_uuid)
|
||||
for value_uuid in value
|
||||
if value_uuid is not None
|
||||
]
|
||||
|
||||
start, end = uuid7_range(uuid_list)
|
||||
except ValidationError as validation_error:
|
||||
detail = str(validation_error.detail[0])
|
||||
raise ValidationError(
|
||||
[
|
||||
{
|
||||
"detail": detail,
|
||||
"status": 400,
|
||||
"source": {"pointer": "/data/relationships/scan"},
|
||||
"code": "invalid",
|
||||
}
|
||||
]
|
||||
)
|
||||
if start == end:
|
||||
return queryset.filter(id__gte=start).filter(scan__id__in=uuid_list)
|
||||
else:
|
||||
return (
|
||||
queryset.filter(id__gte=start)
|
||||
.filter(id__lt=end)
|
||||
.filter(scan__id__in=uuid_list)
|
||||
)
|
||||
|
||||
def filter_inserted_at(self, queryset, name, value):
|
||||
value = self.maybe_date_to_datetime(value)
|
||||
start = uuid7_start(datetime_to_uuid7(value))
|
||||
|
||||
return queryset.filter(id__gte=start).filter(inserted_at__date=value)
|
||||
|
||||
def filter_inserted_at_gte(self, queryset, name, value):
|
||||
value = self.maybe_date_to_datetime(value)
|
||||
start = uuid7_start(datetime_to_uuid7(value))
|
||||
|
||||
return queryset.filter(id__gte=start).filter(inserted_at__gte=value)
|
||||
|
||||
def filter_inserted_at_lte(self, queryset, name, value):
|
||||
value = self.maybe_date_to_datetime(value)
|
||||
end = uuid7_start(datetime_to_uuid7(value))
|
||||
|
||||
return queryset.filter(id__lte=end).filter(inserted_at__lte=value)
|
||||
|
||||
@staticmethod
|
||||
def maybe_date_to_datetime(value):
|
||||
dt = value
|
||||
if isinstance(value, date):
|
||||
dt = datetime.combine(value, datetime.min.time(), tzinfo=timezone.utc)
|
||||
return dt
|
||||
|
||||
|
||||
class ProviderSecretFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
provider = UUIDFilter(field_name="provider__id", lookup_expr="exact")
|
||||
|
||||
class Meta:
|
||||
model = ProviderSecret
|
||||
fields = {
|
||||
"name": ["exact", "icontains"],
|
||||
}
|
||||
|
||||
|
||||
class InvitationFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
updated_at = DateFilter(field_name="updated_at", lookup_expr="date")
|
||||
expires_at = DateFilter(field_name="expires_at", lookup_expr="date")
|
||||
state = ChoiceFilter(choices=Invitation.State.choices)
|
||||
state__in = ChoiceInFilter(choices=Invitation.State.choices, lookup_expr="in")
|
||||
|
||||
class Meta:
|
||||
model = Invitation
|
||||
fields = {
|
||||
"email": ["exact", "icontains"],
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"updated_at": ["date", "gte", "lte"],
|
||||
"expires_at": ["date", "gte", "lte"],
|
||||
"inviter": ["exact"],
|
||||
}
|
||||
filter_overrides = {
|
||||
InvitationStateEnumField: {
|
||||
"filter_class": CharFilter,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class UserFilter(FilterSet):
|
||||
date_joined = DateFilter(field_name="date_joined", lookup_expr="date")
|
||||
|
||||
class Meta:
|
||||
model = User
|
||||
fields = {
|
||||
"name": ["exact", "icontains"],
|
||||
"email": ["exact", "icontains"],
|
||||
"company_name": ["exact", "icontains"],
|
||||
"date_joined": ["date", "gte", "lte"],
|
||||
"is_active": ["exact"],
|
||||
}
|
||||
|
||||
|
||||
class ComplianceOverviewFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
provider_type = ChoiceFilter(choices=Provider.ProviderChoices.choices)
|
||||
provider_type__in = ChoiceInFilter(choices=Provider.ProviderChoices.choices)
|
||||
scan_id = UUIDFilter(field_name="scan__id")
|
||||
|
||||
class Meta:
|
||||
model = ComplianceOverview
|
||||
fields = {
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"compliance_id": ["exact", "icontains"],
|
||||
"framework": ["exact", "iexact", "icontains"],
|
||||
"version": ["exact", "icontains"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
|
||||
|
||||
class ScanSummaryFilter(FilterSet):
|
||||
inserted_at = DateFilter(field_name="inserted_at", lookup_expr="date")
|
||||
provider_id = UUIDFilter(field_name="scan__provider__id", lookup_expr="exact")
|
||||
provider_type = ChoiceFilter(
|
||||
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
|
||||
)
|
||||
provider_type__in = ChoiceInFilter(
|
||||
field_name="scan__provider__provider", choices=Provider.ProviderChoices.choices
|
||||
)
|
||||
region = CharFilter(field_name="region")
|
||||
muted_findings = BooleanFilter(method="filter_muted_findings")
|
||||
|
||||
def filter_muted_findings(self, queryset, name, value):
|
||||
if not value:
|
||||
return queryset.exclude(muted__gt=0)
|
||||
return queryset
|
||||
|
||||
class Meta:
|
||||
model = ScanSummary
|
||||
fields = {
|
||||
"inserted_at": ["date", "gte", "lte"],
|
||||
"region": ["exact", "icontains", "in"],
|
||||
}
|
||||
28
api/src/backend/api/fixtures/dev/0_dev_users.json
Normal file
28
api/src/backend/api/fixtures/dev/0_dev_users.json
Normal file
@@ -0,0 +1,28 @@
|
||||
[
|
||||
{
|
||||
"model": "api.user",
|
||||
"pk": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
|
||||
"fields": {
|
||||
"password": "pbkdf2_sha256$720000$vA62S78kog2c2ytycVQdke$Fp35GVLLMyy5fUq3krSL9I02A+ocQ+RVa4S22LIAO5s=",
|
||||
"last_login": null,
|
||||
"name": "Devie Prowlerson",
|
||||
"email": "dev@prowler.com",
|
||||
"company_name": "Prowler Developers",
|
||||
"is_active": true,
|
||||
"date_joined": "2024-09-17T09:04:20.850Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.user",
|
||||
"pk": "b6493a3a-c997-489b-8b99-278bf74de9f6",
|
||||
"fields": {
|
||||
"password": "pbkdf2_sha256$720000$vA62S78kog2c2ytycVQdke$Fp35GVLLMyy5fUq3krSL9I02A+ocQ+RVa4S22LIAO5s=",
|
||||
"last_login": null,
|
||||
"name": "Devietoo Prowlerson",
|
||||
"email": "dev2@prowler.com",
|
||||
"company_name": "Prowler Developers",
|
||||
"is_active": true,
|
||||
"date_joined": "2024-09-18T09:04:20.850Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
50
api/src/backend/api/fixtures/dev/1_dev_tenants.json
Normal file
50
api/src/backend/api/fixtures/dev/1_dev_tenants.json
Normal file
@@ -0,0 +1,50 @@
|
||||
[
|
||||
{
|
||||
"model": "api.tenant",
|
||||
"pk": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"fields": {
|
||||
"inserted_at": "2024-03-21T23:00:00Z",
|
||||
"updated_at": "2024-03-21T23:00:00Z",
|
||||
"name": "Tenant1"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.tenant",
|
||||
"pk": "0412980b-06e3-436a-ab98-3c9b1d0333d3",
|
||||
"fields": {
|
||||
"inserted_at": "2024-03-21T23:00:00Z",
|
||||
"updated_at": "2024-03-21T23:00:00Z",
|
||||
"name": "Tenant2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.membership",
|
||||
"pk": "2b0db93a-7e0b-4edf-a851-ea448676b7eb",
|
||||
"fields": {
|
||||
"user": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
|
||||
"tenant": "0412980b-06e3-436a-ab98-3c9b1d0333d3",
|
||||
"role": "owner",
|
||||
"date_joined": "2024-09-19T11:03:59.712Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.membership",
|
||||
"pk": "797d7cee-abc9-4598-98bb-4bf4bfb97f27",
|
||||
"fields": {
|
||||
"user": "8b38e2eb-6689-4f1e-a4ba-95b275130200",
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"role": "owner",
|
||||
"date_joined": "2024-09-19T11:02:59.712Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.membership",
|
||||
"pk": "dea37563-7009-4dcf-9f18-25efb41462a7",
|
||||
"fields": {
|
||||
"user": "b6493a3a-c997-489b-8b99-278bf74de9f6",
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"role": "member",
|
||||
"date_joined": "2024-09-19T11:03:59.712Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
177
api/src/backend/api/fixtures/dev/2_dev_providers.json
Normal file
177
api/src/backend/api/fixtures/dev/2_dev_providers.json
Normal file
@@ -0,0 +1,177 @@
|
||||
[
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "37b065f8-26b0-4218-a665-0b23d07b27d9",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:20:27.050Z",
|
||||
"updated_at": "2024-08-01T17:20:27.050Z",
|
||||
"provider": "gcp",
|
||||
"uid": "a12322-test321",
|
||||
"alias": "gcp_testing_2",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:19:42.453Z",
|
||||
"updated_at": "2024-08-01T17:19:42.453Z",
|
||||
"provider": "gcp",
|
||||
"uid": "a12345-test123",
|
||||
"alias": "gcp_testing_1",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:19:09.556Z",
|
||||
"updated_at": "2024-08-01T17:19:09.556Z",
|
||||
"provider": "aws",
|
||||
"uid": "123456789020",
|
||||
"alias": "aws_testing_2",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "baa7b895-8bac-4f47-b010-4226d132856e",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:20:16.962Z",
|
||||
"updated_at": "2024-08-01T17:20:16.962Z",
|
||||
"provider": "gcp",
|
||||
"uid": "a12322-test123",
|
||||
"alias": "gcp_testing_3",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "d7c7ea89-d9af-423b-a364-1290dcad5a01",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-01T17:18:58.132Z",
|
||||
"updated_at": "2024-08-01T17:18:58.132Z",
|
||||
"provider": "aws",
|
||||
"uid": "123456789015",
|
||||
"alias": "aws_testing_1",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-06T16:03:26.176Z",
|
||||
"updated_at": "2024-08-06T16:03:26.176Z",
|
||||
"provider": "azure",
|
||||
"uid": "8851db6b-42e5-4533-aa9e-30a32d67e875",
|
||||
"alias": "azure_testing",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "26e55a24-cb2c-4cef-ac87-6f91fddb2c97",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-08-06T16:03:07.037Z",
|
||||
"updated_at": "2024-08-06T16:03:07.037Z",
|
||||
"provider": "kubernetes",
|
||||
"uid": "kubernetes-test-12345",
|
||||
"alias": "k8s_testing",
|
||||
"connected": null,
|
||||
"connection_last_checked_at": null,
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.provider",
|
||||
"pk": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:45:26.352Z",
|
||||
"updated_at": "2024-10-18T11:16:23.533Z",
|
||||
"provider": "aws",
|
||||
"uid": "106908755759",
|
||||
"alias": "real testing aws provider",
|
||||
"connected": true,
|
||||
"connection_last_checked_at": "2024-10-18T11:16:23.503Z",
|
||||
"metadata": {},
|
||||
"scanner_args": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providersecret",
|
||||
"pk": "11491b47-75ae-4f71-ad8d-3e630a72182e",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-11T08:03:05.026Z",
|
||||
"updated_at": "2024-10-11T08:04:47.033Z",
|
||||
"name": "GCP static secrets",
|
||||
"secret_type": "static",
|
||||
"_secret": "Z0FBQUFBQm5DTndmZW9KakRZUHM2UHhQN2V3RzN0QmM1cERham8yMHp5cnVTT0lzdGFyS1FuVmJXUlpYSGsyU0cxR3RMMEdQYXlYMUVsaWtqLU1OZWlaVUp6OFREYlotZTVBY3BuTlZYbm9YcUJydzAxV2p5dkpLamI1Y2tUYzA0MmJUNWxsNTBRM0E1SDRCa0pPQWVlb05YU3dfeUhkLTRmOEh3dGczOGh1ZGhQcVdZdVAtYmtoSWlwNXM4VGFoVmF3dno2X1hrbk5GZjZTWjVuWEdEZUFXeHJSQjEzbTlVakhNdzYyWTdiVEpvUEc2MTNpRzUtczhEank1eGI0b3MyMlAyaGN6dlByZmtUWHByaDNUYWFqYS1tYnNBUkRKTzBacFNSRjFuVmd5bUtFUEJhd1ZVS1ZDd2xSUV9PaEtLTnc0XzVkY2lhM01WTjQwaWdJSk9wNUJSXzQ4RUNQLXFPNy1VdzdPYkZyWkVkU3RyQjVLTS1MVHN0R3k4THNKZ2NBNExaZnl3Q1EwN2dwNGRsUXptMjB0LXUzTUpzTDE2Q1hmS0ZSN2g1ZjBPeV8taFoxNUwxc2FEcktXX0dCM1IzeUZTTHNiTmNxVXBvNWViZTJScUVWV2VYTFQ4UHlid21PY1A0UjdNMGtERkZCd0lLMlJENDMzMVZUM09DQ0twd1N3VHlZd09XLUctOWhYcFJIR1p5aUlZeEUzejc2dWRYdGNsd0xOODNqRUFEczhSTWNtWU0tdFZ1ZTExaHNHUVYtd0Zxdld1LTdKVUNINzlZTGdHODhKeVVpQmRZMHRUNTJRRWhwS1F1Y3I2X2Iwc0c1NHlXSVRLZWxreEt0dVRnOTZFMkptU2VMS1dWXzdVOVRzMUNUWXM2aFlxVDJXdGo3d2cxSVZGWlI2ZWhIZzZBcEl4bEJ6UnVHc0RYWVNHcjFZUHI5ZUYyWG9rSlo0QUVSUkFCX3h2UmtJUTFzVXJUZ25vTmk2VzdoTTNta05ucmNfTi0yR1ZxN1E2MnZJOVVKOGxmMXMzdHMxVndmSVhQbUItUHgtMVpVcHJwMU5JVHJLb0Y1aHV5OEEwS0kzQkEtcFJkdkRnWGxmZnprNFhndWg1TmQyd09yTFdTRmZ3d2ZvZFUtWXp4a2VYb3JjckFIcE13MDUzX0RHSnlzM0N2ZE5IRzJzMXFMc0k4MDRyTHdLZFlWOG9SaFF0LU43Ynd6VFlEcVNvdFZ0emJEVk10aEp4dDZFTFNFNzk0UUo2WTlVLWRGYm1fanZHaFZreHBIMmtzVjhyS0xPTk9fWHhiVTJHQXZwVlVuY3JtSjFUYUdHQzhEaHFNZXhwUHBmY0kxaUVrOHo4a0FYOTdpZVJDbFRvdFlQeWo3eFZHX1ZMZ1Myc3prU3o2c3o2eXNja1U4N0Y1T0d1REVjZFRGNTByUkgyemVCSjlQYkY2bmJ4YTZodHB0cUNzd2xZcENycUdsczBIaEZPbG1jVUlqNlM2cEE3aGpVaWswTzBDLVFGUHM5UHhvM09saWNtaDhaNVlsc3FZdktKeWlheDF5OGhTODE2N3JWamdTZG5Fa3JSQ2ZUSEVfRjZOZXdreXRZLTBZRFhleVFFeC1YUzc0cWhYeEhobGxvdnZ3Rm15WFlBWXp0dm1DeTA5eExLeEFRRXVRSXBXdTNEaWdZZ3JDenItdDhoZlFiTzI0SGZ1c01FR1FNaFVweVBKR1YxWGRUMW1Mc2JVdW9raWR6UHk2ZTBnS05pV3oyZVBjREdkY3k4ZHZPUWE5S281MkJRSHF3NnpTclZ5bl90bk1wUEh6Tkp5dXlDcE5paWRqcVhxRFVObWIzRldWOGJ2aC1CRHZpbFZrb0hjNGpCMm5POGRiS2lETUpMLUVfQlhCdTZPLW9USW1LTFlTSF9zRUJYZ1NKeFFEQjNOR215ZXJDbkFndmcxWl9rWlk9",
|
||||
"provider": "8851db6b-42e5-4533-aa9e-30a32d67e875"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providersecret",
|
||||
"pk": "40191ad5-d8c2-40a9-826d-241397626b68",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-10T11:11:44.515Z",
|
||||
"updated_at": "2024-10-11T07:59:56.102Z",
|
||||
"name": "AWS static secrets",
|
||||
"secret_type": "static",
|
||||
"_secret": "Z0FBQUFBQm5DTnI4Y1RyV19UWEJzc3kzQUExcU5tdlQzbFVLeDdZMWd1MzkwWkl2UF9oZGhiVEJHVWpSMXV4MjYyN3g2OVpvNVpkQUQ3S0VGaGdQLTFhQWE3MkpWZUt2cnVhODc4d3FpY3FVZkpwdHJzNUJPeFRwZ3N4bGpPZTlkNWRNdFlwTHU3aTNWR3JjSzJwLWRITHdfQWpXb1F0c1l3bVFxbnFrTEpPTGgxcnF1VUprSzZ5dGRQU2VGYmZhTTlwbVpsNFBNWlFhVW9RbjJyYnZ5N0oweE5kV0ZEaUdpUUpNVExOa3oyQ2dNREVSenJ0TEFZc0RrRWpXNUhyMmtybGNLWDVOR0FabEl4QVR1bkZyb2hBLWc1MFNIekVyeXI0SmVreHBjRnJ1YUlVdXpVbW9JZkk0aEgxYlM1VGhSRlhtcS14YzdTYUhXR2xodElmWjZuNUVwaHozX1RVTG1QWHdPZWd4clNHYnAyOTBsWEl5UU83RGxZb0RKWjdadjlsTmJtSHQ0Yl9uaDJoODB0QV9sWmFYbFAxcjA1bmhNVlNqc2xEeHlvcUJFbVZvY250ZENnMnZLT1psb1JDclB3WVR6NGdZb2pzb3U4Ny04QlB0UTZub0dMOXZEUTZEcVJhZldCWEZZSDdLTy02UVZqck5zVTZwS3pObGlOejNJeHUzbFRabFM2V2xaekZVRjZtX3VzZlplendnOWQzT01WMFd3ejNadHVlTFlqRGR2dk5Da29zOFYwOUdOaEc4OHhHRnJFMmJFMk12VDNPNlBBTGlsXy13cUM1QkVYb0o1Z2U4ZXJnWXpZdm1sWjA5bzQzb2NFWC1xbmIycGZRbGtCaGNaOWlkX094UUNNampwbkZoREctNWI4QnZRaE8zM3BEQ1BwNzA1a3BzOGczZXdIM2s1NHFGN1ZTbmJhZkc4RVdfM0ZIZU5udTBYajd1RGxpWXZpRWdSMmhHa2RKOEIzbmM0X2F1OGxrN2p6LW9UVldDOFVpREoxZ1UzcTBZX19OQ0xJb0syWlhNSlQ4MzQwdzRtVG94Y01GS3FMLV95UVlxOTFORk8zdjE5VGxVaXdhbGlzeHdoYWNzazZWai1GUGtUM2gzR0ZWTTY4SThWeVFnZldIaklOTTJqTTg1VkhEYW5wNmdEVllXMmJCV2tpVmVYeUV2c0E1T00xbHJRNzgzVG9wb0Q1cV81UEhqYUFsQ2p1a0VpRDVINl9SVkpyZVRNVnVXQUxwY3NWZnJrNmRVREpiLWNHYUpXWmxkQlhNbWhuR1NmQ1BaVDlidUxCWHJMaHhZbk1FclVBaEVZeWg1ZlFoenZzRHlKbV8wa3lmMGZrd3NmTDZjQkE0UXNSUFhpTWtUUHBrX29BVzc4QzEtWEJIQW1GMGFuZVlXQWZIOXJEamloeGFCeHpYMHNjMFVfNXpQdlJfSkk2bzFROU5NU0c1SHREWW1nbkFNZFZ0UjdPRGdjaF96RGplY1hjdFFzLVR6MTVXYlRjbHIxQ2JRejRpVko5NWhBU0ZHR3ZvczU5elljRGpHRTdIc0FsSm5fUHEwT1gtTS1lN3M3X3ZZRnlkYUZoZXRQeEJsZlhLdFdTUzU1NUl4a29aOWZIdTlPM0Fnak1xYWVkYTNiMmZXUHlXS2lwUVBZLXQyaUxuRmtQNFFieE9SVmdZVW9WTHlzbnBPZlNIdGVHOE1LNVNESjN3cGtVSHVpT1NJWHE1ZzNmUTVTOC0xX3NGSmJqU19IbjZfQWtMRG1YNUQtRy13TUJIZFlyOXJkQzFQbkdZVXVzM2czbS1HWHFBT1pXdVd3N09tcG82SVhnY1ZtUWxqTEg2UzJCUmllb2pweVN2aGwwS1FVRUhjNEN2amRMc3MwVU4zN3dVMWM5Slg4SERtenFaQk1yMWx0LWtxVWtLZVVtbU4yejVEM2h6TEt0RGdfWE09",
|
||||
"provider": "b85601a8-4b45-4194-8135-03fb980ef428"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providersecret",
|
||||
"pk": "ed89d1ea-366a-4d12-a602-f2ab77019742",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-10T11:11:44.515Z",
|
||||
"updated_at": "2024-10-11T07:59:56.102Z",
|
||||
"name": "Azure static secrets",
|
||||
"secret_type": "static",
|
||||
"_secret": "Z0FBQUFBQm5DTnI4Y1RyV19UWEJzc3kzQUExcU5tdlQzbFVLeDdZMWd1MzkwWkl2UF9oZGhiVEJHVWpSMXV4MjYyN3g2OVpvNVpkQUQ3S0VGaGdQLTFhQWE3MkpWZUt2cnVhODc4d3FpY3FVZkpwdHJzNUJPeFRwZ3N4bGpPZTlkNWRNdFlwTHU3aTNWR3JjSzJwLWRITHdfQWpXb1F0c1l3bVFxbnFrTEpPTGgxcnF1VUprSzZ5dGRQU2VGYmZhTTlwbVpsNFBNWlFhVW9RbjJyYnZ5N0oweE5kV0ZEaUdpUUpNVExOa3oyQ2dNREVSenJ0TEFZc0RrRWpXNUhyMmtybGNLWDVOR0FabEl4QVR1bkZyb2hBLWc1MFNIekVyeXI0SmVreHBjRnJ1YUlVdXpVbW9JZkk0aEgxYlM1VGhSRlhtcS14YzdTYUhXR2xodElmWjZuNUVwaHozX1RVTG1QWHdPZWd4clNHYnAyOTBsWEl5UU83RGxZb0RKWjdadjlsTmJtSHQ0Yl9uaDJoODB0QV9sWmFYbFAxcjA1bmhNVlNqc2xEeHlvcUJFbVZvY250ZENnMnZLT1psb1JDclB3WVR6NGdZb2pzb3U4Ny04QlB0UTZub0dMOXZEUTZEcVJhZldCWEZZSDdLTy02UVZqck5zVTZwS3pObGlOejNJeHUzbFRabFM2V2xaekZVRjZtX3VzZlplendnOWQzT01WMFd3ejNadHVlTFlqRGR2dk5Da29zOFYwOUdOaEc4OHhHRnJFMmJFMk12VDNPNlBBTGlsXy13cUM1QkVYb0o1Z2U4ZXJnWXpZdm1sWjA5bzQzb2NFWC1xbmIycGZRbGtCaGNaOWlkX094UUNNampwbkZoREctNWI4QnZRaE8zM3BEQ1BwNzA1a3BzOGczZXdIM2s1NHFGN1ZTbmJhZkc4RVdfM0ZIZU5udTBYajd1RGxpWXZpRWdSMmhHa2RKOEIzbmM0X2F1OGxrN2p6LW9UVldDOFVpREoxZ1UzcTBZX19OQ0xJb0syWlhNSlQ4MzQwdzRtVG94Y01GS3FMLV95UVlxOTFORk8zdjE5VGxVaXdhbGlzeHdoYWNzazZWai1GUGtUM2gzR0ZWTTY4SThWeVFnZldIaklOTTJqTTg1VkhEYW5wNmdEVllXMmJCV2tpVmVYeUV2c0E1T00xbHJRNzgzVG9wb0Q1cV81UEhqYUFsQ2p1a0VpRDVINl9SVkpyZVRNVnVXQUxwY3NWZnJrNmRVREpiLWNHYUpXWmxkQlhNbWhuR1NmQ1BaVDlidUxCWHJMaHhZbk1FclVBaEVZeWg1ZlFoenZzRHlKbV8wa3lmMGZrd3NmTDZjQkE0UXNSUFhpTWtUUHBrX29BVzc4QzEtWEJIQW1GMGFuZVlXQWZIOXJEamloeGFCeHpYMHNjMFVfNXpQdlJfSkk2bzFROU5NU0c1SHREWW1nbkFNZFZ0UjdPRGdjaF96RGplY1hjdFFzLVR6MTVXYlRjbHIxQ2JRejRpVko5NWhBU0ZHR3ZvczU5elljRGpHRTdIc0FsSm5fUHEwT1gtTS1lN3M3X3ZZRnlkYUZoZXRQeEJsZlhLdFdTUzU1NUl4a29aOWZIdTlPM0Fnak1xYWVkYTNiMmZXUHlXS2lwUVBZLXQyaUxuRmtQNFFieE9SVmdZVW9WTHlzbnBPZlNIdGVHOE1LNVNESjN3cGtVSHVpT1NJWHE1ZzNmUTVTOC0xX3NGSmJqU19IbjZfQWtMRG1YNUQtRy13TUJIZFlyOXJkQzFQbkdZVXVzM2czbS1HWHFBT1pXdVd3N09tcG82SVhnY1ZtUWxqTEg2UzJCUmllb2pweVN2aGwwS1FVRUhjNEN2amRMc3MwVU4zN3dVMWM5Slg4SERtenFaQk1yMWx0LWtxVWtLZVVtbU4yejVEM2h6TEt0RGdfWE09",
|
||||
"provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providersecret",
|
||||
"pk": "ae48ecde-75cd-4814-92ab-18f48719e5d9",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:45:26.412Z",
|
||||
"updated_at": "2024-10-18T10:45:26.412Z",
|
||||
"name": "Valid AWS Credentials",
|
||||
"secret_type": "static",
|
||||
"_secret": "Z0FBQUFBQm5FanhHa3dXS0I3M2NmWm56SktiaGNqdDZUN0xQU1QwUi15QkhLZldFUmRENk1BXzlscG9JSUxVSTF5ekxuMkdEanlJNjhPUS1VSV9wVTBvU2l4ZnNGOVJhYW93RC1LTEhmc2pyOTJvUWwyWnpFY19WN1pRQk5IdDYwYnBDQnF1eU9nUzdwTGU3QU5qMGFyX1E4SXdpSk9paGVLcVpOVUhwb3duaXgxZ0ZxME5Pcm40QzBGWEZKY2lmRVlCMGFuVFVzemxuVjVNalZVQ2JsY2ZqNWt3Z01IYUZ0dk92YkdtSUZ5SlBvQWZoVU5DWlRFWmExNnJGVEY4Q1Bnd2VJUW9TSWdRcG9rSDNfREQwRld3Q1RYVnVYWVJLWWIxZmpsWGpwd0xQM0dtLTlYUjdHOVhhNklLWXFGTHpFQUVyVmNhYW9CU0tocGVyX3VjMkVEcVdjdFBfaVpsLTBzaUxrWTlta3dpelNtTG9xYVhBUHUzNUE4RnI1WXdJdHcxcFVfaG1XRHhDVFBKamxJb1FaQ2lsQ3FzRmxZbEJVemVkT1E2aHZfbDJqWDJPT3ViOWJGYzQ3eTNWNlFQSHBWRDFiV2tneDM4SmVqMU9Bd01TaXhPY2dmWG5RdENURkM2b2s5V3luVUZQcnFKNldnWEdYaWE2MnVNQkEwMHd6cUY5cVJkcGw4bHBtNzhPeHhkREdwSXNEc1JqQkxUR1FYRTV0UFNwbVlVSWF5LWgtbVhJZXlPZ0Q4cG9HX2E0Qld0LTF1TTFEVy1XNGdnQTRpLWpQQmFJUEdaOFJGNDVoUVJnQ25YVU5DTENMaTY4YmxtYWJFRERXTjAydVN2YnBDb3RkUE0zSDRlN1A3TXc4d2h1Wmd0LWUzZEcwMUstNUw2YnFyS2Z0NEVYMXllQW5GLVBpeU55SkNhczFIeFhrWXZpVXdwSFVrTDdiQjQtWHZJdERXVThzSnJsT2FNZzJDaUt6Y2NXYUZhUlo3VkY0R1BrSHNHNHprTmxjYmp1TXVKakRha0VtNmRFZWRmZHJWdnRCOVNjVGFVWjVQM3RwWWl4SkNmOU1pb2xqMFdOblhNY3Y3aERpOHFlWjJRc2dtRDkzZm1Qc29wdk5OQmJPbGk5ZUpGM1I2YzRJN2gxR3FEMllXR1pma1k0emVqSjZyMUliMGZsc3NfSlVDbGt4QzJTc3hHOU9FRHlZb09zVnlvcDR6WC1uclRSenI0Yy13WlFWNzJWRkwydjhmSjFZdnZ5X3NmZVF6UWRNMXo5STVyV3B0d09UUlFtOURITGhXSDVIUl9zYURJc05KWUNxekVyYkxJclNFNV9leEk4R2xsMGJod3lYeFIwaXR2dllwLTZyNWlXdDRpRkxVYkxWZFdvYUhKck5aeElBZUtKejNKS2tYVW1rTnVrRjJBQmdlZmV6ckozNjNwRmxLS1FaZzRVTTBZYzFFYi1idjBpZkQ3bWVvbEdRZXJrWFNleWZmSmFNdG1wQlp0YmxjWDV5T0tEbHRsYnNHbjRPRjl5MkttOUhRWlJtd1pmTnY4Z1lPRlZoTzFGVDdTZ0RDY1ByV0RndTd5LUNhcHNXUnNIeXdLMEw3WS1tektRTWFLQy1zakpMLWFiM3FOakE1UWU4LXlOX2VPbmd4MTZCRk9OY3Z4UGVDSWxhRlg4eHI4X1VUTDZZM0pjV0JDVi1UUjlTUl85cm1LWlZ0T1dzU0lpdWUwbXgtZ0l6eHNSNExRTV9MczJ6UkRkVElnRV9Rc0RoTDFnVHRZSEFPb2paX200TzZiRzVmRE5hOW5CTjh5Qi1WaEtueEpqRzJDY1luVWZtX1pseUpQSE5lQ0RrZ05EbWo5cU9MZ0ZkcXlqUll4UUkyejRfY2p4RXdEeC1PS1JIQVNUcmNIdkRJbzRiUktMWEQxUFM3aGNzeVFWUDdtcm5xNHlOYUU9",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555"
|
||||
}
|
||||
}
|
||||
]
|
||||
218
api/src/backend/api/fixtures/dev/3_dev_scans.json
Normal file
218
api/src/backend/api/fixtures/dev/3_dev_scans.json
Normal file
@@ -0,0 +1,218 @@
|
||||
[
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "0191e280-9d2f-71c8-9b18-487a23ba185e",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "37b065f8-26b0-4218-a665-0b23d07b27d9",
|
||||
"trigger": "manual",
|
||||
"name": "test scan 1",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 1,
|
||||
"duration": 5,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
},
|
||||
"inserted_at": "2024-09-01T17:25:27.050Z",
|
||||
"started_at": "2024-09-01T17:25:27.050Z",
|
||||
"updated_at": "2024-09-01T17:25:27.050Z",
|
||||
"completed_at": "2024-09-01T17:25:32.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01920573-aa9c-73c9-bcda-f2e35c9b19d2",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"trigger": "manual",
|
||||
"name": "test aws scan 2",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 1,
|
||||
"duration": 20,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
},
|
||||
"inserted_at": "2024-09-02T17:24:27.050Z",
|
||||
"started_at": "2024-09-02T17:24:27.050Z",
|
||||
"updated_at": "2024-09-02T17:24:27.050Z",
|
||||
"completed_at": "2024-09-01T17:24:37.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01920573-ea5b-77fd-a93f-1ed2ae12f728",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "baa7b895-8bac-4f47-b010-4226d132856e",
|
||||
"trigger": "manual",
|
||||
"name": "test gcp scan",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 10,
|
||||
"duration": 10,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudsql_instance_automated_backups"
|
||||
]
|
||||
},
|
||||
"inserted_at": "2024-09-02T19:26:27.050Z",
|
||||
"started_at": "2024-09-02T19:26:27.050Z",
|
||||
"updated_at": "2024-09-02T19:26:27.050Z",
|
||||
"completed_at": "2024-09-01T17:26:37.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01920573-ea5b-77fd-a93f-1ed2ae12f728",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"trigger": "manual",
|
||||
"name": "test aws scan",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 1,
|
||||
"duration": 35,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
},
|
||||
"inserted_at": "2024-09-02T19:27:27.050Z",
|
||||
"started_at": "2024-09-02T19:27:27.050Z",
|
||||
"updated_at": "2024-09-02T19:27:27.050Z",
|
||||
"completed_at": "2024-09-01T17:27:37.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "c281c924-23f3-4fcc-ac63-73a22154b7de",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"trigger": "scheduled",
|
||||
"name": "test scheduled aws scan",
|
||||
"state": "available",
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudformation_stack_outputs_find_secrets"
|
||||
]
|
||||
},
|
||||
"scheduled_at": "2030-09-02T19:20:27.050Z",
|
||||
"inserted_at": "2024-09-02T19:24:27.050Z",
|
||||
"updated_at": "2024-09-02T19:24:27.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "25c8907c-b26e-4ec0-966b-a1f53a39d8e6",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "b85601a8-4b45-4194-8135-03fb980ef428",
|
||||
"trigger": "scheduled",
|
||||
"name": "test scheduled aws scan 2",
|
||||
"state": "available",
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled",
|
||||
"cloudformation_stack_outputs_find_secrets"
|
||||
]
|
||||
},
|
||||
"scheduled_at": "2030-08-02T19:31:27.050Z",
|
||||
"inserted_at": "2024-09-02T19:38:27.050Z",
|
||||
"updated_at": "2024-09-02T19:38:27.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "25c8907c-b26e-4ec0-966b-a1f53a39d8e6",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "baa7b895-8bac-4f47-b010-4226d132856e",
|
||||
"trigger": "scheduled",
|
||||
"name": "test scheduled gcp scan",
|
||||
"state": "available",
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"cloudsql_instance_automated_backups",
|
||||
"iam_audit_logs_enabled"
|
||||
]
|
||||
},
|
||||
"scheduled_at": "2030-07-02T19:30:27.050Z",
|
||||
"inserted_at": "2024-09-02T19:29:27.050Z",
|
||||
"updated_at": "2024-09-02T19:29:27.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "25c8907c-b26e-4ec0-966b-a1f53a39d8e6",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
|
||||
"trigger": "scheduled",
|
||||
"name": "test scheduled azure scan",
|
||||
"state": "available",
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"aks_cluster_rbac_enabled",
|
||||
"defender_additional_email_configured_with_a_security_contact"
|
||||
]
|
||||
},
|
||||
"scheduled_at": "2030-08-05T19:32:27.050Z",
|
||||
"inserted_at": "2024-09-02T19:29:27.050Z",
|
||||
"updated_at": "2024-09-02T19:29:27.050Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01929f3b-ed2e-7623-ad63-7c37cd37828f",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "real scan 1",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"trigger": "manual",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 19,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled"
|
||||
]
|
||||
},
|
||||
"duration": 7,
|
||||
"scheduled_at": null,
|
||||
"inserted_at": "2024-10-18T10:45:57.678Z",
|
||||
"updated_at": "2024-10-18T10:46:05.127Z",
|
||||
"started_at": "2024-10-18T10:45:57.909Z",
|
||||
"completed_at": "2024-10-18T10:46:05.127Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.scan",
|
||||
"pk": "01929f57-c0ee-7553-be0b-cbde006fb6f7",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"name": "real scan 2",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"trigger": "manual",
|
||||
"state": "completed",
|
||||
"unique_resource_count": 20,
|
||||
"progress": 100,
|
||||
"scanner_args": {
|
||||
"checks_to_execute": [
|
||||
"accessanalyzer_enabled",
|
||||
"account_security_contact_information_is_registered"
|
||||
]
|
||||
},
|
||||
"duration": 4,
|
||||
"scheduled_at": null,
|
||||
"inserted_at": "2024-10-18T11:16:21.358Z",
|
||||
"updated_at": "2024-10-18T11:16:26.060Z",
|
||||
"started_at": "2024-10-18T11:16:21.593Z",
|
||||
"completed_at": "2024-10-18T11:16:26.060Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
322
api/src/backend/api/fixtures/dev/4_dev_resources.json
Normal file
322
api/src/backend/api/fixtures/dev/4_dev_resources.json
Normal file
@@ -0,0 +1,322 @@
|
||||
[
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "0234477d-0b8e-439f-87d3-ce38dff3a434",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.772Z",
|
||||
"updated_at": "2024-10-18T11:16:24.466Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root",
|
||||
"name": "",
|
||||
"region": "eu-south-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'eu':7C 'eu-south':6C 'iam':3A 'other':11 'root':5A 'south':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "17ce30a3-6e77-42a5-bb08-29dfcad7396a",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.882Z",
|
||||
"updated_at": "2024-10-18T11:16:24.533Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root2",
|
||||
"name": "",
|
||||
"region": "eu-west-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'eu':7C 'eu-west':6C 'iam':3A 'other':11 'root':5A 'west':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "1f9de587-ba5b-415a-b9b0-ceed4c6c9f32",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.091Z",
|
||||
"updated_at": "2024-10-18T11:16:24.637Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root3",
|
||||
"name": "",
|
||||
"region": "ap-northeast-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'ap':7C 'ap-northeast':6C 'arn':1A 'aws':2A 'iam':3A 'northeast':8C 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "29b35668-6dad-411d-bfec-492311889892",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.008Z",
|
||||
"updated_at": "2024-10-18T11:16:24.600Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root4",
|
||||
"name": "",
|
||||
"region": "us-west-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'iam':3A 'other':11 'root':5A 'us':7C 'us-west':6C 'west':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "30505514-01d4-42bb-8b0c-471bbab27460",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T11:16:26.014Z",
|
||||
"updated_at": "2024-10-18T11:16:26.023Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root5",
|
||||
"name": "",
|
||||
"region": "us-east-1",
|
||||
"service": "account",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'account':10 'arn':1A 'aws':2A 'east':8C 'iam':3A 'other':11 'root':5A 'us':7C 'us-east':6C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "372932f0-e4df-4968-9721-bb4f6236fae4",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.848Z",
|
||||
"updated_at": "2024-10-18T11:16:24.516Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root6",
|
||||
"name": "",
|
||||
"region": "eu-west-3",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'3':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'eu':7C 'eu-west':6C 'iam':3A 'other':11 'root':5A 'west':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "3a37d124-7637-43f6-9df7-e9aa7ef98c53",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.979Z",
|
||||
"updated_at": "2024-10-18T11:16:24.585Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root7",
|
||||
"name": "",
|
||||
"region": "sa-east-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'east':8C 'iam':3A 'other':11 'root':5A 'sa':7C 'sa-east':6C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "3c49318e-03c6-4f12-876f-40451ce7de3d",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.072Z",
|
||||
"updated_at": "2024-10-18T11:16:24.630Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root8",
|
||||
"name": "",
|
||||
"region": "ap-southeast-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'ap':7C 'ap-southeast':6C 'arn':1A 'aws':2A 'iam':3A 'other':11 'root':5A 'southeast':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "430bf313-8733-4bc5-ac70-5402adfce880",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.994Z",
|
||||
"updated_at": "2024-10-18T11:16:24.593Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root9",
|
||||
"name": "",
|
||||
"region": "eu-north-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'eu':7C 'eu-north':6C 'iam':3A 'north':8C 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "78bd2a52-82f9-45df-90a9-4ad78254fdc4",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.055Z",
|
||||
"updated_at": "2024-10-18T11:16:24.622Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root10",
|
||||
"name": "",
|
||||
"region": "ap-northeast-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'ap':7C 'ap-northeast':6C 'arn':1A 'aws':2A 'iam':3A 'northeast':8C 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "7973e332-795e-4a74-b4d4-a53a21c98c80",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.896Z",
|
||||
"updated_at": "2024-10-18T11:16:24.542Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root11",
|
||||
"name": "",
|
||||
"region": "us-east-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'east':8C 'iam':3A 'other':11 'root':5A 'us':7C 'us-east':6C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "8ca0a188-5699-436e-80fd-e566edaeb259",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.938Z",
|
||||
"updated_at": "2024-10-18T11:16:24.565Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root12",
|
||||
"name": "",
|
||||
"region": "ca-central-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'ca':7C 'ca-central':6C 'central':8C 'iam':3A 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "8fe4514f-71d7-46ab-b0dc-70cef23b4d13",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.965Z",
|
||||
"updated_at": "2024-10-18T11:16:24.578Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root13",
|
||||
"name": "",
|
||||
"region": "eu-west-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'eu':7C 'eu-west':6C 'iam':3A 'other':11 'root':5A 'west':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "9ab35225-dc7c-4ebd-bbc0-d81fb5d9de77",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.909Z",
|
||||
"updated_at": "2024-10-18T11:16:24.549Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root14",
|
||||
"name": "",
|
||||
"region": "ap-south-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'ap':7C 'ap-south':6C 'arn':1A 'aws':2A 'iam':3A 'other':11 'root':5A 'south':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "9be26c1d-adf0-4ba8-9ca9-c740f4a0dc4e",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.863Z",
|
||||
"updated_at": "2024-10-18T11:16:24.524Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root15",
|
||||
"name": "",
|
||||
"region": "eu-central-2",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'2':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'central':8C 'eu':7C 'eu-central':6C 'iam':3A 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "ba108c01-bcad-44f1-b211-c1d8985da89d",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.110Z",
|
||||
"updated_at": "2024-10-18T11:16:24.644Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root16",
|
||||
"name": "",
|
||||
"region": "ap-northeast-3",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'3':9C '112233445566':4A 'accessanalyzer':10 'ap':7C 'ap-northeast':6C 'arn':1A 'aws':2A 'iam':3A 'northeast':8C 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "dc6cfb5d-6835-4c7b-9152-c18c734a6eaa",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.038Z",
|
||||
"updated_at": "2024-10-18T11:16:24.615Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root17",
|
||||
"name": "",
|
||||
"region": "eu-central-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'central':8C 'eu':7C 'eu-central':6C 'iam':3A 'other':11 'root':5A"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "e0664164-cfda-44a4-b743-acee1c69386c",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.924Z",
|
||||
"updated_at": "2024-10-18T11:16:24.557Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root18",
|
||||
"name": "",
|
||||
"region": "us-west-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'arn':1A 'aws':2A 'iam':3A 'other':11 'root':5A 'us':7C 'us-west':6C 'west':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "e1929daa-a984-4116-8131-492a48321dba",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:05.023Z",
|
||||
"updated_at": "2024-10-18T11:16:24.607Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:iam::112233445566:root19",
|
||||
"name": "",
|
||||
"region": "ap-southeast-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9C '112233445566':4A 'accessanalyzer':10 'ap':7C 'ap-southeast':6C 'arn':1A 'aws':2A 'iam':3A 'other':11 'root':5A 'southeast':8C"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.resource",
|
||||
"pk": "e37bb1f1-1669-4bb3-be86-e3378ddfbcba",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"inserted_at": "2024-10-18T10:46:04.952Z",
|
||||
"updated_at": "2024-10-18T11:16:24.571Z",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"uid": "arn:aws:access-analyzer:us-east-1:112233445566:analyzer/ConsoleAnalyzer-83b66ad7-d024-454e-b851-52d11cc1cf7c",
|
||||
"name": "",
|
||||
"region": "us-east-1",
|
||||
"service": "accessanalyzer",
|
||||
"type": "Other",
|
||||
"text_search": "'1':9A,15C '112233445566':10A 'access':4A 'access-analyzer':3A 'accessanalyzer':16 'analyzer':5A 'analyzer/consoleanalyzer-83b66ad7-d024-454e-b851-52d11cc1cf7c':11A 'arn':1A 'aws':2A 'east':8A,14C 'other':17 'us':7A,13C 'us-east':6A,12C"
|
||||
}
|
||||
}
|
||||
]
|
||||
2498
api/src/backend/api/fixtures/dev/5_dev_findings.json
Normal file
2498
api/src/backend/api/fixtures/dev/5_dev_findings.json
Normal file
File diff suppressed because it is too large
Load Diff
62
api/src/backend/api/fixtures/dev/6_dev_rbac.json
Normal file
62
api/src/backend/api/fixtures/dev/6_dev_rbac.json
Normal file
@@ -0,0 +1,62 @@
|
||||
[
|
||||
{
|
||||
"model": "api.providergroup",
|
||||
"pk": "3fe28fb8-e545-424c-9b8f-69aff638f430",
|
||||
"fields": {
|
||||
"name": "first_group",
|
||||
"inserted_at": "2024-11-13T11:36:19.503Z",
|
||||
"updated_at": "2024-11-13T11:36:19.503Z",
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providergroup",
|
||||
"pk": "525e91e7-f3f3-4254-bbc3-27ce1ade86b1",
|
||||
"fields": {
|
||||
"name": "second_group",
|
||||
"inserted_at": "2024-11-13T11:36:25.421Z",
|
||||
"updated_at": "2024-11-13T11:36:25.421Z",
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providergroup",
|
||||
"pk": "481769f5-db2b-447b-8b00-1dee18db90ec",
|
||||
"fields": {
|
||||
"name": "third_group",
|
||||
"inserted_at": "2024-11-13T11:36:37.603Z",
|
||||
"updated_at": "2024-11-13T11:36:37.603Z",
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providergroupmembership",
|
||||
"pk": "13625bd3-f428-4021-ac1b-b0bd41b6e02f",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "1b59e032-3eb6-4694-93a5-df84cd9b3ce2",
|
||||
"provider_group": "3fe28fb8-e545-424c-9b8f-69aff638f430",
|
||||
"inserted_at": "2024-11-13T11:55:17.138Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providergroupmembership",
|
||||
"pk": "54784ebe-42d2-4937-aa6a-e21c62879567",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"provider_group": "3fe28fb8-e545-424c-9b8f-69aff638f430",
|
||||
"inserted_at": "2024-11-13T11:55:17.138Z"
|
||||
}
|
||||
},
|
||||
{
|
||||
"model": "api.providergroupmembership",
|
||||
"pk": "c8bd52d5-42a5-48fe-8e0a-3eef154b8ebe",
|
||||
"fields": {
|
||||
"tenant": "12646005-9067-4d2a-a098-8bb378604362",
|
||||
"provider": "15fce1fa-ecaa-433f-a9dc-62553f3a2555",
|
||||
"provider_group": "525e91e7-f3f3-4254-bbc3-27ce1ade86b1",
|
||||
"inserted_at": "2024-11-13T11:55:41.237Z"
|
||||
}
|
||||
}
|
||||
]
|
||||
1
api/src/backend/api/fixtures/dev/7_dev_compliance.json
Normal file
1
api/src/backend/api/fixtures/dev/7_dev_compliance.json
Normal file
File diff suppressed because one or more lines are too long
49
api/src/backend/api/middleware.py
Normal file
49
api/src/backend/api/middleware.py
Normal file
@@ -0,0 +1,49 @@
|
||||
import logging
|
||||
import time
|
||||
|
||||
from config.custom_logging import BackendLogger
|
||||
|
||||
|
||||
def extract_auth_info(request) -> dict:
|
||||
if getattr(request, "auth", None) is not None:
|
||||
tenant_id = request.auth.get("tenant_id", "N/A")
|
||||
user_id = request.auth.get("sub", "N/A")
|
||||
else:
|
||||
tenant_id, user_id = "N/A", "N/A"
|
||||
return {"tenant_id": tenant_id, "user_id": user_id}
|
||||
|
||||
|
||||
class APILoggingMiddleware:
|
||||
"""
|
||||
Middleware for logging API requests.
|
||||
|
||||
This middleware logs details of API requests, including the typical request metadata among other useful information.
|
||||
|
||||
Args:
|
||||
get_response (Callable): A callable to get the response, typically the next middleware or view.
|
||||
"""
|
||||
|
||||
def __init__(self, get_response):
|
||||
self.get_response = get_response
|
||||
self.logger = logging.getLogger(BackendLogger.API)
|
||||
|
||||
def __call__(self, request):
|
||||
request_start_time = time.time()
|
||||
|
||||
response = self.get_response(request)
|
||||
duration = time.time() - request_start_time
|
||||
auth_info = extract_auth_info(request)
|
||||
self.logger.info(
|
||||
"",
|
||||
extra={
|
||||
"user_id": auth_info["user_id"],
|
||||
"tenant_id": auth_info["tenant_id"],
|
||||
"method": request.method,
|
||||
"path": request.path,
|
||||
"query_params": request.GET.dict(),
|
||||
"status_code": response.status_code,
|
||||
"duration": duration,
|
||||
},
|
||||
)
|
||||
|
||||
return response
|
||||
1566
api/src/backend/api/migrations/0001_initial.py
Normal file
1566
api/src/backend/api/migrations/0001_initial.py
Normal file
File diff suppressed because it is too large
Load Diff
23
api/src/backend/api/migrations/0002_token_migrations.py
Normal file
23
api/src/backend/api/migrations/0002_token_migrations.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from django.conf import settings
|
||||
from django.db import migrations
|
||||
|
||||
from api.db_utils import DB_PROWLER_USER
|
||||
|
||||
DB_NAME = settings.DATABASES["default"]["NAME"]
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("api", "0001_initial"),
|
||||
("token_blacklist", "0012_alter_outstandingtoken_user"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RunSQL(
|
||||
f"""
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON token_blacklist_blacklistedtoken TO {DB_PROWLER_USER};
|
||||
GRANT SELECT, INSERT, UPDATE, DELETE ON token_blacklist_outstandingtoken TO {DB_PROWLER_USER};
|
||||
GRANT SELECT, DELETE ON django_admin_log TO {DB_PROWLER_USER};
|
||||
"""
|
||||
),
|
||||
]
|
||||
0
api/src/backend/api/migrations/__init__.py
Normal file
0
api/src/backend/api/migrations/__init__.py
Normal file
954
api/src/backend/api/models.py
Normal file
954
api/src/backend/api/models.py
Normal file
@@ -0,0 +1,954 @@
|
||||
import json
|
||||
import re
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
from cryptography.fernet import Fernet
|
||||
from django.conf import settings
|
||||
from django.contrib.auth.models import AbstractBaseUser
|
||||
from django.contrib.postgres.indexes import GinIndex
|
||||
from django.contrib.postgres.search import SearchVector, SearchVectorField
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django_celery_results.models import TaskResult
|
||||
from psqlextra.manager import PostgresManager
|
||||
from psqlextra.models import PostgresPartitionedModel
|
||||
from psqlextra.types import PostgresPartitioningMethod
|
||||
from uuid6 import uuid7
|
||||
|
||||
from api.db_utils import (
|
||||
CustomUserManager,
|
||||
FindingDeltaEnumField,
|
||||
InvitationStateEnumField,
|
||||
MemberRoleEnumField,
|
||||
ProviderEnumField,
|
||||
ProviderSecretTypeEnumField,
|
||||
ScanTriggerEnumField,
|
||||
SeverityEnumField,
|
||||
StateEnumField,
|
||||
StatusEnumField,
|
||||
enum_to_choices,
|
||||
generate_random_token,
|
||||
one_week_from_now,
|
||||
)
|
||||
from api.exceptions import ModelValidationError
|
||||
from api.rls import (
|
||||
BaseSecurityConstraint,
|
||||
RowLevelSecurityConstraint,
|
||||
RowLevelSecurityProtectedModel,
|
||||
Tenant,
|
||||
)
|
||||
from prowler.lib.check.models import Severity
|
||||
|
||||
fernet = Fernet(settings.SECRETS_ENCRYPTION_KEY.encode())
|
||||
|
||||
# Convert Prowler Severity enum to Django TextChoices
|
||||
SeverityChoices = enum_to_choices(Severity)
|
||||
|
||||
|
||||
class StatusChoices(models.TextChoices):
|
||||
"""
|
||||
This list is based on the finding status in the Prowler CLI.
|
||||
|
||||
However, it adds another state, MUTED, which is not in the CLI.
|
||||
"""
|
||||
|
||||
FAIL = "FAIL", _("Fail")
|
||||
PASS = "PASS", _("Pass")
|
||||
MANUAL = "MANUAL", _("Manual")
|
||||
MUTED = "MUTED", _("Muted")
|
||||
|
||||
|
||||
class StateChoices(models.TextChoices):
|
||||
AVAILABLE = "available", _("Available")
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
EXECUTING = "executing", _("Executing")
|
||||
COMPLETED = "completed", _("Completed")
|
||||
FAILED = "failed", _("Failed")
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
|
||||
|
||||
class ActiveProviderManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(self.active_provider_filter())
|
||||
|
||||
def active_provider_filter(self):
|
||||
if self.model is Provider:
|
||||
return Q(is_deleted=False)
|
||||
elif self.model in [Finding, ComplianceOverview, ScanSummary]:
|
||||
return Q(scan__provider__is_deleted=False)
|
||||
else:
|
||||
return Q(provider__is_deleted=False)
|
||||
|
||||
|
||||
class ActiveProviderPartitionedManager(PostgresManager, ActiveProviderManager):
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(self.active_provider_filter())
|
||||
|
||||
|
||||
class User(AbstractBaseUser):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
name = models.CharField(max_length=150, validators=[MinLengthValidator(3)])
|
||||
email = models.EmailField(
|
||||
max_length=254,
|
||||
unique=True,
|
||||
help_text="Case insensitive",
|
||||
error_messages={"unique": "Please check the email address and try again."},
|
||||
)
|
||||
company_name = models.CharField(max_length=150, blank=True)
|
||||
is_active = models.BooleanField(default=True)
|
||||
date_joined = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
|
||||
USERNAME_FIELD = "email"
|
||||
REQUIRED_FIELDS = ["name"]
|
||||
|
||||
objects = CustomUserManager()
|
||||
|
||||
def is_member_of_tenant(self, tenant_id):
|
||||
return self.memberships.filter(tenant_id=tenant_id).exists()
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
if self.email:
|
||||
self.email = self.email.strip().lower()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta:
|
||||
db_table = "users"
|
||||
|
||||
constraints = [
|
||||
BaseSecurityConstraint(
|
||||
name="statements_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
)
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "users"
|
||||
|
||||
|
||||
class Membership(models.Model):
|
||||
class RoleChoices(models.TextChoices):
|
||||
OWNER = "owner", _("Owner")
|
||||
MEMBER = "member", _("Member")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
user = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="memberships",
|
||||
related_query_name="membership",
|
||||
)
|
||||
tenant = models.ForeignKey(
|
||||
Tenant,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="memberships",
|
||||
related_query_name="membership",
|
||||
)
|
||||
role = MemberRoleEnumField(choices=RoleChoices.choices, default=RoleChoices.MEMBER)
|
||||
date_joined = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
|
||||
class Meta:
|
||||
db_table = "memberships"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("user", "tenant"),
|
||||
name="unique_resources_by_membership",
|
||||
),
|
||||
BaseSecurityConstraint(
|
||||
name="statements_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "memberships"
|
||||
|
||||
|
||||
class Provider(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class ProviderChoices(models.TextChoices):
|
||||
AWS = "aws", _("AWS")
|
||||
AZURE = "azure", _("Azure")
|
||||
GCP = "gcp", _("GCP")
|
||||
KUBERNETES = "kubernetes", _("Kubernetes")
|
||||
|
||||
@staticmethod
|
||||
def validate_aws_uid(value):
|
||||
if not re.match(r"^\d{12}$", value):
|
||||
raise ModelValidationError(
|
||||
detail="AWS provider ID must be exactly 12 digits.",
|
||||
code="aws-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_azure_uid(value):
|
||||
try:
|
||||
val = UUID(value, version=4)
|
||||
if str(val) != value:
|
||||
raise ValueError
|
||||
except ValueError:
|
||||
raise ModelValidationError(
|
||||
detail="Azure provider ID must be a valid UUID.",
|
||||
code="azure-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_gcp_uid(value):
|
||||
if not re.match(r"^[a-z][a-z0-9-]{5,29}$", value):
|
||||
raise ModelValidationError(
|
||||
detail="GCP provider ID must be 6 to 30 characters, start with a letter, and contain only lowercase "
|
||||
"letters, numbers, and hyphens.",
|
||||
code="gcp-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def validate_kubernetes_uid(value):
|
||||
if not re.match(
|
||||
r"(^[a-z0-9]([-a-z0-9]{1,61}[a-z0-9])?$)|(^arn:aws(-cn|-us-gov|-iso|-iso-b)?:[a-zA-Z0-9\-]+:([a-z]{2}-[a-z]+-\d{1})?:(\d{12})?:[a-zA-Z0-9\-_\/:\.\*]+(:\d+)?$)",
|
||||
value,
|
||||
):
|
||||
raise ModelValidationError(
|
||||
detail="The value must either be a valid Kubernetes UID (up to 63 characters, "
|
||||
"starting and ending with a lowercase letter or number, containing only "
|
||||
"lowercase alphanumeric characters and hyphens) or a valid EKS ARN.",
|
||||
code="kubernetes-uid",
|
||||
pointer="/data/attributes/uid",
|
||||
)
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
is_deleted = models.BooleanField(default=False)
|
||||
provider = ProviderEnumField(
|
||||
choices=ProviderChoices.choices, default=ProviderChoices.AWS
|
||||
)
|
||||
uid = models.CharField(
|
||||
"Unique identifier for the provider, set by the provider",
|
||||
max_length=63,
|
||||
blank=False,
|
||||
validators=[MinLengthValidator(3)],
|
||||
)
|
||||
alias = models.CharField(
|
||||
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
|
||||
)
|
||||
connected = models.BooleanField(null=True, blank=True)
|
||||
connection_last_checked_at = models.DateTimeField(null=True, blank=True)
|
||||
metadata = models.JSONField(default=dict, blank=True)
|
||||
scanner_args = models.JSONField(default=dict, blank=True)
|
||||
|
||||
def clean(self):
|
||||
super().clean()
|
||||
getattr(self, f"validate_{self.provider}_uid")(self.uid)
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.full_clean()
|
||||
super().save(*args, **kwargs)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "providers"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider", "uid"),
|
||||
name="unique_provider_uids",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "providers"
|
||||
|
||||
|
||||
class ProviderGroup(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
name = models.CharField(max_length=255)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
providers = models.ManyToManyField(
|
||||
Provider, through="ProviderGroupMembership", related_name="provider_groups"
|
||||
)
|
||||
|
||||
class Meta:
|
||||
db_table = "provider_groups"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["tenant_id", "name"],
|
||||
name="unique_group_name_per_tenant",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-groups"
|
||||
|
||||
|
||||
class ProviderGroupMembership(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
provider = models.ForeignKey(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
provider_group = models.ForeignKey(
|
||||
ProviderGroup,
|
||||
on_delete=models.CASCADE,
|
||||
)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
|
||||
class Meta:
|
||||
db_table = "provider_group_memberships"
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=["provider_id", "provider_group"],
|
||||
name="unique_provider_group_membership",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-group-memberships"
|
||||
|
||||
|
||||
class Task(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
task_runner_task = models.OneToOneField(
|
||||
TaskResult,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="task",
|
||||
related_query_name="task",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "tasks"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["id", "task_runner_task"],
|
||||
name="tasks_id_trt_id_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "tasks"
|
||||
|
||||
|
||||
class Scan(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class TriggerChoices(models.TextChoices):
|
||||
SCHEDULED = "scheduled", _("Scheduled")
|
||||
MANUAL = "manual", _("Manual")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
|
||||
name = models.CharField(
|
||||
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
|
||||
)
|
||||
provider = models.ForeignKey(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="scans",
|
||||
related_query_name="scan",
|
||||
)
|
||||
task = models.ForeignKey(
|
||||
Task,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="scans",
|
||||
related_query_name="scan",
|
||||
null=True,
|
||||
blank=True,
|
||||
)
|
||||
trigger = ScanTriggerEnumField(
|
||||
choices=TriggerChoices.choices,
|
||||
)
|
||||
state = StateEnumField(choices=StateChoices.choices, default=StateChoices.AVAILABLE)
|
||||
unique_resource_count = models.IntegerField(default=0)
|
||||
progress = models.IntegerField(default=0)
|
||||
scanner_args = models.JSONField(default=dict)
|
||||
duration = models.IntegerField(null=True, blank=True)
|
||||
scheduled_at = models.DateTimeField(null=True, blank=True)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
started_at = models.DateTimeField(null=True, blank=True)
|
||||
completed_at = models.DateTimeField(null=True, blank=True)
|
||||
next_scan_at = models.DateTimeField(null=True, blank=True)
|
||||
# TODO: mutelist foreign key
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "scans"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["provider", "state", "trigger", "scheduled_at"],
|
||||
name="scans_prov_state_trig_sche_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "scans"
|
||||
|
||||
|
||||
class ResourceTag(RowLevelSecurityProtectedModel):
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
key = models.TextField(blank=False)
|
||||
value = models.TextField(blank=False)
|
||||
|
||||
text_search = models.GeneratedField(
|
||||
expression=SearchVector("key", weight="A", config="simple")
|
||||
+ SearchVector("value", weight="B", config="simple"),
|
||||
output_field=SearchVectorField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resource_tags"
|
||||
|
||||
indexes = [
|
||||
GinIndex(fields=["text_search"], name="gin_resource_tags_search_idx"),
|
||||
]
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "key", "value"),
|
||||
name="unique_resource_tags_by_tenant_key_value",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Resource(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
provider = models.ForeignKey(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="resources",
|
||||
related_query_name="resource",
|
||||
)
|
||||
|
||||
uid = models.TextField(
|
||||
"Unique identifier for the resource, set by the provider", blank=False
|
||||
)
|
||||
name = models.TextField("Name of the resource, as set in the provider", blank=False)
|
||||
region = models.TextField(
|
||||
"Location of the resource, as set by the provider", blank=False
|
||||
)
|
||||
service = models.TextField(
|
||||
"Service of the resource, as set by the provider", blank=False
|
||||
)
|
||||
type = models.TextField("Type of the resource, as set by the provider", blank=False)
|
||||
|
||||
text_search = models.GeneratedField(
|
||||
expression=SearchVector("uid", weight="A", config="simple")
|
||||
+ SearchVector("name", weight="B", config="simple")
|
||||
+ SearchVector("region", weight="C", config="simple")
|
||||
+ SearchVector("service", "type", weight="D", config="simple"),
|
||||
output_field=SearchVectorField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
tags = models.ManyToManyField(
|
||||
ResourceTag,
|
||||
verbose_name="Tags associated with the resource, by provider",
|
||||
through="ResourceTagMapping",
|
||||
)
|
||||
|
||||
def get_tags(self) -> dict:
|
||||
return {tag.key: tag.value for tag in self.tags.all()}
|
||||
|
||||
def clear_tags(self):
|
||||
self.tags.clear()
|
||||
self.save()
|
||||
|
||||
def upsert_or_delete_tags(self, tags: list[ResourceTag] | None):
|
||||
if tags is None:
|
||||
self.clear_tags()
|
||||
return
|
||||
|
||||
# Add new relationships with the tenant_id field
|
||||
for tag in tags:
|
||||
ResourceTagMapping.objects.update_or_create(
|
||||
tag=tag, resource=self, tenant_id=self.tenant_id
|
||||
)
|
||||
|
||||
# Save the instance
|
||||
self.save()
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resources"
|
||||
|
||||
indexes = [
|
||||
models.Index(
|
||||
fields=["uid", "region", "service", "name"],
|
||||
name="resource_uid_reg_serv_name_idx",
|
||||
),
|
||||
GinIndex(fields=["text_search"], name="gin_resources_search_idx"),
|
||||
]
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "provider_id", "uid"),
|
||||
name="unique_resources_by_provider",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "resources"
|
||||
|
||||
|
||||
class ResourceTagMapping(RowLevelSecurityProtectedModel):
|
||||
# NOTE that we don't really need a primary key here,
|
||||
# but everything is easier with django if we do
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
|
||||
tag = models.ForeignKey(ResourceTag, on_delete=models.CASCADE)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resource_tag_mappings"
|
||||
|
||||
# django will automatically create indexes for:
|
||||
# - resource_id
|
||||
# - tag_id
|
||||
# - tenant_id
|
||||
# - id
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "resource_id", "tag_id"),
|
||||
name="unique_resource_tag_mappings_by_tenant",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class Finding(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
Defines the Finding model.
|
||||
|
||||
Findings uses a partitioned table to store findings. The partitions are created based on the UUIDv7 `id` field.
|
||||
|
||||
Note when creating migrations, you must use `python manage.py pgmakemigrations` to create the migrations.
|
||||
"""
|
||||
|
||||
objects = ActiveProviderPartitionedManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class PartitioningMeta:
|
||||
method = PostgresPartitioningMethod.RANGE
|
||||
key = ["id"]
|
||||
|
||||
class DeltaChoices(models.TextChoices):
|
||||
NEW = "new", _("New")
|
||||
CHANGED = "changed", _("Changed")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid7, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
|
||||
uid = models.CharField(max_length=300)
|
||||
delta = FindingDeltaEnumField(
|
||||
choices=DeltaChoices.choices,
|
||||
blank=True,
|
||||
null=True,
|
||||
)
|
||||
|
||||
status = StatusEnumField(choices=StatusChoices)
|
||||
status_extended = models.TextField(blank=True, null=True)
|
||||
|
||||
severity = SeverityEnumField(choices=SeverityChoices)
|
||||
|
||||
impact = SeverityEnumField(choices=SeverityChoices)
|
||||
impact_extended = models.TextField(blank=True, null=True)
|
||||
|
||||
raw_result = models.JSONField(default=dict)
|
||||
tags = models.JSONField(default=dict, null=True, blank=True)
|
||||
check_id = models.CharField(max_length=100, blank=False, null=False)
|
||||
check_metadata = models.JSONField(default=dict, null=False)
|
||||
|
||||
# Relationships
|
||||
scan = models.ForeignKey(to=Scan, related_name="findings", on_delete=models.CASCADE)
|
||||
|
||||
# many-to-many Resources. Relationship is defined on Resource
|
||||
resources = models.ManyToManyField(
|
||||
Resource,
|
||||
verbose_name="Resources associated with the finding",
|
||||
through="ResourceFindingMapping",
|
||||
related_name="findings",
|
||||
)
|
||||
|
||||
# TODO: Add resource search
|
||||
text_search = models.GeneratedField(
|
||||
expression=SearchVector(
|
||||
"impact_extended", "status_extended", weight="A", config="simple"
|
||||
),
|
||||
output_field=SearchVectorField(),
|
||||
db_persist=True,
|
||||
null=True,
|
||||
editable=False,
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "findings"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "UPDATE", "INSERT", "DELETE"],
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s_default",
|
||||
partition_name="default",
|
||||
statements=["SELECT", "UPDATE", "INSERT", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
indexes = [
|
||||
models.Index(fields=["uid"], name="findings_uid_idx"),
|
||||
models.Index(
|
||||
fields=[
|
||||
"scan_id",
|
||||
"impact",
|
||||
"severity",
|
||||
"status",
|
||||
"check_id",
|
||||
"delta",
|
||||
],
|
||||
name="findings_filter_idx",
|
||||
),
|
||||
GinIndex(fields=["text_search"], name="gin_findings_search_idx"),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "findings"
|
||||
|
||||
def add_resources(self, resources: list[Resource] | None):
|
||||
# Add new relationships with the tenant_id field
|
||||
for resource in resources:
|
||||
ResourceFindingMapping.objects.update_or_create(
|
||||
resource=resource, finding=self, tenant_id=self.tenant_id
|
||||
)
|
||||
|
||||
# Save the instance
|
||||
self.save()
|
||||
|
||||
|
||||
class ResourceFindingMapping(PostgresPartitionedModel, RowLevelSecurityProtectedModel):
|
||||
"""
|
||||
Defines the ResourceFindingMapping model.
|
||||
|
||||
ResourceFindingMapping is used to map a Finding to a Resource.
|
||||
|
||||
It follows the same partitioning strategy as the Finding model.
|
||||
"""
|
||||
|
||||
# NOTE that we don't really need a primary key here,
|
||||
# but everything is easier with django if we do
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
resource = models.ForeignKey(Resource, on_delete=models.CASCADE)
|
||||
finding = models.ForeignKey(Finding, on_delete=models.CASCADE)
|
||||
|
||||
class PartitioningMeta:
|
||||
method = PostgresPartitioningMethod.RANGE
|
||||
key = ["finding_id"]
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "resource_finding_mappings"
|
||||
base_manager_name = "objects"
|
||||
abstract = False
|
||||
|
||||
# django will automatically create indexes for:
|
||||
# - resource_id
|
||||
# - finding_id
|
||||
# - tenant_id
|
||||
# - id
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant_id", "resource_id", "finding_id"),
|
||||
name="unique_resource_finding_mappings_by_tenant",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name=f"rls_on_{db_table}_default",
|
||||
partition_name="default",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
class ProviderSecret(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
class TypeChoices(models.TextChoices):
|
||||
STATIC = "static", _("Key-value pairs")
|
||||
ROLE = "role", _("Role assumption")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
name = models.CharField(
|
||||
blank=True, null=True, max_length=100, validators=[MinLengthValidator(3)]
|
||||
)
|
||||
secret_type = ProviderSecretTypeEnumField(choices=TypeChoices.choices)
|
||||
_secret = models.BinaryField(db_column="secret")
|
||||
provider = models.OneToOneField(
|
||||
Provider,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="secret",
|
||||
related_query_name="secret",
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "provider_secrets"
|
||||
|
||||
constraints = [
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "provider-secrets"
|
||||
|
||||
@property
|
||||
def secret(self):
|
||||
if isinstance(self._secret, memoryview):
|
||||
encrypted_bytes = self._secret.tobytes()
|
||||
elif isinstance(self._secret, str):
|
||||
encrypted_bytes = self._secret.encode()
|
||||
else:
|
||||
encrypted_bytes = self._secret
|
||||
decrypted_data = fernet.decrypt(encrypted_bytes)
|
||||
return json.loads(decrypted_data.decode())
|
||||
|
||||
@secret.setter
|
||||
def secret(self, value):
|
||||
encrypted_data = fernet.encrypt(json.dumps(value).encode())
|
||||
self._secret = encrypted_data
|
||||
|
||||
|
||||
class Invitation(RowLevelSecurityProtectedModel):
|
||||
class State(models.TextChoices):
|
||||
PENDING = "pending", _("Invitation is pending")
|
||||
ACCEPTED = "accepted", _("Invitation was accepted by a user")
|
||||
EXPIRED = "expired", _("Invitation expired after the configured time")
|
||||
REVOKED = "revoked", _("Invitation was revoked by a user")
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
email = models.EmailField(max_length=254, blank=False, null=False)
|
||||
state = InvitationStateEnumField(choices=State.choices, default=State.PENDING)
|
||||
token = models.CharField(
|
||||
max_length=14,
|
||||
unique=True,
|
||||
default=generate_random_token,
|
||||
editable=False,
|
||||
blank=False,
|
||||
null=False,
|
||||
validators=[MinLengthValidator(14)],
|
||||
)
|
||||
expires_at = models.DateTimeField(default=one_week_from_now)
|
||||
inviter = models.ForeignKey(
|
||||
User,
|
||||
on_delete=models.SET_NULL,
|
||||
related_name="invitations",
|
||||
related_query_name="invitation",
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "invitations"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant", "token", "email"),
|
||||
name="unique_tenant_token_email_by_invitation",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "invitations"
|
||||
|
||||
|
||||
class ComplianceOverview(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
compliance_id = models.CharField(max_length=100, blank=False, null=False)
|
||||
framework = models.CharField(max_length=100, blank=False, null=False)
|
||||
version = models.CharField(max_length=50, blank=True)
|
||||
description = models.TextField(blank=True)
|
||||
region = models.CharField(max_length=50, blank=True)
|
||||
requirements = models.JSONField(default=dict)
|
||||
requirements_passed = models.IntegerField(default=0)
|
||||
requirements_failed = models.IntegerField(default=0)
|
||||
requirements_manual = models.IntegerField(default=0)
|
||||
total_requirements = models.IntegerField(default=0)
|
||||
|
||||
scan = models.ForeignKey(
|
||||
Scan,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="compliance_overviews",
|
||||
related_query_name="compliance_overview",
|
||||
null=True,
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "compliance_overviews"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant", "scan", "compliance_id", "region"),
|
||||
name="unique_tenant_scan_region_compliance_by_compliance_overview",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "DELETE"],
|
||||
),
|
||||
]
|
||||
indexes = [
|
||||
models.Index(fields=["compliance_id"], name="comp_ov_cp_id_idx"),
|
||||
models.Index(fields=["requirements_failed"], name="comp_ov_req_fail_idx"),
|
||||
models.Index(
|
||||
fields=["compliance_id", "requirements_failed"],
|
||||
name="comp_ov_cp_id_req_fail_idx",
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "compliance-overviews"
|
||||
|
||||
|
||||
class ScanSummary(RowLevelSecurityProtectedModel):
|
||||
objects = ActiveProviderManager()
|
||||
all_objects = models.Manager()
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
check_id = models.CharField(max_length=100, blank=False, null=False)
|
||||
service = models.TextField(blank=False)
|
||||
severity = SeverityEnumField(choices=SeverityChoices)
|
||||
region = models.TextField(blank=False)
|
||||
_pass = models.IntegerField(db_column="pass", default=0)
|
||||
fail = models.IntegerField(default=0)
|
||||
muted = models.IntegerField(default=0)
|
||||
total = models.IntegerField(default=0)
|
||||
new = models.IntegerField(default=0)
|
||||
changed = models.IntegerField(default=0)
|
||||
unchanged = models.IntegerField(default=0)
|
||||
|
||||
fail_new = models.IntegerField(default=0)
|
||||
fail_changed = models.IntegerField(default=0)
|
||||
pass_new = models.IntegerField(default=0)
|
||||
pass_changed = models.IntegerField(default=0)
|
||||
muted_new = models.IntegerField(default=0)
|
||||
muted_changed = models.IntegerField(default=0)
|
||||
|
||||
scan = models.ForeignKey(
|
||||
Scan,
|
||||
on_delete=models.CASCADE,
|
||||
related_name="aggregations",
|
||||
related_query_name="aggregation",
|
||||
)
|
||||
|
||||
class Meta(RowLevelSecurityProtectedModel.Meta):
|
||||
db_table = "scan_summaries"
|
||||
|
||||
constraints = [
|
||||
models.UniqueConstraint(
|
||||
fields=("tenant", "scan", "check_id", "service", "severity", "region"),
|
||||
name="unique_scan_summary",
|
||||
),
|
||||
RowLevelSecurityConstraint(
|
||||
field="tenant_id",
|
||||
name="rls_on_%(class)s",
|
||||
statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
]
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "scan-summaries"
|
||||
6
api/src/backend/api/pagination.py
Normal file
6
api/src/backend/api/pagination.py
Normal file
@@ -0,0 +1,6 @@
|
||||
from rest_framework_json_api.pagination import JsonApiPageNumberPagination
|
||||
|
||||
|
||||
class ComplianceOverviewPagination(JsonApiPageNumberPagination):
|
||||
page_size = 50
|
||||
max_page_size = 100
|
||||
203
api/src/backend/api/partitions.py
Normal file
203
api/src/backend/api/partitions.py
Normal file
@@ -0,0 +1,203 @@
|
||||
from datetime import datetime, timezone
|
||||
from typing import Generator, Optional
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from django.conf import settings
|
||||
from psqlextra.partitioning import (
|
||||
PostgresPartitioningManager,
|
||||
PostgresRangePartition,
|
||||
PostgresRangePartitioningStrategy,
|
||||
PostgresTimePartitionSize,
|
||||
PostgresPartitioningError,
|
||||
)
|
||||
from psqlextra.partitioning.config import PostgresPartitioningConfig
|
||||
from uuid6 import UUID
|
||||
|
||||
from api.models import Finding, ResourceFindingMapping
|
||||
from api.rls import RowLevelSecurityConstraint
|
||||
from api.uuid_utils import datetime_to_uuid7
|
||||
|
||||
|
||||
class PostgresUUIDv7RangePartition(PostgresRangePartition):
|
||||
def __init__(
|
||||
self,
|
||||
from_values: UUID,
|
||||
to_values: UUID,
|
||||
size: PostgresTimePartitionSize,
|
||||
name_format: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.from_values = from_values
|
||||
self.to_values = to_values
|
||||
self.size = size
|
||||
self.name_format = name_format
|
||||
|
||||
self.rls_statements = None
|
||||
if "rls_statements" in kwargs:
|
||||
self.rls_statements = kwargs["rls_statements"]
|
||||
|
||||
start_timestamp_ms = self.from_values.time
|
||||
|
||||
self.start_datetime = datetime.fromtimestamp(
|
||||
start_timestamp_ms / 1000, timezone.utc
|
||||
)
|
||||
|
||||
def name(self) -> str:
|
||||
if not self.name_format:
|
||||
raise PostgresPartitioningError("Unknown size/unit")
|
||||
|
||||
return self.start_datetime.strftime(self.name_format).lower()
|
||||
|
||||
def deconstruct(self) -> dict:
|
||||
return {
|
||||
**super().deconstruct(),
|
||||
"size_unit": self.size.unit.value,
|
||||
"size_value": self.size.value,
|
||||
}
|
||||
|
||||
def create(
|
||||
self,
|
||||
model,
|
||||
schema_editor,
|
||||
comment,
|
||||
) -> None:
|
||||
super().create(model, schema_editor, comment)
|
||||
|
||||
# if this model has RLS statements, add them to the partition
|
||||
if isinstance(self.rls_statements, list):
|
||||
schema_editor.add_constraint(
|
||||
model,
|
||||
constraint=RowLevelSecurityConstraint(
|
||||
"tenant_id",
|
||||
name=f"rls_on_{self.name()}",
|
||||
partition_name=self.name(),
|
||||
statements=self.rls_statements,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
class PostgresUUIDv7PartitioningStrategy(PostgresRangePartitioningStrategy):
|
||||
def __init__(
|
||||
self,
|
||||
size: PostgresTimePartitionSize,
|
||||
count: int,
|
||||
start_date: datetime = None,
|
||||
max_age: Optional[relativedelta] = None,
|
||||
name_format: Optional[str] = None,
|
||||
**kwargs,
|
||||
) -> None:
|
||||
self.start_date = start_date.replace(
|
||||
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
self.size = size
|
||||
self.count = count
|
||||
self.max_age = max_age
|
||||
self.name_format = name_format
|
||||
|
||||
self.rls_statements = None
|
||||
if "rls_statements" in kwargs:
|
||||
self.rls_statements = kwargs["rls_statements"]
|
||||
|
||||
def to_create(self) -> Generator[PostgresUUIDv7RangePartition, None, None]:
|
||||
current_datetime = (
|
||||
self.start_date if self.start_date else self.get_start_datetime()
|
||||
)
|
||||
|
||||
for _ in range(self.count):
|
||||
end_datetime = (
|
||||
current_datetime + self.size.as_delta() - relativedelta(microseconds=1)
|
||||
)
|
||||
start_uuid7 = datetime_to_uuid7(current_datetime)
|
||||
end_uuid7 = datetime_to_uuid7(end_datetime)
|
||||
|
||||
yield PostgresUUIDv7RangePartition(
|
||||
from_values=start_uuid7,
|
||||
to_values=end_uuid7,
|
||||
size=self.size,
|
||||
name_format=self.name_format,
|
||||
rls_statements=self.rls_statements,
|
||||
)
|
||||
|
||||
current_datetime += self.size.as_delta()
|
||||
|
||||
def to_delete(self) -> Generator[PostgresUUIDv7RangePartition, None, None]:
|
||||
if not self.max_age:
|
||||
return
|
||||
|
||||
current_datetime = self.get_start_datetime() - self.max_age
|
||||
|
||||
while True:
|
||||
end_datetime = current_datetime + self.size.as_delta()
|
||||
start_uuid7 = datetime_to_uuid7(current_datetime)
|
||||
end_uuid7 = datetime_to_uuid7(end_datetime)
|
||||
|
||||
# dropping table will delete indexes and policies
|
||||
yield PostgresUUIDv7RangePartition(
|
||||
from_values=start_uuid7,
|
||||
to_values=end_uuid7,
|
||||
size=self.size,
|
||||
name_format=self.name_format,
|
||||
)
|
||||
|
||||
current_datetime -= self.size.as_delta()
|
||||
|
||||
def get_start_datetime(self) -> datetime:
|
||||
"""
|
||||
Gets the start of the current month in UTC timezone.
|
||||
|
||||
This function returns a `datetime` object set to the first day of the current
|
||||
month, at midnight (00:00:00), in UTC.
|
||||
|
||||
Returns:
|
||||
datetime: A `datetime` object representing the start of the current month in UTC.
|
||||
"""
|
||||
return datetime.now(timezone.utc).replace(
|
||||
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
|
||||
|
||||
def relative_days_or_none(value):
|
||||
if value is None:
|
||||
return None
|
||||
return relativedelta(days=value)
|
||||
|
||||
|
||||
#
|
||||
# To manage the partitions, run `python manage.py pgpartition --using admin`
|
||||
#
|
||||
# For more info on the partitioning manager, see https://github.com/SectorLabs/django-postgres-extra
|
||||
manager = PostgresPartitioningManager(
|
||||
[
|
||||
PostgresPartitioningConfig(
|
||||
model=Finding,
|
||||
strategy=PostgresUUIDv7PartitioningStrategy(
|
||||
start_date=datetime.now(timezone.utc),
|
||||
size=PostgresTimePartitionSize(
|
||||
months=settings.FINDINGS_TABLE_PARTITION_MONTHS
|
||||
),
|
||||
count=settings.FINDINGS_TABLE_PARTITION_COUNT,
|
||||
max_age=relative_days_or_none(
|
||||
settings.FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS
|
||||
),
|
||||
name_format="%Y_%b",
|
||||
rls_statements=["SELECT", "INSERT", "UPDATE", "DELETE"],
|
||||
),
|
||||
),
|
||||
# ResourceFindingMapping should always follow the Finding partitioning
|
||||
PostgresPartitioningConfig(
|
||||
model=ResourceFindingMapping,
|
||||
strategy=PostgresUUIDv7PartitioningStrategy(
|
||||
start_date=datetime.now(timezone.utc),
|
||||
size=PostgresTimePartitionSize(
|
||||
months=settings.FINDINGS_TABLE_PARTITION_MONTHS
|
||||
),
|
||||
count=settings.FINDINGS_TABLE_PARTITION_COUNT,
|
||||
max_age=relative_days_or_none(
|
||||
settings.FINDINGS_TABLE_PARTITION_MAX_AGE_MONTHS
|
||||
),
|
||||
name_format="%Y_%b",
|
||||
rls_statements=["SELECT"],
|
||||
),
|
||||
),
|
||||
]
|
||||
)
|
||||
23
api/src/backend/api/renderers.py
Normal file
23
api/src/backend/api/renderers.py
Normal file
@@ -0,0 +1,23 @@
|
||||
from contextlib import nullcontext
|
||||
|
||||
from rest_framework_json_api.renderers import JSONRenderer
|
||||
|
||||
from api.db_utils import rls_transaction
|
||||
|
||||
|
||||
class APIJSONRenderer(JSONRenderer):
|
||||
"""JSONRenderer override to apply tenant RLS when there are included resources in the request."""
|
||||
|
||||
def render(self, data, accepted_media_type=None, renderer_context=None):
|
||||
request = renderer_context.get("request")
|
||||
tenant_id = getattr(request, "tenant_id", None) if request else None
|
||||
include_param_present = "include" in request.query_params if request else False
|
||||
|
||||
# Use rls_transaction if needed for included resources, otherwise do nothing
|
||||
context_manager = (
|
||||
rls_transaction(tenant_id)
|
||||
if tenant_id and include_param_present
|
||||
else nullcontext()
|
||||
)
|
||||
with context_manager:
|
||||
return super().render(data, accepted_media_type, renderer_context)
|
||||
188
api/src/backend/api/rls.py
Normal file
188
api/src/backend/api/rls.py
Normal file
@@ -0,0 +1,188 @@
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.db import DEFAULT_DB_ALIAS
|
||||
from django.db import models
|
||||
from django.db.backends.ddl_references import Statement, Table
|
||||
|
||||
from api.db_utils import DB_USER, POSTGRES_TENANT_VAR
|
||||
|
||||
|
||||
class Tenant(models.Model):
|
||||
"""
|
||||
The Tenant is the basic grouping in the system. It is used to separate data between customers.
|
||||
"""
|
||||
|
||||
id = models.UUIDField(primary_key=True, default=uuid4, editable=False)
|
||||
|
||||
inserted_at = models.DateTimeField(auto_now_add=True, editable=False)
|
||||
updated_at = models.DateTimeField(auto_now=True, editable=False)
|
||||
name = models.CharField(max_length=100)
|
||||
|
||||
class Meta:
|
||||
db_table = "tenants"
|
||||
|
||||
class JSONAPIMeta:
|
||||
resource_name = "tenants"
|
||||
|
||||
|
||||
class RowLevelSecurityConstraint(models.BaseConstraint):
|
||||
"""
|
||||
Model constraint to enforce row-level security on a tenant based model, in addition to the least privileges.
|
||||
|
||||
The constraint can be applied to a partitioned table by specifying the `partition_name` keyword argument.
|
||||
"""
|
||||
|
||||
rls_sql_query = """
|
||||
ALTER TABLE %(table_name)s ENABLE ROW LEVEL SECURITY;
|
||||
ALTER TABLE %(table_name)s FORCE ROW LEVEL SECURITY;
|
||||
"""
|
||||
|
||||
policy_sql_query = """
|
||||
CREATE POLICY %(db_user)s_%(table_name)s_{statement}
|
||||
ON %(table_name)s
|
||||
FOR {statement}
|
||||
TO %(db_user)s
|
||||
{clause} (
|
||||
CASE
|
||||
WHEN current_setting('%(tenant_setting)s', True) IS NULL THEN FALSE
|
||||
ELSE %(field_column)s = current_setting('%(tenant_setting)s')::uuid
|
||||
END
|
||||
);
|
||||
"""
|
||||
|
||||
grant_sql_query = """
|
||||
GRANT {statement} ON %(table_name)s TO %(db_user)s;
|
||||
"""
|
||||
|
||||
drop_sql_query = """
|
||||
ALTER TABLE %(table_name)s NO FORCE ROW LEVEL SECURITY;
|
||||
ALTER TABLE %(table_name)s DISABLE ROW LEVEL SECURITY;
|
||||
REVOKE ALL ON TABLE %(table_name) TO %(db_user)s;
|
||||
"""
|
||||
|
||||
drop_policy_sql_query = """
|
||||
DROP POLICY IF EXISTS %(db_user)s_%(table_name)s_{statement} on %(table_name)s;
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, field: str, name: str, statements: list | None = None, **kwargs
|
||||
) -> None:
|
||||
super().__init__(name=name)
|
||||
self.target_field: str = field
|
||||
self.statements = statements or ["SELECT"]
|
||||
self.partition_name = None
|
||||
if "partition_name" in kwargs:
|
||||
self.partition_name = kwargs["partition_name"]
|
||||
|
||||
def create_sql(self, model: Any, schema_editor: Any) -> Any:
|
||||
field_column = schema_editor.quote_name(self.target_field)
|
||||
|
||||
policy_queries = ""
|
||||
grant_queries = ""
|
||||
for statement in self.statements:
|
||||
clause = f"{'WITH CHECK' if statement == 'INSERT' else 'USING'}"
|
||||
policy_queries = f"{policy_queries}{self.policy_sql_query.format(statement=statement, clause=clause)}"
|
||||
grant_queries = (
|
||||
f"{grant_queries}{self.grant_sql_query.format(statement=statement)}"
|
||||
)
|
||||
|
||||
full_create_sql_query = (
|
||||
f"{self.rls_sql_query}" f"{policy_queries}" f"{grant_queries}"
|
||||
)
|
||||
|
||||
table_name = model._meta.db_table
|
||||
if self.partition_name:
|
||||
table_name = f"{table_name}_{self.partition_name}"
|
||||
|
||||
return Statement(
|
||||
full_create_sql_query,
|
||||
table_name=table_name,
|
||||
field_column=field_column,
|
||||
db_user=DB_USER,
|
||||
tenant_setting=POSTGRES_TENANT_VAR,
|
||||
partition_name=self.partition_name,
|
||||
)
|
||||
|
||||
def remove_sql(self, model: Any, schema_editor: Any) -> Any:
|
||||
field_column = schema_editor.quote_name(self.target_field)
|
||||
full_drop_sql_query = (
|
||||
f"{self.drop_sql_query}"
|
||||
f"{''.join([self.drop_policy_sql_query.format(statement) for statement in self.statements])}"
|
||||
)
|
||||
table_name = model._meta.db_table
|
||||
if self.partition_name:
|
||||
table_name = f"{table_name}_{self.partition_name}"
|
||||
return Statement(
|
||||
full_drop_sql_query,
|
||||
table_name=Table(table_name, schema_editor.quote_name),
|
||||
field_column=field_column,
|
||||
db_user=DB_USER,
|
||||
partition_name=self.partition_name,
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, RowLevelSecurityConstraint):
|
||||
return self.name == other.name and self.target_field == other.target_field
|
||||
return super().__eq__(other)
|
||||
|
||||
def deconstruct(self) -> tuple[str, tuple, dict]:
|
||||
path, _, kwargs = super().deconstruct()
|
||||
return (path, (self.target_field,), kwargs)
|
||||
|
||||
def validate(self, model, instance, exclude=None, using=DEFAULT_DB_ALIAS): # noqa: F841
|
||||
if not hasattr(instance, "tenant_id"):
|
||||
raise ValidationError(f"{model.__name__} does not have a tenant_id field.")
|
||||
|
||||
|
||||
class BaseSecurityConstraint(models.BaseConstraint):
|
||||
"""Model constraint to grant the least privileges to the API database user."""
|
||||
|
||||
grant_sql_query = """
|
||||
GRANT {statement} ON %(table_name)s TO %(db_user)s;
|
||||
"""
|
||||
|
||||
drop_sql_query = """
|
||||
REVOKE ALL ON TABLE %(table_name) TO %(db_user)s;
|
||||
"""
|
||||
|
||||
def __init__(self, name: str, statements: list | None = None) -> None:
|
||||
super().__init__(name=name)
|
||||
self.statements = statements or ["SELECT"]
|
||||
|
||||
def create_sql(self, model: Any, schema_editor: Any) -> Any:
|
||||
grant_queries = ""
|
||||
for statement in self.statements:
|
||||
grant_queries = (
|
||||
f"{grant_queries}{self.grant_sql_query.format(statement=statement)}"
|
||||
)
|
||||
|
||||
return Statement(
|
||||
grant_queries,
|
||||
table_name=model._meta.db_table,
|
||||
db_user=DB_USER,
|
||||
)
|
||||
|
||||
def remove_sql(self, model: Any, schema_editor: Any) -> Any:
|
||||
return Statement(
|
||||
self.drop_sql_query,
|
||||
table_name=Table(model._meta.db_table, schema_editor.quote_name),
|
||||
db_user=DB_USER,
|
||||
)
|
||||
|
||||
def __eq__(self, other: object) -> bool:
|
||||
if isinstance(other, BaseSecurityConstraint):
|
||||
return self.name == other.name
|
||||
return super().__eq__(other)
|
||||
|
||||
def deconstruct(self) -> tuple[str, tuple, dict]:
|
||||
path, args, kwargs = super().deconstruct()
|
||||
return path, args, kwargs
|
||||
|
||||
|
||||
class RowLevelSecurityProtectedModel(models.Model):
|
||||
tenant = models.ForeignKey("Tenant", on_delete=models.CASCADE)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
35
api/src/backend/api/signals.py
Normal file
35
api/src/backend/api/signals.py
Normal file
@@ -0,0 +1,35 @@
|
||||
from celery import states
|
||||
from celery.signals import before_task_publish
|
||||
from django.db.models.signals import post_delete
|
||||
from django.dispatch import receiver
|
||||
from django_celery_beat.models import PeriodicTask
|
||||
from django_celery_results.backends.database import DatabaseBackend
|
||||
|
||||
from api.models import Provider
|
||||
from config.celery import celery_app
|
||||
|
||||
|
||||
def create_task_result_on_publish(sender=None, headers=None, **kwargs): # noqa: F841
|
||||
"""Celery signal to store TaskResult entries when tasks reach the broker."""
|
||||
db_result_backend = DatabaseBackend(celery_app)
|
||||
request = type("request", (object,), headers)
|
||||
|
||||
db_result_backend.store_result(
|
||||
headers["id"],
|
||||
None,
|
||||
states.PENDING,
|
||||
traceback=None,
|
||||
request=request,
|
||||
)
|
||||
|
||||
|
||||
before_task_publish.connect(
|
||||
create_task_result_on_publish, dispatch_uid="create_task_result_on_publish"
|
||||
)
|
||||
|
||||
|
||||
@receiver(post_delete, sender=Provider)
|
||||
def delete_provider_scan_task(sender, instance, **kwargs): # noqa: F841
|
||||
# Delete the associated periodic task when the provider is deleted
|
||||
task_name = f"scan-perform-scheduled-{instance.id}"
|
||||
PeriodicTask.objects.filter(name=task_name).delete()
|
||||
7989
api/src/backend/api/specs/v1.yaml
Normal file
7989
api/src/backend/api/specs/v1.yaml
Normal file
File diff suppressed because it is too large
Load Diff
0
api/src/backend/api/tests/__init__.py
Normal file
0
api/src/backend/api/tests/__init__.py
Normal file
100
api/src/backend/api/tests/integration/test_authentication.py
Normal file
100
api/src/backend/api/tests/integration/test_authentication.py
Normal file
@@ -0,0 +1,100 @@
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
from unittest.mock import patch
|
||||
from rest_framework.test import APIClient
|
||||
|
||||
from conftest import TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
|
||||
|
||||
@patch("api.v1.views.MainRouter.admin_db", new="default")
|
||||
@pytest.mark.django_db
|
||||
def test_basic_authentication():
|
||||
client = APIClient()
|
||||
|
||||
test_user = "test_email@prowler.com"
|
||||
test_password = "test_password"
|
||||
|
||||
# Check that a 401 is returned when no basic authentication is provided
|
||||
no_auth_response = client.get(reverse("provider-list"))
|
||||
assert no_auth_response.status_code == 401
|
||||
|
||||
# Check that we can create a new user without any kind of authentication
|
||||
user_creation_response = client.post(
|
||||
reverse("user-list"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "users",
|
||||
"attributes": {
|
||||
"name": "test",
|
||||
"email": test_user,
|
||||
"password": test_password,
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert user_creation_response.status_code == 201
|
||||
|
||||
# Check that using our new user's credentials we can authenticate and get the providers
|
||||
access_token, _ = get_api_tokens(client, test_user, test_password)
|
||||
auth_headers = get_authorization_header(access_token)
|
||||
|
||||
auth_response = client.get(
|
||||
reverse("provider-list"),
|
||||
headers=auth_headers,
|
||||
)
|
||||
assert auth_response.status_code == 200
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_refresh_token(create_test_user, tenants_fixture):
|
||||
client = APIClient()
|
||||
|
||||
# Assert that we can obtain a new access token using the refresh one
|
||||
access_token, refresh_token = get_api_tokens(
|
||||
client, create_test_user.email, TEST_PASSWORD
|
||||
)
|
||||
valid_refresh_response = client.post(
|
||||
reverse("token-refresh"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "tokens-refresh",
|
||||
"attributes": {"refresh": refresh_token},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert valid_refresh_response.status_code == 200
|
||||
assert (
|
||||
valid_refresh_response.json()["data"]["attributes"]["refresh"] != refresh_token
|
||||
)
|
||||
|
||||
# Assert the former refresh token gets invalidated
|
||||
invalid_refresh_response = client.post(
|
||||
reverse("token-refresh"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "tokens-refresh",
|
||||
"attributes": {"refresh": refresh_token},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert invalid_refresh_response.status_code == 400
|
||||
|
||||
# Assert that the new refresh token could be used
|
||||
new_refresh_response = client.post(
|
||||
reverse("token-refresh"),
|
||||
data={
|
||||
"data": {
|
||||
"type": "tokens-refresh",
|
||||
"attributes": {
|
||||
"refresh": valid_refresh_response.json()["data"]["attributes"][
|
||||
"refresh"
|
||||
]
|
||||
},
|
||||
}
|
||||
},
|
||||
format="vnd.api+json",
|
||||
)
|
||||
assert new_refresh_response.status_code == 200
|
||||
97
api/src/backend/api/tests/integration/test_tenants.py
Normal file
97
api/src/backend/api/tests/integration/test_tenants.py
Normal file
@@ -0,0 +1,97 @@
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
from django.urls import reverse
|
||||
|
||||
from conftest import TEST_USER, TEST_PASSWORD, get_api_tokens, get_authorization_header
|
||||
|
||||
|
||||
@patch("api.v1.views.schedule_provider_scan")
|
||||
@pytest.mark.django_db
|
||||
def test_check_resources_between_different_tenants(
|
||||
schedule_mock,
|
||||
enforce_test_user_db_connection,
|
||||
authenticated_api_client,
|
||||
tenants_fixture,
|
||||
):
|
||||
client = authenticated_api_client
|
||||
|
||||
tenant1 = str(tenants_fixture[0].id)
|
||||
tenant2 = str(tenants_fixture[1].id)
|
||||
|
||||
tenant1_token, _ = get_api_tokens(
|
||||
client, TEST_USER, TEST_PASSWORD, tenant_id=tenant1
|
||||
)
|
||||
tenant2_token, _ = get_api_tokens(
|
||||
client, TEST_USER, TEST_PASSWORD, tenant_id=tenant2
|
||||
)
|
||||
|
||||
tenant1_headers = get_authorization_header(tenant1_token)
|
||||
tenant2_headers = get_authorization_header(tenant2_token)
|
||||
|
||||
# Create a provider on tenant 1
|
||||
provider_data = {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"alias": "test_provider_tenant_1",
|
||||
"provider": "aws",
|
||||
"uid": "123456789012",
|
||||
},
|
||||
}
|
||||
}
|
||||
provider1_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data=provider_data,
|
||||
format="vnd.api+json",
|
||||
headers=tenant1_headers,
|
||||
)
|
||||
assert provider1_response.status_code == 201
|
||||
provider1_id = provider1_response.json()["data"]["id"]
|
||||
|
||||
# Create a provider on tenant 2
|
||||
provider_data = {
|
||||
"data": {
|
||||
"type": "providers",
|
||||
"attributes": {
|
||||
"alias": "test_provider_tenant_2",
|
||||
"provider": "aws",
|
||||
"uid": "123456789013",
|
||||
},
|
||||
}
|
||||
}
|
||||
provider2_response = client.post(
|
||||
reverse("provider-list"),
|
||||
data=provider_data,
|
||||
format="vnd.api+json",
|
||||
headers=tenant2_headers,
|
||||
)
|
||||
assert provider2_response.status_code == 201
|
||||
provider2_id = provider2_response.json()["data"]["id"]
|
||||
|
||||
# Try to get the provider from tenant 1 on tenant 2 and vice versa
|
||||
tenant1_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider1_id}),
|
||||
headers=tenant2_headers,
|
||||
)
|
||||
assert tenant1_response.status_code == 404
|
||||
tenant2_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider1_id}),
|
||||
headers=tenant1_headers,
|
||||
)
|
||||
assert tenant2_response.status_code == 200
|
||||
assert tenant2_response.json()["data"]["id"] == provider1_id
|
||||
|
||||
# Vice versa
|
||||
|
||||
tenant2_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider2_id}),
|
||||
headers=tenant1_headers,
|
||||
)
|
||||
assert tenant2_response.status_code == 404
|
||||
tenant1_response = client.get(
|
||||
reverse("provider-detail", kwargs={"pk": provider2_id}),
|
||||
headers=tenant2_headers,
|
||||
)
|
||||
assert tenant1_response.status_code == 200
|
||||
assert tenant1_response.json()["data"]["id"] == provider2_id
|
||||
284
api/src/backend/api/tests/test_compliance.py
Normal file
284
api/src/backend/api/tests/test_compliance.py
Normal file
@@ -0,0 +1,284 @@
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
from api.compliance import (
|
||||
get_prowler_provider_checks,
|
||||
get_prowler_provider_compliance,
|
||||
load_prowler_compliance,
|
||||
load_prowler_checks,
|
||||
generate_scan_compliance,
|
||||
generate_compliance_overview_template,
|
||||
)
|
||||
from api.models import Provider
|
||||
|
||||
|
||||
class TestCompliance:
|
||||
@patch("api.compliance.CheckMetadata")
|
||||
def test_get_prowler_provider_checks(self, mock_check_metadata):
|
||||
provider_type = Provider.ProviderChoices.AWS
|
||||
mock_check_metadata.get_bulk.return_value = {
|
||||
"check1": MagicMock(),
|
||||
"check2": MagicMock(),
|
||||
"check3": MagicMock(),
|
||||
}
|
||||
checks = get_prowler_provider_checks(provider_type)
|
||||
assert set(checks) == {"check1", "check2", "check3"}
|
||||
mock_check_metadata.get_bulk.assert_called_once_with(provider_type)
|
||||
|
||||
@patch("api.compliance.Compliance")
|
||||
def test_get_prowler_provider_compliance(self, mock_compliance):
|
||||
provider_type = Provider.ProviderChoices.AWS
|
||||
mock_compliance.get_bulk.return_value = {
|
||||
"compliance1": MagicMock(),
|
||||
"compliance2": MagicMock(),
|
||||
}
|
||||
compliance_data = get_prowler_provider_compliance(provider_type)
|
||||
assert compliance_data == mock_compliance.get_bulk.return_value
|
||||
mock_compliance.get_bulk.assert_called_once_with(provider_type)
|
||||
|
||||
@patch("api.models.Provider.ProviderChoices")
|
||||
@patch("api.compliance.get_prowler_provider_compliance")
|
||||
@patch("api.compliance.generate_compliance_overview_template")
|
||||
@patch("api.compliance.load_prowler_checks")
|
||||
def test_load_prowler_compliance(
|
||||
self,
|
||||
mock_load_prowler_checks,
|
||||
mock_generate_compliance_overview_template,
|
||||
mock_get_prowler_provider_compliance,
|
||||
mock_provider_choices,
|
||||
):
|
||||
mock_provider_choices.values = ["aws", "azure"]
|
||||
|
||||
compliance_data_aws = {"compliance_aws": MagicMock()}
|
||||
compliance_data_azure = {"compliance_azure": MagicMock()}
|
||||
|
||||
compliance_data_dict = {
|
||||
"aws": compliance_data_aws,
|
||||
"azure": compliance_data_azure,
|
||||
}
|
||||
|
||||
def mock_get_compliance(provider_type):
|
||||
return compliance_data_dict[provider_type]
|
||||
|
||||
mock_get_prowler_provider_compliance.side_effect = mock_get_compliance
|
||||
|
||||
mock_generate_compliance_overview_template.return_value = {
|
||||
"template_key": "template_value"
|
||||
}
|
||||
|
||||
mock_load_prowler_checks.return_value = {"checks_key": "checks_value"}
|
||||
|
||||
load_prowler_compliance()
|
||||
|
||||
from api.compliance import PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE, PROWLER_CHECKS
|
||||
|
||||
assert PROWLER_COMPLIANCE_OVERVIEW_TEMPLATE == {
|
||||
"template_key": "template_value"
|
||||
}
|
||||
assert PROWLER_CHECKS == {"checks_key": "checks_value"}
|
||||
|
||||
expected_prowler_compliance = compliance_data_dict
|
||||
mock_get_prowler_provider_compliance.assert_any_call("aws")
|
||||
mock_get_prowler_provider_compliance.assert_any_call("azure")
|
||||
mock_generate_compliance_overview_template.assert_called_once_with(
|
||||
expected_prowler_compliance
|
||||
)
|
||||
mock_load_prowler_checks.assert_called_once_with(expected_prowler_compliance)
|
||||
|
||||
@patch("api.compliance.get_prowler_provider_checks")
|
||||
@patch("api.models.Provider.ProviderChoices")
|
||||
def test_load_prowler_checks(
|
||||
self, mock_provider_choices, mock_get_prowler_provider_checks
|
||||
):
|
||||
mock_provider_choices.values = ["aws"]
|
||||
|
||||
mock_get_prowler_provider_checks.return_value = ["check1", "check2", "check3"]
|
||||
|
||||
prowler_compliance = {
|
||||
"aws": {
|
||||
"compliance1": MagicMock(
|
||||
Requirements=[
|
||||
MagicMock(
|
||||
Checks=["check1", "check2"],
|
||||
),
|
||||
],
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
expected_checks = {
|
||||
"aws": {
|
||||
"check1": {"compliance1"},
|
||||
"check2": {"compliance1"},
|
||||
"check3": set(),
|
||||
}
|
||||
}
|
||||
|
||||
checks = load_prowler_checks(prowler_compliance)
|
||||
assert checks == expected_checks
|
||||
mock_get_prowler_provider_checks.assert_called_once_with("aws")
|
||||
|
||||
@patch("api.compliance.PROWLER_CHECKS", new_callable=dict)
|
||||
def test_generate_scan_compliance(self, mock_prowler_checks):
|
||||
mock_prowler_checks["aws"] = {
|
||||
"check1": {"compliance1"},
|
||||
"check2": {"compliance1", "compliance2"},
|
||||
}
|
||||
|
||||
compliance_overview = {
|
||||
"compliance1": {
|
||||
"requirements": {
|
||||
"requirement1": {
|
||||
"checks": {"check1": None, "check2": None},
|
||||
"checks_status": {
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"manual": 0,
|
||||
"total": 2,
|
||||
},
|
||||
"status": "PASS",
|
||||
}
|
||||
},
|
||||
"requirements_status": {"passed": 1, "failed": 0, "manual": 0},
|
||||
},
|
||||
"compliance2": {
|
||||
"requirements": {
|
||||
"requirement2": {
|
||||
"checks": {"check2": None},
|
||||
"checks_status": {
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"manual": 0,
|
||||
"total": 1,
|
||||
},
|
||||
"status": "PASS",
|
||||
}
|
||||
},
|
||||
"requirements_status": {"passed": 1, "failed": 0, "manual": 0},
|
||||
},
|
||||
}
|
||||
|
||||
provider_type = "aws"
|
||||
check_id = "check2"
|
||||
status = "FAIL"
|
||||
|
||||
generate_scan_compliance(compliance_overview, provider_type, check_id, status)
|
||||
|
||||
assert (
|
||||
compliance_overview["compliance1"]["requirements"]["requirement1"][
|
||||
"checks"
|
||||
]["check2"]
|
||||
== "FAIL"
|
||||
)
|
||||
assert (
|
||||
compliance_overview["compliance1"]["requirements"]["requirement1"][
|
||||
"checks_status"
|
||||
]["fail"]
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
compliance_overview["compliance1"]["requirements"]["requirement1"]["status"]
|
||||
== "FAIL"
|
||||
)
|
||||
assert compliance_overview["compliance1"]["requirements_status"]["passed"] == 0
|
||||
assert compliance_overview["compliance1"]["requirements_status"]["failed"] == 1
|
||||
|
||||
assert (
|
||||
compliance_overview["compliance2"]["requirements"]["requirement2"][
|
||||
"checks"
|
||||
]["check2"]
|
||||
== "FAIL"
|
||||
)
|
||||
assert (
|
||||
compliance_overview["compliance2"]["requirements"]["requirement2"][
|
||||
"checks_status"
|
||||
]["fail"]
|
||||
== 1
|
||||
)
|
||||
assert (
|
||||
compliance_overview["compliance2"]["requirements"]["requirement2"]["status"]
|
||||
== "FAIL"
|
||||
)
|
||||
assert compliance_overview["compliance2"]["requirements_status"]["passed"] == 0
|
||||
assert compliance_overview["compliance2"]["requirements_status"]["failed"] == 1
|
||||
|
||||
assert (
|
||||
compliance_overview["compliance1"]["requirements"]["requirement1"][
|
||||
"checks"
|
||||
]["check1"]
|
||||
is None
|
||||
)
|
||||
|
||||
@patch("api.models.Provider.ProviderChoices")
|
||||
def test_generate_compliance_overview_template(self, mock_provider_choices):
|
||||
mock_provider_choices.values = ["aws"]
|
||||
|
||||
requirement1 = MagicMock(
|
||||
Id="requirement1",
|
||||
Name="Requirement 1",
|
||||
Description="Description of requirement 1",
|
||||
Attributes=[],
|
||||
Checks=["check1", "check2"],
|
||||
)
|
||||
requirement2 = MagicMock(
|
||||
Id="requirement2",
|
||||
Name="Requirement 2",
|
||||
Description="Description of requirement 2",
|
||||
Attributes=[],
|
||||
Checks=[],
|
||||
)
|
||||
compliance1 = MagicMock(
|
||||
Requirements=[requirement1, requirement2],
|
||||
Framework="Framework 1",
|
||||
Version="1.0",
|
||||
Description="Description of compliance1",
|
||||
)
|
||||
prowler_compliance = {"aws": {"compliance1": compliance1}}
|
||||
|
||||
template = generate_compliance_overview_template(prowler_compliance)
|
||||
|
||||
expected_template = {
|
||||
"aws": {
|
||||
"compliance1": {
|
||||
"framework": "Framework 1",
|
||||
"version": "1.0",
|
||||
"provider": "aws",
|
||||
"description": "Description of compliance1",
|
||||
"requirements": {
|
||||
"requirement1": {
|
||||
"name": "Requirement 1",
|
||||
"description": "Description of requirement 1",
|
||||
"attributes": [],
|
||||
"checks": {"check1": None, "check2": None},
|
||||
"checks_status": {
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"manual": 0,
|
||||
"total": 2,
|
||||
},
|
||||
"status": "PASS",
|
||||
},
|
||||
"requirement2": {
|
||||
"name": "Requirement 2",
|
||||
"description": "Description of requirement 2",
|
||||
"attributes": [],
|
||||
"checks": {},
|
||||
"checks_status": {
|
||||
"pass": 0,
|
||||
"fail": 0,
|
||||
"manual": 0,
|
||||
"total": 0,
|
||||
},
|
||||
"status": "PASS",
|
||||
},
|
||||
},
|
||||
"requirements_status": {
|
||||
"passed": 1, # total_requirements - manual
|
||||
"failed": 0,
|
||||
"manual": 1, # requirement2 has 0 checks
|
||||
},
|
||||
"total_requirements": 2,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert template == expected_template
|
||||
29
api/src/backend/api/tests/test_database.py
Normal file
29
api/src/backend/api/tests/test_database.py
Normal file
@@ -0,0 +1,29 @@
|
||||
import pytest
|
||||
from django.conf import settings
|
||||
from django.db.migrations.recorder import MigrationRecorder
|
||||
from django.db.utils import ConnectionRouter
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.rls import Tenant
|
||||
from config.django.base import DATABASE_ROUTERS as PROD_DATABASE_ROUTERS
|
||||
|
||||
|
||||
class TestMainDatabaseRouter:
|
||||
@pytest.fixture(scope="module")
|
||||
def router(self):
|
||||
testing_routers = settings.DATABASE_ROUTERS.copy()
|
||||
settings.DATABASE_ROUTERS = PROD_DATABASE_ROUTERS
|
||||
yield ConnectionRouter()
|
||||
settings.DATABASE_ROUTERS = testing_routers
|
||||
|
||||
@pytest.mark.parametrize("api_model", [Tenant])
|
||||
def test_router_api_models(self, api_model, router):
|
||||
assert router.db_for_read(api_model) == "default"
|
||||
assert router.db_for_write(api_model) == "default"
|
||||
|
||||
assert router.allow_migrate_model(MainRouter.admin_db, api_model)
|
||||
assert not router.allow_migrate_model("default", api_model)
|
||||
|
||||
def test_router_django_models(self, router):
|
||||
assert router.db_for_read(MigrationRecorder.Migration) == MainRouter.admin_db
|
||||
assert not router.db_for_read(MigrationRecorder.Migration) == "default"
|
||||
108
api/src/backend/api/tests/test_db_utils.py
Normal file
108
api/src/backend/api/tests/test_db_utils.py
Normal file
@@ -0,0 +1,108 @@
|
||||
from datetime import datetime, timezone
|
||||
from enum import Enum
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.db_utils import enum_to_choices, one_week_from_now, generate_random_token
|
||||
|
||||
|
||||
class TestEnumToChoices:
|
||||
def test_enum_to_choices_simple(self):
|
||||
class Color(Enum):
|
||||
RED = 1
|
||||
GREEN = 2
|
||||
BLUE = 3
|
||||
|
||||
expected_result = [
|
||||
(1, "Red"),
|
||||
(2, "Green"),
|
||||
(3, "Blue"),
|
||||
]
|
||||
|
||||
result = enum_to_choices(Color)
|
||||
assert result == expected_result
|
||||
|
||||
def test_enum_to_choices_with_underscores(self):
|
||||
class Status(Enum):
|
||||
PENDING_APPROVAL = "pending"
|
||||
IN_PROGRESS = "in_progress"
|
||||
COMPLETED_SUCCESSFULLY = "completed"
|
||||
|
||||
expected_result = [
|
||||
("pending", "Pending Approval"),
|
||||
("in_progress", "In Progress"),
|
||||
("completed", "Completed Successfully"),
|
||||
]
|
||||
|
||||
result = enum_to_choices(Status)
|
||||
assert result == expected_result
|
||||
|
||||
def test_enum_to_choices_empty_enum(self):
|
||||
class EmptyEnum(Enum):
|
||||
pass
|
||||
|
||||
expected_result = []
|
||||
|
||||
result = enum_to_choices(EmptyEnum)
|
||||
assert result == expected_result
|
||||
|
||||
def test_enum_to_choices_numeric_values(self):
|
||||
class Numbers(Enum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
THREE = 3
|
||||
|
||||
expected_result = [
|
||||
(1, "One"),
|
||||
(2, "Two"),
|
||||
(3, "Three"),
|
||||
]
|
||||
|
||||
result = enum_to_choices(Numbers)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
class TestOneWeekFromNow:
|
||||
def test_one_week_from_now(self):
|
||||
with patch("api.db_utils.datetime") as mock_datetime:
|
||||
mock_datetime.now.return_value = datetime(2023, 1, 1, tzinfo=timezone.utc)
|
||||
expected_result = datetime(2023, 1, 8, tzinfo=timezone.utc)
|
||||
|
||||
result = one_week_from_now()
|
||||
assert result == expected_result
|
||||
|
||||
def test_one_week_from_now_with_timezone(self):
|
||||
with patch("api.db_utils.datetime") as mock_datetime:
|
||||
mock_datetime.now.return_value = datetime(
|
||||
2023, 6, 15, 12, 0, tzinfo=timezone.utc
|
||||
)
|
||||
expected_result = datetime(2023, 6, 22, 12, 0, tzinfo=timezone.utc)
|
||||
|
||||
result = one_week_from_now()
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
class TestGenerateRandomToken:
|
||||
def test_generate_random_token_default_length(self):
|
||||
token = generate_random_token()
|
||||
assert len(token) == 14
|
||||
|
||||
def test_generate_random_token_custom_length(self):
|
||||
length = 20
|
||||
token = generate_random_token(length=length)
|
||||
assert len(token) == length
|
||||
|
||||
def test_generate_random_token_with_symbols(self):
|
||||
symbols = "ABC123"
|
||||
token = generate_random_token(length=10, symbols=symbols)
|
||||
assert len(token) == 10
|
||||
assert all(char in symbols for char in token)
|
||||
|
||||
def test_generate_random_token_unique(self):
|
||||
tokens = {generate_random_token() for _ in range(1000)}
|
||||
# Assuming that generating 1000 tokens should result in unique values
|
||||
assert len(tokens) == 1000
|
||||
|
||||
def test_generate_random_token_no_symbols_provided(self):
|
||||
token = generate_random_token(length=5, symbols="")
|
||||
# Default symbols
|
||||
assert len(token) == 5
|
||||
36
api/src/backend/api/tests/test_decorators.py
Normal file
36
api/src/backend/api/tests/test_decorators.py
Normal file
@@ -0,0 +1,36 @@
|
||||
import uuid
|
||||
from unittest.mock import call, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from api.db_utils import POSTGRES_TENANT_VAR, SET_CONFIG_QUERY
|
||||
from api.decorators import set_tenant
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestSetTenantDecorator:
|
||||
@patch("api.decorators.connection.cursor")
|
||||
def test_set_tenant(self, mock_cursor):
|
||||
mock_cursor.return_value.__enter__.return_value = mock_cursor
|
||||
|
||||
@set_tenant
|
||||
def random_func(arg):
|
||||
return arg
|
||||
|
||||
tenant_id = str(uuid.uuid4())
|
||||
|
||||
result = random_func("test_arg", tenant_id=tenant_id)
|
||||
|
||||
assert (
|
||||
call(SET_CONFIG_QUERY, [POSTGRES_TENANT_VAR, tenant_id])
|
||||
in mock_cursor.execute.mock_calls
|
||||
)
|
||||
assert result == "test_arg"
|
||||
|
||||
def test_set_tenant_exception(self):
|
||||
@set_tenant
|
||||
def random_func(arg):
|
||||
return arg
|
||||
|
||||
with pytest.raises(KeyError):
|
||||
random_func("test_arg")
|
||||
54
api/src/backend/api/tests/test_middleware.py
Normal file
54
api/src/backend/api/tests/test_middleware.py
Normal file
@@ -0,0 +1,54 @@
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
from django.http import HttpResponse
|
||||
from django.test import RequestFactory
|
||||
|
||||
from api.middleware import APILoggingMiddleware
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
@patch("logging.getLogger")
|
||||
def test_api_logging_middleware_logging(mock_logger):
|
||||
factory = RequestFactory()
|
||||
|
||||
request = factory.get("/test-path?param1=value1¶m2=value2")
|
||||
request.method = "GET"
|
||||
|
||||
response = HttpResponse()
|
||||
response.status_code = 200
|
||||
|
||||
get_response = MagicMock(return_value=response)
|
||||
|
||||
with patch("api.middleware.extract_auth_info") as mock_extract_auth_info:
|
||||
mock_extract_auth_info.return_value = {
|
||||
"user_id": "user123",
|
||||
"tenant_id": "tenant456",
|
||||
}
|
||||
|
||||
with patch("api.middleware.logging.getLogger") as mock_get_logger:
|
||||
mock_logger = MagicMock()
|
||||
mock_get_logger.return_value = mock_logger
|
||||
|
||||
middleware = APILoggingMiddleware(get_response)
|
||||
|
||||
with patch("api.middleware.time.time") as mock_time:
|
||||
mock_time.side_effect = [1000.0, 1001.0] # Start time and end time
|
||||
|
||||
middleware(request)
|
||||
|
||||
get_response.assert_called_once_with(request)
|
||||
|
||||
mock_extract_auth_info.assert_called_once_with(request)
|
||||
|
||||
expected_extra = {
|
||||
"user_id": "user123",
|
||||
"tenant_id": "tenant456",
|
||||
"method": "GET",
|
||||
"path": "/test-path",
|
||||
"query_params": {"param1": "value1", "param2": "value2"},
|
||||
"status_code": 200,
|
||||
"duration": 1.0,
|
||||
}
|
||||
|
||||
mock_logger.info.assert_called_once_with("", extra=expected_extra)
|
||||
89
api/src/backend/api/tests/test_models.py
Normal file
89
api/src/backend/api/tests/test_models.py
Normal file
@@ -0,0 +1,89 @@
|
||||
import pytest
|
||||
|
||||
from api.models import Resource, ResourceTag
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
class TestResourceModel:
|
||||
def test_setting_tags(self, providers_fixture):
|
||||
provider, *_ = providers_fixture
|
||||
|
||||
resource = Resource.objects.create(
|
||||
tenant_id=provider.tenant_id,
|
||||
provider=provider,
|
||||
uid="arn:aws:ec2:us-east-1:123456789012:instance/i-1234567890abcdef0",
|
||||
name="My Instance 1",
|
||||
region="us-east-1",
|
||||
service="ec2",
|
||||
type="prowler-test",
|
||||
)
|
||||
|
||||
tags = [
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=provider.tenant_id,
|
||||
key="key",
|
||||
value="value",
|
||||
),
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=provider.tenant_id,
|
||||
key="key2",
|
||||
value="value2",
|
||||
),
|
||||
]
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
assert len(tags) == len(resource.tags.all())
|
||||
|
||||
tags_dict = resource.get_tags()
|
||||
|
||||
for tag in tags:
|
||||
assert tag.key in tags_dict
|
||||
assert tag.value == tags_dict[tag.key]
|
||||
|
||||
def test_adding_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
|
||||
tags = [
|
||||
ResourceTag.objects.create(
|
||||
tenant_id=resource.tenant_id,
|
||||
key="env",
|
||||
value="test",
|
||||
),
|
||||
]
|
||||
before_count = len(resource.tags.all())
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
assert before_count + 1 == len(resource.tags.all())
|
||||
|
||||
tags_dict = resource.get_tags()
|
||||
|
||||
assert "env" in tags_dict
|
||||
assert tags_dict["env"] == "test"
|
||||
|
||||
def test_adding_duplicate_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
|
||||
tags = resource.tags.all()
|
||||
|
||||
before_count = len(resource.tags.all())
|
||||
|
||||
resource.upsert_or_delete_tags(tags)
|
||||
|
||||
# should be the same number of tags
|
||||
assert before_count == len(resource.tags.all())
|
||||
|
||||
def test_add_tags_none(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
resource.upsert_or_delete_tags(None)
|
||||
|
||||
assert len(resource.tags.all()) == 0
|
||||
assert resource.get_tags() == {}
|
||||
|
||||
def test_clear_tags(self, resources_fixture):
|
||||
resource, *_ = resources_fixture
|
||||
resource.clear_tags()
|
||||
|
||||
assert len(resource.tags.all()) == 0
|
||||
assert resource.get_tags() == {}
|
||||
318
api/src/backend/api/tests/test_utils.py
Normal file
318
api/src/backend/api/tests/test_utils.py
Normal file
@@ -0,0 +1,318 @@
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
import pytest
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Invitation
|
||||
from api.models import Provider
|
||||
from api.utils import (
|
||||
merge_dicts,
|
||||
return_prowler_provider,
|
||||
initialize_prowler_provider,
|
||||
prowler_provider_connection_test,
|
||||
get_prowler_provider_kwargs,
|
||||
)
|
||||
from api.utils import validate_invitation
|
||||
|
||||
|
||||
class TestMergeDicts:
|
||||
def test_simple_merge(self):
|
||||
default_dict = {"key1": "value1", "key2": "value2"}
|
||||
replacement_dict = {"key2": "new_value2", "key3": "value3"}
|
||||
expected_result = {"key1": "value1", "key2": "new_value2", "key3": "value3"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_nested_merge(self):
|
||||
default_dict = {
|
||||
"key1": "value1",
|
||||
"key2": {"nested_key1": "nested_value1", "nested_key2": "nested_value2"},
|
||||
}
|
||||
replacement_dict = {
|
||||
"key2": {
|
||||
"nested_key2": "new_nested_value2",
|
||||
"nested_key3": "nested_value3",
|
||||
},
|
||||
"key3": "value3",
|
||||
}
|
||||
expected_result = {
|
||||
"key1": "value1",
|
||||
"key2": {
|
||||
"nested_key1": "nested_value1",
|
||||
"nested_key2": "new_nested_value2",
|
||||
"nested_key3": "nested_value3",
|
||||
},
|
||||
"key3": "value3",
|
||||
}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_no_overlap(self):
|
||||
default_dict = {"key1": "value1"}
|
||||
replacement_dict = {"key2": "value2"}
|
||||
expected_result = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_replacement_dict_empty(self):
|
||||
default_dict = {"key1": "value1", "key2": "value2"}
|
||||
replacement_dict = {}
|
||||
expected_result = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_default_dict_empty(self):
|
||||
default_dict = {}
|
||||
replacement_dict = {"key1": "value1", "key2": "value2"}
|
||||
expected_result = {"key1": "value1", "key2": "value2"}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_nested_empty_in_replacement_dict(self):
|
||||
default_dict = {"key1": {"nested_key1": "nested_value1"}}
|
||||
replacement_dict = {"key1": {}}
|
||||
expected_result = {"key1": {}}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
def test_deep_nested_merge(self):
|
||||
default_dict = {"key1": {"nested_key1": {"deep_key1": "deep_value1"}}}
|
||||
replacement_dict = {"key1": {"nested_key1": {"deep_key1": "new_deep_value1"}}}
|
||||
expected_result = {"key1": {"nested_key1": {"deep_key1": "new_deep_value1"}}}
|
||||
|
||||
result = merge_dicts(default_dict, replacement_dict)
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
class TestReturnProwlerProvider:
|
||||
@pytest.mark.parametrize(
|
||||
"provider_type, expected_provider",
|
||||
[
|
||||
(Provider.ProviderChoices.AWS.value, AwsProvider),
|
||||
(Provider.ProviderChoices.GCP.value, GcpProvider),
|
||||
(Provider.ProviderChoices.AZURE.value, AzureProvider),
|
||||
(Provider.ProviderChoices.KUBERNETES.value, KubernetesProvider),
|
||||
],
|
||||
)
|
||||
def test_return_prowler_provider(self, provider_type, expected_provider):
|
||||
provider = MagicMock()
|
||||
provider.provider = provider_type
|
||||
prowler_provider = return_prowler_provider(provider)
|
||||
assert prowler_provider == expected_provider
|
||||
|
||||
def test_return_prowler_provider_unsupported_provider(self):
|
||||
provider = MagicMock()
|
||||
provider.provider = "UNSUPPORTED_PROVIDER"
|
||||
with pytest.raises(ValueError):
|
||||
return return_prowler_provider(provider)
|
||||
|
||||
|
||||
class TestInitializeProwlerProvider:
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_initialize_prowler_provider(self, mock_return_prowler_provider):
|
||||
provider = MagicMock()
|
||||
provider.secret.secret = {"key": "value"}
|
||||
mock_return_prowler_provider.return_value = MagicMock()
|
||||
|
||||
initialize_prowler_provider(provider)
|
||||
mock_return_prowler_provider.return_value.assert_called_once_with(key="value")
|
||||
|
||||
|
||||
class TestProwlerProviderConnectionTest:
|
||||
@patch("api.utils.return_prowler_provider")
|
||||
def test_prowler_provider_connection_test(self, mock_return_prowler_provider):
|
||||
provider = MagicMock()
|
||||
provider.uid = "1234567890"
|
||||
provider.secret.secret = {"key": "value"}
|
||||
mock_return_prowler_provider.return_value = MagicMock()
|
||||
|
||||
prowler_provider_connection_test(provider)
|
||||
mock_return_prowler_provider.return_value.test_connection.assert_called_once_with(
|
||||
key="value", provider_id="1234567890", raise_on_exception=False
|
||||
)
|
||||
|
||||
|
||||
class TestGetProwlerProviderKwargs:
|
||||
@pytest.mark.parametrize(
|
||||
"provider_type, expected_extra_kwargs",
|
||||
[
|
||||
(
|
||||
Provider.ProviderChoices.AWS.value,
|
||||
{},
|
||||
),
|
||||
(
|
||||
Provider.ProviderChoices.AZURE.value,
|
||||
{"subscription_ids": ["provider_uid"]},
|
||||
),
|
||||
(
|
||||
Provider.ProviderChoices.GCP.value,
|
||||
{"project_ids": ["provider_uid"]},
|
||||
),
|
||||
(
|
||||
Provider.ProviderChoices.KUBERNETES.value,
|
||||
{"context": "provider_uid"},
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_prowler_provider_kwargs(self, provider_type, expected_extra_kwargs):
|
||||
provider_uid = "provider_uid"
|
||||
secret_dict = {"key": "value"}
|
||||
secret_mock = MagicMock()
|
||||
secret_mock.secret = secret_dict
|
||||
|
||||
provider = MagicMock()
|
||||
provider.provider = provider_type
|
||||
provider.secret = secret_mock
|
||||
provider.uid = provider_uid
|
||||
|
||||
result = get_prowler_provider_kwargs(provider)
|
||||
|
||||
expected_result = {**secret_dict, **expected_extra_kwargs}
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_unsupported_provider(self):
|
||||
# Setup
|
||||
provider_uid = "provider_uid"
|
||||
secret_dict = {"key": "value"}
|
||||
secret_mock = MagicMock()
|
||||
secret_mock.secret = secret_dict
|
||||
|
||||
provider = MagicMock()
|
||||
provider.provider = "UNSUPPORTED_PROVIDER"
|
||||
provider.secret = secret_mock
|
||||
provider.uid = provider_uid
|
||||
|
||||
result = get_prowler_provider_kwargs(provider)
|
||||
|
||||
expected_result = secret_dict.copy()
|
||||
assert result == expected_result
|
||||
|
||||
def test_get_prowler_provider_kwargs_no_secret(self):
|
||||
# Setup
|
||||
provider_uid = "provider_uid"
|
||||
secret_mock = MagicMock()
|
||||
secret_mock.secret = {}
|
||||
|
||||
provider = MagicMock()
|
||||
provider.provider = Provider.ProviderChoices.AWS.value
|
||||
provider.secret = secret_mock
|
||||
provider.uid = provider_uid
|
||||
|
||||
result = get_prowler_provider_kwargs(provider)
|
||||
|
||||
expected_result = {}
|
||||
assert result == expected_result
|
||||
|
||||
|
||||
class TestValidateInvitation:
|
||||
@pytest.fixture
|
||||
def invitation(self):
|
||||
invitation = MagicMock(spec=Invitation)
|
||||
invitation.token = "VALID_TOKEN"
|
||||
invitation.email = "user@example.com"
|
||||
invitation.expires_at = datetime.now(timezone.utc) + timedelta(days=1)
|
||||
invitation.state = Invitation.State.PENDING
|
||||
invitation.tenant = MagicMock()
|
||||
return invitation
|
||||
|
||||
def test_valid_invitation(self, invitation):
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using:
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.return_value = invitation
|
||||
|
||||
result = validate_invitation("VALID_TOKEN", "user@example.com")
|
||||
|
||||
assert result == invitation
|
||||
mock_db.get.assert_called_once_with(
|
||||
token="VALID_TOKEN", email="user@example.com"
|
||||
)
|
||||
|
||||
def test_invitation_not_found_raises_validation_error(self):
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using:
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.side_effect = Invitation.DoesNotExist
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
validate_invitation("INVALID_TOKEN", "user@example.com")
|
||||
|
||||
assert exc_info.value.detail == {
|
||||
"invitation_token": "Invalid invitation code."
|
||||
}
|
||||
mock_db.get.assert_called_once_with(
|
||||
token="INVALID_TOKEN", email="user@example.com"
|
||||
)
|
||||
|
||||
def test_invitation_not_found_raises_not_found(self):
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using:
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.side_effect = Invitation.DoesNotExist
|
||||
|
||||
with pytest.raises(NotFound) as exc_info:
|
||||
validate_invitation(
|
||||
"INVALID_TOKEN", "user@example.com", raise_not_found=True
|
||||
)
|
||||
|
||||
assert exc_info.value.detail == "Invitation is not valid."
|
||||
mock_db.get.assert_called_once_with(
|
||||
token="INVALID_TOKEN", email="user@example.com"
|
||||
)
|
||||
|
||||
def test_invitation_expired(self, invitation):
|
||||
expired_time = datetime.now(timezone.utc) - timedelta(days=1)
|
||||
invitation.expires_at = expired_time
|
||||
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using, patch(
|
||||
"api.utils.datetime"
|
||||
) as mock_datetime:
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.return_value = invitation
|
||||
mock_datetime.now.return_value = datetime.now(timezone.utc)
|
||||
|
||||
with pytest.raises(InvitationTokenExpiredException):
|
||||
validate_invitation("VALID_TOKEN", "user@example.com")
|
||||
|
||||
# Ensure the invitation state was updated to EXPIRED
|
||||
assert invitation.state == Invitation.State.EXPIRED
|
||||
invitation.save.assert_called_once_with(using=MainRouter.admin_db)
|
||||
|
||||
def test_invitation_not_pending(self, invitation):
|
||||
invitation.state = Invitation.State.ACCEPTED
|
||||
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using:
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.return_value = invitation
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
validate_invitation("VALID_TOKEN", "user@example.com")
|
||||
|
||||
assert exc_info.value.detail == {
|
||||
"invitation_token": "This invitation is no longer valid."
|
||||
}
|
||||
|
||||
def test_invitation_with_different_email(self):
|
||||
with patch("api.utils.Invitation.objects.using") as mock_using:
|
||||
mock_db = mock_using.return_value
|
||||
mock_db.get.side_effect = Invitation.DoesNotExist
|
||||
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
validate_invitation("VALID_TOKEN", "different@example.com")
|
||||
|
||||
assert exc_info.value.detail == {
|
||||
"invitation_token": "Invalid invitation code."
|
||||
}
|
||||
mock_db.get.assert_called_once_with(
|
||||
token="VALID_TOKEN", email="different@example.com"
|
||||
)
|
||||
113
api/src/backend/api/tests/test_uuid_utils.py
Normal file
113
api/src/backend/api/tests/test_uuid_utils.py
Normal file
@@ -0,0 +1,113 @@
|
||||
from datetime import datetime, timezone
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from uuid6 import UUID
|
||||
|
||||
from api.uuid_utils import (
|
||||
transform_into_uuid7,
|
||||
datetime_to_uuid7,
|
||||
datetime_from_uuid7,
|
||||
uuid7_start,
|
||||
uuid7_end,
|
||||
uuid7_range,
|
||||
)
|
||||
|
||||
|
||||
def test_transform_into_uuid7_valid():
|
||||
uuid_v7 = datetime_to_uuid7(datetime.now(timezone.utc))
|
||||
transformed_uuid = transform_into_uuid7(uuid_v7)
|
||||
assert transformed_uuid == UUID(hex=uuid_v7.hex.upper())
|
||||
assert transformed_uuid.version == 7
|
||||
|
||||
|
||||
def test_transform_into_uuid7_invalid_version():
|
||||
uuid_v4 = uuid4()
|
||||
with pytest.raises(ValidationError) as exc_info:
|
||||
transform_into_uuid7(UUID(str(uuid_v4)))
|
||||
assert str(exc_info.value.detail[0]) == "Invalid UUIDv7 value."
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_datetime",
|
||||
[
|
||||
datetime(2024, 9, 11, 7, 20, 27, tzinfo=timezone.utc),
|
||||
datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
],
|
||||
)
|
||||
def test_datetime_to_uuid7(input_datetime):
|
||||
uuid7 = datetime_to_uuid7(input_datetime)
|
||||
assert isinstance(uuid7, UUID)
|
||||
assert uuid7.version == 7
|
||||
expected_timestamp_ms = int(input_datetime.timestamp() * 1000) & 0xFFFFFFFFFFFF
|
||||
assert uuid7.time == expected_timestamp_ms
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"input_datetime",
|
||||
[
|
||||
datetime(2024, 9, 11, 7, 20, 27, tzinfo=timezone.utc),
|
||||
datetime(2023, 1, 1, 0, 0, 0, tzinfo=timezone.utc),
|
||||
],
|
||||
)
|
||||
def test_datetime_from_uuid7(input_datetime):
|
||||
uuid7 = datetime_to_uuid7(input_datetime)
|
||||
extracted_datetime = datetime_from_uuid7(uuid7)
|
||||
assert extracted_datetime == input_datetime
|
||||
|
||||
|
||||
def test_datetime_from_uuid7_invalid():
|
||||
uuid_v4 = uuid4()
|
||||
with pytest.raises(ValueError):
|
||||
datetime_from_uuid7(UUID(str(uuid_v4)))
|
||||
|
||||
|
||||
def test_uuid7_start():
|
||||
dt = datetime.now(timezone.utc)
|
||||
uuid = datetime_to_uuid7(dt)
|
||||
start_uuid = uuid7_start(uuid)
|
||||
expected_dt = dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
expected_timestamp_ms = int(expected_dt.timestamp() * 1000) & 0xFFFFFFFFFFFF
|
||||
assert start_uuid.time == expected_timestamp_ms
|
||||
assert start_uuid.version == 7
|
||||
|
||||
|
||||
@pytest.mark.parametrize("months_offset", [0, 1, 10, 30, 60])
|
||||
def test_uuid7_end(months_offset):
|
||||
dt = datetime.now(timezone.utc)
|
||||
uuid = datetime_to_uuid7(dt)
|
||||
end_uuid = uuid7_end(uuid, months_offset)
|
||||
expected_dt = dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
expected_dt += relativedelta(months=months_offset, microseconds=-1)
|
||||
expected_timestamp_ms = int(expected_dt.timestamp() * 1000) & 0xFFFFFFFFFFFF
|
||||
assert end_uuid.time == expected_timestamp_ms
|
||||
assert end_uuid.version == 7
|
||||
|
||||
|
||||
def test_uuid7_range():
|
||||
dt_now = datetime.now(timezone.utc)
|
||||
uuid_list = [
|
||||
datetime_to_uuid7(dt_now),
|
||||
datetime_to_uuid7(dt_now.replace(year=2023)),
|
||||
datetime_to_uuid7(dt_now.replace(year=2024)),
|
||||
datetime_to_uuid7(dt_now.replace(year=2025)),
|
||||
]
|
||||
start_uuid, end_uuid = uuid7_range(uuid_list)
|
||||
|
||||
# Expected start of range
|
||||
start_dt = datetime_from_uuid7(min(uuid_list, key=lambda u: u.time))
|
||||
start_dt = start_dt.replace(hour=0, minute=0, second=0, microsecond=0)
|
||||
expected_start_timestamp_ms = int(start_dt.timestamp() * 1000) & 0xFFFFFFFFFFFF
|
||||
|
||||
# Expected end of range
|
||||
end_dt = datetime_from_uuid7(max(uuid_list, key=lambda u: u.time))
|
||||
end_dt = end_dt.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
end_dt += relativedelta(months=1, microseconds=-1)
|
||||
expected_end_timestamp_ms = int(end_dt.timestamp() * 1000) & 0xFFFFFFFFFFFF
|
||||
|
||||
assert start_uuid.time == expected_start_timestamp_ms
|
||||
assert end_uuid.time == expected_end_timestamp_ms
|
||||
assert start_uuid.version == 7
|
||||
assert end_uuid.version == 7
|
||||
3421
api/src/backend/api/tests/test_views.py
Normal file
3421
api/src/backend/api/tests/test_views.py
Normal file
File diff suppressed because it is too large
Load Diff
189
api/src/backend/api/utils.py
Normal file
189
api/src/backend/api/utils.py
Normal file
@@ -0,0 +1,189 @@
|
||||
from datetime import datetime, timezone
|
||||
|
||||
from prowler.providers.aws.aws_provider import AwsProvider
|
||||
from prowler.providers.azure.azure_provider import AzureProvider
|
||||
from prowler.providers.common.models import Connection
|
||||
from prowler.providers.gcp.gcp_provider import GcpProvider
|
||||
from prowler.providers.kubernetes.kubernetes_provider import KubernetesProvider
|
||||
from rest_framework.exceptions import ValidationError, NotFound
|
||||
|
||||
from api.db_router import MainRouter
|
||||
from api.exceptions import InvitationTokenExpiredException
|
||||
from api.models import Provider, Invitation
|
||||
|
||||
|
||||
def merge_dicts(default_dict: dict, replacement_dict: dict) -> dict:
|
||||
"""
|
||||
Recursively merge two dictionaries, using `default_dict` as the base and `replacement_dict` for overriding values.
|
||||
|
||||
Args:
|
||||
default_dict (dict): The base dictionary containing default key-value pairs.
|
||||
replacement_dict (dict): The dictionary containing values that should override those in `default_dict`.
|
||||
|
||||
Returns:
|
||||
dict: A new dictionary containing all keys from `default_dict` with values from `replacement_dict` replacing
|
||||
any overlapping keys. If a key in both `default_dict` and `replacement_dict` contains dictionaries,
|
||||
this function will merge them recursively.
|
||||
"""
|
||||
result = default_dict.copy()
|
||||
|
||||
for key, value in replacement_dict.items():
|
||||
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
|
||||
if value:
|
||||
result[key] = merge_dicts(result[key], value)
|
||||
else:
|
||||
result[key] = value
|
||||
else:
|
||||
result[key] = value
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def return_prowler_provider(
|
||||
provider: Provider,
|
||||
) -> [AwsProvider | AzureProvider | GcpProvider | KubernetesProvider]:
|
||||
"""Return the Prowler provider class based on the given provider type.
|
||||
|
||||
Args:
|
||||
provider (Provider): The provider object containing the provider type and associated secrets.
|
||||
|
||||
Returns:
|
||||
AwsProvider | AzureProvider | GcpProvider | KubernetesProvider: The corresponding provider class.
|
||||
|
||||
Raises:
|
||||
ValueError: If the provider type specified in `provider.provider` is not supported.
|
||||
"""
|
||||
match provider.provider:
|
||||
case Provider.ProviderChoices.AWS.value:
|
||||
prowler_provider = AwsProvider
|
||||
case Provider.ProviderChoices.GCP.value:
|
||||
prowler_provider = GcpProvider
|
||||
case Provider.ProviderChoices.AZURE.value:
|
||||
prowler_provider = AzureProvider
|
||||
case Provider.ProviderChoices.KUBERNETES.value:
|
||||
prowler_provider = KubernetesProvider
|
||||
case _:
|
||||
raise ValueError(f"Provider type {provider.provider} not supported")
|
||||
return prowler_provider
|
||||
|
||||
|
||||
def get_prowler_provider_kwargs(provider: Provider) -> dict:
|
||||
"""Get the Prowler provider kwargs based on the given provider type.
|
||||
|
||||
Args:
|
||||
provider (Provider): The provider object containing the provider type and associated secret.
|
||||
|
||||
Returns:
|
||||
dict: The provider kwargs for the corresponding provider class.
|
||||
"""
|
||||
prowler_provider_kwargs = provider.secret.secret
|
||||
if provider.provider == Provider.ProviderChoices.AZURE.value:
|
||||
prowler_provider_kwargs = {
|
||||
**prowler_provider_kwargs,
|
||||
"subscription_ids": [provider.uid],
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.GCP.value:
|
||||
prowler_provider_kwargs = {
|
||||
**prowler_provider_kwargs,
|
||||
"project_ids": [provider.uid],
|
||||
}
|
||||
elif provider.provider == Provider.ProviderChoices.KUBERNETES.value:
|
||||
prowler_provider_kwargs = {**prowler_provider_kwargs, "context": provider.uid}
|
||||
return prowler_provider_kwargs
|
||||
|
||||
|
||||
def initialize_prowler_provider(
|
||||
provider: Provider,
|
||||
) -> AwsProvider | AzureProvider | GcpProvider | KubernetesProvider:
|
||||
"""Initialize a Prowler provider instance based on the given provider type.
|
||||
|
||||
Args:
|
||||
provider (Provider): The provider object containing the provider type and associated secrets.
|
||||
|
||||
Returns:
|
||||
AwsProvider | AzureProvider | GcpProvider | KubernetesProvider: An instance of the corresponding provider class
|
||||
(`AwsProvider`, `AzureProvider`, `GcpProvider`, or `KubernetesProvider`) initialized with the
|
||||
provider's secrets.
|
||||
"""
|
||||
prowler_provider = return_prowler_provider(provider)
|
||||
prowler_provider_kwargs = get_prowler_provider_kwargs(provider)
|
||||
return prowler_provider(**prowler_provider_kwargs)
|
||||
|
||||
|
||||
def prowler_provider_connection_test(provider: Provider) -> Connection:
|
||||
"""Test the connection to a Prowler provider based on the given provider type.
|
||||
|
||||
Args:
|
||||
provider (Provider): The provider object containing the provider type and associated secrets.
|
||||
|
||||
Returns:
|
||||
Connection: A connection object representing the result of the connection test for the specified provider.
|
||||
"""
|
||||
prowler_provider = return_prowler_provider(provider)
|
||||
prowler_provider_kwargs = provider.secret.secret
|
||||
return prowler_provider.test_connection(
|
||||
**prowler_provider_kwargs, provider_id=provider.uid, raise_on_exception=False
|
||||
)
|
||||
|
||||
|
||||
def validate_invitation(
|
||||
invitation_token: str, email: str, raise_not_found=False
|
||||
) -> Invitation:
|
||||
"""
|
||||
Validates an invitation based on the provided token and email.
|
||||
|
||||
This function attempts to retrieve an Invitation object using the given
|
||||
`invitation_token` and `email`. It performs several checks to ensure that
|
||||
the invitation is valid, not expired, and in the correct state for acceptance.
|
||||
|
||||
Args:
|
||||
invitation_token (str): The token associated with the invitation.
|
||||
email (str): The email address associated with the invitation.
|
||||
raise_not_found (bool, optional): If True, raises a `NotFound` exception
|
||||
when the invitation is not found. If False, raises a `ValidationError`.
|
||||
Defaults to False.
|
||||
|
||||
Returns:
|
||||
Invitation: The validated Invitation object.
|
||||
|
||||
Raises:
|
||||
NotFound: If `raise_not_found` is True and the invitation does not exist.
|
||||
ValidationError: If the invitation does not exist and `raise_not_found`
|
||||
is False, or if the invitation is invalid or in an incorrect state.
|
||||
InvitationTokenExpiredException: If the invitation has expired.
|
||||
|
||||
Notes:
|
||||
- This function uses the admin database connector to bypass RLS protection
|
||||
since the invitation may belong to a tenant the user is not a member of yet.
|
||||
- If the invitation has expired, its state is updated to EXPIRED, and an
|
||||
`InvitationTokenExpiredException` is raised.
|
||||
- Only invitations in the PENDING state can be accepted.
|
||||
|
||||
Examples:
|
||||
invitation = validate_invitation("TOKEN123", "user@example.com")
|
||||
"""
|
||||
try:
|
||||
# Admin DB connector is used to bypass RLS protection since the invitation belongs to a tenant the user
|
||||
# is not a member of yet
|
||||
invitation = Invitation.objects.using(MainRouter.admin_db).get(
|
||||
token=invitation_token, email=email
|
||||
)
|
||||
except Invitation.DoesNotExist:
|
||||
if raise_not_found:
|
||||
raise NotFound(detail="Invitation is not valid.")
|
||||
else:
|
||||
raise ValidationError({"invitation_token": "Invalid invitation code."})
|
||||
|
||||
# Check if the invitation has expired
|
||||
if invitation.expires_at < datetime.now(timezone.utc):
|
||||
invitation.state = Invitation.State.EXPIRED
|
||||
invitation.save(using=MainRouter.admin_db)
|
||||
raise InvitationTokenExpiredException()
|
||||
|
||||
# Check the state of the invitation
|
||||
if invitation.state != Invitation.State.PENDING:
|
||||
raise ValidationError(
|
||||
{"invitation_token": "This invitation is no longer valid."}
|
||||
)
|
||||
|
||||
return invitation
|
||||
148
api/src/backend/api/uuid_utils.py
Normal file
148
api/src/backend/api/uuid_utils.py
Normal file
@@ -0,0 +1,148 @@
|
||||
from datetime import datetime, timezone
|
||||
from random import getrandbits
|
||||
|
||||
from dateutil.relativedelta import relativedelta
|
||||
from rest_framework_json_api.serializers import ValidationError
|
||||
from uuid6 import UUID
|
||||
|
||||
|
||||
def transform_into_uuid7(uuid_obj: UUID) -> UUID:
|
||||
"""
|
||||
Validates that the given UUID object is a UUIDv7 and returns it.
|
||||
|
||||
This function checks if the provided UUID object is of version 7.
|
||||
If it is, it returns a new UUID object constructed from the uppercase
|
||||
hexadecimal representation of the input UUID. If not, it raises a ValidationError.
|
||||
|
||||
Args:
|
||||
uuid_obj (UUID): The UUID object to validate and transform.
|
||||
|
||||
Returns:
|
||||
UUID: A new UUIDv7 object constructed from the uppercase hexadecimal
|
||||
representation of the input UUID.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the provided UUID is not a version 7 UUID.
|
||||
"""
|
||||
try:
|
||||
if uuid_obj.version != 7:
|
||||
raise ValueError
|
||||
return UUID(hex=uuid_obj.hex.upper())
|
||||
except ValueError:
|
||||
raise ValidationError("Invalid UUIDv7 value.")
|
||||
|
||||
|
||||
def datetime_to_uuid7(dt: datetime) -> UUID:
|
||||
"""
|
||||
Generates a UUIDv7 from a given datetime object.
|
||||
|
||||
Constructs a UUIDv7 using the provided datetime timestamp.
|
||||
Ensures that the version and variant bits are set correctly.
|
||||
|
||||
Args:
|
||||
dt: A datetime object representing the desired timestamp for the UUIDv7.
|
||||
|
||||
Returns:
|
||||
A UUIDv7 object corresponding to the given datetime.
|
||||
"""
|
||||
timestamp_ms = int(dt.timestamp() * 1000) & 0xFFFFFFFFFFFF # 48 bits
|
||||
|
||||
# Generate 12 bits of randomness for the sequence
|
||||
rand_seq = getrandbits(12)
|
||||
# Generate 62 bits of randomness for the node
|
||||
rand_node = getrandbits(62)
|
||||
|
||||
# Build the UUID integer
|
||||
uuid_int = timestamp_ms << 80 # Shift timestamp to bits 80-127
|
||||
|
||||
# Set the version to 7 in bits 76-79
|
||||
uuid_int |= 0x7 << 76
|
||||
|
||||
# Set 12 bits of randomness in bits 64-75
|
||||
uuid_int |= rand_seq << 64
|
||||
|
||||
# Set the variant to "10" in bits 62-63
|
||||
uuid_int |= 0x2 << 62
|
||||
|
||||
# Set 62 bits of randomness in bits 0-61
|
||||
uuid_int |= rand_node
|
||||
|
||||
return UUID(int=uuid_int)
|
||||
|
||||
|
||||
def datetime_from_uuid7(uuid7: UUID) -> datetime:
|
||||
"""
|
||||
Extracts the timestamp from a UUIDv7 and returns it as a datetime object.
|
||||
|
||||
Args:
|
||||
uuid7: A UUIDv7 object.
|
||||
|
||||
Returns:
|
||||
A datetime object representing the timestamp encoded in the UUIDv7.
|
||||
"""
|
||||
timestamp_ms = uuid7.time
|
||||
return datetime.fromtimestamp(timestamp_ms / 1000, tz=timezone.utc)
|
||||
|
||||
|
||||
def uuid7_start(uuid_obj: UUID) -> UUID:
|
||||
"""
|
||||
Returns a UUIDv7 that represents the start of the day for the given UUID.
|
||||
|
||||
Args:
|
||||
uuid_obj: A UUIDv7 object.
|
||||
|
||||
Returns:
|
||||
A UUIDv7 object representing the start of the day for the given UUID's timestamp.
|
||||
"""
|
||||
start_of_day = datetime_from_uuid7(uuid_obj).replace(
|
||||
hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
return datetime_to_uuid7(start_of_day)
|
||||
|
||||
|
||||
def uuid7_end(uuid_obj: UUID, offset_months: int = 1) -> UUID:
|
||||
"""
|
||||
Returns a UUIDv7 that represents the end of the month for the given UUID.
|
||||
|
||||
Args:
|
||||
uuid_obj: A UUIDv7 object.
|
||||
offset_days: Number of months to offset from the given UUID's date. Defaults to 1 to handle if
|
||||
partitions are not being used, if so the value will be the one set at FINDINGS_TABLE_PARTITION_MONTHS.
|
||||
|
||||
Returns:
|
||||
A UUIDv7 object representing the end of the month for the given UUID's date plus offset_months.
|
||||
"""
|
||||
end_of_month = datetime_from_uuid7(uuid_obj).replace(
|
||||
day=1, hour=0, minute=0, second=0, microsecond=0
|
||||
)
|
||||
end_of_month += relativedelta(months=offset_months, microseconds=-1)
|
||||
return datetime_to_uuid7(end_of_month)
|
||||
|
||||
|
||||
def uuid7_range(uuid_list: list[UUID]) -> list[UUID]:
|
||||
"""
|
||||
For the given list of UUIDv7s, returns the start and end UUIDv7 values that represent
|
||||
the range of days covered by the UUIDs.
|
||||
|
||||
Args:
|
||||
uuid_list: A list of UUIDv7 objects.
|
||||
|
||||
Returns:
|
||||
A list containing two UUIDv7 objects: the start and end of the day range.
|
||||
|
||||
Raises:
|
||||
ValidationError: If the list is empty or contains invalid UUIDv7 objects.
|
||||
"""
|
||||
if not uuid_list:
|
||||
raise ValidationError("UUID list is empty.")
|
||||
|
||||
try:
|
||||
start_uuid = min(uuid_list, key=lambda u: u.time)
|
||||
end_uuid = max(uuid_list, key=lambda u: u.time)
|
||||
except AttributeError:
|
||||
raise ValidationError("Invalid UUIDv7 objects in the list.")
|
||||
|
||||
start_range = uuid7_start(start_uuid)
|
||||
end_range = uuid7_end(end_uuid)
|
||||
|
||||
return [start_range, end_range]
|
||||
0
api/src/backend/api/v1/__init__.py
Normal file
0
api/src/backend/api/v1/__init__.py
Normal file
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user