Skip to content

Commit 4b05ac9

Browse files
author
Garance Gourdel
committed
feat(incidents): implement models and client for /incidents/secrets/ endpoint
1 parent f226e8a commit 4b05ac9

File tree

5 files changed

+482
-2
lines changed

5 files changed

+482
-2
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
### Added
2+
3+
- Added the models and client for to retrieve a secret incident https://api.gitguardian.com/docs#tag/Secret-Incidents/operation/retrieve-incidents

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: 173 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
from dataclasses import dataclass, field
55
from datetime import date, datetime
66
from enum import Enum
7-
from typing import Any, ClassVar, Dict, List, Optional, cast
7+
from typing import Any, ClassVar, Dict, List, Literal, Optional, cast
88
from uuid import UUID
99

1010
import marshmallow_dataclass
@@ -794,3 +794,175 @@ class JWTService(Enum):
794794
"""Enum for the different services GIM can generate a JWT for."""
795795

796796
HMSL = "hmsl"
797+
798+
799+
@dataclass
800+
class Detector(Base, FromDictMixin):
801+
name: str
802+
display_name: str
803+
nature: str
804+
family: str
805+
detector_group_name: str
806+
detector_group_display_name: str
807+
808+
809+
Severity = Literal["low", "medium", "high", "critical", "unknown"]
810+
ValidityStatus = Literal["valid", "invalid", "failed_to_check", "no_checker", "unknown"]
811+
IncidentStatus = Literal["IGNORED", "TRIGGERED", "RESOLVED", "ASSIGNED"]
812+
Tag = Literal[
813+
"DEFAULT_BRANCH",
814+
"FROM_HISTORICAL_SCAN",
815+
"CHECK_RUN_SKIP_FALSE_POSITIVE",
816+
"CHECK_RUN_SKIP_LOW_RISK",
817+
"CHECK_RUN_SKIP_TEST_CRED",
818+
"PUBLIC",
819+
"PUBLICLY_EXPOSED",
820+
"PUBLICLY_LEAKED",
821+
"REGRESSION",
822+
"SENSITIVE_FILE",
823+
"TEST_FILE",
824+
]
825+
IgnoreReason = Literal["test_credential", "false_positive", "low_risk"]
826+
OccurrenceKind = Literal["realtime", "historical"]
827+
OccurencePresence = Literal["present", "removed"]
828+
829+
830+
@dataclass
831+
class SecretPresence(Base, FromDictMixin):
832+
files_requiring_code_fix: int
833+
files_pending_merge: int
834+
files_fixed: int
835+
outside_vcs: int
836+
removed_outside_vcs: int
837+
in_vcs: int
838+
removed_in_vcs: int
839+
840+
841+
@dataclass
842+
class Answer(Base, FromDictMixin):
843+
type: str
844+
field_ref: str
845+
field_label: str
846+
boolean: Optional[bool] = None
847+
text: Optional[str] = None
848+
849+
850+
@dataclass
851+
class Feedback(Base, FromDictMixin):
852+
created_at: datetime
853+
updated_at: datetime
854+
member_id: int
855+
email: str
856+
answers: List[Answer]
857+
858+
859+
@dataclass
860+
class Source(Base, FromDictMixin):
861+
id: int
862+
url: str
863+
type: str
864+
full_name: str
865+
health: Literal["safe", "unknown", "at_risk"]
866+
default_branch: str
867+
default_branch_head: Optional[str]
868+
open_incidents_count: int
869+
closed_incidents_count: int
870+
secret_incidents_breakdown: Dict[str, Any] # TODO: add SecretIncidentsBreakdown
871+
visibility: str
872+
external_id: str
873+
source_criticality: str
874+
last_scan: Dict[str, Any] # TODO: add LastScan
875+
monitored: bool
876+
877+
878+
@dataclass
879+
class MatchOccurrence(Base, FromDictMixin):
880+
"""
881+
Describes the match of an occurrence, different from the Match return as part of a PolicyBreak.
882+
883+
name: type of the match such as "api_key", "password", "client_id", "client_secret"...
884+
indice_start: start index of the match in the document
885+
indice_end: end index of the match in the document
886+
pre_line_start: start line of the match in the document (before the git patch)
887+
pre_line_end: end line of the match in the document (before the git patch)
888+
post_line_start: start line of the match in the document (after the git patch)
889+
post_line_end: end line of the match in the document (after the git patch)
890+
"""
891+
892+
name: str
893+
indice_start: int
894+
indice_end: int
895+
pre_line_start: Optional[int]
896+
pre_line_end: Optional[int]
897+
post_line_start: int
898+
post_line_end: int
899+
900+
901+
@dataclass
902+
class SecretOccurrence(Base, FromDictMixin):
903+
id: int
904+
incident_id: int
905+
kind: OccurrenceKind
906+
source: Source
907+
author_name: str
908+
author_info: str
909+
date: datetime # Publish date
910+
url: str
911+
matches: List[MatchOccurrence]
912+
tags: List[str]
913+
sha: Optional[str] # Commit sha
914+
presence: OccurencePresence
915+
filepath: Optional[str]
916+
917+
918+
SecretOccurrence.SCHEMA = cast(
919+
BaseSchema,
920+
marshmallow_dataclass.class_schema(SecretOccurrence, base_schema=BaseSchema)(),
921+
)
922+
923+
924+
@dataclass(repr=False) # the default repr would be too long
925+
class SecretIncident(Base, FromDictMixin):
926+
"""
927+
Secret Incident describes a leaked secret incident.
928+
"""
929+
930+
id: int
931+
date: datetime
932+
detector: Detector
933+
secret_hash: str
934+
hmsl_hash: str
935+
gitguardian_url: str
936+
regression: bool
937+
status: IncidentStatus
938+
assignee_id: Optional[int]
939+
assignee_email: Optional[str]
940+
occurrences_count: int
941+
secret_presence: SecretPresence
942+
ignore_reason: Optional[IgnoreReason]
943+
triggered_at: Optional[datetime]
944+
ignored_at: Optional[datetime]
945+
ignorer_id: Optional[int]
946+
ignorer_api_token_id: Optional[UUID]
947+
resolver_id: Optional[int]
948+
resolver_api_token_id: Optional[UUID]
949+
secret_revoked: bool
950+
severity: Severity
951+
validity: ValidityStatus
952+
resolved_at: Optional[datetime]
953+
share_url: Optional[str]
954+
tags: List[Tag]
955+
feedback_list: List[Feedback]
956+
occurrences: Optional[List[SecretOccurrence]]
957+
958+
def __repr__(self) -> str:
959+
return (
960+
f"id:{self.id}, detector_name:{self.detector.name},"
961+
f" url:{self.gitguardian_url}"
962+
)
963+
964+
965+
SecretIncident.SCHEMA = cast(
966+
BaseSchema,
967+
marshmallow_dataclass.class_schema(SecretIncident, base_schema=BaseSchema)(),
968+
)

tests/test_client.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -616,6 +616,88 @@ 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+
"feedback_list": [
673+
{
674+
"created_at": "2021-05-20T12:40:55.662949Z",
675+
"updated_at": "2021-05-20T12:40:55.662949Z",
676+
"member_id": 42,
677+
"email": "[email protected]",
678+
"answers": [
679+
{
680+
"type": "boolean",
681+
"field_ref": "actual_secret_yes_no",
682+
"field_label": "Is it an actual secret?",
683+
"boolean": True,
684+
}
685+
],
686+
}
687+
],
688+
"occurrences": None,
689+
},
690+
)
691+
692+
result = client.retrieve_secret_incident(3759)
693+
694+
assert mock_response.call_count == 1
695+
assert result.id == 3759
696+
assert result.detector.name == "slack_bot_token"
697+
assert result.ignore_reason == "test_credential"
698+
assert result.secret_revoked is False
699+
700+
619701
@responses.activate
620702
def test_rate_limit():
621703
"""

0 commit comments

Comments
 (0)