diff --git a/pygitguardian/client.py b/pygitguardian/client.py index 5275c6cb..6dad3ed1 100644 --- a/pygitguardian/client.py +++ b/pygitguardian/client.py @@ -37,6 +37,7 @@ QuotaResponse, RemediationMessages, ScanResult, + SecretIncident, SecretScanPreferences, ServerMetadata, ) @@ -454,6 +455,30 @@ def multi_content_scan( return obj + def retrieve_secret_incident( + self, incident_id: int, with_occurrences: int = 0 + ) -> Union[Detail, SecretIncident]: + """ + retrieve_secret_incident handles the /incidents/secret/{incident_id} endpoint of the API + + :param incident_id: incident id + :param with_occurrences: number of occurrences of the incident to retrieve (default 0) + """ + + resp = self.get( + endpoint=f"incidents/secrets/{incident_id}", + params={"with_occurrences": with_occurrences}, + ) + + obj: Union[Detail, SecretIncident] + if is_ok(resp): + obj = SecretIncident.from_dict(resp.json()) + else: + obj = load_detail(resp) + + obj.status_code = resp.status_code + return obj + def quota_overview( self, extra_headers: Optional[Dict[str, str]] = None, diff --git a/pygitguardian/models.py b/pygitguardian/models.py index 3f8c69d7..a47259c2 100644 --- a/pygitguardian/models.py +++ b/pygitguardian/models.py @@ -255,6 +255,88 @@ def __repr__(self) -> str: ) +class SecretIncidentSchema(BaseSchema): + id = fields.Integer(required=True) + date = fields.AwareDateTime(required=True, allow_none=True) + detector = fields.Dict(required=False, allow_none=True) + secret_hash = fields.String(required=False) + hmsl_hash = fields.String(required=False, allow_none=True) + gitguardian_url = fields.String(required=False) + status = fields.String(required=True) + assignee_id = fields.Integer(required=False, allow_none=True) + assignee_email = fields.String(required=False, allow_none=True) + occurrences_count = fields.Integer(required=True) + ignore_reason = fields.String(required=False, allow_none=True) + triggered_at = fields.AwareDateTime(required=False) + ignored_at = fields.AwareDateTime(required=False, allow_none=True) + secret_revoked = fields.Boolean(required=False) + severity = fields.String(required=False) + validity = fields.String(required=False) + resolved_at = fields.AwareDateTime(required=False, allow_none=True) + share_url = fields.String(required=False, allow_none=True) + tags = fields.List(fields.String(), required=False) + + @post_load + def make_incident(self, data: Dict[str, Any], **kwargs: Any) -> "SecretIncident": + return SecretIncident(**data) + + +class SecretIncident(Base, FromDictMixin): + """ + Secret Incident describes a leaked secret incident. + """ + + SCHEMA = SecretIncidentSchema() + + def __init__( + self, + id: int, + date: date, + detector: Optional[Dict[str, Any]] = None, + secret_hash: Optional[str] = None, + hmsl_hash: Optional[str] = None, + gitguardian_url: Optional[str] = None, + status: Optional[str] = None, + assignee_id: Optional[int] = None, + assignee_email: Optional[str] = None, + occurrences_count: Optional[int] = None, + ignore_reason: Optional[str] = None, + triggered_at: Optional[datetime] = None, + ignored_at: Optional[datetime] = None, + secret_revoked: Optional[bool] = None, + severity: Optional[str] = None, + validity: Optional[str] = None, + resolved_at: Optional[datetime] = None, + share_url: Optional[str] = None, + tags: Optional[List[str]] = None, + ): + super().__init__() + self.id = id + self.date = date + self.detector = detector + self.secret_hash = secret_hash + self.hmsl_hash = hmsl_hash + self.gitguardian_url = gitguardian_url + self.status = status + self.assignee_id = assignee_id + self.assignee_email = assignee_email + self.occurrences_count = occurrences_count + self.ignore_reason = ignore_reason + self.triggered_at = triggered_at + self.ignored_at = ignored_at + self.secret_revoked = secret_revoked + self.severity = severity + self.validity = validity + self.resolved_at = resolved_at + self.share_url = share_url + + def __repr__(self) -> str: + return ( + f"id:{self.id}, detector_name:{self.detector.get('name') if self.detector else None}," + f"secret_hash:{self.secret_hash}, url:{self.gitguardian_url}" + ) + + class PolicyBreakSchema(BaseSchema): break_type = fields.String(data_key="type", required=True) policy = fields.String(required=True) diff --git a/tests/test_client.py b/tests/test_client.py index 3dbf410c..e1efd803 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -616,6 +616,71 @@ def test_multiscan_parameters( assert mock_response.call_count == 1 +@responses.activate +def test_retrieve_secret_incident(client: GGClient): + """ + GIVEN a ggclient + WHEN calling retrieve_secret_incident with parameters + THEN the parameters are passed in the request + """ + + mock_response = responses.get( + url=client._url_from_endpoint("incidents/secrets/3759", "v1"), + status=200, + match=[matchers.query_param_matcher({"with_occurrences": 0})], + json={ + "id": 3759, + "date": "2019-08-22T14:15:22Z", + "detector": { + "name": "slack_bot_token", + "display_name": "Slack Bot Token", + "nature": "specific", + "family": "apikey", + "detector_group_name": "slackbot_token", + "detector_group_display_name": "Slack Bot Token", + }, + "secret_hash": "Ri9FjVgdOlPnBmujoxP4XPJcbe82BhJXB/SAngijw/juCISuOMgPzYhV28m6OG24", + "hmsl_hash": "05975add34ddc9a38a0fb57c7d3e676ffed57080516fc16bf8d8f14308fedb86", + "gitguardian_url": "https://dashboard.gitguardian.com/workspace/1/incidents/3899", + "regression": False, + "status": "IGNORED", + "assignee_id": 309, + "assignee_email": "eric@gitguardian.com", + "occurrences_count": 4, + "secret_presence": { + "files_requiring_code_fix": 1, + "files_pending_merge": 1, + "files_fixed": 1, + "outside_vcs": 1, + "removed_outside_vcs": 0, + "in_vcs": 3, + "removed_in_vcs": 0, + }, + "ignore_reason": "test_credential", + "triggered_at": "2019-05-12T09:37:49Z", + "ignored_at": "2019-08-24T14:15:22Z", + "ignorer_id": 309, + "ignorer_api_token_id": "fdf075f9-1662-4cf1-9171-af50568158a8", + "resolver_id": 395, + "resolver_api_token_id": "fdf075f9-1662-4cf1-9171-af50568158a8", + "secret_revoked": False, + "severity": "high", + "validity": "valid", + "resolved_at": None, + "share_url": "https://dashboard.gitguardian.com/share/incidents/11111111-1111-1111-1111-111111111111", + "tags": ["FROM_HISTORICAL_SCAN", "SENSITIVE_FILE"], + }, + ) + + result = client.retrieve_secret_incident(3759) + + assert mock_response.call_count == 1 + assert result.id == 3759 + assert result.detector["name"] == "slack_bot_token" + assert result.ignore_reason == "test_credential" + assert result.secret_revoked is False + + @responses.activate def test_rate_limit(): """