Skip to content

Commit e75a1d7

Browse files
authored
Added Slack plugin support (#77)
1 parent 8cbf2da commit e75a1d7

File tree

8 files changed

+111
-13
lines changed

8 files changed

+111
-13
lines changed

README.md

+13
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ If you don't want to provide the Socket API Token every time then you can use th
8181
The Python CLI currently Supports the following plugins:
8282

8383
- Jira
84+
- Slack
8485

8586
##### Jira
8687

@@ -95,6 +96,18 @@ Example `SOCKET_JIRA_CONFIG_JSON` value
9596
{"url": "https://REPLACE_ME.atlassian.net", "email": "[email protected]", "api_token": "REPLACE_ME", "project": "REPLACE_ME" }
9697
````
9798

99+
##### Slack
100+
101+
| Environment Variable | Required | Default | Description |
102+
|:-------------------------|:---------|:--------|:-----------------------------------|
103+
| SOCKET_SLACK_ENABLED | False | false | Enables/Disables the Slack Plugin |
104+
| SOCKET_SLACK_CONFIG_JSON | True | None | Required if the Plugin is enabled. |
105+
106+
Example `SOCKET_SLACK_CONFIG_JSON` value
107+
108+
````json
109+
{"url": "https://REPLACE_ME_WEBHOOK"}
110+
````
98111

99112
## File Selection Behavior
100113

pyproject.toml

+1-1
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
66

77
[project]
88
name = "socketsecurity"
9-
version = "2.0.50"
9+
version = "2.0.51"
1010
requires-python = ">= 3.10"
1111
license = {"file" = "LICENSE"}
1212
dependencies = [

socketsecurity/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11
__author__ = 'socket.dev'
2-
__version__ = '2.0.50'
2+
__version__ = '2.0.51'

socketsecurity/config.py

+6
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@ class CliConfig:
5252
include_module_folders: bool = False
5353
version: str = __version__
5454
jira_plugin: PluginConfig = field(default_factory=PluginConfig)
55+
slack_plugin: PluginConfig = field(default_factory=PluginConfig)
5556

5657
@classmethod
5758
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':
100101
enabled=os.getenv("SOCKET_JIRA_ENABLED", "false").lower() == "true",
101102
levels=os.getenv("SOCKET_JIRA_LEVELS", "block,warn").split(","),
102103
config=get_plugin_config_from_env("SOCKET_JIRA")
104+
),
105+
"slack_plugin": PluginConfig(
106+
enabled=os.getenv("SOCKET_SLACK_ENABLED", "false").lower() == "true",
107+
levels=os.getenv("SOCKET_SLACK_LEVELS", "block,warn").split(","),
108+
config=get_plugin_config_from_env("SOCKET_SLACK")
103109
)
104110
})
105111

socketsecurity/output.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,14 @@
66
from .core.classes import Diff, Issue
77
from .config import CliConfig
88
from socketsecurity.plugins.manager import PluginManager
9+
from socketdev import socketdev
910

1011

1112
class OutputHandler:
1213
config: CliConfig
1314
logger: logging.Logger
1415

15-
def __init__(self, config: CliConfig):
16+
def __init__(self, config: CliConfig, sdk: socketdev):
1617
self.config = config
1718
self.logger = logging.getLogger("socketcli")
1819

@@ -24,16 +25,23 @@ def handle_output(self, diff_report: Diff) -> None:
2425
self.output_console_sarif(diff_report, self.config.sbom_file)
2526
else:
2627
self.output_console_comments(diff_report, self.config.sbom_file)
27-
if hasattr(self.config, "jira_plugin") and self.config.jira_plugin.enabled:
28+
if self.config.jira_plugin.enabled:
2829
jira_config = {
2930
"enabled": self.config.jira_plugin.enabled,
3031
"levels": self.config.jira_plugin.levels or [],
3132
**(self.config.jira_plugin.config or {})
3233
}
33-
3434
plugin_mgr = PluginManager({"jira": jira_config})
35+
plugin_mgr.send(diff_report, config=self.config)
36+
37+
if self.config.slack_plugin.enabled:
38+
slack_config = {
39+
"enabled": self.config.slack_plugin.enabled,
40+
"levels": self.config.slack_plugin.levels or [],
41+
**(self.config.slack_plugin.config or {})
42+
}
3543

