Skip to content
Open
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
20 changes: 20 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
# The image needs only the package source, its build metadata and the entrypoint
# script. Keep dist/: Dockerfile.preview copies the preview wheel from it.
Comment on lines +1 to +2

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] Optional: the header says the image needs only the package source, its build metadata and the entrypoint script, but the file lists exclusions, which must grow whenever a new top-level directory appears. An allow-list would say what the comment says:

*
!socketsecurity
!pyproject.toml
!uv.lock
!README.md
!LICENSE
!scripts/docker-entrypoint.sh
!dist

.git
.github
.context
.venv
.venv-test
venv
**/__pycache__
**/*.pyc
.pytest_cache
.coverage*
htmlcov
tests
benchmarks
docs
examples
instructions
workflows
session.md
120 changes: 120 additions & 0 deletions .github/workflows/e2e-image.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
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: 45
strategy:
fail-fast: false
matrix:
fixture: [simple-npm, simple-cargo]
Comment on lines +40 to +41

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] Each matrix entry builds the ~3.4 GB image separately and pulls the cached layers from Docker Hub on its own, so every run builds the image twice. That build is by far the most expensive step here. One job could build once and loop over both fixtures, recording each fixture's result so one failure doesn't hide the other. The trade-off is losing a separate check name per fixture.

name: e2e-image-reachability (${{ matrix.fixture }})
env:
IMAGE: socket-python-cli:e2e
FIXTURE: tests/e2e/fixtures/${{ matrix.fixture }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false

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

- 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: |
set -o pipefail
# 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
# retries.
in_image() {
docker run --rm -v "$PWD:/repo" -w /repo \
-v socket-cache:/root/.cache/socket \
-e SOCKET_SECURITY_API_KEY \
-e GIT_CONFIG_COUNT=1 -e GIT_CONFIG_KEY_0=safe.directory -e GIT_CONFIG_VALUE_0=/repo \
"$@"
}
# Same retry policy as e2e-test.yml's reachability job: the backend
# intermittently returns nothing to analyze while the CLI reports
# success, so retry while the facts file has no alerted components
# and let validation classify a result that stays empty.
max_attempts=3
attempt=1
while :; do
in_image "$IMAGE" --target-path "$FIXTURE" --reach --disable-blocking --enable-debug 2>&1 | tee /tmp/e2e-output.log
if in_image --entrypoint python "$IMAGE" tests/e2e/validate-image-reachability.py "$FIXTURE" --probe; then
break
fi
if [ "$attempt" -ge "$max_attempts" ]; then
echo "::warning title=e2e-image incomplete results::no alerted components after ${max_attempts} attempts; letting validation classify the result"
echo "diagnostics=true" >> "$GITHUB_OUTPUT"
break
fi
echo "::warning title=e2e-image transient retry::attempt ${attempt} produced no alerted components (suspected backend transient); retrying"
attempt=$((attempt+1))
sleep 30
done
Comment on lines +87 to +102

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] This retry loop is a copy of the one in e2e-test.yml, and the probe and the inconclusive check in validate-image-reachability.py repeat reach-facts-probe.sh and validate-reachability.sh (see the comments there). That's two copies of the policy for the backend's intermittent empty results, which will drift. A shared script for the loop, used by both workflows, would keep them in step. This touches the e2e-test files, so it may suit a follow-up better.


- name: Validate results
run: >-
docker run --rm -v "$PWD:/repo" -v /tmp/e2e-output.log:/tmp/e2e-output.log:ro -w /repo
--entrypoint python "$IMAGE"
tests/e2e/validate-image-reachability.py "$FIXTURE" --log /tmp/e2e-output.log

- name: Upload diagnostics
if: failure() || steps.run-cli.outputs.diagnostics == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: e2e-image-${{ matrix.fixture }}-diagnostics-attempt${{ github.run_attempt }}
path: |
/tmp/e2e-output.log
${{ env.FIXTURE }}/.socket.facts.json
if-no-files-found: ignore
include-hidden-files: true
retention-days: 14
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
9 changes: 7 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,13 @@ jobs:
with:
push: true
platforms: linux/amd64,linux/arm64
cache-from: type=gha
cache-to: type=gha,mode=max
# A registry cache rather than type=gha: Actions caches are scoped to
# the ref that wrote them, and each release builds on its own tag, so
# no release can read the one before it. Reusing cached layers is
# what 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
tags: |
socketdev/cli:latest
socketdev/cli:${{ env.VERSION }}
Expand Down
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,18 @@
# Changelog

## 2.11.12

### Changed: smaller Docker image, and upgrades reuse its toolchain layers

- 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.
- Rust is installed with rustup's minimal profile (`rustc`, `cargo`, `rust-std`).
The image no longer includes Rust's offline HTML documentation, `rustfmt`, or
`clippy`.
- Removed `strace` from the image, and the npm and pip download caches. Run
`apk add strace` in a derived image if you need it.

## 2.11.11

### Changed: use lowercase Python CLI User-Agent
Expand Down
37 changes: 22 additions & 15 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -11,20 +11,13 @@ 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)" != "<" ]

Expand Down Expand Up @@ -104,11 +97,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 @@ -148,18 +144,29 @@ COPY pyproject.toml uv.lock /tmp/socket-cli-lock/
# 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 \
pip install --no-cache-dir --require-hashes --no-deps \

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] --no-cache-dir is now on every pip install in this file (here and at lines 27, 169, 184 and 192). A single ARG PIP_NO_CACHE_DIR=1 just after FROM python:3-alpine would cover them all, including any future calls. Its value never changes, so it wouldn't cause the cache misses this PR fixes, and as an ARG rather than an ENV it applies only at build time.

--index-url https://pypi.org/simple --extra-index-url https://pypi.org/simple \
-r /tmp/socket-cli-lock/requirements.txt

# CLI and SDK arguments
# Declared here rather than at the top: every RUN after an ARG is cache-keyed on
# its value, and CLI_VERSION changes on every release. Declared earlier, it
# would rebuild every toolchain layer above, so each release would be a full
# re-pull for anyone upgrading.
Comment on lines +152 to +155

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[agent] Minor: this comment and the cache comment in release.yml ("A registry cache rather than type=gha") both compare the code with the alternative it replaced. Our comment conventions keep code comments to the lasting reason. Here that's the warning itself, which is worth keeping: declared earlier, these ARGs would rebuild every toolchain layer on each release. It just doesn't need the "rather than at the top" framing.

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"; \
else \
cli_installed=false; \
for i in $(seq 1 10); do \
echo "Attempt $i/10: Installing socketsecurity==$CLI_VERSION"; \
if pip install --no-deps --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION; then \
if pip install --no-cache-dir --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 @@ -174,15 +181,15 @@ RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
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}; \
pip install --no-cache-dir --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketdev==${SDK_VERSION}; \
fi; \
fi

# Copy local source and install in editable mode if USE_LOCAL_INSTALL is true
COPY . /app
WORKDIR /app
RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \
pip install --no-deps -e . && pip check; \
pip install --no-cache-dir --no-deps -e . && pip check; \
fi

# Create workspace directory with proper permissions
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ build-backend = "hatchling.build"

[project]
name = "socketsecurity"
version = "2.11.11"
version = "2.11.12"
requires-python = ">= 3.11"
license = {"file" = "LICENSE"}
dependencies = [
Expand Down
2 changes: 1 addition & 1 deletion socketsecurity/__init__.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
__author__ = 'socket.dev'
__version__ = '2.11.11'
__version__ = '2.11.12'
USER_AGENT_NAME = 'socket-python-cli'
USER_AGENT = f'{USER_AGENT_NAME}/{__version__}'
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