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
12 changes: 12 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# The image needs only the package source, its build metadata and the entrypoint
# script. dist/ holds the wheel Dockerfile.preview installs.
*
!socketsecurity
!pyproject.toml
!uv.lock
!README.md
!LICENSE
!scripts/docker-entrypoint.sh
!dist
**/__pycache__
**/*.pyc
8 changes: 8 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,14 @@ updates:
schedule:
interval: "weekly"
open-pull-requests-limit: 2
# Node majors move by hand: the reachability engine sets a minimum Node
# version, and Dependabot doesn't know Node's release schedule, so it
# would propose odd-numbered majors, which never get LTS. Check a new
# major with e2e-image.yml. The floating node:<major>-alpine tag picks up
# minor and patch releases on every build without a Dependabot PR.
ignore:
- dependency-name: "node"
update-types: ["version-update:semver-major"]
labels:
- "dependencies"
- "docker"
Expand Down
119 changes: 119 additions & 0 deletions .github/workflows/e2e-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
name: E2E Image

# Runs reachability inside the Docker image built from this commit, so a missing
# or broken toolchain fails here rather than in a user's pipeline. e2e-test.yml
# covers reachability run directly on the runner.
on:
pull_request:
paths:
- "Dockerfile"
- ".dockerignore"
- "scripts/docker-entrypoint.sh"
- "socketsecurity/core/tools/**"
- "tests/e2e/**"
- ".github/workflows/e2e-image.yml"
push:
branches: [main]
paths:
- "Dockerfile"
- ".dockerignore"
- "scripts/docker-entrypoint.sh"
- "socketsecurity/core/tools/**"
- "tests/e2e/**"
- ".github/workflows/e2e-image.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
reachability:
# Needs SOCKET_CLI_API_TOKEN, which fork and Dependabot PRs don't get.
if: >-
(github.event_name != 'pull_request' ||
github.event.pull_request.head.repo.full_name == github.repository) &&
github.event.pull_request.user.login != 'dependabot[bot]'
runs-on: ubuntu-latest
timeout-minutes: 60
name: e2e-image-reachability
env:
IMAGE: socket-python-cli:e2e
FIXTURES: simple-npm simple-cargo
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

- uses: docker/setup-buildx-action@f87e5991a6d7451dcb8d9637bfbc97413f497069 # v4.4.1

# One build for every fixture: the image is by far the most expensive
# part of this job.
- name: Build the image
uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7.4.0
with:
context: .
load: true
tags: ${{ env.IMAGE }}
build-args: |
USE_LOCAL_INSTALL=true
# Read-only: reuse the release build's toolchain layers without
# writing this commit's layers back.
cache-from: type=registry,ref=socketdev/cli:buildcache

- name: Run reachability in the image
id: run-cli
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
run: |
# The checkout belongs to the runner user and the container runs as
# root, so git needs the mount marked safe to read repository details.
# The named volume keeps the downloaded reachability engine across
# runs.
in_image() {
docker run --rm -v "$PWD:/repo" -w /repo \
-v socket-cache:/root/.cache/socket \
-v /tmp/e2e-logs:/tmp/e2e-logs \
-e SOCKET_SECURITY_API_KEY \
-e GIT_CONFIG_COUNT=1 -e GIT_CONFIG_KEY_0=safe.directory -e GIT_CONFIG_VALUE_0=/repo \
"$@"
}
export -f in_image
mkdir -p /tmp/e2e-logs

# Every fixture runs even when an earlier one fails, so one failure
# can't hide another.
failed=""
for fixture in $FIXTURES; do
target="tests/e2e/fixtures/$fixture"
log="/tmp/e2e-logs/$fixture.log"
echo "::group::$fixture"
if bash tests/e2e/run-with-retries.sh "e2e-image-$fixture" \
"in_image --entrypoint python $IMAGE tests/e2e/reach_facts.py $target --probe" \
in_image "$IMAGE" --target-path "$target" --reach --disable-blocking --enable-debug; then
cp /tmp/e2e-output.log "$log"
in_image --entrypoint python "$IMAGE" tests/e2e/reach_facts.py "$target" --validate-analyzed "$log" \
|| failed="$failed $fixture"
else
cp /tmp/e2e-output.log "$log"
echo "FAIL: the CLI exited with an error for $fixture"
failed="$failed $fixture"
fi
echo "::endgroup::"
done
if [ -n "$failed" ]; then
echo "::error title=e2e-image-reachability::failed for:$failed"
exit 1
fi

- name: Upload diagnostics
if: failure() || steps.run-cli.outputs.diagnostics == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: e2e-image-diagnostics-attempt${{ github.run_attempt }}
path: |
/tmp/e2e-logs/
tests/e2e/fixtures/*/.socket.facts.json
if-no-files-found: ignore
include-hidden-files: true
retention-days: 14
36 changes: 8 additions & 28 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ jobs:
# 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
retry-probe: uv run python tests/e2e/reach_facts.py tests/e2e/fixtures/simple-npm --probe

