Skip to content

Commit 878b4b1

Browse files
author
Jonathan Mucha
committed
added ability to prevent merges based on failed check run
1 parent 9007613 commit 878b4b1

File tree

4 files changed

+171
-0
lines changed

4 files changed

+171
-0
lines changed

socketsecurity/config.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ class CliConfig:
8686
only_facts_file: bool = False
8787
reach_use_only_pregenerated_sboms: bool = False
8888
max_purl_batch_size: int = 5000
89+
enable_commit_status: bool = False
8990

9091
@classmethod
9192
def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
@@ -164,6 +165,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig':
164165
'only_facts_file': args.only_facts_file,
165166
'reach_use_only_pregenerated_sboms': args.reach_use_only_pregenerated_sboms,
166167
'max_purl_batch_size': args.max_purl_batch_size,
168+
'enable_commit_status': args.enable_commit_status,
167169
'version': __version__
168170
}
169171
try:
@@ -512,6 +514,18 @@ def create_argument_parser() -> argparse.ArgumentParser:
512514
action="store_true",
513515
help=argparse.SUPPRESS
514516
)
517+
output_group.add_argument(
518+
"--enable-commit-status",
519+
dest="enable_commit_status",
520+
action="store_true",
521+
help="Report scan result as a commit status on GitLab (requires GitLab SCM)"
522+
)
523+
output_group.add_argument(
524+
"--enable_commit_status",
525+
dest="enable_commit_status",
526+
action="store_true",
527+
help=argparse.SUPPRESS
528+
)
515529

516530
# Plugin Configuration
517531
plugin_group = parser.add_argument_group('Plugin Configuration')

