diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 76ed953..edd0e77 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 0000000..db131ed --- /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 0000000..239d369 --- /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 0000000..51ab143 --- /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 0000000..98f4fd5 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE/improvement.md @@ -0,0 +1,10 @@ + + +## Public Changelog + + + +N/A + + + diff --git a/.github/workflows/docker-stable.yml b/.github/workflows/docker-stable.yml new file mode 100644 index 0000000..2a4c92d --- /dev/null +++ b/.github/workflows/docker-stable.yml @@ -0,0 +1,44 @@ +name: Mark Release as Stable +on: + workflow_dispatch: + inputs: + version: + description: 'Version to mark as stable (e.g., 1.2.3)' + required: true + +jobs: + stable: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Check if version exists in PyPI + id: version_check + 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" + exit 1 + fi + echo "Version ${{ inputs.version }} found on PyPI - proceeding with release" + + - name: Set up QEMU + uses: docker/setup-qemu-action@v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@v3 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push Stable Docker + uses: docker/build-push-action@v5 + with: + push: true + platforms: linux/amd64,linux/arm64 + tags: socketdev/cli:stable + build-args: | + CLI_VERSION=${{ inputs.version }} + \ No newline at end of file diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000..cc986cb --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,109 @@ +name: E2E Test + +on: + push: + branches: [main] + pull_request: + +jobs: + e2e-scan: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.12' + + - name: Install CLI from local repo + run: | + python -m pip install --upgrade pip + pip install . + + - name: Run Socket CLI scan + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + set -o pipefail + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --disable-blocking \ + --enable-debug \ + 2>&1 | tee /tmp/scan-output.log + + - name: Verify scan produced a report + run: | + if grep -q "Full scan report URL: https://socket.dev/" /tmp/scan-output.log; then + echo "PASS: Full scan report URL found" + grep "Full scan report URL:" /tmp/scan-output.log + elif grep -q "Diff Url: https://socket.dev/" /tmp/scan-output.log; then + echo "PASS: Diff URL found" + grep "Diff Url:" /tmp/scan-output.log + else + echo "FAIL: No report URL found in scan output" + cat /tmp/scan-output.log + exit 1 + fi + + e2e-reachability: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.12' + + - uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af + with: + node-version: '20' + + - name: Install CLI from local repo + run: | + python -m pip install --upgrade pip + pip install . + + - name: Install uv + run: pip install uv + + - name: Run Socket CLI with reachability + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + set -o pipefail + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --reach \ + --disable-blocking \ + --enable-debug \ + 2>&1 | tee /tmp/reach-output.log + + - name: Verify reachability analysis completed + run: | + if grep -q "Reachability analysis completed successfully" /tmp/reach-output.log; then + echo "PASS: Reachability analysis completed" + grep "Reachability analysis completed successfully" /tmp/reach-output.log + grep "Results written to:" /tmp/reach-output.log || true + else + echo "FAIL: Reachability analysis did not complete successfully" + cat /tmp/reach-output.log + exit 1 + fi + + - name: Verify scan produced a report + run: | + if grep -q "Full scan report URL: https://socket.dev/" /tmp/reach-output.log; then + echo "PASS: Full scan report URL found" + grep "Full scan report URL:" /tmp/reach-output.log + elif grep -q "Diff Url: https://socket.dev/" /tmp/reach-output.log; then + echo "PASS: Diff URL found" + grep "Diff Url:" /tmp/reach-output.log + else + echo "FAIL: No report URL found in scan output" + cat /tmp/reach-output.log + exit 1 + fi diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml new file mode 100644 index 0000000..2ee9b7e --- /dev/null +++ b/.github/workflows/pr-preview.yml @@ -0,0 +1,149 @@ +name: PR Preview +on: + pull_request: + types: [opened, synchronize, ready_for_review] + +jobs: + preview: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + pull-requests: write + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.13' + + # Install all dependencies from pyproject.toml + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install "virtualenv<20.36" + pip install hatchling==1.27.0 hatch==1.14.0 + + - name: Inject full dynamic version + run: python .hooks/sync_version.py --dev + + - name: Clean previous builds + run: rm -rf dist/ build/ *.egg-info + + - name: Get Hatch version + id: version + run: | + VERSION=$(hatch version | cut -d+ -f1) + echo "VERSION=$VERSION" >> $GITHUB_ENV + + - name: Build package + if: steps.version_check.outputs.exists != 'true' + run: | + hatch build + + - name: Publish to Test PyPI + if: steps.version_check.outputs.exists != 'true' + uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d + with: + repository-url: https://test.pypi.org/legacy/ + verbose: true + + - name: Comment on PR + if: steps.version_check.outputs.exists != 'true' + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea + env: + VERSION: ${{ env.VERSION }} + with: + script: | + const version = process.env.VERSION; + const prNumber = context.payload.pull_request.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + // Find existing bot comments + const comments = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + }); + + const botComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('🚀 Preview package published!') + ); + + const comment = ` + 🚀 Preview package published! + + Install with: + \`\`\`bash + pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple socketsecurity==${version} + \`\`\` + + Docker image: \`socketdev/cli:pr-${prNumber}\` + `; + + if (botComment) { + // Update existing comment + await github.rest.issues.updateComment({ + owner: owner, + repo: repo, + comment_id: botComment.id, + body: comment + }); + } else { + // Create new comment + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: prNumber, + body: comment + }); + } + + - name: Verify package is available + if: steps.version_check.outputs.exists != 'true' + id: verify_package + env: + VERSION: ${{ env.VERSION }} + run: | + for i in {1..30}; do + if pip install --index-url https://test.pypi.org/simple/ --extra-index-url https://pypi.org/simple socketsecurity==${VERSION}; then + echo "Package ${VERSION} is now available and installable on Test PyPI" + pip uninstall -y socketsecurity + echo "success=true" >> $GITHUB_OUTPUT + exit 0 + fi + echo "Attempt $i: Package not yet installable, waiting 20s... (${i}/30)" + sleep 20 + done + echo "success=false" >> $GITHUB_OUTPUT + exit 1 + + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 + + - name: Login to Docker Hub with Organization Token + if: steps.verify_package.outputs.success == 'true' + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Build & Push Docker Preview + if: steps.verify_package.outputs.success == 'true' + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 + env: + VERSION: ${{ env.VERSION }} + with: + push: true + platforms: linux/amd64,linux/arm64 + 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 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..99372b6 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,116 @@ +name: Release +on: + release: + types: [published] + +jobs: + release: + runs-on: ubuntu-latest + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.13' + + # Install all dependencies from pyproject.toml + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install "virtualenv<20.36" + pip install hatchling==1.27.0 hatch==1.14.0 + + - name: Get Version + id: version + run: | + 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 hatch version (v$RAW_VERSION)" + exit 1 + fi + + - name: Check if version exists on PyPI + id: version_check + env: + VERSION: ${{ env.VERSION }} + run: | + if curl -s -f https://pypi.org/pypi/socketsecurity/$VERSION/json > /dev/null; then + echo "Version ${VERSION} already exists on PyPI" + echo "pypi_exists=true" >> $GITHUB_OUTPUT + else + echo "Version ${VERSION} not found on PyPI - proceeding with PyPI deployment" + echo "pypi_exists=false" >> $GITHUB_OUTPUT + fi + + - name: Check Docker image existence + id: docker_check + env: + VERSION: ${{ env.VERSION }} + run: | + if curl -s -f "https://hub.docker.com/v2/repositories/socketdev/cli/tags/${{ env.VERSION }}" > /dev/null; then + echo "Docker image socketdev/cli:${VERSION} already exists" + echo "docker_exists=true" >> $GITHUB_OUTPUT + else + echo "docker_exists=false" >> $GITHUB_OUTPUT + fi + + - name: Build package + if: steps.version_check.outputs.pypi_exists != 'true' + run: | + pip install hatchling + hatch build + + - name: Publish to PyPI + if: steps.version_check.outputs.pypi_exists != 'true' + uses: pypa/gh-action-pypi-publish@ab69e431e9c9f48a3310be0a56527c679f56e04d + + - name: Set up QEMU + uses: docker/setup-qemu-action@49b3bc8e6bdd4a60e6116a5414239cba5943d3cf + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@c47758b77c9736f4b2ef4073d4d51994fabfe349 + + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 + with: + username: ${{ secrets.DOCKERHUB_USERNAME }} + password: ${{ secrets.DOCKERHUB_TOKEN }} + + - name: Verify package is installable + id: verify_package + env: + VERSION: ${{ env.VERSION }} + run: | + for i in {1..30}; do + if pip install socketsecurity==${VERSION}; then + echo "Package ${VERSION} is now available and installable on PyPI" + pip uninstall -y socketsecurity + echo "success=true" >> $GITHUB_OUTPUT + exit 0 + fi + echo "Attempt $i: Package not yet installable, waiting 20s... (${i}/30)" + sleep 20 + done + echo "success=false" >> $GITHUB_OUTPUT + exit 1 + + - name: Build & Push Docker + if: | + steps.verify_package.outputs.success == 'true' && + steps.docker_check.outputs.docker_exists != 'true' + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 + env: + VERSION: ${{ env.VERSION }} + with: + push: true + platforms: linux/amd64,linux/arm64 + tags: | + socketdev/cli:latest + socketdev/cli:${{ env.VERSION }} + build-args: | + CLI_VERSION=${{ env.VERSION }} \ No newline at end of file diff --git a/.github/workflows/version-check.yml b/.github/workflows/version-check.yml new file mode 100644 index 0000000..5e4335c --- /dev/null +++ b/.github/workflows/version-check.yml @@ -0,0 +1,90 @@ +name: Version Check +on: + pull_request: + types: [opened, synchronize, ready_for_review] + paths: + - 'socketsecurity/**' + - 'setup.py' + - 'pyproject.toml' + +jobs: + check_version: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 # Fetch all history for all branches + + - name: Check version increment + id: version_check + run: | + # 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 "'") + echo "MAIN_VERSION=$MAIN_VERSION" >> $GITHUB_ENV + + # Compare versions using Python + python3 -c " + 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}') + " + + - name: Manage PR Comment + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea + if: always() + env: + MAIN_VERSION: ${{ env.MAIN_VERSION }} + PR_VERSION: ${{ env.PR_VERSION }} + CHECK_RESULT: ${{ steps.version_check.outcome }} + with: + script: | + const success = process.env.CHECK_RESULT === 'success'; + const prNumber = context.payload.pull_request.number; + const owner = context.repo.owner; + const repo = context.repo.repo; + const comments = await github.rest.issues.listComments({ + owner: owner, + repo: repo, + issue_number: prNumber, + }); + + const versionComment = comments.data.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Version Check') + ); + + if (versionComment) { + if (success) { + // Delete the warning comment if check passes + await github.rest.issues.deleteComment({ + owner: owner, + repo: repo, + comment_id: versionComment.id + }); + } else { + // Update existing warning + await github.rest.issues.updateComment({ + owner: owner, + repo: repo, + comment_id: versionComment.id, + body: `❌ **Version Check Failed**\n\nPlease increment...` + }); + } + } else if (!success) { + // Create new warning comment only if check fails + await github.rest.issues.createComment({ + owner: owner, + repo: repo, + issue_number: prNumber, + body: `❌ **Version Check Failed**\n\nPlease increment...` + }); + } \ No newline at end of file diff --git a/.gitignore b/.gitignore index 23f720f..06780f9 100644 --- a/.gitignore +++ b/.gitignore @@ -1,20 +1,30 @@ .idea venv .venv +.venv-test build dist *.build *.dist *.egg-info -test *.env run_container.sh *.zip bin scripts/*.py *.json +!tests/**/*.json markdown_overview_temp.md markdown_security_temp.md .DS_Store *.pyc -test.py \ No newline at end of file +test.py +*.cpython-312.pyc` +file_generator.py +.coverage +.env.local +Pipfile +test/ +logs +ai_testing/ +verify_find_files_lazy_loading.py diff --git a/.hooks/sync_version.py b/.hooks/sync_version.py new file mode 100644 index 0000000..f26dd76 --- /dev/null +++ b/.hooks/sync_version.py @@ -0,0 +1,99 @@ +#!/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") + +VERSION_PATTERN = re.compile(r"__version__\s*=\s*['\"]([^'\"]+)['\"]") +PYPROJECT_PATTERN = re.compile(r'^version\s*=\s*".*"$', re.MULTILINE) +PYPI_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 fetch_existing_versions() -> set: + try: + with urllib.request.urlopen(PYPI_API) as response: + data = json.load(response) + return set(data.get("releases", {}).keys()) + except Exception as e: + print(f"⚠️ Warning: Failed to fetch existing versions from Test PyPI: {e}") + return set() + +def find_next_available_dev_version(base_version: str) -> str: + existing_versions = fetch_existing_versions() + 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 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 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) + print("⚠️ Version was unchanged — auto-bumped. Please git add + commit again.") + sys.exit(0) + else: + new_version = bump_patch_version(current_version) + inject_version(new_version) + print("⚠️ Version was unchanged — auto-bumped. Please git add + commit again.") + sys.exit(1) + else: + print("✅ Version already bumped — 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 0000000..d201e7f --- /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 0000000..e4fba21 --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..683e0ad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +## 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 569e2fd..0d9f2cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,8 +1,123 @@ 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 -RUN apk update \ - && apk add --no-cache git nodejs npm yarn -RUN pip install socketsecurity --upgrade \ - && socketcli -v \ - && socketcli -v | grep -q $CLI_VERSION \ No newline at end of file +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 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 + +# 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 + +ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"] diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..c0fb1b0 --- /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 839da36..0000000 --- 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 7f29426..0000000 --- a/Pipfile.lock +++ /dev/null @@ -1,206 +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:3cd43f1c6fa7dedc5899d69d3ad0398fd018ad1a17fba83ddaf78aa46c747516", - "sha256:ddc6c8ce995e6987e7faf5e3f1b02b302836a0e5d98ece18392cb1a36c72ad56" - ], - "markers": "python_version >= '3.6'", - "version": "==2024.6.2" - }, - "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 6c3148d..0a13219 100644 --- a/README.md +++ b/README.md @@ -1,38 +1,888 @@ # 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. +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 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 +``` + +**GitLab CI:** +```bash +socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-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 + +Pre-configured workflow examples are available in the [`workflows/`](workflows/) directory: + +- **[GitHub Actions](workflows/github-actions.yml)** - Complete workflow with concurrency control and automatic PR detection +- **[GitLab CI](workflows/gitlab-ci.yml)** - Pipeline configuration with caching and environment variable handling +- **[Bitbucket Pipelines](workflows/bitbucket-pipelines.yml)** - Basic pipeline setup with optional path filtering + +These examples are production-ready and include best practices for each platform. + +## 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) + +**Multiple output formats:** +```bash +socketcli --enable-json \ + --enable-sarif \ + --enable-gitlab-security \ + --repo owner/repo +``` + +This will simultaneously generate: +- JSON output to console +- SARIF format to console +- GitLab Security Dashboard report to file + +### 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] [--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] +socketcli [-h] [--api-token API_TOKEN] [--repo REPO] [--workspace WORKSPACE] [--repo-is-public] [--branch BRANCH] [--integration {api,github,gitlab,azure,bitbucket}] + [--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] [--default-branch] [--pending-head] [--generate-license] [--enable-debug] + [--enable-json] [--enable-sarif] [--enable-gitlab-security] [--gitlab-security-file ] + [--disable-overview] [--exclude-license-details] [--allow-unverified] [--disable-security-issue] + [--ignore-commit-files] [--disable-blocking] [--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-ecosystems REACH_ECOSYSTEMS] [--reach-exclude-paths REACH_EXCLUDE_PATHS] + [--reach-min-severity {low,medium,high,critical}] [--reach-skip-cache] [--reach-disable-analytics] [--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_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"]` it will only look for those files and not glob the path | +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) | + +#### 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 will include manifest files from folders like node_modules | + +#### 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 | +| --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 | +| --reach-version | False | latest | Version of @coana-tech/cli to use for analysis | +| --reach-analysis-timeout | False | 1200 | Timeout in seconds for the reachability analysis (default: 1200 seconds / 20 minutes) | +| --reach-analysis-memory-limit | False | 4096 | Memory limit in MB for the reachability analysis (default: 4096 MB / 4 GB) | +| --reach-concurrency | False | | Control parallel analysis execution (must be >= 1) | +| --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-exclude-paths | False | | Comma-separated list of file paths or patterns to exclude from reachability analysis | +| --reach-min-severity | False | | Minimum severity level for reporting reachability results (low, medium, high, critical) | +| --reach-skip-cache | False | False | Skip cache and force fresh reachability analysis | +| --reach-disable-analytics | False | False | Disable analytics collection during reachability analysis | +| --reach-output-file | False | .socket.facts.json | Path where reachability analysis results should be saved | +| --only-facts-file | False | False | Submit only the .socket.facts.json file to an existing scan (requires --reach and a prior scan) | + +**Reachability Analysis Requirements:** +- `npm` - Required to install and run @coana-tech/cli +- `npx` - Required to execute @coana-tech/cli +- `uv` - Required for Python environment management + +The CLI will automatically install @coana-tech/cli if not present. Use `--reach` to enable reachability analysis during a full scan, or use `--only-facts-file` with `--reach` to submit reachability results to an existing scan. + +#### Advanced Configuration +| Parameter | Required | Default | Description | +|:-------------------------|:---------|:--------|:----------------------------------------------------------------------| +| --ignore-commit-files | False | False | Ignore commit files | +| --disable-blocking | False | False | Disable blocking mode | +| --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 CLI flag 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 blocking alerts (error=true) from diff scans +- `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 GitLab CI:** +```bash +socketcli --target-path $CI_PROJECT_DIR --scm gitlab --pr-number ${CI_MERGE_REQUEST_IID:-0} --strict-blocking +``` + +**In GitHub Actions:** +```bash +socketcli --target-path $GITHUB_WORKSPACE --scm github --pr-number $PR_NUMBER --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 complete GitLab CI/CD examples, see: +- [`.gitlab-ci-strict-blocking-demo.yml`](.gitlab-ci-strict-blocking-demo.yml) - Comprehensive demo +- [`.gitlab-ci-strict-blocking-production.yml`](.gitlab-ci-strict-blocking-production.yml) - Production-ready template +- [`STRICT-BLOCKING-GITLAB-CI.md`](STRICT-BLOCKING-GITLAB-CI.md) - Full documentation + +## 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) +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 + +The CLI supports GitLab integration with automatic authentication pattern detection for different token types. + +### Supported Token Types + +GitLab API supports two authentication methods, and the CLI automatically detects which one to use: + +1. **Bearer Token Authentication** (`Authorization: Bearer `) + - GitLab CI Job Tokens (`$CI_JOB_TOKEN`) + - Personal Access Tokens with `glpat-` prefix + - OAuth 2.0 tokens (long alphanumeric tokens) + +2. **Private Token Authentication** (`PRIVATE-TOKEN: `) + - Legacy personal access tokens + - Custom tokens that don't match Bearer patterns + +### Token Detection Logic + +The CLI automatically determines the authentication method using this logic: + +``` +if token == $CI_JOB_TOKEN: + use Bearer authentication +elif token starts with "glpat-": + use Bearer authentication +elif token is long (>40 chars) and alphanumeric: + use Bearer authentication +else: + use PRIVATE-TOKEN authentication +``` + +### Automatic Fallback + +If the initial authentication method fails with a 401 error, the CLI automatically retries with the alternative method: + +- **Bearer → PRIVATE-TOKEN**: If Bearer authentication fails, retry with PRIVATE-TOKEN +- **PRIVATE-TOKEN → Bearer**: If PRIVATE-TOKEN fails, retry with Bearer authentication + +This ensures maximum compatibility across different GitLab configurations and token types. + +### Environment Variables + +| Variable | Description | Example | +|:---------|:------------|:--------| +| `GITLAB_TOKEN` | GitLab API token (required for GitLab integration) | `glpat-xxxxxxxxxxxxxxxxxxxx` | +| `CI_JOB_TOKEN` | GitLab CI job token (automatically used in GitLab CI) | Automatically provided by GitLab CI | + +### Usage Examples + +**GitLab CI with job token (recommended):** +```yaml +variables: + GITLAB_TOKEN: $CI_JOB_TOKEN +``` + +**GitLab CI with personal access token:** +```yaml +variables: + GITLAB_TOKEN: $GITLAB_PERSONAL_ACCESS_TOKEN # Set in GitLab project/group variables +``` + +**Local development:** +```bash +export GITLAB_TOKEN="glpat-your-personal-access-token" +socketcli --integration gitlab --repo owner/repo --pr-number 123 +``` + +### Scan Behavior + +The CLI determines scanning behavior intelligently: + +- **Manifest files changed**: Performs differential scan with PR/MR comments when supported +- **No manifest files changed**: Creates full repository scan report without waiting for diff results +- **Force API mode**: When no supported manifest files are detected, automatically enables non-blocking mode + +## 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` + +## Debugging and Troubleshooting + +### Saving Submitted Files List + +The CLI provides a debugging option to save the list of files that were submitted for scanning: + +```bash +socketcli --save-submitted-files-list submitted_files.json +``` + +This will create a JSON file containing: +- Timestamp of when the scan was performed +- Total number of files submitted +- Total size of all files (in bytes and human-readable format) +- Complete list of file paths that were found and submitted for scanning + +Example output file: +```json +{ + "timestamp": "2025-01-22 10:30:45 UTC", + "total_files": 3, + "total_size_bytes": 2048, + "total_size_human": "2.00 KB", + "files": [ + "./package.json", + "./requirements.txt", + "./Pipfile" + ] +} +``` + +This feature is useful for: +- **Debugging**: Understanding which files the CLI found and submitted +- **Verification**: Confirming that expected manifest files are being detected +- **Size Analysis**: Understanding the total size of manifest files being uploaded +- **Troubleshooting**: Identifying why certain files might not be included in scans or if size limits are being hit + +> **Note**: This option works with both differential scans (when git commits are detected) and full scans (API mode). + +### Saving Manifest Files Archive + +For backup, sharing, or analysis purposes, you can save all manifest files to a compressed tar.gz archive: + +```bash +socketcli --save-manifest-tar manifest_files.tar.gz +``` + +This will create a compressed archive containing all the manifest files that were found and submitted for scanning, preserving their original directory structure relative to the scanned directory. + +Example usage with other options: +```bash +# Save both files list and archive +socketcli --save-submitted-files-list files.json --save-manifest-tar backup.tar.gz + +# Use with specific target path +socketcli --target-path ./my-project --save-manifest-tar my-project-manifests.tar.gz +``` + +The manifest archive feature is useful for: +- **Backup**: Creating portable backups of all dependency manifest files +- **Sharing**: Sending the exact files being analyzed to colleagues or support +- **Analysis**: Examining the dependency files offline or with other tools +- **Debugging**: Verifying file discovery and content issues +- **Compliance**: Maintaining records of scanned dependency files + +> **Note**: The tar.gz archive preserves the original directory structure, making it easy to extract and examine the files in their proper context. + +### Differential scan skipped on octopus merge + +When your repo uses an **octopus merge** (3+ parents), the CLI may not detect all changed files. +This is expected Git behavior: the default diff only compares the merge result to the first parent. + +## 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 --enable-sarif +``` + +This command will: +- Output JSON format to console +- Save GitLab Security Dashboard report to `gl-dependency-scanning-report.json` +- Save SARIF report (if configured) + +### 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. + +### Report Schema + +Socket CLI generates reports compliant with [GitLab Dependency Scanning schema version 15.0.0](https://docs.gitlab.com/ee/development/integrations/secure.html). The reports include: + +- **Scan metadata**: Analyzer and scanner information +- **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 + +### 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:** +- This is normal if no new security issues were detected +- Check Socket.dev dashboard for full analysis details + +## Development + +This project uses `pyproject.toml` as the primary dependency specification. + +### Development Workflows + +The following Make targets provide streamlined workflows for common development tasks: + +#### Initial Setup (Choose One) + +1. Standard Setup (using PyPI packages): +```bash +pyenv local 3.11 # Ensure correct Python version +make first-time-setup +``` + +2. Local Development Setup (for SDK development): +```bash +pyenv local 3.11 # Ensure correct Python version +SOCKET_SDK_PATH=~/path/to/socketdev make first-time-local-setup +``` +The default SDK path is `../socketdev` if not specified. + +#### Ongoing Development Tasks + +After changing dependencies in pyproject.toml: +```bash +make update-deps +``` + +After pulling changes: +```bash +make sync-all +``` + +### Available Make targets: + +High-level workflows: +- `make first-time-setup`: Complete setup using PyPI packages +- `make first-time-local-setup`: Complete setup for local SDK development +- `make update-lock`: Update uv.lock file after changing pyproject.toml +- `make sync-all`: Sync dependencies after pulling changes +- `make dev-setup`: Setup for local development (included in first-time-local-setup) + +Implementation targets: +- `make local-dev`: Installs dependencies needed for local development +- `make setup`: Creates virtual environment and installs dependencies from uv.lock +- `make sync`: Installs exact versions from uv.lock +- `make clean`: Removes virtual environment and cache files +- `make test`: Runs pytest suite using uv run +- `make lint`: Runs ruff for code formatting and linting using uv run + +### Environment Variables + +#### Core Configuration +- `SOCKET_SECURITY_API_TOKEN`: Socket Security API token (alternative to --api-token parameter) + - For backwards compatibility, also accepts: `SOCKET_SECURITY_API_KEY`, `SOCKET_API_KEY`, `SOCKET_API_TOKEN` +- `SOCKET_SDK_PATH`: Path to local socketdev repository (default: ../socketdev) + +#### GitLab Integration +- `GITLAB_TOKEN`: GitLab API token for GitLab integration (supports both Bearer and PRIVATE-TOKEN authentication) +- `CI_JOB_TOKEN`: GitLab CI job token (automatically provided in GitLab CI environments) + +### Manual Development Environment Setup + +For manual setup without using the Make targets, follow these steps: + +1. **Create a virtual environment:** +```bash +python -m venv .venv +``` + +2. **Activate the virtual environment:** +```bash +source .venv/bin/activate +``` + +3. **Sync dependencies with uv:** +```bash +uv sync +``` + +4. **Install pre-commit:** +```bash +uv add --dev pre-commit +``` + +5. **Register the pre-commit hook:** +```bash +pre-commit install +``` + +> **Note**: This manual setup is an alternative to the streamlined Make targets described above. For most development workflows, using `make first-time-setup` or `make first-time-local-setup` is recommended. + diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..e6826fa --- /dev/null +++ b/docs/README.md @@ -0,0 +1,10 @@ +# 1. Clone the repo and create a virtualenv (Python 3.12+) +python3.12 -m venv .venv +source .venv/bin/activate + +# 2. Install dependencies +pip install --upgrade pip +pip install .[dev] + +# 3. Set up pre-commit hooks +pre-commit install diff --git a/instructions/gitlab-commit-status/uat.md b/instructions/gitlab-commit-status/uat.md new file mode 100644 index 0000000..f3b62a8 --- /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 7568ed7..1a59754 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,17 +1,24 @@ [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.2.74" +requires-python = ">= 3.10" +license = {"file" = "LICENSE"} dependencies = [ 'requests', 'mdutils', 'prettytable', - 'argparse', - 'GitPython' + 'GitPython', + 'packaging', + 'python-dotenv', + "socketdev>=3.0.31,<4.0.0", + "bs4>=0.0.2", + "markdown>=3.10", ] readme = "README.md" description = "Socket Security CLI for CI/CD" @@ -25,23 +32,138 @@ 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 = [] + +# 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", ] -[tool.setuptools.dynamic] -version = {attr = "socketsecurity.__version__"} \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000..69591c0 --- /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 dfd906d..0000000 --- a/requirements.txt +++ /dev/null @@ -1,5 +0,0 @@ -requests>=2.32.0 -mdutils~=1.6.0 -prettytable -argparse -gitpython>=3.1.43 \ No newline at end of file diff --git a/scripts/build_container.sh b/scripts/build_container.sh index bb6768c..2e078d4 100755 --- a/scripts/build_container.sh +++ b/scripts/build_container.sh @@ -1,15 +1,146 @@ #!/bin/sh VERSION=$(grep -o "__version__.*" socketsecurity/__init__.py | awk '{print $3}' | tr -d "'") -BYPASS_PYPI_BUILD=$1 +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=