Skip to content

handle excluded policy breaks #118

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

Merged
merged 3 commits into from
Oct 21, 2024
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
<!--
A new scriv changelog fragment.

Uncomment the section that is right (remove the HTML comment wrapper).
-->

<!--
### Removed

- A bullet item for the Removed category.

-->
<!--
### Added

- A bullet item for the Added category.

-->

### Changed

- `content_scan` and `multi_content_scan` now accept `all_secrets` parameter.
- `PolicyBreak` now contains two new fields: `is_excluded` and `exclude_reason`.
<!--

### Deprecated

- A bullet item for the Deprecated category.

-->

<!--
### Fixed

- A bullet item for the Fixed category.

-->
<!--
### Security

- A bullet item for the Security category.

-->
21 changes: 16 additions & 5 deletions pygitguardian/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
"""

Expand All @@ -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]
Expand All @@ -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.
Expand All @@ -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
Expand All @@ -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,
Expand Down
8 changes: 7 additions & 1 deletion pygitguardian/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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":
Expand All @@ -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__()
Expand All @@ -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:
Expand Down
45 changes: 40 additions & 5 deletions tests/test_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
28 changes: 28 additions & 0 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
Loading