from __future__ import annotations from dataclasses import dataclass from vuln_scan.github.repo_client import GitHubRepoClient class StatusPublisher: def set_status(self, state: str) -> None: raise NotImplementedError def comment(self, body: str, dependabot_url: str, has_blocking: bool) -> None: raise NotImplementedError @dataclass class NullPublisher(StatusPublisher): def set_status(self, state: str) -> None: """ No-op implementation used in local/dev environments. We intentionally do not publish commit statuses when running locally to avoid unnecessary GitHub API calls. """ def comment(self, body: str, dependabot_url: str, has_blocking: bool) -> None: """ No-op implementation used in local/dev environments. Prevents posting PR comments during local execution. """ @dataclass class GitHubPRPublisher(GitHubRepoClient, StatusPublisher): pr_number: int commit_sha: str build_url: str context: str = "GitHub Vulnerability Scanner" def __post_init__(self) -> None: super().__post_init__() self._pr = self._repo.get_pull(self.pr_number) self._commit = self._repo.get_commit(self.commit_sha) def set_status(self, state: str) -> None: self._commit.create_status( state=state, target_url=self.build_url, description="Jenkins Build of GitHub Vulnerability Scanner", context=self.context, ) def comment(self, body: str, dependabot_url: str, has_blocking: bool) -> None: if has_blocking: icon = "🚨" level = "ERROR" summary = "Blocking vulnerabilities found. These must be resolved before merging." else: icon = "⚠️" level = "WARNING" summary = "Non-blocking vulnerabilities found. Please review and plan to remediate." formatted_body = self._format_body(body) final = ( f"## {icon} GitHub Vulnerability Scanner — {level}\n\n" f"{summary}\n\n" f"{formatted_body}\n\n" f"🔗 [View Dependabot Alerts]({dependabot_url})" ) self._pr.create_issue_comment(final) @staticmethod def _format_body(body: str) -> str: """ Format the reporter output for a PR comment. - Markdown content (starting with | or ✅) is left as-is. - ASCII table content is wrapped in a code fence, with summary text placed outside as bold Markdown. """ stripped = body.strip() # Markdown reporter output — already PR-ready if stripped.startswith("|") or stripped.startswith("✅") or stripped.startswith("**"): return stripped # PrettyTable output — may contain \x00 delimiter if "\x00" in stripped: table_part, summary_part = stripped.split("\x00", 1) return f"```\n{table_part.strip()}\n```\n\n**{summary_part.strip()}**" return f"```\n{stripped}\n```"