socketsecurity/core/scm/gitlab.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -260,6 +260,31 @@ def add_socket_comments(
260260
log.debug("No Previous version of Security Issue comment, posting")
261261
self.post_comment(security_comment)
262262

263+
def set_commit_status(self, state: str, description: str, target_url: str = '') -> None:
264+
"""Post a commit status to GitLab. state should be 'success' or 'failed'."""
265+
if not self.config.mr_project_id:
266+
log.debug("No mr_project_id, skipping commit status")
267+
return
268+
path = f"projects/{self.config.mr_project_id}/statuses/{self.config.commit_sha}"
269+
payload = {
270+
"state": state,
271+
"name": "socket-security",
272+
"description": description,
273+
}
274+
if target_url:
275+
payload["target_url"] = target_url
276+
try:
277+
self._request_with_fallback(
278+
path=path,
279+
payload=payload,
280+
method="POST",
281+
headers=self.config.headers,
282+
base_url=self.config.api_url
283+
)
284+
log.info(f"Commit status set to '{state}' on {self.config.commit_sha[:8]}")
285+
except Exception as e:
286+
log.error(f"Failed to set commit status: {e}")
287+
263288
def remove_comment_alerts(self, comments: dict):
264289
security_alert = comments.get("security")
265290
if security_alert is not None:

socketsecurity/socketcli.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -641,6 +641,20 @@ def main_code():
641641
log.debug("Temporarily enabling disable_blocking due to no supported manifest files")
642642
config.disable_blocking = True
643643

644+
# Post commit status to GitLab if enabled
645+
if config.enable_commit_status and scm is not None:
646+
from socketsecurity.core.scm.gitlab import Gitlab
647+
if isinstance(scm, Gitlab) and scm.config.mr_project_id:
648+
passed = output_handler.report_pass(diff)
649+
state = "success" if passed else "failed"
650+
blocking_count = sum(1 for a in diff.new_alerts if a.error)
651+
if passed:
652+
description = "No blocking issues"
653+
else:
654+
description = f"{blocking_count} blocking alert(s) found"
655+
target_url = diff.report_url or diff.diff_url or ""
656+
scm.set_commit_status(state, description, target_url)
657+
644658
sys.exit(output_handler.return_exit_code(diff))
645659

646660

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
"""Tests for GitLab commit status integration"""
2+
import os
3+
import pytest
4+
from unittest.mock import patch, MagicMock, call
5+
6+
from socketsecurity.core.scm.gitlab import Gitlab, GitlabConfig
7+
8+
9+
def _make_gitlab_config(**overrides):
10+
defaults = dict(
11+
commit_sha="abc123def456",
12+
api_url="https://gitlab.example.com/api/v4",
13+
project_dir="/builds/test",
14+
mr_source_branch="feature",
15+
mr_iid="42",
16+
mr_project_id="99",
17+
commit_message="test commit",
18+
default_branch="main",
19+
project_name="test-project",
20+
pipeline_source="merge_request_event",
21+
commit_author="dev@example.com",
22+
token="glpat-test",
23+
repository="test-project",
24+
is_default_branch=False,
25+
headers={"Authorization": "Bearer glpat-test", "accept": "application/json"},
26+
)
27+
defaults.update(overrides)
28+
return GitlabConfig(**defaults)
29+
30+
31+
class TestSetCommitStatus:
32+
"""Test Gitlab.set_commit_status()"""
33+
34+
def test_calls_correct_api_path(self):
35+
config = _make_gitlab_config()
36+
client = MagicMock()
37+
gl = Gitlab(client=client, config=config)
38+
gl._request_with_fallback = MagicMock()
39+
40+
gl.set_commit_status("success", "No blocking issues", "https://app.socket.dev/report/123")
41+
42+
gl._request_with_fallback.assert_called_once_with(
43+
path="projects/99/statuses/abc123def456",
44+
payload={
45+
"state": "success",
46+
"name": "socket-security",
47+
"description": "No blocking issues",
48+
"target_url": "https://app.socket.dev/report/123",
49+
},
50+
method="POST",
51+
headers=config.headers,
52+
base_url=config.api_url,
53+
)
54+
55+
def test_failed_state_payload(self):
56+
config = _make_gitlab_config()
57+
client = MagicMock()
58+
gl = Gitlab(client=client, config=config)
59+
gl._request_with_fallback = MagicMock()
60+
61+
gl.set_commit_status("failed", "3 blocking alert(s) found")
62+
63+
args = gl._request_with_fallback.call_args
64+
assert args.kwargs["payload"]["state"] == "failed"
65+
assert args.kwargs["payload"]["description"] == "3 blocking alert(s) found"
66+
assert "target_url" not in args.kwargs["payload"]
67+
68+
def test_skipped_when_no_mr_project_id(self):
69+
config = _make_gitlab_config(mr_project_id=None)
70+
client = MagicMock()
71+
gl = Gitlab(client=client, config=config)
72+
gl._request_with_fallback = MagicMock()
73+
74+
gl.set_commit_status("success", "No blocking issues")
75+
76+
gl._request_with_fallback.assert_not_called()
77+
78+
def test_graceful_error_handling(self):
79+
config = _make_gitlab_config()
80+
client = MagicMock()
81+
gl = Gitlab(client=client, config=config)
82+
gl._request_with_fallback = MagicMock(side_effect=Exception("API error"))
83+
84+
# Should not raise
85+
gl.set_commit_status("success", "No blocking issues")
86+
87+
def test_no_target_url_omitted_from_payload(self):
88+
config = _make_gitlab_config()
89+
client = MagicMock()
90+
gl = Gitlab(client=client, config=config)
91+
gl._request_with_fallback = MagicMock()
92+
93+
gl.set_commit_status("success", "No blocking issues", target_url="")
94+
95+
payload = gl._request_with_fallback.call_args.kwargs["payload"]
96+
assert "target_url" not in payload
97+
98+
99+
class TestEnableCommitStatusCliArg:
100+
"""Test --enable-commit-status CLI argument parsing"""
101+
102+
def test_default_is_false(self):
103+
from socketsecurity.config import create_argument_parser
104+
parser = create_argument_parser()
105+
args = parser.parse_args([])
106+
assert args.enable_commit_status is False
107+
108+
def test_flag_sets_true(self):
109+
from socketsecurity.config import create_argument_parser
110+
parser = create_argument_parser()
111+
args = parser.parse_args(["--enable-commit-status"])
112+
assert args.enable_commit_status is True
113+
114+
def test_underscore_alias(self):
115+
from socketsecurity.config import create_argument_parser
116+
parser = create_argument_parser()
117+
args = parser.parse_args(["--enable_commit_status"])
118+
assert args.enable_commit_status is True

0 commit comments

Comments
 (0)