36-
# The Jira plugin knows how to build title + description from diff/config
44+
plugin_mgr = PluginManager({"slack": slack_config})
3745
plugin_mgr.send(diff_report, config=self.config)
3846

3947
self.save_sbom_file(diff_report, self.config.sbom_file)

socketsecurity/plugins/base.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@ class Plugin:
22
def __init__(self, config):
33
self.config = config
44

5-
def send(self, message, level):
5+
def send(self, diff, config):
66
raise NotImplementedError("Plugin must implement send()")

socketsecurity/plugins/slack.py

+76-5
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,83 @@
1-
from .base import Plugin
1+
import logging
22
import requests
3+
from config import CliConfig
4+
from .base import Plugin
5+
from socketsecurity.core.classes import Diff
6+
from socketsecurity.core.messages import Messages
7+
8+
logger = logging.getLogger(__name__)
9+
310

411
class SlackPlugin(Plugin):
5-
def send(self, message, level):
12+
@staticmethod
13+
def get_name():
14+
return "slack"
15+
16+
def send(self, diff, config: CliConfig):
617
if not self.config.get("enabled", False):
718
return
8-
if level not in self.config.get("levels", ["block", "warn"]):
19+
if not self.config.get("url"):
20+
logger.warning("Slack webhook URL not configured.")
21+
return
22+
else:
23+
url = self.config.get("url")
24+
25+
if not diff.new_alerts:
26+
logger.debug("No new alerts to notify via Slack.")
927
return
1028

11-
payload = {"text": message.get("title", "No title")}
12-
requests.post(self.config["webhook_url"], json=payload)
29+
logger.debug("Slack Plugin Enabled")
30+
logger.debug("Alert levels: %s", self.config.get("levels"))
31+
32+
message = self.create_slack_blocks_from_diff(diff, config)
33+
logger.debug(f"Sending message to {url}")
34+
response = requests.post(
35+
url,
36+
json={"blocks": message}
37+
)
38+
39+
if response.status_code >= 400:
40+
logger.error("Slack error %s: %s", response.status_code, response.text)
41+
42+
@staticmethod
43+
def create_slack_blocks_from_diff(diff: Diff, config: CliConfig):
44+
pr = getattr(config, "pr_number", None)
45+
sha = getattr(config, "commit_sha", None)
46+
scan_link = getattr(diff, "diff_url", "")
47+
scan = f"<{scan_link}|scan>"
48+
title_part = ""
49+
if pr:
50+
title_part += f" for PR {pr}"
51+
if sha:
52+
title_part += f" - {sha[:8]}"
53+
blocks = [
54+
{
55+
"type": "section",
56+
"text": {
57+
"type": "mrkdwn",
58+
"text": f"*Socket Security issues were found in this *{scan}*{title_part}*"
59+
}
60+
},
61+
{"type": "divider"}
62+
]
63+
64+
for alert in diff.new_alerts:
65+
manifest_str, source_str = Messages.create_sources(alert, "plain")
66+
manifest_str = manifest_str.lstrip("• ")
67+
source_str = source_str.lstrip("• ")
68+
blocks.append({
69+
"type": "section",
70+
"text": {
71+
"type": "mrkdwn",
72+
"text": (
73+
f"*{alert.title}*\n"
74+
f"<{alert.url}|{alert.purl}>\n"
75+
f"*Introduced by:* `{source_str}`\n"
76+
f"*Manifest:* `{manifest_str}`\n"
77+
f"*CI Status:* {'Block' if alert.error else 'Warn'}"
78+
)
79+
}
80+
})
81+
blocks.append({"type": "divider"})
82+
83+
return blocks

socketsecurity/socketcli.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,6 @@ def main_code():
4747
config = CliConfig.from_args()
4848
log.info(f"Starting Socket Security CLI version {config.version}")
4949
log.debug(f"config: {config.to_dict()}")
50-
output_handler = OutputHandler(config)
5150

5251
# Validate API token
5352
if not config.api_token:
@@ -57,6 +56,7 @@ def main_code():
5756
sys.exit(3)
5857

5958
sdk = socketdev(token=config.api_token)
59+
output_handler = OutputHandler(config, sdk)
6060
log.debug("sdk loaded")
6161

6262
if config.enable_debug:

0 commit comments

Comments
 (0)