Skip to content

Commit fc24237

Browse files
authored
Return headers on request exception (#23)
* added headers, cleaned up exceptions, and added new endpoints
1 parent 12e1bd5 commit fc24237

File tree

18 files changed

+704
-360
lines changed

18 files changed

+704
-360
lines changed

Diff for: socketdev/__init__.py

+11-6
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from socketdev.dependencies import Dependencies
55
from socketdev.export import Export
66
from socketdev.fullscans import FullScans
7+
from socketdev.historical import Historical
78
from socketdev.npm import NPM
89
from socketdev.openapi import OpenAPI
910
from socketdev.org import Orgs
@@ -14,6 +15,7 @@
1415
from socketdev.repositories import Repositories
1516
from socketdev.sbom import Sbom
1617
from socketdev.settings import Settings
18+
from socketdev.triage import Triage
1719
from socketdev.utils import Utils, IntegrationType, INTEGRATION_TYPES
1820
from socketdev.version import __version__
1921

@@ -23,7 +25,6 @@
2325
__all__ = ["socketdev", "Utils", "IntegrationType", "INTEGRATION_TYPES"]
2426

2527

26-
2728
global encoded_key
2829
encoded_key: str
2930

@@ -32,6 +33,8 @@
3233
log = logging.getLogger("socketdev")
3334
log.addHandler(logging.NullHandler())
3435

36+
# TODO: Add debug flag to constructor to enable verbose error logging for API response parsing.
37+
3538

3639
class socketdev:
3740
def __init__(self, token: str, timeout: int = 1200):
@@ -41,18 +44,20 @@ def __init__(self, token: str, timeout: int = 1200):
4144
self.api.set_timeout(timeout)
4245

4346
self.dependencies = Dependencies(self.api)
47+
self.export = Export(self.api)
48+
self.fullscans = FullScans(self.api)
49+
self.historical = Historical(self.api)
4450
self.npm = NPM(self.api)
4551
self.openapi = OpenAPI(self.api)
4652
self.org = Orgs(self.api)
53+
self.purl = Purl(self.api)
4754
self.quota = Quota(self.api)
4855
self.report = Report(self.api)
49-
self.sbom = Sbom(self.api)
50-
self.purl = Purl(self.api)
51-
self.fullscans = FullScans(self.api)
52-
self.export = Export(self.api)
53-
self.repositories = Repositories(self.api)
5456
self.repos = Repos(self.api)
57+
self.repositories = Repositories(self.api)
58+
self.sbom = Sbom(self.api)
5559
self.settings = Settings(self.api)
60+
self.triage = Triage(self.api)
5661
self.utils = Utils()
5762

5863
@staticmethod

Diff for: socketdev/core/api.py

+52-23
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,16 @@
22
import requests
33
from socketdev.core.classes import Response
44
from socketdev.exceptions import (
5-
APIKeyMissing, APIFailure, APIAccessDenied, APIInsufficientQuota,
6-
APIResourceNotFound, APITimeout, APIConnectionError, APIBadGateway,
7-
APIInsufficientPermissions, APIOrganizationNotAllowed
5+
APIKeyMissing,
6+
APIFailure,
7+
APIAccessDenied,
8+
APIInsufficientQuota,
9+
APIResourceNotFound,
10+
APITimeout,
11+
APIConnectionError,
12+
APIBadGateway,
13+
APIInsufficientPermissions,
14+
APIOrganizationNotAllowed,
815
)
916
from socketdev.version import __version__
1017
from requests.exceptions import Timeout, ConnectionError
@@ -24,7 +31,12 @@ def set_timeout(self, timeout: int):
2431
self.request_timeout = timeout
2532

2633
def do_request(
27-
self, path: str, headers: dict | None = None, payload: [dict, str] = None, files: list = None, method: str = "GET"
34+
self,
35+
path: str,
36+
headers: dict | None = None,
37+
payload: [dict, str] = None,
38+
files: list = None,
39+
method: str = "GET",
2840
) -> Response:
2941
if self.encoded_key is None or self.encoded_key == "":
3042
raise APIKeyMissing
@@ -36,33 +48,39 @@ def do_request(
3648
"accept": "application/json",
3749
}
3850
url = f"{self.api_url}/{path}"
51+
52+
def format_headers(headers_dict):
53+
return "\n".join(f"{k}: {v}" for k, v in headers_dict.items())
54+
3955
try:
4056
start_time = time.time()
4157
response = requests.request(
4258
method.upper(), url, headers=headers, data=payload, files=files, timeout=self.request_timeout
4359
)
4460
request_duration = time.time() - start_time
45-
61+
62+
headers_str = f"\n\nHeaders:\n{format_headers(response.headers)}" if response.headers else ""
63+
path_str = f"\nPath: {url}"
64+
4665
if response.status_code == 401:
47-
raise APIAccessDenied("Unauthorized")
66+
raise APIAccessDenied(f"Unauthorized{path_str}{headers_str}")
4867
if response.status_code == 403:
4968
try:
50-
error_message = response.json().get('error', {}).get('message', '')
69+
error_message = response.json().get("error", {}).get("message", "")
5170
if "Insufficient permissions for API method" in error_message:
52-
raise APIInsufficientPermissions(error_message)
71+
raise APIInsufficientPermissions(f"{error_message}{path_str}{headers_str}")
5372
elif "Organization not allowed" in error_message:
54-
raise APIOrganizationNotAllowed(error_message)
73+
raise APIOrganizationNotAllowed(f"{error_message}{path_str}{headers_str}")
5574
elif "Insufficient max quota" in error_message:
56-
raise APIInsufficientQuota(error_message)
75+
raise APIInsufficientQuota(f"{error_message}{path_str}{headers_str}")
5776
else:
58-
raise APIAccessDenied(error_message or "Access denied")
77+
raise APIAccessDenied(f"{error_message or 'Access denied'}{path_str}{headers_str}")
5978
except ValueError:
60-
# If JSON parsing fails
61-
raise APIAccessDenied("Access denied")
79+
raise APIAccessDenied(f"Access denied{path_str}{headers_str}")
6280
if response.status_code == 404:
63-
raise APIResourceNotFound(f"Path not found {path}")
81+
raise APIResourceNotFound(f"Path not found {path}{path_str}{headers_str}")
6482
if response.status_code == 429:
65-
retry_after = response.headers.get('retry-after')
83+
retry_after = response.headers.get("retry-after")
6684
if retry_after:
6785
try:
6886
seconds = int(retry_after)
@@ -73,23 +91,34 @@ def do_request(
7391
time_msg = f" Retry after: {retry_after}"
7492
else:
7593
time_msg = ""
76-
raise APIInsufficientQuota(f"Insufficient quota for API route.{time_msg}")
94+
raise APIInsufficientQuota(f"Insufficient quota for API route.{time_msg}{path_str}{headers_str}")
7795
if response.status_code == 502:
78-
raise APIBadGateway("Upstream server error")
96+
raise APIBadGateway(f"Upstream server error{path_str}{headers_str}")
7997
if response.status_code >= 400:
80-
raise APIFailure(f"Bad Request: HTTP {response.status_code}")
81-
98+
raise APIFailure(
99+
f"Bad Request: HTTP original_status_code:{response.status_code}{path_str}{headers_str}",
100+
status_code=500,
101+
)
102+
82103
return response
83-
104+
84105
except Timeout:
85106
request_duration = time.time() - start_time
86107
raise APITimeout(f"Request timed out after {request_duration:.2f} seconds")
87108
except ConnectionError as error:
88109
request_duration = time.time() - start_time
89110
raise APIConnectionError(f"Connection error after {request_duration:.2f} seconds: {error}")
90-
except (APIAccessDenied, APIInsufficientQuota, APIResourceNotFound, APIFailure,
91-
APITimeout, APIConnectionError, APIBadGateway, APIInsufficientPermissions,
92-
APIOrganizationNotAllowed):
111+
except (
112+
APIAccessDenied,
113+
APIInsufficientQuota,
114+
APIResourceNotFound,
115+
APIFailure,
116+
APITimeout,
117+
APIConnectionError,
118+
APIBadGateway,
119+
APIInsufficientPermissions,
120+
APIOrganizationNotAllowed,
121+
):
93122
# Let all our custom exceptions propagate up unchanged
94123
raise
95124
except Exception as error:

Diff for: socketdev/dependencies/__init__.py

+9-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
import json
22
from urllib.parse import urlencode
3-
3+
import logging
44
from socketdev.tools import load_files
55

6+
log = logging.getLogger("socketdev")
7+
8+
# TODO: Add types for responses. Not currently used in the CLI.
9+
610

711
class Dependencies:
812
def __init__(self, api):
@@ -17,8 +21,8 @@ def post(self, files: list, params: dict) -> dict:
1721
result = response.json()
1822
else:
1923
result = {}
20-
print(f"Error posting {files} to the Dependency API")
21-
print(response.text)
24+
log.error(f"Error posting {files} to the Dependency API")
25+
log.error(response.text)
2226
return result
2327

2428
def get(
@@ -34,6 +38,6 @@ def get(
3438
result = response.json()
3539
else:
3640
result = {}
37-
print("Unable to retrieve Dependencies")
38-
print(response.text)
41+
log.error("Unable to retrieve Dependencies")
42+
log.error(response.text)
3943
return result

Diff for: socketdev/export/__init__.py

+30-17
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
from urllib.parse import urlencode
22
from dataclasses import dataclass, asdict
33
from typing import Optional
4+
import logging
5+
6+
log = logging.getLogger("socketdev")
47

58

69
@dataclass
@@ -23,40 +26,50 @@ class Export:
2326
def __init__(self, api):
2427
self.api = api
2528

26-
def cdx_bom(self, org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None) -> dict:
29+
def cdx_bom(
30+
self, org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None, use_types: bool = False
31+
) -> dict:
2732
"""
2833
Export a Socket SBOM as a CycloneDX SBOM
2934
:param org_slug: String - The slug of the organization
3035
:param id: String - The id of either a full scan or an sbom report
3136
:param query_params: Optional[ExportQueryParams] - Query parameters for filtering
32-
:return:
37+
:param use_types: Optional[bool] - Whether to return typed responses
38+
:return: dict
3339
"""
3440
path = f"orgs/{org_slug}/export/cdx/{id}"
3541
if query_params:
3642
path += query_params.to_query_params()
3743
response = self.api.do_request(path=path)
38-
try:
39-
sbom = response.json()
40-
sbom["success"] = True
41-
except Exception as error:
42-
sbom = {"success": False, "message": str(error)}
43-
return sbom
44-
45-
def spdx_bom(self, org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None) -> dict:
44+
45+
if response.status_code == 200:
46+
return response.json()
47+
# TODO: Add typed response when types are defined
48+
49+
log.error(f"Error exporting CDX BOM: {response.status_code}")
50+
print(response.text)
51+
return {}
52+
53+
def spdx_bom(
54+
self, org_slug: str, id: str, query_params: Optional[ExportQueryParams] = None, use_types: bool = False
55+
) -> dict:
4656
"""
4757
Export a Socket SBOM as an SPDX SBOM
4858
:param org_slug: String - The slug of the organization
4959
:param id: String - The id of either a full scan or an sbom report
5060
:param query_params: Optional[ExportQueryParams] - Query parameters for filtering
51-
:return:
61+
:param use_types: Optional[bool] - Whether to return typed responses
62+
:return: dict
5263
"""
5364
path = f"orgs/{org_slug}/export/spdx/{id}"
5465
if query_params:
5566
path += query_params.to_query_params()
5667
response = self.api.do_request(path=path)
57-
try:
58-
sbom = response.json()
59-
sbom["success"] = True
60-
except Exception as error:
61-
sbom = {"success": False, "message": str(error)}
62-
return sbom
68+
69+
if response.status_code == 200:
70+
return response.json()
71+
# TODO: Add typed response when types are defined
72+
73+
log.error(f"Error exporting SPDX BOM: {response.status_code}")
74+
print(response.text)
75+
return {}

0 commit comments

Comments
 (0)