Skip to content

Commit 225c7fb

Browse files
committed
feat(detector_details): Add mathod to retrieve detector details
1 parent d246b6a commit 225c7fb

File tree

4 files changed

+245
-0
lines changed

4 files changed

+245
-0
lines changed

pygitguardian/client.py

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
CreateTeamMemberParameters,
2929
DeleteMemberParameters,
3030
Detail,
31+
DetectorDetailsResponse,
3132
Document,
3233
DocumentSchema,
3334
HealthCheckResponse,
@@ -570,6 +571,35 @@ def retrieve_secret_incident(
570571
obj.status_code = resp.status_code
571572
return obj
572573

574+
def detector_details(
575+
self,
576+
detector_name: str,
577+
extra_headers: Optional[Dict[str, str]] = None,
578+
) -> Union[Detail, DetectorDetailsResponse]:
579+
"""
580+
detector_details handles the /detectors/{detector_name} endpoint of the API
581+
582+
:param detector_name: detector name
583+
:param extra_headers: additional headers to add to the request
584+
:return: Detail or Detector response and status code
585+
"""
586+
587+
resp = self.get(
588+
endpoint=f"secret_detectors/{detector_name}",
589+
extra_headers=extra_headers,
590+
)
591+
592+
obj: Union[Detail, DetectorDetailsResponse]
593+
if is_ok(resp):
594+
print(resp.json())
595+
obj = DetectorDetailsResponse.from_dict(resp.json())
596+
else:
597+
obj = load_detail(resp)
598+
599+
obj.status_code = resp.status_code
600+
601+
return obj
602+
573603
def quota_overview(
574604
self,
575605
extra_headers: Optional[Dict[str, str]] = None,

pygitguardian/models.py

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -709,6 +709,147 @@ def __repr__(self) -> str:
709709
)
710710

711711

712+
class DetectorType(str, Enum):
713+
SPECIFIC = "specific"
714+
GENERIC = "generic"
715+
CUSTOM = "custom"
716+
717+
718+
class DetectorDetailsSchema(BaseSchema):
719+
name = fields.String(required=True)
720+
display_name = fields.String(required=True)
721+
type = fields.Enum(DetectorType, by_value=True, required=True)
722+
category = fields.String(required=True)
723+
is_active = fields.Boolean(required=True)
724+
scans_code_only = fields.Boolean(required=True)
725+
checkable = fields.Boolean(required=True)
726+
use_with_validity_check_disabled = fields.Boolean(required=True)
727+
frequency = fields.Float(required=True)
728+
removed_at = fields.String(required=False, load_default=None, dump_default=None)
729+
open_incidents_count = fields.Int(required=True)
730+
ignored_incidents_count = fields.Int(required=True)
731+
resolved_incidents_count = fields.Int(required=True)
732+
733+
@post_load
734+
def make_detector(self, data: Dict[str, Any], **kwargs: Any) -> "DetectorDetails":
735+
return DetectorDetails(**data)
736+
737+
738+
class DetectorDetails(Base, FromDictMixin):
739+
""" "
740+
Response from /v1/detectors, to retrieve a detetor details
741+
from the API
742+
{
743+
"name": "aws_iam",
744+
"display_name": "AWS Keys",
745+
"type": "specific",
746+
"category": "Cloud Provider",
747+
"is_active": true,
748+
"scans_code_only": false,
749+
"checkable": true,
750+
"use_with_validity_check_disabled": true,
751+
"frequency": "1O3.74",
752+
"removed_at": null,
753+
"open_incidents_count": 17,
754+
"ignored_incidents_count": 9,
755+
"resolved_incidents_count": 42
756+
}
757+
"""
758+
759+
SCHEMA = DetectorDetailsSchema()
760+
761+
def __init__(
762+
self,
763+
name: str,
764+
display_name: str,
765+
type: DetectorType,
766+
category: str,
767+
is_active: bool,
768+
scans_code_only: bool,
769+
checkable: bool,
770+
use_with_validity_check_disabled: bool,
771+
frequency: float,
772+
removed_at: str | None,
773+
open_incidents_count: int,
774+
ignored_incidents_count: int,
775+
resolved_incidents_count: int,
776+
**kwargs: Any,
777+
):
778+
super().__init__()
779+
self.name = name
780+
self.display_name = display_name
781+
self.type = type
782+
self.category = category
783+
self.is_active = is_active
784+
self.scans_code_only = scans_code_only
785+
self.checkable = checkable
786+
self.use_with_validity_check_disabled = use_with_validity_check_disabled
787+
self.frequency = frequency
788+
self.removed_at = removed_at
789+
self.open_incidents_count = open_incidents_count
790+
self.ignored_incidents_count = ignored_incidents_count
791+
self.resolved_incidents_count = resolved_incidents_count
792+
793+
794+
class DetectorDetailsResponseSchema(BaseSchema):
795+
name = fields.String(required=True)
796+
display_name = fields.String(required=True)
797+
type = fields.Enum(DetectorType, by_value=True, required=True)
798+
category = fields.String(required=True)
799+
is_active = fields.Boolean(required=True)
800+
scans_code_only = fields.Boolean(required=True)
801+
checkable = fields.Boolean(required=True)
802+
use_with_validity_check_disabled = fields.Boolean(required=True)
803+
frequency = fields.Float(required=True)
804+
removed_at = fields.String(required=False, load_default=None, dump_default=None)
805+
open_incidents_count = fields.Int(required=True)
806+
ignored_incidents_count = fields.Int(required=True)
807+
resolved_incidents_count = fields.Int(required=True)
808+
809+
@post_load
810+
def make_detector(self, data: Dict[str, Any], **kwargs: Any) -> "DetectorDetails":
811+
return DetectorDetails(**data)
812+
813+
814+
class DetectorDetailsResponse(Base, FromDictMixin):
815+
SCHEMA = DetectorDetailsResponseSchema()
816+
817+
def __init__(self, detector: DetectorDetails, **kwargs: Any):
818+
super().__init__()
819+
self.name = detector.name
820+
self.display_name = detector.display_name
821+
self.type = detector.type
822+
self.category = detector.category
823+
self.is_active = detector.is_active
824+
self.scans_code_only = detector.scans_code_only
825+
self.checkable = detector.checkable
826+
self.use_with_validity_check_disabled = (
827+
detector.use_with_validity_check_disabled
828+
)
829+
self.frequency = detector.frequency
830+
self.removed_at = detector.removed_at
831+
self.open_incidents_count = detector.open_incidents_count
832+
self.ignored_incidents_count = detector.ignored_incidents_count
833+
self.resolved_incidents_count = detector.resolved_incidents_count
834+
835+
def __repr__(self) -> str:
836+
return (
837+
f"name:{self.name}, "
838+
f"display_name:{self.display_name}, "
839+
f"type:{self.type}, "
840+
f"category:{self.category}, "
841+
f"is_active:{self.is_active}, "
842+
f"scans_code_only:{self.scans_code_only}, "
843+
f"checkable:{self.checkable}, "
844+
f"use_with_validity_check_disabled:{self.use_with_validity_check_disabled}, "
845+
f"frequency:{self.frequency}, "
846+
f"removed_at:{self.removed_at}, "
847+
f"open_incidents_count:{self.open_incidents_count}, "
848+
f"ignored_incidents_count:{self.ignored_incidents_count}, "
849+
f"resolved_incidents_count:{self.resolved_incidents_count}"
850+
)
851+
852+
712853
class TokenType(str, Enum):
713854
PERSONAL_ACCESS_TOKEN = "personal_access_token"
714855
SERVICE_ACCOUNT = "service_account"

tests/cassettes/detector_details.yaml

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
interactions:
2+
- request:
3+
body: null
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
User-Agent:
12+
- pygitguardian/1.20.0 (Linux;py3.10.13)
13+
method: GET
14+
uri: https://api.gitguardian.com/v1/secret_detectors/1password_service_account_token
15+
response:
16+
body:
17+
string: '{"name":"1password_service_account_token","display_name":"1Password
18+
Service Account Token","is_active":true,"type":"specific","category":"other","checkable":false,"frequency":0.02,"use_with_validity_check_disabled":true,"scans_code_only":false,"removed_at":null,"open_incidents_count":0,"ignored_incidents_count":0,"resolved_incidents_count":0}'
19+
headers:
20+
access-control-expose-headers:
21+
- X-App-Version
22+
allow:
23+
- GET, HEAD, OPTIONS
24+
content-length:
25+
- '344'
26+
content-type:
27+
- application/json
28+
cross-origin-opener-policy:
29+
- same-origin
30+
date:
31+
- Tue, 08 Apr 2025 12:51:08 GMT
32+
referrer-policy:
33+
- strict-origin-when-cross-origin
34+
server:
35+
- istio-envoy
36+
strict-transport-security:
37+
- max-age=31536000; includeSubDomains
38+
vary:
39+
- Cookie
40+
x-app-version:
41+
- v2.185.0
42+
x-content-type-options:
43+
- nosniff
44+
- nosniff
45+
x-envoy-upstream-service-time:
46+
- '38'
47+
x-frame-options:
48+
- DENY
49+
- SAMEORIGIN
50+
x-secrets-engine-version:
51+
- 2.135.3
52+
x-xss-protection:
53+
- 1; mode=block
54+
status:
55+
code: 200
56+
message: OK
57+
version: 1

tests/test_client.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
CreateTeamMemberParameters,
3434
DeleteMemberParameters,
3535
Detail,
36+
DetectorDetails,
3637
HoneytokenResponse,
3738
HoneytokenWithContextResponse,
3839
IncidentPermission,
@@ -754,6 +755,22 @@ def test_bogus_rate_limit():
754755
callbacks.on_rate_limited.assert_not_called()
755756

756757

758+
def test_detector_overview(client: GGClient):
759+
with my_vcr.use_cassette("detector_details.yaml"):
760+
detector_response = client.detector_details("1password_service_account_token")
761+
assert detector_response.status_code == 200
762+
if isinstance(detector_response, DetectorDetails):
763+
assert detector_response.name == "1password_service_account_token"
764+
assert detector_response.display_name == "1Password Service Account Token"
765+
assert detector_response.type == "specific"
766+
assert detector_response.category == "other"
767+
assert detector_response.is_active
768+
else:
769+
pytest.fail("returned should be a DetectorDetails")
770+
771+
assert type(detector_response.to_dict()) == OrderedDict
772+
773+
757774
def test_quota_overview(client: GGClient):
758775
with my_vcr.use_cassette("quota.yaml"):
759776
quota_response = client.quota_overview()

0 commit comments

Comments
 (0)