diff --git a/.gitignore b/.gitignore index e01bafe..990cbcc 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,8 @@ test.py *.cpython-312.pyc` file_generator.py .coverage +.coverage.* +htmlcov/ .env.local Pipfile test/ diff --git a/CHANGELOG.md b/CHANGELOG.md index e2ea6c0..8ab4d97 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,9 +1,48 @@ # Changelog +## 2.2.90 + +- Migrated license enrichment PURL lookup to the org-scoped endpoint (`POST /v0/orgs/{slug}/purl`) from the deprecated global endpoint (`POST /v0/purl`). + ## 2.2.83 - Fixed branch detection in detached-HEAD CI checkouts. When `git name-rev --name-only HEAD` returned an output with a suffix operator (e.g. `remotes/origin/master~1`, `master^0`), the `~N`/`^N` was previously passed through as the branch name and rejected by the Socket API as an invalid Git ref. The suffix is now stripped before the prefix split, producing the bare branch name. +## 2.2.80 + +- Hardened GitHub Actions workflows. +- Fixed broken links on PyPI page. + +## 2.2.79 + +- Updated minimum required Python version. +- Tweaked CI checks. + +## 2.2.78 + +- Fixed reachability filtering. +- Added config file support. + +## 2.2.77 + +- Fixed `has_manifest_files` failing to match root-level manifest files. + +## 2.2.76 + +- Added SARIF file output support. +- Improved reachability filtering. + +## 2.2.75 + +- Fixed `workspace` flag regression by updating SDK dependency. + +## 2.2.74 + +- Added `--workspace` flag to CLI args. +- Added GitLab branch protection flag. +- Added e2e tests for full scans and full scans with reachability. +- Bumped dependencies: `cryptography`, `virtualenv`, `filelock`, `urllib3`. + ## 2.2.71 - Added `strace` to the Docker image for debugging purposes. diff --git a/docs/cli-reference.md b/docs/cli-reference.md index bef8a5a..c26c3a4 100644 --- a/docs/cli-reference.md +++ b/docs/cli-reference.md @@ -305,7 +305,7 @@ The CLI will automatically install `@coana-tech/cli` if not present. Use `--reac | Parameter | Required | Default | Description | |:-------------------------|:---------|:--------|:----------------------------------------------------------------------| | `--ignore-commit-files` | False | False | Ignore commit files | -| `--disable-blocking` | False | False | Disable blocking mode | +| `--disable-blocking` | False | False | Non-blocking CI mode: the CLI always exits **0**, even when blocking alerts are present (including with `--strict-blocking`). Also exits 0 on uncaught runtime errors and Socket API failures, so the job is treated as successful while findings and errors are still logged. Takes precedence over `--strict-blocking`. | | `--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) | diff --git a/pyproject.toml b/pyproject.toml index 4b7f380..81b9d87 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,7 +6,7 @@ build-backend = "hatchling.build" [project] name = "socketsecurity" -version = "2.2.89" +version = "2.2.90" requires-python = ">= 3.11" license = {"file" = "LICENSE"} dependencies = [ @@ -16,7 +16,7 @@ dependencies = [ 'GitPython', 'packaging', 'python-dotenv', - "socketdev>=3.0.33,<4.0.0", + "socketdev>=3.1.0,<4.0.0", "bs4>=0.0.2", "markdown>=3.10", ] @@ -57,7 +57,7 @@ socketcli = "socketsecurity.socketcli:cli" socketclidev = "socketsecurity.socketcli:cli" [project.urls] -Homepage = "https://socket.dev" +Homepage = "https://github.com/SocketDev/socket-python-cli" [tool.coverage.run] source = ["socketsecurity"] diff --git a/socketsecurity/__init__.py b/socketsecurity/__init__.py index 9547cf5..90049de 100644 --- a/socketsecurity/__init__.py +++ b/socketsecurity/__init__.py @@ -1,3 +1,3 @@ __author__ = 'socket.dev' -__version__ = '2.2.89' +__version__ = '2.2.90' USER_AGENT = f'SocketPythonCLI/{__version__}' diff --git a/socketsecurity/config.py b/socketsecurity/config.py index 1d18c6a..1e2717c 100644 --- a/socketsecurity/config.py +++ b/socketsecurity/config.py @@ -695,7 +695,11 @@ def create_argument_parser() -> argparse.ArgumentParser: "--disable-blocking", dest="disable_blocking", action="store_true", - help="Disable blocking mode" + help=( + "Non-blocking CI mode: always exit 0, even when blocking alerts are present " + "(including with --strict-blocking), on uncaught errors, or on Socket API failures. " + "Findings and errors are still logged. Overrides --strict-blocking." + ), ) advanced_group.add_argument( "--disable_blocking", diff --git a/socketsecurity/core/__init__.py b/socketsecurity/core/__init__.py index daaa9a8..0a6b827 100644 --- a/socketsecurity/core/__init__.py +++ b/socketsecurity/core/__init__.py @@ -898,6 +898,7 @@ def get_license_text_via_purl(self, packages: dict[str, Package], batch_size: in results = self.sdk.purl.post( license=True, components=batch_components, + org_slug=self.config.org_slug, licenseattrib=True, licensedetails=True ) @@ -946,6 +947,8 @@ def get_added_and_removed_packages( ) except APIFailure as e: log.error(f"API Error: {e}") + if self.cli_config and self.cli_config.disable_blocking: + sys.exit(0) sys.exit(1) except Exception as e: import traceback @@ -1123,6 +1126,8 @@ def create_new_diff( os.unlink(temp_file) except OSError: pass + if self.cli_config and self.cli_config.disable_blocking: + sys.exit(0) sys.exit(1) except Exception as e: import traceback diff --git a/tests/core/test_package_and_alerts.py b/tests/core/test_package_and_alerts.py index 09a8455..f616479 100644 --- a/tests/core/test_package_and_alerts.py +++ b/tests/core/test_package_and_alerts.py @@ -228,4 +228,41 @@ def test_get_new_alerts_with_readded(self): # With ignore_readded=False new_alerts = Core.get_new_alerts(added_alerts, removed_alerts, ignore_readded=False) - assert len(new_alerts) == 1 + assert len(new_alerts) == 1 + + def test_get_license_text_via_purl_uses_org_scoped_endpoint(self, core, mock_sdk): + """Test license enrichment calls the org-scoped PURL SDK method.""" + core.sdk.purl = Mock() + core.sdk.purl.post.return_value = [ + { + "type": "npm", + "name": "lodash", + "version": "4.18.1", + "licenseAttrib": [{"name": "MIT"}], + "licenseDetails": [{"license": "MIT"}], + } + ] + + packages = { + "npm/lodash@4.18.1": Package( + id="pkg:npm/lodash@4.18.1", + type="npm", + name="lodash", + version="4.18.1", + score={}, + alerts=[], + topLevelAncestors=[], + ) + } + + result = core.get_license_text_via_purl(packages) + + core.sdk.purl.post.assert_called_once_with( + license=True, + components=[{"purl": "pkg:/npm/lodash@4.18.1"}], + org_slug="test-org", + licenseattrib=True, + licensedetails=True, + ) + assert result["npm/lodash@4.18.1"].licenseAttrib == [{"name": "MIT"}] + assert result["npm/lodash@4.18.1"].licenseDetails == [{"license": "MIT"}] diff --git a/tests/e2e/validate-reachability.sh b/tests/e2e/validate-reachability.sh index e6e365e..e32f004 100755 --- a/tests/e2e/validate-reachability.sh +++ b/tests/e2e/validate-reachability.sh @@ -27,31 +27,47 @@ else exit 1 fi -# 3. Run SARIF with --sarif-reachability all -socketcli \ - --target-path tests/e2e/fixtures/simple-npm \ - --reach \ - --sarif-file /tmp/sarif-all.sarif \ - --sarif-scope full \ - --sarif-reachability all \ - --disable-blocking \ - 2>/dev/null +FACTS_PATH="tests/e2e/fixtures/simple-npm/.socket.facts.json" +if [ ! -f "$FACTS_PATH" ]; then + echo "FAIL: Expected reachability facts at $FACTS_PATH after initial scan" + exit 1 +fi +echo "PASS: Reachability facts file present at $FACTS_PATH" + +# 3-4. Build SARIF from the facts file produced by the initial --reach run. +# Avoid re-running reach + full scan here; duplicate API scans are slow and flaky in CI. +uv run python -c " +import json +from pathlib import Path -# 4. Run SARIF with --sarif-reachability reachable (filtered) -socketcli \ - --target-path tests/e2e/fixtures/simple-npm \ - --reach \ - --sarif-file /tmp/sarif-reachable.sarif \ - --sarif-scope full \ - --sarif-reachability reachable \ - --disable-blocking \ - 2>/dev/null +from socketsecurity.core.alert_selection import load_components_with_alerts +from socketsecurity.core.messages import Messages + +target = 'tests/e2e/fixtures/simple-npm' +facts_file = '.socket.facts.json' +components = load_components_with_alerts(target, facts_file) +if not components: + raise SystemExit('FAIL: no components with alerts in .socket.facts.json') + +for outfile, reach_filter in [ + ('/tmp/sarif-all.sarif', 'all'), + ('/tmp/sarif-reachable.sarif', 'reachable'), +]: + sarif = Messages.create_security_comment_sarif_from_facts( + components, + reachability_filter=reach_filter, + grouping='instance', + ) + Path(outfile).write_text(json.dumps(sarif, indent=2)) + count = len(sarif['runs'][0]['results']) + print(f'PASS: Wrote {outfile} ({count} results, filter={reach_filter})') +" # 5. Verify reachable-only results are a subset of all results test -f /tmp/sarif-all.sarif test -f /tmp/sarif-reachable.sarif -python3 -c " +uv run python -c " import json with open('/tmp/sarif-all.sarif') as f: all_data = json.load(f) diff --git a/uv.lock b/uv.lock index 3266b98..ec4b94f 100644 --- a/uv.lock +++ b/uv.lock @@ -1155,20 +1155,20 @@ wheels = [ [[package]] name = "socketdev" -version = "3.0.33" +version = "3.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/19/8c/b4608637bda66dd32cf9a421cee66e93d429f7445c8bd709032772e0f4ca/socketdev-3.0.33.tar.gz", hash = "sha256:704d672649f27390733cef4cbdad9ce8dc994794a4af56f77d2f2dc815bfe762", size = 172013, upload-time = "2026-04-24T17:02:48.616Z" } +sdist = { url = "https://files.pythonhosted.org/packages/95/3e/50f05942e23d12043028d71c0e502c0d02c470686afc3dfbab0d1931e5c1/socketdev-3.1.0.tar.gz", hash = "sha256:a9534189d50c9f6c39e802280cc2317f830dd0c9970677e8cde843a69daa84ed", size = 172581, upload-time = "2026-05-21T17:14:03.607Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/78/4408ba21724ce0648e39bd17854b19186ab77f6baedbb8b98721d6dd287a/socketdev-3.0.33-py3-none-any.whl", hash = "sha256:642eebd0b01b884c6aba8b5264e749ff71310147104e8e8de02ab24e4eab5837", size = 66975, upload-time = "2026-04-24T17:02:46.439Z" }, + { url = "https://files.pythonhosted.org/packages/df/76/4fb37245468dd9c67137059ce6833db97d76c808bf0d10397f1b5a2943d1/socketdev-3.1.0-py3-none-any.whl", hash = "sha256:e9245916d423952aba4f0018bea2bca28740530ec30308089c48dddb2133e38a", size = 67255, upload-time = "2026-05-21T17:14:01.873Z" }, ] [[package]] name = "socketsecurity" -version = "2.2.89" +version = "2.2.90" source = { editable = "." } dependencies = [ { name = "bs4" }, @@ -1221,7 +1221,7 @@ requires-dist = [ { name = "python-dotenv" }, { name = "requests" }, { name = "ruff", marker = "extra == 'dev'", specifier = ">=0.3.0" }, - { name = "socketdev", specifier = ">=3.0.33,<4.0.0" }, + { name = "socketdev", specifier = ">=3.1.0,<4.0.0" }, { name = "twine", marker = "extra == 'dev'" }, { name = "uv", marker = "extra == 'dev'", specifier = ">=0.1.0" }, ]