socketsecurity 2.6.0__tar.gz → 2.6.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/CHANGELOG.md +22 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/PKG-INFO +1 -1
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/pyproject.toml +1 -1
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/__init__.py +1 -1
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/__init__.py +183 -30
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/conftest.py +21 -0
- socketsecurity-2.6.1/tests/core/test_diff_scan_polling.py +122 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_sdk_methods.py +26 -12
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/uv.lock +1 -1
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/CODEOWNERS +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/PULL_REQUEST_TEMPLATE/bug-fix.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/PULL_REQUEST_TEMPLATE/feature.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/PULL_REQUEST_TEMPLATE/improvement.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/actions/setup-docker/action.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/actions/setup-hatch/action.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/actions/setup-sfw/action.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/dependabot.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/dependency-review.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/docker-stable.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/e2e-test.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/package-check.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/pr-preview.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/python-tests.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/release.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/workflows/version-check.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.github/zizmor.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.gitignore +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.hooks/sync_version.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.pre-commit-config.yaml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/.python-version +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/CONTRIBUTING.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/Dockerfile +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/Dockerfile.preview +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/LICENSE +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/Makefile +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/README.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/docs/ci-cd.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/docs/cli-reference.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/docs/development.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/docs/troubleshooting.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/examples/config/sarif-dashboard-parity.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/examples/config/sarif-dashboard-parity.toml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/examples/config/sarif-diff-ci-cd.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/examples/config/sarif-diff-ci-cd.toml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/examples/config/sarif-instance-detail.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/examples/config/sarif-instance-detail.toml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/instructions/gitlab-commit-status/uat.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/pytest.ini +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/scripts/build_container.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/scripts/build_container_flexible.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/scripts/deploy-test-docker.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/scripts/deploy-test-pypi.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/scripts/docker-entrypoint.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/scripts/run.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/session.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socket.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/config.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/alert_selection.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/classes.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/cli_client.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/cli_run.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/exceptions.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/git_interface.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/helper/__init__.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/helper/socket_facts_loader.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/lazy_file_loader.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/log_uploader.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/logging.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/messages.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/resource_utils.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/scm/__init__.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/scm/base.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/scm/client.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/scm/github.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/scm/gitlab.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/scm_comments.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/socket_config.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/streaming.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/tools/reachability.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/utils.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/fossa_compat.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/output.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/__init__.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/base.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/formatters/__init__.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/formatters/slack.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/jira.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/manager.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/slack.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/teams.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/plugins/webhook.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/socketcli.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/__init__.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/create_diff_input.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_diff_alerts.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_diff_generation.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_facts_compression.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_has_manifest_files.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_package_and_alerts.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/core/test_supporting_methods.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/create_response.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/diff/stream_diff.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/diff/stream_diff_full.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/head_scan/metadata.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/head_scan/stream_scan.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/head_scan/stream_scan_full.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/new_scan/metadata.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/new_scan/stream_scan.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/repos/repo_info_error.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/repos/repo_info_no_head.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/repos/repo_info_success.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/settings/security-policy.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/fixtures/simple-npm/index.js +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/fixtures/simple-npm/package.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/fixtures/simple-pypi/requirements.txt +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/reach-facts-probe.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/validate-gitlab.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/validate-json.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/validate-reachability.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/validate-sarif.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/validate-scan.sh +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/README.md +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-analyze-empty.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-analyze-populated.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-sbom-empty-deep.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-sbom-populated.json +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/__init__.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_alert_selection.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_cli_config.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_cli_run.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_client.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_config.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_dependency_overview.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_disable_ignore.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_exclude_paths.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_fossa_compat.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_fossa_parity.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_full_scan_retry.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_gitlab_auth.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_gitlab_auth_fallback.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_gitlab_commit_status.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_gitlab_format.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_ignore_telemetry_filtering.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_include_dirs.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_log_uploader.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_output.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_reachability.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_slack_plugin.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_socketcli.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_streaming.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/unit/test_tier1_finalize.py +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/workflows/bitbucket-pipelines.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/workflows/buildkite.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/workflows/github-actions.yml +0 -0
- {socketsecurity-2.6.0 → socketsecurity-2.6.1}/workflows/gitlab-ci.yml +0 -0
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 2.6.1
|
|
4
|
+
|
|
5
|
+
### Changed: scan comparison now polls the diff-scans endpoints
|
|
6
|
+
|
|
7
|
+
- Diff mode no longer holds a single idle HTTP connection open while the API
|
|
8
|
+
computes the scan comparison. The CLI now creates a diff-scan resource
|
|
9
|
+
(`POST /orgs/{org}/diff-scans/from-ids`) and polls
|
|
10
|
+
`GET /orgs/{org}/diff-scans/{id}?cached=true` with short, bounded requests
|
|
11
|
+
until the comparison is ready (HTTP 200 instead of 202). This fixes
|
|
12
|
+
intermittent `Connection reset by peer` failures on the final comparison
|
|
13
|
+
step when scans take several minutes to compare and network middleboxes
|
|
14
|
+
(e.g. Azure NAT gateways, which default to a 4-minute TCP idle timeout)
|
|
15
|
+
reap the idle connection.
|
|
16
|
+
- Duplicate scan pairs are resolved after an HTTP 409 and then polled through
|
|
17
|
+
the same cached endpoint. This avoids automatically following the API's 302
|
|
18
|
+
duplicate redirect with an uncached, potentially long-lived GET request.
|
|
19
|
+
- The change is transparent: no flags or workflow changes are needed. If the
|
|
20
|
+
org API token is missing the `diff-scans:create`, `diff-scans:list` or
|
|
21
|
+
`full-scans:list` scopes — or the new flow fails for any other reason — the
|
|
22
|
+
CLI logs a warning and falls back to the legacy streaming comparison.
|
|
23
|
+
- Requires the pinned `socketdev==3.5.0` SDK.
|
|
24
|
+
|
|
3
25
|
## 2.6.0
|
|
4
26
|
|
|
5
27
|
### Changed: pin all Python dependencies
|
|
@@ -15,7 +15,7 @@ if TYPE_CHECKING:
|
|
|
15
15
|
from socketsecurity.config import CliConfig
|
|
16
16
|
from socketdev import socketdev
|
|
17
17
|
from socketdev.exceptions import APIFailure
|
|
18
|
-
from socketdev.fullscans import FullScanParams, SocketArtifact
|
|
18
|
+
from socketdev.fullscans import DiffArtifacts, FullScanParams, SocketArtifact
|
|
19
19
|
from socketdev.org import Organization
|
|
20
20
|
from socketdev.repos import RepositoryInfo
|
|
21
21
|
import copy
|
|
@@ -92,6 +92,25 @@ FULL_SCAN_UPLOAD_BACKOFF_SCHEDULE_SECONDS = (10.0, 30.0, None)
|
|
|
92
92
|
FULL_SCAN_UPLOAD_MAX_ATTEMPTS = len(FULL_SCAN_UPLOAD_BACKOFF_SCHEDULE_SECONDS)
|
|
93
93
|
FULL_SCAN_UPLOAD_BACKOFF_JITTER_SECONDS = 2.0
|
|
94
94
|
|
|
95
|
+
# Diff-scan polling policy. The legacy scan comparison (fullscans.stream_diff) holds a
|
|
96
|
+
# single HTTP connection open, fully idle, while the backend computes the diff; network
|
|
97
|
+
# middleboxes with TCP idle timeouts (notably Azure NAT gateways, which default to
|
|
98
|
+
# 4 minutes) kill that connection with a RST, surfacing as an intermittent
|
|
99
|
+
# ConnectionResetError on large scans. The diff-scans flow instead creates a
|
|
100
|
+
# diff-scan resource and polls its cached endpoint with short bounded requests: the API
|
|
101
|
+
# answers 202 while the comparison is still computing and 200 with the result once it is
|
|
102
|
+
# ready, so no connection is ever idle long enough to be reaped.
|
|
103
|
+
#
|
|
104
|
+
# Each poll consumes 1 unit of API quota, so the interval backs off toward
|
|
105
|
+
# DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS to stay quota-friendly on comparisons that take
|
|
106
|
+
# minutes to compute. The timeout is a backstop against a diff scan that never
|
|
107
|
+
# completes; on expiry (or any other failure of this flow) the caller falls back to the
|
|
108
|
+
# legacy streaming comparison rather than failing the scan outright.
|
|
109
|
+
DIFF_SCAN_POLL_INITIAL_INTERVAL_SECONDS = 5.0
|
|
110
|
+
DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS = 30.0
|
|
111
|
+
DIFF_SCAN_POLL_BACKOFF_MULTIPLIER = 1.5
|
|
112
|
+
DIFF_SCAN_POLL_TIMEOUT_SECONDS = 30 * 60.0
|
|
113
|
+
|
|
95
114
|
|
|
96
115
|
def _humanize_alert_type(alert_type: str) -> str:
|
|
97
116
|
"""Convert a camelCase/PascalCase alert type into a Title-Cased label.
|
|
@@ -1303,6 +1322,120 @@ class Core:
|
|
|
1303
1322
|
|
|
1304
1323
|
return packages
|
|
1305
1324
|
|
|
1325
|
+
def get_diff_scan_artifacts(
|
|
1326
|
+
self,
|
|
1327
|
+
head_full_scan_id: str,
|
|
1328
|
+
new_full_scan_id: str
|
|
1329
|
+
) -> DiffArtifacts:
|
|
1330
|
+
"""Compare two full scans via the diff-scans endpoints, polling for the result.
|
|
1331
|
+
|
|
1332
|
+
Creates a diff-scan resource from the two full scan IDs, then polls
|
|
1333
|
+
``GET /orgs/{org}/diff-scans/{id}?cached=true`` until the API returns the
|
|
1334
|
+
computed comparison (200) instead of a processing status (202). Unlike the
|
|
1335
|
+
legacy ``fullscans.stream_diff`` call, no request is ever left idle while
|
|
1336
|
+
the backend computes, so the comparison survives network idle timeouts.
|
|
1337
|
+
See the DIFF_SCAN_POLL_* constants for the polling policy.
|
|
1338
|
+
|
|
1339
|
+
Requires an org token with the ``diff-scans:create``, ``diff-scans:list``
|
|
1340
|
+
and ``full-scans:list`` scopes; callers are expected to catch failures and
|
|
1341
|
+
fall back to the legacy streaming comparison.
|
|
1342
|
+
|
|
1343
|
+
Note that cached diff-scan responses always embed per-package license
|
|
1344
|
+
details (the API ignores ``omit_license_details`` when ``cached=true``),
|
|
1345
|
+
so unlike the legacy streaming comparison there is no lean-response
|
|
1346
|
+
option here; see the comment on ``poll_params`` below.
|
|
1347
|
+
|
|
1348
|
+
Args:
|
|
1349
|
+
head_full_scan_id: The before/base full scan ID
|
|
1350
|
+
new_full_scan_id: The after/head full scan ID
|
|
1351
|
+
|
|
1352
|
+
Returns:
|
|
1353
|
+
DiffArtifacts with the added/removed/unchanged/replaced/updated lists
|
|
1354
|
+
"""
|
|
1355
|
+
create_params = {
|
|
1356
|
+
"before": head_full_scan_id,
|
|
1357
|
+
"after": new_full_scan_id,
|
|
1358
|
+
"description": f"Socket Security CLI v{__version__} scan comparison",
|
|
1359
|
+
}
|
|
1360
|
+
try:
|
|
1361
|
+
result = self.sdk.diffscans.create_from_ids(self.config.org_slug, create_params)
|
|
1362
|
+
diff_scan = result.get("diff_scan") or {}
|
|
1363
|
+
response_summary = result
|
|
1364
|
+
except APIFailure as error:
|
|
1365
|
+
if error.status_code != 409:
|
|
1366
|
+
raise
|
|
1367
|
+
|
|
1368
|
+
# Do not use on_duplicate=redirect here. The SDK follows that 302
|
|
1369
|
+
# automatically with a GET that lacks cached=true, which can leave
|
|
1370
|
+
# the connection idle while an existing diff scan is still computing.
|
|
1371
|
+
# Resolve the duplicate resource explicitly so every result fetch
|
|
1372
|
+
# continues through the bounded cached polling path below.
|
|
1373
|
+
existing = self.sdk.diffscans.list(
|
|
1374
|
+
self.config.org_slug,
|
|
1375
|
+
params={
|
|
1376
|
+
"before_full_scan_id": head_full_scan_id,
|
|
1377
|
+
"after_full_scan_id": new_full_scan_id,
|
|
1378
|
+
"per_page": 1,
|
|
1379
|
+
},
|
|
1380
|
+
)
|
|
1381
|
+
matches = existing.get("results") or []
|
|
1382
|
+
diff_scan = matches[0] if matches else {}
|
|
1383
|
+
response_summary = existing
|
|
1384
|
+
|
|
1385
|
+
diff_scan_id = diff_scan.get("id")
|
|
1386
|
+
if not diff_scan_id:
|
|
1387
|
+
raise Exception(
|
|
1388
|
+
"Error creating or resolving diff scan: "
|
|
1389
|
+
f"unexpected response: {str(response_summary)[:500]}"
|
|
1390
|
+
)
|
|
1391
|
+
artifacts_dict = diff_scan.get("artifacts")
|
|
1392
|
+
|
|
1393
|
+
# cached=true is the polling contract (202 while computing, 200 when
|
|
1394
|
+
# ready). The API ignores omit_license_details when cached=true - cached
|
|
1395
|
+
# results always embed license details - so there is no lean-response
|
|
1396
|
+
# option on this path (unlike stream_diff with
|
|
1397
|
+
# include_license_details=false, the lean-payload mitigation). If that extra
|
|
1398
|
+
# payload ever gets a response truncated on a huge dependency tree,
|
|
1399
|
+
# response.json() fails and the caller falls back to the legacy
|
|
1400
|
+
# streaming comparison, which still requests the lean payload.
|
|
1401
|
+
poll_params = {"cached": "true"}
|
|
1402
|
+
deadline = time.monotonic() + DIFF_SCAN_POLL_TIMEOUT_SECONDS
|
|
1403
|
+
interval = DIFF_SCAN_POLL_INITIAL_INTERVAL_SECONDS
|
|
1404
|
+
while artifacts_dict is None:
|
|
1405
|
+
try:
|
|
1406
|
+
response = self.sdk.diffscans.get(self.config.org_slug, diff_scan_id, params=poll_params)
|
|
1407
|
+
except APIFailure as error:
|
|
1408
|
+
if not error.is_transient_error():
|
|
1409
|
+
raise
|
|
1410
|
+
# A dropped/timed-out poll is retryable: the diff scan keeps
|
|
1411
|
+
# computing server-side regardless of what happens to any one poll.
|
|
1412
|
+
log.warning(
|
|
1413
|
+
f"Transient error polling diff scan {diff_scan_id} "
|
|
1414
|
+
f"({type(error).__name__}), retrying in {interval:.0f}s"
|
|
1415
|
+
)
|
|
1416
|
+
response = {"status": "processing"}
|
|
1417
|
+
if response.get("status") != "processing":
|
|
1418
|
+
scan = response.get("diff_scan") or {}
|
|
1419
|
+
if scan.get("artifacts") is None:
|
|
1420
|
+
raise Exception(
|
|
1421
|
+
f"Error fetching diff scan {diff_scan_id}: unexpected response: {str(response)[:500]}"
|
|
1422
|
+
)
|
|
1423
|
+
artifacts_dict = scan["artifacts"]
|
|
1424
|
+
break
|
|
1425
|
+
if time.monotonic() >= deadline:
|
|
1426
|
+
raise Exception(
|
|
1427
|
+
f"Timed out waiting for diff scan {diff_scan_id} after "
|
|
1428
|
+
f"{DIFF_SCAN_POLL_TIMEOUT_SECONDS:.0f} seconds"
|
|
1429
|
+
)
|
|
1430
|
+
log.debug(f"Diff scan {diff_scan_id} still processing, polling again in {interval:.0f}s")
|
|
1431
|
+
time.sleep(interval)
|
|
1432
|
+
interval = min(interval * DIFF_SCAN_POLL_BACKOFF_MULTIPLIER, DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS)
|
|
1433
|
+
|
|
1434
|
+
return DiffArtifacts.from_dict({
|
|
1435
|
+
key: artifacts_dict.get(key) or []
|
|
1436
|
+
for key in ("added", "removed", "unchanged", "replaced", "updated")
|
|
1437
|
+
})
|
|
1438
|
+
|
|
1306
1439
|
def get_added_and_removed_packages(
|
|
1307
1440
|
self,
|
|
1308
1441
|
head_full_scan_id: str,
|
|
@@ -1315,8 +1448,12 @@ class Core:
|
|
|
1315
1448
|
Args:
|
|
1316
1449
|
head_full_scan_id: Previous scan (maybe None if first scan)
|
|
1317
1450
|
new_full_scan_id: New scan just created
|
|
1318
|
-
include_license_details: Whether to ask the
|
|
1319
|
-
per-package license attribution/details in the
|
|
1451
|
+
include_license_details: Whether to ask the *legacy streaming* diff
|
|
1452
|
+
endpoint to embed per-package license attribution/details in the
|
|
1453
|
+
response. Only consulted on the fallback path: the primary
|
|
1454
|
+
diff-scans path always receives embedded license details, since
|
|
1455
|
+
the API ignores ``omit_license_details`` for cached reads (see
|
|
1456
|
+
get_diff_scan_artifacts).
|
|
1320
1457
|
|
|
1321
1458
|
Defaults to ``False`` on purpose. The diff endpoint exists to
|
|
1322
1459
|
compare alerts between two scans; the license fields it can embed
|
|
@@ -1343,39 +1480,55 @@ class Core:
|
|
|
1343
1480
|
|
|
1344
1481
|
log.info(f"Comparing scans - Head scan ID: {head_full_scan_id}, New scan ID: {new_full_scan_id}")
|
|
1345
1482
|
diff_start = time.time()
|
|
1483
|
+
diff_artifacts = None
|
|
1346
1484
|
try:
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
head_full_scan_id,
|
|
1351
|
-
new_full_scan_id,
|
|
1352
|
-
use_types=True,
|
|
1353
|
-
include_license_details=str(include_license_details).lower()
|
|
1354
|
-
).data
|
|
1485
|
+
diff_artifacts = self.get_diff_scan_artifacts(
|
|
1486
|
+
head_full_scan_id,
|
|
1487
|
+
new_full_scan_id
|
|
1355
1488
|
)
|
|
1356
|
-
except
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1489
|
+
except Exception as error:
|
|
1490
|
+
# SDK error messages can span many lines (path + response headers); the
|
|
1491
|
+
# first line carries the status, which is all the warning needs.
|
|
1492
|
+
error_summary = str(error).strip().splitlines()[0] if str(error).strip() else ""
|
|
1493
|
+
log.warning(
|
|
1494
|
+
f"Diff scan comparison failed with {type(error).__name__}({error_summary}), "
|
|
1495
|
+
"falling back to the streaming scan comparison"
|
|
1496
|
+
)
|
|
1497
|
+
|
|
1498
|
+
if diff_artifacts is None:
|
|
1499
|
+
try:
|
|
1500
|
+
diff_artifacts = (
|
|
1501
|
+
self.sdk.fullscans.stream_diff(
|
|
1502
|
+
self.config.org_slug,
|
|
1503
|
+
head_full_scan_id,
|
|
1504
|
+
new_full_scan_id,
|
|
1505
|
+
use_types=True,
|
|
1506
|
+
include_license_details=str(include_license_details).lower()
|
|
1507
|
+
).data.artifacts
|
|
1508
|
+
)
|
|
1509
|
+
except APIFailure as e:
|
|
1510
|
+
log.error(f"API Error: {e}")
|
|
1511
|
+
if self.cli_config and self.cli_config.disable_blocking:
|
|
1512
|
+
sys.exit(0)
|
|
1513
|
+
sys.exit(1)
|
|
1514
|
+
except Exception as e:
|
|
1515
|
+
import traceback
|
|
1516
|
+
log.error(f"Error getting diff report: {str(e)}")
|
|
1517
|
+
log.error(f"Stack trace:\n{traceback.format_exc()}")
|
|
1518
|
+
raise
|
|
1366
1519
|
|
|
1367
1520
|
diff_end = time.time()
|
|
1368
1521
|
log.info(f"Diff Report Gathered in {diff_end - diff_start:.2f} seconds")
|
|
1369
1522
|
log.info("Diff report artifact counts:")
|
|
1370
|
-
log.info(f"Added: {len(
|
|
1371
|
-
log.info(f"Removed: {len(
|
|
1372
|
-
log.info(f"Unchanged: {len(
|
|
1373
|
-
log.info(f"Replaced: {len(
|
|
1374
|
-
log.info(f"Updated: {len(
|
|
1375
|
-
|
|
1376
|
-
added_artifacts =
|
|
1377
|
-
removed_artifacts =
|
|
1378
|
-
unchanged_artifacts =
|
|
1523
|
+
log.info(f"Added: {len(diff_artifacts.added)}")
|
|
1524
|
+
log.info(f"Removed: {len(diff_artifacts.removed)}")
|
|
1525
|
+
log.info(f"Unchanged: {len(diff_artifacts.unchanged)}")
|
|
1526
|
+
log.info(f"Replaced: {len(diff_artifacts.replaced)}")
|
|
1527
|
+
log.info(f"Updated: {len(diff_artifacts.updated)}")
|
|
1528
|
+
|
|
1529
|
+
added_artifacts = diff_artifacts.added + diff_artifacts.updated
|
|
1530
|
+
removed_artifacts = diff_artifacts.removed + diff_artifacts.replaced
|
|
1531
|
+
unchanged_artifacts = diff_artifacts.unchanged
|
|
1379
1532
|
|
|
1380
1533
|
added_packages: Dict[str, Package] = {}
|
|
1381
1534
|
removed_packages: Dict[str, Package] = {}
|
|
@@ -87,6 +87,22 @@ def stream_diff_response(data_dir, load_json):
|
|
|
87
87
|
})
|
|
88
88
|
|
|
89
89
|
|
|
90
|
+
@pytest.fixture
|
|
91
|
+
def diff_scan_get_response(data_dir, load_json):
|
|
92
|
+
"""GET /orgs/{org}/diff-scans/{id} response built from the stream_diff fixture.
|
|
93
|
+
|
|
94
|
+
The diff-scans endpoint returns the same artifact shape as the legacy
|
|
95
|
+
streaming diff, wrapped in a diff_scan object.
|
|
96
|
+
"""
|
|
97
|
+
json_data = load_json(data_dir / "fullscans" / "diff" / "stream_diff.json")
|
|
98
|
+
return {
|
|
99
|
+
"diff_scan": {
|
|
100
|
+
"id": "diff-scan-123",
|
|
101
|
+
"artifacts": json_data["data"]["artifacts"],
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
|
|
90
106
|
|
|
91
107
|
|
|
92
108
|
|
|
@@ -138,6 +154,7 @@ def mock_sdk_with_responses(
|
|
|
138
154
|
new_scan_metadata,
|
|
139
155
|
new_scan_stream,
|
|
140
156
|
stream_diff_response,
|
|
157
|
+
diff_scan_get_response,
|
|
141
158
|
create_full_scan_response,
|
|
142
159
|
):
|
|
143
160
|
sdk = mock_socket_sdk.return_value
|
|
@@ -173,4 +190,8 @@ def mock_sdk_with_responses(
|
|
|
173
190
|
lambda org_slug, head_id, new_id, **kwargs: stream_diff_response
|
|
174
191
|
)
|
|
175
192
|
|
|
193
|
+
# Diff-scans endpoints (primary scan-comparison path)
|
|
194
|
+
sdk.diffscans.create_from_ids.return_value = {"diff_scan": {"id": "diff-scan-123"}}
|
|
195
|
+
sdk.diffscans.get.return_value = diff_scan_get_response
|
|
196
|
+
|
|
176
197
|
return sdk
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
"""Tests for the diff-scans polling scan comparison.
|
|
2
|
+
|
|
3
|
+
The comparison must never hold an idle connection open: it creates a diff-scan
|
|
4
|
+
resource and polls the cached endpoint (202 while processing, 200 when ready),
|
|
5
|
+
falling back to the legacy streaming diff if the new flow is unavailable.
|
|
6
|
+
"""
|
|
7
|
+
import pytest
|
|
8
|
+
from socketdev.exceptions import APIConnectionError, APIFailure
|
|
9
|
+
|
|
10
|
+
import socketsecurity.core as core_module
|
|
11
|
+
from socketsecurity.core import Core
|
|
12
|
+
from socketsecurity.core.socket_config import SocketConfig
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@pytest.fixture
|
|
16
|
+
def core(mock_sdk_with_responses):
|
|
17
|
+
config = SocketConfig(api_key="test_key")
|
|
18
|
+
return Core(config=config, sdk=mock_sdk_with_responses)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@pytest.fixture
|
|
22
|
+
def no_sleep(mocker):
|
|
23
|
+
return mocker.patch("socketsecurity.core.time.sleep")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_polls_until_diff_scan_ready(core, diff_scan_get_response, no_sleep):
|
|
27
|
+
"""202 processing responses are polled through until the 200 result arrives."""
|
|
28
|
+
processing = {"status": "processing", "id": "diff-scan-123"}
|
|
29
|
+
core.sdk.diffscans.get.side_effect = [processing, processing, diff_scan_get_response]
|
|
30
|
+
|
|
31
|
+
artifacts = core.get_diff_scan_artifacts("head", "new")
|
|
32
|
+
|
|
33
|
+
assert core.sdk.diffscans.get.call_count == 3
|
|
34
|
+
assert no_sleep.call_count == 2 # slept between polls, never during them
|
|
35
|
+
assert len(artifacts.added) > 0
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_poll_interval_backs_off(core, diff_scan_get_response, no_sleep, monkeypatch):
|
|
39
|
+
"""The poll interval grows toward the max so long comparisons stay quota-friendly."""
|
|
40
|
+
monkeypatch.setattr(core_module, "DIFF_SCAN_POLL_INITIAL_INTERVAL_SECONDS", 4.0)
|
|
41
|
+
monkeypatch.setattr(core_module, "DIFF_SCAN_POLL_MAX_INTERVAL_SECONDS", 10.0)
|
|
42
|
+
processing = {"status": "processing", "id": "diff-scan-123"}
|
|
43
|
+
core.sdk.diffscans.get.side_effect = [processing] * 4 + [diff_scan_get_response]
|
|
44
|
+
|
|
45
|
+
core.get_diff_scan_artifacts("head", "new")
|
|
46
|
+
|
|
47
|
+
waits = [call.args[0] for call in no_sleep.call_args_list]
|
|
48
|
+
assert waits == [4.0, 6.0, 9.0, 10.0] # 1.5x backoff, capped at the max
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_transient_poll_error_is_retried(core, diff_scan_get_response, no_sleep):
|
|
52
|
+
"""A dropped poll doesn't abandon the flow - the diff keeps computing server-side."""
|
|
53
|
+
core.sdk.diffscans.get.side_effect = [APIConnectionError("reset"), diff_scan_get_response]
|
|
54
|
+
|
|
55
|
+
artifacts = core.get_diff_scan_artifacts("head", "new")
|
|
56
|
+
|
|
57
|
+
assert core.sdk.diffscans.get.call_count == 2
|
|
58
|
+
assert len(artifacts.added) > 0
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_non_transient_poll_error_raises(core, no_sleep):
|
|
62
|
+
"""Deterministic API errors (e.g. 403 missing scopes) propagate to the caller."""
|
|
63
|
+
core.sdk.diffscans.get.side_effect = APIFailure("forbidden", status_code=403)
|
|
64
|
+
|
|
65
|
+
with pytest.raises(APIFailure):
|
|
66
|
+
core.get_diff_scan_artifacts("head", "new")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_poll_timeout_raises(core, no_sleep, monkeypatch):
|
|
70
|
+
"""A diff scan that never completes hits the polling backstop."""
|
|
71
|
+
monkeypatch.setattr(core_module, "DIFF_SCAN_POLL_TIMEOUT_SECONDS", 0.0)
|
|
72
|
+
core.sdk.diffscans.get.return_value = {"status": "processing", "id": "diff-scan-123"}
|
|
73
|
+
|
|
74
|
+
with pytest.raises(Exception, match="Timed out waiting for diff scan"):
|
|
75
|
+
core.get_diff_scan_artifacts("head", "new")
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
def test_duplicate_conflict_uses_cached_polling(core, diff_scan_get_response):
|
|
79
|
+
"""A duplicate is resolved explicitly so the SDK cannot follow an uncached redirect."""
|
|
80
|
+
core.sdk.diffscans.create_from_ids.side_effect = APIFailure(
|
|
81
|
+
"duplicate", status_code=409
|
|
82
|
+
)
|
|
83
|
+
core.sdk.diffscans.list.return_value = {
|
|
84
|
+
"results": [{"id": "existing-diff-scan"}],
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
artifacts = core.get_diff_scan_artifacts("head", "new")
|
|
88
|
+
|
|
89
|
+
create_params = core.sdk.diffscans.create_from_ids.call_args.args[1]
|
|
90
|
+
assert "on_duplicate" not in create_params
|
|
91
|
+
core.sdk.diffscans.list.assert_called_once_with(
|
|
92
|
+
core.config.org_slug,
|
|
93
|
+
params={
|
|
94
|
+
"before_full_scan_id": "head",
|
|
95
|
+
"after_full_scan_id": "new",
|
|
96
|
+
"per_page": 1,
|
|
97
|
+
},
|
|
98
|
+
)
|
|
99
|
+
core.sdk.diffscans.get.assert_called_once_with(
|
|
100
|
+
core.config.org_slug,
|
|
101
|
+
"existing-diff-scan",
|
|
102
|
+
params={"cached": "true"},
|
|
103
|
+
)
|
|
104
|
+
assert len(artifacts.added) > 0
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def test_fallback_to_streaming_diff_on_failure(core):
|
|
108
|
+
"""If the diff-scans flow fails (e.g. token missing the diff-scans scopes),
|
|
109
|
+
the comparison falls back to the legacy streaming diff transparently."""
|
|
110
|
+
core.sdk.diffscans.create_from_ids.side_effect = APIFailure("forbidden", status_code=403)
|
|
111
|
+
|
|
112
|
+
added, removed, all_packages = core.get_added_and_removed_packages("head", "new")
|
|
113
|
+
|
|
114
|
+
core.sdk.fullscans.stream_diff.assert_called_once_with(
|
|
115
|
+
core.config.org_slug,
|
|
116
|
+
"head",
|
|
117
|
+
"new",
|
|
118
|
+
use_types=True,
|
|
119
|
+
include_license_details="false",
|
|
120
|
+
)
|
|
121
|
+
assert "dp3" in added
|
|
122
|
+
assert "dp2" in removed
|
|
@@ -225,19 +225,27 @@ def test_get_added_and_removed_packages(core):
|
|
|
225
225
|
"""Test getting added and removed packages between two scans"""
|
|
226
226
|
# Get two different scans to compare
|
|
227
227
|
added, removed, all_packages = core.get_added_and_removed_packages("head", "new")
|
|
228
|
-
|
|
229
|
-
# Verify SDK was called correctly
|
|
230
|
-
#
|
|
231
|
-
#
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
228
|
+
|
|
229
|
+
# Verify SDK was called correctly: the comparison goes through the diff-scans
|
|
230
|
+
# endpoints (create + poll) rather than the legacy streaming diff, so no
|
|
231
|
+
# connection is left idle while the backend computes.
|
|
232
|
+
create_args = core.sdk.diffscans.create_from_ids.call_args
|
|
233
|
+
assert create_args[0][0] == core.config.org_slug
|
|
234
|
+
create_params = create_args[0][1]
|
|
235
|
+
assert create_params["before"] == "head"
|
|
236
|
+
assert create_params["after"] == "new"
|
|
237
|
+
assert "on_duplicate" not in create_params
|
|
238
|
+
|
|
239
|
+
# cached=true is the polling contract (202 while computing, 200 when ready).
|
|
240
|
+
# No omit_license_details param: the API ignores it for cached reads (cached
|
|
241
|
+
# results always embed license details), so sending it would only suggest a
|
|
242
|
+
# leanness guarantee this path doesn't have.
|
|
243
|
+
core.sdk.diffscans.get.assert_called_once_with(
|
|
235
244
|
core.config.org_slug,
|
|
236
|
-
"
|
|
237
|
-
"
|
|
238
|
-
use_types=True,
|
|
239
|
-
include_license_details="false",
|
|
245
|
+
"diff-scan-123",
|
|
246
|
+
params={"cached": "true"},
|
|
240
247
|
)
|
|
248
|
+
core.sdk.fullscans.stream_diff.assert_not_called()
|
|
241
249
|
|
|
242
250
|
# Verify the results
|
|
243
251
|
# Added packages
|
|
@@ -252,7 +260,13 @@ def test_get_added_and_removed_packages(core):
|
|
|
252
260
|
assert "pypi/direct_package_1@1.6.0" in all_packages # Unchanged package is in full package map
|
|
253
261
|
|
|
254
262
|
def test_get_added_and_removed_packages_license_override(core):
|
|
255
|
-
"""
|
|
263
|
+
"""include_license_details only governs the legacy fallback path now: the
|
|
264
|
+
diff-scans path always receives embedded license details (the API ignores
|
|
265
|
+
omit_license_details for cached reads), so the seam must survive through to
|
|
266
|
+
the stream_diff call when the primary path is unavailable."""
|
|
267
|
+
from socketdev.exceptions import APIFailure
|
|
268
|
+
|
|
269
|
+
core.sdk.diffscans.create_from_ids.side_effect = APIFailure("forbidden", status_code=403)
|
|
256
270
|
core.get_added_and_removed_packages("head", "new", include_license_details=True)
|
|
257
271
|
|
|
258
272
|
core.sdk.fullscans.stream_diff.assert_called_once_with(
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/socketsecurity/core/helper/socket_facts_loader.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/diff/stream_diff_full.json
RENAMED
|
File without changes
|
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/head_scan/stream_scan.json
RENAMED
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/head_scan/stream_scan_full.json
RENAMED
|
File without changes
|
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/data/fullscans/new_scan/stream_scan.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/e2e/fixtures/simple-pypi/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-analyze-populated.json
RENAMED
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-sbom-empty-deep.json
RENAMED
|
File without changes
|
{socketsecurity-2.6.0 → socketsecurity-2.6.1}/tests/fixtures/fossa/fossa-sbom-populated.json
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|