Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
49 changes: 48 additions & 1 deletion .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ jobs:
--enable-debug
validate: tests/e2e/validate-reachability.sh
setup-node: "true"
# The tier-1 reachability backend intermittently returns empty
# results while the CLI reports success. The probe exits 0 when
# the facts file has alerted components; anything else is
# retried before validation fails the job.
retry-probe: bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm

- name: gitlab
args: >-
Expand Down Expand Up @@ -94,17 +99,59 @@ jobs:
run: pip install uv

- name: Run Socket CLI
id: run-cli
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
RETRY_PROBE: ${{ matrix.retry-probe }}
run: |
set -o pipefail
socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log
# Entries with retry-probe get up to 3 attempts: the probe exits 0
# when the scan output looks complete, and a run that fails it is
# re-run on the assumption of a transient backend failure. A
# persistent incomplete result still reaches validation. Validation
# only treats the explicit zero-project backend signature as
# inconclusive; any other empty result remains a failure. Retries are
# surfaced as warning annotations so flake frequency stays visible.
max_attempts=3
attempt=1
while :; do
socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log
[ -z "$RETRY_PROBE" ] && break
if bash -c "$RETRY_PROBE"; then
break
fi
if [ "$attempt" -ge "$max_attempts" ]; then
echo "::warning title=e2e-${{ matrix.name }} incomplete results::output still fails the completeness probe after ${max_attempts} attempts; letting validation classify the result"
echo "diagnostics=true" >> "$GITHUB_OUTPUT"
break
fi
echo "::warning title=e2e-${{ matrix.name }} transient retry::attempt ${attempt} failed the completeness probe (suspected backend transient); retrying"
echo "e2e-${{ matrix.name }}: retry after attempt ${attempt} — completeness probe failed (suspected transient)" >> "$GITHUB_STEP_SUMMARY"
attempt=$((attempt+1))
sleep 30
done
- name: Validate results
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
run: bash ${{ matrix.validate }}

