Skip to content

Commit 4d3bae8

Browse files
committed
feat(metadata): add remediation messages
1 parent a99ced7 commit 4d3bae8

File tree

5 files changed

+149
-1
lines changed

5 files changed

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

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: 73 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,73 @@ 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+
"marketplaces__aws_product_url": None,
1160+
"on_premise__restrict_signup": True,
1161+
"on_premise__is_email_server_configured": True,
1162+
"on_premise__default_sso_config_api_id": None,
1163+
"onboarding__segmentation_v1_enabled": True,
1164+
"general__maximum_payload_size": 26214400,
1165+
"general__mutual_tls_mode": "disabled",
1166+
},
1167+
"secret_scan_preferences": {
1168+
"maximum_documents_per_scan": 20,
1169+
"maximum_document_size": 1048576,
1170+
},
1171+
}
1172+
1173+
1174+
@responses.activate
1175+
def test_read_metadata_no_remediation_message(client: GGClient):
1176+
"""
1177+
GIVEN a /metadata endpoint that returns a 200 status code but no remediation message
1178+
THEN a call to read_metadata() does not fail
1179+
AND remediation_message are the default ones
1180+
"""
1181+
mock_response = responses.get(
1182+
url=client._url_from_endpoint("metadata", "v1"),
1183+
body=json.dumps(METADATA_RESPONSE_NO_REMEDIATION_MESSAGES),
1184+
content_type="application/json",
1185+
)
1186+
1187+
client.read_metadata()
1188+
1189+
assert mock_response.call_count == 1
1190+
assert client.remediation_messages.pre_commit == DEFAULT_PRE_COMMIT_MESSAGE
1191+
assert client.remediation_messages.pre_push == DEFAULT_PRE_PUSH_MESSAGE
1192+
assert client.remediation_messages.pre_receive == DEFAULT_PRE_RECEIVE_MESSAGE
1193+
1194+
1195+
@responses.activate
1196+
def test_read_metadata_remediation_message(client: GGClient):
1197+
"""
1198+
GIVEN a /metadata endpoint that returns a 200 status code with a correct body with remediation message
1199+
THEN a call to read_metadata() does not fail
1200+
AND returns a valid Detail instance
1201+
"""
1202+
messages = {
1203+
"pre_commit": "message for pre-commit",
1204+
"pre_push": "message for pre-push",
1205+
"pre_receive": "message for pre-receive",
1206+
}
1207+
mock_response = responses.get(
1208+
content_type="application/json",
1209+
url=client._url_from_endpoint("metadata", "v1"),
1210+
body=json.dumps(
1211+
{
1212+
**METADATA_RESPONSE_NO_REMEDIATION_MESSAGES,
1213+
"remediation_messages": messages,
1214+
}
1215+
),
1216+
)
1217+
1218+
client.read_metadata()
1219+
1220+
assert mock_response.call_count == 1
1221+
assert client.remediation_messages.pre_commit == messages["pre_commit"]
1222+
assert client.remediation_messages.pre_push == messages["pre_push"]
1223+
assert client.remediation_messages.pre_receive == messages["pre_receive"]

0 commit comments

Comments
 (0)