diff --git a/ggshield/verticals/secret/output/secret_text_output_handler.py b/ggshield/verticals/secret/output/secret_text_output_handler.py index 8d5e08c212..c488084941 100644 --- a/ggshield/verticals/secret/output/secret_text_output_handler.py +++ b/ggshield/verticals/secret/output/secret_text_output_handler.py @@ -3,7 +3,7 @@ from typing import Dict, List, Optional, Tuple from pygitguardian.client import VERSIONS -from pygitguardian.models import PolicyBreak +from pygitguardian.models import DiffKind, PolicyBreak from ggshield.core.filter import group_policy_breaks_by_ignore_sha from ggshield.core.lines import Line, get_offset, get_padding @@ -31,17 +31,29 @@ class SecretTextOutputHandler(SecretOutputHandler): def _process_scan_impl(self, scan: SecretScanCollection) -> str: """Output Secret Scan Collection in text format""" - processed_scan_results = self.process_scan_results(scan) + diff_kinds = [DiffKind.ADDITION] + if self.verbose: + diff_kinds += [DiffKind.DELETION, DiffKind.CONTEXT] + processed_scan_results = self.process_scan_results(scan, diff_kinds=diff_kinds) scan_buf = StringIO() if self.verbose: scan_buf.write(secrets_engine_version()) scan_buf.write(processed_scan_results) if not processed_scan_results: + if self.ignore_known_secrets and scan.known_secrets_count: + scan_buf.write(no_new_leak_message()) + elif scan.deletion_or_context_secrets_count: + scan_buf.write(no_introduced_leak_message()) + else: + scan_buf.write(no_leak_message()) + + if not self.verbose and scan.deletion_or_context_secrets_count: scan_buf.write( - no_new_leak_message() - if (self.ignore_known_secrets and scan.known_secrets_count) - else no_leak_message() + f"\nWarning: {scan.deletion_or_context_secrets_count} " + f"{pluralize('secret', scan.deletion_or_context_secrets_count)} ignored " + f"because {pluralize('it is', scan.deletion_or_context_secrets_count, 'they are')} removed or in the " + f"context of a commit.\nUse `--verbose` for more details.\n" ) if self.ignore_known_secrets and scan.known_secrets_count > 0: @@ -52,14 +64,21 @@ def _process_scan_impl(self, scan: SecretScanCollection) -> str: ) if self.verbose: - scan_buf.write(self.process_scan_results(scan, True)) + scan_buf.write( + self.process_scan_results( + scan, diff_kinds=diff_kinds, show_only_known_secrets=True + ) + ) else: scan_buf.write("Use `--verbose` for more details.\n") return scan_buf.getvalue() def process_scan_results( - self, scan: SecretScanCollection, show_only_known_secrets: bool = False + self, + scan: SecretScanCollection, + diff_kinds: List[DiffKind], + show_only_known_secrets: bool = False, ) -> str: """Iterate through the scans and sub-scan results to prepare the display.""" results_buf = StringIO() @@ -67,7 +86,7 @@ def process_scan_results( current_result_buf = StringIO() for result in scan.results.results: current_result_buf.write( - self.process_result(result, show_only_known_secrets) + self.process_result(result, diff_kinds, show_only_known_secrets) ) current_result_string = current_result_buf.getvalue() @@ -80,14 +99,17 @@ def process_scan_results( if scan.scans: for sub_scan in scan.scans: inner_scan_str = self.process_scan_results( - sub_scan, show_only_known_secrets + sub_scan, diff_kinds, show_only_known_secrets ) results_buf.write(inner_scan_str) return results_buf.getvalue() def process_result( - self, result: Result, show_only_known_secrets: bool = False + self, + result: Result, + diff_kinds: List[DiffKind], + show_only_known_secrets: bool = False, ) -> str: """ Build readable message on the found incidents. @@ -99,7 +121,9 @@ def process_result( """ result_buf = StringIO() - sha_dict = group_policy_breaks_by_ignore_sha(result.scan.policy_breaks) + sha_dict = group_policy_breaks_by_ignore_sha( + result.policy_breaks(diff_kinds=diff_kinds) + ) if not self.show_secrets: result.censor() @@ -306,6 +330,13 @@ def no_new_leak_message() -> str: return format_text("\nNo new secrets have been found\n", STYLE["no_secret"]) +def no_introduced_leak_message() -> str: + """ + Build a message if no secrets are introduced. + """ + return format_text("\nNo introduced secrets have been found\n", STYLE["no_secret"]) + + def format_line_with_secret( line_content: str, secret_index_start: int, diff --git a/ggshield/verticals/secret/secret_scan_collection.py b/ggshield/verticals/secret/secret_scan_collection.py index 0ba6815e35..1b2f52810b 100644 --- a/ggshield/verticals/secret/secret_scan_collection.py +++ b/ggshield/verticals/secret/secret_scan_collection.py @@ -3,7 +3,14 @@ from typing import Any, Dict, Iterable, List, NamedTuple, Optional, Tuple, Union, cast from pygitguardian import GGClient -from pygitguardian.models import Detail, DiffKind, Match, ScanResult, SecretIncident +from pygitguardian.models import ( + Detail, + DiffKind, + Match, + PolicyBreak, + ScanResult, + SecretIncident, +) from ggshield.core.errors import UnexpectedError, handle_api_error from ggshield.core.filter import group_policy_breaks_by_ignore_sha @@ -70,6 +77,18 @@ def censor(self) -> None: def has_policy_breaks(self) -> bool: return self.scan.has_policy_breaks + def policy_breaks( + self, *, diff_kinds: Optional[List[DiffKind]] = None + ) -> List[PolicyBreak]: + policy_breaks = self.scan.policy_breaks + if diff_kinds is not None and self.scan.is_diff: + policy_breaks = [ + policy_break + for policy_break in self.scan.policy_breaks + if policy_break.diff_kind in diff_kinds + ] + return policy_breaks + class Error(NamedTuple): files: List[Tuple[str, Filemode]] @@ -133,6 +152,9 @@ def __init__( self.known_secrets_count, self.new_secrets_count, ) = self._get_known_new_secrets_count() + self.deletion_or_context_secrets_count = ( + self._get_deletion_or_context_secrets_count() + ) @property def has_new_secrets(self) -> bool: @@ -163,6 +185,16 @@ def introduces_secret(self) -> bool: return True return False + def _get_deletion_or_context_secrets_count(self) -> int: + deletion_or_context_secrets = 0 + for result in self.get_all_results(): + if not result.scan.is_diff: + continue + for policy_break in result.scan.policy_breaks: + if policy_break.diff_kind in [DiffKind.DELETION, DiffKind.CONTEXT]: + deletion_or_context_secrets += 1 + return deletion_or_context_secrets + def _get_known_new_secrets_count(self) -> Tuple[int, int]: policy_breaks = [] for result in self.get_all_results():