From 6b57cef8161a17d3343b164ed1bf7d4bd63f3f43 Mon Sep 17 00:00:00 2001 From: Garance Gourdel Date: Mon, 1 Jul 2024 17:57:19 +0200 Subject: [PATCH] feat(metadata): add remediation messages --- ...stom_remediation_message_in_ggshield_if.md | 3 + pygitguardian/client.py | 4 ++ pygitguardian/config.py | 48 +++++++++++++ pygitguardian/models.py | 18 ++++- tests/test_client.py | 67 +++++++++++++++++++ 5 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 changelog.d/20240701_180056_fnareoh_scrt_4626_ggshield_display_custom_remediation_message_in_ggshield_if.md diff --git a/changelog.d/20240701_180056_fnareoh_scrt_4626_ggshield_display_custom_remediation_message_in_ggshield_if.md b/changelog.d/20240701_180056_fnareoh_scrt_4626_ggshield_display_custom_remediation_message_in_ggshield_if.md new file mode 100644 index 00000000..68aa9ccd --- /dev/null +++ b/changelog.d/20240701_180056_fnareoh_scrt_4626_ggshield_display_custom_remediation_message_in_ggshield_if.md @@ -0,0 +1,3 @@ +### Added + +- GGClient now contains remediation messages obtained from the API `/metadata` endpoint. diff --git a/pygitguardian/client.py b/pygitguardian/client.py index d847a77f..5275c6cb 100644 --- a/pygitguardian/client.py +++ b/pygitguardian/client.py @@ -35,6 +35,7 @@ JWTService, MultiScanResult, QuotaResponse, + RemediationMessages, ScanResult, SecretScanPreferences, ServerMetadata, @@ -151,6 +152,7 @@ class GGClient: user_agent: str extra_headers: Dict secret_scan_preferences: SecretScanPreferences + remediation_messages: RemediationMessages callbacks: Optional[GGClientCallbacks] def __init__( @@ -214,6 +216,7 @@ def __init__( ) self.maximum_payload_size = MAXIMUM_PAYLOAD_SIZE self.secret_scan_preferences = SecretScanPreferences() + self.remediation_messages = RemediationMessages() def request( self, @@ -676,6 +679,7 @@ def read_metadata(self) -> Optional[Detail]: "general__maximum_payload_size", MAXIMUM_PAYLOAD_SIZE ) self.secret_scan_preferences = metadata.secret_scan_preferences + self.remediation_messages = metadata.remediation_messages return None def create_jwt( diff --git a/pygitguardian/config.py b/pygitguardian/config.py index 55c7b38c..90c2d247 100644 --- a/pygitguardian/config.py +++ b/pygitguardian/config.py @@ -5,3 +5,51 @@ MULTI_DOCUMENT_LIMIT = 20 DOCUMENT_SIZE_THRESHOLD_BYTES = 1048576 # 1MB MAXIMUM_PAYLOAD_SIZE = 2621440 # 25MB + + +DEFAULT_REWRITE_GIT_HISTORY_MESSAGE = """ + To prevent having to rewrite git history in the future, setup ggshield as a pre-commit hook: + https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit +""" + +DEFAULT_PRE_COMMIT_MESSAGE = """> How to remediate + + Since the secret was detected before the commit was made: + 1. replace the secret with its reference (e.g. environment variable). + 2. commit again. + +> [To apply with caution] If you want to bypass ggshield (false positive or other reason), run: + - if you use the pre-commit framework: + + SKIP=ggshield git commit -m """" + +DEFAULT_PRE_PUSH_MESSAGE = ( + """> How to remediate + + Since the secret was detected before the push BUT after the commit, you need to: + 1. rewrite the git history making sure to replace the secret with its reference (e.g. environment variable). + 2. push again. +""" + + DEFAULT_REWRITE_GIT_HISTORY_MESSAGE + + """ +> [To apply with caution] If you want to bypass ggshield (false positive or other reason), run: + - if you use the pre-commit framework: + + SKIP=ggshield-push git push""" +) + +DEFAULT_PRE_RECEIVE_MESSAGE = ( + """> How to remediate + + A pre-receive hook set server side prevented you from pushing secrets. + + Since the secret was detected during the push BUT after the commit, you need to: + 1. rewrite the git history making sure to replace the secret with its reference (e.g. environment variable). + 2. push again. +""" + + DEFAULT_REWRITE_GIT_HISTORY_MESSAGE + + """ +> [To apply with caution] If you want to bypass ggshield (false positive or other reason), run: + + git push -o breakglass""" +) diff --git a/pygitguardian/models.py b/pygitguardian/models.py index 87e77918..3f8c69d7 100644 --- a/pygitguardian/models.py +++ b/pygitguardian/models.py @@ -19,7 +19,13 @@ ) from typing_extensions import Self -from .config import DOCUMENT_SIZE_THRESHOLD_BYTES, MULTI_DOCUMENT_LIMIT +from .config import ( + DEFAULT_PRE_COMMIT_MESSAGE, + DEFAULT_PRE_PUSH_MESSAGE, + DEFAULT_PRE_RECEIVE_MESSAGE, + DOCUMENT_SIZE_THRESHOLD_BYTES, + MULTI_DOCUMENT_LIMIT, +) class ToDictMixin: @@ -734,6 +740,13 @@ class SecretScanPreferences: maximum_documents_per_scan: int = MULTI_DOCUMENT_LIMIT +@dataclass +class RemediationMessages: + pre_commit: str = DEFAULT_PRE_COMMIT_MESSAGE + pre_push: str = DEFAULT_PRE_PUSH_MESSAGE + pre_receive: str = DEFAULT_PRE_RECEIVE_MESSAGE + + @dataclass class ServerMetadata(Base, FromDictMixin): version: str @@ -741,6 +754,9 @@ class ServerMetadata(Base, FromDictMixin): secret_scan_preferences: SecretScanPreferences = field( default_factory=SecretScanPreferences ) + remediation_messages: RemediationMessages = field( + default_factory=RemediationMessages + ) ServerMetadata.SCHEMA = cast( diff --git a/tests/test_client.py b/tests/test_client.py index 7a9fcbfb..3dbf410c 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -16,6 +16,9 @@ from pygitguardian.client import GGClientCallbacks, is_ok, load_detail from pygitguardian.config import ( DEFAULT_BASE_URI, + DEFAULT_PRE_COMMIT_MESSAGE, + DEFAULT_PRE_PUSH_MESSAGE, + DEFAULT_PRE_RECEIVE_MESSAGE, DOCUMENT_SIZE_THRESHOLD_BYTES, MULTI_DOCUMENT_LIMIT, ) @@ -1148,3 +1151,67 @@ def test_read_metadata_bad_response(client: GGClient): assert mock_response.call_count == 1 assert detail.status_code == 500 assert detail.detail == "Failed" + + +METADATA_RESPONSE_NO_REMEDIATION_MESSAGES = { + "version": "dev", + "preferences": { + "general__maximum_payload_size": 26214400, + }, + "secret_scan_preferences": { + "maximum_documents_per_scan": 20, + "maximum_document_size": 1048576, + }, +} + + +@responses.activate +def test_read_metadata_no_remediation_message(client: GGClient): + """ + GIVEN a /metadata endpoint that returns a 200 status code but no remediation message + THEN a call to read_metadata() does not fail + AND remediation_message are the default ones + """ + mock_response = responses.get( + url=client._url_from_endpoint("metadata", "v1"), + body=json.dumps(METADATA_RESPONSE_NO_REMEDIATION_MESSAGES), + content_type="application/json", + ) + + client.read_metadata() + + assert mock_response.call_count == 1 + assert client.remediation_messages.pre_commit == DEFAULT_PRE_COMMIT_MESSAGE + assert client.remediation_messages.pre_push == DEFAULT_PRE_PUSH_MESSAGE + assert client.remediation_messages.pre_receive == DEFAULT_PRE_RECEIVE_MESSAGE + + +@responses.activate +def test_read_metadata_remediation_message(client: GGClient): + """ + GIVEN a /metadata endpoint that returns a 200 status code with a correct body with remediation message + THEN a call to read_metadata() does not fail + AND returns a valid Detail instance + """ + messages = { + "pre_commit": "message for pre-commit", + "pre_push": "message for pre-push", + "pre_receive": "message for pre-receive", + } + mock_response = responses.get( + content_type="application/json", + url=client._url_from_endpoint("metadata", "v1"), + body=json.dumps( + { + **METADATA_RESPONSE_NO_REMEDIATION_MESSAGES, + "remediation_messages": messages, + } + ), + ) + + client.read_metadata() + + assert mock_response.call_count == 1 + assert client.remediation_messages.pre_commit == messages["pre_commit"] + assert client.remediation_messages.pre_push == messages["pre_push"] + assert client.remediation_messages.pre_receive == messages["pre_receive"]