Skip to content

Commit 2d9ef13

Browse files
authored
Added support for MS Sentinel (#12)
* Added support for MS Sentinel * Incremented version
1 parent d74df76 commit 2d9ef13

File tree

7 files changed

+259
-3
lines changed

7 files changed

+259
-3
lines changed

README.md

+3
Original file line numberDiff line numberDiff line change
@@ -33,4 +33,7 @@ jobs:
3333
trivy_exclude_dir: "/path/to/ignore"
3434
sumo_logic_enabled: true
3535
sumo_logic_http_source_url: https://example/url
36+
ms_sentinel_enabled: true
37+
ms_sentinel_workspace_id: REPLACE_ME
38+
ms_sentinel_shared_key: REPLACE_ME
3639
```

src/core/__init__.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
"base_github"
1212
]
1313

14-
__version__ = "1.0.11"
14+
__version__ = "1.0.12"
1515
__author__ = "socket.dev"
1616
base_github = "https://github.com"
1717

src/core/connectors/bandit/__init__.py

+21
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,24 @@ def create_output(data: dict, marker: str, repo: str, commit: str, cwd: str) ->
6161
md.create_md_file()
6262
output_str = md.file_data_text.lstrip()
6363
return output_str, bandit_result
64+
65+
@staticmethod
66+
def transform_bandit_event(event):
67+
"""Transforms a Bandit security event into the correct Sentinel schema."""
68+
return {
69+
"TimeGenerated": datetime.utcnow().isoformat(),
70+
"SourceComputerId": event.get("cwd", "Unknown"),
71+
"OperationStatus": event.get("issue_severity", "Unknown"),
72+
"Detail": event.get("issue_text", "Unknown"),
73+
"OperationCategory": event.get("test_name", "Static Analysis"),
74+
"Solution": event.get("more_info", "No remediation guide available"),
75+
"Message": event.get("issue_text", "Unknown issue"),
76+
"FilePath": event.get("filename", "Unknown"),
77+
"URL": event.get("url", "N/A"),
78+
"Timestamp": event.get("timestamp", datetime.utcnow().isoformat()),
79+
"Plugin": "Bandit",
80+
"Severity": event.get("issue_severity", "Unknown"),
81+
"TestID": event.get("test_id", "Unknown"),
82+
"CWE_ID": event.get("issue_cwe", {}).get("id", "Unknown"),
83+
"CWE_Link": event.get("issue_cwe", {}).get("link", "Unknown")
84+
}

src/core/load_plugins.py

+22-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import os
22
from core.plugins.sumologic import Sumologic
3+
from core.plugins.microsoft_sentinel import Sentinel
34

45

56
def load_sumo_logic_plugin():
@@ -8,7 +9,7 @@ def load_sumo_logic_plugin():
89
910
:return: Instance of the Sumologic class or None if not enabled/configured.
1011
"""
11-
sumo_logic_enabled = os.getenv("SUMO_LOGIC_ENABLED", "false").lower() == "true"
12+
sumo_logic_enabled = os.getenv("INPUT_SUMO_LOGIC_ENABLED", "false").lower() == "true"
1213
if not sumo_logic_enabled:
1314
print("Sumo Logic integration is disabled.")
1415
return None
@@ -20,3 +21,23 @@ def load_sumo_logic_plugin():
2021
return None
2122

2223
return Sumologic(sumo_logic_http_source_url)
24+
25+
def load_ms_sentinel_plugin():
26+
"""
27+
Loads the Microsoft Sentinel plugin if it is enabled and properly configured.
28+
29+
:return: Instance of the Microsoft Sentinel class or None if not enabled/configured.
30+
"""
31+
ms_sentinel_enabled = os.getenv("INPUT_MS_SENTINEL_ENABLED", "false").lower() == "true"
32+
if not ms_sentinel_enabled:
33+
print("Microsoft Sentinel integration is disabled.")
34+
return None
35+
36+
MS_SENTINEL_WORKSPACE_ID = os.getenv("INPUT_MS_SENTINEL_WORKSPACE_ID")
37+
MS_SENTINEL_SHARED_KEY = os.getenv("INPUT_MS_SENTINEL_SHARED_KEY")
38+
39+
if not all([MS_SENTINEL_WORKSPACE_ID, MS_SENTINEL_SHARED_KEY]):
40+
print("Microsoft Sentinel environment variables are not properly configured!")
41+
return None
42+
43+
return Sentinel(MS_SENTINEL_WORKSPACE_ID, MS_SENTINEL_SHARED_KEY)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .sentinel import Sentinel
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,182 @@
1+
import json
2+
import hashlib
3+
import hmac
4+
import base64
5+
import requests
6+
from datetime import datetime, timezone
7+
8+
9+
default_log_type = 'SocketSecurityTool'
10+
11+
12+
class Sentinel:
13+
def __init__(self, workspace_id: str, shared_key: str):
14+
"""
15+
Initializes the Microsoft Sentinel client with credentials and HTTP source URL.
16+
17+
:param workspace_id: The Microsoft Sentinel Customer ID
18+
:param shared_key: The Microsoft Sentinel Shared Key
19+
:param log_type: The Microsoft Sentinel Log Type
20+
"""
21+
self.workspace_id = workspace_id
22+
self.shared_key = shared_key
23+
self.uri = f"https://{self.workspace_id}.ods.opinsights.azure.com/api/logs?api-version=2016-04-01"
24+
25+
def _generate_signature(self, content_length: int, date: str) -> str:
26+
"""
27+
Generates the HMAC SHA256 signature required for authentication.
28+
29+
:param content_length: Length of the request body
30+
:param date: Current date in RFC 1123 format
31+
:return: Authorization signature string
32+
"""
33+
method = 'POST'
34+
content_type = 'application/json'
35+
resource = '/api/logs'
36+
x_headers = f"x-ms-date:{date}"
37+
string_to_hash = f"{method}\n{content_length}\n{content_type}\n{x_headers}\n{resource}"
38+
bytes_to_hash = bytes(string_to_hash, encoding="utf-8")
39+
40+
decoded_key = base64.b64decode(self.shared_key)
41+
hashed_string = hmac.new(decoded_key, bytes_to_hash, digestmod=hashlib.sha256).digest()
42+
signature = base64.b64encode(hashed_string).decode()
43+
44+
return f"SharedKey {self.workspace_id}:{signature}"
45+
46+
def send_events(self, events: list, log_type: str = default_log_type) -> list:
47+
"""
48+
Sends a single event to Microsoft Sentinel.
49+
50+
:param events: Dictionary representing the event data
51+
:param log_type:
52+
:return: Response from the Sentinel API
53+
"""
54+
errors = []
55+
events = Sentinel.normalize_events(events, log_type)
56+
for event in events:
57+
response = self.send_event(event, log_type)
58+
if response["status_code"] != 200:
59+
errors.append(response)
60+
return errors
61+
62+
def send_event(self, event_data: dict, log_type: str = default_log_type) -> dict:
63+
"""
64+
Sends a batch of events to a logging endpoint. This function serializes a
65+
list of events into JSON format, computes the necessary authorization
66+
headers, and sends them via an HTTP POST request to the configured logging
67+
endpoint.
68+
69+
:param event_data: An event that is serialized to JSON
70+
and sent to the logging endpoint.
71+
:type event_data: dict
72+
:param log_type: The type of log under which the events should be
73+
categorized. Defaults to the class's `default_log_type`.
74+
:type log_type: str, optional
75+
:return: A dictionary with the HTTP response status code and response text
76+
from the logging endpoint.
77+
:rtype: dict
78+
"""
79+
body = json.dumps(
80+
{
81+
"detail": event_data,
82+
"SourceComputerId": "socket-security-tools"
83+
}
84+
)
85+
content_length = len(body)
86+
rfc1123date = datetime.utcnow().strftime('%a, %d %b %Y %H:%M:%S GMT')
87+
authorization = self._generate_signature(content_length, rfc1123date)
88+
89+
headers = {
90+
"Content-Type": "application/json",
91+
"Authorization": authorization,
92+
"Log-Type": log_type,
93+
"x-ms-date": rfc1123date
94+
}
95+
96+
response = requests.post(self.uri, data=body, headers=headers)
97+
return {
98+
"status_code": response.status_code,
99+
"response_text": response.text
100+
}
101+
102+
@staticmethod
103+
def transform_gosec_event(event):
104+
"""Transforms a Gosec security event into the correct Sentinel schema."""
105+
return {
106+
"TimeGenerated": datetime.now(timezone.utc).isoformat(),
107+
"SourceComputerId": event.get("cwd", "Unknown"),
108+
"OperationStatus": event.get("confidence", "Unknown"),
109+
"Detail": event.get("details", "Unknown"),
110+
"OperationCategory": "Static Analysis",
111+
"Solution": event.get("cwe", {}).get("url", "No remediation guide available"),
112+
"Message": event.get("details", "Unknown"),
113+
"FilePath": event.get("file", "Unknown"),
114+
"URL": event.get("url", "N/A"),
115+
"Timestamp": event.get("timestamp", datetime.now(timezone.utc).isoformat()),
116+
"Plugin": "Gosec",
117+
"Severity": event.get("severity", "Unknown"),
118+
"RuleID": event.get("rule_id", "Unknown"),
119+
"CWE_ID": event.get("cwe", {}).get("id", "Unknown"),
120+
}
121+
122+
@staticmethod
123+
def transform_bandit_event(event):
124+
"""Transforms a Bandit security event into the correct Sentinel schema."""
125+
return {
126+
"TimeGenerated": datetime.now(timezone.utc).isoformat(),
127+
"SourceComputerId": event.get("cwd", "Unknown"),
128+
"OperationStatus": event.get("issue_severity", "Unknown"),
129+
"Detail": event.get("issue_text", "Unknown"),
130+
"OperationCategory": event.get("test_name", "Static Analysis"),
131+
"Solution": event.get("more_info", "No remediation guide available"),
132+
"Message": event.get("issue_text", "Unknown issue"),
133+
"FilePath": event.get("filename", "Unknown"),
134+
"URL": event.get("url", "N/A"),
135+
"Timestamp": event.get("timestamp", datetime.now(timezone.utc).isoformat()),
136+
"Plugin": "Bandit",
137+
"Severity": event.get("issue_severity", "Unknown"),
138+
"TestID": event.get("test_id", "Unknown"),
139+
"CWE_ID": event.get("issue_cwe", {}).get("id", "Unknown"),
140+
"CWE_Link": event.get("issue_cwe", {}).get("link", "Unknown")
141+
}
142+
143+
@staticmethod
144+
def transform_trufflehog_event(event):
145+
"""Transforms a Trufflehog event into the correct Sentinel schema."""
146+
return {
147+
"TimeGenerated": datetime.now(timezone.utc).isoformat(),
148+
"SourceComputerId": event.get("cwd", "Unknown"),
149+
"OperationStatus": "Success" if event.get("Verified", False) else "Failure",
150+
"Detail": event.get("DetectorName", "Unknown Detection"),
151+
"OperationCategory": event.get("SourceName", "Secret Scanning"),
152+
"Solution": event.get("ExtraData", {}).get("rotation_guide", "No remediation guide available"),
153+
"Message": event.get("Raw", "Potential secret detected"),
154+
"FilePath": event.get("SourceMetadata", {}).get("Data", {}).get("Filesystem", {}).get("file", "Unknown"),
155+
"Timestamp": event.get("timestamp", datetime.now(timezone.utc).isoformat()),
156+
"Plugin": "Trufflehog",
157+
"Severity": "HIGH" if not event.get("Verified", False) else "LOW",
158+
"SourceType": event.get("SourceType", "Unknown"),
159+
"DetectorType": event.get("DetectorType", "Unknown")
160+
}
161+
162+
@staticmethod
163+
def normalize_events(events: list, plugin_name: str):
164+
"""Detects event type and normalizes them for Sentinel ingestion."""
165+
formatted_events = []
166+
167+
for event in events:
168+
if isinstance(event, str):
169+
try:
170+
event = json.loads(event) # Convert from string if necessary
171+
except json.JSONDecodeError:
172+
print(f"Skipping invalid event: {event}")
173+
continue # Skip invalid JSON entries
174+
175+
if "plugin_name" in event:
176+
if "bandit" in plugin_name.lower():
177+
formatted_events.append(Sentinel.transform_bandit_event(event))
178+
elif "trufflehog" in plugin_name.lower():
179+
formatted_events.append(Sentinel.transform_trufflehog_event(event))
180+
elif "gosec" in plugin_name.lower():
181+
formatted_events.append(Sentinel.transform_gosec_event(event))
182+
return formatted_events

src/socket_external_tools_runner.py

+29-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from core.connectors.bandit import Bandit
99
from core.connectors.gosec import Gosec
1010
from core.connectors.trufflehog import Trufflehog
11-
from core.load_plugins import load_sumo_logic_plugin
11+
from core.load_plugins import load_sumo_logic_plugin, load_ms_sentinel_plugin
1212
import os
1313

1414

@@ -19,6 +19,22 @@
1919
exit(1)
2020

2121

22+
def format_events(events):
23+
formatted_events = []
24+
25+
for event in events:
26+
if isinstance(event, str): # If it's a JSON string, parse it into a dictionary
27+
try:
28+
event = json.loads(event) # Convert string to dictionary
29+
except json.JSONDecodeError:
30+
print(f"Skipping invalid event: {event}")
31+
continue # Skip invalid JSON entries
32+
33+
formatted_events.append(event) # Append properly formatted event
34+
35+
return formatted_events
36+
37+
2238
def load_json(name, connector: str, connector_type: str = 'single') -> dict:
2339
json_data = {}
2440
if connector_type == 'single':
@@ -45,6 +61,7 @@ def load_json(name, connector: str, connector_type: str = 'single') -> dict:
4561

4662

4763
sumo_client = load_sumo_logic_plugin()
64+
ms_sentinel = load_ms_sentinel_plugin()
4865

4966
tool_bandit_name = "Bandit"
5067
tool_gosec_name = "Gosec"
@@ -102,6 +119,17 @@ def load_json(name, connector: str, connector_type: str = 'single') -> dict:
102119
print(errors) if (errors := sumo_client.send_events(bandit_events.get("events"), bandit_name)) else []
103120
print(errors) if (errors := sumo_client.send_events(gosec_events.get("events"), gosec_name)) else []
104121
print(errors) if (errors := sumo_client.send_events(truffle_events.get("events"), trufflehog_name)) else []
122+
if ms_sentinel:
123+
print("Issues detected with Security Tools. Please check Microsoft Sentinel Events")
124+
ms_bandit_name = f"SocketSecurityToolsBandit"
125+
ms_gosec_name = f"SocketSecurityToolsGosec"
126+
ms_trufflehog_name = f"SocketSecurityToolsTrufflehog"
127+
ms_bandit_events = format_events(bandit_events.get("events"))
128+
ms_gosec_events = format_events(gosec_events.get("events"))
129+
ms_trufflehog_events = format_events(truffle_events.get("events"))
130+
print(errors) if (errors := ms_sentinel.send_events(ms_bandit_events, ms_bandit_name)) else []
131+
print(errors) if (errors := ms_sentinel.send_events(ms_gosec_events, ms_gosec_name)) else []
132+
print(errors) if (errors := ms_sentinel.send_events(ms_trufflehog_events, ms_trufflehog_name)) else []
105133
exit(1)
106134
else:
107135
print("No issues detected with Socket Security Tools")

0 commit comments

Comments
 (0)