Skip to content

Commit 8e9748d

Browse files
committed
feat(client): add sources related endpoints
1 parent d322eaa commit 8e9748d

File tree

8 files changed

+607
-12
lines changed

8 files changed

+607
-12
lines changed

pygitguardian/client.py

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,14 +48,18 @@
4848
SecretIncident,
4949
SecretScanPreferences,
5050
ServerMetadata,
51+
Source,
52+
SourceParameters,
5153
Team,
5254
TeamInvitation,
5355
TeamInvitationParameter,
5456
TeamMember,
5557
TeamMemberParameter,
58+
TeamSourceParameters,
5659
TeamsParameter,
5760
UpdateMember,
5861
UpdateTeam,
62+
UpdateTeamSource,
5963
)
6064
from .sca_models import (
6165
ComputeSCAFilesResult,
@@ -1213,3 +1217,64 @@ def delete_team_member(
12131217
return response.status_code
12141218

12151219
return load_detail(response)
1220+
1221+
def list_sources(
1222+
self,
1223+
parameters: Optional[SourceParameters] = None,
1224+
extra_headers: Optional[Dict[str, str]] = None,
1225+
) -> Union[Detail, CursorPaginatedResponse[Source]]:
1226+
response = self.get(
1227+
endpoint="sources",
1228+
data=parameters.to_dict() if parameters else {},
1229+
extra_headers=extra_headers,
1230+
)
1231+
1232+
obj: Union[Detail, CursorPaginatedResponse[Source]]
1233+
if is_ok(response):
1234+
obj = CursorPaginatedResponse[Source].from_response(response, Source)
1235+
else:
1236+
obj = load_detail(response)
1237+
1238+
obj.status_code
1239+
return obj
1240+
1241+
def list_teams_sources(
1242+
self,
1243+
team_id: int,
1244+
parameters: Optional[TeamSourceParameters] = None,
1245+
extra_headers: Optional[Dict[str, str]] = None,
1246+
) -> Union[Detail, CursorPaginatedResponse[Source]]:
1247+
response = self.get(
1248+
endpoint=f"teams/{team_id}/sources",
1249+
data=parameters.to_dict() if parameters else {},
1250+
extra_headers=extra_headers,
1251+
)
1252+
1253+
obj: Union[Detail, CursorPaginatedResponse[Source]]
1254+
if is_ok(response):
1255+
obj = CursorPaginatedResponse[Source].from_response(response, Source)
1256+
else:
1257+
obj = load_detail(response)
1258+
1259+
obj.status_code
1260+
return obj
1261+
1262+
def update_team_source(
1263+
self,
1264+
team_sources: UpdateTeamSource,
1265+
extra_headers: Optional[Dict[str, str]] = None,
1266+
) -> Union[Detail, int]:
1267+
team_id = team_sources.team_id
1268+
data = team_sources.to_dict()
1269+
del data["team_id"]
1270+
1271+
response = self.post(
1272+
endpoint=f"teams/{team_id}/sources",
1273+
data=data,
1274+
extra_headers=extra_headers,
1275+
)
1276+
1277+
if response.status_code == 204:
1278+
return 204
1279+
1280+
return load_detail(response)

pygitguardian/config.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
DEFAULT_BASE_URI = "http://localhost:3000/exposed"
1+
DEFAULT_BASE_URI = "https://api.gitguardian.com"
22
DEFAULT_API_VERSION = "v1"
33
DEFAULT_TIMEOUT = 20.0 # 20s default timeout
44

pygitguardian/models.py

Lines changed: 101 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -982,25 +982,70 @@ class Feedback(Base, FromDictMixin):
982982
answers: List[Answer]
983983

984984

985+
@dataclass
986+
class SecretIncidentStats(Base, FromDictMixin):
987+
total: int
988+
severity_breakdown: dict[Severity, int]
989+
990+
991+
@dataclass
992+
class SecretIncidentsBreakdown(Base, FromDictMixin):
993+
open_secret_incidents: SecretIncidentStats
994+
closed_secret_incidents: SecretIncidentStats
995+
996+
997+
ScanStatus = Literal[
998+
"pending",
999+
"running",
1000+
"canceled",
1001+
"failed",
1002+
"too_large",
1003+
"timeout",
1004+
"pending_timeout",
1005+
"finished",
1006+
]
1007+
1008+
1009+
@dataclass
1010+
class Scan(Base, FromDictMixin):
1011+
date: datetime
1012+
status: ScanStatus
1013+
failing_reason: str
1014+
commits_scanned: int
1015+
branches_scanned: int
1016+
duration: str
1017+
1018+
1019+
SourceHealth = Literal["safe", "unknown", "at_risk"]
1020+
SourceCriticality = Literal["critical", "high", "medium", "low", "unknown"]
1021+
1022+
9851023
@dataclass
9861024
class Source(Base, FromDictMixin):
9871025
id: int
9881026
url: str
9891027
type: str
9901028
full_name: str
991-
health: Literal["safe", "unknown", "at_risk"]
1029+
health: SourceHealth
9921030
default_branch: Optional[str]
9931031
default_branch_head: Optional[str]
9941032
open_incidents_count: int
9951033
closed_incidents_count: int
996-
secret_incidents_breakdown: Dict[str, Any] # TODO: add SecretIncidentsBreakdown
1034+
secret_incidents_breakdown: SecretIncidentsBreakdown
9971035
visibility: Visibility
9981036
external_id: str
999-
source_criticality: str
1000-
last_scan: Optional[Dict[str, Any]] # TODO: add LastScan
1037+
source_criticality: SourceCriticality
1038+
last_scan: Scan
10011039
monitored: bool
10021040

10031041

1042+
SourceSchema = cast(
1043+
Type[BaseSchema],
1044+
marshmallow_dataclass.class_schema(Source, base_schema=BaseSchema),
1045+
)
1046+
Source.SCHEMA = SourceSchema()
1047+
1048+
10041049
@dataclass
10051050
class OccurrenceMatch(Base, FromDictMixin):
10061051
"""
@@ -1103,14 +1148,14 @@ class AccessLevel(str, Enum):
11031148
RESTRICTED = "restricted"
11041149

11051150

1106-
class PaginationParameter(Base, FromDictMixin):
1151+
class PaginationParameter(ToDictMixin):
11071152
"""Pagination mixin used for endpoints that support pagination."""
11081153

11091154
cursor: str = ""
11101155
per_page: int = 20
11111156

11121157

1113-
class SearchParameter(Base, FromDictMixin):
1158+
class SearchParameter(ToDictMixin):
11141159
search: Optional[str] = None
11151160

11161161

@@ -1142,7 +1187,7 @@ def from_response(
11421187

11431188

11441189
@dataclass
1145-
class MembersParameters(PaginationParameter, SearchParameter, Base, FromDictMixin):
1190+
class MembersParameters(PaginationParameter, SearchParameter, ToDictMixin):
11461191
"""
11471192
Members query parameters
11481193
"""
@@ -1239,7 +1284,7 @@ class DeleteMember(Base, FromDictMixin):
12391284
DeleteMember.SCHEMA = DeleteMemberSchema()
12401285

12411286

1242-
class TeamsParameter(PaginationParameter, SearchParameter, Base, FromDictMixin):
1287+
class TeamsParameter(PaginationParameter, SearchParameter, ToDictMixin):
12431288
is_global: Optional[bool] = None
12441289

12451290

@@ -1311,7 +1356,7 @@ class IncidentPermission(str, Enum):
13111356

13121357

13131358
@dataclass
1314-
class TeamInvitationParameter(PaginationParameter, Base, FromDictMixin):
1359+
class TeamInvitationParameter(PaginationParameter, ToDictMixin):
13151360
invitation_id: Optional[int] = None
13161361
is_team_leader: Optional[bool] = None
13171362
incident_permission: Optional[IncidentPermission] = None
@@ -1387,7 +1432,7 @@ class Meta:
13871432
CreateTeamInvitation.SCHEMA = CreateTeamInvitationSchema()
13881433

13891434

1390-
class TeamMemberParameter(PaginationParameter, SearchParameter, Base, FromDictMixin):
1435+
class TeamMemberParameter(PaginationParameter, SearchParameter, ToDictMixin):
13911436
is_team_leader: Optional[bool] = None
13921437
incident_permission: Optional[IncidentPermission] = None
13931438
member_id: Optional[int] = None
@@ -1440,7 +1485,7 @@ def return_team_membership(self, data: dict[str, Any], **kwargs: dict[str, Any])
14401485

14411486

14421487
@dataclass
1443-
class CreateTeamMemberParameter(Base, FromDictMixin):
1488+
class CreateTeamMemberParameter(ToDictMixin):
14441489
send_email: bool
14451490

14461491

@@ -1475,3 +1520,48 @@ def return_create_team_membership(
14751520

14761521

14771522
CreateTeamMember.SCHEMA = CreateTeamMemberSchema()
1523+
1524+
1525+
@dataclass
1526+
class TeamSourceParameters(PaginationParameter, SearchParameter, ToDictMixin):
1527+
last_scan_status: Optional[ScanStatus] = None
1528+
type: Optional[str] = None
1529+
health: Optional[SourceHealth] = None
1530+
type: Optional[str] = None
1531+
ordering: Optional[Literal["last_scan_date", "-last_scan_date"]] = None
1532+
visibility: Optional[Visibility] = None
1533+
external_id: Optional[str] = None
1534+
1535+
1536+
TeamSourceParametersSchema = cast(
1537+
Type[BaseSchema],
1538+
marshmallow_dataclass.class_schema(TeamSourceParameters, base_schema=BaseSchema),
1539+
)
1540+
TeamSourceParameters.SCHEMA = TeamSourceParametersSchema()
1541+
1542+
1543+
@dataclass
1544+
class UpdateTeamSource(Base, FromDictMixin):
1545+
team_id: int
1546+
sources_to_add: list[int]
1547+
sources_to_remove: list[int]
1548+
1549+
1550+
UpdateTeamSourceSchema = cast(
1551+
Type[BaseSchema],
1552+
marshmallow_dataclass.class_schema(UpdateTeamSource, base_schema=BaseSchema),
1553+
)
1554+
UpdateTeamSource.SCHEMA = UpdateTeamSourceSchema()
1555+
1556+
1557+
@dataclass
1558+
class SourceParameters(TeamSourceParameters):
1559+
source_criticality: Optional[SourceCriticality] = None
1560+
monitored: Optional[bool] = None
1561+
1562+
1563+
SourceParametersSchema = cast(
1564+
Type[BaseSchema],
1565+
marshmallow_dataclass.class_schema(SourceParameters, base_schema=BaseSchema),
1566+
)
1567+
SourceParameters.SCHEMA = SourceParametersSchema()
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
interactions:
2+
- request:
3+
body: '{"sources_to_add": [126], "sources_to_remove": []}'
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Content-Length:
12+
- '50'
13+
Content-Type:
14+
- application/json
15+
User-Agent:
16+
- pygitguardian/1.18.0 (Darwin;py3.11.8)
17+
method: POST
18+
uri: https://api.gitguardian.com/v1/teams/9/sources
19+
response:
20+
body:
21+
string: ''
22+
headers:
23+
Access-Control-Expose-Headers:
24+
- X-App-Version
25+
Allow:
26+
- GET, POST, HEAD, OPTIONS
27+
Connection:
28+
- keep-alive
29+
Content-Length:
30+
- '0'
31+
Cross-Origin-Opener-Policy:
32+
- same-origin
33+
Date:
34+
- Fri, 06 Dec 2024 12:46:14 GMT
35+
Referrer-Policy:
36+
- same-origin
37+
Server:
38+
- nginx/1.24.0
39+
Vary:
40+
- Cookie
41+
X-App-Version:
42+
- dev
43+
X-Content-Type-Options:
44+
- nosniff
45+
X-Frame-Options:
46+
- DENY
47+
X-Request-ID:
48+
- 05e58838d26c922add8312a362dba69c
49+
X-SCA-Engine-Version:
50+
- 2.2.0
51+
X-Secrets-Engine-Version:
52+
- 2.127.0
53+
status:
54+
code: 204
55+
message: No Content
56+
- request:
57+
body: type=azure_devops
58+
headers:
59+
Accept:
60+
- '*/*'
61+
Accept-Encoding:
62+
- gzip, deflate
63+
Connection:
64+
- keep-alive
65+
Content-Length:
66+
- '17'
67+
Content-Type:
68+
- application/x-www-form-urlencoded
69+
User-Agent:
70+
- pygitguardian/1.18.0 (Darwin;py3.11.8)
71+
method: GET
72+
uri: https://api.gitguardian.com/v1/teams/9/sources
73+
response:
74+
body:
75+
string: '[{"id":124,"type":"azure_devops","full_name":"gg-integration-test /
76+
gg-test / default_branch","health":"at_risk","source_criticality":"unknown","default_branch":"test","default_branch_head":null,"open_incidents_count":19,"closed_incidents_count":0,"last_scan":{"date":"2024-12-06T12:27:52.346395Z","status":"finished","failing_reason":"","commits_scanned":23,"duration":"0.437131","branches_scanned":1,"progress":100},"monitored":true,"visibility":"private","external_id":"05b69081-f346-4022-8784-198f50aed182","secret_incidents_breakdown":{"open_secret_incidents":{"total":19,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":19}},"closed_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}}},"url":"https://dev.azure.com/gg-integration-test/gg-test/_git/default_branch"},{"id":125,"type":"azure_devops","full_name":"gg-integration-test
77+
/ gg-test / gg-test","health":"at_risk","source_criticality":"unknown","default_branch":"main","default_branch_head":null,"open_incidents_count":19,"closed_incidents_count":0,"last_scan":{"date":"2024-12-06T12:27:52.346418Z","status":"finished","failing_reason":"","commits_scanned":43,"duration":"0.492489","branches_scanned":1,"progress":100},"monitored":true,"visibility":"private","external_id":"7655868e-bb15-47ab-bd62-abd97212e5e8","secret_incidents_breakdown":{"open_secret_incidents":{"total":19,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":19}},"closed_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}}},"url":"https://dev.azure.com/gg-integration-test/gg-test/_git/gg-test"},{"id":126,"type":"azure_devops","full_name":"gg-integration-test
78+
/ gg-test / huge_repo","health":"safe","source_criticality":"unknown","default_branch":"master","default_branch_head":null,"open_incidents_count":0,"closed_incidents_count":0,"last_scan":{"date":"2024-12-06T12:27:52.346442Z","status":"finished","failing_reason":"","commits_scanned":1007,"duration":"1.102781","branches_scanned":1,"progress":100},"monitored":true,"visibility":"private","external_id":"455c4e0c-6dc3-48ce-a0e7-819d9a8d7523","secret_incidents_breakdown":{"open_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}},"closed_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}}},"url":"https://dev.azure.com/gg-integration-test/gg-test/_git/huge_repo"},{"id":127,"type":"azure_devops","full_name":"gg-integration-test
79+
/ gg-test / new_repo","health":"safe","source_criticality":"unknown","default_branch":"master","default_branch_head":null,"open_incidents_count":0,"closed_incidents_count":0,"last_scan":{"date":"2024-12-06T12:27:52.346464Z","status":"finished","failing_reason":"","commits_scanned":1,"duration":"0.216094","branches_scanned":1,"progress":100},"monitored":true,"visibility":"private","external_id":"f9b583fb-dcfd-46ec-8938-44b427d3e596","secret_incidents_breakdown":{"open_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}},"closed_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}}},"url":"https://dev.azure.com/gg-integration-test/gg-test/_git/new_repo"},{"id":128,"type":"azure_devops","full_name":"gg-integration-test
80+
/ gg-test / t e s t i n g","health":"at_risk","source_criticality":"unknown","default_branch":"main","default_branch_head":null,"open_incidents_count":110,"closed_incidents_count":0,"last_scan":{"date":"2024-12-06T12:27:52.346299Z","status":"finished","failing_reason":"","commits_scanned":113,"duration":"7.597231","branches_scanned":2,"progress":100},"monitored":true,"visibility":"private","external_id":"8a132329-0c77-4efc-a18a-882bda6ab28b","secret_incidents_breakdown":{"open_secret_incidents":{"total":110,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":110}},"closed_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}}},"url":"https://dev.azure.com/gg-integration-test/gg-test/_git/t%20e%20s%20t%20i%20n%20g"},{"id":129,"type":"azure_devops","full_name":"gg-integration-test
81+
/ gg-test / test abc","health":"at_risk","source_criticality":"unknown","default_branch":"main","default_branch_head":null,"open_incidents_count":28,"closed_incidents_count":0,"last_scan":{"date":"2024-12-06T12:27:52.346367Z","status":"finished","failing_reason":"","commits_scanned":55,"duration":"0.623082","branches_scanned":1,"progress":100},"monitored":true,"visibility":"private","external_id":"2aa16b64-2fb2-4639-afeb-70c0c4e8a267","secret_incidents_breakdown":{"open_secret_incidents":{"total":28,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":28}},"closed_secret_incidents":{"total":0,"severity_breakdown":{"critical":0,"high":0,"medium":0,"low":0,"info":0,"unknown":0}}},"url":"https://dev.azure.com/gg-integration-test/gg-test/_git/test%20abc"}]'
82+
headers:
83+
Access-Control-Expose-Headers:
84+
- X-App-Version
85+
Allow:
86+
- GET, POST, HEAD, OPTIONS
87+
Connection:
88+
- keep-alive
89+
Content-Length:
90+
- '5158'
91+
Content-Type:
92+
- application/json
93+
Cross-Origin-Opener-Policy:
94+
- same-origin
95+
Date:
96+
- Fri, 06 Dec 2024 12:46:14 GMT
97+
Link:
98+
- ''
99+
Referrer-Policy:
100+
- same-origin
101+
Server:
102+
- nginx/1.24.0
103+
Vary:
104+
- Cookie
105+
X-App-Version:
106+
- dev
107+
X-Content-Type-Options:
108+
- nosniff
109+
X-Frame-Options:
110+
- DENY
111+
X-Per-Page:
112+
- '20'
113+
X-Request-ID:
114+
- 235112590e3f298f700971bb283acb6c
115+
X-SCA-Engine-Version:
116+
- 2.2.0
117+
X-Secrets-Engine-Version:
118+
- 2.127.0
119+
status:
120+
code: 200
121+
message: OK
122+
version: 1

0 commit comments

Comments
 (0)