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 index 0f113b0..3639ffc 100644 --- a/.github/workflows/docker-stable.yml +++ b/.github/workflows/docker-stable.yml @@ -6,32 +6,46 @@ 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + 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: Set up QEMU + uses: docker/setup-qemu-action@c7c53464625b32c7a7e944ae62b3e17d2b600130 # v3 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@8d2750c68a42422c14e847fe6c8ac0403b4cbd6f # v3 - - name: Login to Docker Hub - uses: docker/login-action@v3 + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # v3 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - name: Build & Push Stable Docker - uses: docker/build-push-action@v5 + uses: docker/build-push-action@ca052bb54ab0790a636c9b5f226502c73d547a25 # 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 + CLI_VERSION=${{ inputs.version }} + diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml new file mode 100644 index 0000000..03584f6 --- /dev/null +++ b/.github/workflows/e2e-test.yml @@ -0,0 +1,201 @@ +name: E2E Test + +on: + push: + branches: [main] + pull_request: + workflow_dispatch: + +permissions: + contents: read + +jobs: + e2e-scan: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + persist-credentials: false + + - 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-sarif: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + persist-credentials: false + + - 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 with --sarif-file + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + set -o pipefail + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --sarif-file /tmp/results.sarif \ + --disable-blocking \ + 2>&1 | tee /tmp/sarif-output.log + + - name: Verify SARIF file is valid + run: | + python3 -c " + import json, sys + with open('/tmp/results.sarif') as f: + data = json.load(f) + assert data['version'] == '2.1.0', f'Invalid version: {data[\"version\"]}' + assert '\$schema' in data, 'Missing \$schema' + count = len(data['runs'][0]['results']) + print(f'PASS: Valid SARIF 2.1.0 with {count} result(s)') + " + + e2e-reachability: + if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + persist-credentials: false + + - 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 + + - name: Run scan with --sarif-file (all results) + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --reach \ + --sarif-file /tmp/sarif-all.sarif \ + --sarif-scope full \ + --sarif-reachability all \ + --disable-blocking \ + 2>/dev/null + + - name: Run scan with --sarif-file --sarif-reachability reachable (filtered results) + env: + SOCKET_SECURITY_API_KEY: ${{ secrets.SOCKET_CLI_API_TOKEN }} + run: | + socketcli \ + --target-path tests/e2e/fixtures/simple-npm \ + --reach \ + --sarif-file /tmp/sarif-reachable.sarif \ + --sarif-scope full \ + --sarif-reachability reachable \ + --disable-blocking \ + 2>/dev/null + + - name: Verify reachable-only results are a subset of all results + run: | + test -f /tmp/sarif-all.sarif + test -f /tmp/sarif-reachable.sarif + python3 -c " + import json + with open('/tmp/sarif-all.sarif') as f: + all_data = json.load(f) + with open('/tmp/sarif-reachable.sarif') as f: + reach_data = json.load(f) + all_count = len(all_data['runs'][0]['results']) + reach_count = len(reach_data['runs'][0]['results']) + print(f'All results: {all_count}, Reachable-only results: {reach_count}') + assert reach_count <= all_count, f'FAIL: reachable ({reach_count}) > all ({all_count})' + print('PASS: Reachable-only results is a subset of all results') + " diff --git a/.github/workflows/pr-preview.yml b/.github/workflows/pr-preview.yml index 8fdd667..1d7115a 100644 --- a/.github/workflows/pr-preview.yml +++ b/.github/workflows/pr-preview.yml @@ -1,70 +1,70 @@ name: PR Preview on: pull_request: - types: [opened, synchronize] + types: [opened, synchronize, ready_for_review] jobs: preview: + if: github.event.pull_request.head.repo.full_name == github.repository 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 with: - python-version: '3.x' + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: '3.13' - - name: Set preview version + # Install all dependencies from pyproject.toml + - name: Install dependencies 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 + 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 - # 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: Clean previous builds + run: rm -rf dist/ build/ *.egg-info - # Verify the change - echo "Updated version in __init__.py:" - cat socketsecurity/__init__.py | grep "__version__" + - name: Get Hatch version + id: version + run: | + VERSION=$(hatch version | cut -d+ -f1) + echo "VERSION=$VERSION" >> $GITHUB_ENV - - name: Check if version exists on Test PyPI + - 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@ab69e431e9c9f48a3310be0a56527c679f56e04d 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@60a0d83039c74a4aee543508d2ffcb1c3799cdea env: VERSION: ${{ env.VERSION }} with: @@ -133,22 +133,30 @@ jobs: echo "success=false" >> $GITHUB_OUTPUT exit 1 - - name: Login to Docker Hub + - 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@v3 + 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@v5 + uses: docker/build-push-action@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 env: VERSION: ${{ env.VERSION }} with: push: true - tags: socketdev/cli:pr-${{ github.event.pull_request.number }} + 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 + 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 0000000..d30e67a --- /dev/null +++ b/.github/workflows/python-tests.yml @@ -0,0 +1,72 @@ +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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 1 + persist-credentials: false + - name: 🐍 setup python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + with: + python-version: ${{ env.PYTHON_VERSION }} + - name: 🛠️ install deps + run: | + python -m pip install --upgrade pip + pip install uv + uv sync --extra test + - name: 🧪 run tests + run: uv run pytest -q tests/unit/ tests/core/ + + unsupported-python-install: + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 1 + persist-credentials: false + - name: 🐍 setup python + uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 + 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 1004ce6..5549b88 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,25 +1,39 @@ 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@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 + with: + fetch-depth: 0 + persist-credentials: false + - uses: actions/setup-python@f677139bbe7f9c59b41e40162b753c062f5d49a3 with: - python-version: '3.x' + 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 + 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 +55,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,17 +65,21 @@ 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@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 - uses: docker/login-action@v3 + - name: Login to Docker Hub with Organization Token + uses: docker/login-action@9780b0c442fbb1117ed29e0efdff1e18412f7567 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} @@ -88,7 +106,7 @@ 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@4f58ea79222b3b9dc2c8bbdd6debcef730109a75 env: VERSION: ${{ env.VERSION }} with: @@ -96,4 +114,6 @@ jobs: platforms: linux/amd64,linux/arm64 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 96cdc09..5b9bda8 100644 --- a/.github/workflows/version-check.yml +++ b/.github/workflows/version-check.yml @@ -1,30 +1,38 @@ name: Version Check on: pull_request: - types: [opened, synchronize] + types: [opened, synchronize, ready_for_review] paths: - 'socketsecurity/**' - 'setup.py' - 'pyproject.toml' +permissions: + contents: read + pull-requests: write + issues: write + jobs: check_version: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 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 @@ -39,8 +47,8 @@ jobs: " - name: Manage PR Comment - uses: actions/github-script@v7 - if: always() + uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea + 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 +95,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 0000000..39d1b18 --- /dev/null +++ b/.github/zizmor.yml @@ -0,0 +1,3 @@ +rules: + secrets-outside-env: + disable: true diff --git a/.gitignore b/.gitignore index 405a91f..e01bafe 100644 --- a/.gitignore +++ b/.gitignore @@ -1,18 +1,21 @@ .idea venv .venv +.venv-test build dist *.build *.dist *.egg-info -test *.env run_container.sh *.zip bin scripts/*.py *.json +*.sarif +!tests/**/*.json +!examples/config/*.json markdown_overview_temp.md markdown_security_temp.md .DS_Store @@ -20,4 +23,10 @@ markdown_security_temp.md test.py *.cpython-312.pyc` file_generator.py -.env.local \ No newline at end of file +.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 949ec58..0d9f2cb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +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 +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 -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 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 ee638cf..0000000 --- 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 6e89d7a..7929189 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,193 @@ # 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` | +| 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 . +``` + +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 }} +``` + +## 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 0000000..edf3a4e --- /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 0000000..0c807f7 --- /dev/null +++ b/docs/cli-reference.md @@ -0,0 +1,734 @@ +# 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] [--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] [--enable-diff] [--scm SCM] [--timeout TIMEOUT] [--include-module-folders] + [--reach] [--reach-version REACH_VERSION] [--reach-timeout REACH_ANALYSIS_TIMEOUT] + [--reach-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_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 (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 | +| `--reach-version` | False | latest | Version of @coana-tech/cli to use for analysis | +| `--reach-timeout` | False | 1200 | Timeout in seconds for the reachability analysis (default: 1200 seconds / 20 minutes) | +| `--reach-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 + +## 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 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` 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. + +### 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 + +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 0000000..9bab1ce --- /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 0000000..80d08f9 --- /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 0000000..e2fa3c8 --- /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 0000000..67b8609 --- /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 0000000..146a36a --- /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 0000000..5137062 --- /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 0000000..6721a51 --- /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 0000000..ebfb2e8 --- /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 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 7ff90d9..6b37502 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.80" +requires-python = ">= 3.11" +license = {"file" = "LICENSE"} dependencies = [ 'requests', 'mdutils', 'prettytable', 'GitPython', 'packaging', + 'python-dotenv', + "socketdev>=3.0.32,<4.0.0", + "bs4>=0.0.2", + "markdown>=3.10", ] readme = "README.md" description = "Socket Security CLI for CI/CD" @@ -25,24 +32,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 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 896774a..0000000 --- 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 a0c2a1b..2e078d4 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=