diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a6349a..9d8c30c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -14,7 +14,7 @@ default_language_version: repos: - repo: https://github.com/ibm/detect-secrets - rev: 0.13.1+ibm.62.dss + rev: 0.13.1+ibm.64.dss hooks: - id: detect-secrets # pragma: whitelist secret args: diff --git a/.secrets.baseline b/.secrets.baseline index fcd0c8f..c10c074 100644 --- a/.secrets.baseline +++ b/.secrets.baseline @@ -3,7 +3,7 @@ "files": "^.secrets.baseline$", "lines": null }, - "generated_at": "2025-06-12T10:16:52Z", + "generated_at": "2026-07-28T11:52:51Z", "plugins_used": [ { "name": "AWSKeyDetector" @@ -77,34 +77,6 @@ } ], "results": { - ".ci/bundle.sh": [ - { - "hashed_secret": "3a272afad27cd2a102600d836c3684fb32a4303b", - "is_secret": false, - "is_verified": false, - "line_number": 56, - "type": "Secret Keyword", - "verified_result": null - } - ], - "docs/kubernetes-env.md": [ - { - "hashed_secret": "9ecc8459ea5f39f9da55cb4d71a70b5d1e0f0b80", - "is_secret": false, - "is_verified": false, - "line_number": 75, - "type": "Secret Keyword", - "verified_result": null - }, - { - "hashed_secret": "374e85f02d474ce2135f17236522c53178923c28", - "is_secret": false, - "is_verified": false, - "line_number": 79, - "type": "Secret Keyword", - "verified_result": null - } - ], "fireblocks-plugin/tests/data/messages_request.json": [ { "hashed_secret": "a4f5b1f3796b07b1b6436bb225cc3d49267ff1c9", @@ -132,95 +104,9 @@ "type": "Hex High Entropy String", "verified_result": null } - ], - "fireblocks-utilities/verifyecdsasignature.py": [ - { - "hashed_secret": "4c01896b029cafed006b5f7c4fcc63e56b2aea90", - "is_secret": false, - "is_verified": false, - "line_number": 19, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "2bd1ca992255001a8711229cb08a7a413c02205e", - "is_secret": false, - "is_verified": false, - "line_number": 21, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "7ca923e8de42415cecc8e46a8fff0695a74092bf", - "is_secret": false, - "is_verified": false, - "line_number": 24, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "4a9c6c77d9389f297b4feb223ff7a1d023770dc0", - "is_secret": false, - "is_verified": false, - "line_number": 25, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "9011c60951228a2558ed1552746ef65e1164b017", - "is_secret": false, - "is_verified": false, - "line_number": 26, - "type": "Hex High Entropy String", - "verified_result": null - } - ], - "gateway-mock/Containerfile": [ - { - "hashed_secret": "b991a3507ae53155dfb37a8c817f74f847d66043", - "is_secret": false, - "is_verified": false, - "line_number": 53, - "type": "Secret Keyword", - "verified_result": null - } - ], - "gateway-mock/src/gateway-mock/fbserverapimock.py": [ - { - "hashed_secret": "f498fdf2b39bdaa43aeaea4d4376d94ae5766e41", - "is_secret": false, - "is_verified": false, - "line_number": 48, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "a4f5b1f3796b07b1b6436bb225cc3d49267ff1c9", - "is_secret": false, - "is_verified": false, - "line_number": 51, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "df16869fed241f9af136290f24694d7534101be2", - "is_secret": false, - "is_verified": false, - "line_number": 55, - "type": "Hex High Entropy String", - "verified_result": null - }, - { - "hashed_secret": "89395f31963680bc4a5acf68a2dd7d3d82315429", - "is_secret": false, - "is_verified": false, - "line_number": 59, - "type": "Hex High Entropy String", - "verified_result": null - } ] }, - "version": "0.13.1+ibm.62.dss", + "version": "0.13.1+ibm.64.dss", "word_list": { "file": null, "hash": null diff --git a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py index 834f59f..286c2e6 100644 --- a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py +++ b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/api/v1alpha1.py @@ -110,8 +110,23 @@ def get(self): @api.route("/status", methods=["GET"]) class Status(Resource): + error_model = api.model( + "Error", + { + "code": fields.String(description="Error code"), + "message": fields.String(description="Error message"), + }, + ) + component_status_model = api.model( - "ComponentStatus", {"status": fields.String(), "error": fields.String()} + "ComponentStatus", + { + "status_code": fields.Integer(description="HTTP status code"), + "status": fields.String(description="Human readable message"), + "errors": fields.List( + fields.Nested(error_model), default=[], description="List of errors" + ), + }, ) @api.response(code=200, description="Success", model=component_status_model) @@ -120,7 +135,11 @@ def get(self): try: current_app.bpm.backend_status() except Exception as e: - logger.exception(e) - abort(503) - - return {"status": "OK"}, 200 + logger.exception("Backend status check failed") + return { + "status_code": 503, + "status": "Unavailable", + "errors": [{"code": "BACKEND_ERROR", "message": str(e)}], + }, 503 + + return {"status_code": 200, "status": "OK", "errors": []}, 200 diff --git a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py index 3b2eea2..51a8379 100644 --- a/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py +++ b/ripple-plugin/src/oso_ripple_plugins/backend_plugin/backend_plugin_manager.py @@ -26,20 +26,23 @@ import urllib3 from urllib3.exceptions import InsecureRequestWarning -from oso_ripple_plugins.common import crypt, errors +from oso_ripple_plugins.common import crypt urllib3.disable_warnings(InsecureRequestWarning) class BackendPluginManager: def __init__(self): - self.cold_bridge_endpoint = os.environ.get("COLD_BRIDGE_ENDPOINT", - "http://localhost:8080") + self.cold_bridge_endpoint = os.environ.get( + "COLD_BRIDGE_ENDPOINT", "http://localhost:8080" + ) self.seed = os.environ.get("SEED", "") logging.basicConfig(stream=sys.stdout, level=logging.INFO) self.logger = logging.getLogger(__name__) - self.logger.info(f"Cold-bridge endpoint configured as: {self.cold_bridge_endpoint}") + self.logger.info( + f"Cold-bridge endpoint configured as: {self.cold_bridge_endpoint}" + ) def backend_status(self): response = requests.get( @@ -49,7 +52,9 @@ def backend_status(self): response.raise_for_status() def bulk_download(self) -> List[Dict]: - response = requests.get(f"{self.cold_bridge_endpoint}/v1/feed/download?clean=True") + response = requests.get( + f"{self.cold_bridge_endpoint}/v1/feed/download?clean=True" + ) response.raise_for_status() response_json = response.json() @@ -135,7 +140,7 @@ def bulk_upload(self, documents): continue if not vault_id: - return Exception("Could not get vault id") + raise Exception("Could not get vault id") content = { "vaultId": vault_id, @@ -146,19 +151,30 @@ def bulk_upload(self, documents): self.logger.info("Performing bulk upload to backend") + vault_file_path = None try: with tempfile.NamedTemporaryFile(mode="w", delete=False) as vault_file: json.dump(content, vault_file) + vault_file_path = vault_file.name - files = {"files": (vault_id, open(vault_file.name, "rb"))} - response = requests.post( - url=f"{self.cold_bridge_endpoint}/v1/feed/upload", - files=files, - ) + with open(vault_file_path, "rb") as f: + response = requests.post( + url=f"{self.cold_bridge_endpoint}/v1/feed/upload", + files={"files": (vault_id, f)}, + ) response.raise_for_status() + except requests.HTTPError: + self.logger.error( + "HTTP error uploading to backend: %s - %s", + response.status_code, + response.text, + ) + raise except Exception as e: - raise e + self.logger.error("Unexpected error during upload: %s", e) + raise finally: - os.remove(vault_file.name) + if vault_file_path: + os.remove(vault_file_path) self.logger.info("Bulk upload finished successfully") diff --git a/ripple-plugin/src/oso_ripple_plugins/common/errors.py b/ripple-plugin/src/oso_ripple_plugins/common/errors.py index 43e760d..82dd289 100644 --- a/ripple-plugin/src/oso_ripple_plugins/common/errors.py +++ b/ripple-plugin/src/oso_ripple_plugins/common/errors.py @@ -14,8 +14,25 @@ # limitations under the License. - class ConfigError(Exception): """Exception raised when an Environment Variable is not found""" pass + + +class NetworkError(Exception): + """Raised for network-level failures (timeout, connection error)""" + + pass + + +class AuthenticationError(Exception): + """Raised when the token endpoint returns 401 or 403""" + + pass + + +class TokenError(Exception): + """Raised when a token cannot be obtained or parsed from the response""" + + pass diff --git a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py index b692d51..66d1aba 100644 --- a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py +++ b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/api/v1alpha1.py @@ -37,10 +37,6 @@ }, ) -component_status_model = api.model( - "ComponentStatus", {"status": fields.String(), "error": fields.String()} -) - @api.route("/documents", methods=["GET"]) class Download(Resource): @@ -93,8 +89,23 @@ def post(self): @api.route("/status", methods=["GET"]) class Status(Resource): + error_model = api.model( + "Error", + { + "code": fields.String(description="Error code"), + "message": fields.String(description="Error message"), + }, + ) + component_status_model = api.model( - "ComponentStatus", {"status": fields.String(), "error": fields.String()} + "ComponentStatus", + { + "status_code": fields.Integer(description="HTTP status code"), + "status": fields.String(description="Human readable message"), + "errors": fields.List( + fields.Nested(error_model), default=[], description="List of errors" + ), + }, ) @api.response(code=200, description="Success", model=component_status_model) @@ -103,7 +114,11 @@ def get(self): try: current_app.fpm.backend_status() except Exception as e: - logger.exception(e) - abort(503) - - return {"status": "OK"}, 200 + logger.exception("Backend status check failed") + return { + "status_code": 503, + "status": "Unavailable", + "errors": [{"code": "BACKEND_ERROR", "message": str(e)}], + }, 503 + + return {"status_code": 200, "status": "OK", "errors": []}, 200 diff --git a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py index 5bff2bf..5d4da2f 100644 --- a/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py +++ b/ripple-plugin/src/oso_ripple_plugins/frontend_plugin/frontend_plugin_manager.py @@ -134,18 +134,54 @@ def _get_token(self) -> Tuple[str, float]: "signature": base64.b64encode(signature).decode("utf-8"), } - response = requests.post( - f"https://{self.hmz_auth_hostname}/token", - data=data, - headers={"Content-Type": "application/x-www-form-urlencoded"}, - verify=self.verify, - ) + try: + response = requests.post( + f"https://{self.hmz_auth_hostname}/token", + data=data, + headers={"Content-Type": "application/x-www-form-urlencoded"}, + verify=self.verify, + timeout=(5, 30), + ) + except requests.exceptions.Timeout as e: + self.logger.error("Timeout while requesting token: %s", e) + raise errors.NetworkError("Token request timeout") from e + except requests.exceptions.ConnectionError as e: + self.logger.error("Connection error while requesting token: %s", e) + raise errors.NetworkError("Token connection error") from e + except requests.exceptions.RequestException as e: + self.logger.error("Unexpected network error requesting token: %s", e) + raise errors.NetworkError("Unexpected token request error") from e + + if response.status_code in (401, 403): + self.logger.error( + "Authentication failed with status %s: %s", + response.status_code, + response.text, + ) + raise errors.AuthenticationError("Authentication failed") + + try: + response.raise_for_status() + except requests.exceptions.HTTPError as e: + self.logger.error( + "HTTP error requesting token: %s - %s", + response.status_code, + response.text, + ) + raise errors.TokenError( + f"Token endpoint returned HTTP {response.status_code}" + ) from e + + try: + response_json = response.json() + except ValueError as e: + self.logger.error("Invalid JSON from token endpoint: %s", response.text) + raise errors.TokenError("Invalid JSON response from token endpoint") from e - response.raise_for_status() - response_json = response.json() token = response_json.get("access_token") if not token: - raise Exception("Could not get token from response json") + self.logger.error("No access_token in response: %s", response_json) + raise errors.TokenError("Could not get token from response") self.logger.info("Successfully generated new JWT access token") return token, time.time() @@ -177,8 +213,8 @@ def bulk_download(self) -> list: response = requests.get( url=url, headers={"Authorization": "Bearer " + token}, - stream=True, verify=self.verify, + timeout=30, ) response.raise_for_status() vault_json = response.json() @@ -199,6 +235,11 @@ def write_document_set(documents, content_key: str, id_key: str): try: document_id = item.get(id_key) + if not document_id: + self.logger.warning( + f"Missing {id_key} in item for {content_key}, skipping" + ) + continue self.logger.info(f"Saving document {document_id} for bulk download") content = copy.deepcopy(empty_content) @@ -219,7 +260,9 @@ def write_document_set(documents, content_key: str, id_key: str): f"Successfully saved document {document_id} for bulk download" ) except Exception as e: - self.logger.exception(e) + self.logger.exception( + f"Failed processing document in {content_key}: %s", e + ) continue documents = []