Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(client): add /incidents/secrets/ endpoint #114

Closed
wants to merge 1 commit into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions pygitguardian/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@
QuotaResponse,
RemediationMessages,
ScanResult,
SecretIncident,
SecretScanPreferences,
ServerMetadata,
)
Expand Down Expand Up @@ -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,
Expand Down
82 changes: 82 additions & 0 deletions pygitguardian/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
65 changes: 65 additions & 0 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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": "[email protected]",
"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():
"""
Expand Down
Loading