diff --git a/docs/cli-reference.md b/docs/cli-reference.md index 0c807f7..409f941 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -151,7 +151,7 @@ socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [-- [--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] [--enable-json] [--enable-sarif] [--sarif-file ] [--sarif-scope {diff,full}] [--sarif-grouping {instance,alert}] [--sarif-reachability {all,reachable,potentially,reachable-or-potentially}] [--enable-gitlab-security] [--gitlab-security-file ] [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] - [--ignore-commit-files] [--disable-blocking] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] + [--ignore-commit-files] [--disable-blocking] [--disable-ignore] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] [--reach] [--reach-version REACH_VERSION] [--reach-timeout REACH_ANALYSIS_TIMEOUT] [--reach-memory-limit REACH_ANALYSIS_MEMORY_LIMIT] [--reach-ecosystems REACH_ECOSYSTEMS] [--reach-exclude-paths REACH_EXCLUDE_PATHS] [--reach-min-severity {low,medium,high,critical}] [--reach-skip-cache] [--reach-disable-analytics] [--reach-output-file REACH_OUTPUT_FILE] @@ -306,6 +306,7 @@ The CLI will automatically install `@coana-tech/cli` if not present. Use `--reac |:-------------------------|:---------|:--------|:----------------------------------------------------------------------| | `--ignore-commit-files` | False | False | Ignore commit files | | `--disable-blocking` | False | False | Disable blocking mode | +| `--disable-ignore` | False | False | Disable support for `@SocketSecurity ignore` commands in PR comments. When set, alerts cannot be suppressed via comments and ignore instructions are hidden from comment output. | | `--strict-blocking` | False | False | Fail on ANY security policy violations (blocking severity), not just new ones. Only works in diff mode. See [Strict Blocking Mode](#strict-blocking-mode) for details. | | `--enable-diff` | False | False | Enable diff mode even when using `--integration api` (forces diff mode without SCM integration) | | `--scm` | False | api | Source control management type | diff --git a/pyproject.toml b/pyproject.toml index 6b37502..c912d72 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.80" +version = "2.2.81" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 92eb029..d4a1870 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.80' +__version__ = '2.2.81' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 9c3b40b..bc7689f 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -91,6 +91,7 @@ class CliConfig: files: str = None ignore_commit_files: bool = False disable_blocking: bool = False + disable_ignore: bool = False strict_blocking: bool = False integration_type: IntegrationType = "api" integration_org_slug: Optional[str] = None @@ -201,6 +202,7 @@ def from_args(cls, args_list: Optional[List[str]] = None) -> 'CliConfig': 'files': args.files, 'ignore_commit_files': args.ignore_commit_files, 'disable_blocking': args.disable_blocking, + 'disable_ignore': args.disable_ignore, 'strict_blocking': args.strict_blocking, 'integration_type': args.integration, 'pending_head': args.pending_head, @@ -693,6 +695,19 @@ def create_argument_parser() -> argparse.ArgumentParser: action="store_true", help=argparse.SUPPRESS ) + advanced_group.add_argument( + "--disable-ignore", + dest="disable_ignore", + action="store_true", + help="Disable support for @SocketSecurity ignore commands in PR comments. " + "Alerts cannot be suppressed via comments when this flag is set." + ) + advanced_group.add_argument( + "--disable_ignore", + dest="disable_ignore", + action="store_true", + help=argparse.SUPPRESS + ) advanced_group.add_argument( "--strict-blocking", dest="strict_blocking", diff --git a/socketsecurity/core/messages.py b/socketsecurity/core/messages.py index db62a6b..fb526b2 100644 --- a/socketsecurity/core/messages.py +++ b/socketsecurity/core/messages.py @@ -816,6 +816,8 @@ def security_comment_template(diff: Diff, config=None) -> str: """ + show_ignore = not (config and getattr(config, 'disable_ignore', False)) + # Loop through security alerts (non-license), dynamically generating rows for alert in security_alerts: severity_icon = Messages.get_severity_icon(alert.severity) @@ -824,6 +826,12 @@ def security_comment_template(diff: Diff, config=None) -> str: # Generate proper manifest URL manifest_url = Messages.get_manifest_file_url(diff, alert.manifests, config) # Generate a table row for each alert + ignore_html = ( + f"

Mark as acceptable risk: To ignore this alert only in this pull request, reply with:
" + f"@SocketSecurity ignore {alert.pkg_name}@{alert.pkg_version}
" + f"Or ignore all future alerts with:
" + f"@SocketSecurity ignore-all

" + ) if show_ignore else "" comment += f""" @@ -836,16 +844,13 @@ def security_comment_template(diff: Diff, config=None) -> str: {alert.pkg_name}@{alert.pkg_version} - {alert.title}

Note: {alert.description}

Source: Manifest File

-

ℹ️ Read more on: - This package | - This alert | +

ℹ️ Read more on: + This package | + This alert | What is known malware?

Suggestion: {alert.suggestion}

-

Mark as acceptable risk: To ignore this alert only in this pull request, reply with:
- @SocketSecurity ignore {alert.pkg_name}@{alert.pkg_version}
- Or ignore all future alerts with:
- @SocketSecurity ignore-all

+ {ignore_html}
@@ -883,14 +888,20 @@ def security_comment_template(diff: Diff, config=None) -> str: # Generate proper manifest URL for license violations license_manifest_url = Messages.get_manifest_file_url(diff, first_alert.manifests, config) - + + license_ignore_html = ( + f"

Mark the package as acceptable risk: To ignore this alert only in this pull request, reply with the comment " + f"@SocketSecurity ignore {first_alert.pkg_name}@{first_alert.pkg_version}. " + f"You can also ignore all packages with @SocketSecurity ignore-all. " + f"To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

" + ) if show_ignore else "" comment += f"""

From: Manifest File

ℹ️ Read more on: This package | What is a license policy violation?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Find a package that does not violate your license policy or adjust your policy to allow this package's license.

-

Mark the package as acceptable risk: To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore {first_alert.pkg_name}@{first_alert.pkg_version}. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

+ {license_ignore_html}
diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 6f17f58..91fb708 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -486,10 +486,13 @@ def main_code(): # 3. Updates the comment to remove ignored alerts # This is completely separate from the main scanning functionality log.info("Comment initiated flow") - - comments = scm.get_comments_for_pr() - log.debug("Removing comment alerts") - scm.remove_comment_alerts(comments) + + if not config.disable_ignore: + comments = scm.get_comments_for_pr() + log.debug("Removing comment alerts") + scm.remove_comment_alerts(comments) + else: + log.info("Ignore commands disabled (--disable-ignore), skipping comment processing") elif scm is not None and scm.check_event_type() != "comment" and not force_api_mode: log.info("Push initiated flow") @@ -497,10 +500,13 @@ def main_code(): log.info("Starting comment logic for PR/MR event") diff = core.create_new_diff(scan_paths, params, no_change=should_skip_scan, save_files_list_path=config.save_submitted_files_list, save_manifest_tar_path=config.save_manifest_tar, base_paths=base_paths, explicit_files=sbom_files_to_submit) comments = scm.get_comments_for_pr() - log.debug("Removing comment alerts") - + # FIXME: this overwrites diff.new_alerts, which was previously populated by Core.create_issue_alerts - diff.new_alerts = Comments.remove_alerts(comments, diff.new_alerts) + if not config.disable_ignore: + log.debug("Removing comment alerts") + diff.new_alerts = Comments.remove_alerts(comments, diff.new_alerts) + else: + log.info("Ignore commands disabled (--disable-ignore), all alerts will be reported") log.debug("Creating Dependency Overview Comment") overview_comment = Messages.dependency_overview_template(diff) diff --git a/tests/unit/test_disable_ignore.py b/tests/unit/test_disable_ignore.py new file mode 100644 index 0000000..e151a3f --- /dev/null +++ b/tests/unit/test_disable_ignore.py @@ -0,0 +1,138 @@ +"""Tests for the --disable-ignore flag.""" + +import pytest +from dataclasses import dataclass + +from socketsecurity.config import CliConfig +from socketsecurity.core.classes import Comment, Diff, Issue +from socketsecurity.core.messages import Messages +from socketsecurity.core.scm_comments import Comments + + +# --- CLI flag parsing tests --- + +class TestDisableIgnoreFlag: + def test_flag_defaults_to_false(self): + config = CliConfig.from_args(["--api-token", "test"]) + assert config.disable_ignore is False + + def test_flag_parsed_with_dashes(self): + config = CliConfig.from_args(["--api-token", "test", "--disable-ignore"]) + assert config.disable_ignore is True + + def test_flag_parsed_with_underscores(self): + config = CliConfig.from_args(["--api-token", "test", "--disable_ignore"]) + assert config.disable_ignore is True + + def test_flag_independent_of_disable_blocking(self): + config = CliConfig.from_args([ + "--api-token", "test", + "--disable-ignore", + "--disable-blocking", + ]) + assert config.disable_ignore is True + assert config.disable_blocking is True + + +# --- Alert suppression tests --- + +def _make_alert(**overrides) -> Issue: + defaults = dict( + pkg_name="lodash", + pkg_version="4.17.21", + pkg_type="npm", + severity="high", + title="Known Malware", + description="Test description", + type="malware", + url="https://socket.dev/test", + manifests="package.json", + props={}, + key="test-key", + purl="pkg:npm/lodash@4.17.21", + error=True, + warn=False, + ignore=False, + monitor=False, + suggestion="Remove this package", + next_step_title="Next steps", + emoji="🚨", + ) + defaults.update(overrides) + return Issue(**defaults) + + +def _make_comment(body: str, comment_id: int = 1) -> Comment: + return Comment( + id=comment_id, + body=body, + body_list=body.split("\n"), + reactions={"+1": 0}, + user={"login": "test-user", "id": 123}, + ) + + +class TestRemoveAlertsRespectedByFlag: + """Verify that Comments.remove_alerts behaves correctly so the + disable_ignore conditional in socketcli.py has the right effect.""" + + def test_remove_alerts_suppresses_matching_alert(self): + """Without --disable-ignore, matching alerts are removed.""" + alert = _make_alert() + ignore_comment = _make_comment("SocketSecurity ignore npm/lodash@4.17.21") + comments = Comments.check_for_socket_comments({ignore_comment.id: ignore_comment}) + result = Comments.remove_alerts(comments, [alert]) + assert len(result) == 0 + + def test_alerts_preserved_when_no_ignore_comments(self): + """With --disable-ignore the caller skips remove_alerts entirely, + which is equivalent to passing empty comments.""" + alert = _make_alert() + result = Comments.remove_alerts({}, [alert]) + assert len(result) == 1 + assert result[0].pkg_name == "lodash" + + def test_ignore_all_suppresses_all_alerts(self): + alert1 = _make_alert() + alert2 = _make_alert(pkg_name="express", pkg_version="4.18.2", + purl="pkg:npm/express@4.18.2") + ignore_comment = _make_comment("SocketSecurity ignore-all") + comments = Comments.check_for_socket_comments({ignore_comment.id: ignore_comment}) + result = Comments.remove_alerts(comments, [alert1, alert2]) + assert len(result) == 0 + + +# --- Comment output tests --- + +@dataclass +class _FakeConfig: + disable_ignore: bool = False + scm: str = "github" + + +class TestSecurityCommentIgnoreInstructions: + def _make_diff_with_alert(self) -> Diff: + diff = Diff() + diff.id = "test-scan-id" + diff.diff_url = "https://socket.dev/test" + diff.new_alerts = [_make_alert()] + return diff + + def test_ignore_instructions_shown_by_default(self): + diff = self._make_diff_with_alert() + config = _FakeConfig(disable_ignore=False) + comment = Messages.security_comment_template(diff, config) + assert "@SocketSecurity ignore" in comment + assert "Mark as acceptable risk" in comment + + def test_ignore_instructions_hidden_when_disabled(self): + diff = self._make_diff_with_alert() + config = _FakeConfig(disable_ignore=True) + comment = Messages.security_comment_template(diff, config) + assert "@SocketSecurity ignore" not in comment + assert "Mark as acceptable risk" not in comment + + def test_ignore_instructions_shown_when_config_is_none(self): + diff = self._make_diff_with_alert() + comment = Messages.security_comment_template(diff, config=None) + assert "@SocketSecurity ignore" in comment diff --git a/uv.lock b/uv.lock index 4c6607f..4d82501 100644 --- a/uv.lock +++ b/uv.lock @@ -1168,7 +1168,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.78" +version = "2.2.81" source = { editable = "." } dependencies = [ { name = "bs4" },