From 86bba2da1ae9a5de6b1a782f19c1f9cfb0637d34 Mon Sep 17 00:00:00 2001 From: Eric Hibbs Date: Tue, 4 Feb 2025 16:23:59 -0800 Subject: [PATCH] Eric/cus 9 support cli updates (#14) * Added typing, bumped version to v2, supports CLI v2 --- .github/workflows/pr-preview.yml | 14 +- .github/workflows/release.yml | 2 +- .github/workflows/version-check.yml | 13 +- .python-version | 1 + Pipfile | 15 - Pipfile.lock | 472 --------------- pyproject.toml | 14 +- requirements-dev.lock | 72 +++ requirements.lock | 72 +++ scripts/build.sh | 2 +- socketdev/__init__.py | 113 +--- socketdev/core/api.py | 53 ++ socketdev/dependencies/__init__.py | 35 +- socketdev/export/__init__.py | 18 +- socketdev/fullscans/__init__.py | 857 +++++++++++++++++++++++++--- socketdev/npm/__init__.py | 15 +- socketdev/openapi/__init__.py | 10 +- socketdev/org/__init__.py | 32 +- socketdev/purl/__init__.py | 10 +- socketdev/quota/__init__.py | 10 +- socketdev/report/__init__.py | 42 +- socketdev/repos/__init__.py | 155 +++-- socketdev/repositories/__init__.py | 9 +- socketdev/sbom/__init__.py | 14 +- socketdev/settings/__init__.py | 101 +++- socketdev/utils/__init__.py | 12 + socketdev/version.py | 1 + 27 files changed, 1335 insertions(+), 829 deletions(-) create mode 100644 .python-version delete mode 100644 Pipfile delete mode 100644 Pipfile.lock create mode 100644 requirements-dev.lock create mode 100644 requirements.lock create mode 100644 socketdev/core/api.py create mode 100644 socketdev/utils/__init__.py create mode 100644 socketdev/version.py diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 3a7725e..128a103 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -23,14 +23,12 @@ jobs: PREVIEW_VERSION="${BASE_VERSION}.dev${{ github.event.pull_request.number }}${{ github.event.pull_request.commits }}" echo "VERSION=${PREVIEW_VERSION}" >> $GITHUB_ENV - # Update version in __init__.py - echo "__version__ = \"${PREVIEW_VERSION}\"" > socketdev/__init__.py.tmp - cat socketdev/__init__.py | grep -v "__version__" >> socketdev/__init__.py.tmp - mv socketdev/__init__.py.tmp socketdev/__init__.py + # Update version in version.py instead of __init__.py + echo "__version__ = \"${PREVIEW_VERSION}\"" > socketdev/version.py # Verify the change - echo "Updated version in __init__.py:" - cat socketdev/__init__.py | grep "__version__" + echo "Updated version in version.py:" + cat socketdev/version.py - name: Check if version exists on Test PyPI id: version_check @@ -55,9 +53,7 @@ jobs: if: always() run: | BASE_VERSION=$(echo $VERSION | cut -d'.' -f1-3) - echo "__version__ = \"${BASE_VERSION}\"" > socketdev/__init__.py.tmp - cat socketdev/__init__.py | grep -v "__version__" >> socketdev/__init__.py.tmp - mv socketdev/__init__.py.tmp socketdev/__init__.py + echo "__version__ = \"${BASE_VERSION}\"" > socketdev/version.py - name: Publish to Test PyPI if: steps.version_check.outputs.exists != 'true' diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d6cfb63..16b8d81 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -16,7 +16,7 @@ jobs: - name: Get Version id: version run: | - RAW_VERSION=$(python -c "from socketdev import __version__; print(__version__)") + RAW_VERSION=$(python -c "from socketdev.version import __version__; print(__version__)") echo "VERSION=$RAW_VERSION" >> $GITHUB_ENV if [ "v$RAW_VERSION" != "${{ github.ref_name }}" ]; then echo "Error: Git tag (${{ github.ref_name }}) does not match package version (v$RAW_VERSION)" diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index 95ec831..11305a5 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -19,13 +19,20 @@ jobs: id: version_check run: | # Get version from current PR - PR_VERSION=$(grep -o '__version__ = "[^"]*"' socketdev/__init__.py | cut -d'"' -f2) + PR_VERSION=$(grep -o '__version__ = "[^"]*"' socketdev/version.py | cut -d'"' -f2) echo "Debug PR version: $PR_VERSION" echo "PR_VERSION=${PR_VERSION}" >> $GITHUB_ENV - # Get version from main branch + # Get version from main branch - try both locations git checkout origin/main - MAIN_VERSION=$(grep -o '__version__ = "[^"]*"' socketdev/__init__.py | cut -d'"' -f2) + if [ -f socketdev/version.py ]; then + MAIN_VERSION=$(grep -o '__version__ = "[^"]*"' socketdev/version.py | cut -d'"' -f2) + else + # Fall back to old location in __init__.py + # Use more specific grep to avoid matching the imported version + MAIN_VERSION=$(grep -o '^__version__ = "[^"]*"' socketdev/__init__.py | cut -d'"' -f2) + fi + echo "Debug main version: $MAIN_VERSION" echo "MAIN_VERSION=${MAIN_VERSION}" >> $GITHUB_ENV diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..56bb660 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12.7 diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 6898670..0000000 --- a/Pipfile +++ /dev/null @@ -1,15 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests = "*" - -[dev-packages] -twine = "*" -wheel = "*" -build = "*" - -[requires] -python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index 0af4cb2..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,472 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "0789e3796a2c5709f50b6a4c5ef9802ad22c9ecd38fa61b127d075075584ca1d" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.12" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "certifi": { - "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.8.30" - }, - "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" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "idna": { - "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" - ], - "markers": "python_version >= '3.6'", - "version": "==3.8" - }, - "requests": { - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.3" - }, - "urllib3": { - "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.3" - } - }, - "develop": { - "build": { - "hashes": [ - "sha256:119b2fb462adef986483438377a13b2f42064a2a3a4161f24a0cca698a07ac8c", - "sha256:277ccc71619d98afdd841a0e96ac9fe1593b823af481d3b0cea748e8894e0613" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==1.2.2" - }, - "certifi": { - "hashes": [ - "sha256:922820b53db7a7257ffbda3f597266d435245903d80737e34f8a45ff3e3230d8", - "sha256:bec941d2aa8195e248a60b31ff9f0558284cf01a52591ceda73ea9afffd69fd9" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.8.30" - }, - "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" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "docutils": { - "hashes": [ - "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", - "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2" - ], - "markers": "python_version >= '3.9'", - "version": "==0.21.2" - }, - "idna": { - "hashes": [ - "sha256:050b4e5baadcd44d760cedbd2b8e639f2ff89bbc7a5730fcc662954303377aac", - "sha256:d838c2c0ed6fced7693d5e8ab8e734d5f8fda53a039c0164afb0b82e771e3603" - ], - "markers": "python_version >= '3.6'", - "version": "==3.8" - }, - "importlib-metadata": { - "hashes": [ - "sha256:45e54197d28b7a7f1559e60b95e7c567032b602131fbd588f1497f47880aa68b", - "sha256:71522656f0abace1d072b9e5481a48f07c138e00f079c38c8f883823f9c26bd7" - ], - "markers": "python_version >= '3.8'", - "version": "==8.5.0" - }, - "jaraco.classes": { - "hashes": [ - "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", - "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790" - ], - "markers": "python_version >= '3.8'", - "version": "==3.4.0" - }, - "jaraco.context": { - "hashes": [ - "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", - "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4" - ], - "markers": "python_version >= '3.8'", - "version": "==6.0.1" - }, - "jaraco.functools": { - "hashes": [ - "sha256:3460c74cd0d32bf82b9576bbb3527c4364d5b27a21f5158a62aed6c4b42e23f5", - "sha256:c9d16a3ed4ccb5a889ad8e0b7a343401ee5b2a71cee6ed192d3f68bc351e94e3" - ], - "markers": "python_version >= '3.8'", - "version": "==4.0.2" - }, - "keyring": { - "hashes": [ - "sha256:8d85a1ea5d6db8515b59e1c5d1d1678b03cf7fc8b8dcfb1651e8c4a524eb42ef", - "sha256:8d963da00ccdf06e356acd9bf3b743208878751032d8599c6cc89eb51310ffae" - ], - "markers": "python_version >= '3.8'", - "version": "==25.3.0" - }, - "markdown-it-py": { - "hashes": [ - "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", - "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb" - ], - "markers": "python_version >= '3.8'", - "version": "==3.0.0" - }, - "mdurl": { - "hashes": [ - "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", - "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba" - ], - "markers": "python_version >= '3.7'", - "version": "==0.1.2" - }, - "more-itertools": { - "hashes": [ - "sha256:037b0d3203ce90cca8ab1defbbdac29d5f993fc20131f3664dc8d6acfa872aef", - "sha256:5482bfef7849c25dc3c6dd53a6173ae4795da2a41a80faea6700d9f5846c5da6" - ], - "markers": "python_version >= '3.8'", - "version": "==10.5.0" - }, - "nh3": { - "hashes": [ - "sha256:0411beb0589eacb6734f28d5497ca2ed379eafab8ad8c84b31bb5c34072b7164", - "sha256:14c5a72e9fe82aea5fe3072116ad4661af5cf8e8ff8fc5ad3450f123e4925e86", - "sha256:19aaba96e0f795bd0a6c56291495ff59364f4300d4a39b29a0abc9cb3774a84b", - "sha256:34c03fa78e328c691f982b7c03d4423bdfd7da69cd707fe572f544cf74ac23ad", - "sha256:36c95d4b70530b320b365659bb5034341316e6a9b30f0b25fa9c9eff4c27a204", - "sha256:3a157ab149e591bb638a55c8c6bcb8cdb559c8b12c13a8affaba6cedfe51713a", - "sha256:42c64511469005058cd17cc1537578eac40ae9f7200bedcfd1fc1a05f4f8c200", - "sha256:5f36b271dae35c465ef5e9090e1fdaba4a60a56f0bb0ba03e0932a66f28b9189", - "sha256:6955369e4d9f48f41e3f238a9e60f9410645db7e07435e62c6a9ea6135a4907f", - "sha256:7b7c2a3c9eb1a827d42539aa64091640bd275b81e097cd1d8d82ef91ffa2e811", - "sha256:8ce0f819d2f1933953fca255db2471ad58184a60508f03e6285e5114b6254844", - "sha256:94a166927e53972a9698af9542ace4e38b9de50c34352b962f4d9a7d4c927af4", - "sha256:a7f1b5b2c15866f2db413a3649a8fe4fd7b428ae58be2c0f6bca5eefd53ca2be", - "sha256:c8b3a1cebcba9b3669ed1a84cc65bf005728d2f0bc1ed2a6594a992e817f3a50", - "sha256:de3ceed6e661954871d6cd78b410213bdcb136f79aafe22aa7182e028b8c7307", - "sha256:f0eca9ca8628dbb4e916ae2491d72957fdd35f7a5d326b7032a345f111ac07fe" - ], - "version": "==0.2.18" - }, - "packaging": { - "hashes": [ - "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002", - "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124" - ], - "markers": "python_version >= '3.8'", - "version": "==24.1" - }, - "pkginfo": { - "hashes": [ - "sha256:5df73835398d10db79f8eecd5cd86b1f6d29317589ea70796994d49399af6297", - "sha256:889a6da2ed7ffc58ab5b900d888ddce90bce912f2d2de1dc1c26f4cb9fe65097" - ], - "markers": "python_version >= '3.6'", - "version": "==1.10.0" - }, - "pygments": { - "hashes": [ - "sha256:786ff802f32e91311bff3889f6e9a86e81505fe99f2735bb6d60ae0c5004f199", - "sha256:b8e6aca0523f3ab76fee51799c488e38782ac06eafcf95e7ba832985c8e7b13a" - ], - "markers": "python_version >= '3.8'", - "version": "==2.18.0" - }, - "pyproject-hooks": { - "hashes": [ - "sha256:4b37730834edbd6bd37f26ece6b44802fb1c1ee2ece0e54ddff8bfc06db86965", - "sha256:7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2" - ], - "markers": "python_version >= '3.7'", - "version": "==1.1.0" - }, - "readme-renderer": { - "hashes": [ - "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", - "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1" - ], - "markers": "python_version >= '3.9'", - "version": "==44.0" - }, - "requests": { - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.3" - }, - "requests-toolbelt": { - "hashes": [ - "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", - "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06" - ], - "markers": "python_version >= '2.7' and python_version not in '3.0, 3.1, 3.2, 3.3'", - "version": "==1.0.0" - }, - "rfc3986": { - "hashes": [ - "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", - "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c" - ], - "markers": "python_version >= '3.7'", - "version": "==2.0.0" - }, - "rich": { - "hashes": [ - "sha256:1760a3c0848469b97b558fc61c85233e3dafb69c7a071b4d60c38099d3cd4c06", - "sha256:8260cda28e3db6bf04d2d1ef4dbc03ba80a824c88b0e7668a0f23126a424844a" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==13.8.1" - }, - "twine": { - "hashes": [ - "sha256:215dbe7b4b94c2c50a7315c0275d2258399280fbb7d04182c7e55e24b5f93997", - "sha256:9aa0825139c02b3434d913545c7b847a21c835e11597f5255842d457da2322db" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==5.1.1" - }, - "urllib3": { - "hashes": [ - "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", - "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.3" - }, - "wheel": { - "hashes": [ - "sha256:2376a90c98cc337d18623527a97c31797bd02bad0033d41547043a1cbfbe448f", - "sha256:a29c3f2817e95ab89aa4660681ad547c0e9547f20e75b0562fe7723c9a2a9d49" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==0.44.0" - }, - "zipp": { - "hashes": [ - "sha256:9960cd8967c8f85a56f920d5d507274e74f9ff813a0ab8889a5b5be2daf44064", - "sha256:c22b14cc4763c5a5b04134207736c107db42e9d3ef2d9779d465f5f1bcba572b" - ], - "markers": "python_version >= '3.8'", - "version": "==3.20.1" - } - } -} diff --git a/pyproject.toml b/pyproject.toml index 4bb4240..a60130d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,8 @@ [build-system] -requires = ["setuptools >= 61.0"] +requires = [ + "setuptools >= 61.0", + "requests" +] build-backend = "setuptools.build_meta" [project] @@ -7,7 +10,8 @@ name = "socket-sdk-python" dynamic = ["version"] requires-python = ">= 3.9" dependencies = [ - 'requests' + 'requests', + 'typing-extensions>=4.12.2' ] readme = "README.rst" license = {file = "LICENSE"} @@ -34,6 +38,9 @@ classifiers = [ [project.optional-dependencies] dev = [ "ruff>=0.3.0", + "twine", + "wheel", + "build", ] [project.urls] @@ -57,10 +64,11 @@ include = [ "socketdev.sbom", "socketdev.settings", "socketdev.tools", + "socketdev.utils", ] [tool.setuptools.dynamic] -version = {attr = "socketdev.__version__"} +version = {attr = "socketdev.version.__version__"} [tool.ruff] # Exclude a variety of commonly ignored directories. diff --git a/requirements-dev.lock b/requirements-dev.lock new file mode 100644 index 0000000..8773aed --- /dev/null +++ b/requirements-dev.lock @@ -0,0 +1,72 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +build==1.2.2.post1 + # via socket-sdk-python +certifi==2024.12.14 + # via requests +charset-normalizer==3.4.1 + # via requests +docutils==0.21.2 + # via readme-renderer +idna==3.10 + # via requests +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.0.1 + # via keyring +jaraco-functools==4.1.0 + # via keyring +keyring==25.6.0 + # via twine +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.6.0 + # via jaraco-classes + # via jaraco-functools +nh3==0.2.20 + # via readme-renderer +packaging==24.2 + # via build + # via twine +pkginfo==1.12.0 + # via twine +pygments==2.19.1 + # via readme-renderer + # via rich +pyproject-hooks==1.2.0 + # via build +readme-renderer==44.0 + # via twine +requests==2.32.3 + # via requests-toolbelt + # via socket-sdk-python + # via twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==13.9.4 + # via twine +ruff==0.9.1 + # via socket-sdk-python +twine==6.0.1 + # via socket-sdk-python +typing-extensions==4.12.2 + # via socket-sdk-python +urllib3==2.3.0 + # via requests + # via twine +wheel==0.45.1 + # via socket-sdk-python diff --git a/requirements.lock b/requirements.lock new file mode 100644 index 0000000..8773aed --- /dev/null +++ b/requirements.lock @@ -0,0 +1,72 @@ +# generated by rye +# use `rye lock` or `rye sync` to update this lockfile +# +# last locked with the following flags: +# pre: false +# features: [] +# all-features: true +# with-sources: false +# generate-hashes: false +# universal: false + +-e file:. +build==1.2.2.post1 + # via socket-sdk-python +certifi==2024.12.14 + # via requests +charset-normalizer==3.4.1 + # via requests +docutils==0.21.2 + # via readme-renderer +idna==3.10 + # via requests +jaraco-classes==3.4.0 + # via keyring +jaraco-context==6.0.1 + # via keyring +jaraco-functools==4.1.0 + # via keyring +keyring==25.6.0 + # via twine +markdown-it-py==3.0.0 + # via rich +mdurl==0.1.2 + # via markdown-it-py +more-itertools==10.6.0 + # via jaraco-classes + # via jaraco-functools +nh3==0.2.20 + # via readme-renderer +packaging==24.2 + # via build + # via twine +pkginfo==1.12.0 + # via twine +pygments==2.19.1 + # via readme-renderer + # via rich +pyproject-hooks==1.2.0 + # via build +readme-renderer==44.0 + # via twine +requests==2.32.3 + # via requests-toolbelt + # via socket-sdk-python + # via twine +requests-toolbelt==1.0.0 + # via twine +rfc3986==2.0.0 + # via twine +rich==13.9.4 + # via twine +ruff==0.9.1 + # via socket-sdk-python +twine==6.0.1 + # via socket-sdk-python +typing-extensions==4.12.2 + # via socket-sdk-python +urllib3==2.3.0 + # via requests + # via twine +wheel==0.45.1 + # via socket-sdk-python diff --git a/scripts/build.sh b/scripts/build.sh index c4daabe..6f92563 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -1,6 +1,6 @@ #!/bin/sh -VERSION=$(grep -o "__version__.*" socketdev/__init__.py | awk '{print $3}' | sed 's/"//g' | sed "s/'//g" | tr -d '\r') +VERSION=$(grep -o "__version__.*" socketdev/version.py | awk '{print $3}' | sed 's/"//g' | sed "s/'//g" | tr -d '\r') ENABLE_PYPI_BUILD=$1 if [ -z $ENABLE_PYPI_BUILD ]; then diff --git a/socketdev/__init__.py b/socketdev/__init__.py index 24af243..0c97b24 100644 --- a/socketdev/__init__.py +++ b/socketdev/__init__.py @@ -1,10 +1,7 @@ import logging -import requests -import base64 -from socketdev.core.classes import Response +from socketdev.core.api import API from socketdev.dependencies import Dependencies -from socketdev.exceptions import APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota, APIResourceNotFound from socketdev.export import Export from socketdev.fullscans import FullScans from socketdev.npm import NPM @@ -17,11 +14,14 @@ from socketdev.repositories import Repositories from socketdev.sbom import Sbom from socketdev.settings import Settings +from socketdev.utils import Utils, IntegrationType, INTEGRATION_TYPES +from socketdev.version import __version__ __author__ = "socket.dev" -__version__ = "1.0.15" -__all__ = ["socketdev"] +__version__ = __version__ +__all__ = ["socketdev", "Utils", "IntegrationType", "INTEGRATION_TYPES"] + global encoded_key @@ -33,90 +33,29 @@ log.addHandler(logging.NullHandler()) -def encode_key(token: str): - global encoded_key - encoded_key = base64.b64encode(token.encode()).decode("ascii") - - -def do_request( - path: str, headers: dict = None, payload: [dict, str] = None, files: list = None, method: str = "GET" -) -> Response: - """ - Shared function for performing the requests against the API. - :param path: String path of the URL - :param headers: Optional dictionary of the headers to include in the request. Defaults to None - :param payload: Optional dictionary or string of the payload to POST. Defaults to None - :param files: Optional list of files to send. Defaults to None - :param method: Optional string of the method for the Request. Defaults to GET - """ - - if encoded_key is None or encoded_key == "": - raise APIKeyMissing - - if headers is None: - headers = { - "Authorization": f"Basic {encoded_key}", - "User-Agent": f"SocketPythonScript/{__version__}", - "accept": "application/json", - } - url = f"{api_url}/{path}" - try: - response = requests.request( - method.upper(), url, headers=headers, data=payload, files=files, timeout=request_timeout - ) - if response.status_code >= 400: - raise APIFailure("Bad Request") - elif response.status_code == 401: - raise APIAccessDenied("Unauthorized") - elif response.status_code == 403: - raise APIInsufficientQuota("Insufficient max_quota for API method") - elif response.status_code == 404: - raise APIResourceNotFound(f"Path not found {path}") - elif response.status_code == 429: - raise APIInsufficientQuota("Insufficient quota for API route") - except Exception as error: - response = Response(text=f"{error}", error=True, status_code=500) - raise APIFailure(response) - return response - - class socketdev: - token: str - timeout: int - dependencies: Dependencies - npm: NPM - openapi: OpenAPI - org: Orgs - quota: Quota - report: Report - sbom: Sbom - purl: Purl - fullscans: FullScans - export: Export - repositories: Repositories - settings: Settings - repos: Repos - def __init__(self, token: str, timeout: int = 30): + self.api = API() self.token = token + ":" - encode_key(self.token) - self.timeout = timeout - socketdev.set_timeout(self.timeout) - self.dependencies = Dependencies() - self.npm = NPM() - self.openapi = OpenAPI() - self.org = Orgs() - self.quota = Quota() - self.report = Report() - self.sbom = Sbom() - self.purl = Purl() - self.fullscans = FullScans() - self.export = Export() - self.repositories = Repositories() - self.repos = Repos() - self.settings = Settings() + self.api.encode_key(self.token) + self.api.set_timeout(timeout) + + self.dependencies = Dependencies(self.api) + self.npm = NPM(self.api) + self.openapi = OpenAPI(self.api) + self.org = Orgs(self.api) + self.quota = Quota(self.api) + self.report = Report(self.api) + self.sbom = Sbom(self.api) + self.purl = Purl(self.api) + self.fullscans = FullScans(self.api) + self.export = Export(self.api) + self.repositories = Repositories(self.api) + self.repos = Repos(self.api) + self.settings = Settings(self.api) + self.utils = Utils() @staticmethod def set_timeout(timeout: int): - global request_timeout - request_timeout = timeout + # Kept for backwards compatibility + pass diff --git a/socketdev/core/api.py b/socketdev/core/api.py new file mode 100644 index 0000000..67ee6f1 --- /dev/null +++ b/socketdev/core/api.py @@ -0,0 +1,53 @@ +import base64 +import requests +from socketdev.core.classes import Response +from socketdev.exceptions import APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota, APIResourceNotFound +from socketdev.version import __version__ + + +class API: + def __init__(self): + self.encoded_key = None + self.api_url = "https://api.socket.dev/v0" + self.request_timeout = 30 + + def encode_key(self, token: str): + self.encoded_key = base64.b64encode(token.encode()).decode("ascii") + + def set_timeout(self, timeout: int): + self.request_timeout = timeout + + def do_request( + self, path: str, headers: dict | None = None, payload: [dict, str] = None, files: list = None, method: str = "GET" + ) -> Response: + if self.encoded_key is None or self.encoded_key == "": + raise APIKeyMissing + + if headers is None: + headers = { + "Authorization": f"Basic {self.encoded_key}", + "User-Agent": f"SocketPythonScript/{__version__}", + "accept": "application/json", + } + url = f"{self.api_url}/{path}" + try: + response = requests.request( + method.upper(), url, headers=headers, data=payload, files=files, timeout=self.request_timeout + ) + + if response.status_code == 401: + raise APIAccessDenied("Unauthorized") + if response.status_code == 403: + raise APIInsufficientQuota("Insufficient max_quota for API method") + if response.status_code == 404: + raise APIResourceNotFound(f"Path not found {path}") + if response.status_code == 429: + raise APIInsufficientQuota("Insufficient quota for API route") + if response.status_code >= 400: + raise APIFailure("Bad Request") + + return response + + except Exception as error: + response = Response(text=f"{error}", error=True, status_code=500) + raise APIFailure(response) diff --git a/socketdev/dependencies/__init__.py b/socketdev/dependencies/__init__.py index b229aa1..45ea8c5 100644 --- a/socketdev/dependencies/__init__.py +++ b/socketdev/dependencies/__init__.py @@ -1,20 +1,18 @@ -import socketdev -from socketdev.tools import load_files -from urllib.parse import urlencode import json +from urllib.parse import urlencode + +from socketdev.tools import load_files class Dependencies: - @staticmethod - def post(files: list, params: dict) -> dict: + def __init__(self, api): + self.api = api + + def post(self, files: list, params: dict) -> dict: loaded_files = [] loaded_files = load_files(files, loaded_files) path = "dependencies/upload?" + urlencode(params) - response = socketdev.do_request( - path=path, - files=loaded_files, - method="POST" - ) + response = self.api.do_request(path=path, files=loaded_files, method="POST") if response.status_code == 200: result = response.json() else: @@ -23,22 +21,15 @@ def post(files: list, params: dict) -> dict: print(response.text) return result - @staticmethod def get( - limit: int = 50, - offset: int = 0, + self, + limit: int = 50, + offset: int = 0, ) -> dict: path = "dependencies/search" - payload = { - "limit": limit, - "offset": offset - } + payload = {"limit": limit, "offset": offset} payload_str = json.dumps(payload) - response = socketdev.do_request( - path=path, - method="POST", - payload=payload_str - ) + response = self.api.do_request(path=path, method="POST", payload=payload_str) if response.status_code == 200: result = response.json() else: diff --git a/socketdev/export/__init__.py b/socketdev/export/__init__.py index 4e3907e..d56f886 100644 --- a/socketdev/export/__init__.py +++ b/socketdev/export/__init__.py @@ -1,7 +1,6 @@ from urllib.parse import urlencode from dataclasses import dataclass, asdict from typing import Optional -import socketdev @dataclass @@ -21,8 +20,10 @@ def to_query_params(self) -> str: class Export: - @staticmethod - def cdx_bom(org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None) -> dict: + def __init__(self, api): + self.api = api + + def cdx_bom(self, org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None) -> dict: """ Export a Socket SBOM as a CycloneDX SBOM :param org_slug: String - The slug of the organization @@ -33,16 +34,15 @@ def cdx_bom(org_slug: str, id: str, query_params: Optional[ExportQueryParams] = path = f"orgs/{org_slug}/export/cdx/{id}" if query_params: path += query_params.to_query_params() - result = socketdev.do_request(path=path) + response = self.api.do_request(path=path) try: - sbom = result.json() + sbom = response.json() sbom["success"] = True except Exception as error: sbom = {"success": False, "message": str(error)} return sbom - @staticmethod - def spdx_bom(org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None) -> dict: + def spdx_bom(self, org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None) -> dict: """ Export a Socket SBOM as an SPDX SBOM :param org_slug: String - The slug of the organization @@ -53,9 +53,9 @@ def spdx_bom(org_slug: str, id: str, query_params: Optional[ExportQueryParams] = path = f"orgs/{org_slug}/export/spdx/{id}" if query_params: path += query_params.to_query_params() - result = socketdev.do_request(path=path) + response = self.api.do_request(path=path) try: - sbom = result.json() + sbom = response.json() sbom["success"] = True except Exception as error: sbom = {"success": False, "message": str(error)} diff --git a/socketdev/fullscans/__init__.py b/socketdev/fullscans/__init__.py index 1c98c2f..a661663 100644 --- a/socketdev/fullscans/__init__.py +++ b/socketdev/fullscans/__init__.py @@ -1,127 +1,820 @@ -import socketdev -from socketdev.tools import load_files import json import logging +from enum import Enum +from typing import Any, Dict, List, Optional +from dataclasses import dataclass, asdict, field + + +from ..utils import IntegrationType, Utils log = logging.getLogger("socketdev") +class SocketPURL_Type(str, Enum): + UNKNOWN = "unknown" + NPM = "npm" + PYPI = "pypi" + GOLANG = "golang" + + +class SocketIssueSeverity(str, Enum): + LOW = "low" + MIDDLE = "middle" + HIGH = "high" + CRITICAL = "critical" + + +class SocketCategory(str, Enum): + SUPPLY_CHAIN_RISK = "supplyChainRisk" + QUALITY = "quality" + MAINTENANCE = "maintenance" + VULNERABILITY = "vulnerability" + LICENSE = "license" + MISCELLANEOUS = "miscellaneous" + +class DiffType(str, Enum): + ADDED = "added" + REMOVED = "removed" + UNCHANGED = "unchanged" + REPLACED = "replaced" + UPDATED = "updated" + +@dataclass(kw_only=True) +class SocketPURL: + type: SocketPURL_Type + name: Optional[str] = None + namespace: Optional[str] = None + release: Optional[str] = None + subpath: Optional[str] = None + version: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SocketPURL": + return cls( + type=SocketPURL_Type(data["type"]), + name=data.get("name"), + namespace=data.get("namespace"), + release=data.get("release"), + subpath=data.get("subpath"), + version=data.get("version") + ) + +@dataclass +class SocketManifestReference: + file: str + start: Optional[int] = None + end: Optional[int] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SocketManifestReference": + return cls( + file=data["file"], + start=data.get("start"), + end=data.get("end") + ) + +@dataclass +class FullScanParams: + repo: str + org_slug: Optional[str] = None + branch: Optional[str] = None + commit_message: Optional[str] = None + commit_hash: Optional[str] = None + pull_request: Optional[int] = None + committers: Optional[List[str]] = None + integration_type: Optional[IntegrationType] = None + integration_org_slug: Optional[str] = None + make_default_branch: Optional[bool] = None + set_as_pending_head: Optional[bool] = None + tmp: Optional[bool] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "FullScanParams": + integration_type = data.get("integration_type") + return cls( + repo=data["repo"], + org_slug=data.get("org_slug"), + branch=data.get("branch"), + commit_message=data.get("commit_message"), + commit_hash=data.get("commit_hash"), + pull_request=data.get("pull_request"), + committers=data.get("committers"), + integration_type=IntegrationType(integration_type) if integration_type else None, + integration_org_slug=data.get("integration_org_slug"), + make_default_branch=data.get("make_default_branch"), + set_as_pending_head=data.get("set_as_pending_head"), + tmp=data.get("tmp") + ) + +@dataclass +class FullScanMetadata: + id: str + created_at: str + updated_at: str + organization_id: str + repository_id: str + branch: str + html_report_url: str + repo: Optional[str] = None # In docs, never shows up + organization_slug: Optional[str] = None # In docs, never shows up + committers: Optional[List[str]] = None + commit_message: Optional[str] = None + commit_hash: Optional[str] = None + pull_request: Optional[int] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "FullScanMetadata": + return cls( + id=data["id"], + created_at=data["created_at"], + updated_at=data["updated_at"], + organization_id=data["organization_id"], + repository_id=data["repository_id"], + branch=data["branch"], + html_report_url=data["html_report_url"], + repo=data.get("repo"), + organization_slug=data.get("organization_slug"), + committers=data.get("committers"), + commit_message=data.get("commit_message"), + commit_hash=data.get("commit_hash"), + pull_request=data.get("pull_request") + ) + +@dataclass +class CreateFullScanResponse: + success: bool + status: int + data: Optional[FullScanMetadata] = None + message: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "CreateFullScanResponse": + return cls( + success=data["success"], + status=data["status"], + message=data.get("message"), + data=FullScanMetadata.from_dict(data.get("data")) if data.get("data") else None + ) + +@dataclass +class GetFullScanMetadataResponse: + success: bool + status: int + data: Optional[FullScanMetadata] = None + message: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "GetFullScanMetadataResponse": + return cls( + success=data["success"], + status=data["status"], + message=data.get("message"), + data=FullScanMetadata.from_dict(data.get("data")) if data.get("data") else None + ) + +@dataclass +class DependencyRef: + direct: bool + toplevelAncestors: List[str] + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "DependencyRef": + return cls( + direct=data["direct"], + toplevelAncestors=data["toplevelAncestors"] + ) + +@dataclass +class SocketScore: + supplyChain: float + quality: float + maintenance: float + vulnerability: float + license: float + overall: float + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SocketScore": + return cls( + supplyChain=data["supplyChain"], + quality=data["quality"], + maintenance=data["maintenance"], + vulnerability=data["vulnerability"], + license=data["license"], + overall=data["overall"] + ) + +@dataclass +class SecurityCapabilities: + env: bool + eval: bool + fs: bool + net: bool + shell: bool + unsafe: bool + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SecurityCapabilities": + return cls( + env=data["env"], + eval=data["eval"], + fs=data["fs"], + net=data["net"], + shell=data["shell"], + unsafe=data["unsafe"] + ) + +@dataclass +class Alert: + key: str + type: int + file: str + start: int + end: int + props: Dict[str, Any] + action: str + actionPolicyIndex: int + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "Alert": + return cls( + key=data["key"], + type=data["type"], + file=data["file"], + start=data["start"], + end=data["end"], + props=data["props"], + action=data["action"], + actionPolicyIndex=data["actionPolicyIndex"] + ) + +@dataclass +class LicenseMatch: + licenseId: str + licenseExceptionId: str + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "LicenseMatch": + return cls( + licenseId=data["licenseId"], + licenseExceptionId=data["licenseExceptionId"] + ) + +@dataclass +class LicenseDetail: + authors: List[str] + charEnd: int + charStart: int + filepath: str + match_strength: int + filehash: str + provenance: str + spdxDisj: List[List[LicenseMatch]] + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "LicenseDetail": + return cls( + authors=data["authors"], + charEnd=data["charEnd"], + charStart=data["charStart"], + filepath=data["filepath"], + match_strength=data["match_strength"], + filehash=data["filehash"], + provenance=data["provenance"], + spdxDisj=[[LicenseMatch.from_dict(match) for match in group] + for group in data["spdxDisj"]] + ) + +@dataclass +class AttributionData: + purl: str + foundAuthors: List[str] + foundInFilepath: Optional[str] = None + spdxExpr: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "AttributionData": + return cls( + purl=data["purl"], + foundAuthors=data["foundAuthors"], + foundInFilepath=data.get("foundInFilepath"), + spdxExpr=data.get("spdxExpr") + ) + +@dataclass +class LicenseAttribution: + attribText: str + attribData: List[AttributionData] + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "LicenseAttribution": + return cls( + attribText=data["attribText"], + attribData=[AttributionData.from_dict(item) for item in data["attribData"]] + ) + +@dataclass +class DiffArtifactAlert: + key: str + type: str + severity: Optional[SocketIssueSeverity] = None + category: Optional[SocketCategory] = None + file: Optional[str] = None + start: Optional[int] = None + end: Optional[int] = None + props: Optional[Dict[str, Any]] = None + action: Optional[str] = None + actionPolicyIndex: Optional[int] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "DiffArtifactAlert": + severity = data.get("severity") + category = data.get("category") + return cls( + key=data["key"], + type=data["type"], + severity=SocketIssueSeverity(severity) if severity else None, + category=SocketCategory(category) if category else None, + file=data.get("file"), + start=data.get("start"), + end=data.get("end"), + props=data.get("props"), + action=data.get("action"), + actionPolicyIndex=data.get("actionPolicyIndex") + ) + +@dataclass +class DiffArtifact: + diffType: DiffType + id: str + type: str + name: str + license: str + scores: SocketScore + capabilities: SecurityCapabilities + files: str + version: str + alerts: List[DiffArtifactAlert] + licenseDetails: List[LicenseDetail] + base: Optional[DependencyRef] = None + head: Optional[DependencyRef] = None + namespace: Optional[str] = None + subpath: Optional[str] = None + artifact_id: Optional[str] = None + artifactId: Optional[str] = None + qualifiers: Optional[Dict[str, Any]] = None + size: Optional[int] = None + author: Optional[str] = None + state: Optional[str] = None + error: Optional[str] = None + licenseAttrib: Optional[List[LicenseAttribution]] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "DiffArtifact": + return cls( + diffType=DiffType(data["diffType"]), + id=data["id"], + type=data["type"], + name=data["name"], + license=data.get("license", ""), + scores=SocketScore.from_dict(data["scores"]), + capabilities=SecurityCapabilities.from_dict(data["capabilities"]), + files=data["files"], + version=data["version"], + alerts=[DiffArtifactAlert.from_dict(alert) for alert in data["alerts"]], + licenseDetails=[LicenseDetail.from_dict(detail) for detail in data["licenseDetails"]], + base=DependencyRef.from_dict(data["base"]) if data.get("base") else None, + head=DependencyRef.from_dict(data["head"]) if data.get("head") else None, + namespace=data.get("namespace"), + subpath=data.get("subpath"), + artifact_id=data.get("artifact_id"), + artifactId=data.get("artifactId"), + qualifiers=data.get("qualifiers"), + size=data.get("size"), + author=data.get("author"), + state=data.get("state"), + error=data.get("error"), + licenseAttrib=[LicenseAttribution.from_dict(attrib) for attrib in data["licenseAttrib"]] if data.get("licenseAttrib") else None + ) + +@dataclass +class DiffArtifacts: + added: List[DiffArtifact] + removed: List[DiffArtifact] + unchanged: List[DiffArtifact] + replaced: List[DiffArtifact] + updated: List[DiffArtifact] + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "DiffArtifacts": + return cls( + added=[DiffArtifact.from_dict(a) for a in data["added"]], + removed=[DiffArtifact.from_dict(a) for a in data["removed"]], + unchanged=[DiffArtifact.from_dict(a) for a in data["unchanged"]], + replaced=[DiffArtifact.from_dict(a) for a in data["replaced"]], + updated=[DiffArtifact.from_dict(a) for a in data["updated"]] + ) + +@dataclass +class CommitInfo: + repository_id: str + branch: str + id: str + organization_id: str + committers: List[str] + commit_message: Optional[str] = None + commit_hash: Optional[str] = None + pull_request: Optional[int] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "CommitInfo": + return cls( + repository_id=data["repository_id"], + branch=data["branch"], + id=data["id"], + organization_id=data["organization_id"], + committers=data["committers"], + commit_message=data.get("commit_message"), + commit_hash=data.get("commit_hash"), + pull_request=data.get("pull_request") + ) + +@dataclass +class FullScanDiffReport: + before: CommitInfo + after: CommitInfo + directDependenciesChanged: bool + diff_report_url: str + artifacts: DiffArtifacts + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "FullScanDiffReport": + return cls( + before=CommitInfo.from_dict(data["before"]), + after=CommitInfo.from_dict(data["after"]), + directDependenciesChanged=data["directDependenciesChanged"], + diff_report_url=data["diff_report_url"], + artifacts=DiffArtifacts.from_dict(data["artifacts"]) + ) + +@dataclass +class StreamDiffResponse: + success: bool + status: int + data: Optional[FullScanDiffReport] = None + message: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "StreamDiffResponse": + return cls( + success=data["success"], + status=data["status"], + message=data.get("message"), + data=FullScanDiffReport.from_dict(data.get("data")) if data.get("data") else None + ) + +@dataclass(kw_only=True) +class SocketArtifactLink: + topLevelAncestors: List[str] + artifact: Optional[Dict] = None + dependencies: Optional[List[str]] = None + direct: Optional[bool] = None + manifestFiles: Optional[List[SocketManifestReference]] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SocketArtifactLink": + manifest_files = data.get("manifestFiles") + return cls( + topLevelAncestors=data["topLevelAncestors"], + artifact=data.get("artifact"), + dependencies=data.get("dependencies"), + direct=data.get("direct"), + manifestFiles=[SocketManifestReference.from_dict(m) for m in manifest_files] if manifest_files else None + ) + +@dataclass +class SocketAlert: + key: str + type: str + severity: SocketIssueSeverity + category: SocketCategory + file: Optional[str] = None + start: Optional[int] = None + end: Optional[int] = None + props: Optional[Dict[str, Any]] = None + action: Optional[str] = None + actionPolicyIndex: Optional[int] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SocketAlert": + return cls( + key=data["key"], + type=data["type"], + severity=SocketIssueSeverity(data["severity"]), + category=SocketCategory(data["category"]), + file=data.get("file"), + start=data.get("start"), + end=data.get("end"), + props=data.get("props"), + action=data.get("action"), + actionPolicyIndex=data.get("actionPolicyIndex") + ) + +@dataclass(kw_only=True) +class SocketArtifact(SocketPURL, SocketArtifactLink): + id: str + alerts: Optional[List[SocketAlert]] = field(default_factory=list) + author: Optional[List[str]] = field(default_factory=list) + batchIndex: Optional[int] = None + license: Optional[str] = None + licenseAttrib: Optional[List[LicenseAttribution]] = field(default_factory=list) + licenseDetails: Optional[List[LicenseDetail]] = field(default_factory=list) + score: Optional[SocketScore] = None + size: Optional[float] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SocketArtifact": + # First get the base class data + purl_data = {k: data.get(k) for k in SocketPURL.__dataclass_fields__} + link_data = {k: data.get(k) for k in SocketArtifactLink.__dataclass_fields__} + + # Handle nested types + alerts = data.get("alerts") + license_attrib = data.get("licenseAttrib") + license_details = data.get("licenseDetails") + score = data.get("score") + + return cls( + **purl_data, + **link_data, + id=data["id"], + alerts=[SocketAlert.from_dict(a) for a in alerts] if alerts is not None else [], + author=data.get("author"), + batchIndex=data.get("batchIndex"), + license=data.get("license"), + licenseAttrib=[LicenseAttribution.from_dict(la) for la in license_attrib] if license_attrib else None, + licenseDetails=[LicenseDetail.from_dict(ld) for ld in license_details] if license_details else None, + score=SocketScore.from_dict(score) if score else None, + size=data.get("size") + ) + +@dataclass +class FullScanStreamResponse: + success: bool + status: int + artifacts: Optional[Dict[str, SocketArtifact]] = None + message: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "FullScanStreamResponse": + return cls( + success=data["success"], + status=data["status"], + message=data.get("message"), + artifacts={ + k: SocketArtifact.from_dict(v) + for k, v in data["artifacts"].items() + } if data.get("artifacts") else None + ) class FullScans: - @staticmethod - def create_params_string(params: dict) -> str: + def __init__(self, api): + self.api = api + + def create_params_string(self, params: dict) -> str: param_str = "" - for name in params: - value = params[name] + for name, value in params.items(): if value: - param_str += f"&{name}={value}" + if name == "committers" and isinstance(value, list): + # Handle committers specially - add multiple params + for committer in value: + param_str += f"&{name}={committer}" + else: + param_str += f"&{name}={value}" param_str = "?" + param_str.lstrip("&") return param_str - @staticmethod - def get(org_slug: str, params: dict) -> dict: - params_arg = FullScans.create_params_string(params) + def get(self, org_slug: str, params: dict) -> GetFullScanMetadataResponse: + params_arg = self.create_params_string(params) + Utils.validate_integration_type(params.get("integration_type", "")) path = "orgs/" + org_slug + "/full-scans" + str(params_arg) headers = None payload = None - response = socketdev.do_request(path=path, headers=headers, payload=payload) + response = self.api.do_request(path=path, headers=headers, payload=payload) if response.status_code == 200: result = response.json() - result["success"] = True - result["status"] = 200 - return result - - result = {"success": False, "status": response.status_code, "message": response.text} - - return result + return GetFullScanMetadataResponse.from_dict({ + "success": True, + "status": 200, + "data": result + }) + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error getting full scan metadata: {response.status_code}, message: {error_message}") + return GetFullScanMetadataResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) + + def post(self, files: list, params: FullScanParams) -> CreateFullScanResponse: + + org_slug = str(params.org_slug) + params_dict = params.to_dict() + params_dict.pop("org_slug") + params_arg = self.create_params_string(params_dict) # Convert params to dict - @staticmethod - def post(files: list, params: dict, workspace: str = None) -> dict: - loaded_files = [] - loaded_files = load_files(files, loaded_files, workspace) - - params_arg = FullScans.create_params_string(params) - - path = "orgs/" + str(params["org_slug"]) + "/full-scans" + str(params_arg) - - response = socketdev.do_request(path=path, method="POST", files=loaded_files) + path = "orgs/" + org_slug + "/full-scans" + str(params_arg) + response = self.api.do_request(path=path, method="POST", files=files) + if response.status_code == 201: result = response.json() - else: - print(f"Error posting {files} to the Fullscans API") - print(response.text) - result = response.text - - return result - - @staticmethod - def delete(org_slug: str, full_scan_id: str) -> dict: + return CreateFullScanResponse.from_dict({ + "success": True, + "status": 201, + "data": result + }) + + log.error(f"Error posting {files} to the Fullscans API") + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(error_message) + + return CreateFullScanResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) + + def delete(self, org_slug: str, full_scan_id: str) -> dict: path = "orgs/" + org_slug + "/full-scans/" + full_scan_id - response = socketdev.do_request(path=path, method="DELETE") + response = self.api.do_request(path=path, method="DELETE") if response.status_code == 200: result = response.json() - else: - result = {} + return result - return result + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error deleting full scan: {response.status_code}, message: {error_message}") + return {} - @staticmethod - def stream_diff(org_slug: str, before: str, after: str, preview: bool = False) -> dict: - path = f"orgs/{org_slug}/full-scans/stream-diff?before={before}&after={after}&preview={preview}" + def stream_diff(self, org_slug: str, before: str, after: str) -> StreamDiffResponse: + path = f"orgs/{org_slug}/full-scans/diff?before={before}&after={after}" - response = socketdev.do_request(path=path, method="GET") + response = self.api.do_request(path=path, method="GET") if response.status_code == 200: - result = response.json() - else: - result = {} - - return result - - @staticmethod - def stream(org_slug: str, full_scan_id: str) -> dict: + return StreamDiffResponse.from_dict({ + "success": True, + "status": 200, + "data": response.json() + }) + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error streaming diff: {response.status_code}, message: {error_message}") + return StreamDiffResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) + + def stream(self, org_slug: str, full_scan_id: str) -> FullScanStreamResponse: path = "orgs/" + org_slug + "/full-scans/" + full_scan_id - response = socketdev.do_request(path=path, method="GET") - + response = self.api.do_request(path=path, method="GET") + if response.status_code == 200: - stream_str = [] - stream_dict = {} - result = response.text - result.strip('"') - result.strip() - for line in result.split("\n"): - if line != '"' and line != "" and line is not None: - item = json.loads(line) - stream_str.append(item) - for val in stream_str: - stream_dict[val["id"]] = val - - stream_dict["success"] = True - stream_dict["status"] = 200 - - return stream_dict + try: + stream_str = [] + artifacts = {} + result = response.text + result.strip('"') + result.strip() + for line in result.split("\n"): + if line != '"' and line != "" and line is not None: + item = json.loads(line) + stream_str.append(item) + for val in stream_str: + artifacts[val["id"]] = val # Just store the raw dict + + return FullScanStreamResponse.from_dict({ + "success": True, + "status": 200, + "artifacts": artifacts # Let from_dict handle the conversion + }) + except Exception as e: + error_message = f"Error parsing stream response: {str(e)}" + log.error(error_message) + return FullScanStreamResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error streaming full scan: {response.status_code}, message: {error_message}") + return FullScanStreamResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) + + def metadata(self, org_slug: str, full_scan_id: str) -> GetFullScanMetadataResponse: + path = "orgs/" + org_slug + "/full-scans/" + full_scan_id + "/metadata" - stream_dict = {"success": False, "status": response.status_code, "message": response.text} + response = self.api.do_request(path=path, method="GET") - return stream_dict + if response.status_code == 200: + return GetFullScanMetadataResponse.from_dict({ + "success": True, + "status": 200, + "data": response.json() + }) - @staticmethod - def metadata(org_slug: str, full_scan_id: str) -> dict: - path = "orgs/" + org_slug + "/full-scans/" + full_scan_id + "/metadata" + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error getting metadata: {response.status_code}, message: {error_message}") + return GetFullScanMetadataResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) - response = socketdev.do_request(path=path, method="GET") - if response.status_code == 200: - result = response.json() - else: - result = {} - return result diff --git a/socketdev/npm/__init__.py b/socketdev/npm/__init__.py index 11bf4b1..a54a6ba 100644 --- a/socketdev/npm/__init__.py +++ b/socketdev/npm/__init__.py @@ -1,20 +1,21 @@ -import socketdev + class NPM: - @staticmethod - def issues(package: str, version: str) -> list: + def __init__(self, api): + self.api = api + + def issues(self, package: str, version: str) -> list: path = f"npm/{package}/{version}/issues" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) issues = [] if response.status_code == 200: issues = response.json() return issues - @staticmethod - def score(package: str, version: str) -> list: + def score(self, package: str, version: str) -> list: path = f"npm/{package}/{version}/score" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) issues = [] if response.status_code == 200: issues = response.json() diff --git a/socketdev/openapi/__init__.py b/socketdev/openapi/__init__.py index 71c4f03..b3df1da 100644 --- a/socketdev/openapi/__init__.py +++ b/socketdev/openapi/__init__.py @@ -1,11 +1,13 @@ -import socketdev + class OpenAPI: - @staticmethod - def get() -> dict: + def __init__(self, api): + self.api = api + + def get(self) -> dict: path = "openapi" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: openapi = response.json() else: diff --git a/socketdev/org/__init__.py b/socketdev/org/__init__.py index 433ed94..12d906c 100644 --- a/socketdev/org/__init__.py +++ b/socketdev/org/__init__.py @@ -1,20 +1,24 @@ -import socketdev +from typing import TypedDict, Dict +class Organization(TypedDict): + id: str + name: str + image: str + plan: str + slug: str + +class OrganizationsResponse(TypedDict): + organizations: Dict[str, Organization] + # Add other fields from the response if needed class Orgs: - @staticmethod - def get() -> dict: - path = "organizations" - headers = None - payload = None + def __init__(self, api): + self.api = api - response = socketdev.do_request( - path=path, - headers=headers, - payload=payload - ) + def get(self) -> OrganizationsResponse: + path = "organizations" + response = self.api.do_request(path=path) if response.status_code == 200: - result = response.json() + return response.json() # Return the full response else: - result = {} - return result + return {"organizations": {}} # Return an empty structure \ No newline at end of file diff --git a/socketdev/purl/__init__.py b/socketdev/purl/__init__.py index 03166d3..6842e11 100644 --- a/socketdev/purl/__init__.py +++ b/socketdev/purl/__init__.py @@ -1,16 +1,16 @@ import json -import socketdev - class Purl: - @staticmethod - def post(license: str = "true", components: list = []) -> dict: + def __init__(self, api): + self.api = api + + def post(self, license: str = "true", components: list = []) -> dict: path = "purl?" + "license=" + license components = {"components": components} components = json.dumps(components) - response = socketdev.do_request(path=path, payload=components, method="POST") + response = self.api.do_request(path=path, payload=components, method="POST") if response.status_code == 200: purl = [] purl_dict = {} diff --git a/socketdev/quota/__init__.py b/socketdev/quota/__init__.py index aebadd9..1494888 100644 --- a/socketdev/quota/__init__.py +++ b/socketdev/quota/__init__.py @@ -1,11 +1,11 @@ -import socketdev - class Quota: - @staticmethod - def get() -> dict: + def __init__(self, api): + self.api = api + + def get(self) -> dict: path = "quota" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: quota = response.json() else: diff --git a/socketdev/report/__init__.py b/socketdev/report/__init__.py index 0c6376d..5483b2a 100644 --- a/socketdev/report/__init__.py +++ b/socketdev/report/__init__.py @@ -1,13 +1,15 @@ -import socketdev + from datetime import datetime, timedelta, timezone class Report: - @staticmethod - def list(from_time: int = None) -> dict: + def __init__(self, api): + self.api = api + + def list(self, from_time: int = None) -> dict: """ This function will return all reports from time specified. - :param from_time: Unix epoch time in seconds. Will default to 30 days + :param from_time: Unix epoch time in seconds. Will default self, to 30 days """ if from_time is None: from_time = int((datetime.now(timezone.utc) - timedelta(days=30)).timestamp()) @@ -17,60 +19,48 @@ def list(from_time: int = None) -> dict: path = "report/list" if from_time is not None: path += f"?from={from_time}" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: reports = response.json() else: reports = {} return reports - @staticmethod - def delete(report_id: str) -> bool: + def delete(self, report_id: str) -> bool: path = f"report/delete/{report_id}" - response = socketdev.do_request( - path=path, - method="DELETE" - ) + response = self.api.do_request(path=path, method="DELETE") if response.status_code == 200: deleted = True else: deleted = False return deleted - @staticmethod - def view(report_id) -> dict: + def view(self, report_id) -> dict: path = f"report/view/{report_id}" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: report = response.json() else: report = {} return report - @staticmethod - def supported() -> dict: + def supported(self) -> dict: path = "report/supported" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: report = response.json() else: report = {} return report - @staticmethod - def create(files: list) -> dict: + def create(self, files: list) -> dict: open_files = [] for name, path in files: - file_info = (name, (name, open(path, 'rb'), 'text/plain')) + file_info = (name, (name, open(path, "rb"), "text/plain")) open_files.append(file_info) path = "report/upload" payload = {} - response = socketdev.do_request( - path=path, - method="PUT", - files=open_files, - payload=payload - ) + response = self.api.do_request(path=path, method="PUT", files=open_files, payload=payload) if response.status_code == 200: reports = response.json() else: diff --git a/socketdev/repos/__init__.py b/socketdev/repos/__init__.py index f057d09..fb4d50a 100644 --- a/socketdev/repos/__init__.py +++ b/socketdev/repos/__init__.py @@ -1,15 +1,73 @@ -import socketdev +import logging +from typing import List, Optional +from dataclasses import dataclass, asdict +log = logging.getLogger("socketdev") + +@dataclass +class RepositoryInfo: + id: str + created_at: str # Could be datetime if we want to parse it + updated_at: str # Could be datetime if we want to parse it + head_full_scan_id: str + name: str + description: str + homepage: str + visibility: str + archived: bool + default_branch: str + slug: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "RepositoryInfo": + return cls( + id=data["id"], + created_at=data["created_at"], + updated_at=data["updated_at"], + head_full_scan_id=data["head_full_scan_id"], + name=data["name"], + description=data["description"], + homepage=data["homepage"], + visibility=data["visibility"], + archived=data["archived"], + default_branch=data["default_branch"], + slug=data.get("slug") + ) + +@dataclass +class GetRepoResponse: + success: bool + status: int + data: Optional[RepositoryInfo] = None + message: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "GetRepoResponse": + return cls( + success=data["success"], + status=data["status"], + message=data.get("message"), + data=RepositoryInfo.from_dict(data.get("data")) if data.get("data") else None + ) class Repos: - @staticmethod - def get(org_slug: str, **kwargs) -> dict[str,]: + def __init__(self, api): + self.api = api + + def get(self, org_slug: str, **kwargs) -> dict[str, List[RepositoryInfo]]: query_params = {} if kwargs: for key, val in kwargs.items(): query_params[key] = val if len(query_params) == 0: return {} + path = "orgs/" + org_slug + "/repos" if query_params is not None: path += "?" @@ -17,68 +75,87 @@ def get(org_slug: str, **kwargs) -> dict[str,]: value = query_params[param] path += f"{param}={value}&" path = path.rstrip("&") - response = socketdev.do_request(path=path) + + response = self.api.do_request(path=path) + if response.status_code == 200: - result = response.json() - else: - result = {} - return result + raw_result = response.json() + result = { + key: [RepositoryInfo.from_dict(repo) for repo in repos] + for key, repos in raw_result.items() + } + return result - @staticmethod - def repo(org_slug: str, repo_name: str) -> dict: + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error getting repositories: {response.status_code}, message: {error_message}") + return {} + + def repo(self, org_slug: str, repo_name: str) -> GetRepoResponse: path = f"orgs/{org_slug}/repos/{repo_name}" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) + if response.status_code == 200: result = response.json() - else: - result = {} - return result + return GetRepoResponse.from_dict({ + "success": True, + "status": 200, + "data": result + }) + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Failed to get repository: {response.status_code}, message: {error_message}") + return GetRepoResponse.from_dict({ + "success": False, + "status": response.status_code, + "message": error_message + }) - @staticmethod - def delete(org_slug: str, name: str) -> dict: + def delete(self, org_slug: str, name: str) -> dict: path = f"orgs/{org_slug}/repos/{name}" - response = socketdev.do_request(path=path, method="DELETE") + response = self.api.do_request(path=path, method="DELETE") + if response.status_code == 200: result = response.json() - else: - result = {} - return result + return result + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error deleting repository: {response.status_code}, message: {error_message}") + return {} - @staticmethod - def post(org_slug: str, **kwargs) -> dict: + def post(self, org_slug: str, **kwargs) -> dict: params = {} if kwargs: for key, val in kwargs.items(): params[key] = val if len(params) == 0: return {} + path = "orgs/" + org_slug + "/repos" - response = socketdev.do_request( - path=path, - method="POST", - payload=params.__dict__ - ) - result = {} + response = self.api.do_request(path=path, method="POST", payload=params.__dict__) + if response.status_code == 200: result = response.json() - return result + return result + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error creating repository: {response.status_code}, message: {error_message}") + return {} - @staticmethod - def update(org_slug: str, repo_name: str, **kwargs) -> dict: + def update(self, org_slug: str, repo_name: str, **kwargs) -> dict: params = {} if kwargs: for key, val in kwargs.keys(): params[key] = val if len(params) == 0: return {} + path = f"orgs/{org_slug}/repos/{repo_name}" - response = socketdev.do_request( - path=path, - method="POST", - payload=params.__dict__ - ) + response = self.api.do_request(path=path, method="POST", payload=params.__dict__) + if response.status_code == 200: result = response.json() - else: - result = {} - return result + return result + + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Error updating repository: {response.status_code}, message: {error_message}") + return {} diff --git a/socketdev/repositories/__init__.py b/socketdev/repositories/__init__.py index 91eab3b..f1eaaa6 100644 --- a/socketdev/repositories/__init__.py +++ b/socketdev/repositories/__init__.py @@ -1,4 +1,3 @@ -import socketdev from typing import TypedDict @@ -12,10 +11,12 @@ class Repo(TypedDict): class Repositories: - @staticmethod - def list() -> dict: + def __init__(self, api): + self.api = api + + def list(self) -> dict: path = "repos" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: repos = response.json() else: diff --git a/socketdev/sbom/__init__.py b/socketdev/sbom/__init__.py index 13f8e37..4752e54 100644 --- a/socketdev/sbom/__init__.py +++ b/socketdev/sbom/__init__.py @@ -1,13 +1,14 @@ -import socketdev import json from socketdev.core.classes import Package class Sbom: - @staticmethod - def view(report_id: str) -> dict[str, dict]: + def __init__(self, api): + self.api = api + + def view(self, report_id: str) -> dict[str, dict]: path = f"sbom/view/{report_id}" - response = socketdev.do_request(path=path) + response = self.api.do_request(path=path) if response.status_code == 200: sbom = [] sbom_dict = {} @@ -19,13 +20,12 @@ def view(report_id: str) -> dict[str, dict]: item = json.loads(line) sbom.append(item) for val in sbom: - sbom_dict[val['id']] = val + sbom_dict[val["id"]] = val else: sbom_dict = {} return sbom_dict - @staticmethod - def create_packages_dict(sbom: dict[str, dict]) -> dict[str, Package]: + def create_packages_dict(self, sbom: dict[str, dict]) -> dict[str, Package]: """ Converts the SBOM Artifacts from the FulLScan into a Dictionary for parsing :param sbom: list - Raw artifacts for the SBOM diff --git a/socketdev/settings/__init__.py b/socketdev/settings/__init__.py index 4cdcb10..733c7e9 100644 --- a/socketdev/settings/__init__.py +++ b/socketdev/settings/__init__.py @@ -1,18 +1,91 @@ -import json +import logging +from enum import Enum +from typing import Dict, Optional +from dataclasses import dataclass, asdict +log = logging.getLogger("socketdev") -import socketdev +class SecurityAction(str, Enum): + DEFER = 'defer' + ERROR = 'error' + WARN = 'warn' + MONITOR = 'monitor' + IGNORE = 'ignore' +@dataclass +class SecurityPolicyRule: + action: SecurityAction + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "SecurityPolicyRule": + return cls( + action=SecurityAction(data["action"]) + ) + +@dataclass +class OrgSecurityPolicyResponse: + success: bool + status: int + securityPolicyRules: Optional[Dict[str, SecurityPolicyRule]] = None + message: Optional[str] = None + + def __getitem__(self, key): return getattr(self, key) + def to_dict(self): return asdict(self) + + @classmethod + def from_dict(cls, data: dict) -> "OrgSecurityPolicyResponse": + return cls( + securityPolicyRules={ + k: SecurityPolicyRule.from_dict(v) + for k, v in data["securityPolicyRules"].items() + } if data.get("securityPolicyRules") else None, + success=data["success"], + status=data["status"], + message=data.get("message") + ) class Settings: - @staticmethod - def get(org_id: str) -> dict: - settings = {} - path = "settings" - payload = [{"organization": org_id}] - response = socketdev.do_request(path=path, method="POST", payload=json.dumps(payload)) - - if response.status_code != 200: - return settings - - settings = response.json() - return settings + def __init__(self, api): + self.api = api + + def create_params_string(self, params: dict) -> str: + param_str = "" + + for name, value in params.items(): + if value: + if name == "committers" and isinstance(value, list): + # Handle committers specially - add multiple params + for committer in value: + param_str += f"&{name}={committer}" + else: + param_str += f"&{name}={value}" + + param_str = "?" + param_str.lstrip("&") + + return param_str + + def get(self, org_slug: str, custom_rules_only: bool = False) -> OrgSecurityPolicyResponse: + path = f"orgs/{org_slug}/settings/security-policy" + params = {"custom_rules_only": custom_rules_only} + params_args = self.create_params_string(params) if custom_rules_only else "" + path += params_args + response = self.api.do_request(path=path, method="GET") + + if response.status_code == 200: + rules = response.json() + return OrgSecurityPolicyResponse.from_dict({ + "securityPolicyRules": rules.get("securityPolicyRules", {}), + "success": True, + "status": 200 + }) + else: + error_message = response.json().get("error", {}).get("message", "Unknown error") + log.error(f"Failed to get security policy: {response.status_code}, message: {error_message}") + return OrgSecurityPolicyResponse.from_dict({ + "securityPolicyRules": {}, + "success": False, + "status": response.status_code, + "message": error_message + }) diff --git a/socketdev/utils/__init__.py b/socketdev/utils/__init__.py new file mode 100644 index 0000000..dd90b16 --- /dev/null +++ b/socketdev/utils/__init__.py @@ -0,0 +1,12 @@ +from typing import Literal + +IntegrationType = Literal["api", "github", "gitlab", "bitbucket", "azure"] +INTEGRATION_TYPES = ("api", "github", "gitlab", "bitbucket", "azure") + + +class Utils: + @staticmethod + def validate_integration_type(integration_type: str) -> IntegrationType: + if integration_type not in INTEGRATION_TYPES: + raise ValueError(f"Invalid integration type: {integration_type}") + return integration_type # type: ignore diff --git a/socketdev/version.py b/socketdev/version.py new file mode 100644 index 0000000..719ffe9 --- /dev/null +++ b/socketdev/version.py @@ -0,0 +1 @@ +__version__ = "2.0.0" \ No newline at end of file