Skip to content

Commit fc766e1

Browse files
authored
Merge pull request #111 from GitGuardian/garancegourdel/scrt-4626-ggshield-display-custom-remediation-message-in-ggshield-if
Add remediation messages to GGclient
2 parents a99ced7 + 6b57cef commit fc766e1

File tree

5 files changed

+139
-1
lines changed

5 files changed

+139
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
- GGClient now contains remediation messages obtained from the API `/metadata` endpoint.

pygitguardian/client.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
JWTService,
3636
MultiScanResult,
3737
QuotaResponse,
38+
RemediationMessages,
3839
ScanResult,
3940
SecretScanPreferences,
4041
ServerMetadata,
@@ -151,6 +152,7 @@ class GGClient:
151152
user_agent: str
152153
extra_headers: Dict
153154
secret_scan_preferences: SecretScanPreferences
155+
remediation_messages: RemediationMessages
154156
callbacks: Optional[GGClientCallbacks]
155157

156158
def __init__(
@@ -214,6 +216,7 @@ def __init__(
214216
)
215217
self.maximum_payload_size = MAXIMUM_PAYLOAD_SIZE
216218
self.secret_scan_preferences = SecretScanPreferences()
219+
self.remediation_messages = RemediationMessages()
217220

218221
def request(
219222
self,
@@ -676,6 +679,7 @@ def read_metadata(self) -> Optional[Detail]:
676679
"general__maximum_payload_size", MAXIMUM_PAYLOAD_SIZE
677680
)
678681
self.secret_scan_preferences = metadata.secret_scan_preferences
682+
self.remediation_messages = metadata.remediation_messages
679683
return None
680684

681685
def create_jwt(

pygitguardian/config.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,3 +5,51 @@
55
MULTI_DOCUMENT_LIMIT = 20
66
DOCUMENT_SIZE_THRESHOLD_BYTES = 1048576 # 1MB
77
MAXIMUM_PAYLOAD_SIZE = 2621440 # 25MB
8+
9+
10+
DEFAULT_REWRITE_GIT_HISTORY_MESSAGE = """
11+
To prevent having to rewrite git history in the future, setup ggshield as a pre-commit hook:
12+
https://docs.gitguardian.com/ggshield-docs/integrations/git-hooks/pre-commit
13+
"""
14+
15+
DEFAULT_PRE_COMMIT_MESSAGE = """> How to remediate
16+
17+
Since the secret was detected before the commit was made:
18+
1. replace the secret with its reference (e.g. environment variable).
19+
2. commit again.
20+
21+
> [To apply with caution] If you want to bypass ggshield (false positive or other reason), run:
22+
- if you use the pre-commit framework:
23+
24+
SKIP=ggshield git commit -m "<your message>"""
25+
26+
DEFAULT_PRE_PUSH_MESSAGE = (
27+
"""> How to remediate
28+
29+
Since the secret was detected before the push BUT after the commit, you need to:
30+
1. rewrite the git history making sure to replace the secret with its reference (e.g. environment variable).
31+
2. push again.
32+
"""
33+
+ DEFAULT_REWRITE_GIT_HISTORY_MESSAGE
34+
+ """
35+
> [To apply with caution] If you want to bypass ggshield (false positive or other reason), run:
36+
- if you use the pre-commit framework:
37+
38+
SKIP=ggshield-push git push"""
39+
)
40+
41+
DEFAULT_PRE_RECEIVE_MESSAGE = (
42+
"""> How to remediate
43+
44+
A pre-receive hook set server side prevented you from pushing secrets.
45+
46+
Since the secret was detected during the push BUT after the commit, you need to:
47+
1. rewrite the git history making sure to replace the secret with its reference (e.g. environment variable).
48+
2. push again.
49+
"""
50+
+ DEFAULT_REWRITE_GIT_HISTORY_MESSAGE
51+
+ """
52+
> [To apply with caution] If you want to bypass ggshield (false positive or other reason), run:
53+
54+
git push -o breakglass"""
55+
)

pygitguardian/models.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,13 @@
1919
)
2020
from typing_extensions import Self
2121

22-
from .config import DOCUMENT_SIZE_THRESHOLD_BYTES, MULTI_DOCUMENT_LIMIT
22+
from .config import (
23+
DEFAULT_PRE_COMMIT_MESSAGE,
24+
DEFAULT_PRE_PUSH_MESSAGE,
25+
DEFAULT_PRE_RECEIVE_MESSAGE,
26+
DOCUMENT_SIZE_THRESHOLD_BYTES,
27+
MULTI_DOCUMENT_LIMIT,
28+
)
2329

2430

2531
class ToDictMixin:
@@ -734,13 +740,23 @@ class SecretScanPreferences:
734740
maximum_documents_per_scan: int = MULTI_DOCUMENT_LIMIT
735741

736742

743+
@dataclass
744+
class RemediationMessages:
745+
pre_commit: str = DEFAULT_PRE_COMMIT_MESSAGE
746+
pre_push: str = DEFAULT_PRE_PUSH_MESSAGE
747+
pre_receive: str = DEFAULT_PRE_RECEIVE_MESSAGE
748+
749+
737750
@dataclass
738751
class ServerMetadata(Base, FromDictMixin):
739752
version: str
740753
preferences: Dict[str, Any]
741754
secret_scan_preferences: SecretScanPreferences = field(
742755
default_factory=SecretScanPreferences
743756
)
757+
remediation_messages: RemediationMessages = field(
758+
default_factory=RemediationMessages
759+
)
744760

745761

746762
ServerMetadata.SCHEMA = cast(

tests/test_client.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616
from pygitguardian.client import GGClientCallbacks, is_ok, load_detail
1717
from pygitguardian.config import (
1818
DEFAULT_BASE_URI,
19+
DEFAULT_PRE_COMMIT_MESSAGE,
20+
DEFAULT_PRE_PUSH_MESSAGE,
21+
DEFAULT_PRE_RECEIVE_MESSAGE,
1922
DOCUMENT_SIZE_THRESHOLD_BYTES,
2023
MULTI_DOCUMENT_LIMIT,
2124
)
@@ -1148,3 +1151,67 @@ def test_read_metadata_bad_response(client: GGClient):
11481151
assert mock_response.call_count == 1
11491152
assert detail.status_code == 500
11501153
assert detail.detail == "Failed"
1154+
1155+
1156+
METADATA_RESPONSE_NO_REMEDIATION_MESSAGES = {
1157+
"version": "dev",
1158+
"preferences": {
1159+
"general__maximum_payload_size": 26214400,
1160+
},
1161+
"secret_scan_preferences": {
1162+
"maximum_documents_per_scan": 20,
1163+
"maximum_document_size": 1048576,
1164+
},
1165+
}
1166+
1167+
1168+
@responses.activate
1169+
def test_read_metadata_no_remediation_message(client: GGClient):
1170+
"""
1171+
GIVEN a /metadata endpoint that returns a 200 status code but no remediation message
1172+
THEN a call to read_metadata() does not fail
1173+
AND remediation_message are the default ones
1174+
"""
1175+
mock_response = responses.get(
1176+
url=client._url_from_endpoint("metadata", "v1"),
1177+
body=json.dumps(METADATA_RESPONSE_NO_REMEDIATION_MESSAGES),
1178+
content_type="application/json",
1179+
)
1180+
1181+
client.read_metadata()
1182+
1183+
assert mock_response.call_count == 1
1184+
assert client.remediation_messages.pre_commit == DEFAULT_PRE_COMMIT_MESSAGE
1185+
assert client.remediation_messages.pre_push == DEFAULT_PRE_PUSH_MESSAGE
1186+
assert client.remediation_messages.pre_receive == DEFAULT_PRE_RECEIVE_MESSAGE
1187+
1188+
1189+
@responses.activate
1190+
def test_read_metadata_remediation_message(client: GGClient):
1191+
"""
1192+
GIVEN a /metadata endpoint that returns a 200 status code with a correct body with remediation message
1193+
THEN a call to read_metadata() does not fail
1194+
AND returns a valid Detail instance
1195+
"""
1196+
messages = {
1197+
"pre_commit": "message for pre-commit",
1198+
"pre_push": "message for pre-push",
1199+
"pre_receive": "message for pre-receive",
1200+
}
1201+
mock_response = responses.get(
1202+
content_type="application/json",
1203+
url=client._url_from_endpoint("metadata", "v1"),
1204+
body=json.dumps(
1205+
{
1206+
**METADATA_RESPONSE_NO_REMEDIATION_MESSAGES,
1207+
"remediation_messages": messages,
1208+
}
1209+
),
1210+
)
1211+
1212+
client.read_metadata()
1213+
1214+
assert mock_response.call_count == 1
1215+
assert client.remediation_messages.pre_commit == messages["pre_commit"]
1216+
assert client.remediation_messages.pre_push == messages["pre_push"]
1217+
assert client.remediation_messages.pre_receive == messages["pre_receive"]

0 commit comments

Comments
 (0)