From 6f2d64a6f35bb8949356acb132675f9de9779f64 Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Thu, 15 Jan 2026 11:04:00 -0500 Subject: [PATCH 1/6] feat: add --strict-blocking flag to fail on any existing security violations Introduces a new --strict-blocking flag that causes builds to fail on ANY security policy violations with blocking severity, not just new ones. This enables enforcement of a zero-tolerance policy on security issues. Key features: - Works in diff mode only (logs warning in API mode) - Only fails on error-level alerts (not warnings) - --disable-blocking takes precedence when both flags are set - Enhanced console output distinguishes NEW vs EXISTING violations - Comprehensive test coverage for all scenarios Implementation details: - Added unchanged_alerts and removed_alerts fields to Diff class - Created get_unchanged_alerts() method to extract alerts from unchanged packages - Updated report_pass() to check both new and unchanged alerts when enabled - Added validation warnings for conflicting flags and API mode limitations Co-Authored-By: Claude Sonnet 4.5 --- socketsecurity/config.py | 8 +++ socketsecurity/core/__init__.py | 94 +++++++++++++++++++++++++++- socketsecurity/core/classes.py | 8 +++ socketsecurity/output.py | 53 ++++++++++++++-- socketsecurity/socketcli.py | 17 +++-- tests/core/test_diff_alerts.py | 79 +++++++++++++++++++++++ tests/unit/test_cli_config.py | 22 ++++++- tests/unit/test_output.py | 107 +++++++++++++++++++++++++++++++- 8 files changed, 377 insertions(+), 11 deletions(-) create mode 100644 tests/core/test_diff_alerts.py diff --git a/socketsecurity/config.py b/socketsecurity/config.py index b32a3d2..602d741 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -45,6 +45,7 @@ class CliConfig: files: str = None ignore_commit_files: bool = False disable_blocking: bool = False + strict_blocking: bool = False integration_type: IntegrationType = "api" integration_org_slug: Optional[str] = None pending_head: bool = False @@ -123,6 +124,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, + 'strict_blocking': args.strict_blocking, 'integration_type': args.integration, 'pending_head': args.pending_head, 'timeout': args.timeout, @@ -523,6 +525,12 @@ def create_argument_parser() -> argparse.ArgumentParser: action="store_true", help=argparse.SUPPRESS ) + advanced_group.add_argument( + "--strict-blocking", + dest="strict_blocking", + action="store_true", + help="Fail on ANY security policy violations (blocking severity), not just new ones. Only works in diff mode." + ) advanced_group.add_argument( "--enable-diff", dest="enable_diff", diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index fa227b6..9bf6ac0 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -1091,7 +1091,13 @@ def create_new_diff( packages ) = self.get_added_and_removed_packages(head_full_scan_id, new_full_scan.id) - diff = self.create_diff_report(added_packages, removed_packages) + # Separate unchanged packages from added/removed for --strict-blocking support + unchanged_packages = { + pkg_id: pkg for pkg_id, pkg in packages.items() + if pkg_id not in added_packages and pkg_id not in removed_packages + } + + diff = self.create_diff_report(added_packages, removed_packages, unchanged_packages) diff.packages = packages base_socket = "https://socket.dev/dashboard/org" @@ -1114,6 +1120,7 @@ def create_diff_report( self, added_packages: Dict[str, Package], removed_packages: Dict[str, Package], + unchanged_packages: Optional[Dict[str, Package]] = None, direct_only: bool = True ) -> Diff: """ @@ -1123,10 +1130,12 @@ def create_diff_report( 1. Records new/removed packages (direct only by default) 2. Collects alerts from both sets of packages 3. Determines new capabilities introduced + 4. Optionally collects alerts from unchanged packages for --strict-blocking Args: added_packages: Dict of packages added in new scan removed_packages: Dict of packages removed in new scan + unchanged_packages: Dict of packages that didn't change (for --strict-blocking) direct_only: If True, only direct dependencies are included in new/removed lists (but alerts are still processed for all packages) @@ -1137,6 +1146,7 @@ def create_diff_report( alerts_in_added_packages: Dict[str, List[Issue]] = {} alerts_in_removed_packages: Dict[str, List[Issue]] = {} + alerts_in_unchanged_packages: Dict[str, List[Issue]] = {} seen_new_packages = set() seen_removed_packages = set() @@ -1169,11 +1179,34 @@ def create_diff_report( packages=removed_packages ) + # Process unchanged packages for --strict-blocking support + if unchanged_packages: + for package_id, package in unchanged_packages.items(): + # Skip packages that are in added or removed (they're already processed) + if package_id in added_packages or package_id in removed_packages: + continue + + self.add_package_alerts_to_collection( + package=package, + alerts_collection=alerts_in_unchanged_packages, + packages=unchanged_packages + ) + diff.new_alerts = Core.get_new_alerts( alerts_in_added_packages, alerts_in_removed_packages ) + # Get unchanged alerts (for --strict-blocking mode) + diff.unchanged_alerts = Core.get_unchanged_alerts( + alerts_in_unchanged_packages + ) + + # Get removed alerts (for completeness) + diff.removed_alerts = Core.get_removed_alerts( + alerts_in_removed_packages + ) + diff.new_capabilities = Core.get_capabilities_for_added_packages(added_packages) Core.add_purl_capabilities(diff) @@ -1433,3 +1466,62 @@ def get_new_alerts( consolidated_alerts.add(alert_str) return alerts + + @staticmethod + def get_unchanged_alerts( + unchanged_package_alerts: Dict[str, List[Issue]] + ) -> List[Issue]: + """ + Extract all alerts from unchanged packages that are errors or warnings. + + This is used for --strict-blocking mode to identify existing violations + that should cause builds to fail. + + Args: + unchanged_package_alerts: Dictionary of alerts from packages that didn't change + + Returns: + List of all error/warning alerts from unchanged packages + """ + alerts: List[Issue] = [] + consolidated_alerts = set() + + for alert_key in unchanged_package_alerts: + for alert in unchanged_package_alerts[alert_key]: + # Consolidate by package and alert type + alert_str = f"{alert.purl},{alert.type}" + + # Only include error or warning alerts + if (alert.error or alert.warn) and alert_str not in consolidated_alerts: + alerts.append(alert) + consolidated_alerts.add(alert_str) + + return alerts + + @staticmethod + def get_removed_alerts( + removed_package_alerts: Dict[str, List[Issue]] + ) -> List[Issue]: + """ + Extract all alerts from removed packages. + + This is mainly for informational purposes - to show alerts that were removed. + + Args: + removed_package_alerts: Dictionary of alerts from packages that were removed + + Returns: + List of all alerts from removed packages + """ + alerts: List[Issue] = [] + consolidated_alerts = set() + + for alert_key in removed_package_alerts: + for alert in removed_package_alerts[alert_key]: + alert_str = f"{alert.purl},{alert.type}" + + if alert_str not in consolidated_alerts: + alerts.append(alert) + consolidated_alerts.add(alert_str) + + return alerts diff --git a/socketsecurity/core/classes.py b/socketsecurity/core/classes.py index e81312c..506c107 100644 --- a/socketsecurity/core/classes.py +++ b/socketsecurity/core/classes.py @@ -474,6 +474,8 @@ class Diff: packages: dict[str, Package] new_capabilities: Dict[str, List[str]] new_alerts: list[Issue] + unchanged_alerts: list[Issue] + removed_alerts: list[Issue] id: str sbom: str report_url: str @@ -490,6 +492,10 @@ def __init__(self, **kwargs): self.removed_packages = [] if not hasattr(self, "new_alerts"): self.new_alerts = [] + if not hasattr(self, "unchanged_alerts"): + self.unchanged_alerts = [] + if not hasattr(self, "removed_alerts"): + self.removed_alerts = [] if not hasattr(self, "new_capabilities"): self.new_capabilities = {} @@ -508,6 +514,8 @@ def to_dict(self) -> dict: "new_capabilities": self.new_capabilities, "removed_packages": [p.to_dict() for p in self.removed_packages], "new_alerts": [alert.__dict__ for alert in self.new_alerts], + "unchanged_alerts": [alert.__dict__ for alert in self.unchanged_alerts] if hasattr(self, "unchanged_alerts") else [], + "removed_alerts": [alert.__dict__ for alert in self.removed_alerts] if hasattr(self, "removed_alerts") else [], "id": self.id, "sbom": self.sbom if hasattr(self, "sbom") else [], "packages": {k: v.to_dict() for k, v in self.packages.items()} if hasattr(self, "packages") else {}, diff --git a/socketsecurity/output.py b/socketsecurity/output.py index 2948bb2..65004c1 100644 --- a/socketsecurity/output.py +++ b/socketsecurity/output.py @@ -77,12 +77,40 @@ def return_exit_code(self, diff_report: Diff) -> int: def output_console_comments(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: """Outputs formatted console comments""" - if len(diff_report.new_alerts) == 0: + has_new_alerts = len(diff_report.new_alerts) > 0 + has_unchanged_alerts = ( + self.config.strict_blocking and + hasattr(diff_report, 'unchanged_alerts') and + len(diff_report.unchanged_alerts) > 0 + ) + + if not has_new_alerts and not has_unchanged_alerts: self.logger.info("No issues found") return + # Count blocking vs warning alerts + new_blocking = sum(1 for issue in diff_report.new_alerts if issue.error) + new_warning = sum(1 for issue in diff_report.new_alerts if issue.warn) + + unchanged_blocking = 0 + unchanged_warning = 0 + if has_unchanged_alerts: + unchanged_blocking = sum(1 for issue in diff_report.unchanged_alerts if issue.error) + unchanged_warning = sum(1 for issue in diff_report.unchanged_alerts if issue.warn) + console_security_comment = Messages.create_console_security_alert_table(diff_report) + + # Build status message self.logger.info("Security issues detected by Socket Security:") + if new_blocking > 0: + self.logger.info(f" - NEW blocking issues: {new_blocking}") + if new_warning > 0: + self.logger.info(f" - NEW warning issues: {new_warning}") + if unchanged_blocking > 0: + self.logger.info(f" - EXISTING blocking issues: {unchanged_blocking} (causing failure due to --strict-blocking)") + if unchanged_warning > 0: + self.logger.info(f" - EXISTING warning issues: {unchanged_warning}") + self.logger.info(f"Diff Url: {diff_report.diff_url}") self.logger.info(f"\n{console_security_comment}") @@ -105,13 +133,30 @@ def output_console_sarif(self, diff_report: Diff, sbom_file_name: Optional[str] def report_pass(self, diff_report: Diff) -> bool: """Determines if the report passes security checks""" - if not diff_report.new_alerts: + # Priority 1: --disable-blocking always passes + if self.config.disable_blocking: return True - if self.config.disable_blocking: + # Check new alerts for blocking issues + has_new_blocking_alerts = any(issue.error for issue in diff_report.new_alerts) + + # Check unchanged alerts if --strict-blocking is enabled + has_unchanged_blocking_alerts = False + if self.config.strict_blocking and hasattr(diff_report, 'unchanged_alerts'): + has_unchanged_blocking_alerts = any( + issue.error for issue in diff_report.unchanged_alerts + ) + + # If no alerts at all, pass + if not diff_report.new_alerts and not ( + self.config.strict_blocking and + hasattr(diff_report, 'unchanged_alerts') and + diff_report.unchanged_alerts + ): return True - return not any(issue.error for issue in diff_report.new_alerts) + # Fail if there are any blocking alerts (new or unchanged with --strict-blocking) + return not (has_new_blocking_alerts or has_unchanged_blocking_alerts) def save_sbom_file(self, diff_report: Diff, sbom_file_name: Optional[str] = None) -> None: """Saves SBOM file if filename is provided""" diff --git a/socketsecurity/socketcli.py b/socketsecurity/socketcli.py index 3045a17..a92b34b 100644 --- a/socketsecurity/socketcli.py +++ b/socketsecurity/socketcli.py @@ -49,7 +49,12 @@ def main_code(): config = CliConfig.from_args() log.info(f"Starting Socket Security CLI version {config.version}") log.debug(f"config: {config.to_dict()}") - + + # Warn if strict-blocking is used with disable-blocking + if config.strict_blocking and config.disable_blocking: + log.warning("Both --strict-blocking and --disable-blocking specified. " + "--disable-blocking takes precedence and will always return exit code 0.") + # Validate API token if not config.api_token: log.info("Socket API Token not found. Please set it using either:\n" @@ -625,9 +630,13 @@ def main_code(): core.save_file(config.license_file_name, json.dumps(all_packages)) # If we forced API mode due to no supported files, behave as if --disable-blocking was set - if force_api_mode and not config.disable_blocking: - log.debug("Temporarily enabling disable_blocking due to no supported manifest files") - config.disable_blocking = True + if force_api_mode: + if config.strict_blocking: + log.warning("--strict-blocking is only supported in diff mode. " + "API mode (no diff) cannot evaluate existing violations.") + if not config.disable_blocking: + log.debug("Temporarily enabling disable_blocking due to no supported manifest files") + config.disable_blocking = True sys.exit(output_handler.return_exit_code(diff)) diff --git a/tests/core/test_diff_alerts.py b/tests/core/test_diff_alerts.py new file mode 100644 index 0000000..5455e3c --- /dev/null +++ b/tests/core/test_diff_alerts.py @@ -0,0 +1,79 @@ +import pytest +from socketsecurity.core import Core +from socketsecurity.core.classes import Issue + + +class TestDiffAlerts: + """Test alert collection for diff reports""" + + def test_get_unchanged_alerts_filters_errors(self): + """Test that get_unchanged_alerts only returns error/warn alerts""" + alerts_dict = { + 'alert1': [ + Issue(error=True, warn=False, purl='npm/pkg1', type='malicious'), + Issue(error=False, warn=False, purl='npm/pkg1', type='info', monitor=True) + ], + 'alert2': [ + Issue(error=False, warn=True, purl='npm/pkg2', type='typosquat') + ] + } + + result = Core.get_unchanged_alerts(alerts_dict) + + # Should only include error=True and warn=True alerts + assert len(result) == 2 + assert any(alert.error for alert in result) + assert any(alert.warn for alert in result) + assert not any(alert.monitor and not (alert.error or alert.warn) for alert in result) + + def test_get_unchanged_alerts_deduplicates(self): + """Test that get_unchanged_alerts deduplicates by purl+type""" + alerts_dict = { + 'alert1': [ + Issue(error=True, warn=False, purl='npm/pkg1', type='malicious'), + Issue(error=True, warn=False, purl='npm/pkg1', type='malicious') # Duplicate + ] + } + + result = Core.get_unchanged_alerts(alerts_dict) + + # Should deduplicate + assert len(result) == 1 + + def test_get_unchanged_alerts_empty(self): + """Test that get_unchanged_alerts handles empty input""" + result = Core.get_unchanged_alerts({}) + assert len(result) == 0 + + def test_get_removed_alerts_all_alerts(self): + """Test that get_removed_alerts returns all alerts from removed packages""" + alerts_dict = { + 'alert1': [ + Issue(error=True, warn=False, purl='npm/pkg1', type='malicious'), + Issue(error=False, warn=True, purl='npm/pkg1', type='typosquat') + ] + } + + result = Core.get_removed_alerts(alerts_dict) + + # Should include all alerts, not just error/warn + assert len(result) == 2 + + def test_get_removed_alerts_deduplicates(self): + """Test that get_removed_alerts deduplicates by purl+type""" + alerts_dict = { + 'alert1': [ + Issue(error=True, warn=False, purl='npm/pkg1', type='malicious'), + Issue(error=True, warn=False, purl='npm/pkg1', type='malicious') # Duplicate + ] + } + + result = Core.get_removed_alerts(alerts_dict) + + # Should deduplicate + assert len(result) == 1 + + def test_get_removed_alerts_empty(self): + """Test that get_removed_alerts handles empty input""" + result = Core.get_removed_alerts({}) + assert len(result) == 0 diff --git a/tests/unit/test_cli_config.py b/tests/unit/test_cli_config.py index 40178d3..b72ed9b 100644 --- a/tests/unit/test_cli_config.py +++ b/tests/unit/test_cli_config.py @@ -40,4 +40,24 @@ def test_enable_diff_with_integration_api(self): """Test that enable_diff can be used with integration api""" config = CliConfig.from_args(["--api-token", "test", "--integration", "api", "--enable-diff"]) assert config.enable_diff is True - assert config.integration_type == "api" \ No newline at end of file + assert config.integration_type == "api" + + def test_strict_blocking_flag(self): + """Test that --strict-blocking flag is parsed correctly""" + config = CliConfig.from_args(["--api-token", "test", "--strict-blocking"]) + assert config.strict_blocking is True + + def test_strict_blocking_default_false(self): + """Test that strict_blocking defaults to False""" + config = CliConfig.from_args(["--api-token", "test"]) + assert config.strict_blocking is False + + def test_strict_blocking_with_disable_blocking(self): + """Test that both flags can be set (disable-blocking should win)""" + config = CliConfig.from_args([ + "--api-token", "test", + "--strict-blocking", + "--disable-blocking" + ]) + assert config.strict_blocking is True + assert config.disable_blocking is True \ No newline at end of file diff --git a/tests/unit/test_output.py b/tests/unit/test_output.py index 35a0c4f..458714c 100644 --- a/tests/unit/test_output.py +++ b/tests/unit/test_output.py @@ -51,4 +51,109 @@ def test_sbom_file_saving(self, handler, tmp_path): diff.sbom = {"test": "data"} sbom_path = tmp_path / "test.json" handler.save_sbom_file(diff, str(sbom_path)) - assert sbom_path.exists() \ No newline at end of file + assert sbom_path.exists() + + def test_report_pass_with_strict_blocking_new_alerts(self): + """Test that strict-blocking fails on new blocking alerts""" + from socketsecurity.config import CliConfig + from unittest.mock import Mock + + # Create config with strict_blocking + config = Mock(spec=CliConfig) + config.disable_blocking = False + config.strict_blocking = True + + handler = OutputHandler(config, Mock()) + + diff = Diff() + diff.new_alerts = [Issue(error=True, warn=False)] + diff.unchanged_alerts = [] + + assert not handler.report_pass(diff) + + def test_report_pass_with_strict_blocking_unchanged_alerts(self): + """Test that strict-blocking fails on unchanged blocking alerts""" + from socketsecurity.config import CliConfig + from unittest.mock import Mock + + config = Mock(spec=CliConfig) + config.disable_blocking = False + config.strict_blocking = True + + handler = OutputHandler(config, Mock()) + + diff = Diff() + diff.new_alerts = [] + diff.unchanged_alerts = [Issue(error=True, warn=False)] + + assert not handler.report_pass(diff) + + def test_report_pass_with_strict_blocking_both_alerts(self): + """Test that strict-blocking fails when both new and unchanged alerts exist""" + from socketsecurity.config import CliConfig + from unittest.mock import Mock + + config = Mock(spec=CliConfig) + config.disable_blocking = False + config.strict_blocking = True + + handler = OutputHandler(config, Mock()) + + diff = Diff() + diff.new_alerts = [Issue(error=True, warn=False)] + diff.unchanged_alerts = [Issue(error=True, warn=False)] + + assert not handler.report_pass(diff) + + def test_report_pass_with_strict_blocking_only_warnings(self): + """Test that strict-blocking passes when only warnings (not errors) exist""" + from socketsecurity.config import CliConfig + from unittest.mock import Mock + + config = Mock(spec=CliConfig) + config.disable_blocking = False + config.strict_blocking = True + + handler = OutputHandler(config, Mock()) + + diff = Diff() + diff.new_alerts = [Issue(error=False, warn=True)] + diff.unchanged_alerts = [Issue(error=False, warn=True)] + + assert handler.report_pass(diff) + + def test_report_pass_strict_blocking_disabled(self): + """Test that strict-blocking without the flag passes with unchanged alerts""" + from socketsecurity.config import CliConfig + from unittest.mock import Mock + + config = Mock(spec=CliConfig) + config.disable_blocking = False + config.strict_blocking = False # Flag not set + + handler = OutputHandler(config, Mock()) + + diff = Diff() + diff.new_alerts = [] + diff.unchanged_alerts = [Issue(error=True, warn=False)] + + # Should pass because strict_blocking is False + assert handler.report_pass(diff) + + def test_disable_blocking_overrides_strict_blocking(self): + """Test that disable-blocking takes precedence over strict-blocking""" + from socketsecurity.config import CliConfig + from unittest.mock import Mock + + config = Mock(spec=CliConfig) + config.disable_blocking = True + config.strict_blocking = True + + handler = OutputHandler(config, Mock()) + + diff = Diff() + diff.new_alerts = [Issue(error=True, warn=False)] + diff.unchanged_alerts = [Issue(error=True, warn=False)] + + # Should pass because disable_blocking takes precedence + assert handler.report_pass(diff) \ No newline at end of file From ff33725be4678bbd70ca0b8384aec8c07f6007a1 Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Thu, 15 Jan 2026 11:04:19 -0500 Subject: [PATCH 2/6] chore: update uv.lock with version 2.2.63 Co-Authored-By: Claude Sonnet 4.5 --- uv.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/uv.lock b/uv.lock index f2b7477..456ec72 100644 --- a/uv.lock +++ b/uv.lock @@ -1263,7 +1263,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.62" +version = "2.2.63" source = { editable = "." } dependencies = [ { name = "bs4" }, From f2f3c3b86ee32ad86d10ed1f7ed8b32f4597e1de Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Thu, 15 Jan 2026 11:23:16 -0500 Subject: [PATCH 3/6] docs: add --strict-blocking flag documentation to README Added comprehensive documentation for the new --strict-blocking feature: - Added flag to Advanced Configuration parameters table - Created dedicated "Strict Blocking Mode" section with: - Behavior comparison (standard vs strict) - Usage examples for different CI/CD platforms - Output examples showing NEW vs EXISTING violations - Common use cases and implementation strategies - Important notes about limitations and flag priority - Flag combination examples - Migration strategy guidance - Links to GitLab CI example files The documentation clearly explains: - Zero-tolerance security policy enforcement - Diff mode requirement - Error-level filtering (not warnings) - --disable-blocking precedence - First scan behavior Co-Authored-By: Claude Sonnet 4.5 --- README.md | 94 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 94 insertions(+) diff --git a/README.md b/README.md index f0d3c27..575e744 100644 --- a/README.md +++ b/README.md @@ -193,6 +193,7 @@ The CLI will automatically install @coana-tech/cli if not present. Use `--reach` |:-------------------------|:---------|:--------|:----------------------------------------------------------------------| | --ignore-commit-files | False | False | Ignore commit files | | --disable-blocking | False | False | Disable blocking mode | +| --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 | | --timeout | False | | Timeout in seconds for API requests | @@ -328,6 +329,99 @@ Bot mode (`bot_configs` array items): - `alert_types` (array, optional): Only send specific alert types - `reachability_alerts_only` (boolean, default: false): Only send reachable vulnerabilities when using `--reach` +## Strict Blocking Mode + +The `--strict-blocking` flag enforces a zero-tolerance security policy by failing builds when **ANY** security violations with blocking severity exist, not just new ones introduced in the current changes. + +### Standard vs Strict Blocking Behavior + +**Standard Behavior (Default)**: +- ✅ Passes if no NEW violations are introduced +- ❌ Fails only on NEW violations from your changes +- 🟡 Existing violations are ignored + +**Strict Blocking Behavior (`--strict-blocking`)**: +- ✅ Passes only if NO violations exist (new or existing) +- ❌ Fails on ANY violation (new OR existing) +- 🔴 Enforces zero-tolerance policy + +### Usage Examples + +**Basic strict blocking:** +```bash +socketcli --target-path ./my-project --strict-blocking +``` + +**In GitLab CI:** +```bash +socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-0} --strict-blocking +``` + +**In GitHub Actions:** +```bash +socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number $PR_NUMBER --strict-blocking +``` + +### Output Differences + +**Standard scan output:** +``` +Security issues detected by Socket Security: + - NEW blocking issues: 2 + - NEW warning issues: 1 +``` + +**Strict blocking scan output:** +``` +Security issues detected by Socket Security: + - NEW blocking issues: 2 + - NEW warning issues: 1 + - EXISTING blocking issues: 5 (causing failure due to --strict-blocking) + - EXISTING warning issues: 3 +``` + +### Use Cases + +1. **Zero-Tolerance Security Policy**: Enforce that no security violations exist in your codebase at any time +2. **Gradual Security Improvement**: Use alongside standard scans to monitor existing violations while blocking new ones +3. **Protected Branch Enforcement**: Require all violations to be resolved before merging to main/production +4. **Security Audits**: Scheduled scans that fail if any violations accumulate + +### Important Notes + +- **Diff Mode Only**: The flag only works in diff mode (with SCM integration). In API mode, a warning is logged. +- **Error-Level Only**: Only fails on `error=True` alerts (blocking severity), not warnings. +- **Priority**: `--disable-blocking` takes precedence - if both flags are set, the build will always pass. +- **First Scan**: On the very first scan of a repository, there are no "existing" violations, so behavior is identical to standard mode. + +### Flag Combinations + +**Strict blocking with debugging:** +```bash +socketcli --strict-blocking --enable-debug +``` + +**Strict blocking with JSON output:** +```bash +socketcli --strict-blocking --enable-json > security-report.json +``` + +**Override for testing** (passes even with violations): +```bash +socketcli --strict-blocking --disable-blocking +``` + +### Migration Strategy + +**Phase 1: Assessment** - Add strict scan with `allow_failure: true` in CI +**Phase 2: Remediation** - Fix or triage all violations +**Phase 3: Enforcement** - Set `allow_failure: false` to block merges + +For complete GitLab CI/CD examples, see: +- [`.gitlab-ci-strict-blocking-demo.yml`](.gitlab-ci-strict-blocking-demo.yml) - Comprehensive demo +- [`.gitlab-ci-strict-blocking-production.yml`](.gitlab-ci-strict-blocking-production.yml) - Production-ready template +- [`STRICT-BLOCKING-GITLAB-CI.md`](STRICT-BLOCKING-GITLAB-CI.md) - Full documentation + ## Automatic Git Detection The CLI now automatically detects repository information from your git environment, significantly simplifying usage in CI/CD pipelines: From a71530fb72a97369e08ecca6fa385edddf8277cf Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Thu, 15 Jan 2026 12:02:34 -0500 Subject: [PATCH 4/6] chore: bump version to 2.3.1 for --strict-blocking feature Bumped version from 2.2.63 to 2.3.1 following semantic versioning (minor version bump for new feature). This version number avoids conflict with the mucha-dev-gitlab-security-output branch which uses 2.3.0 for the GitLab Security Dashboard feature. Co-Authored-By: Claude Sonnet 4.5 --- pyproject.toml | 2 +- uv.lock | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 280708f..ce41afc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.63" +version = "2.3.1" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/uv.lock b/uv.lock index 456ec72..433d768 100644 --- a/uv.lock +++ b/uv.lock @@ -1263,7 +1263,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.63" +version = "2.3.1" source = { editable = "." } dependencies = [ { name = "bs4" }, From 20f8cf2cb9f77bdbc20ac9ad8282440eafb311a1 Mon Sep 17 00:00:00 2001 From: Jonathan Mucha Date: Fri, 16 Jan 2026 05:52:19 -0500 Subject: [PATCH 5/6] version sync --- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index ce41afc..57af4c2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.3.1" +version = "2.2.64" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 52754b7..0aa8a5e 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.63' +__version__ = '2.2.64' USER_AGENT = f'SocketPythonCLI/{__version__}' From c7c7550af2f9b0c9934ecc18eae89ae0a453f41b Mon Sep 17 00:00:00 2001 From: Douglas Coburn Date: Fri, 16 Jan 2026 10:54:07 -0800 Subject: [PATCH 6/6] Bumping version --- pyproject.toml | 2 +- socketsecurity/__init__.py | 2 +- uv.lock | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 0ad67b9..e524c2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.65" +version = "2.2.66" requires-python = ">= 3.10" license = {"file" = "LICENSE"} dependencies = [ diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 97ca4ed..eab582e 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.65' +__version__ = '2.2.66' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/uv.lock b/uv.lock index 90efc43..3101764 100644 --- a/uv.lock +++ b/uv.lock @@ -1263,7 +1263,7 @@ wheels = [ [[package]] name = "socketsecurity" -version = "2.2.64" +version = "2.2.65" source = { editable = "." } dependencies = [ { name = "bs4" },