Skip to content

Commit bb886d1

Browse files
author
Garance Gourdel
committed
feat(client): add /incidents/secrets/ endpoint
1 parent 582a78e commit bb886d1

File tree

3 files changed

+172
-0
lines changed

3 files changed

+172
-0
lines changed

pygitguardian/client.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
QuotaResponse,
3838
RemediationMessages,
3939
ScanResult,
40+
SecretIncident,
4041
SecretScanPreferences,
4142
ServerMetadata,
4243
)
@@ -454,6 +455,30 @@ def multi_content_scan(
454455

455456
return obj
456457

458+
def retrieve_secret_incident(
459+
self, incident_id: int, with_occurrences: int = 0
460+
) -> Union[Detail, SecretIncident]:
461+
"""
462+
retrieve_secret_incident handles the /incidents/secret/{incident_id} endpoint of the API
463+
464+
:param incident_id: incident id
465+
:param with_occurrences: number of occurrences of the incident to retrieve (default 0)
466+
"""
467+
468+
resp = self.get(
469+
endpoint=f"incidents/secrets/{incident_id}",
470+
params={"with_occurrences": with_occurrences},
471+
)
472+
473+
obj: Union[Detail, SecretIncident]
474+
if is_ok(resp):
475+
obj = SecretIncident.from_dict(resp.json())
476+
else:
477+
obj = load_detail(resp)
478+
479+
obj.status_code = resp.status_code
480+
return obj
481+
457482
def quota_overview(
458483
self,
459484
extra_headers: Optional[Dict[str, str]] = None,

pygitguardian/models.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,88 @@ def __repr__(self) -> str:
255255
)
256256

257257

258+
class SecretIncidentSchema(BaseSchema):
259+
id = fields.Integer(required=True)
260+
date = fields.AwareDateTime(required=True, allow_none=True)
261+
detector = fields.Dict(required=False, allow_none=True)
262+
secret_hash = fields.String(required=False)
263+
hmsl_hash = fields.String(required=False, allow_none=True)
264+
gitguardian_url = fields.String(required=False)
265+
status = fields.String(required=True)
266+
assignee_id = fields.Integer(required=False, allow_none=True)
267+
assignee_email = fields.String(required=False, allow_none=True)
268+
occurrences_count = fields.Integer(required=True)
269+
ignore_reason = fields.String(required=False, allow_none=True)
270+
triggered_at = fields.AwareDateTime(required=False)
271+
ignored_at = fields.AwareDateTime(required=False, allow_none=True)
272+
secret_revoked = fields.Boolean(required=False)
273+
severity = fields.String(required=False)
274+
validity = fields.String(required=False)
275+
resolved_at = fields.AwareDateTime(required=False, allow_none=True)
276+
share_url = fields.String(required=False, allow_none=True)
277+
tags = fields.List(fields.String(), required=False)
278+
279+
@post_load
280+
def make_incident(self, data: Dict[str, Any], **kwargs: Any) -> "SecretIncident":
281+
return SecretIncident(**data)
282+
283+
284+
class SecretIncident(Base, FromDictMixin):
285+
"""
286+
Secret Incident describes a leaked secret incident.
287+
"""
288+
289+
SCHEMA = SecretIncidentSchema()
290+
291+
def __init__(
292+
self,
293+
id: int,
294+
date: date,
295+
detector: Optional[Dict[str, Any]] = None,
296+
secret_hash: Optional[str] = None,
297+
hmsl_hash: Optional[str] = None,
298+
gitguardian_url: Optional[str] = None,
299+
status: Optional[str] = None,
300+
assignee_id: Optional[int] = None,
301+
assignee_email: Optional[str] = None,
302+
occurrences_count: Optional[int] = None,
303+
ignore_reason: Optional[str] = None,
304+
triggered_at: Optional[datetime] = None,
305+
ignored_at: Optional[datetime] = None,
306+
secret_revoked: Optional[bool] = None,
307+
severity: Optional[str] = None,
308+
validity: Optional[str] = None,
309+
resolved_at: Optional[datetime] = None,
310+
share_url: Optional[str] = None,
311+
tags: Optional[List[str]] = None,
312+
):
313+
super().__init__()
314+
self.id = id
315+
self.date = date
316+
self.detector = detector
317+
self.secret_hash = secret_hash
318+
self.hmsl_hash = hmsl_hash
319+
self.gitguardian_url = gitguardian_url
320+
self.status = status
321+
self.assignee_id = assignee_id
322+
self.assignee_email = assignee_email
323+
self.occurrences_count = occurrences_count
324+
self.ignore_reason = ignore_reason
325+
self.triggered_at = triggered_at
326+
self.ignored_at = ignored_at
327+
self.secret_revoked = secret_revoked
328+
self.severity = severity
329+
self.validity = validity
330+
self.resolved_at = resolved_at
331+
self.share_url = share_url
332+
333+
def __repr__(self) -> str:
334+
return (
335+
f"id:{self.id}, detector_name:{self.detector.get('name') if self.detector else None},"
336+
f"secret_hash:{self.secret_hash}, url:{self.gitguardian_url}"
337+
)
338+
339+
258340
class PolicyBreakSchema(BaseSchema):
259341
break_type = fields.String(data_key="type", required=True)
260342
policy = fields.String(required=True)

tests/test_client.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,71 @@ def test_multiscan_parameters(
616616
assert mock_response.call_count == 1
617617

618618

619+
@responses.activate
620+
def test_retrieve_secret_incident(client: GGClient):
621+
"""
622+
GIVEN a ggclient
623+
WHEN calling retrieve_secret_incident with parameters
624+
THEN the parameters are passed in the request
625+
"""
626+
627+
mock_response = responses.get(
628+
url=client._url_from_endpoint("incidents/secrets/3759", "v1"),
629+
status=200,
630+
match=[matchers.query_param_matcher({"with_occurrences": 0})],
631+
json={
632+
"id": 3759,
633+
"date": "2019-08-22T14:15:22Z",
634+
"detector": {
635+
"name": "slack_bot_token",
636+
"display_name": "Slack Bot Token",
637+
"nature": "specific",
638+
"family": "apikey",
639+
"detector_group_name": "slackbot_token",
640+
"detector_group_display_name": "Slack Bot Token",
641+
},
642+
"secret_hash": "Ri9FjVgdOlPnBmujoxP4XPJcbe82BhJXB/SAngijw/juCISuOMgPzYhV28m6OG24",
643+
"hmsl_hash": "05975add34ddc9a38a0fb57c7d3e676ffed57080516fc16bf8d8f14308fedb86",
644+
"gitguardian_url": "https://dashboard.gitguardian.com/workspace/1/incidents/3899",
645+
"regression": False,
646+
"status": "IGNORED",
647+
"assignee_id": 309,
648+
"assignee_email": "[email protected]",
649+
"occurrences_count": 4,
650+
"secret_presence": {
651+
"files_requiring_code_fix": 1,
652+
"files_pending_merge": 1,
653+
"files_fixed": 1,
654+
"outside_vcs": 1,
655+
"removed_outside_vcs": 0,
656+
"in_vcs": 3,
657+
"removed_in_vcs": 0,
658+
},
659+
"ignore_reason": "test_credential",
660+
"triggered_at": "2019-05-12T09:37:49Z",
661+
"ignored_at": "2019-08-24T14:15:22Z",
662+
"ignorer_id": 309,
663+
"ignorer_api_token_id": "fdf075f9-1662-4cf1-9171-af50568158a8",
664+
"resolver_id": 395,
665+
"resolver_api_token_id": "fdf075f9-1662-4cf1-9171-af50568158a8",
666+
"secret_revoked": False,
667+
"severity": "high",
668+
"validity": "valid",
669+
"resolved_at": None,
670+
"share_url": "https://dashboard.gitguardian.com/share/incidents/11111111-1111-1111-1111-111111111111",
671+
"tags": ["FROM_HISTORICAL_SCAN", "SENSITIVE_FILE"],
672+
},
673+
)
674+
675+
result = client.retrieve_secret_incident(3759)
676+
677+
assert mock_response.call_count == 1
678+
assert result.id == 3759
679+
assert result.detector["name"] == "slack_bot_token"
680+
assert result.ignore_reason == "test_credential"
681+
assert result.secret_revoked is False
682+
683+
619684
@responses.activate
620685
def test_rate_limit():
621686
"""

0 commit comments

Comments
 (0)