diff --git a/Pipfile b/Pipfile index 363a2cc..922af4f 100644 --- a/Pipfile +++ b/Pipfile @@ -6,7 +6,7 @@ name = "pypi" [packages] crowdstrike-falconpy = "*" docker = ">=5.0.3" -retry = ">=0.9.2" +tenacity = ">=9.1.0" requests = ">=2.32.0" urllib3 = ">=1.26.0" diff --git a/Pipfile.lock b/Pipfile.lock index 6cc4381..8d68ab9 100644 --- a/Pipfile.lock +++ b/Pipfile.lock @@ -1,7 +1,7 @@ { "_meta": { "hash": { - "sha256": "72353487b802fbd299f52bc10a5fe709da554ca258b7142f9d4ac2f8a0e3066e" + "sha256": "77113b96ea5d6b1be6cbe89beb2e1a5ee6ee4aa9896673219ef1d5dc886bba34" }, "pipfile-spec": 6, "requires": { @@ -18,124 +18,117 @@ "default": { "certifi": { "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" + "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", + "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b" ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2024.7.4" + "markers": "python_version >= '3.7'", + "version": "==2025.6.15" }, "charset-normalizer": { "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" + "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4", + "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45", + "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", + "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", + "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", + "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", + "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d", + "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", + "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184", + "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", + "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b", + "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64", + "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", + "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", + "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", + "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344", + "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58", + "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", + "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471", + "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", + "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", + "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836", + "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", + "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", + "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", + "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1", + "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01", + "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", + "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58", + "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", + "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", + "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2", + "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a", + "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597", + "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", + "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5", + "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb", + "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f", + "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", + "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", + "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", + "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", + "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7", + "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7", + "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455", + "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", + "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4", + "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", + "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3", + "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", + "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", + "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", + "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", + "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", + "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", + "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", + "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12", + "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa", + "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", + "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", + "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f", + "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", + "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", + "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5", + "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02", + "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", + "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", + "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e", + "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", + "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", + "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", + "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", + "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681", + "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba", + "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", + "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a", + "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", + "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", + "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", + "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", + "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027", + "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7", + "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518", + "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", + "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", + "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", + "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", + "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da", + "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", + "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f", + "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", + "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f" ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" + "markers": "python_version >= '3.7'", + "version": "==3.4.2" }, "crowdstrike-falconpy": { "hashes": [ - "sha256:033df072d30c1522b04fdc27d3cc478690a3b320948e12290ee60ca6f1eea816", - "sha256:518a215f0d7e5382392ddf7e3e6e150e1701a21e9b85af985da38fd3c0071ae1" + "sha256:138c2110c471d750da4a1339cf97327da122b3dbf091216db68e9f93758b3b6f", + "sha256:35a59b304f43fa51fb7353d224f4fab7d7bd41095de496466e84fc9fc3c46d41" ], "index": "pypi", - "version": "==1.4.3" - }, - "decorator": { - "hashes": [ - "sha256:637996211036b6385ef91435e4fae22989472f9d571faba8927ba8253acbc330", - "sha256:b8c3f85900b9dc423225913c5aace94729fe1fa9763b38939a95226f02d37186" - ], - "markers": "python_version >= '3.5'", - "version": "==5.1.1" + "version": "==1.5.3" }, "docker": { "hashes": [ @@ -147,61 +140,35 @@ }, "idna": { "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" - ], - "markers": "python_version >= '3.5'", - "version": "==3.7" - }, - "packaging": { - "hashes": [ - "sha256:048fb0e9405036518eaaf48a55953c750c11e1a1b68e0dd1a9d62ed0c092cfc5", - "sha256:8c491190033a9af7e1d931d0b5dacc2ef47509b34dd0de67ed209b5203fc88c7" + "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", + "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3" ], - "markers": "python_version >= '3.7'", - "version": "==23.2" - }, - "py": { - "hashes": [ - "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719", - "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3, 3.4'", - "version": "==1.11.0" + "markers": "python_version >= '3.6'", + "version": "==3.10" }, "requests": { "hashes": [ - "sha256:dd951ff5ecf3e3b3aa26b40703ba77495dab41da839ae72ef3c8e5d8e2433289", - "sha256:fc06670dd0ed212426dfeb94fc1b983d917c4f9847c863f313c9dfaaffb7c23c" + "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", + "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422" ], "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==2.32.2" + "version": "==2.32.4" }, - "retry": { + "tenacity": { "hashes": [ - "sha256:ccddf89761fa2c726ab29391837d4327f819ea14d244c232a1d24c67a2f98606", - "sha256:f8bfa8b99b69c4506d6f5bd3b0aabf77f98cdb17f3c9fc3f5ca820033336fba4" + "sha256:1169d376c297e7de388d18b4481760d478b0e99a777cad3a9c86e556f4b697cb", + "sha256:f77bf36710d8b73a50b2dd155c97b870017ad21afe6ab300326b0371b3b05138" ], "index": "pypi", - "version": "==0.9.2" + "version": "==9.1.2" }, "urllib3": { "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" + "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", + "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc" ], "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.2.2" - }, - "websocket-client": { - "hashes": [ - "sha256:084072e0a7f5f347ef2ac3d8698a5e0b4ffbfcab607628cadabc650fc9a83a24", - "sha256:b3324019b3c28572086c4a319f91d1dcd44e6e11cd340232978c684a7650d0df" - ], - "markers": "python_version >= '3.8'", - "version": "==1.6.4" + "version": "==2.5.0" } }, "develop": {} diff --git a/README.md b/README.md index 22a937e..221fbf8 100644 --- a/README.md +++ b/README.md @@ -79,9 +79,12 @@ pip3 install -r requirements.txt $ python3 cs_scanimage.py --help usage: cs_scanimage.py [-h] -u CLIENT_ID -r REPO [-t TAG] [-c {us-1,us-2,eu-1,us-gov-1}] [-s SCORE] + [-st {low,medium,high,critical}] [-sm {default,cps}] + [--ignore-unfixed ] [--cve-ignore-file FILE] [--json-report REPORT] [--log-level {DEBUG,INFO,WARNING,ERROR,CRITICAL}] [-R RETRY_COUNT] [--plugin] [--user-agent USERAGENT] + [--skip-push] optional arguments: -h, --help show this help message and exit @@ -91,6 +94,7 @@ optional arguments: --plugin Prints the report as json to stdout --user-agent USERAGENT HTTP User agent to use for API calls + --skip-push Skip image push required arguments: -u CLIENT_ID, --clientid CLIENT_ID @@ -101,6 +105,14 @@ required arguments: CrowdStrike cloud region -s SCORE, --score_threshold SCORE Vulnerability score threshold + -st {low,medium,high,critical}, --severity_threshold {low,medium,high,critical} + Vulnerability severity threshold (default: low) + -sm {default,cps}, --severity_mode {default,cps} + Vulnerability severity compute mode (default: default) + --ignore-unfixed + Ignore unfixed vulnerabilities + --cve-ignore-file FILE + Path of the file containing CVEs to ignore -R RETRY_COUNT, --retry_count RETRY_COUNT Scan report retry count ``` @@ -112,6 +124,76 @@ required arguments: > > Establishing and retrieving API credentials can be performed at . +## Advanced Configuration + +The script provides several options to fine-tune vulnerability filtering: + +### Severity Thresholds + +Filter vulnerabilities based on severity level: + +```shell +python cs_scanimage.py --clientid FALCON_CLIENT_ID --repo --tag \ + --severity_threshold high +``` + +Valid severity thresholds: `low`, `medium`, `high`, `critical` (default: `low`) + +### Severity Mode + +Choose how severity is determined: + +```shell +python cs_scanimage.py --clientid FALCON_CLIENT_ID --repo --tag \ + --severity_mode cps +``` + +Valid severity modes: `default` (cvss v3 score), `cps` (CrowdStrike Priority Score) + +### Ignoring Unfixed Vulnerabilities + +Filter out vulnerabilities that don't have fixes available: + +```shell +python cs_scanimage.py --clientid FALCON_CLIENT_ID --repo --tag \ + --ignore-unfixed +``` + +### CVE Ignore List + +Specify a file containing CVEs to ignore: + +```shell +python cs_scanimage.py --clientid FALCON_CLIENT_ID --repo --tag \ + --cve-ignore-file /path/to/ignore-list.txt +``` + +Sample ignore file content: +``` +# No fix is available for these CVE +CVE-2024-2961 +CVE-2024-2962 + +# Unable to fix missconfigurations on base image +cis-SetUIDBitFoundInImage + +# False positive secrets, ignore all +secrets-ignore-all +``` + +The ignore file should contain one CVE ID per line. Lines starting with # are treated as comments. + +### Skip Image Push + +For workflows where the image has already been pushed: + +```shell +python cs_scanimage.py --clientid FALCON_CLIENT_ID --repo --tag \ + --skip-push +``` + +This is useful in CI/CD pipelines where the image push step is handled separately. + ## Example Scans ### Example 1 @@ -156,24 +238,6 @@ python cs_scanimage.py --clientid FALCON_CLIENT_ID --repo --tag \ ``` -The `echo $?` command can be utilized to review the return code, e.g: - -```shell -echo $? -1 -``` - -The `echo $?` above displays the returned code with the following mappings: - -```shell -VulnerabilityScoreExceeded = 1 -Malware = 2 -Secrets = 3 -Success = 0 -Misconfig = 0 -ScriptFailure = 10 -``` - ## Running the Scan using CICD - You can use the [container-image-scan](https://github.com/marketplace/actions/crowdstrike-container-image-scan) GitHub Action in your GitHub workflows. Checkout the action at [https://github.com/marketplace/actions/crowdstrike-container-image-scan](https://github.com/marketplace/actions/crowdstrike-container-image-scan) @@ -198,3 +262,34 @@ docker run -it --rm -e FALCON_CLIENT_ID -e FALCON_CLIENT_SECRET \ -v /var/run/docker.sock:/var/run/docker.sock \ quay.io/crowdstrike/container-image-scan:latest --repo --tag -c us-2 ``` +You can mount an ignore file to the container to ignore specific CVEs: + +```shell +docker run -it --rm -e FALCON_CLIENT_ID -e FALCON_CLIENT_SECRET \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v ./.ignore-cve:/ignore \ + quay.io/crowdstrike/container-image-scan:latest --repo --tag --cve-ignore-file /ignore + +``` + +## Exit Codes and Failure Reasons + +The script exits with specific codes to indicate the result of the scan. These codes can be used in CI/CD pipelines to determine the cause of a failure: + +| Exit Code | Reason | Description | +|-----------|---------------------------------------------|-----------------------------------------------------------------------------| +| 0 | Success | No critical issues found; scan passed. | +| 1 | VulnerabilityScoreExceeded | The vulnerability score threshold was exceeded. | +| 2 | Malware | Malware was detected in the container image. | +| 3 | Secrets | Leaked secrets were detected in the container image. | +| 4 | Misconfig | Misconfigurations or CIS issues were found (treated as non-blocking by default). | +| 10 | ScriptFailure | A script or API error occurred, or scan report retries were exhausted. | + +You can use these exit codes in your CI/CD pipeline to take appropriate actions based on the scan results. + +The `echo $?` command can be utilized to review the return code, e.g: + +```shell +echo $? +1 +``` diff --git a/cs_scanimage.py b/cs_scanimage.py index a995f6d..414eccd 100755 --- a/cs_scanimage.py +++ b/cs_scanimage.py @@ -1,4 +1,4 @@ -#!/usr/bin/env python3 +# !/usr/bin/env python3 r""" MM''''''YMM dP oo M' .mmm. `M 88 @@ -35,23 +35,21 @@ | / | / \_| | |_/ | /\ | / | | |/) |/ \___/ |/\_/ \/ \/ \/|_//(_)|_/ |/|/| \/|_/ """ -from __future__ import print_function import argparse import json import logging import sys -from os import environ as env +import os from enum import Enum import time import getpass from falconpy import FalconContainer, ContainerBaseURL -from retry import retry - +from tenacity import retry, wait_fixed, stop_after_attempt, stop_after_delay logging.basicConfig(stream=sys.stdout, format="%(levelname)-8s%(message)s") log = logging.getLogger("cs_scanimage") -VERSION = "2.4.1" +VERSION = "2.5.0" class ScanImage(Exception): @@ -59,7 +57,7 @@ class ScanImage(Exception): # pylint:disable=too-many-instance-attributes def __init__( - self, client_id, client_secret, repo, tag, client, runtime, cloud + self, client_id, client_secret, repo, tag, client, runtime, cloud ): # pylint:disable=too-many-positional-arguments self.client_id = client_id self.client_secret = client_secret @@ -71,9 +69,9 @@ def __init__( self.auth_config = None # Step 1: perform container tag to the registry corresponding to the cloud entered - def container_tag(self): - local_tag = "%s:%s" % (self.repo, self.tag) - url_tag = "%s/%s" % (self.server_domain, self.repo) + def container_tag(self) -> None: + local_tag = f"{self.repo}:{self.tag}" + url_tag = f"{self.server_domain}/{self.repo}" container_image = "".join( ( @@ -90,40 +88,29 @@ def container_tag(self): self.client.images.get(local_tag).tag(url_tag, self.tag, force=True) # Step 2: login using the credentials supplied - def container_login(self): + def container_login(self) -> None: log.info("Performing login to CrowdStrike Image Assessment Service") - if self.runtime == "docker": - try: - login = self.client.login( - username=self.client_id, - password=self.client_secret, - registry=self.server_domain, - reauth=True, - ) - log.info(login["Status"]) - except Exception as e: - log.error("Docker login failed: %s", str(e)) - raise - else: # podman - try: - auth_config = { - "username": self.client_id, - "password": self.client_secret, - } - response = self.client.login(registry=self.server_domain, **auth_config) - # Store auth config for subsequent operations + try: + auth_config = { + "username": self.client_id, + "password": self.client_secret, + } + login = self.client.login( + registry=self.server_domain, + reauth=self.runtime == "docker", + **auth_config) + log.info(login["Status"]) + if self.runtime == "podman": self.auth_config = auth_config - log.info(response["Status"]) - except Exception as e: - log.error("Podman login failed: %s", str(e)) - raise + except Exception as e: + log.error("%s login failed: %s", self.runtime.title(), str(e)) + raise # Step 3: perform container push using the repo and tag supplied - @retry(TimeoutError, tries=5, delay=5) - def container_push(self): - image_str = "%s/%s:%s" % (self.server_domain, self.repo, self.tag) + @retry(wait=wait_fixed(5), stop=(stop_after_attempt(5) | stop_after_delay(600))) + def container_push(self) -> None: + image_str = f"{self.server_domain}/{self.repo}:{self.tag}" log.info("Performing container push to %s", image_str) - try: if self.runtime == "docker": image_push = self.client.images.push( @@ -151,7 +138,7 @@ def container_push(self): end="\r", ) elif "status" in line: - log.info("%s: %s", self.runtime.capitalize(), line["status"]) + log.debug("%s: %s", self.runtime.capitalize(), line["status"]) else: log.debug(line) except Exception as e: @@ -161,7 +148,7 @@ def container_push(self): # Step 4: poll and get scanreport for specified amount of retries def get_scanreport( - client_id, client_secret, cloud, user_agent, repo, tag, retry_count + client_id, client_secret, cloud, user_agent, repo, tag, retry_count ): # pylint:disable=too-many-positional-arguments log.info("Downloading Image Scan Report") falcon = FalconContainer( @@ -195,19 +182,15 @@ class ScanReport(dict): details_str_key = "Details" detect_str_key = "Detections" + severity_low = "low" + severity_medium = "medium" severity_high = "high" + severity_critical = "critical" type_malware = "malware" type_secret = "secret" # nosec type_misconfig = "misconfiguration" type_cis = "cis" - def status_code(self): - vuln_code = self.get_alerts_vuln() - mal_code = self.get_alerts_malware() - sec_code = self.get_alerts_secrets() - mcfg_code = self.get_alerts_misconfig() - return vuln_code | mal_code | sec_code | mcfg_code - def export(self, filename): with open(filename, "w", encoding="utf-8") as f: f.write(json.dumps(self, indent=4)) @@ -215,7 +198,7 @@ def export(self, filename): # Step 5: pass the vulnerabilities from scan report, # loop through and find high severity vulns # return HighVulnerability enum value - def get_alerts_vuln(self): + def get_alerts_vuln(self, severity_threshold: str, severity_mode: str, ignore_unfixed: bool, cve_ignore_list: []): log.info("Searching for vulnerabilities in scan report...") critical_score = 2000 high_score = 500 @@ -227,38 +210,61 @@ def get_alerts_vuln(self): for vulnerability in vulnerabilities: vuln = vulnerability["Vulnerability"] cve = vuln.get("CVEID", "CVE-unknown") - details = vuln.get("Details") - - if isinstance(details, dict): - severity = details.get("severity") - if severity is None: - cvss_v3 = details.get("cvss_v3_score", {}) - severity = cvss_v3.get("severity") - if severity is None: - cvss_v2 = details.get("cvss_v2_score", {}) - severity = cvss_v2.get("severity", "") + ignore_cve_detected = cve.lower() in cve_ignore_list + ignore_unfixed_detected = ignore_unfixed and not vuln.get("Remediation", []) + ignore_vuln = ignore_cve_detected or ignore_unfixed_detected + severity = self.get_severity_score(vuln, severity_mode) + if not ignore_vuln: + if severity == self.severity_low and severity_threshold == self.severity_low: + vuln_score += low_score + elif severity == self.severity_medium and severity_threshold in [self.severity_low, + self.severity_medium]: + vuln_score += medium_score + elif severity == self.severity_high and severity_threshold in [self.severity_low, + self.severity_medium, + self.severity_high]: + vuln_score += high_score + elif severity == self.severity_critical: + vuln_score += critical_score + else: + ignore_vuln = True + if ignore_vuln: + log.info( + "%-8s %-16s Vulnerability detected but %s", + severity, + cve, + "ignored (in ignore list)" if ignore_cve_detected else "ignored (unfixed)" if ignore_unfixed_detected else "ignored (severity)" + ) else: - severity = "" - - product = vuln.get("Product", {}) - affects = product.get("PackageSource", product) - log.warning( - "%-8s %-16s Vulnerability detected affecting %s", - severity, - cve, - affects, - ) - if severity.lower() == "low": - vuln_score = vuln_score + low_score - if severity.lower() == "medium": - vuln_score = vuln_score + medium_score - if severity.lower() == "high": - vuln_score = vuln_score + high_score - if severity.lower() == "critical": - vuln_score = vuln_score + critical_score + details = vuln.get("Details", {}) + log.warning( + "%-8s %-16s Vulnerability detected affecting %s\n %s", + severity, + cve, + vuln.get("Product", {}).get("PackageSource", vuln.get("Product", {})), + details.get("description", "").replace("\n\n", "\n").replace("\n", "\n ")) return vuln_score - # Step 6: pass the detections from scan report, + def get_severity_score(self, vuln, severity_mode: str): + severity = "" + details = vuln.get("Details") + if isinstance(details, dict): + if severity_mode == "cps": + # Use the CPS score to determine severity + cps_rating = details.get("cps_rating", {}) + if cps_rating: + current_rating = cps_rating.get("CurrentRating", {}) + severity = current_rating.get("Rating", "") + # severity_mode default + if not severity: + cvss_v3 = details.get("cvss_v3_score", {}) + severity = cvss_v3.get("severity") + if not severity: + cvss_v2 = details.get("cvss_v2_score", {}) + severity = cvss_v2.get("severity", "") + return severity.lower() if severity else "" + + # Step 6: pass the detections from scan report # loop through and find if detection type is malware # return Malware enum value def get_alerts_malware(self): @@ -270,58 +276,73 @@ def get_alerts_malware(self): try: if detection["Detection"]["Type"].lower() == self.type_malware: log.warning("Alert: Malware found") - det_code = ScanStatusCode.Malware.value - break + return ScanStatusCode.Malware.value except KeyError: continue - return det_code + return ScanStatusCode.Success.value # Step 7: pass the detections from scan report, # loop through and find if detection type is secret # return Success enum value - def get_alerts_secrets(self): + def get_alerts_secrets(self, ignore_list: []): log.info("Searching for leaked secrets in scan report...") - det_code = 0 detections = self[self.detect_str_key] if detections is not None: for detection in detections: try: if detection["Detection"]["Type"].lower() == self.type_secret: - log.error("Alert: Leaked secrets detected") - det_code = ScanStatusCode.Secrets.value + if not "secrets-ignore-all" in ignore_list: + log.warning("Alert: Leaked secrets detected") + return ScanStatusCode.Secrets.value + else: + log.info( + "Alert: Leaked secrets detected but ignored due to secrets-ignore-all in ignore file") break except KeyError: continue - return det_code + return ScanStatusCode.Success.value # Step 8: pass the detections from scan report, # loop through and find if detection type is misconfig # return Success enum value - def get_alerts_misconfig(self): + def get_alerts_misconfig(self, ignore_list: []): log.info("Searching for misconfigurations in scan report...") - det_code = 0 detections = self[self.detect_str_key] + names = [] if detections is not None: for detection in detections: try: - if detection["Detection"]["Type"].lower() in [ + det = detection["Detection"] + type_detection = det["Type"].lower() + if type_detection in [ self.type_misconfig, self.type_cis, ]: - log.warning("Alert: Misconfiguration found") - det_code = ScanStatusCode.Success.value - break + name = det.get("Name", "") + if name in names: + continue + names.append(name) + key = f"{type_detection}-{name}" + if key.lower() in ignore_list: + log.info("Ignoring %s whitelisted on the ignore file", key) + continue + severity = det.get("Severity", "") + description = (det.get("Title", "") + "\n " + det.get("Description", "") + .replace("\n", "\n ")) + log.warning("Misconfiguration %s '%s'\n %s", severity, key, description) + return ScanStatusCode.Misconfig.value except KeyError: continue - return det_code + return ScanStatusCode.Success.value # these statues are returned and bitwise or'ed class ScanStatusCode(Enum): + Success = 0 Vulnerability = 1 Malware = 2 Secrets = 3 - Success = 0 + Misconfig = 4 ScriptFailure = 10 @@ -335,7 +356,7 @@ def __init__(self, status): self.status = status def __str__(self): - return "APIError: status={}".format(self.status) + return f"APIError: status={self.status}" class RetryExhaustedError(Exception): @@ -346,8 +367,8 @@ class RetryExhaustedError(Exception): # See https://stackoverflow.com/questions/10551117/setting-options-from-environment-variables-when-using-argparse/10551190#10551190 class EnvDefault(argparse.Action): def __init__(self, envvar, required=True, default=None, **kwargs): - if envvar in env: - default = env[envvar] + if envvar in os.environ: + default = os.environ[envvar] if required and default: required = False super(EnvDefault, self).__init__(default=default, required=required, **kwargs) @@ -359,7 +380,7 @@ def __call__(self, parser, namespace, values, option_string=None): # End code authored by Russell Heilling -def parse_args(): +def parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(formatter_class=argparse.RawTextHelpFormatter) required = parser.add_argument_group("required arguments") required.add_argument( @@ -401,10 +422,44 @@ def parse_args(): "-s", "--score_threshold", action=EnvDefault, - dest="score", + dest="score_threshold", default="500", envvar="SCORE", - help="Vulnerability score threshold", + help="Vulnerability score threshold (default 500)", + ) + required.add_argument( + "-st", + "--severity_threshold", + action=EnvDefault, + dest="severity_threshold", + default="low", + envvar="SEVERITY_THRESHOLD", + help="Vulnerability severity threshold (default low)", + ) + required.add_argument( + "-sm", + "--severity_mode", + action=EnvDefault, + dest="severity_mode", + default="default", + envvar="SEVERITY_MODE", + help="Vulnerability severity compute mode (default cps)" + ) + required.add_argument( + "--ignore-unfixed", + action="store_true", + dest="ignore_unfixed", + default=False, + help="Ignore unfixed vulnerabilities", + ) + required.add_argument( + "--cve-ignore-file", + action=EnvDefault, + dest="ignore_file", + envvar="IGNORE_FILE", + default=None, + required=False, + help="Path of the file containing CVEs to ignore", ) parser.add_argument( "--json-report", @@ -453,16 +508,34 @@ def parse_args(): parser.add_argument( "--skip-push", default=False, action="store_true", help="Skip image push" ) - args = parser.parse_args() logging.getLogger().setLevel(args.log_level) - + if args.severity_threshold.lower() not in [ + "low", + "medium", + "high", + "critical", + ]: + raise ValueError( + "Invalid severity threshold. Must be one of: low, medium, high, critical" + ) + if args.severity_mode.lower() not in [ + "default", + "cps", + ]: + raise ValueError( + "Invalid severity mode. Must be one of: [default, cps]" + ) return ( args.client_id, args.repo, args.tag, args.cloud, - args.score, + int(args.score_threshold), + args.severity_threshold.lower(), + args.severity_mode.lower(), + args.ignore_unfixed, + args.ignore_file, args.report, args.retry_count, args.plugin, @@ -473,73 +546,81 @@ def parse_args(): def detect_container_runtime(): """Detect whether Docker or Podman is available and return the appropriate client""" + docker_available = True + podman_available = True + try: + import docker + except ImportError: + docker_available = False try: - import docker # pylint:disable=C0415 + import podman + except ImportError: + podman_available = False + # Fail early if neither library is available + if not docker_available and not podman_available: + raise RuntimeError("Neither Docker nor Podman libraries are available") + # Try Docker first if available + if docker_available: try: client = docker.from_env() client.ping() return client, "docker" - except (docker.errors.APIError, docker.errors.DockerException): - import podman # pylint:disable=C0415 + except (docker.errors.APIError, docker.errors.DockerException) as exc: + if not podman_available: + raise RuntimeError("Could not connect to Docker.") from exc - client = podman.from_env() - try: - client.ping() - except (ConnectionRefusedError, podman.errors.exceptions.APIError) as exc: - raise RuntimeError( - "Could not connect to Podman socket. " - "If running rootless, ensure your podman.socket is running (systemctl --user start podman.socket). " - "If running as root, ensure the CONTAINER_HOST environment variable is set correctly " - "(e.g. unix:///var/run/podman/podman.sock)." - ) from exc - return client, "podman" - except ImportError: + # Try Podman if available + if podman_available: try: - import podman # pylint:disable=C0415 - client = podman.from_env() - try: - client.ping() - except (ConnectionRefusedError, podman.errors.exceptions.APIError) as exc: - raise RuntimeError( - "Could not connect to Podman socket. " - "If running rootless, ensure your podman.socket is running (systemctl --user start podman.socket). " - "If running as root, ensure the CONTAINER_HOST environment variable is set correctly " - "(e.g. unix:///var/run/podman/podman.sock)." - ) from exc + client.ping() return client, "podman" - except ImportError as exc: + except (ConnectionRefusedError, podman.errors.exceptions.APIError) as exc: raise RuntimeError( - "Neither Docker nor Podman client libraries are available" + "Could not connect to Docker or Podman socket.\n" + "About podman: " + "If running rootless, ensure your podman.socket is running (systemctl --user start podman.socket). " + "If running as root, ensure the CONTAINER_HOST environment variable is set correctly " + "(e.g. unix:///var/run/podman/podman.sock)." ) from exc + # If we reach here, something unexpected happened + raise RuntimeError("Could not connect to Docker or Podman socket.") -def main(): # pylint:disable=R0915 +def main() -> None: try: ( client_id, repo, tag, cloud, - score, + score_threshold, + severity_threshold, + severity_mode, + ignore_unfixed, + ignore_file, json_report, retry_count, plugin, useragent, skip_push, ) = parse_args() - client_secret = env.get("FALCON_CLIENT_SECRET") - if client_secret is None: - print("Please enter your Falcon OAuth2 API Secret") - client_secret = getpass.getpass() - + client_secret = os.environ.get("FALCON_CLIENT_SECRET") or getpass.getpass("Enter Falcon OAuth2 API Secret: ") client, runtime = detect_container_runtime() log.info("Using %s container runtime", runtime) - + ignore_list = [] + if ignore_file and os.path.exists(ignore_file): + log.debug("Cve ignore file found %s", ignore_file) + with open(ignore_file, "r", encoding="utf-8") as f: + ignore_list = [ + line.strip().lower() for line in f if line.strip() and not line.lstrip().startswith("#") + ] + elif ignore_file: + log.warning("Cve ignore file not found %s", ignore_file) if not skip_push: - useragent = "%s/%s" % (useragent, VERSION) + useragent = f"{useragent}/{VERSION}" scan_image = ScanImage( client_id, client_secret, repo, tag, client, runtime, cloud ) @@ -557,31 +638,37 @@ def main(): # pylint:disable=R0915 if json_report: scan_report.export(json_report) - f_vuln_score = int(scan_report.get_alerts_vuln()) - f_secrets = int(scan_report.get_alerts_secrets()) + + f_vuln_score = int(scan_report.get_alerts_vuln(severity_threshold, severity_mode, ignore_unfixed, ignore_list)) + f_secrets = int(scan_report.get_alerts_secrets(ignore_list)) f_malware = int(scan_report.get_alerts_malware()) - scan_report.get_alerts_misconfig() + f_misconfig = scan_report.get_alerts_misconfig(ignore_list) - if f_secrets == ScanStatusCode.Secrets.value: - log.error("Exiting: Secrets found in container image") - sys.exit(ScanStatusCode.Secrets.value) - if f_malware == ScanStatusCode.Malware.value: - log.error("Exiting: Malware found in container image") - sys.exit(ScanStatusCode.Malware.value) - if f_vuln_score >= int(score): + if f_vuln_score >= score_threshold: log.error( - "Exiting: Vulnerability score threshold exceeded: '%s' out of '%s'", + "Exiting %s: Vulnerability score threshold exceeded: '%s' out of '%s'", + ScanStatusCode.Vulnerability.value, f_vuln_score, - score, + score_threshold, ) sys.exit(ScanStatusCode.Vulnerability.value) - else: + elif f_vuln_score > 0: log.info( "Vulnerability score threshold not met: '%s' out of '%s'", f_vuln_score, - score, + score_threshold, ) - sys.exit(ScanStatusCode.Success.value) + if f_malware == ScanStatusCode.Malware.value: + log.error("Exiting %s: Malware found in container image", f_malware) + sys.exit(ScanStatusCode.Malware.value) + if f_secrets == ScanStatusCode.Secrets.value: + log.error("Exiting %s: Secrets found in container image", f_secrets) + sys.exit(ScanStatusCode.Secrets.value) + if f_misconfig == ScanStatusCode.Misconfig.value: + log.error("Exiting %s: Misconfig found in container image", f_misconfig) + sys.exit(ScanStatusCode.Misconfig.value) + + sys.exit(ScanStatusCode.Success.value) except APIError: log.exception("Unable to scan") diff --git a/requirements.txt b/requirements.txt index 7f3cb17..c1b206e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,7 +2,7 @@ crowdstrike-falconpy setuptools>=59.6.0 docker>=5.0.3 podman>=5.0.0 -retry>=0.9.2 +tenacity>=9.1.0 requests>=2.32.0 urllib3>=2.2.2 certifi>=2024.7.4