- name: gitlab
args: >-
Expand Down Expand Up @@ -103,33 +103,13 @@ jobs:
env:
SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }}
RETRY_PROBE: ${{ matrix.retry-probe }}
run: |
set -o pipefail
# 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
CLI_ARGS: ${{ matrix.args }}
LABEL: e2e-${{ matrix.name }}
# Entries with retry-probe are retried while the probe fails. Validation
# treats only the known zero-project backend result as inconclusive;
# any other empty result remains a failure. CLI_ARGS is word-split on
# purpose.
run: bash tests/e2e/run-with-retries.sh "$LABEL" "$RETRY_PROBE" socketcli $CLI_ARGS

- name: Validate results
env:
Expand Down
1 change: 1 addition & 0 deletions .github/workflows/python-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ on:
- "tests/core/**/*.py"
- "pyproject.toml"
- "uv.lock"
- "Dockerfile"
- ".github/workflows/python-tests.yml"
# Unfiltered: a required check behind a paths filter never reports on a
# pull request that misses the filter, which blocks the merge.
Expand Down
8 changes: 6 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,12 @@ jobs:
with:
push: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
# The cache lives in the registry because Actions caches are scoped to
# the ref that wrote them, and each release builds on its own tag.
# Reusing cached layers keeps their digests stable, so users
# upgrading pull only the layers that changed.
cache-from: type=registry,ref=socketdev/cli:buildcache
cache-to: type=registry,ref=socketdev/cli:buildcache,mode=max,image-manifest=true,oci-mediatypes=true
# Attach an SPDX SBOM generated from the built image, next to the
# provenance attestation the action already adds.
sbom: true
Expand Down
34 changes: 22 additions & 12 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,30 +2,40 @@

## 2.12.0

### Added: SBOM attestation on the Docker image
### Added

