diff --git a/changelog.d/20241014_101918_mathias.millet_handle_excluded_policy_breaks.md b/changelog.d/20241014_101918_mathias.millet_handle_excluded_policy_breaks.md new file mode 100644 index 00000000..26c580f0 --- /dev/null +++ b/changelog.d/20241014_101918_mathias.millet_handle_excluded_policy_breaks.md @@ -0,0 +1,43 @@ + + + + + +### Changed + +- `content_scan` and `multi_content_scan` now accept `all_secrets` parameter. +- `PolicyBreak` now contains two new fields: `is_excluded` and `exclude_reason`. + + + + diff --git a/pygitguardian/client.py b/pygitguardian/client.py index c6162fd8..e740928c 100644 --- a/pygitguardian/client.py +++ b/pygitguardian/client.py @@ -358,6 +358,8 @@ def content_scan( document: str, filename: Optional[str] = None, extra_headers: Optional[Dict[str, str]] = None, + *, + all_secrets: Optional[bool] = None, ) -> Union[Detail, ScanResult]: """ content_scan handles the /scan endpoint of the API. @@ -368,6 +370,7 @@ def content_scan( :param filename: name of file, example: "intro.py" :param document: content of file :param extra_headers: additional headers to add to the request + :param all_secrets: indicates whether all secrets should be returned :return: Detail or ScanResult response and status code """ @@ -379,11 +382,15 @@ def content_scan( DocumentSchema.validate_size( request_obj, self.secret_scan_preferences.maximum_document_size ) + params = {} + if all_secrets is not None: + params["all_secrets"] = all_secrets resp = self.post( endpoint="scan", data=request_obj, extra_headers=extra_headers, + params=params, ) obj: Union[Detail, ScanResult] @@ -401,6 +408,8 @@ def multi_content_scan( documents: List[Dict[str, str]], extra_headers: Optional[Dict[str, str]] = None, ignore_known_secrets: Optional[bool] = None, + *, + all_secrets: Optional[bool] = None, ) -> Union[Detail, MultiScanResult]: """ multi_content_scan handles the /multiscan endpoint of the API. @@ -413,6 +422,7 @@ def multi_content_scan( example: [{"document":"example content","filename":"intro.py"}] :param extra_headers: additional headers to add to the request :param ignore_known_secrets: indicates whether known secrets should be ignored + :param all_secrets: indicates whether all secrets should be returned :return: Detail or ScanResult response and status code """ max_documents = self.secret_scan_preferences.maximum_documents_per_scan @@ -433,11 +443,12 @@ def multi_content_scan( document, self.secret_scan_preferences.maximum_document_size ) - params = ( - {"ignore_known_secrets": ignore_known_secrets} - if ignore_known_secrets - else {} - ) + params = {} + if ignore_known_secrets is not None: + params["ignore_known_secrets"] = ignore_known_secrets + if all_secrets is not None: + params["all_secrets"] = all_secrets + resp = self.post( endpoint="multiscan", data=request_obj, diff --git a/pygitguardian/models.py b/pygitguardian/models.py index 0d8b4196..bc10c4d3 100644 --- a/pygitguardian/models.py +++ b/pygitguardian/models.py @@ -260,8 +260,10 @@ class PolicyBreakSchema(BaseSchema): policy = fields.String(required=True) validity = fields.String(required=False, load_default=None, dump_default=None) known_secret = fields.Boolean(required=False, load_default=False, dump_default=None) - incident_url = fields.String(required=False, load_default=False, dump_default=None) + incident_url = fields.String(required=False, load_default=None, dump_default=None) matches = fields.List(fields.Nested(MatchSchema), required=True) + is_excluded = fields.Boolean(required=False, load_default=False, dump_default=False) + exclude_reason = fields.String(required=False, load_default=None, dump_default=None) @post_load def make_policy_break(self, data: Dict[str, Any], **kwargs: Any) -> "PolicyBreak": @@ -286,6 +288,8 @@ def __init__( matches: List[Match], known_secret: bool = False, incident_url: Optional[str] = None, + is_excluded: bool = False, + exclude_reason: Optional[str] = None, **kwargs: Any, ) -> None: super().__init__() @@ -295,6 +299,8 @@ def __init__( self.known_secret = known_secret self.incident_url = incident_url self.matches = matches + self.is_excluded = is_excluded + self.exclude_reason = exclude_reason @property def is_secret(self) -> bool: diff --git a/tests/test_client.py b/tests/test_client.py index 071ad61f..cfcd68c3 100644 --- a/tests/test_client.py +++ b/tests/test_client.py @@ -575,19 +575,53 @@ def test_extra_headers( @responses.activate -def test_multiscan_parameters( - client: GGClient, -): +@pytest.mark.parametrize("all_secrets", (None, True, False)) +def test_scan_parameters(client: GGClient, all_secrets): + """ + GIVEN a ggclient + WHEN calling content_scan with parameters + THEN the parameters are passed in the request + """ + + to_match = {} + if all_secrets is not None: + to_match["all_secrets"] = all_secrets + + mock_response = responses.post( + url=client._url_from_endpoint("scan", "v1"), + status=200, + match=[matchers.query_param_matcher(to_match)], + ) + + client.content_scan( + DOCUMENT, + FILENAME, + all_secrets=all_secrets, + ) + + assert mock_response.call_count == 1 + + +@responses.activate +@pytest.mark.parametrize("ignore_known_secrets", (None, True, False)) +@pytest.mark.parametrize("all_secrets", (None, True, False)) +def test_multiscan_parameters(client: GGClient, ignore_known_secrets, all_secrets): """ GIVEN a ggclient WHEN calling multi_content_scan with parameters THEN the parameters are passed in the request """ + to_match = {} + if ignore_known_secrets is not None: + to_match["ignore_known_secrets"] = ignore_known_secrets + if all_secrets is not None: + to_match["all_secrets"] = all_secrets + mock_response = responses.post( url=client._url_from_endpoint("multiscan", "v1"), status=200, - match=[matchers.query_param_matcher({"ignore_known_secrets": True})], + match=[matchers.query_param_matcher(to_match)], json=[ { "policy_break_count": 1, @@ -610,7 +644,8 @@ def test_multiscan_parameters( client.multi_content_scan( [{"filename": FILENAME, "document": DOCUMENT}], - ignore_known_secrets=True, + ignore_known_secrets=ignore_known_secrets, + all_secrets=all_secrets, ) assert mock_response.call_count == 1 diff --git a/tests/test_models.py b/tests/test_models.py index 1c1acca9..dbddde79 100644 --- a/tests/test_models.py +++ b/tests/test_models.py @@ -111,6 +111,34 @@ def test_document_handle_surrogates(self): "matches": [{"match": "hello", "type": "hello"}], }, ), + ( + PolicyBreakSchema, + PolicyBreak, + { + "type": "hello", + "policy": "hello", + "validity": "hey", + "known_secret": True, + "incident_url": "https://api.gitguardian.com/workspace/2/incidents/3", + "matches": [{"match": "hello", "type": "hello"}], + "is_excluded": True, + "exclude_reason": "bad secret", + }, + ), + ( + PolicyBreakSchema, + PolicyBreak, + { + "type": "hello", + "policy": "hello", + "validity": "hey", + "known_secret": True, + "incident_url": "https://api.gitguardian.com/workspace/2/incidents/3", + "matches": [{"match": "hello", "type": "hello"}], + "is_excluded": False, + "exclude_reason": None, + }, + ), ( QuotaSchema, Quota,