diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 76ed9536..edd0e77a 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @SocketDev/eng \ No newline at end of file +* @SocketDev/customer-engineering \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..db131ed9 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,5 @@ +Click on the "Preview" tab and select appropriate PR template: + +[New Feature](?expand=1&template=feature.md) +[Bug Fix](?expand=1&template=bug-fix.md) +[Improvement](?expand=1&template=improvement.md) diff --git a/.github/PULL_REQUEST_TEMPLATE/bug-fix.md b/.github/PULL_REQUEST_TEMPLATE/bug-fix.md new file mode 100644 index 00000000..239d369a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/bug-fix.md @@ -0,0 +1,19 @@ + + +## Root Cause + + + + +## Fix + + +## Public Changelog + + + +N/A + + + + diff --git a/.github/PULL_REQUEST_TEMPLATE/feature.md b/.github/PULL_REQUEST_TEMPLATE/feature.md new file mode 100644 index 00000000..51ab143a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/feature.md @@ -0,0 +1,16 @@ + + + +## Why? + + + + +## Public Changelog + + + +N/A + + + diff --git a/.github/PULL_REQUEST_TEMPLATE/improvement.md b/.github/PULL_REQUEST_TEMPLATE/improvement.md new file mode 100644 index 00000000..98f4fd59 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/improvement.md @@ -0,0 +1,10 @@ + + +## Public Changelog + + + +N/A + + + diff --git a/.github/actions/setup-docker/action.yml b/.github/actions/setup-docker/action.yml new file mode 100644 index 00000000..846efd47 --- /dev/null +++ b/.github/actions/setup-docker/action.yml @@ -0,0 +1,23 @@ +name: "Set up Docker" +description: >- + Set up QEMU + Docker Buildx and authenticate to Docker Hub for multi-arch + image builds. Centralizes the QEMU/Buildx/login trio used by release, + preview, and stable workflows. + +inputs: + dockerhub-username: + description: "Docker Hub username (pass from secrets)" + required: true + dockerhub-token: + description: "Docker Hub token/password (pass from secrets)" + required: true + +runs: + using: "composite" + steps: + - uses: docker/setup-qemu-action@06116385d9baf250c9f4dcb4858b16962ea869c3 # v4.1.0 + - uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 + - uses: docker/login-action@650006c6eb7dba73a995cc03b0b2d7f5ca915bee # v4.2.0 + with: + username: ${{ inputs.dockerhub-username }} + password: ${{ inputs.dockerhub-token }} diff --git a/.github/actions/setup-hatch/action.yml b/.github/actions/setup-hatch/action.yml new file mode 100644 index 00000000..0da5160b --- /dev/null +++ b/.github/actions/setup-hatch/action.yml @@ -0,0 +1,13 @@ +name: "Set up Hatch build tooling" +description: >- + Install the pinned hatch / hatchling / virtualenv toolchain used to build + and publish the package. Assumes Python is already set up by the caller. + +runs: + using: "composite" + steps: + - shell: bash + run: | + python -m pip install --upgrade pip + pip install "virtualenv<20.36" + pip install hatchling==1.27.0 hatch==1.14.0 diff --git a/.github/actions/setup-sfw/action.yml b/.github/actions/setup-sfw/action.yml new file mode 100644 index 00000000..456f90b9 --- /dev/null +++ b/.github/actions/setup-sfw/action.yml @@ -0,0 +1,49 @@ +name: "Set up Socket Firewall" +description: >- + Set up the requested language toolchain and install Socket Firewall (free + or enterprise edition) so subsequent steps can run package-manager commands + wrapped with `sfw`. Defaults to free/anonymous mode (no API token -- safe on + untrusted / Dependabot / fork PRs). Pass mode: firewall-enterprise + + socket-token for full org-policy enforcement on trusted maintainer PRs. + +inputs: + python: + description: "Set up Python 3.12" + default: "false" + node: + description: "Set up Node 20 (needed for npm-wrapped checks)" + default: "false" + uv: + description: "Install uv (implies Python)" + default: "false" + mode: + description: "socketdev/action mode: firewall-free or firewall-enterprise" + default: "firewall-free" + socket-token: + description: "Socket API token (only used/required for firewall-enterprise)" + default: "" + +runs: + using: "composite" + steps: + - if: ${{ inputs.python == 'true' || inputs.uv == 'true' }} + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.12" + + - if: ${{ inputs.node == 'true' }} + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: "20" + + # Official Socket setup action. Wires up sfw routing correctly. + # socket-token is ignored in firewall-free mode and empty when absent. + - uses: socketdev/action@ba6de6cc0565af1f42295590380973573297e31f # v1.3.2 + with: + mode: ${{ inputs.mode }} + socket-token: ${{ inputs.socket-token }} + + - if: ${{ inputs.uv == 'true' }} + name: Install uv + shell: bash + run: python -m pip install --upgrade pip uv diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..89e2ed00 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,76 @@ +# Dependabot configuration for socket-python-cli. +# +# Design notes: +# - Python deps are grouped into a weekly PR (minor/patch). +# - GitHub Actions are grouped similarly into one weekly PR. +# - Docker (the project Dockerfile) is tracked separately. +# - 7-day cooldown enforced across all ecosystems. + +version: 2 +updates: + + # Main app Python deps (uv-tracked) + - package-ecosystem: "uv" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 2 + groups: + python-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + python-major: + patterns: + - "*" + update-types: + - "major" + labels: + - "dependencies" + - "python:uv" + commit-message: + prefix: "chore" + include: "scope" + cooldown: + default-days: 7 + + # GitHub Actions used in workflows and local composite actions. + - package-ecosystem: "github-actions" + directories: + - "/" + - "/.github/actions/*" + schedule: + interval: "weekly" + open-pull-requests-limit: 2 + groups: + github-actions-minor-patch: + patterns: + - "*" + update-types: + - "minor" + - "patch" + labels: + - "dependencies" + - "github-actions" + commit-message: + prefix: "ci" + include: "scope" + cooldown: + default-days: 7 + + # Project Dockerfile base images and pinned binaries + - package-ecosystem: "docker" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 2 + labels: + - "dependencies" + - "docker" + commit-message: + prefix: "chore" + include: "scope" + cooldown: + default-days: 7 diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 00000000..a39694f8 --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,653 @@ +name: dependency-review + +# Supply-chain guardrails for dependency-update PRs -- for BOTH Dependabot +# and maintainers. +# +# Inspects the changed files, then conditionally runs Socket Firewall (sfw) +# install smoke jobs for the affected manifests, picking the firewall edition +# per PR: +# +# - Trusted authors: any in-repo (non-fork) PR other than Dependabot's +# (i.e. someone with write access) -> Socket Firewall ENTERPRISE through +# the socket-firewall environment and its SOCKET_SFW_API_TOKEN secret +# (authenticated, full org-policy enforcement). +# - Everyone else: Dependabot and all fork PRs from external contributors -> +# Socket Firewall FREE (anonymous, no API token), which is safe in the +# unprivileged `pull_request` context. +# +# Only Enterprise jobs declare the socket-firewall environment. Free jobs do +# not touch that environment or its token. +# +# Each sfw smoke job collects an sfw-artifacts/ directory (provenance context +# + the firewall's console logs) and uploads it as a build artifact +# (if: always(), so the report survives even when sfw BLOCKS an install -- +# which is exactly when you want to read it). +# +# Pattern adapted from SocketDev/socket-basics and SocketDev/socket-sdk-python. + +on: + pull_request: + types: [opened, synchronize, reopened, ready_for_review] + +permissions: + contents: read + +concurrency: + group: dependency-review-${{ github.event.pull_request.number }} + cancel-in-progress: true + +jobs: + inspect: + runs-on: ubuntu-latest + timeout-minutes: 5 + outputs: + python_deps_changed: ${{ steps.diff.outputs.python_deps_changed }} + fixture_npm_changed: ${{ steps.diff.outputs.fixture_npm_changed }} + fixture_pypi_changed: ${{ steps.diff.outputs.fixture_pypi_changed }} + dockerfile_changed: ${{ steps.diff.outputs.dockerfile_changed }} + workflow_or_action_changed: ${{ steps.diff.outputs.workflow_or_action_changed }} + sfw_mode: ${{ steps.mode.outputs.sfw_mode }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - name: Inspect changed files + id: diff + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + run: | + CHANGED_FILES="$(git diff --name-only "$BASE_SHA" "$HEAD_SHA")" + + { + echo "## Changed files" + echo '```' + printf '%s\n' "$CHANGED_FILES" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + has_file() { + local pattern="$1" + if printf '%s\n' "$CHANGED_FILES" | grep -Eq "$pattern"; then + echo "true" + else + echo "false" + fi + } + + { + echo "python_deps_changed=$(has_file '^(pyproject\.toml|uv\.lock)$')" + echo "fixture_npm_changed=$(has_file '^tests/e2e/fixtures/simple-npm/')" + echo "fixture_pypi_changed=$(has_file '^tests/e2e/fixtures/simple-pypi/')" + echo "dockerfile_changed=$(has_file '^Dockerfile$')" + echo "workflow_or_action_changed=$(has_file '^\.github/workflows/|^\.github/actions/|^\.github/dependabot\.yml$')" + } >> "$GITHUB_OUTPUT" + + - name: Determine Socket Firewall mode + id: mode + # Trusted == any in-repo (non-fork) PR that isn't Dependabot's. Only + # accounts with write access can push a branch to this repo, so a + # non-fork PR already implies a trusted author -- the same boundary + # GitHub uses to decide whether secrets are exposed at all. + # + # NB: author_association is deliberately NOT used to require strict org + # membership. It only reflects PUBLIC org membership, so private members + # (the common case) show up as CONTRIBUTOR and would be misclassified. + # Reliable strict-membership detection would need a read:org token or + # public membership. This step references NO secret regardless -- it + # only decides which smoke job runs. + env: + IS_DEPENDABOT: ${{ github.event.pull_request.user.login == 'dependabot[bot]' }} + IS_FORK: ${{ github.event.pull_request.head.repo.full_name != github.repository }} + AUTHOR_ASSOC: ${{ github.event.pull_request.author_association }} + run: | + mode=firewall-free + if [ "$IS_DEPENDABOT" != "true" ] && [ "$IS_FORK" != "true" ]; then + mode=firewall-enterprise + fi + + echo "sfw_mode=$mode" >> "$GITHUB_OUTPUT" + { + echo "## Socket Firewall mode: \`$mode\`" + echo "- author_association: \`$AUTHOR_ASSOC\`" + echo "- dependabot: \`$IS_DEPENDABOT\` | fork: \`$IS_FORK\`" + } >> "$GITHUB_STEP_SUMMARY" + + - name: Summarize review expectations + env: + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + { + echo "## Dependency Review Checklist" + echo "- PR: $PR_URL" + echo "- Confirm upstream release notes before merge" + echo "- Do not treat a dependency PR as trusted solely because of the actor" + echo "- This workflow runs in pull_request context only; no publish secrets are exposed" + } >> "$GITHUB_STEP_SUMMARY" + + python-sfw-smoke-free: + needs: inspect + if: | + needs.inspect.outputs.python_deps_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-free' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Prepare SFW artifact directory + run: | + mkdir -p sfw-artifacts + { + echo "mode=firewall-free" + echo "manifest=python" + echo "pr=${{ github.event.pull_request.number }}" + echo "sha=${{ github.event.pull_request.head.sha }}" + } > sfw-artifacts/context.txt + + - uses: ./.github/actions/setup-sfw + with: + uv: "true" + mode: firewall-free + + - name: Sync project through Socket Firewall + # `sfw uv sync` is the intended way to route uv through Socket Firewall + # (per Socket's own uv wrapper guidance). --locked verifies the exact + # uv.lock set and fails on lockfile drift rather than silently + # re-resolving, so the firewall inspects precisely what would install. + # Note: uv's sfw integration is quieter than npm/pip -- it does not + # print the "N packages fetched" footer, but interception is active. + # + # Use the runner's setup-python interpreter and forbid managed-Python + # downloads. The firewall is here to vet PyPI installs, not the + # interpreter/toolchain download path. + # + # pipefail keeps sfw's exit code through the tee so a firewall block + # still fails the job; tee captures the report for the artifact upload. + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: | + set -o pipefail + sfw uv sync --locked --extra test --extra dev 2>&1 | tee sfw-artifacts/sfw-uv-sync.log + + - name: Import smoke test + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: | + set -o pipefail + uv run python -c " + from socketsecurity.socketcli import cli, build_socket_sdk + from socketsecurity.core import Core + from socketsecurity.core.exceptions import ( + APIFailure, RequestTimeoutExceeded, APIResourceNotFound, + ) + from socketsecurity.core.git_interface import Git + from socketsecurity.config import CliConfig + print('import smoke OK') + " 2>&1 | tee sfw-artifacts/import-smoke.log + + - name: Collect SFW JSON report + # socketdev/action points sfw at SFW_JSON_REPORT_PATH (a $RUNNER_TEMP + # file) and reads it back in its post step to render the job summary, so + # COPY (don't move) the report into the bundle. sfw writes it even when + # it blocks an install -- always() keeps it on failures too. + if: always() + run: | + if [ -n "${SFW_JSON_REPORT_PATH:-}" ] && [ -f "$SFW_JSON_REPORT_PATH" ]; then + cp "$SFW_JSON_REPORT_PATH" "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report.json" + echo "Collected SFW report -> sfw-artifacts/sfw-report.json" + else + echo "No SFW JSON report found at '${SFW_JSON_REPORT_PATH:-}'." \ + > "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report-missing.txt" + fi + + - name: Upload SFW report artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: socket-firewall-free-python-${{ github.event.pull_request.number }} + path: sfw-artifacts/ + if-no-files-found: warn + retention-days: 14 + + python-sfw-smoke-enterprise: + needs: inspect + if: | + needs.inspect.outputs.python_deps_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-enterprise' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: socket-firewall + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Prepare SFW artifact directory + run: | + mkdir -p sfw-artifacts + { + echo "mode=firewall-enterprise" + echo "manifest=python" + echo "pr=${{ github.event.pull_request.number }}" + echo "sha=${{ github.event.pull_request.head.sha }}" + } > sfw-artifacts/context.txt + + - uses: ./.github/actions/setup-sfw + with: + uv: "true" + mode: firewall-enterprise + socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} + + - name: Sync project through Socket Firewall + # `sfw uv sync` is the intended way to route uv through Socket Firewall + # (per Socket's own uv wrapper guidance). --locked verifies the exact + # uv.lock set and fails on lockfile drift rather than silently + # re-resolving, so the firewall inspects precisely what would install. + # Note: uv's sfw integration is quieter than npm/pip -- it does not + # print the "N packages fetched" footer, but interception is active. + # + # Use the runner's setup-python interpreter and forbid managed-Python + # downloads. The firewall is here to vet PyPI installs, not the + # interpreter/toolchain download path. + # + # pipefail keeps sfw's exit code through the tee so a firewall block + # still fails the job; tee captures the report for the artifact upload. + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: | + set -o pipefail + sfw uv sync --locked --extra test --extra dev 2>&1 | tee sfw-artifacts/sfw-uv-sync.log + + - name: Import smoke test + env: + UV_PYTHON: "3.12" + UV_PYTHON_DOWNLOADS: never + run: | + set -o pipefail + uv run python -c " + from socketsecurity.socketcli import cli, build_socket_sdk + from socketsecurity.core import Core + from socketsecurity.core.exceptions import ( + APIFailure, RequestTimeoutExceeded, APIResourceNotFound, + ) + from socketsecurity.core.git_interface import Git + from socketsecurity.config import CliConfig + print('import smoke OK') + " 2>&1 | tee sfw-artifacts/import-smoke.log + + - name: Collect SFW JSON report + # socketdev/action points sfw at SFW_JSON_REPORT_PATH (a $RUNNER_TEMP + # file) and reads it back in its post step to render the job summary, so + # COPY (don't move) the report into the bundle. sfw writes it even when + # it blocks an install -- always() keeps it on failures too. + if: always() + run: | + if [ -n "${SFW_JSON_REPORT_PATH:-}" ] && [ -f "$SFW_JSON_REPORT_PATH" ]; then + cp "$SFW_JSON_REPORT_PATH" "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report.json" + echo "Collected SFW report -> sfw-artifacts/sfw-report.json" + else + echo "No SFW JSON report found at '${SFW_JSON_REPORT_PATH:-}'." \ + > "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report-missing.txt" + fi + + - name: Upload SFW report artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: socket-firewall-enterprise-python-${{ github.event.pull_request.number }} + path: sfw-artifacts/ + if-no-files-found: warn + retention-days: 14 + + fixture-npm-sfw-smoke-free: + needs: inspect + if: | + needs.inspect.outputs.fixture_npm_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-free' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Prepare SFW artifact directory + run: | + mkdir -p sfw-artifacts + { + echo "mode=firewall-free" + echo "manifest=npm" + echo "pr=${{ github.event.pull_request.number }}" + echo "sha=${{ github.event.pull_request.head.sha }}" + } > sfw-artifacts/context.txt + + - uses: ./.github/actions/setup-sfw + with: + node: "true" + mode: firewall-free + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-npm + # Tee to an absolute path under the workspace so the log lands in the + # repo-root sfw-artifacts/ dir despite this step's working-directory. + run: | + set -o pipefail + sfw npm install --no-audit --no-fund --ignore-scripts 2>&1 | tee "$GITHUB_WORKSPACE/sfw-artifacts/sfw-npm-install.log" + + - name: Collect SFW JSON report + # socketdev/action points sfw at SFW_JSON_REPORT_PATH (a $RUNNER_TEMP + # file) and reads it back in its post step to render the job summary, so + # COPY (don't move) the report into the bundle. sfw writes it even when + # it blocks an install -- always() keeps it on failures too. + if: always() + run: | + if [ -n "${SFW_JSON_REPORT_PATH:-}" ] && [ -f "$SFW_JSON_REPORT_PATH" ]; then + cp "$SFW_JSON_REPORT_PATH" "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report.json" + echo "Collected SFW report -> sfw-artifacts/sfw-report.json" + else + echo "No SFW JSON report found at '${SFW_JSON_REPORT_PATH:-}'." \ + > "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report-missing.txt" + fi + + - name: Upload SFW report artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: socket-firewall-free-npm-${{ github.event.pull_request.number }} + path: sfw-artifacts/ + if-no-files-found: warn + retention-days: 14 + + fixture-npm-sfw-smoke-enterprise: + needs: inspect + if: | + needs.inspect.outputs.fixture_npm_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-enterprise' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: socket-firewall + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Prepare SFW artifact directory + run: | + mkdir -p sfw-artifacts + { + echo "mode=firewall-enterprise" + echo "manifest=npm" + echo "pr=${{ github.event.pull_request.number }}" + echo "sha=${{ github.event.pull_request.head.sha }}" + } > sfw-artifacts/context.txt + + - uses: ./.github/actions/setup-sfw + with: + node: "true" + mode: firewall-enterprise + socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-npm + # Tee to an absolute path under the workspace so the log lands in the + # repo-root sfw-artifacts/ dir despite this step's working-directory. + run: | + set -o pipefail + sfw npm install --no-audit --no-fund --ignore-scripts 2>&1 | tee "$GITHUB_WORKSPACE/sfw-artifacts/sfw-npm-install.log" + + - name: Collect SFW JSON report + # socketdev/action points sfw at SFW_JSON_REPORT_PATH (a $RUNNER_TEMP + # file) and reads it back in its post step to render the job summary, so + # COPY (don't move) the report into the bundle. sfw writes it even when + # it blocks an install -- always() keeps it on failures too. + if: always() + run: | + if [ -n "${SFW_JSON_REPORT_PATH:-}" ] && [ -f "$SFW_JSON_REPORT_PATH" ]; then + cp "$SFW_JSON_REPORT_PATH" "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report.json" + echo "Collected SFW report -> sfw-artifacts/sfw-report.json" + else + echo "No SFW JSON report found at '${SFW_JSON_REPORT_PATH:-}'." \ + > "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report-missing.txt" + fi + + - name: Upload SFW report artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: socket-firewall-enterprise-npm-${{ github.event.pull_request.number }} + path: sfw-artifacts/ + if-no-files-found: warn + retention-days: 14 + + fixture-pypi-sfw-smoke-free: + needs: inspect + if: | + needs.inspect.outputs.fixture_pypi_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-free' + runs-on: ubuntu-latest + timeout-minutes: 15 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Prepare SFW artifact directory + run: | + mkdir -p sfw-artifacts + { + echo "mode=firewall-free" + echo "manifest=pypi" + echo "pr=${{ github.event.pull_request.number }}" + echo "sha=${{ github.event.pull_request.head.sha }}" + } > sfw-artifacts/context.txt + + - uses: ./.github/actions/setup-sfw + with: + python: "true" + mode: firewall-free + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-pypi + # Tee to an absolute path under the workspace so the log lands in the + # repo-root sfw-artifacts/ dir despite this step's working-directory. + run: | + set -o pipefail + python -m venv .venv + # shellcheck disable=SC1091 + source .venv/bin/activate + sfw pip install -r requirements.txt 2>&1 | tee "$GITHUB_WORKSPACE/sfw-artifacts/sfw-pip-install.log" + + - name: Collect SFW JSON report + # socketdev/action points sfw at SFW_JSON_REPORT_PATH (a $RUNNER_TEMP + # file) and reads it back in its post step to render the job summary, so + # COPY (don't move) the report into the bundle. sfw writes it even when + # it blocks an install -- always() keeps it on failures too. + if: always() + run: | + if [ -n "${SFW_JSON_REPORT_PATH:-}" ] && [ -f "$SFW_JSON_REPORT_PATH" ]; then + cp "$SFW_JSON_REPORT_PATH" "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report.json" + echo "Collected SFW report -> sfw-artifacts/sfw-report.json" + else + echo "No SFW JSON report found at '${SFW_JSON_REPORT_PATH:-}'." \ + > "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report-missing.txt" + fi + + - name: Upload SFW report artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: socket-firewall-free-pypi-${{ github.event.pull_request.number }} + path: sfw-artifacts/ + if-no-files-found: warn + retention-days: 14 + + fixture-pypi-sfw-smoke-enterprise: + needs: inspect + if: | + needs.inspect.outputs.fixture_pypi_changed == 'true' && + needs.inspect.outputs.sfw_mode == 'firewall-enterprise' + runs-on: ubuntu-latest + timeout-minutes: 15 + environment: socket-firewall + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Prepare SFW artifact directory + run: | + mkdir -p sfw-artifacts + { + echo "mode=firewall-enterprise" + echo "manifest=pypi" + echo "pr=${{ github.event.pull_request.number }}" + echo "sha=${{ github.event.pull_request.head.sha }}" + } > sfw-artifacts/context.txt + + - uses: ./.github/actions/setup-sfw + with: + python: "true" + mode: firewall-enterprise + socket-token: ${{ secrets.SOCKET_SFW_API_TOKEN }} + + - name: Install fixture through Socket Firewall + working-directory: tests/e2e/fixtures/simple-pypi + # Tee to an absolute path under the workspace so the log lands in the + # repo-root sfw-artifacts/ dir despite this step's working-directory. + run: | + set -o pipefail + python -m venv .venv + # shellcheck disable=SC1091 + source .venv/bin/activate + sfw pip install -r requirements.txt 2>&1 | tee "$GITHUB_WORKSPACE/sfw-artifacts/sfw-pip-install.log" + + - name: Collect SFW JSON report + # socketdev/action points sfw at SFW_JSON_REPORT_PATH (a $RUNNER_TEMP + # file) and reads it back in its post step to render the job summary, so + # COPY (don't move) the report into the bundle. sfw writes it even when + # it blocks an install -- always() keeps it on failures too. + if: always() + run: | + if [ -n "${SFW_JSON_REPORT_PATH:-}" ] && [ -f "$SFW_JSON_REPORT_PATH" ]; then + cp "$SFW_JSON_REPORT_PATH" "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report.json" + echo "Collected SFW report -> sfw-artifacts/sfw-report.json" + else + echo "No SFW JSON report found at '${SFW_JSON_REPORT_PATH:-}'." \ + > "$GITHUB_WORKSPACE/sfw-artifacts/sfw-report-missing.txt" + fi + + - name: Upload SFW report artifact + if: always() + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: socket-firewall-enterprise-pypi-${{ github.event.pull_request.number }} + path: sfw-artifacts/ + if-no-files-found: warn + retention-days: 14 + + dockerfile-smoke: + needs: inspect + if: needs.inspect.outputs.dockerfile_changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + + - name: Build the Dockerfile (no push) + run: docker build --pull -t socket-python-cli:dependabot-smoke . + + workflow-notice: + needs: inspect + if: needs.inspect.outputs.workflow_or_action_changed == 'true' + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Flag workflow-sensitive updates + run: | + { + echo "## Sensitive File Notice" + echo "This PR changes workflow, composite-action, or dependabot config files." + echo "Require explicit human review before merge." + } >> "$GITHUB_STEP_SUMMARY" + + # Single required status check that aggregates the conditional smoke jobs + # above. Branch protection can't require those jobs individually: each is + # conditional (per-manifest, and Firewall-free vs -enterprise per author), so + # on any given PR most are legitimately skipped -- and a required check whose + # job is skipped sits at "Expected -- Waiting for status to be reported" + # forever, blocking merge (the same trap that stranded Dependabot PRs on the + # e2e-* checks). + # + # This gate always runs (if: always(), so it reports even when upstream jobs + # are skipped or fail) and collapses them into one pass/fail signal: it FAILS + # if any smoke job that ran ended in failure or was cancelled, and passes when + # everything either succeeded or was not applicable. 'skipped' is expected and + # allowed -- it just means the job didn't apply to this PR. + # + # Mark THIS check (dependency-review-gate) required in branch protection. It + # satisfies Dependabot/fork PRs (which run the Firewall-free job) and + # maintainer PRs (which run Firewall-enterprise) alike, and -- crucially -- a + # Socket Firewall BLOCK now fails the gate and blocks merge, instead of living + # in a non-required enterprise job that nobody is forced to run. + dependency-review-gate: + needs: + - inspect + - python-sfw-smoke-free + - python-sfw-smoke-enterprise + - fixture-npm-sfw-smoke-free + - fixture-npm-sfw-smoke-enterprise + - fixture-pypi-sfw-smoke-free + - fixture-pypi-sfw-smoke-enterprise + - dockerfile-smoke + if: always() + runs-on: ubuntu-latest + timeout-minutes: 2 + steps: + - name: Verify no smoke job failed + env: + RESULTS: ${{ toJSON(needs) }} + run: | + echo "Upstream job results:" + printf '%s\n' "$RESULTS" | python3 -m json.tool + + # Fail the gate if any needed job ended in failure or was cancelled. + # 'success' and 'skipped' both pass: skipped means the job did not + # apply to this PR (wrong manifest, or free-vs-enterprise mismatch). + failed="$(printf '%s\n' "$RESULTS" | python3 -c " + import json, sys + data = json.load(sys.stdin) + bad = [name for name, info in data.items() + if info.get('result') in ('failure', 'cancelled')] + print(' '.join(sorted(bad))) + ")" + + if [ -n "$failed" ]; then + echo "::error::dependency-review smoke job(s) failed: $failed" + { + echo "## Dependency Review Gate: FAILED" + echo "The following smoke job(s) failed or were cancelled: \`$failed\`" + echo "If a Socket Firewall job is listed, it likely BLOCKED an install --" + echo "inspect its uploaded sfw-artifacts/ report before merging." + } >> "$GITHUB_STEP_SUMMARY" + exit 1 + fi + + echo "All dependency-review smoke jobs passed or were not applicable." + echo "## Dependency Review Gate: PASSED" >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml index 0f113b06..934e0d9a 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -6,32 +6,42 @@ on: description: 'Version to mark as stable (e.g., 1.2.3)' required: true +permissions: + contents: read + jobs: stable: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false - name: Check if version exists in PyPI id: version_check + env: + INPUT_VERSION: ${{ inputs.version }} run: | - if ! curl -s -f https://pypi.org/pypi/socketsecurity/${{ inputs.version }}/json > /dev/null; then - echo "Error: Version ${{ inputs.version }} not found on PyPI" + if ! curl -s -f "https://pypi.org/pypi/socketsecurity/${INPUT_VERSION}/json" > /dev/null; then + echo "Error: Version ${INPUT_VERSION} not found on PyPI" exit 1 fi - echo "Version ${{ inputs.version }} found on PyPI - proceeding with release" + echo "Version ${INPUT_VERSION} found on PyPI - proceeding with release" - - name: Login to Docker Hub - uses: docker/login-action@v3 + - name: Set up Docker publishing + uses: ./.github/actions/setup-docker with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build & Push Stable Docker - uses: docker/build-push-action@v5 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 with: push: true platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max tags: socketdev/cli:stable build-args: | - CLI_VERSION=${{ inputs.version }} \ No newline at end of file + CLI_VERSION=${{ inputs.version }} + diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 00000000..1dab8a89 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,137 @@ +name: E2E Tests + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e: + # Skip e2e on: + # - PRs from forks (no secrets) + # - Dependabot PRs (no secrets, and dependency-bump risk is already + # covered by dependency-review.yml's Socket Firewall smoke jobs) + 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 + strategy: + fail-fast: false + matrix: + include: + - name: scan + args: >- + --target-path tests/e2e/fixtures/simple-npm + --disable-blocking + --enable-debug + validate: tests/e2e/validate-scan.sh + + - name: sarif + args: >- + --target-path tests/e2e/fixtures/simple-npm + --sarif-file /tmp/results.sarif + --disable-blocking + validate: tests/e2e/validate-sarif.sh + + - name: reachability + args: >- + --target-path tests/e2e/fixtures/simple-npm + --reach + --disable-blocking + --enable-debug + validate: tests/e2e/validate-reachability.sh + setup-node: "true" + + - name: gitlab + args: >- + --target-path tests/e2e/fixtures/simple-npm + --enable-gitlab-security + --disable-blocking + validate: tests/e2e/validate-gitlab.sh + + - name: json + args: >- + --target-path tests/e2e/fixtures/simple-npm + --enable-json + --disable-blocking + validate: tests/e2e/validate-json.sh + + - name: pypi + args: >- + --target-path tests/e2e/fixtures/simple-pypi + --disable-blocking + --enable-debug + validate: tests/e2e/validate-scan.sh + + name: e2e-${{ matrix.name }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: '3.12' + + - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + if: matrix.setup-node == 'true' + with: + node-version: '20' + + - name: Install CLI from local repo + run: | + python -m pip install --upgrade pip + pip install . + + - name: Install uv + if: matrix.setup-node == 'true' + run: pip install uv + + - name: Run Socket CLI + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + set -o pipefail + socketcli ${{ matrix.args }} 2>&1 | tee /tmp/e2e-output.log + + - name: Validate results + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: bash ${{ matrix.validate }} + + # 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 + # matrix, so the e2e-* check contexts are never created and the required + # checks sit at "Expected -- Waiting for status to be reported" forever, + # permanently blocking merge. + # + # This bypass reports a green status under the SAME e2e-* check names for + # exactly those PRs, satisfying branch protection without running the real + # tests (which need SOCKET_CLI_API_TOKEN). Its `if` is the precise negation + # of the e2e job's run condition, so the two are mutually exclusive: any + # given PR runs one or the other, never both, and never neither. + # + # Dependency-bump risk on these PRs is still covered by dependency-review.yml's + # Socket Firewall smoke jobs, which run without repository secrets. + e2e-bypass: + 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 + strategy: + matrix: + name: [scan, sarif, reachability, gitlab, json, pypi] + name: e2e-${{ matrix.name }} + steps: + - name: Report skip status + run: | + echo "Skipping e2e-${{ matrix.name }} for a PR without repository secrets" + echo "(fork or Dependabot). Dependency risk is covered by dependency-review.yml." diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 8fdd6678..eb29ef91 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -1,70 +1,78 @@ name: PR Preview on: pull_request: - types: [opened, synchronize] + types: [opened, synchronize, ready_for_review] + +# Cancel an in-flight preview when the PR is pushed again -- previews are slow +# (publish + multi-step Docker build), so superseded runs shouldn't keep going. +concurrency: + group: pr-preview-${{ github.event.pull_request.number }} + cancel-in-progress: true jobs: preview: + # Skip on: + # - PRs from forks (no access to publish secrets) + # - Dependabot PRs: preview-publishing a dependency bump to Test PyPI / + # Docker Hub is pointless and fails (no version bump, secret access). + if: >- + github.event.pull_request.head.repo.full_name == github.repository && + github.event.pull_request.user.login != 'dependabot[bot]' runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: '3.x' + python-version: '3.13' - - name: Set preview version - run: | - BASE_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'") - PREVIEW_VERSION="${BASE_VERSION}.dev${{ github.event.pull_request.number }}${{ github.event.pull_request.commits }}" - echo "VERSION=${PREVIEW_VERSION}" >> $GITHUB_ENV + - name: Install build tooling + uses: ./.github/actions/setup-hatch - # Update version in __init__.py - echo "__version__ = \"${PREVIEW_VERSION}\"" > socketsecurity/__init__.py.tmp - cat socketsecurity/__init__.py | grep -v "__version__" >> socketsecurity/__init__.py.tmp - mv socketsecurity/__init__.py.tmp socketsecurity/__init__.py + - name: Inject full dynamic version + run: python .hooks/sync_version.py --dev - # Verify the change - echo "Updated version in __init__.py:" - cat socketsecurity/__init__.py | grep "__version__" + - name: Clean previous builds + run: rm -rf dist/ build/ *.egg-info - - name: Check if version exists on Test PyPI + - name: Get Hatch version + id: version + run: | + VERSION=$(hatch version | cut -d+ -f1) + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Check if version already exists on Test PyPI id: version_check env: VERSION: ${{ env.VERSION }} run: | - if curl -s -f https://test.pypi.org/pypi/socketsecurity/$VERSION/json > /dev/null; then - echo "Version ${VERSION} already exists on Test PyPI" + if curl -s -f https://test.pypi.org/pypi/socketsecurity/${VERSION}/json > /dev/null; then echo "exists=true" >> $GITHUB_OUTPUT else - echo "Version ${VERSION} not found on Test PyPI" echo "exists=false" >> $GITHUB_OUTPUT fi - name: Build package if: steps.version_check.outputs.exists != 'true' run: | - pip install build - python -m build - - - name: Restore original version - if: always() - run: | - BASE_VERSION=$(echo $VERSION | cut -d'.' -f1-3) - echo "__version__ = \"${BASE_VERSION}\"" > socketsecurity/__init__.py.tmp - cat socketsecurity/__init__.py | grep -v "__version__" >> socketsecurity/__init__.py.tmp - mv socketsecurity/__init__.py.tmp socketsecurity/__init__.py + hatch build - name: Publish to Test PyPI if: steps.version_check.outputs.exists != 'true' - uses: pypa/gh-action-pypi-publish@v1.8.11 + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 with: repository-url: https://test.pypi.org/legacy/ - password: ${{ secrets.TEST_PYPI_TOKEN }} verbose: true - name: Comment on PR if: steps.version_check.outputs.exists != 'true' - uses: actions/github-script@v7 + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: VERSION: ${{ env.VERSION }} with: @@ -133,22 +141,29 @@ jobs: echo "success=false" >> $GITHUB_OUTPUT exit 1 - - name: Login to Docker Hub + - name: Set up Docker publishing if: steps.verify_package.outputs.success == 'true' - uses: docker/login-action@v3 + uses: ./.github/actions/setup-docker with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build & Push Docker Preview if: steps.verify_package.outputs.success == 'true' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 env: VERSION: ${{ env.VERSION }} with: push: true - tags: socketdev/cli:pr-${{ github.event.pull_request.number }} + # Preview images are for quick testing -- build amd64 only. arm64 via + # QEMU emulation is the slowest part of the job; release builds keep + # multi-arch. GHA layer cache speeds up repeated preview builds. + platforms: linux/amd64 + cache-from: type=gha + cache-to: type=gha,mode=max + tags: | + socketdev/cli:pr-${{ github.event.pull_request.number }} build-args: | CLI_VERSION=${{ env.VERSION }} PIP_INDEX_URL=https://test.pypi.org/simple - PIP_EXTRA_INDEX_URL=https://pypi.org/simple \ No newline at end of file + PIP_EXTRA_INDEX_URL=https://pypi.org/simple diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml new file mode 100644 index 00000000..3247275a --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,88 @@ +name: Unit Tests + +env: + PYTHON_VERSION: "3.12" + +on: + push: + branches: [main] + paths: + - "socketsecurity/**/*.py" + - "tests/unit/**/*.py" + - "tests/core/**/*.py" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/python-tests.yml" + pull_request: + paths: + - "socketsecurity/**/*.py" + - "tests/unit/**/*.py" + - "tests/core/**/*.py" + - "pyproject.toml" + - "uv.lock" + - ".github/workflows/python-tests.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: python-tests-${{ github.ref }} + cancel-in-progress: true + +jobs: + python-tests: + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + - name: 🐍 setup python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: 🛠️ install deps + run: | + python -m pip install --upgrade pip + pip install uv + uv sync --extra test + - name: 🔒 verify uv.lock is in sync with pyproject.toml + run: uv lock --locked + - name: 🧪 run tests + run: uv run pytest -q tests/unit/ tests/core/ + - name: 💨 import smoke (catches API-removal breaks from upgraded deps) + run: | + uv run python -c " + from socketsecurity.socketcli import cli + from socketsecurity.core import Core + from socketsecurity.core.exceptions import APIFailure, APIResourceNotFound + from socketsecurity.core.git_interface import Git + from socketsecurity.config import CliConfig + print('import smoke OK') + " + - name: 🛡️ pip-audit (known CVEs in the locked deps) + run: | + uv export --no-hashes --no-emit-project --format requirements-txt > /tmp/req-audit.txt + uvx pip-audit --strict --progress-spinner off --disable-pip --no-deps -r /tmp/req-audit.txt + + unsupported-python-install: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 1 + persist-credentials: false + - name: 🐍 setup python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 + with: + python-version: "3.10" + - name: 🚫 verify install is rejected on unsupported python + run: | + python -m pip install --upgrade pip + if pip install .; then + echo "Expected pip install . to fail on Python 3.10" + exit 1 + fi diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 1004ce64..6c41e9c0 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,25 +1,35 @@ name: Release on: - push: - tags: - - 'v*' + release: + types: [published] jobs: release: runs-on: ubuntu-latest + permissions: + id-token: write + contents: read steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: '3.x' + python-version: '3.13' + + - name: Install build tooling + uses: ./.github/actions/setup-hatch - name: Get Version id: version + env: + REF_NAME: ${{ github.ref_name }} run: | - RAW_VERSION=$(python -c "from socketsecurity import __version__; print(__version__)") + RAW_VERSION=$(hatch version) echo "VERSION=$RAW_VERSION" >> $GITHUB_ENV - if [ "v$RAW_VERSION" != "${{ github.ref_name }}" ]; then - echo "Error: Git tag (${{ github.ref_name }}) does not match package version (v$RAW_VERSION)" + if [ "v$RAW_VERSION" != "$REF_NAME" ]; then + echo "Error: Git tag ($REF_NAME) does not match hatch version (v$RAW_VERSION)" exit 1 fi @@ -41,7 +51,7 @@ jobs: env: VERSION: ${{ env.VERSION }} run: | - if curl -s -f "https://hub.docker.com/v2/repositories/socketdev/cli/tags/${{ env.VERSION }}" > /dev/null; then + if curl -s -f "https://hub.docker.com/v2/repositories/socketdev/cli/tags/${VERSION}" > /dev/null; then echo "Docker image socketdev/cli:${VERSION} already exists" echo "docker_exists=true" >> $GITHUB_OUTPUT else @@ -51,20 +61,18 @@ jobs: - name: Build package if: steps.version_check.outputs.pypi_exists != 'true' run: | - pip install build - python -m build + pip install hatchling + hatch build - name: Publish to PyPI if: steps.version_check.outputs.pypi_exists != 'true' - uses: pypa/gh-action-pypi-publish@v1.8.11 - with: - password: ${{ secrets.PYPI_TOKEN }} + uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0 - - name: Login to Docker Hub - uses: docker/login-action@v3 + - name: Set up Docker publishing + uses: ./.github/actions/setup-docker with: - username: ${{ secrets.DOCKERHUB_USERNAME }} - password: ${{ secrets.DOCKERHUB_TOKEN }} + dockerhub-username: ${{ secrets.DOCKERHUB_USERNAME }} + dockerhub-token: ${{ secrets.DOCKERHUB_TOKEN }} - name: Verify package is installable id: verify_package @@ -88,12 +96,16 @@ jobs: if: | steps.verify_package.outputs.success == 'true' && steps.docker_check.outputs.docker_exists != 'true' - uses: docker/build-push-action@v5 + uses: docker/build-push-action@f9f3042f7e2789586610d6e8b85c8f03e5195baf # v7.2.0 env: VERSION: ${{ env.VERSION }} with: push: true platforms: linux/amd64,linux/arm64 + cache-from: type=gha + cache-to: type=gha,mode=max tags: | socketdev/cli:latest - socketdev/cli:${{ env.VERSION }} \ No newline at end of file + socketdev/cli:${{ env.VERSION }} + build-args: | + CLI_VERSION=${{ env.VERSION }} diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml index 96cdc093..a0972080 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -1,46 +1,97 @@ name: Version Check on: pull_request: - types: [opened, synchronize] + types: [opened, synchronize, ready_for_review] paths: - 'socketsecurity/**' - - 'setup.py' - 'pyproject.toml' + - 'uv.lock' + +permissions: + contents: read + pull-requests: write + issues: write jobs: check_version: + # Skip on Dependabot PRs: they bump dependencies (touching uv.lock / + # pyproject.toml) without bumping the app version, so the increment check + # would always fail. App-version bumps come from maintainer PRs. + if: github.event.pull_request.user.login != 'dependabot[bot]' runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 # Fetch all history for all branches + persist-credentials: false - name: Check version increment id: version_check run: | + python -m pip install --upgrade pip + pip install packaging + # Get version from current PR PR_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'") echo "PR_VERSION=$PR_VERSION" >> $GITHUB_ENV # Get version from main branch - git checkout origin/main - MAIN_VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'") + MAIN_VERSION=$(git show origin/main:socketsecurity/__init__.py | grep -o "__version__.*" | awk '{print $3}' | tr -d "'") echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV - # Compare versions using Python - python3 -c " + export PR_VERSION + export MAIN_VERSION + + # Compare against both main and latest published PyPI release. + python3 <<'PY' + import json + import os + import urllib.request from packaging import version - pr_ver = version.parse('${PR_VERSION}') - main_ver = version.parse('${MAIN_VERSION}') - if pr_ver <= main_ver: - print(f'❌ Version must be incremented! Main: {main_ver}, PR: {pr_ver}') - exit(1) - print(f'✅ Version properly incremented from {main_ver} to {pr_ver}') - " + + pr_ver = version.parse(os.environ["PR_VERSION"]) + main_ver = version.parse(os.environ["MAIN_VERSION"]) + + with urllib.request.urlopen("https://pypi.org/pypi/socketsecurity/json") as response: + pypi_data = json.load(response) + + published_versions = [] + for raw in pypi_data.get("releases", {}).keys(): + parsed = version.parse(raw) + if not parsed.is_prerelease and not parsed.is_devrelease: + published_versions.append(parsed) + + pypi_ver = max(published_versions) if published_versions else version.parse("0.0.0") + required_floor = max(main_ver, pypi_ver) + + if pr_ver <= required_floor: + print( + f"❌ Version must be greater than main and PyPI! " + f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}" + ) + raise SystemExit(1) + + print( + f"✅ Version properly incremented. " + f"Main: {main_ver}, PyPI: {pypi_ver}, PR: {pr_ver}" + ) + PY + + - name: Require uv.lock update when pyproject changes + run: | + CHANGED_FILES="$(git diff --name-only origin/main...HEAD)" + + if echo "$CHANGED_FILES" | grep -qx 'pyproject.toml'; then + if ! echo "$CHANGED_FILES" | grep -qx 'uv.lock'; then + echo "❌ pyproject.toml changed, but uv.lock was not updated." + echo "Run 'uv lock' and commit uv.lock with the version bump." + exit 1 + fi + fi - name: Manage PR Comment - uses: actions/github-script@v7 - if: always() + uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 + if: always() && github.event.pull_request.head.repo.full_name == github.repository env: MAIN_VERSION: ${{ env.MAIN_VERSION }} PR_VERSION: ${{ env.PR_VERSION }} @@ -87,4 +138,4 @@ jobs: issue_number: prNumber, body: `❌ **Version Check Failed**\n\nPlease increment...` }); - } \ No newline at end of file + } diff --git a/.github/zizmor.yml b/.github/zizmor.yml new file mode 100644 index 00000000..39d1b180 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,3 @@ +rules: + secrets-outside-env: + disable: true diff --git a/.gitignore b/.gitignore index 405a91f3..10615ef9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,23 +1,58 @@ -.idea -venv -.venv -build -dist +# --- Python bytecode / cache --- +*.pyc +__pycache__/ +.coverage +.coverage.* +coverage.xml +htmlcov/ +.pytest_cache/ + +# --- Virtual environments --- +venv/ +.venv/ +.venv-test/ +Pipfile + +# --- Build / packaging --- *.build *.dist -*.egg-info -test -*.env -run_container.sh +*.egg-info/ +bin/ +build/ +dist/ *.zip -bin -scripts/*.py + +# --- Editor / IDE --- +.idea/ +*.swp +*.swo + +# --- OS --- +.DS_Store + +# --- Logs --- +logs/ + +# --- Env files --- +*.env +.env.local + +# --- Generated output --- *.json +!tests/**/*.json +!examples/config/*.json +*.sarif markdown_overview_temp.md markdown_security_temp.md -.DS_Store -*.pyc -test.py -*.cpython-312.pyc` + +# --- Project-specific scratch --- +ai_testing/ file_generator.py -.env.local \ No newline at end of file +run_container.sh +scripts/*.py +test/ +test.py +verify_find_files_lazy_loading.py + +# --- Conductor workspace --- +.context/ diff --git a/.hooks/sync_version.py b/.hooks/sync_version.py new file mode 100644 index 00000000..57b29d31 --- /dev/null +++ b/.hooks/sync_version.py @@ -0,0 +1,173 @@ +#!/usr/bin/env python3 +import subprocess +import pathlib +import re +import sys +import urllib.request +import json + +INIT_FILE = pathlib.Path("socketsecurity/__init__.py") +PYPROJECT_FILE = pathlib.Path("pyproject.toml") +UV_LOCK_FILE = pathlib.Path("uv.lock") + +VERSION_PATTERN = re.compile(r"__version__\s*=\s*['\"]([^'\"]+)['\"]") +PYPROJECT_PATTERN = re.compile(r'^version\s*=\s*".*"$', re.MULTILINE) +STABLE_VERSION_PATTERN = re.compile(r"^\d+\.\d+\.\d+$") +PYPI_PROD_API = "https://pypi.org/pypi/socketsecurity/json" +PYPI_TEST_API = "https://test.pypi.org/pypi/socketsecurity/json" + +def read_version_from_init(path: pathlib.Path) -> str: + content = path.read_text() + match = VERSION_PATTERN.search(content) + if not match: + print(f"❌ Could not find __version__ in {path}") + sys.exit(1) + return match.group(1) + +def read_version_from_git(path: str) -> str: + try: + output = subprocess.check_output(["git", "show", f"HEAD:{path}"], text=True) + match = VERSION_PATTERN.search(output) + if not match: + return None + return match.group(1) + except subprocess.CalledProcessError: + return None + +def bump_patch_version(version: str) -> str: + if ".dev" in version: + version = version.split(".dev")[0] + parts = version.split(".") + parts[-1] = str(int(parts[-1]) + 1) + return ".".join(parts) + +def parse_stable_version(version: str): + if not STABLE_VERSION_PATTERN.fullmatch(version): + return None + return tuple(int(part) for part in version.split(".")) + + +def format_stable_version(version_parts) -> str: + return ".".join(str(part) for part in version_parts) + + +def fetch_existing_versions(api_url: str) -> set: + try: + with urllib.request.urlopen(api_url) as response: + data = json.load(response) + return set(data.get("releases", {}).keys()) + except Exception as e: + print(f"⚠️ Warning: Failed to fetch versions from {api_url}: {e}") + return set() + + +def fetch_latest_stable_pypi_version(): + versions = fetch_existing_versions(PYPI_PROD_API) + stable_versions = [] + for ver in versions: + parsed = parse_stable_version(ver) + if parsed is not None: + stable_versions.append(parsed) + if not stable_versions: + return None + return max(stable_versions) + +def find_next_available_dev_version(base_version: str) -> str: + existing_versions = fetch_existing_versions(PYPI_TEST_API) + for i in range(1, 100): + candidate = f"{base_version}.dev{i}" + if candidate not in existing_versions: + return candidate + print("❌ Could not find available .devN slot after 100 attempts.") + sys.exit(1) + + +def find_next_stable_patch_version(current_version: str) -> str: + current_stable = current_version.split(".dev")[0] if ".dev" in current_version else current_version + current_parts = parse_stable_version(current_stable) + if current_parts is None: + print(f"❌ Unsupported version format for stable bump: {current_version}") + sys.exit(1) + + latest_pypi_parts = fetch_latest_stable_pypi_version() + base_parts = max([current_parts, latest_pypi_parts] if latest_pypi_parts else [current_parts]) + next_parts = (base_parts[0], base_parts[1], base_parts[2] + 1) + return format_stable_version(next_parts) + +def inject_version(version: str): + print(f"🔁 Updating version to: {version}") + + # Update __init__.py + init_content = INIT_FILE.read_text() + new_init_content = VERSION_PATTERN.sub(f"__version__ = '{version}'", init_content) + INIT_FILE.write_text(new_init_content) + + # Update pyproject.toml + pyproject = PYPROJECT_FILE.read_text() + if PYPROJECT_PATTERN.search(pyproject): + new_pyproject = PYPROJECT_PATTERN.sub(f'version = "{version}"', pyproject) + else: + new_pyproject = re.sub(r"(\[project\])", rf"\1\nversion = \"{version}\"", pyproject) + PYPROJECT_FILE.write_text(new_pyproject) + + +def run_uv_lock() -> bool: + before = UV_LOCK_FILE.read_bytes() if UV_LOCK_FILE.exists() else b"" + try: + subprocess.run(["uv", "lock"], check=True, text=True) + except FileNotFoundError: + print("❌ `uv` is required but was not found in PATH.") + sys.exit(1) + except subprocess.CalledProcessError: + print("❌ `uv lock` failed. Please run it manually and fix any errors.") + sys.exit(1) + + after = UV_LOCK_FILE.read_bytes() if UV_LOCK_FILE.exists() else b"" + return before != after + +def main(): + dev_mode = "--dev" in sys.argv + current_version = read_version_from_init(INIT_FILE) + previous_version = read_version_from_git("socketsecurity/__init__.py") + + print(f"Current: {current_version}, Previous: {previous_version}") + + if current_version == previous_version: + if dev_mode: + base_version = current_version.split(".dev")[0] if ".dev" in current_version else current_version + new_version = find_next_available_dev_version(base_version) + inject_version(new_version) + uv_lock_changed = run_uv_lock() + lock_hint = " and uv.lock" if uv_lock_changed else "" + print(f"⚠️ Version was unchanged — auto-bumped. Please git add{lock_hint} + commit again.") + sys.exit(0) + else: + new_version = find_next_stable_patch_version(current_version) + inject_version(new_version) + uv_lock_changed = run_uv_lock() + lock_hint = " and uv.lock" if uv_lock_changed else "" + print(f"⚠️ Version was unchanged — auto-bumped to {new_version}. Please git add{lock_hint} + commit again.") + sys.exit(1) + else: + if not dev_mode: + current_parts = parse_stable_version(current_version) + latest_pypi_parts = fetch_latest_stable_pypi_version() + if current_parts is not None and latest_pypi_parts is not None and current_parts <= latest_pypi_parts: + next_parts = (latest_pypi_parts[0], latest_pypi_parts[1], latest_pypi_parts[2] + 1) + new_version = format_stable_version(next_parts) + inject_version(new_version) + uv_lock_changed = run_uv_lock() + lock_hint = " and uv.lock" if uv_lock_changed else "" + print(f"⚠️ Version {current_version} is already published on PyPI — auto-bumped to {new_version}. Please git add{lock_hint} + commit again.") + sys.exit(1) + + uv_lock_changed = run_uv_lock() + if uv_lock_changed: + print("⚠️ Version already bumped, but uv.lock was out of date and has been updated. Please git add uv.lock + commit again.") + sys.exit(1) + + print("✅ Version already bumped and uv.lock is up to date — proceeding.") + sys.exit(0) + +if __name__ == "__main__": + main() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000..d201e7f5 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: local + hooks: + - id: sync-version + name: Sync __version__ with hatch version + entry: python .hooks/sync_version.py + language: python + always_run: true + pass_filenames: false \ No newline at end of file diff --git a/.python-version b/.python-version new file mode 100644 index 00000000..e4fba218 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..39e686a1 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,336 @@ +# Changelog + +## 2.4.10 + +### Added: opt directories back into manifest discovery via `--include-dirs` + +- New `--include-dirs` flag (comma-separated directory names) that re-includes directories + the CLI excludes from manifest discovery by default. The default exclude list + (`node_modules`, `bower_components`, `jspm_packages`, `__pycache__`, `.venv`, `venv`, + `build`, `dist`, `.tox`, `.mypy_cache`, `.pytest_cache`, `*.egg-info`, `vendor`) is a sane + default, but some projects keep manifest files under those names — e.g. `build/requirements.txt`. + Pass `--include-dirs build,dist` to scan them. Names are matched against any path segment, + mirroring how the default exclude list is applied. +- `--include-module-folders` now functions as documented: it re-includes the JS/TS module + folders (`node_modules`, `bower_components`, `jspm_packages`) as a group. Previously the + flag was accepted but had no effect. + +## 2.4.9 + +### Added: opt-in streaming log channel via `--upload-logs` + +- New `--upload-logs` flag (default off). When set, each CLI invocation registers a run, reports a per-run status (`in_progress` / `success` / `failure` / `cancelled`), and uploads a transcript of its own log output to the Socket backend for that run, visible in the Socket admin views. The transcript is captured regardless of the local `--enable-debug` state; the existing terminal verbosity is unchanged. +- New `--no-upload-logs` flag (mutually exclusive with `--upload-logs`) explicitly opts the run out of uploading logs, even when an org-level override would otherwise enable it. Use this when you need a guaranteed no-upload guarantee (e.g. legal/consent reasons). +- The Socket backend can also force-enable streaming for specific orgs in the absence of an explicit opt-out. The feature is best-effort — registration or upload failures silently degrade and never block the scan. + +## 2.4.8 + +### Fixed: retry transient full-scan upload failures + +- The full-scan upload (`POST /orgs//full-scans`) now retries transient + gateway/connection failures — HTTP 502/503/504/408, dropped or reset connections, and + request timeouts — up to 3 total attempts with increasing waits (~10s, then ~30s, plus + jitter). Such failures are intermittent and a retried upload almost always succeeds. + In these failure modes the server never finished reading the request body, so no scan + was created and a retry does not duplicate one; in the rare case where a gateway + timeout races a request the server later completes, the extra scan is benign and + superseded by the retried one (as if the CLI had run twice). + Non-transient errors (400/401/403/404/429 and error payloads) are never retried. Each + retry logs a warning explaining what failed and when the next attempt happens. +- Requires `socketdev>=3.3.0`: the SDK now records the HTTP status code on the exceptions + it raises and owns the transient-vs-deterministic classification + (`APIFailure.is_transient_error()`), so the CLI no longer parses status codes out of + exception message text. + +## 2.4.7 + +### Changed: pin @coana-tech/cli version; auto-update is now opt-in + +- Reachability analysis now runs a fixed `@coana-tech/cli` version pinned to this CLI release + (`15.3.24`) via `npx`, instead of silently pulling the latest published version on every run. + Engine version changes now ride with the Socket Python CLI release (standard `pip` upgrade), + giving advance notice of analysis-engine changes. +- The CLI no longer runs `npm install -g @coana-tech/cli`; an existing global install is left + untouched (never auto-updated or downgraded). +- Opt into always-newest with `--reach-version latest`; pin an explicit version with + `--reach-version ` (unchanged). +- Runs the engine via `npx --yes --force` (the same flags the Socket Node CLI passes for + coana); `--yes` skips npx's interactive install prompt so non-interactive/CI runs don't hang. +- Added an `npm install` + `node` fallback for when the `npx` launcher is missing or fails + before the engine starts. The installed engine is cached per version for the process + lifetime (installs once). Tunable via `SOCKET_CLI_COANA_FORCE_NPM_INSTALL` (use the fallback + as the primary path) and `SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK` (never fall back). `node` is + now part of the up-front prerequisite check. Also strips `npm_package_*` env vars before + spawning the engine to avoid `E2BIG` in large monorepos. + +## 2.4.6 + +### Docs: reachability reference corrections + +- Documented the `uv` and Enterprise-plan prerequisites the CLI enforces **before** running + reachability (exit code 3 if unmet), and clarified that per-ecosystem build toolchains + (JDK / .NET / Go / a compatible Python interpreter) are checked by the analysis engine at + runtime, not pre-checked by the CLI. +- Corrected the `--reach-min-severity` values to `info, low, moderate, high, critical`. +- Documented the previously-undocumented reachability flags: `--reach-enable-analysis-splitting`, + `--reach-detailed-analysis-log-file`, `--reach-lazy-mode`, and `--reach-use-only-pregenerated-sboms`. +- Clarified that `--only-facts-file` submits only the facts file when **creating** the full scan + (it does not require a pre-existing scan). +- Documentation-only; no functional code changes. + +## 2.4.5 + +### Changed: Bump required SDK version to `>=3.2.1` + +- Picks up `socketdev 3.2.1`. +- No CLI logic changes. + +## 2.4.4 + +### Changed: Bump required SDK version to `>=3.2.0` + +- Picks up `socketdev 3.2.0`, which adds `OTHER = "other"` to `SocketCategory` + so the backend's `other` alert category no longer trips the + "Unknown SocketCategory" warning fallback (SDK PR #85). +- No CLI logic changes. + +## 2.4.3 + +### Added: unified `--exclude-paths` for manifest discovery and reachability + +- New `--exclude-paths` flag (comma-separated globs) that excludes matching paths from + BOTH SCA manifest discovery and reachability analysis. Patterns are scan-root-relative + anchored globs (`*` does not cross `/`, `**` does), matching the Node CLI's behavior. +- Pattern validation rejects unsupported forms (negation, absolute paths, `..` traversal, + and match-everything patterns). Patterns may be supplied on the CLI as a comma-separated + string or via a `--config` file list. +- `--reach-exclude-paths` is now deprecated in favor of `--exclude-paths`. It still works + (and is unioned into the Coana `--exclude-dirs` argument) but is marked deprecated in + `--help` and warns at runtime. + +## 2.4.2 + +### Added: reachability flag and Coana environment alignment with the Node CLI + +- New `--reach-disable-external-tool-checks` flag (passes `--disable-external-tool-checks` + to the Coana CLI). +- New `--reach-debug` flag to enable Coana debug output (`--debug`) independently of the + global `--enable-debug`. +- Node-style `--reach-analysis-timeout` and `--reach-analysis-memory-limit` are now the + primary flag names; the previous `--reach-timeout` / `--reach-memory-limit` continue to + work as hidden aliases. +- The Coana subprocess now receives `SOCKET_CLI_VERSION` and `SOCKET_CALLER_USER_AGENT` so + calls are attributed to the Python CLI. Proxies continue to work via the inherited + `HTTPS_PROXY` / `HTTP_PROXY` environment variables, which Coana reads itself. +- `SOCKET_REPO_NAME` / `SOCKET_BRANCH_NAME` are no longer forwarded to Coana when the repo + and branch are the default sentinels, avoiding cross-run reachability cache-bucket + collisions. +- Tier 1 reachability finalize now retries with exponential backoff instead of giving up on + the first transient error. + +## 2.4.1 + +### Added: pyenv in the Docker image + +- The `socketdev/cli` Docker image now bundles [pyenv](https://github.com/pyenv/pyenv) + (pinned to `v2.7.1`) along with the Alpine build dependencies needed to compile + CPython from source, so the image can build/install arbitrary Python versions on + demand. +- The CLI itself is unchanged — this release only affects the published Docker image. + +## 2.4.0 + +### Changed: license details are no longer requested on the full-scan diff + +- Full-scan diff requests now always set `include_license_details=false`, keeping + large diff responses smaller and avoiding truncation crashes on large repos. +- Soft breaking change for flag-scripted use: `--exclude-license-details` still + controls the dashboard report URL, but no longer affects the internal diff + request. Its `--help` text has been updated to reflect the narrower scope. +- License artifact output is unchanged: `--generate-license` continues to fetch + license details from the dedicated PURL endpoint. +- Requires `socketdev>=3.1.2`. + +## 2.3.1 + +### New: brotli-compressed `.socket.facts.json` upload + +The reachability facts file (`.socket.facts.json`) is now brotli-compressed before it is +uploaded as part of a full scan. The Socket API transparently decompresses any multipart +part named exactly `.socket.facts.json.br` and stores it as plain `.socket.facts.json`, so +the stored result is unchanged — but the on-the-wire payload shrinks dramatically (a +~262 MB facts file compresses to roughly 15–30 MB). + +This fixes large tier‑1 reachability scans that previously failed when the uncompressed +facts file exceeded the API's per‑file upload size cap (surfaced to the CLI as an HTTP +4xx/“502”, leaving the scan stuck with no report). + +Details: + +- Compression happens at the upload boundary (`Core.create_full_scan`); the file on disk is + left untouched, so local consumers (SARIF/JSON output, tier‑1 finalize, alert selection) + continue to read the plain `.socket.facts.json`. +- Only a file whose basename is exactly `.socket.facts.json` is compressed (the API matches + that exact name). A custom `--reach-output-file` name is uploaded uncompressed, as before. +- Empty baseline-scan placeholder files are not compressed. +- Compression never blocks an upload: if it fails for any reason it falls back to uploading + the plain file, and a partially-written `.socket.facts.json.br` is removed rather than + left behind in the target directory. +- Adds a `brotli` (CPython) / `brotlicffi` (PyPy) dependency. + +## 2.3.0 + +### New: `--exit-code-on-api-error` + +- Added `--exit-code-on-api-error` so CI can distinguish API / infrastructure + failures from blocking security findings. The default remains `3`; the flag + only changes behavior when set explicitly. +- `--disable-blocking` still takes precedence and exits `0` for all outcomes. + +### New: commit message auto-truncation + +- `--commit-message` values longer than 200 characters are now truncated before + being sent to the API, preventing HTTP 413 errors from oversized query + parameters. + +### Improved: Buildkite log formatting + +- Infrastructure errors now emit Buildkite log section markers when + `BUILDKITE=true`, making those failures easier to find in Buildkite logs. + +### Fixed + +- `--timeout` is now honored end-to-end: it was only applied to the local + `CliClient`, but the full-scan diff comparison uses the Socket SDK instance, + which was constructed without the CLI timeout and defaulted to 1200s. +- `--exclude-license-details` now propagates to the full-scan diff comparison + request (it was only applied to full-scan params / report URLs before). + +## 2.2.93 + +- Bundled twelve Dependabot dependency updates: `urllib3`, `gitpython`, `python-dotenv`, `pytest`, `uv`, `cryptography`, `pygments`, `requests`, and `idna` (main app), plus `axios`, `requests`, and `flask` (e2e fixtures). `idna` 3.11 → 3.15 includes the fix for CVE-2026-45409. +- Added `.github/dependabot.yml` with grouped weekly updates, a 7-day cooldown, and e2e fixtures excluded. +- Added a `dependabot-review` workflow that runs Socket Firewall (`sfw`) install checks on Dependabot PRs with no API token required. +- Added a `uv.lock` drift check, an import smoke test, and `pip-audit` to the test workflow; skipped e2e tests on Dependabot PRs. +- Tidied `.gitignore` and backfilled missing CHANGELOG entries for `2.2.81`, `2.2.85`, `2.2.86`, `2.2.88`, `2.2.89`, `2.2.91`, and `2.2.92`. + +## 2.2.92 + +- Fixed dependency-overview rendering for unmapped alert types: alert types the SDK + has no metadata for now fall back to a humanized Title-Cased label (e.g. + `gptDidYouMean` -> "Possible typosquat attack (GPT)", `SQLInjection` -> "SQL + Injection") instead of surfacing the raw camelCase identifier. + +## 2.2.91 + +- Added legal/compliance artifact presets (`--legal`) and FOSSA-compatible output + shapes (`--legal-format fossa`) for license and SBOM reporting. + +## 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.89 + +- Added `uv.lock` to the version-incrementation CI check so a `pyproject.toml` / + `__init__.py` version bump without a matching lockfile sync no longer slips through. +- Updated the local Python pre-commit hook to keep `uv.lock` in sync with + `pyproject.toml` and `socketsecurity/__init__.py` version changes automatically. + +## 2.2.88 + +- Added `bun.lock`, `bun.lockb`, and `vlt-lock.json` to the recognized manifest files + for Socket scanning, with matching unit-test coverage. + +## 2.2.86 + +- Bumped `socketdev` to `>=3.0.33,<4.0.0` to pick up the SDK fix for unknown alert + categories (the SDK previously crashed while deserializing diff alerts when the API + returned a category like `"other"`). +- Normalized diff artifacts with `score=None` to an empty score map in the CLI model + layer; PR-comment dependency-overview rendering no longer crashes on missing or + partial score data. +- Defaulted missing badge values to a valid `100%` fallback rather than producing + invalid badge URLs. + +## 2.2.85 + +- Added four hidden `--reach-continue-on-*` flags in preparation for Coana CLI v15: + `--reach-continue-on-analysis-errors`, `--reach-continue-on-install-errors`, + `--reach-continue-on-missing-lock-files`, `--reach-continue-on-no-source-files`. + Each forwards to the matching Coana flag and opts out of one of Coana v15's new + halt-by-default behaviors. No-op against today's default Coana version; will take + effect automatically once Coana v15 becomes the default. + +## 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.81 + +- Fixed GitLab security report schema compliance: corrected schema validation errors so + Socket-produced reports parse cleanly under GitLab's dependency-scanning ingestion. +- Populated scan alert data in the GitLab security report so previously-empty alert + sections now carry the expected findings. + +## 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. + +## 2.2.70 + +- Set the scan to `'socket_tier1'` when using the `--reach` flag. This ensures Tier 1 scans are properly integrated into the organization-wide alerts. + +## 2.2.69 + +- Added `--reach-enable-analysis-splitting` flag to enable analysis splitting (disabled by default). +- Added `--reach-detailed-analysis-log-file` flag to print detailed analysis log file path. +- Added `--reach-lazy-mode` flag to enable lazy mode for reachability analysis. +- Changed default behavior: analysis splitting is now disabled by default. The old `--reach-disable-analysis-splitting` flag is kept as a hidden no-op for backwards compatibility. + +## 2.2.64 + +- Included PyPy in the Docker image. + +## 2.2.57 + +- Fixed Dockerfile to set `GOROOT` to `/usr/lib/go` when using system Go (`GO_VERSION=system`) instead of always using `/usr/local/go`. + +## 2.2.56 + +- Removed process timeout from reachability analysis subprocess. Timeouts are now only passed to the Coana CLI via the `--analysis-timeout` flag. diff --git a/Dockerfile b/Dockerfile index 949ec588..75110780 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,146 @@ FROM python:3-alpine LABEL org.opencontainers.image.authors="socket.dev" + +# Language version arguments with defaults +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 +RUN apk update && apk add --no-cache \ + git nodejs npm yarn curl wget \ + ruby ruby-dev build-base strace + +# Install Go with version control +RUN if [ "$GO_VERSION" = "system" ]; then \ + apk add --no-cache go && \ + echo "/usr/lib/go" > /etc/goroot; \ + else \ + cd /tmp && \ + ARCH=$(uname -m) && \ + case $ARCH in \ + x86_64) GOARCH=amd64 ;; \ + aarch64) GOARCH=arm64 ;; \ + *) echo "Unsupported architecture: $ARCH" && exit 1 ;; \ + esac && \ + wget https://golang.org/dl/go${GO_VERSION}.linux-${GOARCH}.tar.gz && \ + tar -C /usr/local -xzf go${GO_VERSION}.linux-${GOARCH}.tar.gz && \ + rm go${GO_VERSION}.linux-${GOARCH}.tar.gz && \ + echo "/usr/local/go" > /etc/goroot; \ + fi + +# Install Java with version control +RUN if [ "$JAVA_VERSION" = "8" ]; then \ + apk add --no-cache openjdk8-jdk; \ + elif [ "$JAVA_VERSION" = "11" ]; then \ + apk add --no-cache openjdk11-jdk; \ + elif [ "$JAVA_VERSION" = "17" ]; then \ + apk add --no-cache openjdk17-jdk; \ + elif [ "$JAVA_VERSION" = "21" ]; then \ + apk add --no-cache openjdk21-jdk; \ + else \ + echo "Unsupported Java version: $JAVA_VERSION. Supported: 8, 11, 17, 21" && exit 1; \ + fi + +# Install .NET with version control +RUN if [ "$DOTNET_VERSION" = "6" ]; then \ + apk add --no-cache dotnet6-sdk; \ + elif [ "$DOTNET_VERSION" = "8" ]; then \ + apk add --no-cache dotnet8-sdk; \ + else \ + echo "Unsupported .NET version: $DOTNET_VERSION. Supported: 6, 8" && exit 1; \ + fi + +# Install PyPy (Alpine-compatible build for x86_64 only) +# PyPy is an alternative Python interpreter that makes the Python reachability analysis faster. +# This is a custom build of PyPy3.11 for Alpine on x86-64. +ARG TARGETARCH # Passed by Docker buildx +RUN if [ "$TARGETARCH" = "amd64" ]; then \ + PYPY_URL="https://github.com/BarrensZeppelin/alpine-pypy/releases/download/alp3.23.1-pypy3.11-7.3.20/pypy3.11-v7.3.20-linux64-alpine3.21.tar.bz2" && \ + PYPY_SHA256="60847fea6ffe96f10a3cd4b703686e944bb4fbcc01b7200c044088dd228425e1" && \ + curl -L -o /tmp/pypy.tar.bz2 "$PYPY_URL" && \ + echo "$PYPY_SHA256 /tmp/pypy.tar.bz2" | sha256sum -c - && \ + mkdir -p /opt/pypy && \ + tar -xj --strip-components=1 -C /opt/pypy -f /tmp/pypy.tar.bz2 && \ + rm /tmp/pypy.tar.bz2 && \ + ln -s /opt/pypy/bin/pypy3 /bin/pypy3 && \ + pypy3 --version; \ + fi + +# Install additional tools +RUN npm install @coana-tech/cli socket -g && \ + gem install bundler && \ + curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y && \ + . ~/.cargo/env && \ + rustup component add rustfmt clippy + +# Set environment paths +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 + +# Install pyenv +# pyenv lets us build/install arbitrary Python versions on demand. We install +# the build dependencies needed to compile CPython on Alpine, then install +# pyenv itself. We deliberately only symlink the `pyenv` binary onto the PATH +# and do NOT add pyenv's shims directory, so its shims don't shadow the system +# Python that the CLI runs on. +RUN apk add --no-cache \ + bash \ + bzip2-dev \ + ca-certificates \ + libffi-dev \ + libxslt-dev \ + linux-headers \ + ncurses-dev \ + openssl-dev \ + readline-dev \ + sqlite-dev \ + xz-dev \ + zlib-dev +RUN curl -L https://raw.githubusercontent.com/pyenv/pyenv-installer/master/bin/pyenv-installer | PYENV_GIT_TAG="v2.7.1" bash && \ + ln -s ~/.pyenv/bin/pyenv /bin/pyenv && \ + pyenv --version + +# Install CLI based on build mode +RUN if [ "$USE_LOCAL_INSTALL" = "true" ]; then \ + echo "Using local development install"; \ + else \ + 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 \ + break; \ + fi; \ + echo "Install failed, waiting 30s before retry..."; \ + sleep 30; \ + done && \ + if [ ! -z "$SDK_VERSION" ]; then \ + pip install --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 --upgrade -e .; \ + pip install --upgrade socketdev; \ + fi + +# Create workspace directory with proper permissions +RUN mkdir -p /go/src && chmod -R 777 /go -RUN apk update \ - && apk add --no-cache git nodejs npm yarn +# Copy and setup entrypoint script +COPY scripts/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh +RUN chmod +x /usr/local/bin/docker-entrypoint.sh -RUN pip install --index-url ${PIP_INDEX_URL} --extra-index-url ${PIP_EXTRA_INDEX_URL} socketsecurity==$CLI_VERSION \ - && socketcli -v \ - && socketcli -v | grep -q $CLI_VERSION \ No newline at end of file +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 00000000..c0fb1b01 --- /dev/null +++ b/Makefile @@ -0,0 +1,62 @@ +.PHONY: setup sync clean test lint update-lock local-dev first-time-setup dev-setup sync-all first-time-local-setup + +# Environment variable for local SDK path (optional) +SOCKET_SDK_PATH ?= ../socketdev + +# Environment variable to control local development mode +USE_LOCAL_SDK ?= false + +# === High-level workflow targets === + +# First-time repo setup after cloning (using PyPI packages) +first-time-setup: clean setup + +# First-time setup for local development (using local SDK) +first-time-local-setup: + $(MAKE) clean + $(MAKE) USE_LOCAL_SDK=true dev-setup + +# Update lock file after changing pyproject.toml +update-lock: + uv lock + +# Setup for local development +dev-setup: clean local-dev setup + +# Sync all dependencies after pulling changes +sync-all: sync + +# === Implementation targets === + +# Installs dependencies needed for local development +# Currently: socketdev from test PyPI or local path +local-dev: +ifeq ($(USE_LOCAL_SDK),true) + uv add --editable $(SOCKET_SDK_PATH) +endif + +# Creates virtual environment and installs dependencies from uv.lock +setup: update-lock + uv sync --all-extras +ifeq ($(USE_LOCAL_SDK),true) + uv add --editable $(SOCKET_SDK_PATH) +endif + +# Installs exact versions from uv.lock into your virtual environment +sync: + uv sync --all-extras +ifeq ($(USE_LOCAL_SDK),true) + uv add --editable $(SOCKET_SDK_PATH) +endif + +# Removes virtual environment and cache files +clean: + rm -rf .venv + find . -type d -name "__pycache__" -exec rm -rf {} + + +test: + uv run pytest + +lint: + uv run ruff check . + uv run ruff format --check . \ No newline at end of file diff --git a/Pipfile b/Pipfile deleted file mode 100644 index 839da360..00000000 --- a/Pipfile +++ /dev/null @@ -1,16 +0,0 @@ -[[source]] -url = "https://pypi.org/simple" -verify_ssl = true -name = "pypi" - -[packages] -requests = ">=2.32.0" -mdutils = "~=1.6.0" -prettytable = "*" -argparse = "*" -gitpython = "*" - -[dev-packages] - -[requires] -python_version = "3.12" diff --git a/Pipfile.lock b/Pipfile.lock deleted file mode 100644 index ee638cfb..00000000 --- a/Pipfile.lock +++ /dev/null @@ -1,207 +0,0 @@ -{ - "_meta": { - "hash": { - "sha256": "9a1e9bcbc5675fd9d1bf3d2ca44406464dfc12b058225c5ecc88442ef0449e88" - }, - "pipfile-spec": 6, - "requires": { - "python_version": "3.12" - }, - "sources": [ - { - "name": "pypi", - "url": "https://pypi.org/simple", - "verify_ssl": true - } - ] - }, - "default": { - "argparse": { - "hashes": [ - "sha256:62b089a55be1d8949cd2bc7e0df0bddb9e028faefc8c32038cc84862aefdd6e4", - "sha256:c31647edb69fd3d465a847ea3157d37bed1f95f19760b11a47aa91c04b666314" - ], - "index": "pypi", - "version": "==1.4.0" - }, - "certifi": { - "hashes": [ - "sha256:5a1e7645bc0ec61a09e26c36f6106dd4cf40c6db3a1fb6352b0244e7fb057c7b", - "sha256:c198e21b1289c2ab85ee4e67bb4b4ef3ead0892059901a8d5b622f24a1101e90" - ], - "index": "pypi", - "markers": "python_version >= '3.6'", - "version": "==2024.7.4" - }, - "charset-normalizer": { - "hashes": [ - "sha256:06435b539f889b1f6f4ac1758871aae42dc3a8c0e24ac9e60c2384973ad73027", - "sha256:06a81e93cd441c56a9b65d8e1d043daeb97a3d0856d177d5c90ba85acb3db087", - "sha256:0a55554a2fa0d408816b3b5cedf0045f4b8e1a6065aec45849de2d6f3f8e9786", - "sha256:0b2b64d2bb6d3fb9112bafa732def486049e63de9618b5843bcdd081d8144cd8", - "sha256:10955842570876604d404661fbccbc9c7e684caf432c09c715ec38fbae45ae09", - "sha256:122c7fa62b130ed55f8f285bfd56d5f4b4a5b503609d181f9ad85e55c89f4185", - "sha256:1ceae2f17a9c33cb48e3263960dc5fc8005351ee19db217e9b1bb15d28c02574", - "sha256:1d3193f4a680c64b4b6a9115943538edb896edc190f0b222e73761716519268e", - "sha256:1f79682fbe303db92bc2b1136016a38a42e835d932bab5b3b1bfcfbf0640e519", - "sha256:2127566c664442652f024c837091890cb1942c30937add288223dc895793f898", - "sha256:22afcb9f253dac0696b5a4be4a1c0f8762f8239e21b99680099abd9b2b1b2269", - "sha256:25baf083bf6f6b341f4121c2f3c548875ee6f5339300e08be3f2b2ba1721cdd3", - "sha256:2e81c7b9c8979ce92ed306c249d46894776a909505d8f5a4ba55b14206e3222f", - "sha256:3287761bc4ee9e33561a7e058c72ac0938c4f57fe49a09eae428fd88aafe7bb6", - "sha256:34d1c8da1e78d2e001f363791c98a272bb734000fcef47a491c1e3b0505657a8", - "sha256:37e55c8e51c236f95b033f6fb391d7d7970ba5fe7ff453dad675e88cf303377a", - "sha256:3d47fa203a7bd9c5b6cee4736ee84ca03b8ef23193c0d1ca99b5089f72645c73", - "sha256:3e4d1f6587322d2788836a99c69062fbb091331ec940e02d12d179c1d53e25fc", - "sha256:42cb296636fcc8b0644486d15c12376cb9fa75443e00fb25de0b8602e64c1714", - "sha256:45485e01ff4d3630ec0d9617310448a8702f70e9c01906b0d0118bdf9d124cf2", - "sha256:4a78b2b446bd7c934f5dcedc588903fb2f5eec172f3d29e52a9096a43722adfc", - "sha256:4ab2fe47fae9e0f9dee8c04187ce5d09f48eabe611be8259444906793ab7cbce", - "sha256:4d0d1650369165a14e14e1e47b372cfcb31d6ab44e6e33cb2d4e57265290044d", - "sha256:549a3a73da901d5bc3ce8d24e0600d1fa85524c10287f6004fbab87672bf3e1e", - "sha256:55086ee1064215781fff39a1af09518bc9255b50d6333f2e4c74ca09fac6a8f6", - "sha256:572c3763a264ba47b3cf708a44ce965d98555f618ca42c926a9c1616d8f34269", - "sha256:573f6eac48f4769d667c4442081b1794f52919e7edada77495aaed9236d13a96", - "sha256:5b4c145409bef602a690e7cfad0a15a55c13320ff7a3ad7ca59c13bb8ba4d45d", - "sha256:6463effa3186ea09411d50efc7d85360b38d5f09b870c48e4600f63af490e56a", - "sha256:65f6f63034100ead094b8744b3b97965785388f308a64cf8d7c34f2f2e5be0c4", - "sha256:663946639d296df6a2bb2aa51b60a2454ca1cb29835324c640dafb5ff2131a77", - "sha256:6897af51655e3691ff853668779c7bad41579facacf5fd7253b0133308cf000d", - "sha256:68d1f8a9e9e37c1223b656399be5d6b448dea850bed7d0f87a8311f1ff3dabb0", - "sha256:6ac7ffc7ad6d040517be39eb591cac5ff87416c2537df6ba3cba3bae290c0fed", - "sha256:6b3251890fff30ee142c44144871185dbe13b11bab478a88887a639655be1068", - "sha256:6c4caeef8fa63d06bd437cd4bdcf3ffefe6738fb1b25951440d80dc7df8c03ac", - "sha256:6ef1d82a3af9d3eecdba2321dc1b3c238245d890843e040e41e470ffa64c3e25", - "sha256:753f10e867343b4511128c6ed8c82f7bec3bd026875576dfd88483c5c73b2fd8", - "sha256:7cd13a2e3ddeed6913a65e66e94b51d80a041145a026c27e6bb76c31a853c6ab", - "sha256:7ed9e526742851e8d5cc9e6cf41427dfc6068d4f5a3bb03659444b4cabf6bc26", - "sha256:7f04c839ed0b6b98b1a7501a002144b76c18fb1c1850c8b98d458ac269e26ed2", - "sha256:802fe99cca7457642125a8a88a084cef28ff0cf9407060f7b93dca5aa25480db", - "sha256:80402cd6ee291dcb72644d6eac93785fe2c8b9cb30893c1af5b8fdd753b9d40f", - "sha256:8465322196c8b4d7ab6d1e049e4c5cb460d0394da4a27d23cc242fbf0034b6b5", - "sha256:86216b5cee4b06df986d214f664305142d9c76df9b6512be2738aa72a2048f99", - "sha256:87d1351268731db79e0f8e745d92493ee2841c974128ef629dc518b937d9194c", - "sha256:8bdb58ff7ba23002a4c5808d608e4e6c687175724f54a5dade5fa8c67b604e4d", - "sha256:8c622a5fe39a48f78944a87d4fb8a53ee07344641b0562c540d840748571b811", - "sha256:8d756e44e94489e49571086ef83b2bb8ce311e730092d2c34ca8f7d925cb20aa", - "sha256:8f4a014bc36d3c57402e2977dada34f9c12300af536839dc38c0beab8878f38a", - "sha256:9063e24fdb1e498ab71cb7419e24622516c4a04476b17a2dab57e8baa30d6e03", - "sha256:90d558489962fd4918143277a773316e56c72da56ec7aa3dc3dbbe20fdfed15b", - "sha256:923c0c831b7cfcb071580d3f46c4baf50f174be571576556269530f4bbd79d04", - "sha256:95f2a5796329323b8f0512e09dbb7a1860c46a39da62ecb2324f116fa8fdc85c", - "sha256:96b02a3dc4381e5494fad39be677abcb5e6634bf7b4fa83a6dd3112607547001", - "sha256:9f96df6923e21816da7e0ad3fd47dd8f94b2a5ce594e00677c0013018b813458", - "sha256:a10af20b82360ab00827f916a6058451b723b4e65030c5a18577c8b2de5b3389", - "sha256:a50aebfa173e157099939b17f18600f72f84eed3049e743b68ad15bd69b6bf99", - "sha256:a981a536974bbc7a512cf44ed14938cf01030a99e9b3a06dd59578882f06f985", - "sha256:a9a8e9031d613fd2009c182b69c7b2c1ef8239a0efb1df3f7c8da66d5dd3d537", - "sha256:ae5f4161f18c61806f411a13b0310bea87f987c7d2ecdbdaad0e94eb2e404238", - "sha256:aed38f6e4fb3f5d6bf81bfa990a07806be9d83cf7bacef998ab1a9bd660a581f", - "sha256:b01b88d45a6fcb69667cd6d2f7a9aeb4bf53760d7fc536bf679ec94fe9f3ff3d", - "sha256:b261ccdec7821281dade748d088bb6e9b69e6d15b30652b74cbbac25e280b796", - "sha256:b2b0a0c0517616b6869869f8c581d4eb2dd83a4d79e0ebcb7d373ef9956aeb0a", - "sha256:b4a23f61ce87adf89be746c8a8974fe1c823c891d8f86eb218bb957c924bb143", - "sha256:bd8f7df7d12c2db9fab40bdd87a7c09b1530128315d047a086fa3ae3435cb3a8", - "sha256:beb58fe5cdb101e3a055192ac291b7a21e3b7ef4f67fa1d74e331a7f2124341c", - "sha256:c002b4ffc0be611f0d9da932eb0f704fe2602a9a949d1f738e4c34c75b0863d5", - "sha256:c083af607d2515612056a31f0a8d9e0fcb5876b7bfc0abad3ecd275bc4ebc2d5", - "sha256:c180f51afb394e165eafe4ac2936a14bee3eb10debc9d9e4db8958fe36afe711", - "sha256:c235ebd9baae02f1b77bcea61bce332cb4331dc3617d254df3323aa01ab47bd4", - "sha256:cd70574b12bb8a4d2aaa0094515df2463cb429d8536cfb6c7ce983246983e5a6", - "sha256:d0eccceffcb53201b5bfebb52600a5fb483a20b61da9dbc885f8b103cbe7598c", - "sha256:d965bba47ddeec8cd560687584e88cf699fd28f192ceb452d1d7ee807c5597b7", - "sha256:db364eca23f876da6f9e16c9da0df51aa4f104a972735574842618b8c6d999d4", - "sha256:ddbb2551d7e0102e7252db79ba445cdab71b26640817ab1e3e3648dad515003b", - "sha256:deb6be0ac38ece9ba87dea880e438f25ca3eddfac8b002a2ec3d9183a454e8ae", - "sha256:e06ed3eb3218bc64786f7db41917d4e686cc4856944f53d5bdf83a6884432e12", - "sha256:e27ad930a842b4c5eb8ac0016b0a54f5aebbe679340c26101df33424142c143c", - "sha256:e537484df0d8f426ce2afb2d0f8e1c3d0b114b83f8850e5f2fbea0e797bd82ae", - "sha256:eb00ed941194665c332bf8e078baf037d6c35d7c4f3102ea2d4f16ca94a26dc8", - "sha256:eb6904c354526e758fda7167b33005998fb68c46fbc10e013ca97f21ca5c8887", - "sha256:eb8821e09e916165e160797a6c17edda0679379a4be5c716c260e836e122f54b", - "sha256:efcb3f6676480691518c177e3b465bcddf57cea040302f9f4e6e191af91174d4", - "sha256:f27273b60488abe721a075bcca6d7f3964f9f6f067c8c4c605743023d7d3944f", - "sha256:f30c3cb33b24454a82faecaf01b19c18562b1e89558fb6c56de4d9118a032fd5", - "sha256:fb69256e180cb6c8a894fee62b3afebae785babc1ee98b81cdf68bbca1987f33", - "sha256:fd1abc0d89e30cc4e02e4064dc67fcc51bd941eb395c502aac3ec19fab46b519", - "sha256:ff8fa367d09b717b2a17a052544193ad76cd49979c805768879cb63d9ca50561" - ], - "markers": "python_full_version >= '3.7.0'", - "version": "==3.3.2" - }, - "gitdb": { - "hashes": [ - "sha256:81a3407ddd2ee8df444cbacea00e2d038e40150acfa3001696fe0dcf1d3adfa4", - "sha256:bf5421126136d6d0af55bc1e7c1af1c397a34f5b7bd79e776cd3e89785c2b04b" - ], - "markers": "python_version >= '3.7'", - "version": "==4.0.11" - }, - "gitpython": { - "hashes": [ - "sha256:35f314a9f878467f5453cc1fee295c3e18e52f1b99f10f6cf5b1682e968a9e7c", - "sha256:eec7ec56b92aad751f9912a73404bc02ba212a23adb2c7098ee668417051a1ff" - ], - "index": "pypi", - "markers": "python_version >= '3.7'", - "version": "==3.1.43" - }, - "idna": { - "hashes": [ - "sha256:028ff3aadf0609c1fd278d8ea3089299412a7a8b9bd005dd08b9f8285bcb5cfc", - "sha256:82fee1fc78add43492d3a1898bfa6d8a904cc97d8427f683ed8e798d07761aa0" - ], - "markers": "python_version >= '3.5'", - "version": "==3.7" - }, - "mdutils": { - "hashes": [ - "sha256:647f3cf00df39fee6c57fa6738dc1160fce1788276b5530c87d43a70cdefdaf1" - ], - "index": "pypi", - "version": "==1.6.0" - }, - "prettytable": { - "hashes": [ - "sha256:6536efaf0757fdaa7d22e78b3aac3b69ea1b7200538c2c6995d649365bddab92", - "sha256:9665594d137fb08a1117518c25551e0ede1687197cf353a4fdc78d27e1073568" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==3.10.0" - }, - "requests": { - "hashes": [ - "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", - "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6" - ], - "index": "pypi", - "markers": "python_version >= '3.8'", - "version": "==2.32.3" - }, - "smmap": { - "hashes": [ - "sha256:dceeb6c0028fdb6734471eb07c0cd2aae706ccaecab45965ee83f11c8d3b1f62", - "sha256:e6d8668fa5f93e706934a62d7b4db19c8d9eb8cf2adbb75ef1b675aa332b69da" - ], - "markers": "python_version >= '3.7'", - "version": "==5.0.1" - }, - "urllib3": { - "hashes": [ - "sha256:a448b2f64d686155468037e1ace9f2d2199776e17f0a46610480d311f73e3472", - "sha256:dd505485549a7a552833da5e6063639d0d177c04f23bc3864e41e5dc5f612168" - ], - "markers": "python_version >= '3.8'", - "version": "==2.2.2" - }, - "wcwidth": { - "hashes": [ - "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", - "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5" - ], - "version": "==0.2.13" - } - }, - "develop": {} -} diff --git a/README.md b/README.md index 6e89d7a3..a3eeb109 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,265 @@ # Socket Security CLI -The Socket Security CLI was created to enable integrations with other tools like Github Actions, Gitlab, BitBucket, local use cases and more. The tool will get the head scan for the provided repo from Socket, create a new one, and then report any new alerts detected. If there are new alerts against the Socket security policy it'll exit with a non-Zero exit code. - -## Usage - -```` shell -socketcli [-h] [--api_token API_TOKEN] [--repo REPO] [--branch BRANCH] [--committer COMMITTER] [--pr_number PR_NUMBER] - [--commit_message COMMIT_MESSAGE] [--default_branch] [--target_path TARGET_PATH] [--scm {api,github,gitlab}] [--sbom-file SBOM_FILE] - [--commit-sha COMMIT_SHA] [--generate-license GENERATE_LICENSE] [-v] [--enable-debug] [--enable-json] [--disable-overview] - [--disable-security-issue] [--files FILES] [--ignore-commit-files] -```` - -If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_KEY` - - -| Parameter | Alternate Name | Required | Default | Description | -|:-------------------------|:---------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| -h | --help | False | | Show the CLI help message | -| --api_token | | False | | Provides the Socket API Token | -| --repo | | True | | The string name in a git approved name for repositories. | -| --branch | | False | | The string name in a git approved name for branches. | -| --committer | | False | | The string name of the person doing the commit or running the CLI. Can be specified multiple times to have more than one committer | -| --pr_number | | False | 0 | The integer for the PR or MR number | -| --commit_message | | False | | The string for a commit message if there is one | -| --default_branch | | False | False | If the flag is specified this will signal that this is the default branch. This needs to be enabled for a report to update Org Alerts and Org Dependencies | -| --target_path | | False | ./ | This is the path to where the manifest files are location. The tool will recursively search for all supported manifest files | -| --scm | | False | api | This is the mode that the tool is to run in. For local runs `api` would be the mode. Other options are `gitlab` and `github` | -| --generate-license | | False | False | If this flag is specified it will generate a json file with the license per package and license text in the current working directory | -| --version | -v | False | | Prints the version and exits | -| --enable-debug | | False | False | Enables debug messaging for the CLI | -| --sbom-file | | False | False | Creates a JSON file with all dependencies and alerts | -| --commit-sha | | False | | The commit hash for the commit | -| --generate-license | | False | False | If enabled with `--sbom-file` will include license details | -| --enable-json | | False | False | If enabled will change the console output format to JSON | -| --disable-overview | | False | False | If enabled will disable Dependency Overview comments | -| --disable-security-issue | | False | False | If enabled will disable Security Issue Comments | -| --files | | False | | If provided in the format of `["file1", "file2"]` will be used to determine if there have been supported file changes. This is used if it isn't a git repo and you would like to only run if it supported files have changed. | -| --ignore-commit-files | | False | False | If enabled then the CLI will ignore what files are changed in the commit and look for all manifest files | -| --disable-blocking | | False | False | Disables failing checks and will only exit with an exit code of 0 | +Socket Python CLI for Socket scans, diff reporting, reachability analysis, and SARIF/GitLab exports. + +Comprehensive docs are available in [`docs/`](https://github.com/SocketDev/socket-python-cli/tree/main/docs) for full flag reference, CI/CD-specific guidance, and contributor setup. + +## Quick start + +### 1) Install + +```bash +pip install socketsecurity +``` + +### 2) Authenticate + +```bash +export SOCKET_SECURITY_API_TOKEN="" +``` + +### 3) Run a basic scan + +```bash +socketcli --target-path . +``` + +## Common use cases + +This section covers the paved path/common workflows. +For advanced options and exhaustive details, see [`docs/cli-reference.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/cli-reference.md). +For CI/CD-specific guidance, see [`docs/ci-cd.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/ci-cd.md). + +### Basic policy scan (no SARIF) + +```bash +socketcli --target-path . +``` + +### GitLab dependency-scanning report + +```bash +socketcli --enable-gitlab-security --gitlab-security-file gl-dependency-scanning-report.json +``` + +## SARIF use cases + +### Full-scope reachable SARIF (grouped alerts) + +```bash +socketcli \ + --reach \ + --sarif-file results.sarif \ + --sarif-scope full \ + --sarif-grouping alert \ + --sarif-reachability reachable \ + --disable-blocking +``` + +### Diff-scope reachable SARIF (PR/CI gating) + +```bash +socketcli \ + --reach \ + --sarif-file results.sarif \ + --sarif-scope diff \ + --sarif-reachability reachable \ + --strict-blocking +``` + +### Full-scope SARIF (instance-level detail) + +```bash +socketcli \ + --reach \ + --sarif-file results.sarif \ + --sarif-scope full \ + --sarif-grouping instance \ + --sarif-reachability all \ + --disable-blocking +``` + +## Choose your mode + +| Use case | Recommended mode | Key flags | +|:--|:--|:--| +| Basic policy enforcement in CI | Diff-based policy check | `--strict-blocking` | +| Legal/compliance artifact generation | Legal preset | `--legal` | +| Reachable-focused SARIF for reporting | Full-scope grouped SARIF | `--reach --sarif-scope full --sarif-grouping alert --sarif-reachability reachable --sarif-file ` | +| Detailed reachability export for investigations | Full-scope instance SARIF | `--reach --sarif-scope full --sarif-grouping instance --sarif-reachability all --sarif-file ` | +| Net-new PR findings only | Diff-scope SARIF | `--reach --sarif-scope diff --sarif-reachability reachable --sarif-file ` | + +Dashboard parity note: +- Full-scope SARIF is the closest match for dashboard-style filtering. +- Exact result counts can still differ from the dashboard due to backend/API consolidation differences and grouping semantics. +- See [`docs/troubleshooting.md#dashboard-vs-cli-result-counts`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md#dashboard-vs-cli-result-counts). + +## Config files (`--config`) + +Use `--config ` with `.toml` or `.json` to avoid long command lines. + +Precedence order: + +`CLI flags` > `environment variables` > `config file` > `built-in defaults` + +Example: + +```toml +[socketcli] +repo = "example-repo" +reach = true +sarif_scope = "full" +sarif_grouping = "alert" +sarif_reachability = "reachable" +sarif_file = "reachable.sarif" +``` + +Equivalent JSON: + +```json +{ + "socketcli": { + "repo": "example-repo", + "reach": true, + "sarif_scope": "full", + "sarif_grouping": "alert", + "sarif_reachability": "reachable", + "sarif_file": "reachable.sarif" + } +} +``` + +Run: + +```bash +socketcli --config .socketcli.toml --target-path . +``` + +Legal/compliance preset example: + +```bash +socketcli --legal --target-path . +``` + +This preset enables license generation and writes default artifacts unless you override them: +- `socket-report.json` +- `socket-summary.txt` +- `socket-report-link.txt` +- `socket-sbom.json` +- `socket-license.json` + +FOSSA-compatibility shaped legal artifacts: + +```bash +socketcli --legal-format fossa --target-path . +``` + +This switches the JSON report and legal artifact payloads to FOSSA-style compatibility shapes: +- the analyze artifact becomes a `project` / `vulnerability` / `licensing` / `quality` report +- the SBOM artifact becomes a FOSSA-attribution-style payload with `copyrightsByLicense`, `deepDependencies`, `directDependencies`, `licenses`, and `project` keys + +When `--legal-format fossa` is used without explicit output paths, the defaults are closer to the FOSSA pipeline contract: +- `fossa-analyze.json` +- `fossa-test.txt` +- `fossa-link.txt` +- `fossa-sbom.json` + +Reference sample configs: + +TOML: +- [`examples/config/sarif-dashboard-parity.toml`](https://github.com/SocketDev/socket-python-cli/blob/main/examples/config/sarif-dashboard-parity.toml) +- [`examples/config/sarif-instance-detail.toml`](https://github.com/SocketDev/socket-python-cli/blob/main/examples/config/sarif-instance-detail.toml) +- [`examples/config/sarif-diff-ci-cd.toml`](https://github.com/SocketDev/socket-python-cli/blob/main/examples/config/sarif-diff-ci-cd.toml) + +JSON: +- [`examples/config/sarif-dashboard-parity.json`](https://github.com/SocketDev/socket-python-cli/blob/main/examples/config/sarif-dashboard-parity.json) +- [`examples/config/sarif-instance-detail.json`](https://github.com/SocketDev/socket-python-cli/blob/main/examples/config/sarif-instance-detail.json) +- [`examples/config/sarif-diff-ci-cd.json`](https://github.com/SocketDev/socket-python-cli/blob/main/examples/config/sarif-diff-ci-cd.json) + +## CI/CD examples + +Prebuilt workflow examples: + +- [GitHub Actions](https://github.com/SocketDev/socket-python-cli/blob/main/workflows/github-actions.yml) +- [Buildkite](https://github.com/SocketDev/socket-python-cli/blob/main/workflows/buildkite.yml) +- [GitLab CI](https://github.com/SocketDev/socket-python-cli/blob/main/workflows/gitlab-ci.yml) +- [Bitbucket Pipelines](https://github.com/SocketDev/socket-python-cli/blob/main/workflows/bitbucket-pipelines.yml) + +Minimal pattern: + +```yaml +- name: Run Socket CLI + run: socketcli --config .socketcli.toml --target-path . + env: + SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }} +``` + +## Exit codes + +| Code | Meaning | +|------|---------| +| `0` | Clean scan — no blocking issues (or `--disable-blocking` set) | +| `1` | Blocking security finding(s) detected | +| `2` | Scan interrupted (SIGINT / Ctrl+C) | +| `3` | Infrastructure or API error (timeout, network failure, unexpected error) | + +`--exit-code-on-api-error ` remaps the infrastructure-error code (`3`) to any +value — e.g. a Buildkite `soft_fail` code, or `0` to swallow infra errors. Exit +`3` is a Socket convention, not an industry standard. + +### How these options interact + +The two flags that affect exit codes can cancel each other out, so the order of +precedence matters: + +- **`--disable-blocking` wins over everything.** It forces exit `0` for *all* + outcomes — security findings *and* infrastructure errors. If you set it, + `--exit-code-on-api-error` has no effect (you'll always get `0`). +- **`--exit-code-on-api-error` only applies when `--disable-blocking` is *not* + set.** It changes the infra-error code (and the generic-error code); it never + touches the security-finding code (`1`). + +So for the common "don't let Socket outages block my pipeline, but still fail on +real findings" goal, use `--exit-code-on-api-error` **without** `--disable-blocking`: + +```yaml +# Buildkite: soft-fail only on infrastructure errors, still block on findings +steps: + - label: ":lock: Socket Security Scan" + command: "socketcli --exit-code-on-api-error 100 ..." # NOT --disable-blocking + soft_fail: + - exit_status: 100 +``` + +Combining `--disable-blocking` with `--exit-code-on-api-error 100` would make the +scan exit `0` on *both* findings and outages — the `soft_fail: 100` rule would +never match, and real findings would stop blocking. That's usually not what you +want. + +## Common gotchas + +See [`docs/troubleshooting.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md#common-gotchas). + +## Quick verification checks + +After generating SARIF files, validate shape/count quickly: + +```bash +jq '.runs[0].results | length' results.sarif +jq -r '.runs[0].results[]?.properties.reachability' results.sarif | sort -u +``` + +For side-by-side comparisons: + +```bash +jq '.runs[0].results | length' sarif-dashboard-parity-reachable.sarif +jq '.runs[0].results | length' sarif-full-instance-all.sarif +jq '.runs[0].results | length' sarif-diff-reachable.sarif +``` + +## Documentation reference + +- Full CLI reference: [`docs/cli-reference.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/cli-reference.md) +- CI/CD guide: [`docs/ci-cd.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/ci-cd.md) +- Troubleshooting guide: [`docs/troubleshooting.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/troubleshooting.md) +- Development guide: [`docs/development.md`](https://github.com/SocketDev/socket-python-cli/blob/main/docs/development.md) diff --git a/docs/ci-cd.md b/docs/ci-cd.md new file mode 100644 index 00000000..edf3a4e0 --- /dev/null +++ b/docs/ci-cd.md @@ -0,0 +1,119 @@ +# CI/CD guide + +Use this guide for pipeline-focused CLI usage across platforms. + +## Recommended patterns + +### Dashboard-style reachable SARIF + +```bash +socketcli \ + --reach \ + --sarif-file results.sarif \ + --sarif-scope full \ + --sarif-grouping alert \ + --sarif-reachability reachable \ + --disable-blocking +``` + +### Diff-based gating on new reachable findings + +```bash +socketcli \ + --reach \ + --sarif-file results.sarif \ + --sarif-scope diff \ + --sarif-reachability reachable \ + --strict-blocking +``` + +## Config file usage in CI + +Use `--config .socketcli.toml` or `--config .socketcli.json` to keep pipeline commands small. + +Precedence order: + +`CLI flags` > `environment variables` > `config file` > `built-in defaults` + +Example: + +```toml +[socketcli] +reach = true +sarif_scope = "full" +sarif_grouping = "alert" +sarif_reachability = "reachable" +sarif_file = "results.sarif" +``` + +Equivalent JSON: + +```json +{ + "socketcli": { + "reach": true, + "sarif_scope": "full", + "sarif_grouping": "alert", + "sarif_reachability": "reachable", + "sarif_file": "results.sarif" + } +} +``` + +## Platform examples + +### GitHub Actions + +```yaml +- name: Run Socket CLI + run: socketcli --config .socketcli.toml --target-path . + env: + SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }} +``` + +### Buildkite + +```yaml +steps: + - label: "Socket scan" + command: "socketcli --config .socketcli.toml --target-path ." + env: + SOCKET_SECURITY_API_TOKEN: "${SOCKET_SECURITY_API_TOKEN}" +``` + +### GitLab CI + +```yaml +socket_scan: + script: + - socketcli --config .socketcli.toml --target-path . + variables: + SOCKET_SECURITY_API_TOKEN: $SOCKET_SECURITY_API_TOKEN +``` + +### Bitbucket Pipelines + +```yaml +pipelines: + default: + - step: + script: + - socketcli --config .socketcli.toml --target-path . +``` + +## Workflow templates + +Prebuilt examples in this repo: + +- [`../workflows/github-actions.yml`](../workflows/github-actions.yml) +- [`../workflows/buildkite.yml`](../workflows/buildkite.yml) +- [`../workflows/gitlab-ci.yml`](../workflows/gitlab-ci.yml) +- [`../workflows/bitbucket-pipelines.yml`](../workflows/bitbucket-pipelines.yml) + +## CI gotchas + +- `--strict-blocking` enables strict diff behavior (`new + unchanged`) for blocking evaluation and diff-based output selection. +- `--sarif-scope full` requires `--reach`. +- `--sarif-grouping alert` currently applies to `--sarif-scope full`. +- Diff-based SARIF can validly be empty when there are no matching net-new alerts. +- Keep API tokens in secret stores (`SOCKET_SECURITY_API_TOKEN`), not in config files. diff --git a/docs/cli-reference.md b/docs/cli-reference.md new file mode 100644 index 00000000..7524495e --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,784 @@ +# Socket Security CLI: Full Reference + +> This is the comprehensive reference document. +> For first-time setup and common workflows, start with [`../README.md`](../README.md). + +The Socket Security CLI was created to enable integrations with other tools like GitHub Actions, Buildkite, GitLab, Bitbucket, local use cases and more. The tool will get the head scan for the provided repo from Socket, create a new one, and then report any new alerts detected. If there are new alerts with blocking actions it'll exit with a non-Zero exit code. + +## Quick Start + +The CLI now features automatic detection of git repository information, making it much simpler to use in CI/CD environments. Most parameters are now optional and will be detected automatically from your git repository. + +### Minimal Usage Examples + +**GitHub Actions:** +```bash +socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number $PR_NUMBER +``` + +**Buildkite:** +```bash +socketcli --target-path ${BUILDKITE_BUILD_CHECKOUT_PATH:-.} --scm api --pr-number ${BUILDKITE_PULL_REQUEST:-0} +``` + +**GitLab CI:** +```bash +socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-0} +``` + +**Bitbucket Pipelines:** +```bash +socketcli --target-path $BITBUCKET_CLONE_DIR --scm api --pr-number ${BITBUCKET_PR_ID:-0} +``` + +**Local Development:** +```bash +socketcli --target-path ./my-project +``` + +The CLI will automatically detect: +- Repository name from git remote +- Branch name from git +- Commit SHA and message from git +- Committer information from git +- Default branch status from git and CI environment +- Changed files from git commit history + +## CI/CD Workflow Examples + +CI/CD-focused usage and platform examples are documented in [`ci-cd.md`](ci-cd.md). +Pre-configured workflow files are in [`../workflows/`](../workflows/). + +## Monorepo Workspace Support + +> **Note:** If you're looking to associate a scan with a named Socket workspace (e.g. because your repo is identified as `org/repo`), see the [`--workspace` flag](#repository) instead. The `--workspace-name` flag described in this section is an unrelated monorepo feature. + +The Socket CLI supports scanning specific workspaces within monorepo structures while preserving git context from the repository root. This is useful for organizations that maintain multiple applications or services in a single repository. + +### Key Features + +- **Multiple Sub-paths**: Specify multiple `--sub-path` options to scan different directories within your monorepo +- **Combined Workspace**: All sub-paths are scanned together as a single workspace in Socket +- **Git Context Preserved**: Repository metadata (commits, branches, etc.) comes from the main target-path +- **Workspace Naming**: Use `--workspace-name` to differentiate scans from different parts of your monorepo + +### Usage Examples + +**Scan multiple frontend and backend workspaces:** +```bash +socketcli --target-path /path/to/monorepo \ + --sub-path frontend \ + --sub-path backend \ + --sub-path services/api \ + --workspace-name main-app +``` + +**GitHub Actions for monorepo workspace:** +```bash +socketcli --target-path $GITHUB_WORKSPACE \ + --sub-path packages/web \ + --sub-path packages/mobile \ + --workspace-name mobile-web \ + --scm github \ + --pr-number $PR_NUMBER +``` + +This will: +- Scan manifest files in `./packages/web/` and `./packages/mobile/` +- Combine them into a single workspace scan +- Create a repository in Socket named like `my-repo-mobile-web` +- Preserve git context (commits, branch info) from the repository root + +**Generate GitLab Security Dashboard report:** +```bash +socketcli --enable-gitlab-security \ + --repo owner/repo \ + --target-path . +``` + +This will: +- Scan all manifest files in the current directory +- Generate a GitLab-compatible Dependency Scanning report +- Save to `gl-dependency-scanning-report.json` +- Include all actionable security alerts (error/warn level) + +**Save SARIF report to file (e.g. for GitHub Code Scanning, SonarQube, or VS Code):** +```bash +socketcli --sarif-file results.sarif \ + --repo owner/repo \ + --target-path . +``` + +**Multiple output formats:** +```bash +socketcli --enable-json \ + --sarif-file results.sarif \ + --enable-gitlab-security \ + --repo owner/repo +``` + +This will simultaneously generate: +- JSON output to console +- SARIF report to `results.sarif` (and stdout) +- GitLab Security Dashboard report to `gl-dependency-scanning-report.json` + +> **Note:** `--enable-sarif` prints SARIF to stdout only. Use `--sarif-file ` to save to a file (this also implies `--enable-sarif`). Use `--sarif-reachability` (requires `--reach` when not `all`) to filter by reachability state. Use `--sarif-scope diff|full` to choose between diff alerts (default) and full reachability facts scope. These flags are independent from `--enable-gitlab-security`, which produces a separate GitLab-specific Dependency Scanning report. +> +> In diff scope, `--strict-blocking` expands selection to include `new + unchanged` diff alerts for evaluation/output paths. +> +> SARIF scope examples: +> - Diff-only reachable findings: `socketcli --reach --sarif-file out.sarif --sarif-scope diff --sarif-reachability reachable` +> - Full reachability scope, reachable only: `socketcli --reach --sarif-file out.sarif --sarif-scope full --sarif-reachability reachable` +> - Full reachability scope, all reachability states: `socketcli --reach --sarif-file out.sarif --sarif-scope full` +> - Dashboard-style grouping (one result per alert key): `socketcli --reach --sarif-file out.sarif --sarif-scope full --sarif-grouping alert --sarif-reachability reachable` +> +> In `--sarif-scope full` mode with `--sarif-file`, SARIF JSON is written to file and stdout JSON is suppressed to avoid oversized CI logs. + +### Requirements + +- Both `--sub-path` and `--workspace-name` must be specified together +- `--sub-path` can be used multiple times to include multiple directories +- All specified sub-paths must exist within the target-path + +## Usage + +```` shell +socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] + [--config ] + [--owner OWNER] [--pr-number PR_NUMBER] [--commit-message COMMIT_MESSAGE] [--commit-sha COMMIT_SHA] [--committers [COMMITTERS ...]] + [--target-path TARGET_PATH] [--sbom-file SBOM_FILE] [--license-file-name LICENSE_FILE_NAME] [--save-submitted-files-list SAVE_SUBMITTED_FILES_LIST] + [--save-manifest-tar SAVE_MANIFEST_TAR] [--files FILES] [--sub-path SUB_PATH] [--workspace-name WORKSPACE_NAME] + [--excluded-ecosystems EXCLUDED_ECOSYSTEMS] [--exclude-paths EXCLUDE_PATHS] [--include-dirs INCLUDE_DIRS] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] + [--enable-json] [--enable-sarif] [--sarif-file ] [--sarif-scope {diff,full}] [--sarif-grouping {instance,alert}] [--sarif-reachability {all,reachable,potentially,reachable-or-potentially}] [--enable-gitlab-security] [--gitlab-security-file ] + [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] + [--ignore-commit-files] [--disable-blocking] [--disable-ignore] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] + [--reach] [--reach-version REACH_VERSION] [--reach-analysis-timeout REACH_ANALYSIS_TIMEOUT] + [--reach-analysis-memory-limit REACH_ANALYSIS_MEMORY_LIMIT] [--reach-concurrency REACH_CONCURRENCY] [--reach-ecosystems REACH_ECOSYSTEMS] + [--reach-min-severity ] [--reach-skip-cache] [--reach-disable-analytics] [--reach-enable-analysis-splitting] [--reach-detailed-analysis-log-file] + [--reach-lazy-mode] [--reach-use-only-pregenerated-sboms] [--reach-debug] [--reach-disable-external-tool-checks] + [--reach-output-file REACH_OUTPUT_FILE] [--only-facts-file] [--version] +```` + +If you don't want to provide the Socket API Token every time then you can use the environment variable `SOCKET_SECURITY_API_TOKEN` + +### Parameters + +#### Authentication +| Parameter | Required | Default | Description | +|:------------|:---------|:--------|:----------------------------------------------------------------------------------| +| `--api-token` | False | | Socket Security API token (can also be set via SOCKET_SECURITY_API_TOKEN env var) | + +#### Repository +| Parameter | Required | Default | Description | +|:-----------------|:---------|:--------|:------------------------------------------------------------------------------------------------------------------| +| `--repo` | False | *auto* | Repository name in owner/repo format (auto-detected from git remote) | +| `--workspace` | False | | The Socket workspace to associate the scan with (e.g. `my-org` in `my-org/my-repo`). See note below. | +| `--repo-is-public` | False | False | If set, flags a new repository creation as public. Defaults to false. | +| `--integration` | False | api | Integration type (api, github, gitlab, azure, bitbucket) | +| `--owner` | False | | Name of the integration owner, defaults to the socket organization slug | +| `--branch` | False | *auto* | Branch name (auto-detected from git) | +| `--committers` | False | *auto* | Committer(s) to filter by (auto-detected from git commit) | + +> **`--workspace` vs `--workspace-name`** — these are two distinct flags for different purposes: +> +> - **`--workspace `** maps to the Socket API's `workspace` query parameter on `CreateOrgFullScan`. Use it when your repository belongs to a named Socket workspace (e.g. an org with multiple workspace groups). Example: `--repo my-repo --workspace my-org`. Without this flag, scans are created without workspace context and may not appear under the correct workspace in the Socket dashboard. +> +> - **`--workspace-name `** is a monorepo feature. It appends a suffix to the repository slug to create a unique name in Socket (e.g. `my-repo-frontend`). It must always be paired with `--sub-path` and has nothing to do with the API `workspace` field. See [Monorepo Workspace Support](#monorepo-workspace-support) below. + +#### Pull Request and Commit +| Parameter | Required | Default | Description | +|:-----------------|:---------|:--------|:-----------------------------------------------| +| `--pr-number` | False | "0" | Pull request number | +| `--commit-message` | False | *auto* | Commit message (auto-detected from git) | +| `--commit-sha` | False | *auto* | Commit SHA (auto-detected from git) | + +#### Path and File +| Parameter | Required | Default | Description | +|:----------------------------|:---------|:----------------------|:---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `--target-path` | False | ./ | Target path for analysis | +| `--sbom-file` | False | | SBOM file path | +| `--license-file-name` | False | `license_output.json` | Name of the file to save the license details to if enabled | +| `--save-submitted-files-list` | False | | Save list of submitted file names to JSON file for debugging purposes | +| `--save-manifest-tar` | False | | Save all manifest files to a compressed tar.gz archive with original directory structure | +| `--files` | False | *auto* | Files to analyze (JSON array string). Auto-detected from git commit changes when not specified | +| `--sub-path` | False | | Sub-path within target-path for manifest file scanning (can be specified multiple times). All sub-paths are combined into a single workspace scan while preserving git context from target-path. Must be used with `--workspace-name` | +| `--workspace-name` | False | | Workspace name suffix to append to repository name (repo-name-workspace_name). Must be used with `--sub-path` | +| `--excluded-ecosystems` | False | [] | List of ecosystems to exclude from analysis (JSON array string). You can get supported files from the [Supported Files API](https://docs.socket.dev/reference/getsupportedfiles) | +| `--exclude-paths` | False | | Comma-separated paths/globs to exclude from **both** manifest discovery (every scan) **and** reachability analysis (e.g. `tests/**,packages/legacy,*.spec.ts`). Patterns are scan-root-relative, case-sensitive globs where `*` does not cross `/` and `**` does. Supersedes `--reach-exclude-paths`. | +| `--include-dirs` | False | | Comma-separated directory **names** that are excluded from manifest discovery by default but should be scanned (e.g. `build,dist`). Names are matched against any path segment, mirroring the default exclude list (`node_modules`, `bower_components`, `jspm_packages`, `__pycache__`, `.venv`, `venv`, `build`, `dist`, `.tox`, `.mypy_cache`, `.pytest_cache`, `*.egg-info`, `vendor`). Use this when manifest files live under a normally-ignored folder, e.g. `build/requirements.txt`. | + +#### Branch and Scan Configuration +| Parameter | Required | Default | Description | +|:-------------------------|:---------|:--------|:------------------------------------------------------------------------------------------------------| +| `--default-branch` | False | *auto* | Make this branch the default branch (auto-detected from git and CI environment when not specified) | +| `--pending-head` | False | *auto* | If true, the new scan will be set as the branch's head scan (automatically synced with default-branch) | +| `--include-module-folders` | False | False | If enabled, re-includes the JS/TS module folders (`node_modules`, `bower_components`, `jspm_packages`) in manifest discovery. For other excluded directories, use `--include-dirs`. | + +#### Output Configuration +| Parameter | Required | Default | Description | +|:--------------------------|:---------|:--------|:----------------------------------------------------------------------------------| +| `--generate-license` | False | False | Generate license information | +| `--enable-debug` | False | False | Enable debug logging | +| `--enable-json` | False | False | Output in JSON format | +| `--enable-sarif` | False | False | Enable SARIF output of results instead of table or JSON format (prints to stdout) | +| `--sarif-file` | False | | Output file path for SARIF report (implies --enable-sarif). Use this to save SARIF output to a file for upload to GitHub Code Scanning, SonarQube, VS Code, or other SARIF-compatible tools | +| `--sarif-scope` | False | diff | SARIF source scope: `diff` for net-new diff alerts, or `full` for full reachability facts scope (requires --reach for full) | +| `--sarif-grouping` | False | instance| SARIF grouping mode: `instance` (one entry per package/version/advisory instance) or `alert` (grouped alert-style output, full scope only) | +| `--sarif-reachability` | False | all | SARIF reachability selector: `all`, `reachable`, `potentially`, or `reachable-or-potentially` (requires --reach when not `all`) | +| `--enable-gitlab-security` | False | False | Enable GitLab Security Dashboard output format (Dependency Scanning report) | +| `--gitlab-security-file` | False | gl-dependency-scanning-report.json | Output file path for GitLab Security report | +| `--disable-overview` | False | False | Disable overview output | +| `--exclude-license-details` | False | False | Exclude license details from the diff report (boosts performance for large repos) | +| `--version` | False | False | Show program's version number and exit | + +#### Security Configuration +| Parameter | Required | Default | Description | +|:-------------------------|:---------|:--------|:------------------------------| +| `--allow-unverified` | False | False | Allow unverified packages | +| `--disable-security-issue` | False | False | Disable security issue checks | + +#### Reachability Analysis +| Parameter | Required | Default | Description | +|:---------------------------------|:---------|:--------|:---------------------------------------------------------------------------------------------------------------------------| +| `--reach` | False | False | Enable reachability analysis to identify which vulnerable functions are actually called by your code. Creates a tier-1 full-application reachability scan (`scan_type=socket_tier1`). | +| `--reach-version` | False | 15.3.24 | Version of @coana-tech/cli to use. Defaults to the pinned version that ships with this CLI release, so the engine only changes when you upgrade the Socket CLI. Pass `latest` to always use the newest published version (opt-in auto-update), or an explicit version (e.g. `1.2.3`) to pin it. | +| `--reach-analysis-timeout` | False | 600 | Timeout in seconds for the reachability analysis. Omitted by default, so coana applies its own default. Alias: `--reach-timeout` | +| `--reach-analysis-memory-limit` | False | 8192 | Memory limit in MB for the reachability analysis. Omitted by default, so coana applies its own default. Alias: `--reach-memory-limit` | +| `--reach-concurrency` | False | 1 | Control parallel analysis execution (must be >= 1). Omitted by default, so coana applies its own default. | +| `--reach-additional-params` | False | | Pass custom parameters to the coana CLI tool | +| `--reach-ecosystems` | False | | Comma-separated list of ecosystems to analyze (e.g., "npm,pypi"). If not specified, all supported ecosystems are analyzed | +| `--reach-min-severity` | False | info | Minimum severity of vulnerabilities to analyze (info, low, moderate, high, critical). Omitted by default, so coana analyzes all severities — equivalent to `info`, the lowest. | +| `--reach-skip-cache` | False | False | Skip cache and force fresh reachability analysis | +| `--reach-disable-analytics` | False | False | Disable analytics collection during reachability analysis | +| `--reach-enable-analysis-splitting` | False | False | Enable analysis splitting/bucketing (a legacy performance feature). Splitting is disabled by default. | +| `--reach-detailed-analysis-log-file` | False | False | Write a detailed analysis log file; its path is printed to stdout | +| `--reach-lazy-mode` | False | False | Enable lazy mode (experimental performance feature) | +| `--reach-use-only-pregenerated-sboms` | False | False | Build the scan only from pre-generated CycloneDX (CDX) and SPDX files in your project (requires --reach) | +| `--reach-debug` | False | False | Enable coana debug output (`--debug`) for the analysis, independent of the global `--enable-debug` | +| `--reach-disable-external-tool-checks` | False | False | Disable coana's external tool availability checks (passes `--disable-external-tool-checks`) | +| `--reach-output-file` | False | .socket.facts.json | Path where reachability analysis results should be saved | +| `--reach-exclude-paths` | False | | **[DEPRECATED — use `--exclude-paths`]** Comma-separated paths to exclude from reachability analysis. Still honored (unioned with `--exclude-paths`) but will be hidden in a future release | +| `--only-facts-file` | False | False | Submit only the .socket.facts.json file when creating the full scan (requires --reach) | + +**Reachability Analysis Requirements:** + +The Python CLI verifies the following **up front** (before invoking the analysis engine) and exits with code **3** if any are unmet: +- `npm` - Required (verified up front; ships alongside `npx`) +- `npx` - Required to fetch (on first use) and run `@coana-tech/cli` (the analysis engine) +- `node` - Required to run the engine (used directly by the `npm install` fallback) +- `uv` - Required by the analysis engine +- An **Enterprise** Socket organization plan (any `enterprise*` plan, including Enterprise trials) + +Separately, the analysis engine (coana) needs the **per-ecosystem build toolchain** for whatever languages your project uses — e.g. a compatible Python interpreter (3.11+, or PyPy) for Python, a JDK for Java/Kotlin/Scala, .NET 6+ for C#, the matching Go toolchain for Go, etc. These are validated by the engine **at analysis time** (the CLI does not pre-check them) and that validation can be skipped with `--reach-disable-external-tool-checks`. + +## Config file support + +Use `--config ` to load defaults from a `.toml` or `.json` file. +CLI arguments always take precedence over config file values. + +Example `socketcli.toml`: + +```toml +[socketcli] +repo = "example-repo" +reach = true +sarif_scope = "full" +sarif_grouping = "alert" +sarif_reachability = "reachable" +sarif_file = "reachable.sarif" +``` + +Equivalent `socketcli.json`: + +```json +{ + "socketcli": { + "repo": "example-repo", + "reach": true, + "sarif_scope": "full", + "sarif_grouping": "alert", + "sarif_reachability": "reachable", + "sarif_file": "reachable.sarif" + } +} +``` + +Sample config files: +- [`../examples/config/sarif-dashboard-parity.toml`](../examples/config/sarif-dashboard-parity.toml) +- [`../examples/config/sarif-dashboard-parity.json`](../examples/config/sarif-dashboard-parity.json) +- [`../examples/config/sarif-instance-detail.toml`](../examples/config/sarif-instance-detail.toml) +- [`../examples/config/sarif-instance-detail.json`](../examples/config/sarif-instance-detail.json) +- [`../examples/config/sarif-diff-ci-cd.toml`](../examples/config/sarif-diff-ci-cd.toml) +- [`../examples/config/sarif-diff-ci-cd.json`](../examples/config/sarif-diff-ci-cd.json) + +### CI/CD usage tips + +For CI-specific examples and guidance, see [`ci-cd.md`](ci-cd.md). + +The CLI runs a pinned `@coana-tech/cli` version via `npx --yes --force` (the same flags the Socket Node CLI passes for coana); it does **not** auto-update the engine or install it globally. `--yes` skips npx's interactive install prompt so non-interactive/CI runs don't hang. If the `npx` launcher is unavailable or fails before the engine starts, the CLI falls back to `npm install`-ing the pinned version into a temp directory and running it via `node`. Pass `--reach-version latest` to opt into the newest published version. Use `--reach` to enable reachability analysis during a full scan, or add `--only-facts-file` (with `--reach`) to submit only the reachability facts file (`.socket.facts.json`) when creating the full scan. + +The launcher fallback can be tuned via environment variables: +- `SOCKET_CLI_COANA_FORCE_NPM_INSTALL` — skip `npx` entirely and always use the `npm install` + `node` path (useful where `npx` is known-broken). +- `SOCKET_CLI_COANA_DISABLE_NPM_FALLBACK` — never fall back; surface the `npx` failure directly. + +#### Advanced Configuration +| Parameter | Required | Default | Description | +|:-------------------------|:---------|:--------|:----------------------------------------------------------------------| +| `--ignore-commit-files` | False | False | Ignore commit files | +| `--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) | +| `--scm` | False | api | Source control management type | +| `--timeout` | False | | Timeout in seconds for API requests | + +#### Plugins + +The Python CLI currently supports the following plugins: + +- Jira +- Slack + +##### Jira + +| Environment Variable | Required | Default | Description | +|:------------------------|:---------|:--------|:-----------------------------------| +| `SOCKET_JIRA_ENABLED` | False | false | Enables/Disables the Jira Plugin | +| `SOCKET_JIRA_CONFIG_JSON` | True | None | Required if the Plugin is enabled. | + +Example `SOCKET_JIRA_CONFIG_JSON` value + +````json +{"url": "https://REPLACE_ME.atlassian.net", "email": "example@example.com", "api_token": "REPLACE_ME", "project": "REPLACE_ME" } +```` + +##### Slack + +| Environment Variable | Required | Default | Description | +|:-------------------------|:---------|:--------|:-----------------------------------| +| `SOCKET_SLACK_CONFIG_JSON` | False | None | Slack configuration (enables plugin when set). Supports webhook or bot mode. Alternatively, use `--slack-webhook` for simple webhook mode. | +| `SOCKET_SLACK_BOT_TOKEN` | False | None | Slack Bot User OAuth Token (starts with `xoxb-`). Required when using bot mode. | + +**Slack supports two modes:** + +1. **Webhook Mode** (default): Posts to incoming webhooks +2. **Bot Mode**: Posts via Slack API with bot token authentication + +###### Webhook Mode Examples + +Simple webhook: + +````json +{"url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL"} +```` + +Multiple webhooks with advanced filtering: + +````json +{ + "mode": "webhook", + "url": [ + { + "name": "prod_alerts", + "url": "https://hooks.slack.com/services/YOUR/WEBHOOK/URL" + }, + { + "name": "critical_only", + "url": "https://hooks.slack.com/services/YOUR/OTHER/WEBHOOK/URL" + } + ], + "url_configs": { + "prod_alerts": { + "reachability_alerts_only": true, + "severities": ["high", "critical"] + }, + "critical_only": { + "severities": ["critical"] + } + } +} +```` + +###### Bot Mode Examples + +**Setting up a Slack Bot:** +1. Go to https://api.slack.com/apps and create a new app +2. Under "OAuth & Permissions", add the `chat:write` bot scope +3. Install the app to your workspace and copy the "Bot User OAuth Token" +4. Invite the bot to your channels: `/invite @YourBotName` + +Basic bot configuration: + +````json +{ + "mode": "bot", + "bot_configs": [ + { + "name": "security_alerts", + "channels": ["security-alerts", "dev-team"] + } + ] +} +```` + +Bot with filtering (reachability-only alerts): + +````json +{ + "mode": "bot", + "bot_configs": [ + { + "name": "critical_reachable", + "channels": ["security-critical"], + "severities": ["critical", "high"], + "reachability_alerts_only": true + }, + { + "name": "all_alerts", + "channels": ["security-all"], + "repos": ["myorg/backend", "myorg/frontend"] + } + ] +} +```` + +Set the bot token: +```bash +export SOCKET_SLACK_BOT_TOKEN="xoxb-your-bot-token-here" +``` + +**Configuration Options:** + +Webhook mode (`url_configs`): +- `reachability_alerts_only` (boolean, default: false): When `--reach` is enabled, only send reachable vulnerabilities from the selected diff alert set (uses reachability facts when available; otherwise falls back to blocking-status behavior) +- `repos` (array): Only send alerts for specific repositories (e.g., `["owner/repo1", "owner/repo2"]`) +- `alert_types` (array): Only send specific alert types (e.g., `["malware", "typosquat"]`) +- `severities` (array): Only send alerts with specific severities (e.g., `["high", "critical"]`) + +Bot mode (`bot_configs` array items): +- `name` (string, required): Friendly name for this configuration +- `channels` (array, required): Channel names (without #) where alerts will be posted +- `severities` (array, optional): Only send alerts with specific severities (e.g., `["high", "critical"]`) +- `repos` (array, optional): Only send alerts for specific repositories +- `alert_types` (array, optional): Only send specific alert types +- `reachability_alerts_only` (boolean, default: false): Only send reachable vulnerabilities when using `--reach` + +## Strict Blocking Mode + +The `--strict-blocking` flag enforces a zero-tolerance security policy by failing builds when **ANY** security violations with blocking severity exist, not just new ones introduced in the current changes. + +### Standard vs Strict Blocking Behavior + +**Standard Behavior (Default)**: +- ✅ Passes if no NEW violations are introduced +- ❌ Fails only on NEW violations from your changes +- 🟡 Existing violations are ignored + +**Strict Blocking Behavior (`--strict-blocking`)**: +- ✅ Passes only if NO violations exist (new or existing) +- ❌ Fails on ANY violation (new OR existing) +- 🔴 Enforces zero-tolerance policy + +### Usage Examples + +**Basic strict blocking:** +```bash +socketcli --target-path ./my-project --strict-blocking +``` + +**In GitHub Actions:** +```bash +socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number $PR_NUMBER --strict-blocking +``` + +**In Buildkite:** +```bash +socketcli --target-path ${BUILDKITE_BUILD_CHECKOUT_PATH:-.} --scm api --pr-number ${BUILDKITE_PULL_REQUEST:-0} --strict-blocking +``` + +**In GitLab CI:** +```bash +socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-0} --strict-blocking +``` + +### Output Differences + +**Standard scan output:** +``` +Security issues detected by Socket Security: + - NEW blocking issues: 2 + - NEW warning issues: 1 +``` + +**Strict blocking scan output:** +``` +Security issues detected by Socket Security: + - NEW blocking issues: 2 + - NEW warning issues: 1 + - EXISTING blocking issues: 5 (causing failure due to --strict-blocking) + - EXISTING warning issues: 3 +``` + +### Use Cases + +1. **Zero-Tolerance Security Policy**: Enforce that no security violations exist in your codebase at any time +2. **Gradual Security Improvement**: Use alongside standard scans to monitor existing violations while blocking new ones +3. **Protected Branch Enforcement**: Require all violations to be resolved before merging to main/production +4. **Security Audits**: Scheduled scans that fail if any violations accumulate + +### Important Notes + +- **Diff Mode Only**: The flag only works in diff mode (with SCM integration). In API mode, a warning is logged. +- **Error-Level Only**: Only fails on `error=True` alerts (blocking severity), not warnings. +- **Priority**: `--disable-blocking` takes precedence - if both flags are set, the build will always pass. +- **First Scan**: On the very first scan of a repository, there are no "existing" violations, so behavior is identical to standard mode. + +### Flag Combinations + +**Strict blocking with debugging:** +```bash +socketcli --strict-blocking --enable-debug +``` + +**Strict blocking with JSON output:** +```bash +socketcli --strict-blocking --enable-json > security-report.json +``` + +**Override for testing** (passes even with violations): +```bash +socketcli --strict-blocking --disable-blocking +``` + +### Migration Strategy + +**Phase 1: Assessment** - Add strict scan with `allow_failure: true` in CI +**Phase 2: Remediation** - Fix or triage all violations +**Phase 3: Enforcement** - Set `allow_failure: false` to block merges + +For CI/CD-oriented strict-blocking examples, see [`ci-cd.md`](ci-cd.md). + +## Automatic Git Detection + +The CLI now automatically detects repository information from your git environment, significantly simplifying usage in CI/CD pipelines: + +### Auto-Detected Information + +- **Repository name**: Extracted from git remote origin URL +- **Branch name**: Current git branch or CI environment variables +- **Commit SHA**: Latest commit hash or CI-provided commit SHA +- **Commit message**: Latest commit message +- **Committer information**: Git commit author details +- **Default branch status**: Determined from git repository and CI environment +- **Changed files**: Files modified in the current commit (for differential scanning) +> **Note on merge commits**: +> Standard merges (two parents) are supported. +> For *octopus merges* (three or more parents), Git only reports changes relative to the first parent. This can lead to incomplete or empty file lists if changes only exist relative to other parents. In these cases, differential scanning may be skipped. To ensure coverage, use `--ignore-commit-files` to force a full scan or specify files explicitly with `--files`. +### Default Branch Detection + +The CLI uses intelligent default branch detection with the following priority: + +1. **Explicit `--default-branch` flag**: Takes highest priority when specified +2. **CI environment detection**: Uses CI platform variables (GitHub Actions, GitLab CI, and Bitbucket Pipelines) +3. **Git repository analysis**: Compares current branch with repository's default branch +4. **Fallback**: Defaults to `false` if none of the above methods succeed + +Both `--default-branch` and `--pending-head` parameters are automatically synchronized to ensure consistent behavior. + +## GitLab Token Configuration + +GitLab token/auth behavior and CI examples are documented in [`ci-cd.md`](ci-cd.md). + +## File Selection Behavior + +The CLI determines which files to scan based on the following logic: + +1. **Git Commit Files (Default)**: The CLI automatically checks files changed in the current git commit. If any of these files match supported manifest patterns (like package.json, requirements.txt, etc.), a scan is triggered. + +2. **`--files` Parameter Override**: When specified, this parameter takes precedence over git commit detection. It accepts a JSON array of file paths to check for manifest files. + +3. **`--ignore-commit-files` Flag**: When set, git commit files are ignored completely, and the CLI will scan all manifest files in the target directory regardless of what changed. + +4. **Automatic Fallback**: If no manifest files are found in git commit changes and no `--files` are specified, the CLI automatically switches to "API mode" and performs a full repository scan. + +> **Important**: The CLI doesn't scan only the specified files - it uses them to determine whether a scan should be performed and what type of scan to run. When triggered, it searches the entire `--target-path` for all supported manifest files. + +### Scanning Modes + +- **Differential Mode**: When manifest files are detected in changes, performs a diff scan with PR/MR comment integration +- **API Mode**: When no manifest files are in changes, creates a full scan report without PR comments but still scans the entire repository +- **Force Mode**: With `--ignore-commit-files`, always performs a full scan regardless of changes +- **Forced Diff Mode**: With `--enable-diff`, forces differential mode even when using `--integration api` (without SCM integration) + +### Examples + +- **Commit with manifest file**: If your commit includes changes to `package.json`, a differential scan will be triggered automatically with PR comment integration. +- **Commit without manifest files**: If your commit only changes non-manifest files (like `.github/workflows/socket.yaml`), the CLI automatically switches to API mode and performs a full repository scan. +- **Using `--files`**: If you specify `--files '["package.json"]'`, the CLI will check if this file exists and is a manifest file before determining scan type. +- **Using `--ignore-commit-files`**: This forces a full scan of all manifest files in the target path, regardless of what's in your commit. +- **Using `--enable-diff`**: Forces diff mode without SCM integration - useful when you want differential scanning but are using `--integration api`. For example: `socketcli --integration api --enable-diff --target-path /path/to/repo` +- **Auto-detection**: Most CI/CD scenarios now work with just `socketcli --target-path /path/to/repo --scm github --pr-number $PR_NUM` + +## Troubleshooting + +Troubleshooting and debugging workflows are documented in [`troubleshooting.md`](troubleshooting.md). + +## GitLab Security Dashboard Integration + +Socket CLI can generate reports compatible with GitLab's Security Dashboard, allowing vulnerability information to be displayed directly in merge requests and security dashboards. This feature complements the existing [Socket GitLab integration](https://docs.socket.dev/docs/gitlab) by providing standardized dependency scanning reports. + +### Generating GitLab Security Reports + +To generate a GitLab-compatible security report: + +```bash +socketcli --enable-gitlab-security --repo owner/repo +``` + +This creates a `gl-dependency-scanning-report.json` file following GitLab's Dependency Scanning report schema. + +### GitLab CI/CD Integration + +Add Socket Security scanning to your GitLab CI pipeline to generate Security Dashboard reports: + +```yaml +# .gitlab-ci.yml +socket_security_scan: + stage: security + image: python:3.11 + before_script: + - pip install socketsecurity + script: + - socketcli + --api-token $SOCKET_API_TOKEN + --repo $CI_PROJECT_PATH + --branch $CI_COMMIT_REF_NAME + --commit-sha $CI_COMMIT_SHA + --enable-gitlab-security + artifacts: + reports: + dependency_scanning: gl-dependency-scanning-report.json + paths: + - gl-dependency-scanning-report.json + expire_in: 1 week + only: + - merge_requests + - main +``` + +**Note**: This Security Dashboard integration can be used alongside the [Socket GitLab App](https://docs.socket.dev/docs/gitlab) for comprehensive protection: +- **Socket GitLab App**: Real-time PR comments, policy enforcement, and blocking +- **Security Dashboard**: Centralized vulnerability tracking and reporting in GitLab's native interface + +### Custom Output Path + +Specify a custom output path for the GitLab security report: + +```bash +socketcli --enable-gitlab-security --gitlab-security-file custom-path.json +``` + +### Multiple Output Formats + +GitLab security reports can be generated alongside other output formats: + +```bash +socketcli --enable-json --enable-gitlab-security --sarif-file results.sarif +``` + +This command will: +- Output JSON format to console +- Save GitLab Security Dashboard report to `gl-dependency-scanning-report.json` +- Save SARIF report to `results.sarif` + +### Security Dashboard Features + +The GitLab Security Dashboard will display: +- **Vulnerability Severity**: Critical, High, Medium, Low levels +- **Affected Packages**: Package name, version, and ecosystem +- **CVE Identifiers**: Direct links to CVE databases when available +- **Dependency Chains**: Distinction between direct and transitive dependencies +- **Remediation Suggestions**: Fix recommendations from Socket Security +- **Alert Categories**: Supply chain risks, malware, vulnerabilities, and more + +### Alert Filtering + +The GitLab report includes **actionable security alerts** based on your Socket policy configuration: + +**Included Alerts** ✅: +- **Error-level alerts** (`error: true`) - Security policy violations that block merges +- **Warning-level alerts** (`warn: true`) - Important security concerns requiring attention + +**Excluded Alerts** ❌: +- **Ignored alerts** (`ignore: true`) - Alerts explicitly ignored in your policy +- **Monitor-only alerts** (`monitor: true` without error/warn) - Tracked but not actionable + +**Socket Alert Types Detected**: +- Supply chain risks (malware, typosquatting, suspicious behavior) +- Security vulnerabilities (CVEs, unsafe code patterns) +- Risky permissions (network access, filesystem access, shell access) +- License policy violations + +All alert types are included in the GitLab report if they're marked as `error` or `warn` by your Socket Security policy, ensuring the Security Dashboard shows only actionable findings. + +### Alert Population: GitLab vs JSON/SARIF + +The GitLab Security Dashboard report and the JSON/SARIF diff outputs use different alert selection strategies, reflecting their distinct purposes: + +| Output Format | Default Alerts | With `--strict-blocking` | +|:---|:---|:---| +| `--enable-gitlab-security` | **All** alerts (new + existing) | All alerts (same) | +| `--enable-json` | New alerts only | New + existing alerts | +| `--enable-sarif` (diff scope) | New alerts only | New + existing alerts | + +**Why the difference?** GitLab's Security Dashboard is designed to present the full security posture of a project. An empty dashboard on a scan with no dependency changes would be misleading -- the vulnerabilities still exist, they just didn't change. By contrast, JSON and SARIF in diff scope are designed to answer "what changed?" and only include existing alerts when `--strict-blocking` explicitly requests it. + +> **Tip:** If you use `--enable-json` alongside `--enable-gitlab-security`, the GitLab report may contain more vulnerabilities than the JSON output. This is expected. To make JSON output match, add `--strict-blocking`. + +### Alert Ignoring via PR/MR Comments + +When using the CLI with SCM integration (`--scm github` or `--scm gitlab`), users can ignore specific alerts by reacting to Socket's PR/MR comments. Ignored alerts are removed from `--enable-json`, `--enable-sarif`, and console output. + +However, the GitLab Security Dashboard report includes **all** alerts matching your security policy (new and existing), regardless of comment-based ignores. This ensures the Security Dashboard always reflects the full set of known issues. To suppress a vulnerability from the GitLab report, adjust the alert's policy in Socket's dashboard rather than ignoring it via a PR comment. + +### Report Schema + +Socket CLI generates reports compliant with [GitLab Dependency Scanning schema version 15.0.0](https://gitlab.com/gitlab-org/security-products/security-report-schemas/-/blob/v15.0.0/dist/dependency-scanning-report-format.json). The reports include: + +- **Scan metadata**: Analyzer and scanner information with ISO 8601 timestamps +- **Vulnerabilities**: Detailed vulnerability data with: + - Unique deterministic UUIDs for tracking + - Package location and dependency information + - Severity levels mapped from Socket's analysis + - Socket-specific alert types and CVE identifiers + - Links to Socket.dev for detailed analysis +- **Dependency files**: Manifest files and their dependencies discovered during the scan + +**Schema compatibility:** The v15.0.0 schema is supported across all GitLab versions 12.0+ (both self-hosted and cloud). The report includes the `dependency_files` field, which is required by v15.0.0 and accepted as an optional extra by newer schema versions, ensuring maximum compatibility across GitLab instances. + +### Performance Notes + +When `--enable-gitlab-security` (or `--enable-json` / `--enable-sarif`) is used with a full scan (non-diff mode), the CLI fetches package and alert data from the scan results to populate the report. This adds time proportional to the number of packages in the scan. Without these output flags, no additional data is fetched and scan performance is unchanged. + +### Requirements + +- **GitLab Version**: GitLab 12.0 or later (for Security Dashboard support) +- **Socket API Token**: Set via `$SOCKET_API_TOKEN` environment variable or `--api-token` parameter +- **CI/CD Artifacts**: Reports must be uploaded as `dependency_scanning` artifacts + +### Troubleshooting + +**Report not appearing in Security Dashboard:** +- Verify the artifact is correctly configured in `.gitlab-ci.yml` +- Check that the job succeeded and artifacts were uploaded +- Ensure the report file follows the correct schema format + +**Empty vulnerabilities array:** +- The GitLab report includes both new and existing alerts, so repeated scans of the same repo should still populate the report as long as Socket detects actionable issues +- If the report is empty, verify the Socket dashboard shows alerts for the scanned packages -- an empty report means no error/warn-level alerts exist +- For full scans (non-diff mode), ensure you are using `--enable-gitlab-security` so alert data is fetched +- Check Socket.dev dashboard for full analysis details + +## Development + +Developer setup, workflows, and contributor notes are documented in [`development.md`](development.md). diff --git a/docs/development.md b/docs/development.md new file mode 100644 index 00000000..9bab1ce7 --- /dev/null +++ b/docs/development.md @@ -0,0 +1,95 @@ +# Development guide + +## Local setup + +This project uses `pyproject.toml` and `uv.lock` for dependency management. + +### Standard setup (PyPI dependencies) + +```bash +pyenv local 3.11 +make first-time-setup +``` + +### Local SDK development setup + +```bash +pyenv local 3.11 +SOCKET_SDK_PATH=~/path/to/socketdev make first-time-local-setup +``` + +Default local SDK path is `../socketdev` when `SOCKET_SDK_PATH` is not set. + +## Ongoing workflows + +After dependency changes: + +```bash +make update-deps +``` + +After pulling latest changes: + +```bash +make sync-all +``` + +Run tests: + +```bash +make test +``` + +Run lint/format checks: + +```bash +make lint +``` + +## Make targets + +High-level: + +- `make first-time-setup` +- `make first-time-local-setup` +- `make update-lock` +- `make sync-all` +- `make dev-setup` + +Implementation: + +- `make local-dev` +- `make setup` +- `make sync` +- `make clean` +- `make test` +- `make lint` + +## Environment variables + +Core: + +- `SOCKET_SECURITY_API_TOKEN` (also supports `SOCKET_SECURITY_API_KEY`, `SOCKET_API_KEY`, `SOCKET_API_TOKEN`) +- `SOCKET_SDK_PATH` (default `../socketdev`) + +GitLab: + +- `GITLAB_TOKEN` +- `CI_JOB_TOKEN` + +## Manual setup (without `make`) + +```bash +python -m venv .venv +source .venv/bin/activate +uv sync +uv add --dev pre-commit +pre-commit install +``` + +## Related docs + +- CLI quick start: [`../README.md`](../README.md) +- CI/CD usage: [`ci-cd.md`](ci-cd.md) +- Full CLI reference: [`cli-reference.md`](cli-reference.md) +- Troubleshooting: [`troubleshooting.md`](troubleshooting.md) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 00000000..80d08f96 --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,81 @@ +# Troubleshooting + +## Common gotchas + +- In diff scope, `--strict-blocking` uses a stricter alert set (`new + unchanged`) for blocking checks and diff-based output selection. +- `--sarif-scope full` requires `--reach`. +- In `--sarif-scope full` with `--sarif-file`, SARIF JSON is written to file and stdout JSON is suppressed. +- `--sarif-grouping alert` currently applies to `--sarif-scope full`. + +## Dashboard vs CLI result counts + +Differences in result counts can be valid, even when filtering appears similar. + +Common reasons: + +- `diff` vs `full` data source: + - `--sarif-scope diff` is based on diff alerts (typically net-new in the compared scan context). + - `--sarif-scope full` is based on full reachability facts data. +- Consolidation differences: + - Dashboard and API/CLI can apply different consolidation/grouping rules. + - `--sarif-grouping alert` and `--sarif-grouping instance` intentionally produce different row counts. +- Policy vs dataset: + - `--strict-blocking` only affects diff-scope behavior and does not make diff output equivalent to full dashboard data. +- Reachability data availability: + - If reachability analysis partially fails and falls back to precomputed tiers, counts can shift. + +Recommended comparison path: + +1. Use full-scope SARIF for parity-oriented comparisons. +2. Keep grouping fixed (`alert` for dashboard-style rollups, `instance` for detailed exports). +3. Compare reachability filters with the same mode and grouping across runs. + +## Save submitted file list + +Use `--save-submitted-files-list` to inspect exactly what was sent for scanning. + +```bash +socketcli --save-submitted-files-list submitted_files.json +``` + +Output includes: + +- timestamp +- total file count +- total size +- complete submitted file list + +## Save manifest archive + +Use `--save-manifest-tar` to export discovered manifest files as `.tar.gz`. + +```bash +socketcli --save-manifest-tar manifest_files.tar.gz +``` + +Combined example: + +```bash +socketcli --save-submitted-files-list files.json --save-manifest-tar backup.tar.gz +``` + +## Octopus merge note + +For octopus merges (3+ parents), Git can report incomplete changed-file sets because default diff compares against the first parent. + +If needed, force full scan behavior with: + +- `--ignore-commit-files` + +## GitLab report troubleshooting + +If report is not visible in GitLab Security Dashboard: + +- verify `dependency_scanning` artifact is configured in `.gitlab-ci.yml` +- verify job completed and artifact uploaded +- verify report file schema is valid + +If vulnerabilities array is empty: + +- this can be expected when no actionable security issues are present in the result scope +- confirm expected scope/flags and compare with Socket dashboard data diff --git a/examples/config/sarif-dashboard-parity.json b/examples/config/sarif-dashboard-parity.json new file mode 100644 index 00000000..e2fa3c89 --- /dev/null +++ b/examples/config/sarif-dashboard-parity.json @@ -0,0 +1,11 @@ +{ + "socketcli": { + "reach": true, + "sarif_scope": "full", + "sarif_grouping": "alert", + "sarif_reachability": "reachable", + "sarif_file": "sarif-dashboard-parity-reachable.sarif", + "disable_blocking": true, + "repo": "example-repo" + } +} diff --git a/examples/config/sarif-dashboard-parity.toml b/examples/config/sarif-dashboard-parity.toml new file mode 100644 index 00000000..67b8609d --- /dev/null +++ b/examples/config/sarif-dashboard-parity.toml @@ -0,0 +1,18 @@ +[socketcli] +# Dashboard-parity style output: +# - Full reachability data +# - Grouped alert-level SARIF results +# - Reachable-only filter +reach = true +sarif_scope = "full" +sarif_grouping = "alert" +sarif_reachability = "reachable" +sarif_file = "sarif-dashboard-parity-reachable.sarif" +disable_blocking = true + +# Optional repo/workspace hints +repo = "example-repo" +# workspace = "example-workspace" + +# Run example: +# socketcli --config examples/config/sarif-dashboard-parity.toml --target-path . diff --git a/examples/config/sarif-diff-ci-cd.json b/examples/config/sarif-diff-ci-cd.json new file mode 100644 index 00000000..146a36a2 --- /dev/null +++ b/examples/config/sarif-diff-ci-cd.json @@ -0,0 +1,11 @@ +{ + "socketcli": { + "reach": true, + "sarif_scope": "diff", + "sarif_grouping": "instance", + "sarif_reachability": "reachable", + "sarif_file": "sarif-diff-reachable.sarif", + "strict_blocking": true, + "repo": "example-repo" + } +} diff --git a/examples/config/sarif-diff-ci-cd.toml b/examples/config/sarif-diff-ci-cd.toml new file mode 100644 index 00000000..5137062e --- /dev/null +++ b/examples/config/sarif-diff-ci-cd.toml @@ -0,0 +1,16 @@ +[socketcli] +# Diff-focused CI/CD output: +# - Diff scope (net-new findings) +# - Reachable-only filter for SARIF in diff mode +# - Blocking enabled to enforce policy in CI/CD +reach = true +sarif_scope = "diff" +sarif_grouping = "instance" +sarif_reachability = "reachable" +sarif_file = "sarif-diff-reachable.sarif" +strict_blocking = true + +repo = "example-repo" + +# Run example: +# socketcli --config examples/config/sarif-diff-ci-cd.toml --target-path . diff --git a/examples/config/sarif-instance-detail.json b/examples/config/sarif-instance-detail.json new file mode 100644 index 00000000..6721a51b --- /dev/null +++ b/examples/config/sarif-instance-detail.json @@ -0,0 +1,11 @@ +{ + "socketcli": { + "reach": true, + "sarif_scope": "full", + "sarif_grouping": "instance", + "sarif_reachability": "all", + "sarif_file": "sarif-full-instance-all.sarif", + "disable_blocking": true, + "repo": "example-repo" + } +} diff --git a/examples/config/sarif-instance-detail.toml b/examples/config/sarif-instance-detail.toml new file mode 100644 index 00000000..ebfb2e89 --- /dev/null +++ b/examples/config/sarif-instance-detail.toml @@ -0,0 +1,16 @@ +[socketcli] +# Instance-detail output: +# - Full reachability data +# - Instance-level SARIF rows (package/version/advisory granularity) +# - Include all reachability states +reach = true +sarif_scope = "full" +sarif_grouping = "instance" +sarif_reachability = "all" +sarif_file = "sarif-full-instance-all.sarif" +disable_blocking = true + +repo = "example-repo" + +# Run example: +# socketcli --config examples/config/sarif-instance-detail.toml --target-path . diff --git a/instructions/gitlab-commit-status/uat.md b/instructions/gitlab-commit-status/uat.md new file mode 100644 index 00000000..f3b62a8d --- /dev/null +++ b/instructions/gitlab-commit-status/uat.md @@ -0,0 +1,54 @@ +# UAT: GitLab Commit Status Integration + +## Feature +`--enable-commit-status` posts a commit status (`success`/`failed`) to GitLab after scan completes. Repo admins can then require `socket-security` as a status check on protected branches. + +## Prerequisites +- GitLab project with CI/CD configured +- `GITLAB_TOKEN` with `api` scope (or `CI_JOB_TOKEN` with sufficient permissions) +- Merge request pipeline (so `CI_MERGE_REQUEST_PROJECT_ID` is set) + +## Test Cases + +### 1. Pass scenario (no blocking alerts) +1. Create MR with no dependency changes (or only safe ones) +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Commit status `socket-security` = `success`, description = "No blocking issues" +4. Verify in GitLab: **Repository > Commits > (sha) > Pipelines** or **MR > Pipeline > External** tab + +### 2. Fail scenario (blocking alerts) +1. Create MR adding a package with known blocking alerts +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Commit status = `failed`, description = "N blocking alert(s) found" + +### 3. Flag omitted (default off) +1. Run: `socketcli --scm gitlab` (no `--enable-commit-status`) +2. **Expected**: No commit status posted + +### 4. Non-MR pipeline (push event without MR) +1. Trigger pipeline on a push (no MR context) +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Commit status skipped (no `mr_project_id`), no error + +### 5. API failure is non-fatal +1. Use an invalid/revoked `GITLAB_TOKEN` +2. Run: `socketcli --scm gitlab --enable-commit-status` +3. **Expected**: Error logged ("Failed to set commit status: ..."), scan still completes with correct exit code + +### 6. Non-GitLab SCM +1. Run: `socketcli --scm github --enable-commit-status` +2. **Expected**: Flag is accepted but commit status is not posted (GitHub not yet supported) + +## Blocking Merges on Failure + +### Option A: Pipelines must succeed (all GitLab tiers) +Since `socketcli` exits with code 1 when blocking alerts are found, the pipeline fails automatically. +1. Go to **Settings > General > Merge requests** +2. Under **Merge checks**, enable **"Pipelines must succeed"** +3. Save — GitLab will now prevent merging when the pipeline fails + +### Option B: External status checks (GitLab Ultimate only) +Use the `socket-security` commit status as a required external check. +1. Go to **Settings > General > Merge requests > Status checks** +2. Add an external status check with name `socket-security` +3. MRs will require Socket's `success` status to merge diff --git a/pyproject.toml b/pyproject.toml index 7ff90d93..357f26ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,26 @@ [build-system] -requires = ["setuptools >= 61.0"] -build-backend = "setuptools.build_meta" +requires = [ + "hatchling" +] +build-backend = "hatchling.build" [project] name = "socketsecurity" -dynamic = ["version"] -requires-python = ">= 3.9" +version = "2.4.10" +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'", ] readme = "README.md" description = "Socket Security CLI for CI/CD" @@ -25,24 +34,137 @@ maintainers = [ classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", - "Programming Language :: Python :: 3.9", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", ] +[project.optional-dependencies] +test = [ + "pytest>=7.4.0", + "pytest-cov>=4.1.0", + "pytest-mock>=3.12.0", + "pytest-asyncio>=0.23.0", + "pytest-watch >=4.2.0" +] +dev = [ + "ruff>=0.3.0", + "twine", # for building + "uv>=0.1.0", # for dependency management + "pre-commit", + "hatch" +] [project.scripts] socketcli = "socketsecurity.socketcli:cli" +socketclidev = "socketsecurity.socketcli:cli" [project.urls] Homepage = "https://socket.dev" -[tool.setuptools.packages.find] +[tool.coverage.run] +source = ["socketsecurity"] +branch = true include = [ - "socketsecurity", - "socketsecurity.core" + "socketsecurity/**/*.py", + "socketsecurity/**/__init__.py" +] + +[tool.coverage.report] +exclude_lines = [ + "pragma: no cover", + "def __repr__", + "if __name__ == .__main__.:", + "raise NotImplementedError", + "if TYPE_CHECKING:", ] +show_missing = true +skip_empty = true + +[tool.ruff] +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".ipynb_checkpoints", + ".mypy_cache", + ".nox", + ".pants.d", + ".pyenv", + ".pytest_cache", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + ".vscode", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "site-packages", + "venv", +] + +[tool.ruff.lint] +# Enable Pyflakes (`F`) and a subset of the pycodestyle (`E`) codes by default. +# Unlike Flake8, Ruff doesn't enable pycodestyle warnings (`W`) or +# McCabe complexity (`C901`) by default. +select = [ + "E4", "E7", "E9", "F", # Current rules + "I", # isort + "F401", # Unused imports + "F403", # Star imports + "F405", # Star imports undefined + "F821", # Undefined names +] + +# Allow fix for all enabled rules (when `--fix`) is provided. +fixable = ["ALL"] +unfixable = [] -[tool.setuptools.dynamic] -version = {attr = "socketsecurity.__version__"} \ No newline at end of file +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" + +[tool.ruff.lint.isort] +known-first-party = ["socketsecurity"] + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +# Enable auto-formatting of code examples in docstrings. Markdown, +# reStructuredText code/literal blocks and doctests are all supported. +# +# This is currently disabled by default, but it is planned for this +# to be opt-out in the future. +docstring-code-format = false + +# Set the line length limit used when formatting code snippets in +# docstrings. +# +# This only has an effect when the `docstring-code-format` setting is +# enabled. +docstring-code-line-length = "dynamic" + +[tool.hatch.build.targets.wheel] +include = ["socketsecurity", "LICENSE"] + +[dependency-groups] +dev = [ + "pre-commit>=4.3.0", +] diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 00000000..69591c0b --- /dev/null +++ b/pytest.ini @@ -0,0 +1,7 @@ +[pytest] +testpaths = tests/unit +; addopts = -vv --no-cov --tb=short -ra +addopts = -vv --tb=short -ra --cov=socketsecurity --cov-report=term-missing +python_files = test_*.py +asyncio_mode = strict +asyncio_default_fixture_loop_scope = function diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 896774a3..00000000 --- a/requirements.txt +++ /dev/null @@ -1,6 +0,0 @@ -requests>=2.32.0 -mdutils~=1.6.0 -prettytable -argparse -gitpython>=3.1.43 -packaging>=24.1 \ No newline at end of file diff --git a/scripts/build_container.sh b/scripts/build_container.sh index a0c2a1b5..2e078d40 100755 --- a/scripts/build_container.sh +++ b/scripts/build_container.sh @@ -2,42 +2,145 @@ VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'") ENABLE_PYPI_BUILD=$1 STABLE_VERSION=$2 + +verify_package() { + local version=$1 + local pip_index=$2 + echo "Verifying package availability..." + + for i in $(seq 1 30); do + if pip install --index-url $pip_index socketsecurity==$version; then + echo "Package $version is now available and installable" + pip uninstall -y socketsecurity + return 0 + fi + echo "Attempt $i: Package not yet installable, waiting 20s... ($i/30)" + sleep 20 + done + + echo "Package verification failed after 30 attempts" + return 1 +} + echo $VERSION if [ -z $ENABLE_PYPI_BUILD ] || [ -z $STABLE_VERSION ]; then - echo "$0 pypi-build=enable stable=true" - echo "\tpypi-build: Build and publish a new version of the package to pypi. Options are prod or test" - echo "\tstable: Only build and publish a new version for the stable docker tag if it has been tested and going on the changelog" - exit + echo "$0 pypi-build=