diff --git a/README.md b/README.md index 0e95f6a..148c6a5 100644 --- a/README.md +++ b/README.md @@ -81,6 +81,7 @@ If you don't want to provide the Socket API Token every time then you can use th The Python CLI currently Supports the following plugins: - Jira +- Slack ##### Jira @@ -95,6 +96,18 @@ Example `SOCKET_JIRA_CONFIG_JSON` value {"url": "https://REPLACE_ME.atlassian.net", "email": "example@example.com", "api_token": "REPLACE_ME", "project": "REPLACE_ME" } ```` +##### Slack + +| Environment Variable | Required | Default | Description | +|:-------------------------|:---------|:--------|:-----------------------------------| +| SOCKET_SLACK_ENABLED | False | false | Enables/Disables the Slack Plugin | +| SOCKET_SLACK_CONFIG_JSON | True | None | Required if the Plugin is enabled. | + +Example `SOCKET_SLACK_CONFIG_JSON` value + +````json +{"url": "https://REPLACE_ME_WEBHOOK"} +```` ## File Selection Behavior diff --git a/pyproject.toml b/pyproject.toml index 028ddae..243b608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.0.50" +version = "2.0.51" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 45c32f5..8b2cf1e 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,2 +1,2 @@ __author__ = 'socket.dev' -__version__ = '2.0.50' +__version__ = '2.0.51' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 42b4684..007eae2 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -52,6 +52,7 @@ class CliConfig: include_module_folders: bool = False version: str = __version__ jira_plugin: PluginConfig = field(default_factory=PluginConfig) + slack_plugin: PluginConfig = field(default_factory=PluginConfig) @classmethod def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': @@ -100,6 +101,11 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': enabled=os.getenv("SOCKET_JIRA_ENABLED", "false").lower() == "true", levels=os.getenv("SOCKET_JIRA_LEVELS", "block,warn").split(","), config=get_plugin_config_from_env("SOCKET_JIRA") + ), + "slack_plugin": PluginConfig( + enabled=os.getenv("SOCKET_SLACK_ENABLED", "false").lower() == "true", + levels=os.getenv("SOCKET_SLACK_LEVELS", "block,warn").split(","), + config=get_plugin_config_from_env("SOCKET_SLACK") ) }) diff --git a/socketsecurity/output.py b/socketsecurity/output.py index 61b8f76..2b523d5 100644 --- a/socketsecurity/output.py +++ b/socketsecurity/output.py @@ -6,13 +6,14 @@ from .core.classes import Diff, Issue from .config import CliConfig from socketsecurity.plugins.manager import PluginManager +from socketdev import socketdev class OutputHandler: config: CliConfig logger: logging.Logger - def __init__(self, config: CliConfig): + def __init__(self, config: CliConfig, sdk: socketdev): self.config = config self.logger = logging.getLogger("socketcli") @@ -24,16 +25,23 @@ def handle_output(self, diff_report: Diff) -> None: self.output_console_sarif(diff_report, self.config.sbom_file) else: self.output_console_comments(diff_report, self.config.sbom_file) - if hasattr(self.config, "jira_plugin") and self.config.jira_plugin.enabled: + if self.config.jira_plugin.enabled: jira_config = { "enabled": self.config.jira_plugin.enabled, "levels": self.config.jira_plugin.levels or [], **(self.config.jira_plugin.config or {}) } - plugin_mgr = PluginManager({"jira": jira_config}) + plugin_mgr.send(diff_report, config=self.config) + + if self.config.slack_plugin.enabled: + slack_config = { + "enabled": self.config.slack_plugin.enabled, + "levels": self.config.slack_plugin.levels or [], + **(self.config.slack_plugin.config or {}) + } - # The Jira plugin knows how to build title + description from diff/config + plugin_mgr = PluginManager({"slack": slack_config}) plugin_mgr.send(diff_report, config=self.config) self.save_sbom_file(diff_report, self.config.sbom_file) diff --git a/socketsecurity/plugins/base.py b/socketsecurity/plugins/base.py index 3aac71c..3aba645 100644 --- a/socketsecurity/plugins/base.py +++ b/socketsecurity/plugins/base.py @@ -2,5 +2,5 @@ class Plugin: def __init__(self, config): self.config = config - def send(self, message, level): + def send(self, diff, config): raise NotImplementedError("Plugin must implement send()") \ No newline at end of file diff --git a/socketsecurity/plugins/slack.py b/socketsecurity/plugins/slack.py index bcd6efb..2ab60c3 100644 --- a/socketsecurity/plugins/slack.py +++ b/socketsecurity/plugins/slack.py @@ -1,12 +1,83 @@ -from .base import Plugin +import logging import requests +from config import CliConfig +from .base import Plugin +from socketsecurity.core.classes import Diff +from socketsecurity.core.messages import Messages + +logger = logging.getLogger(__name__) + class SlackPlugin(Plugin): - def send(self, message, level): + @staticmethod + def get_name(): + return "slack" + + def send(self, diff, config: CliConfig): if not self.config.get("enabled", False): return - if level not in self.config.get("levels", ["block", "warn"]): + if not self.config.get("url"): + logger.warning("Slack webhook URL not configured.") + return + else: + url = self.config.get("url") + + if not diff.new_alerts: + logger.debug("No new alerts to notify via Slack.") return - payload = {"text": message.get("title", "No title")} - requests.post(self.config["webhook_url"], json=payload) \ No newline at end of file + logger.debug("Slack Plugin Enabled") + logger.debug("Alert levels: %s", self.config.get("levels")) + + message = self.create_slack_blocks_from_diff(diff, config) + logger.debug(f"Sending message to {url}") + response = requests.post( + url, + json={"blocks": message} + ) + + if response.status_code >= 400: + logger.error("Slack error %s: %s", response.status_code, response.text) + + @staticmethod + def create_slack_blocks_from_diff(diff: Diff, config: CliConfig): + pr = getattr(config, "pr_number", None) + sha = getattr(config, "commit_sha", None) + scan_link = getattr(diff, "diff_url", "") + scan = f"<{scan_link}|scan>" + title_part = "" + if pr: + title_part += f" for PR {pr}" + if sha: + title_part += f" - {sha[:8]}" + blocks = [ + { + "type": "section", + "text": { + "type": "mrkdwn", + "text": f"*Socket Security issues were found in this *{scan}*{title_part}*" + } + }, + {"type": "divider"} + ] + + for alert in diff.new_alerts: + manifest_str, source_str = Messages.create_sources(alert, "plain") + manifest_str = manifest_str.lstrip("• ") + source_str = source_str.lstrip("• ") + blocks.append({ + "type": "section", + "text": { + "type": "mrkdwn", + "text": ( + f"*{alert.title}*\n" + f"<{alert.url}|{alert.purl}>\n" + f"*Introduced by:* `{source_str}`\n" + f"*Manifest:* `{manifest_str}`\n" + f"*CI Status:* {'Block' if alert.error else 'Warn'}" + ) + } + }) + blocks.append({"type": "divider"}) + + return blocks diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index c471bc1..64c3f3c 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -47,7 +47,6 @@ def main_code(): config = CliConfig.from_args() log.info(f"Starting Socket Security CLI version {config.version}") log.debug(f"config: {config.to_dict()}") - output_handler = OutputHandler(config) # Validate API token if not config.api_token: @@ -57,6 +56,7 @@ def main_code(): sys.exit(3) sdk = socketdev(token=config.api_token) + output_handler = OutputHandler(config, sdk) log.debug("sdk loaded") if config.enable_debug: