From d5acdc766ae15da2f8f96cd2d097a62da4d84112 Mon Sep 17 00:00:00 2001 From: Sergio Garcia <38561120+sergargar@users.noreply.github.com> Date: Fri, 4 Oct 2024 10:59:51 -0400 Subject: [PATCH] chore(ocsf): adapt mapping for version 1.3.0 (#5287) --- poetry.lock | 100 +++++++++++---------- prowler/lib/outputs/ocsf/ocsf.py | 90 +++++++++++-------- pyproject.toml | 4 +- tests/lib/outputs/ocsf/ocsf_test.py | 135 +++++++++++++++++----------- 4 files changed, 189 insertions(+), 140 deletions(-) diff --git a/poetry.lock b/poetry.lock index d8266587d7..b6f64932be 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1410,13 +1410,13 @@ files = [ [[package]] name = "email-validator" -version = "2.1.1" +version = "2.2.0" description = "A robust email address syntax and deliverability validation library." optional = false python-versions = ">=3.8" files = [ - {file = "email_validator-2.1.1-py3-none-any.whl", hash = "sha256:97d882d174e2a65732fb43bfce81a3a834cbc1bde8bf419e30ef5ea976370a05"}, - {file = "email_validator-2.1.1.tar.gz", hash = "sha256:200a70680ba08904be6d1eef729205cc0d687634399a5924d842533efb824b84"}, + {file = "email_validator-2.2.0-py3-none-any.whl", hash = "sha256:561977c2d73ce3611850a06fa56b414621e0c8faa9d66f2611407d87465da631"}, + {file = "email_validator-2.2.0.tar.gz", hash = "sha256:cb690f344c617a714f22e66ae771445a1ceb46821152df8e165c5f9a364582b7"}, ] [package.dependencies] @@ -3463,18 +3463,19 @@ test = ["enum34", "ipaddress", "mock", "pywin32", "wmi"] [[package]] name = "py-ocsf-models" -version = "0.1.1" +version = "0.2.0" description = "This is a Python implementation of the OCSF models. The models are used to represent the data of the OCSF Schema defined in https://schema.ocsf.io/." optional = false python-versions = "<3.13,>=3.9" files = [ - {file = "py_ocsf_models-0.1.1-py3-none-any.whl", hash = "sha256:c6ea465fda85470b938a48da65b1f19664f6d83820ebe849ef5551094e6768de"}, - {file = "py_ocsf_models-0.1.1.tar.gz", hash = "sha256:b0f2d4495a2596793f75e61a1ba218edea3a2d17a2b9911d46ee0fa623cc657b"}, + {file = "py_ocsf_models-0.2.0-py3-none-any.whl", hash = "sha256:ac75fd21077694b343ebaad3479194db113c274879b114277560ff287d5cd7b5"}, + {file = "py_ocsf_models-0.2.0.tar.gz", hash = "sha256:3e12648d05329e6776a0e6b1ffea87a3eb60aa7d8cb2c4afd69e5724f443ce03"}, ] [package.dependencies] -email-validator = "2.1.1" -pydantic = "1.10.15" +cryptography = "43.0.1" +email-validator = "2.2.0" +pydantic = "1.10.18" [[package]] name = "py-partiql-parser" @@ -3539,47 +3540,54 @@ files = [ [[package]] name = "pydantic" -version = "1.10.15" +version = "1.10.18" description = "Data validation and settings management using python type hints" optional = false python-versions = ">=3.7" files = [ - {file = "pydantic-1.10.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:22ed12ee588b1df028a2aa5d66f07bf8f8b4c8579c2e96d5a9c1f96b77f3bb55"}, - {file = "pydantic-1.10.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:75279d3cac98186b6ebc2597b06bcbc7244744f6b0b44a23e4ef01e5683cc0d2"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:50f1666a9940d3d68683c9d96e39640f709d7a72ff8702987dab1761036206bb"}, - {file = "pydantic-1.10.15-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:82790d4753ee5d00739d6cb5cf56bceb186d9d6ce134aca3ba7befb1eedbc2c8"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:d207d5b87f6cbefbdb1198154292faee8017d7495a54ae58db06762004500d00"}, - {file = "pydantic-1.10.15-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:e49db944fad339b2ccb80128ffd3f8af076f9f287197a480bf1e4ca053a866f0"}, - {file = "pydantic-1.10.15-cp310-cp310-win_amd64.whl", hash = "sha256:d3b5c4cbd0c9cb61bbbb19ce335e1f8ab87a811f6d589ed52b0254cf585d709c"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c3d5731a120752248844676bf92f25a12f6e45425e63ce22e0849297a093b5b0"}, - {file = "pydantic-1.10.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c365ad9c394f9eeffcb30a82f4246c0006417f03a7c0f8315d6211f25f7cb654"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3287e1614393119c67bd4404f46e33ae3be3ed4cd10360b48d0a4459f420c6a3"}, - {file = "pydantic-1.10.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be51dd2c8596b25fe43c0a4a59c2bee4f18d88efb8031188f9e7ddc6b469cf44"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:6a51a1dd4aa7b3f1317f65493a182d3cff708385327c1c82c81e4a9d6d65b2e4"}, - {file = "pydantic-1.10.15-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:4e316e54b5775d1eb59187f9290aeb38acf620e10f7fd2f776d97bb788199e53"}, - {file = "pydantic-1.10.15-cp311-cp311-win_amd64.whl", hash = "sha256:0d142fa1b8f2f0ae11ddd5e3e317dcac060b951d605fda26ca9b234b92214986"}, - {file = "pydantic-1.10.15-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:7ea210336b891f5ea334f8fc9f8f862b87acd5d4a0cbc9e3e208e7aa1775dabf"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3453685ccd7140715e05f2193d64030101eaad26076fad4e246c1cc97e1bb30d"}, - {file = "pydantic-1.10.15-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bea1f03b8d4e8e86702c918ccfd5d947ac268f0f0cc6ed71782e4b09353b26f"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:005655cabc29081de8243126e036f2065bd7ea5b9dff95fde6d2c642d39755de"}, - {file = "pydantic-1.10.15-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:af9850d98fc21e5bc24ea9e35dd80a29faf6462c608728a110c0a30b595e58b7"}, - {file = "pydantic-1.10.15-cp37-cp37m-win_amd64.whl", hash = "sha256:d31ee5b14a82c9afe2bd26aaa405293d4237d0591527d9129ce36e58f19f95c1"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:5e09c19df304b8123938dc3c53d3d3be6ec74b9d7d0d80f4f4b5432ae16c2022"}, - {file = "pydantic-1.10.15-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:7ac9237cd62947db00a0d16acf2f3e00d1ae9d3bd602b9c415f93e7a9fc10528"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:584f2d4c98ffec420e02305cf675857bae03c9d617fcfdc34946b1160213a948"}, - {file = "pydantic-1.10.15-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bbc6989fad0c030bd70a0b6f626f98a862224bc2b1e36bfc531ea2facc0a340c"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:d573082c6ef99336f2cb5b667b781d2f776d4af311574fb53d908517ba523c22"}, - {file = "pydantic-1.10.15-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6bd7030c9abc80134087d8b6e7aa957e43d35714daa116aced57269a445b8f7b"}, - {file = "pydantic-1.10.15-cp38-cp38-win_amd64.whl", hash = "sha256:3350f527bb04138f8aff932dc828f154847fbdc7a1a44c240fbfff1b57f49a12"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:51d405b42f1b86703555797270e4970a9f9bd7953f3990142e69d1037f9d9e51"}, - {file = "pydantic-1.10.15-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a980a77c52723b0dc56640ced396b73a024d4b74f02bcb2d21dbbac1debbe9d0"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:67f1a1fb467d3f49e1708a3f632b11c69fccb4e748a325d5a491ddc7b5d22383"}, - {file = "pydantic-1.10.15-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:676ed48f2c5bbad835f1a8ed8a6d44c1cd5a21121116d2ac40bd1cd3619746ed"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:92229f73400b80c13afcd050687f4d7e88de9234d74b27e6728aa689abcf58cc"}, - {file = "pydantic-1.10.15-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:2746189100c646682eff0bce95efa7d2e203420d8e1c613dc0c6b4c1d9c1fde4"}, - {file = "pydantic-1.10.15-cp39-cp39-win_amd64.whl", hash = "sha256:394f08750bd8eaad714718812e7fab615f873b3cdd0b9d84e76e51ef3b50b6b7"}, - {file = "pydantic-1.10.15-py3-none-any.whl", hash = "sha256:28e552a060ba2740d0d2aabe35162652c1459a0b9069fe0db7f4ee0e18e74d58"}, - {file = "pydantic-1.10.15.tar.gz", hash = "sha256:ca832e124eda231a60a041da4f013e3ff24949d94a01154b137fc2f2a43c3ffb"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:e405ffcc1254d76bb0e760db101ee8916b620893e6edfbfee563b3c6f7a67c02"}, + {file = "pydantic-1.10.18-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e306e280ebebc65040034bff1a0a81fd86b2f4f05daac0131f29541cafd80b80"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:11d9d9b87b50338b1b7de4ebf34fd29fdb0d219dc07ade29effc74d3d2609c62"}, + {file = "pydantic-1.10.18-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b661ce52c7b5e5f600c0c3c5839e71918346af2ef20062705ae76b5c16914cab"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:c20f682defc9ef81cd7eaa485879ab29a86a0ba58acf669a78ed868e72bb89e0"}, + {file = "pydantic-1.10.18-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c5ae6b7c8483b1e0bf59e5f1843e4fd8fd405e11df7de217ee65b98eb5462861"}, + {file = "pydantic-1.10.18-cp310-cp310-win_amd64.whl", hash = "sha256:74fe19dda960b193b0eb82c1f4d2c8e5e26918d9cda858cbf3f41dd28549cb70"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:72fa46abace0a7743cc697dbb830a41ee84c9db8456e8d77a46d79b537efd7ec"}, + {file = "pydantic-1.10.18-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ef0fe7ad7cbdb5f372463d42e6ed4ca9c443a52ce544472d8842a0576d830da5"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a00e63104346145389b8e8f500bc6a241e729feaf0559b88b8aa513dd2065481"}, + {file = "pydantic-1.10.18-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ae6fa2008e1443c46b7b3a5eb03800121868d5ab6bc7cda20b5df3e133cde8b3"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_i686.whl", hash = "sha256:9f463abafdc92635da4b38807f5b9972276be7c8c5121989768549fceb8d2588"}, + {file = "pydantic-1.10.18-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:3445426da503c7e40baccefb2b2989a0c5ce6b163679dd75f55493b460f05a8f"}, + {file = "pydantic-1.10.18-cp311-cp311-win_amd64.whl", hash = "sha256:467a14ee2183bc9c902579bb2f04c3d3dac00eff52e252850509a562255b2a33"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:efbc8a7f9cb5fe26122acba1852d8dcd1e125e723727c59dcd244da7bdaa54f2"}, + {file = "pydantic-1.10.18-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:24a4a159d0f7a8e26bf6463b0d3d60871d6a52eac5bb6a07a7df85c806f4c048"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b74be007703547dc52e3c37344d130a7bfacca7df112a9e5ceeb840a9ce195c7"}, + {file = "pydantic-1.10.18-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fcb20d4cb355195c75000a49bb4a31d75e4295200df620f454bbc6bdf60ca890"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:46f379b8cb8a3585e3f61bf9ae7d606c70d133943f339d38b76e041ec234953f"}, + {file = "pydantic-1.10.18-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cbfbca662ed3729204090c4d09ee4beeecc1a7ecba5a159a94b5a4eb24e3759a"}, + {file = "pydantic-1.10.18-cp312-cp312-win_amd64.whl", hash = "sha256:c6d0a9f9eccaf7f438671a64acf654ef0d045466e63f9f68a579e2383b63f357"}, + {file = "pydantic-1.10.18-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:3d5492dbf953d7d849751917e3b2433fb26010d977aa7a0765c37425a4026ff1"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fe734914977eed33033b70bfc097e1baaffb589517863955430bf2e0846ac30f"}, + {file = "pydantic-1.10.18-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:15fdbe568beaca9aacfccd5ceadfb5f1a235087a127e8af5e48df9d8a45ae85c"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:c3e742f62198c9eb9201781fbebe64533a3bbf6a76a91b8d438d62b813079dbc"}, + {file = "pydantic-1.10.18-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:19a3bd00b9dafc2cd7250d94d5b578edf7a0bd7daf102617153ff9a8fa37871c"}, + {file = "pydantic-1.10.18-cp37-cp37m-win_amd64.whl", hash = "sha256:2ce3fcf75b2bae99aa31bd4968de0474ebe8c8258a0110903478bd83dfee4e3b"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:335a32d72c51a313b33fa3a9b0fe283503272ef6467910338e123f90925f0f03"}, + {file = "pydantic-1.10.18-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:34a3613c7edb8c6fa578e58e9abe3c0f5e7430e0fc34a65a415a1683b9c32d9a"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e9ee4e6ca1d9616797fa2e9c0bfb8815912c7d67aca96f77428e316741082a1b"}, + {file = "pydantic-1.10.18-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:23e8ec1ce4e57b4f441fc91e3c12adba023fedd06868445a5b5f1d48f0ab3682"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:44ae8a3e35a54d2e8fa88ed65e1b08967a9ef8c320819a969bfa09ce5528fafe"}, + {file = "pydantic-1.10.18-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:d5389eb3b48a72da28c6e061a247ab224381435256eb541e175798483368fdd3"}, + {file = "pydantic-1.10.18-cp38-cp38-win_amd64.whl", hash = "sha256:069b9c9fc645474d5ea3653788b544a9e0ccd3dca3ad8c900c4c6eac844b4620"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:80b982d42515632eb51f60fa1d217dfe0729f008e81a82d1544cc392e0a50ddf"}, + {file = "pydantic-1.10.18-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:aad8771ec8dbf9139b01b56f66386537c6fe4e76c8f7a47c10261b69ad25c2c9"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:941a2eb0a1509bd7f31e355912eb33b698eb0051730b2eaf9e70e2e1589cae1d"}, + {file = "pydantic-1.10.18-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:65f7361a09b07915a98efd17fdec23103307a54db2000bb92095457ca758d485"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:6951f3f47cb5ca4da536ab161ac0163cab31417d20c54c6de5ddcab8bc813c3f"}, + {file = "pydantic-1.10.18-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:7a4c5eec138a9b52c67f664c7d51d4c7234c5ad65dd8aacd919fb47445a62c86"}, + {file = "pydantic-1.10.18-cp39-cp39-win_amd64.whl", hash = "sha256:49e26c51ca854286bffc22b69787a8d4063a62bf7d83dc21d44d2ff426108518"}, + {file = "pydantic-1.10.18-py3-none-any.whl", hash = "sha256:06a189b81ffc52746ec9c8c007f16e5167c8b0a696e1a726369327e3db7b2a82"}, + {file = "pydantic-1.10.18.tar.gz", hash = "sha256:baebdff1907d1d96a139c25136a9bb7d17e118f133a76a2ef3b845e831e3403a"}, ] [package.dependencies] @@ -5060,4 +5068,4 @@ type = ["pytest-mypy"] [metadata] lock-version = "2.0" python-versions = ">=3.9,<3.13" -content-hash = "89dbfce575ce3df09a5633796e5f4f8b582bca0c65939274a41768544b62aa56" +content-hash = "f99a8859e7618ddc32babfd9b742ee8f8cc4520c274e245b358a0df252d56ee6" diff --git a/prowler/lib/outputs/ocsf/ocsf.py b/prowler/lib/outputs/ocsf/ocsf.py index b948185b8c..b9eb78ce63 100644 --- a/prowler/lib/outputs/ocsf/ocsf.py +++ b/prowler/lib/outputs/ocsf/ocsf.py @@ -9,7 +9,6 @@ from py_ocsf_models.events.findings.detection_finding import ( from py_ocsf_models.events.findings.finding import ActivityID, FindingInformation from py_ocsf_models.objects.account import Account, TypeID from py_ocsf_models.objects.cloud import Cloud -from py_ocsf_models.objects.container import Container from py_ocsf_models.objects.group import Group from py_ocsf_models.objects.metadata import Metadata from py_ocsf_models.objects.organization import Organization @@ -37,7 +36,7 @@ class OCSF(Output): - transform(findings: List[Finding]) -> None: Transforms the findings into the OCSF Detection Finding format. - batch_write_data_to_file() -> None: Writes the findings to a file using the OCSF Detection Finding format using the `Output._file_descriptor`. - get_account_type_id_by_provider(provider: str) -> TypeID: Returns the TypeID based on the provider. - - get_finding_status_id(status: str, muted: bool) -> StatusID: Returns the StatusID based on the status and muted values. + - get_finding_status_id(muted: bool) -> StatusID: Returns the StatusID based on the muted value. References: - OCSF: https://schema.ocsf.io/1.2.0/classes/detection_finding @@ -59,21 +58,24 @@ class OCSF(Output): finding_severity = getattr( SeverityID, finding.severity.capitalize(), SeverityID.Unknown ) - finding_status = self.get_finding_status_id( - finding.status, finding.muted - ) + finding_status = self.get_finding_status_id(finding.muted) detection_finding = DetectionFinding( + message=finding.status_extended, activity_id=finding_activity.value, activity_name=finding_activity.name, finding_info=FindingInformation( - created_time=finding.timestamp, + created_time_dt=finding.timestamp, + created_time=int(finding.timestamp.timestamp()), desc=finding.description, title=finding.check_title, uid=finding.finding_uid, + name=finding.resource_name, product_uid="prowler", + types=[finding.check_type], ), - event_time=finding.timestamp, + time_dt=finding.timestamp, + time=int(finding.timestamp.timestamp()), remediation=Remediation( desc=finding.remediation_recommendation_text, references=list( @@ -96,31 +98,51 @@ class OCSF(Output): status_code=finding.status, status_detail=finding.status_extended, risk_details=finding.risk, - resources=[ - ResourceDetails( - labels=unroll_dict_to_list(finding.resource_tags), - name=finding.resource_name, - uid=finding.resource_uid, - group=Group(name=finding.service_name), - type=finding.resource_type, - # TODO: this should be included only if using the Cloud profile - cloud_partition=finding.partition, - region=finding.region, - data={"details": finding.resource_details}, - ) - ], + resources=( + [ + ResourceDetails( + labels=unroll_dict_to_list(finding.resource_tags), + name=finding.resource_name, + uid=finding.resource_uid, + group=Group(name=finding.service_name), + type=finding.resource_type, + # TODO: this should be included only if using the Cloud profile + cloud_partition=finding.partition, + region=finding.region, + data={"details": finding.resource_details}, + ) + ] + if finding.provider != "kubernetes" + else [ + ResourceDetails( + labels=unroll_dict_to_list(finding.resource_tags), + name=finding.resource_name, + uid=finding.resource_uid, + group=Group(name=finding.service_name), + type=finding.resource_type, + data={"details": finding.resource_details}, + namespace=finding.region.replace("namespace: ", ""), + ) + ] + ), metadata=Metadata( event_code=finding.check_id, product=Product( + uid="prowler", name="Prowler", vendor_name="Prowler", version=finding.prowler_version, ), + profiles=( + ["cloud", "datetime"] + if finding.provider != "kubernetes" + else ["container", "datetime"] + ), + tenant_uid=finding.account_organization_uid, ), type_uid=DetectionFindingTypeID.Create, - type_name=DetectionFindingTypeID.Create.name, + type_name=f"Detection Finding: {DetectionFindingTypeID.Create.name}", unmapped={ - "check_type": finding.check_type, "related_url": finding.related_url, "categories": finding.categories, "depends_on": finding.depends_on, @@ -129,26 +151,19 @@ class OCSF(Output): "compliance": finding.compliance, }, ) - - if finding.provider == "kubernetes": - detection_finding.container = Container( - name=finding.resource_name, - uid=finding.resource_uid, - ) - # TODO: Get the PID of the namespace (we only have the name of the namespace) - # detection_finding.namespace_pid=, - else: + if finding.provider != "kubernetes": detection_finding.cloud = Cloud( account=Account( name=finding.account_name, type_id=cloud_account_type.value, - type=cloud_account_type.name, + type=cloud_account_type.name.replace("_", " "), uid=finding.account_uid, labels=unroll_dict_to_list(finding.account_tags), ), org=Organization( uid=finding.account_organization_uid, name=finding.account_organization_name, + # TODO: add the org unit id and name ), provider=finding.provider, region=finding.region, @@ -208,20 +223,17 @@ class OCSF(Output): return type_id @staticmethod - def get_finding_status_id(status: str, muted: bool) -> StatusID: + def get_finding_status_id(muted: bool) -> StatusID: """ - Returns the StatusID based on the status and muted values. + Returns the StatusID based on the muted value. Args: - status (str): The status value muted (bool): The muted value Returns: - StatusID: The StatusID based on the status and muted values + StatusID: The StatusID based on the muted value """ - status_id = StatusID.Other - if status == "FAIL": - status_id = StatusID.New + status_id = StatusID.New if muted: status_id = StatusID.Suppressed return status_id diff --git a/pyproject.toml b/pyproject.toml index 720a110fd1..7327862a0b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,8 +62,8 @@ microsoft-kiota-abstractions = "1.3.3" msgraph-sdk = "1.8.0" numpy = "2.0.2" pandas = "2.2.3" -py-ocsf-models = "0.1.1" -pydantic = "1.10.15" +py-ocsf-models = "0.2.0" +pydantic = "1.10.18" python = ">=3.9,<3.13" python-dateutil = "^2.9.0.post0" pytz = "2024.2" diff --git a/tests/lib/outputs/ocsf/ocsf_test.py b/tests/lib/outputs/ocsf/ocsf_test.py index 4fa889dcac..52cb636fc5 100644 --- a/tests/lib/outputs/ocsf/ocsf_test.py +++ b/tests/lib/outputs/ocsf/ocsf_test.py @@ -2,6 +2,7 @@ import json from datetime import datetime from io import StringIO +import requests from freezegun import freeze_time from mock import patch from py_ocsf_models.events.base_event import SeverityID, StatusID @@ -45,12 +46,18 @@ class TestOCSF: assert isinstance(output_data, DetectionFinding) assert output_data.activity_id == ActivityID.Create.value assert output_data.activity_name == ActivityID.Create.name - assert output_data.finding_info.created_time == findings[0].timestamp + assert output_data.message == findings[0].status_extended + assert output_data.finding_info.created_time == int( + findings[0].timestamp.timestamp() + ) + assert output_data.finding_info.created_time_dt == findings[0].timestamp assert output_data.finding_info.desc == findings[0].description assert output_data.finding_info.title == findings[0].check_title assert output_data.finding_info.uid == findings[0].finding_uid assert output_data.finding_info.product_uid == "prowler" - assert output_data.event_time == findings[0].timestamp + assert output_data.finding_info.types == ["test-type"] + assert output_data.time == int(findings[0].timestamp.timestamp()) + assert output_data.time_dt == findings[0].timestamp assert ( output_data.remediation.desc == findings[0].remediation_recommendation_text ) @@ -71,14 +78,19 @@ class TestOCSF: assert output_data.resources[0].data == { "details": findings[0].resource_details } + assert output_data.metadata.profiles == ["cloud", "datetime"] + assert output_data.metadata.tenant_uid == "test-organization-id" assert output_data.metadata.event_code == findings[0].check_id assert output_data.metadata.product.name == "Prowler" assert output_data.metadata.product.vendor_name == "Prowler" + assert output_data.metadata.product.uid == "prowler" assert output_data.metadata.product.version == prowler_version assert output_data.type_uid == DetectionFindingTypeID.Create - assert output_data.type_name == DetectionFindingTypeID.Create.name + assert ( + output_data.type_name + == f"Detection Finding: {DetectionFindingTypeID.Create.name}" + ) assert output_data.unmapped == { - "check_type": findings[0].check_type, "related_url": findings[0].related_url, "categories": findings[0].categories, "depends_on": findings[0].depends_on, @@ -87,6 +99,36 @@ class TestOCSF: "compliance": findings[0].compliance, } + def test_validate_ocsf(self): + mock_file = StringIO() + findings = [ + generate_finding_output( + status="FAIL", + severity="low", + muted=False, + region=AWS_REGION_EU_WEST_1, + timestamp=datetime.now(), + resource_details="resource_details", + resource_name="resource_name", + resource_uid="resource-id", + status_extended="status extended", + ) + ] + + output = OCSF(findings) + output._file_descriptor = mock_file + + with patch.object(mock_file, "close", return_value=None): + output.batch_write_data_to_file() + + mock_file.seek(0) + content = mock_file.read() + json_data = json.loads(content) + url = "https://schema.ocsf.io/api/v2/validate" + headers = {"content-type": "application/json"} + response = requests.post(url, headers=headers, json=json_data[0]) + assert response.json()["error_count"] == 0 + @freeze_time(datetime.now()) def test_batch_write_data_to_file(self): mock_file = StringIO() @@ -106,14 +148,18 @@ class TestOCSF: expected_json_output = [ { + "message": "status extended", "metadata": { "event_code": "test-check-id", "product": { "name": "Prowler", + "uid": "prowler", "vendor_name": "Prowler", "version": prowler_version, }, - "version": "1.2.0", + "version": "1.3.0", + "profiles": ["cloud", "datetime"], + "tenant_uid": "test-organization-id", }, "severity_id": 2, "severity": "Low", @@ -122,7 +168,6 @@ class TestOCSF: "status_detail": "status extended", "status_id": 1, "unmapped": { - "check_type": "test-type", "related_url": "test-url", "categories": "test-category", "depends_on": "test-dependency", @@ -133,11 +178,13 @@ class TestOCSF: "activity_name": "Create", "activity_id": 1, "finding_info": { - "created_time": datetime.now().isoformat(), + "created_time": int(datetime.now().timestamp()), + "created_time_dt": datetime.now().isoformat(), "desc": "check description", "product_uid": "prowler", "title": "test-check-id", "uid": "test-unique-finding", + "types": ["test-type"], }, "resources": [ { @@ -153,12 +200,12 @@ class TestOCSF: ], "category_name": "Findings", "category_uid": 2, - "class_name": "DetectionFinding", + "class_name": "Detection Finding", "class_uid": 2004, "cloud": { "account": { "name": "123456789012", - "type": "AWS_Account", + "type": "AWS Account", "type_id": 10, "uid": "123456789012", "labels": ["test-tag:test-value"], @@ -170,11 +217,12 @@ class TestOCSF: "provider": "aws", "region": "eu-west-1", }, - "event_time": datetime.now().isoformat(), + "time": int(datetime.now().timestamp()), + "time_dt": datetime.now().isoformat(), "remediation": {"desc": "", "references": []}, "risk_details": "test-risk", "type_uid": 200401, - "type_name": "Create", + "type_name": "Detection Finding: Create", } ] @@ -186,7 +234,6 @@ class TestOCSF: mock_file.seek(0) content = mock_file.read() - assert json.loads(content) == expected_json_output def test_batch_write_data_to_file_without_findings(self): @@ -211,14 +258,18 @@ class TestOCSF: finding_information = finding_ocsf.finding_info assert isinstance(finding_information, FindingInformation) - assert finding_information.created_time == finding_output.timestamp + assert finding_information.created_time == int( + finding_output.timestamp.timestamp() + ) + assert finding_information.created_time_dt == finding_output.timestamp assert finding_information.desc == finding_output.description assert finding_information.title == finding_output.check_title assert finding_information.uid == finding_output.finding_uid assert finding_information.product_uid == "prowler" # Event time - assert finding_ocsf.event_time == finding_output.timestamp + assert finding_ocsf.time == int(finding_output.timestamp.timestamp()) + assert finding_ocsf.time_dt == finding_output.timestamp # Remediation remediation = finding_ocsf.remediation @@ -241,7 +292,6 @@ class TestOCSF: # Unmapped Data assert finding_ocsf.unmapped == { - "check_type": finding_output.check_type, "related_url": finding_output.related_url, "categories": finding_output.categories, "depends_on": finding_output.depends_on, @@ -281,7 +331,10 @@ class TestOCSF: # Type assert finding_ocsf.type_uid == DetectionFindingTypeID.Create - assert finding_ocsf.type_name == DetectionFindingTypeID.Create.name + assert ( + finding_ocsf.type_name + == f"Detection Finding: {DetectionFindingTypeID.Create.name}" + ) # Cloud cloud = finding_ocsf.cloud @@ -293,7 +346,7 @@ class TestOCSF: assert isinstance(cloud_account, Account) assert cloud_account.name == finding_output.account_name assert cloud_account.type_id == TypeID.AWS_Account - assert cloud_account.type == TypeID.AWS_Account.name + assert cloud_account.type == TypeID.AWS_Account.name.replace("_", " ") assert cloud_account.uid == finding_output.account_uid assert cloud_account.labels == ["test-tag:test-value"] @@ -314,8 +367,10 @@ class TestOCSF: finding_ocsf = OCSF([finding_output]) finding_ocsf = finding_ocsf.data[0] - assert finding_ocsf.container.name == finding_output.resource_name - assert finding_ocsf.container.uid == finding_output.resource_uid + assert finding_ocsf.metadata.profiles == ["container", "datetime"] + assert finding_ocsf.resources[0].namespace == finding_output.region.replace( + "namespace: ", "" + ) def test_finding_output_cloud_fail_low_not_muted(self): finding_output = generate_finding_output( @@ -340,8 +395,8 @@ class TestOCSF: finding_ocsf = finding_ocsf.data[0] # Status - assert finding_ocsf.status_id == StatusID.Other.value - assert finding_ocsf.status == StatusID.Other.name + assert finding_ocsf.status_id == StatusID.New.value + assert finding_ocsf.status == StatusID.New.name assert finding_ocsf.status_code == finding_output.status assert finding_ocsf.status_detail == finding_output.status_extended @@ -365,38 +420,12 @@ class TestOCSF: provider = "None" assert OCSF.get_account_type_id_by_provider(provider) == TypeID.Other - # Returns StatusID.New when status is "FAIL" and muted is False - def test_new_when_status_fail_and_not_muted(self): - status = "FAIL" + # Returns StatusID.New when muted is False + def test_new_when_not_muted(self): muted = False - assert OCSF.get_finding_status_id(status, muted) == StatusID.New + assert OCSF.get_finding_status_id(muted) == StatusID.New - # Returns StatusID.Suppressed when status is "FAIL" and muted is True - def test_suppressed_when_status_fail_and_muted(self): - status = "FAIL" + # Returns StatusID.Suppressed when muted is True + def test_suppressed_when_muted(self): muted = True - assert OCSF.get_finding_status_id(status, muted) == StatusID.Suppressed - - # Returns StatusID.Other when status is PASS and muted is False - def test_other_when_status_whatever_and_not_muted(self): - status = "PASS" - muted = False - assert OCSF.get_finding_status_id(status, muted) == StatusID.Other - - # Returns StatusID.Suppresed when status is PASS and muted is True - def test_other_when_status_whatever_and_muted(self): - status = "PASS" - muted = True - assert OCSF.get_finding_status_id(status, muted) == StatusID.Suppressed - - # Returns StatusID.Suppressed when muted is True and status is not "FAIL" - def test_suppressed_when_status_pass_and_muted(self): - status = "PASS" - muted = True - assert OCSF.get_finding_status_id(status, muted) == StatusID.Suppressed - - # Returns StatusID.Other when muted is False and status is not "FAIL" - def test_other_when_status_pass_and_not_muted(self): - status = "PASS" - muted = False - assert OCSF.get_finding_status_id(status, muted) == StatusID.Other + assert OCSF.get_finding_status_id(muted) == StatusID.Suppressed