- Release images on Docker Hub now carry an SPDX SBOM attestation alongside the
existing provenance attestation. See
[Verify the image](docs/ci-cd.md#verify-the-image).
- New [Docker image](docs/ci-cd.md#docker-image) section in the CI/CD guide,
covering how to cache the reachability engine between container runs and keep
image pulls cheap in CI.

### Changed: `stable` points at the published release image
### Changed

- Marking a release stable now retags the image that release published instead of
rebuilding it, so `socketdev/cli:stable` is the same image as
- The Docker image is about 1 GB smaller. Rust is installed with rustup's
minimal profile (`rustc`, `cargo`, `rust-std`), which leaves out Rust's offline
HTML documentation, and the npm and pip download caches are no longer kept.
- Upgrading `socketdev/cli` from one release to the next now downloads only the
layers that changed. Previously each release rebuilt every layer, so an upgrade
downloaded nearly the whole image.
- Marking a release stable now retags the image that release published instead
of rebuilding it, so `socketdev/cli:stable` is the same image as
`socketdev/cli:<version>`, attestations included.

### Added: Docker image guide
### Removed

- New [Docker image](docs/ci-cd.md#docker-image) section covering how to cache the
reachability engine between container runs and keep image pulls cheap in CI.
- The Docker image no longer includes `rustfmt` or `clippy` (left out of
rustup's minimal profile) or `strace`. Reachability analysis only needs
`cargo`. If you rely on these tools, add them in a derived image with
`rustup component add rustfmt clippy` or `apk add strace`.

### Fixed: Docker image publishing
### Fixed

- The `socketdev/cli` Docker image now publishes reliably when PyPI is slow to
list a new release. A delay could leave a release on PyPI without a matching
Docker image.
- The Docker image now publishes reliably when PyPI is slow to list a new
release. A delay could leave a release on PyPI without a matching Docker image.

### Security: bump `soupsieve`
### Security

- Bumped the locked `soupsieve` (pulled in by `beautifulsoup4`) from `2.9.2` to
`4.1` on Python 3.11.5 and later, and to `2.10` on earlier 3.11 releases,
Expand Down
31 changes: 19 additions & 12 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -15,27 +15,20 @@ ARG GO_VERSION=system
ARG JAVA_VERSION=17
ARG DOTNET_VERSION=8

# CLI and SDK arguments
ARG CLI_VERSION
ARG SDK_VERSION
ARG PIP_INDEX_URL=https://pypi.org/simple
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
ARG USE_LOCAL_INSTALL=false

# Install base packages first
# ca-certificates must be 20261020-r0 or newer, which drops the roots the
# compromised eMudhra intermediates chain to. The base image ships an older
# one, and `apk add` does not upgrade installed packages, so upgrade it here.
RUN apk update && apk add --no-cache \
RUN apk add --no-cache \
git curl wget \
ruby ruby-dev build-base strace && \
ruby ruby-dev build-base && \
apk upgrade --no-cache ca-certificates && \
[ "$(apk version -t "$(apk list -I ca-certificates | awk '{print $1}' | sed 's/^ca-certificates-//')" 20261020-r0)" != "<" ]

# pip uses its own vendored CA bundle, not the system store, and the base image's
# pip trusts the compromised eMudhra intermediates. 26.2.2 vendors certifi
# 2026.10.19. Upgrade it against the system store fixed above.
RUN python -m pip install --no-cache-dir --cert /etc/ssl/certs/ca-certificates.crt --upgrade "pip>=26.2.2" && \
RUN python -m pip install --cert /etc/ssl/certs/ca-certificates.crt --upgrade "pip>=26.2.2" && \
python -c 'import sys, pip; sys.exit(tuple(map(int, pip.__version__.split(".")[:3])) < (26, 2, 2))'

# npm, npx and yarn are symlinks in the node image. COPY would dereference them
Expand Down Expand Up @@ -108,11 +101,14 @@ RUN if [ "$TARGETARCH" = "amd64" ]; then \
fi

# Install additional tools
# Reachability only runs cargo, so install the minimal Rust profile (rustc,
# cargo, rust-std). The default profile adds about 900 MB of offline HTML docs.
RUN npm install socket -g && \
npm cache clean --force && \
gem install bundler && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal && \
. ~/.cargo/env && \
rustup component add rustfmt clippy
cargo --version

# Set environment paths
ENV PATH="/usr/local/go/bin:/usr/lib/go/bin:/root/.cargo/bin:${PATH}"
Expand Down Expand Up @@ -156,6 +152,17 @@ RUN uv export --directory /tmp/socket-cli-lock --frozen --no-dev --no-emit-proje
--index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple \
-r /tmp/socket-cli-lock/requirements.txt

# CLI and SDK arguments
# Keep these below the toolchain layers. Every RUN after an ARG is cache-keyed on
# its value, and CLI_VERSION changes on every release, so declared any earlier
# they would rebuild every toolchain layer on each release, and every upgrade
# would be a full re-pull.
ARG CLI_VERSION
ARG SDK_VERSION
ARG PIP_INDEX_URL=https://pypi.org/simple
ARG PIP_EXTRA_INDEX_URL=https://pypi.org/simple
ARG USE_LOCAL_INSTALL=false

# Install CLI based on build mode
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
echo "Using local development install"; \
Expand Down
2 changes: 2 additions & 0 deletions docs/ci-cd.md
Original file line number Diff line number Diff line change
Expand Up @@ -582,6 +582,8 @@ different version is harmless: the CLI downloads the version it needs.
Registry, Artifactory). This also avoids Docker Hub's pull rate limits.
- Ephemeral hosted runners pull the whole image on every job. Long-lived or
self-hosted runners pull it once per host.
- Upgrading from one release to the next downloads only the layers that changed,
usually the CLI and its Python dependencies, not the toolchains.

### Verify the image

Expand Down
16 changes: 16 additions & 0 deletions tests/e2e/fixtures/simple-cargo/Cargo.lock

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

10 changes: 10 additions & 0 deletions tests/e2e/fixtures/simple-cargo/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
[package]
name = "simple-cargo"
version = "0.1.0"
edition = "2021"
publish = false

[dependencies]
# Pinned to a release affected by GHSA-43w2-9j62-hq99 (insert_many buffer
# overflow), so reachability has a known vulnerability to analyze.
smallvec = "=1.6.0"
7 changes: 7 additions & 0 deletions tests/e2e/fixtures/simple-cargo/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
use smallvec::SmallVec;

fn main() {
let mut values: SmallVec<[u8; 4]> = SmallVec::new();
values.insert_many(0, vec![1, 2, 3]);
println!("{:?}", values);
}
Loading
Loading