diff --git a/.gitignore b/.gitignore index 9f566db..77da6d1 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ dist *.egg-info *.cpython-312.pyc example-socket-export.py -__pycache__/ \ No newline at end of file +__pycache__/ +.coverage +.coverage.* +htmlcov/ diff --git a/README.rst b/README.rst index 2ed1265..61b64ce 100644 --- a/README.rst +++ b/README.rst @@ -28,9 +28,11 @@ Supported Functions ------------------- -purl.post(license, components) -"""""""""""""""""""""""""""""" -Retrieve the package information for a purl post +purl.post(license, components, org_slug=None) +""""""""""""""""""""""""""""""""""""""""""""" +Retrieve package information for one or more PURLs. Pass ``org_slug`` to use the +current org-scoped endpoint. Omitting ``org_slug`` keeps the legacy deprecated +endpoint for backwards compatibility. **Usage:** @@ -38,6 +40,7 @@ Retrieve the package information for a purl post from socketdev import socketdev socket = socketdev(token="REPLACE_ME") + org_slug = "your-org-slug" license = "true" components = [ { @@ -47,12 +50,13 @@ Retrieve the package information for a purl post "purl": "pkg:pypi/socketsecurity" } ] - print(socket.purl.post(license, components)) + print(socket.purl.post(license, components, org_slug=org_slug)) **PARAMETERS:** - **license (str)** - The license parameter if enabled will show alerts and license information. If disabled will only show the basic package metadata and scores. Default is true - **components (array{dict})** - The components list of packages urls +- **org_slug (str, optional)** - Organization slug for the supported org-scoped PURL endpoint. If omitted, the SDK uses the deprecated legacy endpoint for backwards compatibility. export.cdx_bom(org_slug, id, query_params) """""""""""""""""""""""""""""""""""""""""" diff --git a/pyproject.toml b/pyproject.toml index 262ca14..5bbc081 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "socketdev" -version = "3.0.32" +version = "3.1.0" requires-python = ">= 3.9" dependencies = [ 'requests', @@ -50,7 +50,7 @@ test = [ ] [project.urls] -Homepage = "https://github.com/socketdev/socketdev" +Homepage = "https://github.com/SocketDev/socket-sdk-python" [tool.ruff] # Exclude a variety of commonly ignored directories. diff --git a/socketdev/purl/__init__.py b/socketdev/purl/__init__.py index 555f3a5..50118ed 100644 --- a/socketdev/purl/__init__.py +++ b/socketdev/purl/__init__.py @@ -1,5 +1,6 @@ import json import urllib.parse +import warnings from socketdev.log import log from ..core.dedupe import Dedupe @@ -8,8 +9,21 @@ class Purl: def __init__(self, api): self.api = api - def post(self, license: str = "false", components: list = None, **kwargs) -> list: - path = "purl?" + def post( + self, + license: str = "false", + components: list = None, + org_slug: str = None, + **kwargs, + ) -> list: + if org_slug is None: + warnings.warn( + "Calling purl.post() without org_slug uses the deprecated POST /v0/purl endpoint. " + "Pass org_slug to migrate to POST /v0/orgs/{org_slug}/purl.", + DeprecationWarning, + stacklevel=2, + ) + path = f"orgs/{org_slug}/purl?" if org_slug else "purl?" if components is None: components = [] purls = {"components": components} diff --git a/socketdev/version.py b/socketdev/version.py index b70a418..f5f41e5 100644 --- a/socketdev/version.py +++ b/socketdev/version.py @@ -1 +1 @@ -__version__ = "3.0.32" +__version__ = "3.1.0" diff --git a/tests/integration/test_all_endpoints.py b/tests/integration/test_all_endpoints.py index 17d934f..e36f074 100644 --- a/tests/integration/test_all_endpoints.py +++ b/tests/integration/test_all_endpoints.py @@ -181,13 +181,13 @@ def test_historical_trend_mocked(self): def test_npm_issues_mocked(self): """Test npm issues endpoint.""" self._mock_success_response([{"type": "security", "severity": "high"}]) - result = self.sdk.npm.issues("lodash", "4.17.21") + result = self.sdk.npm.issues("lodash", "4.18.1") self.assertIsInstance(result, list) def test_npm_score_mocked(self): """Test npm score endpoint.""" self._mock_success_response([{"category": "security", "value": 85}]) - result = self.sdk.npm.score("lodash", "4.17.21") + result = self.sdk.npm.score("lodash", "4.18.1") self.assertIsInstance(result, list) # OpenAPI endpoints @@ -206,9 +206,13 @@ def test_org_get_mocked(self): # PURL endpoints def test_purl_post_mocked(self): - """Test purl post endpoint.""" - self._mock_success_response([{"purl": "pkg:npm/lodash@4.17.21", "valid": True}]) - result = self.sdk.purl.post("false", [{"purl": "pkg:npm/lodash@4.17.21"}]) + """Test org-scoped purl post endpoint.""" + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {'content-type': 'application/x-ndjson'} + mock_response.text = '{"inputPurl": "pkg:npm/lodash@4.18.1", "purl": "pkg:npm/lodash@4.18.1", "type": "npm", "name": "lodash", "version": "4.18.1", "valid": true, "alerts": []}' + self.mock_requests.request.return_value = mock_response + result = self.sdk.purl.post("false", [{"purl": "pkg:npm/lodash@4.18.1"}], org_slug="test-org") self.assertIsInstance(result, list) # Quota endpoints @@ -372,7 +376,7 @@ def setUpClass(cls): test_package = { "name": "test-integration-package", "version": "1.0.0", - "dependencies": {"lodash": "4.17.21"} + "dependencies": {"lodash": "4.18.1"} } with open(cls.package_json_path, 'w') as f: json.dump(test_package, f, indent=2) @@ -414,20 +418,20 @@ def test_openapi_get_integration(self): # NPM endpoints (should work for public packages) def test_npm_issues_integration(self): """Test npm issues endpoint.""" - result = self._try_endpoint(self.sdk.npm.issues, "lodash", "4.17.21") + result = self._try_endpoint(self.sdk.npm.issues, "lodash", "4.18.1") if result: self.assertIsInstance(result, list) def test_npm_score_integration(self): """Test npm score endpoint.""" - result = self._try_endpoint(self.sdk.npm.score, "lodash", "4.17.21") + result = self._try_endpoint(self.sdk.npm.score, "lodash", "4.18.1") if result: self.assertIsInstance(result, list) # PURL endpoints def test_purl_post_integration(self): """Test purl post endpoint.""" - components = [{"purl": "pkg:npm/lodash@4.17.21"}] + components = [{"purl": "pkg:npm/lodash@4.18.1"}] result = self._try_endpoint(self.sdk.purl.post, "false", components) if result: self.assertIsInstance(result, list) @@ -515,7 +519,7 @@ def test_dependencies_get_integration(self): """Test dependencies get endpoint.""" result = self._try_endpoint( self.sdk.dependencies.get, - self.org_slug, "npm", "lodash", "4.17.21" + self.org_slug, "npm", "lodash", "4.18.1" ) if result: self.assertIsInstance(result, dict) diff --git a/tests/integration/test_comprehensive_integration.py b/tests/integration/test_comprehensive_integration.py index 137723b..4259b6b 100644 --- a/tests/integration/test_comprehensive_integration.py +++ b/tests/integration/test_comprehensive_integration.py @@ -53,7 +53,7 @@ def setUpClass(cls): "name": "test-integration-project", "version": "1.0.0", "dependencies": { - "lodash": "4.17.21" + "lodash": "4.18.1" } } @@ -264,7 +264,7 @@ def test_npm_endpoints(self): """Test NPM-related endpoints.""" # Test getting package issues - this should work for most packages try: - issues = self.sdk.npm.issues("lodash", "4.17.21") + issues = self.sdk.npm.issues("lodash", "4.18.1") self.assertIsInstance(issues, dict) except Exception as e: print(f"NPM issues endpoint not available: {e}") @@ -281,9 +281,13 @@ def test_purl_endpoint(self): """Test PURL (Package URL) functionality.""" try: # Test with a common npm package - purl = "pkg:npm/lodash@4.17.21" - result = self.sdk.purl.post([purl]) - self.assertIsInstance(result, dict) + purl = "pkg:npm/lodash@4.18.1" + result = self.sdk.purl.post( + license="false", + components=[{"purl": purl}], + org_slug=self.org_slug, + ) + self.assertIsInstance(result, list) except Exception as e: print(f"PURL endpoint not available: {e}") diff --git a/tests/unit/test_all_endpoints_unit.py b/tests/unit/test_all_endpoints_unit.py index 40f90bc..64895ee 100644 --- a/tests/unit/test_all_endpoints_unit.py +++ b/tests/unit/test_all_endpoints_unit.py @@ -44,11 +44,11 @@ def _mock_response(self, data=None, status_code=200): # Dependencies endpoints def test_dependencies_post_unit(self): """Test dependencies post with proper file handling.""" - expected_data = {"packages": [{"name": "lodash", "version": "4.17.21"}]} + expected_data = {"packages": [{"name": "lodash", "version": "4.18.1"}]} self._mock_response(expected_data) with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f: - json.dump({"name": "test-package", "dependencies": {"lodash": "4.17.21"}}, f) + json.dump({"name": "test-package", "dependencies": {"lodash": "4.18.1"}}, f) f.flush() try: @@ -72,12 +72,12 @@ def test_dependencies_get_unit(self): expected_data = {"dependencies": [{"name": "sub-dependency", "version": "1.0.0"}]} self._mock_response(expected_data) - result = self.sdk.dependencies.get("test-org", "npm", "lodash", "4.17.21") + result = self.sdk.dependencies.get("test-org", "npm", "lodash", "4.18.1") self.assertEqual(result, expected_data) call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "GET") - self.assertIn("/orgs/test-org/dependencies/npm/lodash/4.17.21", call_args[0][1]) + self.assertIn("/orgs/test-org/dependencies/npm/lodash/4.18.1", call_args[0][1]) # DiffScans endpoints def test_diffscans_list_unit(self): @@ -305,24 +305,24 @@ def test_npm_issues_unit(self): expected_data = [{"type": "security", "severity": "high", "title": "Test issue"}] self._mock_response(expected_data) - result = self.sdk.npm.issues("lodash", "4.17.21") + result = self.sdk.npm.issues("lodash", "4.18.1") self.assertEqual(result, expected_data) call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "GET") - self.assertIn("/npm/lodash/4.17.21/issues", call_args[0][1]) + self.assertIn("/npm/lodash/4.18.1/issues", call_args[0][1]) def test_npm_score_unit(self): """Test npm score endpoint.""" expected_data = [{"category": "security", "value": 85}] self._mock_response(expected_data) - result = self.sdk.npm.score("lodash", "4.17.21") + result = self.sdk.npm.score("lodash", "4.18.1") self.assertEqual(result, expected_data) call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "GET") - self.assertIn("/npm/lodash/4.17.21/score", call_args[0][1]) + self.assertIn("/npm/lodash/4.18.1/score", call_args[0][1]) # OpenAPI endpoints def test_openapi_get_unit(self): @@ -352,14 +352,14 @@ def test_org_get_unit(self): # PURL endpoints def test_purl_post_unit(self): - """Test PURL validation endpoint.""" + """Test org-scoped PURL validation endpoint.""" # Expected final result after deduplication - should match what the dedupe function produces expected_data = [{ - "inputPurl": "pkg:npm/lodash@4.17.21", - "purl": "pkg:npm/lodash@4.17.21", + "inputPurl": "pkg:npm/lodash@4.18.1", + "purl": "pkg:npm/lodash@4.18.1", "type": "npm", "name": "lodash", - "version": "4.17.21", + "version": "4.18.1", "valid": True, "alerts": [], "releases": ["npm"] @@ -367,7 +367,7 @@ def test_purl_post_unit(self): # Mock the NDJSON response that would come from the actual API # This simulates what the API returns: newline-delimited JSON with SocketArtifact objects - mock_ndjson_response = '{"inputPurl": "pkg:npm/lodash@4.17.21", "purl": "pkg:npm/lodash@4.17.21", "type": "npm", "name": "lodash", "version": "4.17.21", "valid": true, "alerts": []}' + mock_ndjson_response = '{"inputPurl": "pkg:npm/lodash@4.18.1", "purl": "pkg:npm/lodash@4.18.1", "type": "npm", "name": "lodash", "version": "4.18.1", "valid": true, "alerts": []}' # Mock the response with NDJSON format mock_response = Mock() @@ -376,13 +376,30 @@ def test_purl_post_unit(self): mock_response.text = mock_ndjson_response self.mock_requests.request.return_value = mock_response - components = [{"purl": "pkg:npm/lodash@4.17.21"}] - result = self.sdk.purl.post("false", components) + components = [{"purl": "pkg:npm/lodash@4.18.1"}] + result = self.sdk.purl.post("false", components, org_slug="test-org") self.assertEqual(result, expected_data) + call_args = self.mock_requests.request.call_args + self.assertEqual(call_args[0][0], "POST") + self.assertIn("/orgs/test-org/purl", call_args[0][1]) + + def test_purl_post_unit_legacy_path(self): + """Test legacy PURL validation endpoint remains available for compatibility.""" + mock_ndjson_response = '{"inputPurl": "pkg:npm/lodash@4.18.1", "purl": "pkg:npm/lodash@4.18.1", "type": "npm", "name": "lodash", "version": "4.18.1", "valid": true, "alerts": []}' + + mock_response = Mock() + mock_response.status_code = 200 + mock_response.headers = {'content-type': 'application/x-ndjson'} + mock_response.text = mock_ndjson_response + self.mock_requests.request.return_value = mock_response + + self.sdk.purl.post("false", [{"purl": "pkg:npm/lodash@4.18.1"}]) + call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "POST") self.assertIn("/purl", call_args[0][1]) + self.assertNotIn("/orgs/", call_args[0][1]) # Quota endpoints def test_quota_get_unit(self): diff --git a/tests/unit/test_socket_sdk_unit.py b/tests/unit/test_socket_sdk_unit.py index b64aaec..973c046 100644 --- a/tests/unit/test_socket_sdk_unit.py +++ b/tests/unit/test_socket_sdk_unit.py @@ -110,13 +110,13 @@ def test_socket_purl_creation(self): type=SocketPURL_Type.NPM, name="lodash", namespace=None, - release="4.17.21" + release="4.18.1" ) self.assertEqual(purl.type, SocketPURL_Type.NPM) self.assertEqual(purl.name, "lodash") self.assertIsNone(purl.namespace) - self.assertEqual(purl.release, "4.17.21") + self.assertEqual(purl.release, "4.18.1") def test_integration_types(self): """Test that all integration types are available.""" @@ -223,7 +223,7 @@ def setUp(self): "name": "test-package", "version": "1.0.0", "dependencies": { - "lodash": "4.17.21" + "lodash": "4.18.1" } } diff --git a/tests/unit/test_working_endpoints_unit.py b/tests/unit/test_working_endpoints_unit.py index 33a69bb..3f8112b 100644 --- a/tests/unit/test_working_endpoints_unit.py +++ b/tests/unit/test_working_endpoints_unit.py @@ -50,24 +50,24 @@ def test_npm_issues_unit(self): expected_data = [{"type": "security", "severity": "high"}] self._mock_response(expected_data) - result = self.sdk.npm.issues("lodash", "4.17.21") + result = self.sdk.npm.issues("lodash", "4.18.1") self.assertEqual(result, expected_data) call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "GET") - self.assertIn("/npm/lodash/4.17.21/issues", call_args[0][1]) + self.assertIn("/npm/lodash/4.18.1/issues", call_args[0][1]) def test_npm_score_unit(self): """Test NPM score endpoint - WORKING.""" expected_data = [{"category": "security", "value": 85}] self._mock_response(expected_data) - result = self.sdk.npm.score("lodash", "4.17.21") + result = self.sdk.npm.score("lodash", "4.18.1") self.assertEqual(result, expected_data) call_args = self.mock_requests.request.call_args self.assertEqual(call_args[0][0], "GET") - self.assertIn("/npm/lodash/4.17.21/score", call_args[0][1]) + self.assertIn("/npm/lodash/4.18.1/score", call_args[0][1]) def test_openapi_get_unit(self): """Test OpenAPI specification retrieval - WORKING."""