- name: Upload diagnostics on failure
if: failure() || steps.run-cli.outputs.diagnostics == 'true'
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: e2e-${{ matrix.name }}-diagnostics-attempt${{ github.run_attempt }}
path: |
/tmp/e2e-output.log
/tmp/*.sarif
tests/e2e/fixtures/simple-npm/.socket.facts.json
tests/e2e/fixtures/simple-pypi/.socket.facts.json
gl-*.json
license_output.json
if-no-files-found: ignore
include-hidden-files: true
retention-days: 14

# Branch protection requires the e2e-* checks, but the `e2e` job above is
# skipped on PRs that can't access repository secrets -- fork PRs and
# Dependabot PRs. A job skipped via a job-level `if` never expands its
Expand Down
24 changes: 24 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,29 @@
# Changelog

## 2.6.0

### Changed: pin all Python dependencies

- Pinned every runtime dependency in `pyproject.toml` to an exact version;
several were previously unpinned or open ranges.
- Replaced the `bs4` shim package with a direct, pinned `beautifulsoup4`
dependency (the shim provided no version control over the actual library).
- Pinned the bundled `socketdev` SDK to `3.5.0` (previously `>=3.3.0,<4.0.0`).
- Docker images now install Python dependencies from the committed `uv.lock`
with pip hash verification (`--require-hashes`), so image builds no longer
resolve dependency versions from PyPI at build time. `pip check` validates
the environment after install.
- Pinned the `hatchling` build backend and the `uv` binary used in the
Dockerfile.

### Changed: e2e reachability jobs retry transient empty results

- Reachability e2e runs that report success with no alerted components in the
facts file are retried up to three times as a suspected transient backend
failure. After retries, only the known zero-project backend signature is
classified as inconclusive — any other empty result still fails — and e2e
jobs upload their logs and reports as diagnostics on failure.

## 2.5.9

### Changed: bump pinned @coana-tech/cli to 15.10.3
Expand Down
19 changes: 16 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,7 @@ ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}"
ENV GOPATH="/go"

# Install uv
COPY --from=ghcr.io/astral-sh/uv:latest /uv /usr/local/bin/uv
COPY --from=ghcr.io/astral-sh/uv:0.10.4 /uv /usr/local/bin/uv

# Install pyenv
# pyenv lets us build/install arbitrary Python versions on demand. We install
Expand All @@ -111,14 +111,26 @@ RUN curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/p
ln -s ~/.pyenv/bin/pyenv /bin/pyenv && \
pyenv --version

# Install Python dependencies from the lockfile with hash verification so the
# image never resolves loose versions from PyPI at build time.
COPY pyproject.toml uv.lock /tmp/socket-cli-lock/
# Index flags are passed explicitly (always production PyPI) so the
# PIP_INDEX_URL/PIP_EXTRA_INDEX_URL ARGs used to point CLI/SDK preview installs
# at TestPyPI don't leak into the locked dependency install via pip's env vars.
RUN uv export --directory /tmp/socket-cli-lock --frozen --no-dev --no-emit-project \
--format requirements-txt -o /tmp/socket-cli-lock/requirements.txt && \
pip install --require-hashes --no-deps \
--index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple \
-r /tmp/socket-cli-lock/requirements.txt

# Install CLI based on build mode
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
echo "Using local development install"; \
else \
cli_installed=false; \
for i in $(seq 1 10); do \
echo "Attempt $i/10: Installing socketsecurity==$CLI_VERSION"; \
if pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \
if pip install --no-deps --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \
cli_installed=true; \
break; \
fi; \
Expand All @@ -131,6 +143,7 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
echo "Failed to install socketsecurity==$CLI_VERSION after 10 attempts"; \
exit 1; \
fi; \
pip check || exit 1; \
if [ ! -z "$SDK_VERSION" ]; then \
pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \
fi; \
Expand All @@ -140,7 +153,7 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
COPY . /app
WORKDIR /app
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
pip install --upgrade -e .; \
pip install --no-deps -e . && pip check; \
fi

# Create workspace directory with proper permissions
Expand Down
26 changes: 13 additions & 13 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,26 +1,26 @@
[build-system]
requires = [
"hatchling"
"hatchling==1.31.0"
]
build-backend = "hatchling.build"

[project]
name = "socketsecurity"
version = "2.5.9"
version = "2.6.0"
requires-python = ">= 3.11"
license = {"file" = "LICENSE"}
dependencies = [
'requests',
'mdutils',
'prettytable',
'GitPython',
'packaging',
'python-dotenv',
"socketdev>=3.3.0,<4.0.0",
"bs4>=0.0.2",
"markdown>=3.10",
"brotli>=1.0.9; platform_python_implementation == 'CPython'",
"brotlicffi>=1.0.9; platform_python_implementation != 'CPython'",
"requests==2.34.2",
"mdutils==1.8.1",
"prettytable==3.18.0",
"GitPython==3.1.57",
"packaging==26.2",
"python-dotenv==1.2.2",
"socketdev==3.5.0",
"beautifulsoup4==4.14.3",
"markdown==3.10.2",
"brotli==1.2.0; platform_python_implementation == 'CPython'",
"brotlicffi==1.2.0.1; platform_python_implementation != 'CPython'",
]
readme = "README.md"
description = "Socket Security CLI for CI/CD"
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
__author__ = 'socket.dev'
__version__ = '2.5.9'
__version__ = '2.6.0'
USER_AGENT = f'SocketPythonCLI/{__version__}'
19 changes: 19 additions & 0 deletions tests/e2e/reach-facts-probe.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/usr/bin/env bash
# Exits 0 when the reachability facts file contains components with alerts.
#
# Used by the e2e workflow's retry-probe hook: a --reach run against the
# known-vulnerable fixture that reports success but yields no alerted
# components is the signature of a transient tier-1 backend failure,
# so the run is worth repeating before validation fails the job.
set -euo pipefail

TARGET="${1:?usage: reach-facts-probe.sh <target-path>}"

uv run python - "$TARGET" <<'PY'
import sys

from socketsecurity.core.alert_selection import load_components_with_alerts

components = load_components_with_alerts(sys.argv[1], ".socket.facts.json")
sys.exit(0 if components else 1)
PY
19 changes: 19 additions & 0 deletions tests/e2e/validate-reachability.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,25 @@ if [ ! -f "$FACTS_PATH" ]; then
fi
echo "PASS: Reachability facts file present at $FACTS_PATH"

# The tier-1 backend intermittently returns the known fixture as one orphaned
# component with zero projects, so Coana has no vulnerability to analyze even
# though manifest upload, facts generation, and scan finalization all succeed.
# After the workflow's bounded retries, classify only that explicit
# upstream signature as inconclusive. Any other empty facts result still fails,
# including the important regression case where Coana received a vulnerability
# but the CLI lost its alerted component.
if ! bash tests/e2e/reach-facts-probe.sh tests/e2e/fixtures/simple-npm; then
if grep -q "Found 1 manifest files for reachability upload" "$LOG" && \
grep -q "Found 0 projects across 0 ecosystems to analyze" "$LOG" && \
grep -q "Filtered out 1 orphaned component" "$LOG"; then
echo "::warning title=e2e-reachability inconclusive backend result::tier-1 returned the known zero-project/orphaned-component signature after retries; core reachability execution and finalization passed"
echo "e2e-reachability: inconclusive after retries — known zero-project backend signature; diagnostics uploaded" >> "${GITHUB_STEP_SUMMARY:-/dev/null}"
exit 0
fi
echo "FAIL: no components with alerts in .socket.facts.json and the known backend signature was not present"
exit 1
fi

# 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 "
Expand Down
44 changes: 16 additions & 28 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.