From 25d944f8ac754950a6db5bda6832d9983f2f47c2 Mon Sep 17 00:00:00 2001 From: Yeison Vargas Date: Mon, 6 Jan 2025 11:11:44 -0500 Subject: [PATCH] chore: switch to pyproject --- .devcontainer/Dockerfile | 69 +++++-- .devcontainer/devcontainer.json | 88 ++++++-- .editorconfig | 18 -- .github/scripts/ci_pyproject.py | 44 ++++ .github/scripts/matrix.py | 75 +++++++ .github/workflows/build.yml | 7 +- .github/workflows/ci.yml | 67 ++++++ .github/workflows/main.yml | 122 ----------- .github/workflows/pr.yml | 66 ++++++ .github/workflows/reusable-build.yml | 78 +++++++ .gitignore | 3 +- .safety-project.ini | 5 + .vscode/launch.json | 298 +++++++-------------------- .vscode/settings.json | 26 +++ LICENSE | 11 - LICENSES/MIT.txt | 18 ++ LICENSES.md => LICENSES/NOTICE.md | 0 MANIFEST.in | 13 -- pyproject.toml | 270 +++++++++++++++++++++++- safety/VERSION | 1 - safety/__init__.py | 9 +- safety/alerts/__init__.py | 9 +- safety/safety.py | 1 + safety/util.py | 4 +- setup.cfg | 68 ------ test_requirements.txt | 23 --- tests/conftest.py | 51 +++++ tests/scan/test_command.py | 25 ++- tests/scan/test_file_finder.py | 3 - tests/scan/test_render.py | 3 - tests/test_cli.py | 8 +- tox.ini | 13 -- 32 files changed, 922 insertions(+), 574 deletions(-) delete mode 100644 .editorconfig create mode 100644 .github/scripts/ci_pyproject.py create mode 100644 .github/scripts/matrix.py create mode 100644 .github/workflows/ci.yml delete mode 100644 .github/workflows/main.yml create mode 100644 .github/workflows/pr.yml create mode 100644 .github/workflows/reusable-build.yml create mode 100644 .safety-project.ini create mode 100644 .vscode/settings.json delete mode 100644 LICENSE create mode 100644 LICENSES/MIT.txt rename LICENSES.md => LICENSES/NOTICE.md (100%) delete mode 100644 MANIFEST.in delete mode 100644 safety/VERSION delete mode 100644 setup.cfg delete mode 100644 test_requirements.txt create mode 100644 tests/conftest.py delete mode 100644 tox.ini diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cbb999ee..7c92b580 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,27 +1,54 @@ -# Use the official Python 3.12 slim image -FROM python:3.12-slim +FROM python:3.8-alpine -# Create a non-root user and a directory for the application -RUN useradd -m appuser && \ - mkdir /app && \ - chown appuser:appuser /app +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 -# Set the working directory -WORKDIR /app +RUN apk add --no-cache \ + git \ + curl \ + wget \ + zsh \ + jq \ + sudo \ + docker \ + docker-compose \ + bash \ + grep \ + sed \ + nodejs \ + npm \ + # Build dependencies for Python packages + gcc \ + musl-dev \ + python3-dev \ + libffi-dev \ + openssl-dev \ + cargo \ + rust \ + make && npm install -g pyright -# Set environment variables in a single step -ENV LC_ALL=C.UTF-8 \ - LANG=C.UTF-8 \ - PYTHONPATH="/app" +RUN pip install --no-cache-dir uv \ + && uv pip install --system hatch hatch-containers -# Install necessary dependencies, clean up after installation to reduce image size -RUN apt-get update && \ - apt-get -y install docker.io jq git && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* +ARG USERNAME=developer +ARG USER_UID=1000 +ARG USER_GID=$USER_UID -# Copy project files into the container (relative to the build context) -COPY . /app/ +RUN addgroup -g $USER_GID $USERNAME \ + && adduser -u $USER_UID -G $USERNAME -s /bin/zsh -D $USERNAME \ + && echo "$USERNAME ALL=(root) NOPASSWD:ALL" > /etc/sudoers.d/$USERNAME \ + && chmod 0440 /etc/sudoers.d/$USERNAME \ + && addgroup $USERNAME docker -# Switch to the non-root user for security reasons -USER appuser +RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" + +RUN sed -i 's|/bin/ash|/bin/zsh|' /etc/passwd + +RUN cp -r /root/.oh-my-zsh /home/$USERNAME/ \ + && cp /root/.zshrc /home/$USERNAME/ \ + && chown -R $USERNAME:$USERNAME /home/$USERNAME/.oh-my-zsh \ + && chown $USERNAME:$USERNAME /home/$USERNAME/.zshrc + +USER $USERNAME + +CMD ["zsh"] \ No newline at end of file diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 20c25d99..073facca 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -1,19 +1,71 @@ { - "name": "Safety-CLI Dev Container", - "build": { - "dockerfile": "Dockerfile", - "context": "..", - "args": { - "SAFETY_VERSION": "DEV" - } - }, - "extensions": [ - "ms-python.python", - "ms-python.vscode-pylance", - "ms-python.debugpy" - ], - "postCreateCommand": "pip install -r test_requirements.txt && pip install ruff requests pre-commit", - "remoteUser": "root", - "workspaceFolder": "/workspaces/safety", - "forwardPorts": [49152] -} + "name": "Safety CLI Development Environment", + + "build": { + "dockerfile": "Dockerfile", + "context": "." + }, + + "remoteUser": "developer", + "workspaceFolder": "${localWorkspaceFolder}", + "workspaceMount": "source=${localWorkspaceFolder},target=${localWorkspaceFolder},type=bind", + + + "mounts": [ + "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind", + "source=${localEnv:HOME}${localEnv:USERPROFILE}/.ssh,target=/home/developer/.ssh,type=bind,consistency=cached" + ], + + "remoteEnv": { + "PYTHONPATH": "${localWorkspaceFolder}", + "TERM": "xterm-256color" + }, + + "customizations": { + "vscode": { + "settings": { + "terminal.integrated.defaultProfile.linux": "zsh", + "terminal.integrated.profiles.linux": { + "zsh": { + "path": "/bin/zsh" + } + }, + "python.defaultInterpreterPath": "${localWorkspaceFolder}/.hatch/bin/python", + "editor.rulers": [80], + "files.exclude": { + "**/__pycache__": true, + "**/.pytest_cache": true + }, + "search.exclude": { + "**/.hatch": true, + } + }, + "extensions": [ + "ms-python.vscode-pylance", + "ms-python.python", + "ms-python.debugpy", + "ms-pyright.pyright", + "charliermarsh.ruff", + "tamasfe.even-better-toml", + "GitHub.copilot", + "streetsidesoftware.code-spell-checker", + "VisualStudioExptTeam.vscodeintellicode", + "VisualStudioExptTeam.intellicode-api-usage-examples", + "mechatroner.rainbow-csv", + "redhat.vscode-yaml", + "eamodio.gitlens", + "github.vscode-github-actions" + ] + } + }, + + "postCreateCommand": "hatch env create default && git config --global core.editor nano", + "postAttachCommand": "sudo chown root:developer /var/run/docker.sock && sudo chmod 660 /var/run/docker.sock && hatch env remove default && hatch env create default", + + "containerEnv": { + "SHELL": "/bin/zsh" + }, + + "waitFor": "postCreateCommand", + "shutdownAction": "stopContainer" +} \ No newline at end of file diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index f37120d1..00000000 --- a/.editorconfig +++ /dev/null @@ -1,18 +0,0 @@ -# http://editorconfig.org - -root = true - -[*] -indent_style = space -indent_size = 4 -trim_trailing_whitespace = true -insert_final_newline = true -charset = utf-8 -end_of_line = lf - -[*.bat] -indent_style = tab -end_of_line = crlf - -[LICENSE] -insert_final_newline = false diff --git a/.github/scripts/ci_pyproject.py b/.github/scripts/ci_pyproject.py new file mode 100644 index 00000000..371ae138 --- /dev/null +++ b/.github/scripts/ci_pyproject.py @@ -0,0 +1,44 @@ +# /// script +# requires-python = ">=3.8" +# dependencies = [] +# /// + +def update_pyproject_toml(file_path: str) -> None: + """ + Updates the pyproject.toml file by replacing 'type = "container"' with + 'type = "virtual"' + + This allows to keep using the same hatch test environment configuration for + local and CI, local uses container. + + This won't be needed if hatch supports a way to set the type of environment + via environment variables. This is a workaround until that is implemented. + + Args: + file_path: Path to the pyproject.toml file + """ + try: + # Read the file + with open(file_path, 'r', encoding='utf-8') as f: + content = f.read() + + # Replace the content + updated_content = content.replace('type = "container"', + 'type = "virtual"') + + # Write back to the file + with open(file_path, 'w', encoding='utf-8') as f: + f.write(updated_content) + + except Exception as e: + print(f"Error updating {file_path}: {str(e)}") + raise + +if __name__ == '__main__': + import sys + + if len(sys.argv) != 2: + print("Usage: python update_config.py ") + sys.exit(1) + + update_pyproject_toml(sys.argv[1]) \ No newline at end of file diff --git a/.github/scripts/matrix.py b/.github/scripts/matrix.py new file mode 100644 index 00000000..4870d0d3 --- /dev/null +++ b/.github/scripts/matrix.py @@ -0,0 +1,75 @@ +# /// script +# requires-python = ">=3.11" +# dependencies = [] +# /// +import json +import sys +from pathlib import Path +import tomllib + +def read_toml_config(file_path: str) -> dict: + """Read and parse TOML configuration file.""" + with open(file_path, 'rb') as f: + return tomllib.load(f) + +def generate_github_matrix(config: dict) -> dict: + """Generate GitHub Actions matrix configuration from Hatch config.""" + test_config = config['tool']['hatch']['envs']['test'] + matrix_configs = test_config['matrix'] + + combinations = [] + + # This logic is very naive and not future proof, kind of hardcoded. + # TODO: Fix this once we have a clear path about what other configurations + # we want to support. + + # First matrix: all Python versions with no target + for python_version in matrix_configs[0]['python']: + combinations.append({ + "python-version": python_version, + "target": None, + "os_type": None + }) + + # Second matrix: specific Python versions with targets + for python_version in matrix_configs[1]['python']: + for target in matrix_configs[1]['targets']: + combinations.append({ + "python-version": python_version, + "target": target, + "os_type": None + }) + + # Third matrix: specific Python versions with os versions + for python_version in matrix_configs[2]['python']: + for target in matrix_configs[2]['targets']: + for os_type in matrix_configs[2]['os_type']: + combinations.append({ + "python-version": python_version, + "target": target, + "os_type": os_type + }) + + return {"include": combinations} + +def main(): + if len(sys.argv) != 2: + print("Usage: python matrix.py ") + sys.exit(1) + + toml_path = Path(sys.argv[1]) + if not toml_path.exists(): + print(f"Error: File {toml_path} not found") + sys.exit(1) + + try: + config = read_toml_config(str(toml_path)) + matrix = generate_github_matrix(config) + # Output single-line JSON for GitHub Actions compatibility + print(json.dumps(matrix, separators=(',', ':'))) + except Exception as e: + print(f"Error processing TOML file: {e}", file=sys.stderr) + sys.exit(1) + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index fd12bdd8..d200bfe7 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -5,7 +5,7 @@ on: jobs: build-and-push: - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 steps: - uses: actions/checkout@v4 @@ -17,10 +17,7 @@ jobs: - name: Safety Version run: | - pip install packaging - package_version=$(cat safety/VERSION) - echo $package_version - echo "SAFETY_VERSION=$package_version" >> $GITHUB_ENV + echo "SAFETY_VERSION=$(python -c 'import tomli; print(tomli.load(open("pyproject.toml", "rb"))["project"]["version"])')" >> $GITHUB_ENV - name: Extract Major and Minor Version run: | diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..7c0d9944 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,67 @@ +name: CI + +on: + workflow_call: + push: + branches: + - main + - chore/* + - ci/* + pull_request: + types: + - opened + - synchronize + schedule: + - cron: "0 0 * * 1" + +jobs: + matrix: + runs-on: ubuntu-24.04 + outputs: + matrix: ${{ steps.set-matrix.outputs.matrix }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.11" + + - name: Generate matrix + id: set-matrix + run: | + matrix=$(python .github/scripts/matrix.py pyproject.toml) + echo "Generated matrix:" + echo "$matrix" | jq '.' + echo "matrix=$matrix" >> $GITHUB_OUTPUT + + test: + needs: matrix + + strategy: + matrix: ${{ fromJson(needs.matrix.outputs.matrix) }} + fail-fast: true + + runs-on: ${{ matrix.os_type || 'ubuntu-24.04' }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install Hatch + run: | + python -m pip install --upgrade pip + pip install hatch + + - name: Create temporary CI config + run: | + python .github/scripts/ci_pyproject.py pyproject.toml + + - name: Run tests + run: | + hatch run test.py${{ matrix.python-version }}${{ matrix.target && format('-{0}', matrix.target) }}${{ matrix.os_type && format('-{0}', matrix.os_type) }}:test diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml deleted file mode 100644 index eb145734..00000000 --- a/.github/workflows/main.yml +++ /dev/null @@ -1,122 +0,0 @@ -name: Python package - -on: [ push ] - -jobs: - test: - runs-on: ubuntu-20.04 - strategy: - matrix: - python-version: [ "3.8", "3.9", "3.10", "3.11", "3.12", "3.13" ] - steps: - - uses: actions/checkout@v3 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: ${{ matrix.python-version }} - - name: Display Python version - run: python -c "import sys; print(sys.version)" - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install -r test_requirements.txt - - name: Test with pytest - run: | - pytest --cov=./ --cov-report=xml - - notify: - needs: test - runs-on: ubuntu-20.04 - - if: contains(fromJson('["refs/heads/main", "refs/heads/binaries-fixes"]'), github.ref) || startsWith(github.ref, 'refs/tags') - - steps: - - name: Slack trigger - uses: slackapi/slack-github-action@v1.23.0 - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} - - build-binaries: - needs: test - runs-on: ${{ matrix.os }} - - strategy: - matrix: - os: [ 'windows-latest', 'ubuntu-20.04', 'macos-latest' ] - env: - BINARY_OS: '${{ matrix.os }}' - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.13' - - name: Install Dependencies - run: python binaries.py install - - name: Test Safety - run: python binaries.py test - - name: Producing Binaries - run: python binaries.py dist - - uses: actions/upload-artifact@v4 - if: ${{ matrix.os == 'windows-latest' }} - with: - name: safety-win-i686.exe - path: dist/safety-win-i686.exe - if-no-files-found: error - - uses: actions/upload-artifact@v4 - if: ${{ matrix.os == 'windows-latest' }} - with: - name: safety-win-x86_64.exe - path: dist/safety-win-x86_64.exe - if-no-files-found: error - - uses: actions/upload-artifact@v4 - if: ${{ matrix.os == 'ubuntu-20.04' }} - with: - name: safety-linux-x86_64 - path: dist/safety-linux-x86_64 - if-no-files-found: error - - uses: actions/upload-artifact@v4 - if: ${{ matrix.os == 'macos-latest' }} - with: - name: safety-macos-x86_64 - path: dist/safety-macos-x86_64 - if-no-files-found: error - - - deploy-pypi: - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - needs: build-binaries - name: Upload release to PyPI - runs-on: ubuntu-latest - environment: - name: pypi - url: https://pypi.org/p/safety - permissions: - id-token: write # Required for trusted publishing - steps: - - uses: actions/checkout@v4 - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Install dependencies - run: | - python -m pip install --upgrade pip - pip install build - - name: Build package - run: python -m build - - name: Publish package distributions to PyPI - uses: pypa/gh-action-pypi-publish@release/v1 - - create-gh-release: - needs: deploy-pypi - runs-on: ubuntu-20.04 - if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags') - permissions: - contents: write - steps: - - uses: actions/checkout@v2 - - uses: ncipollo/release-action@v1 - with: - artifacts: "dist/*" - draft: True diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml new file mode 100644 index 00000000..2d51554a --- /dev/null +++ b/.github/workflows/pr.yml @@ -0,0 +1,66 @@ +name: Pull Request + +on: + pull_request: + types: [opened, synchronize] + branches: [main] + +jobs: + build: + uses: ./.github/workflows/reusable-build.yml + with: + bump-command: "local-bump" + branch-name: ${{ github.head_ref }} + + comment: + needs: build + runs-on: ubuntu-24.04 + steps: + - name: Comment PR + uses: actions/github-script@v6 + with: + script: | + const version = '${{ needs.build.outputs.package-version }}' + const artifactsUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}/artifacts` + + const comment = ` + ## 🚀 Build artifacts are ready for testing! + + Download the wheel file and binaries from the [workflow artifacts](${artifactsUrl}). + + ### Installation + + **Local release:** + \`\`\`bash + pip install safety-${version}-py3-none-any.whl + \`\`\` + + > Note: You need to be logged in to GitHub to access the artifacts. + ` + + const { data: comments } = await github.rest.issues.listComments({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + }) + + const botComment = comments.find(comment => + comment.user.type === 'Bot' && + comment.body.includes('Build artifacts are ready for testing!') + ) + + if (botComment) { + await github.rest.issues.updateComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: botComment.id, + body: comment + }) + } else { + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: context.issue.number, + body: comment + }) + } \ No newline at end of file diff --git a/.github/workflows/reusable-build.yml b/.github/workflows/reusable-build.yml new file mode 100644 index 00000000..c2a62504 --- /dev/null +++ b/.github/workflows/reusable-build.yml @@ -0,0 +1,78 @@ +name: Build Package and Binaries + +on: + workflow_call: + inputs: + python-version: + required: false + type: string + default: "3.12" + bump-command: + required: true + type: string + branch-name: + required: false + type: string + default: "" + outputs: + package-version: + description: "The version of the package that was built" + value: ${{ jobs.build.outputs.version }} + +jobs: + build: + runs-on: ubuntu-24.04 + outputs: + version: ${{ steps.get-version.outputs.version }} + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: ${{ inputs.python-version }} + + - name: Install hatch + run: pip install hatch + + # PR version bump + - name: Generate SLUG + if: inputs.bump-command == 'local-bump' + id: slug + run: | + if [[ -z "${{ inputs.branch-name }}" ]]; then + echo "Error: branch-name is required for local-bump command" + exit 1 + fi + BRANCH_NAME="${{ inputs.branch-name }}" + SLUG=$(echo "$BRANCH_NAME" | iconv -t ascii//TRANSLIT | sed -r s/[^a-zA-Z0-9]+/-/g | sed -r s/^-+\|-+$//g | tr A-Z a-z) + echo "SLUG=$SLUG" >> $GITHUB_OUTPUT + + - name: Version bump (PR) + if: inputs.branch-name != '' + run: | + VERSION_STRING="$(hatch version)+${{ steps.slug.outputs.SLUG }}" + hatch run local-bump "$VERSION_STRING" + + # Other version bumps + - name: Version bump (other) + if: inputs.bump-command != '' && inputs.bump-command != 'local-bump' + run: hatch run ${{ inputs.bump-command }} + + - name: Get version + id: get-version + run: echo "version=$(hatch version)" >> $GITHUB_OUTPUT + + - name: Build package + run: hatch build + + # Hook build binaries logic here + # - name: Build binaries + + - name: Upload package artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/ + retention-days: 2 \ No newline at end of file diff --git a/.gitignore b/.gitignore index 176131f2..75ef1a6f 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,8 @@ # Other -.vscode/ .direnv/ .envrc +uv.lock +.hatch/ # Byte-compiled / optimized / DLL files __pycache__/ diff --git a/.safety-project.ini b/.safety-project.ini new file mode 100644 index 00000000..00f29606 --- /dev/null +++ b/.safety-project.ini @@ -0,0 +1,5 @@ +[project] +id = safety +url = /projects/aa1f1929-42d3-4fb7-8e6c-f9e187a832a2/findings +name = safety + diff --git a/.vscode/launch.json b/.vscode/launch.json index b716936c..e63fffac 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -2,241 +2,87 @@ "version": "0.2.0", "configurations": [ { - "name": "Safety Auth Login", + "name": "[CLI]: Safety CLI", "type": "debugpy", "request": "launch", "module": "safety", - "args": [ - "auth" - ], + "args": "${input:mainCommand}", + // This uses the default environment which is a virtual environment + // created by Hatch + "python": "${workspaceFolder}/.hatch/bin/python", "console": "integratedTerminal" - }, - { - "name": "Safety Auth Login --headless", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "auth","login","--headless" + } + ], + "inputs": [ + { + "id": "testPath", + "type": "pickString", + "description": "Choose the test path (selecting 'tests' will run all tests)", + // Note: This is a manual list and works for now. + "options": [ + "tests", + "tests/alerts", + "tests/auth", + "tests/scan", + "tests/formatters", + "tests/test_cli.py", + "tests/test_models.py", + "tests/test_output_utils.py", + "tests/test_safety.py", + "tests/test_util.py", + "tests/test_policy_file.py", + "tests/test_db.py" ], - "console": "integratedTerminal" - }, - { - "name": "Safety Auth Logout", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ + "default": "tests/" + }, + { + "id": "environment", + "type": "pickString", + "description": "Select environment", + // Note: This is a manual list and works for now. + "options": [ + "py3.8", + "py3.9", + "py3.10", + "py3.11", + "py3.12", + "py3.13", + "py3.8-pydantic-latest", + "py3.8-typer-latest", + "py3.13-pydantic-latest", + "py3.13-typer-latest", + ], + "default": "py3.8" + }, + { + "id": "mainCommand", + "type": "pickString", + "description": "Select Safety command with options", + "options": [ + // Auth commands "auth", - "logout" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "scan" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan --use-server-matching", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "scan", "--use-server-matching" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan API Key", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "--key","ADD-YOUR-API-KEY", "scan" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety License", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "license" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan --detailed-output", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ + "auth login", + "auth login --headless", + "auth logout", + + // Scan commands "scan", - "--detailed-output" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan --debug", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "--debug", - "scan" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan --disable-optional-telemetry", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "--disable-optional-telemetry", - "scan" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Scan --output json", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "scan", - "--output", - "json", - "--output-file", - "test.json" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Check", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ - "check" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety Check --debug", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ + "--key ADD-YOUR-API-KEY scan", + "scan --use-server-matching", + "scan --detailed-output", + "--debug scan", + "--disable-optional-telemetry scan", + "scan --output json --output-file json", + + // Check commands "check", - "--debug" - ], - "console": "integratedTerminal" - }, - { - "name": "Safety --help", - "type": "debugpy", - "request": "launch", - "module": "safety", - "args": [ + "--debug check", + + // Other commands + "license", "--help" ], - "console": "integratedTerminal" - }, - { - "name": "Run All Tests", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [], - "console": "integratedTerminal" - }, - { - "name": "Run test_render.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "./tests/scan/test_render.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_cli.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_cli.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_db.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_db.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_output_utils.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_output_utils.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_policy_file.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_policy_file.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_util.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_util.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_safety.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_safety.py" - ], - "console": "integratedTerminal" - }, - { - "name": "Run test_models.py", - "type": "debugpy", - "request": "launch", - "module": "pytest", - "args": [ - "tests/test_models.py" - ], - "console": "integratedTerminal" - } + "default": "scan" + } ] } \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b18958b7 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,26 @@ +{ + "python.testing.pytestEnabled": true, + "python.testing.unittestEnabled": false, + // This uses the default environment which is a virtual environment + // created by Hatch + "python.testing.pytestPath": "${workspaceFolder}/.hatch/bin/pytest", + "python.testing.autoTestDiscoverOnSaveEnabled": true, + "python.testing.pytestArgs": [ + "-v", + "-s", + // Disable assertion rewriting, to prevent conflicts with + // debugpy + "--assert=plain", + ], + "cSpell.words": [ + "Authlib", + "dparse", + "filelock", + "psutil", + "pydantic", + "pytest", + "ruamel", + "setuptools", + "typer" + ] +} \ No newline at end of file diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 796a276c..00000000 --- a/LICENSE +++ /dev/null @@ -1,11 +0,0 @@ - -MIT License - -Copyright (c) 2016, safetycli.com - -Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. - diff --git a/LICENSES/MIT.txt b/LICENSES/MIT.txt new file mode 100644 index 00000000..c41b1765 --- /dev/null +++ b/LICENSES/MIT.txt @@ -0,0 +1,18 @@ +MIT License + +Copyright (c) 2016 Safety CLI Cybersecurity Inc + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and +associated documentation files (the "Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the +following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial +portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT +LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO +EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER +IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE +USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/LICENSES.md b/LICENSES/NOTICE.md similarity index 100% rename from LICENSES.md rename to LICENSES/NOTICE.md diff --git a/MANIFEST.in b/MANIFEST.in deleted file mode 100644 index 518319c7..00000000 --- a/MANIFEST.in +++ /dev/null @@ -1,13 +0,0 @@ - -include CHANGELOG.md -include LICENSE -include README.md -include README.old -include safety/VERSION -include safety/safety-policy-template.yml -include safety/alerts/templates/* - -recursive-include safety/templates * -recursive-include tests * -recursive-exclude * __pycache__ -recursive-exclude * *.py[co] diff --git a/pyproject.toml b/pyproject.toml index b0f07653..19a73bd5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,3 +1,269 @@ [build-system] -requires = ["setuptools>=42"] -build-backend = "setuptools.build_meta" +requires = ["hatchling"] +build-backend = "hatchling.build" + +[project] +name = "safety" +version = "3.2.14" +description = "Scan dependencies for known vulnerabilities and licenses." +keywords = ["safety", "vulnerabilities", "dependencies", "licenses", "scan"] +readme = "README.md" +authors = [ + {name = "Safety", email = "cli@safetycli.com"}, +] +classifiers = [ + "Development Status :: 5 - Production/Stable", + "License :: OSI Approved :: MIT License", + "Topic :: Security", + "Natural Language :: English", + "Intended Audience :: Developers", + "Intended Audience :: Information Technology", + "Intended Audience :: System Administrators", + "Intended Audience :: Telecommunications Industry", + "Operating System :: POSIX :: Linux", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", +] +requires-python = ">=3.8" +dependencies = [ + "Authlib>=1.2.0", + "Click>=8.0.2", # Just to set a minimum version, Typer works with >=8.0.0 + "dparse>=0.6.4", + "filelock~=3.16.1", + "jinja2>=3.1.0", + "marshmallow>=3.15.0", # TODO: To be removed + "packaging>=21.0", + "psutil~=6.1.0", + "pydantic>=2.6.0,<2.10.0", + "requests", # TODO: To be replaced by httpx or aiohttp + "ruamel.yaml>=0.17.21", + "safety-schemas==0.0.10", + # TODO: To be removed after migrate away from pkg_resources + "setuptools>=65.5.1", + "typer>=0.12.1", + "typing-extensions>=4.7.1", +] +license = "MIT" +license-files = ["LICENSES/*"] + + +[project.urls] +homepage = "https://safetycli.com" +source = "https://github.com/pyupio/safety" +issues = "https://github.com/pyupio/safety/issues" +changelog = "https://github.com/pyupio/safety/blob/main/CHANGELOG.md" +documentation = "https://docs.safetycli.com/safety-docs" + +[project.optional-dependencies] +github = [ + "pygithub>=1.43.3" +] +gitlab = [ + "python-gitlab>=1.3.0" +] +spdx = [ + "spdx-tools>=0.8.2" +] + +[project.scripts] +safety = "safety.cli:cli" + + +# Build specific configurations + +[tool.hatch.build.targets.sdist] +only-include = [ + "CHANGELOG.md", + "README.md", + "SECURITY.md", + "LICENSES/*", + "safety", + "tests", + "pyproject.toml" +] + +[tool.hatch.build.targets.wheel] +packages = [ + "safety" +] +include-package-data = true +zip-safe = false + +# Tool specific configurations + +## Hatch environments +[tool.hatch.envs.default] +installer = "uv" +type = "virtual" +# This should always be set to the minimum supported Python version. +# To test with a different version, modify this value, refresh +# the environment, and run it via the VSCode launch configuration +# (it will pick up the new interpreter). +# +# To reset the default environment, run: +# hatch env remove +# hatch env create +python = "3.8" +path = ".hatch" + +dependencies = [ + "coverage[toml]>=6.0", + "pytest==8.3.4", + "commitizen", + "tomli" +] + + +# Common scripts used across environments +[tool.hatch.envs.default.scripts] +bump = "cz bump --check-consistency --yes && cz changelog --merge-prerelease" +beta-bump = "cz bump -pr beta --check-consistency --yes && cz changelog --merge-prerelease" +local-bump = "cz bump {args} --files-only --yes" + + +[tool.hatch.envs.test] +installer = "uv" +type = "container" + + +[tool.hatch.envs.test.scripts] +test = "pytest {args:tests}" +test-cov = 'coverage run -m pytest -m "" {args:tests}' +cov-report = [ + "coverage combine", + "coverage report --show-missing", +] +cov = [ + "test-cov", + "cov-report", +] + +[tool.hatch.envs.test.overrides] +matrix.targets.dependencies = [ + # { value = "pydantic==2.6.0", if = ["pydantic-minimal"] }, + { value = "pydantic~=2.9.0", if = ["pydantic-latest"] }, + + { value = "typer==0.12.1", if = ["typer-minimal"] }, + { value = "Click==8.0.2", if = ["typer-minimal"] }, + + { value = "typer~=0.13", if = ["typer-latest"] }, + { value = "Click~=8.1", if = ["typer-latest"] } +] + +# Prevent running the OS specific tests locally +matrix.os_type.scripts = [ + { key = "test", value = "echo Skipped", env = ["REMOTE_CONTAINERS=true"]}, + { key = "test-cov", value = "echo Skipped", env = ["REMOTE_CONTAINERS=true"]}, + { key = "cov-report", value = "echo Skipped", env = ["REMOTE_CONTAINERS=true"]}, + { key = "cov", value = "echo Skipped", env = ["REMOTE_CONTAINERS=true"]} +] + +# Note: scripts/matrix.py is used to generate the matrix for +# the GitHub CI workflow, the script is very naive and a switch in the +# order of the matrixes can cause the script to fail. +# This was pragmatic to avoid having to maintain the matrix in two places. +# +# Make sure to update the script if you structural changes to the matrixes. +# Examples: +# * Add/Remove python versions is okay. +# * Add/Remove targets is okay. +# * Reorder matrixes is not okay. +# +[[tool.hatch.envs.test.matrix]] +# Full Python version testing +python = ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + +# Critical dependency testing only on boundary versions +[[tool.hatch.envs.test.matrix]] +python = [ + "3.8", "3.13" +] +targets = [ + "typer-minimal", + # "pydantic-minimal", + "pydantic-latest", + "typer-latest" +] + +# Critical tests for OS compatibility +# This only runs on GitHub CI +[[tool.hatch.envs.test.matrix]] +python = [ + "3.8", "3.13" +] +targets = [ + "typer-latest" +] +os_type = [ + "windows-latest", + "macos-latest", +] + +[tool.hatch.envs.lint] +detached = true +dependencies = [ + "ruff", +] +[tool.hatch.envs.lint.scripts] +typing = "pyright" +style = [ + "ruff check {args:.}", + "ruff format --check --diff {args:.}", +] +fmt = [ + "ruff format {args:.}", + "ruff --fix {args:.}", + "style", +] +all = [ + "style", + "typing", +] + +[tool.pyright] +include = [ + "safety/scan/" +] +pythonVersion = "3.8" +pythonPlatform = "All" +reportMissingImports = "error" +reportMissingTypeStubs = false + +[tool.pytest.ini_options] +addopts = "--strict-markers" +markers = [ + "basic: requires no extras", +] + +[tool.coverage.run] +source_pkgs = ["safety"] +branch = true +parallel = true +omit = [ +] + +[tool.coverage.paths] +source = ["safety", "*/safety"] + +[tool.coverage.report] +exclude_lines = [ +] +[tool.commitizen] +name = "cz_conventional_commits" +tag_format = "$version" +version_scheme = "pep440" +version_provider = "pep621" +update_changelog_on_bump = false +annotated_tag = true +changelog_incremental = false + + +[tool.flake8] +exclude = ["docs"] diff --git a/safety/VERSION b/safety/VERSION deleted file mode 100644 index 23c635a1..00000000 --- a/safety/VERSION +++ /dev/null @@ -1 +0,0 @@ -3.2.14 diff --git a/safety/__init__.py b/safety/__init__.py index 6d3d19d3..73fcd701 100644 --- a/safety/__init__.py +++ b/safety/__init__.py @@ -1,11 +1,4 @@ # -*- coding: utf-8 -*- __author__ = """safetycli.com""" -__email__ = 'support@safetycli.com' - -import os - -ROOT = os.path.dirname(os.path.abspath(__file__)) - -with open(os.path.join(ROOT, 'VERSION')) as version_file: - VERSION = version_file.read().strip() +__email__ = 'cli@safetycli.com' diff --git a/safety/alerts/__init__.py b/safety/alerts/__init__.py index 0304f182..28972b3c 100644 --- a/safety/alerts/__init__.py +++ b/safety/alerts/__init__.py @@ -6,14 +6,17 @@ from dataclasses import dataclass -from safety.cli_util import SafetyCLILegacyGroup - from . import github from safety.util import SafetyPolicyFile from safety.scan.constants import CLI_ALERT_COMMAND_HELP LOG = logging.getLogger(__name__) + +def get_safety_cli_legacy_group(): + from safety.cli_util import SafetyCLILegacyGroup + return SafetyCLILegacyGroup + @dataclass class Alert: """ @@ -30,7 +33,7 @@ class Alert: policy: Any = None requirements_files: Any = None -@click.group(cls=SafetyCLILegacyGroup, help=CLI_ALERT_COMMAND_HELP, deprecated=True, utility_command=True) +@click.group(cls=get_safety_cli_legacy_group(), help=CLI_ALERT_COMMAND_HELP, deprecated=True, utility_command=True) @click.option('--check-report', help='JSON output of Safety Check to work with.', type=click.File('r'), default=sys.stdin, required=True) @click.option("--key", envvar="SAFETY_API_KEY", help="API Key for safetycli.com's vulnerability database. Can be set as SAFETY_API_KEY " diff --git a/safety/safety.py b/safety/safety.py index 7bb6ad7d..e41f5ae2 100644 --- a/safety/safety.py +++ b/safety/safety.py @@ -1581,6 +1581,7 @@ def get_packages(files: Optional[List[str]] = None, stdin: bool = False) -> List if stdin: return list(read_requirements(sys.stdin)) + # TODO: Migrate away from pkg_resources and use importlib import pkg_resources def allowed_version(pkg: str, version: str) -> bool: diff --git a/safety/util.py b/safety/util.py index 420eb13a..83c30527 100644 --- a/safety/util.py +++ b/safety/util.py @@ -237,8 +237,8 @@ def get_safety_version() -> str: Returns: str: The Safety version. """ - from safety import VERSION - return VERSION + from importlib.metadata import version + return version("safety") def get_primary_announcement(announcements: List[Dict[str, Any]]) -> Optional[Dict[str, Any]]: diff --git a/setup.cfg b/setup.cfg deleted file mode 100644 index 72c36621..00000000 --- a/setup.cfg +++ /dev/null @@ -1,68 +0,0 @@ -[metadata] -name = safety -version = file: safety/VERSION -description = Checks installed dependencies for known vulnerabilities and licenses. -long_description = file: README.md, CHANGELOG.md, LICENSE -long_description_content_type = text/markdown -author = safetycli.com -author_email = support@safetycli.com -url = https://github.com/pyupio/safety -project_urls = - Bug Tracker = https://github.com/pyupio/safety/issues - Source = https://github.com/pyupio/safety/ - Documentation = https://docs.pyup.io/docs/getting-started-with-safety-cli -keywords = safety, vulnerabilities, dependencies, licenses, check -license = MIT license -classifiers = - Development Status :: 5 - Production/Stable - Intended Audience :: Developers - License :: OSI Approved :: MIT License - Natural Language :: English - Operating System :: POSIX :: Linux - Operating System :: MacOS - Operating System :: Microsoft :: Windows - Programming Language :: Python :: 3 :: Only - Programming Language :: Python :: 3.6 - Programming Language :: Python :: 3.7 - Programming Language :: Python :: 3.8 - Programming Language :: Python :: 3.9 - Programming Language :: Python :: 3.10 - Programming Language :: Python :: 3.11 - -[options] -zip_safe = False -include_package_data = True -packages = safety, safety.formatters,safety.formatters.schemas, safety.alerts, safety.auth, safety.scan, safety.scan.finder, safety.scan.ecosystems, safety.scan.ecosystems.python, safety.alerts.templates, safety.templates, safety.scan.fun_mode -python_requires = >=3.8 -package_dir = - safety = safety -install_requires = - setuptools>=65.5.1 - Click>=8.0.2 - urllib3>=1.26.5 - requests - packaging>=21.0 - dparse>=0.6.4 - ruamel.yaml>=0.17.21 - jinja2>=3.1.0 - marshmallow>=3.15.0 - Authlib>=1.2.0 - rich - typer>=0.12.1 - pydantic>=2.6.0,<2.10.0 - safety_schemas==0.0.10 - typing-extensions>=4.7.1 - filelock~=3.16.1 - psutil~=6.1.0 - -[options.entry_points] -console_scripts = - safety = safety.cli:cli - -[options.extras_require] -github = pygithub>=1.43.3 -gitlab = python-gitlab>=1.3.0 -spdx = spdx-tools>=0.8.2 - -[flake8] -exclude = docs diff --git a/test_requirements.txt b/test_requirements.txt deleted file mode 100644 index 6757cff8..00000000 --- a/test_requirements.txt +++ /dev/null @@ -1,23 +0,0 @@ -pytest==7.4.4 -pytest-cov==4.1.0 -setuptools>=65.5.1; python_version>="3.7" -setuptools; python_version=="3.6" -Click>=8.0.2 -urllib3>=1.26.5 -requests -packaging>=21.0,<=23.0 -dparse>=0.6.4 -ruamel.yaml>=0.17.21 -dataclasses==0.8; python_version=="3.6" -jinja2; python_version=="3.6" -jinja2>=3.1.0; python_version>="3.7" -marshmallow; python_version=="3.6" -marshmallow>=3.15.0; python_version>="3.7" -Authlib>=1.2.0 -rich -typer>=0.12.1 -pydantic>=2.6.0,<2.10.0 -safety_schemas==0.0.10 -typing-extensions>=4.7.1 -filelock~=3.16.1 -psutil~=6.1.0 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 00000000..11747a4b --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,51 @@ +from importlib.metadata import distributions +import tomli +from packaging.requirements import Requirement +from packaging.utils import canonicalize_name + +def get_project_dependencies(): + try: + with open("pyproject.toml", "rb") as f: + pyproject = tomli.load(f) + deps = pyproject.get("project", {}).get("dependencies", []) + return {canonicalize_name(Requirement(dep).name): dep + for dep in deps if isinstance(dep, str)} + except Exception as e: + print(f"Error reading dependencies: {e}") + return {} + +def pytest_configure(config): + main_deps_specs = get_project_dependencies() + all_dists = {canonicalize_name(dist.metadata['Name']): + (dist.metadata['Name'], dist.version) + for dist in distributions()} + + # Main dependencies table + print(f"\n[{len(main_deps_specs)}] Main Dependencies:") + print("-" * 60) + print("%-20s %-25s %-15s" % ("Package", "Specification", "Installed")) + print("-" * 60) + + for pkg_norm, spec in sorted(main_deps_specs.items()): + if pkg_norm in all_dists: + name, version = all_dists[pkg_norm] + print("%-20s %-25s %-15s" % (name, spec, version)) + + other_pkgs = [f"{name} {ver}" + for pkg_norm, (name, ver) in sorted(all_dists.items()) + if pkg_norm not in main_deps_specs] + + # Other dependencies in wrapped format + print(f"\n[{len(other_pkgs)}] Other Dependencies:") + print("-" * 80) + + # Print other dependencies with wrapping + line = "" + for pkg in other_pkgs: + if len(line) + len(pkg) + 2 > 78: + print(line.rstrip(", ")) + line = pkg + ", " + else: + line += pkg + ", " + if line: + print(line.rstrip(", ")) \ No newline at end of file diff --git a/tests/scan/test_command.py b/tests/scan/test_command.py index e8d9a9ef..36cef9f7 100644 --- a/tests/scan/test_command.py +++ b/tests/scan/test_command.py @@ -1,25 +1,30 @@ -import os import unittest +import tempfile -from unittest.mock import patch, Mock from click.testing import CliRunner +from safety.auth.models import Auth from safety.cli import cli - -from safety.scan.command import scan -from safety.scan.command import scan_project_app +from safety.console import main_console as console +from unittest.mock import patch class TestScanCommand(unittest.TestCase): def setUp(self): self.runner = CliRunner(mix_stderr=False) - self.dirname = os.path.dirname(__file__) + self.target = tempfile.mkdtemp() + # Make sure the console is not quiet + # TODO: This is a workaround, we should improve the way the console + # is initialized in the CLI + console.quiet = False - def test_scan(self): - result = self.runner.invoke(cli, ["--stage", "cicd", "scan", "--target", self.dirname, "--output", "json"]) + @patch.object(Auth, 'is_valid', return_value=False) + @patch('safety.auth.utils.SafetyAuthSession.get_authentication_type', return_value="unauthenticated") + def test_scan(self, mock_is_valid, mock_get_auth_type): + result = self.runner.invoke(cli, ["scan", "--target", self.target, "--output", "json"]) self.assertEqual(result.exit_code, 1) - result = self.runner.invoke(cli, ["--stage", "production", "scan", "--target", self.dirname, "--output", "json"]) + result = self.runner.invoke(cli, ["--stage", "production", "scan", "--target", self.target, "--output", "json"]) self.assertEqual(result.exit_code, 1) - result = self.runner.invoke(cli, ["--stage", "cicd", "scan", "--target", self.dirname, "--output", "screen"]) + result = self.runner.invoke(cli, ["--stage", "cicd", "scan", "--target", self.target, "--output", "screen"]) self.assertEqual(result.exit_code, 1) diff --git a/tests/scan/test_file_finder.py b/tests/scan/test_file_finder.py index a45e98e0..93658214 100644 --- a/tests/scan/test_file_finder.py +++ b/tests/scan/test_file_finder.py @@ -52,6 +52,3 @@ def test_search(self, mock_os_walk, mock_glob): self.assertEqual(dir_path, Path('/root')) self.assertEqual(len(files), 0) # No files should be found as we didn't mock the handlers - -if __name__ == '__main__': - unittest.main() \ No newline at end of file diff --git a/tests/scan/test_render.py b/tests/scan/test_render.py index c934662c..4c5296fc 100644 --- a/tests/scan/test_render.py +++ b/tests/scan/test_render.py @@ -115,6 +115,3 @@ def test_print_summary(self, mock_render_to_console): call('0 security issues found, 0 fixes suggested.'), call('[number]0[/number] fixes suggested, resolving [number]0[/number] vulnerabilities.') ]) - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_cli.py b/tests/test_cli.py index 1d8425f4..1114afb7 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -17,6 +17,7 @@ from packaging.version import Version from safety import cli +from safety.console import main_console as console from safety.models import CVE, SafetyRequirement, Severity, Vulnerability from safety.util import Package, SafetyContext, get_safety_version from safety.auth.models import Auth @@ -75,6 +76,10 @@ def setUp(self): self.runner = CliRunner(mix_stderr=False) self.output_options = ['screen', 'text', 'json', 'bare'] self.dirname = os.path.dirname(__file__) + # Make sure the console is not quiet + # TODO: This is a workaround, we should improve the way the console + # is initialized in the CLI + console.quiet = False def test_command_line_interface(self): runner = CliRunner() @@ -528,7 +533,6 @@ def test_license_with_file(self, fetch_database_url): @patch('safety.auth.cli.get_auth_info', return_value={'email': 'test@test.com'}) @patch.object(Auth, 'is_valid', return_value=True) @patch('safety.auth.utils.SafetyAuthSession.get_authentication_type', return_value=AuthenticationType.TOKEN) - @patch('builtins.input', lambda *args: '') @patch('safety.safety.fetch_database', return_value={'vulnerable_packages': []}) def test_debug_flag(self, mock_get_auth_info, mock_is_valid, mock_get_auth_type, mock_fetch_database): """ @@ -558,7 +562,6 @@ def test_debug_flag(self, mock_get_auth_info, mock_is_valid, mock_get_auth_type, @patch('safety.auth.cli.get_auth_info', return_value={'email': 'test@test.com'}) @patch.object(Auth, 'is_valid', return_value=True) @patch('safety.auth.utils.SafetyAuthSession.get_authentication_type', return_value=AuthenticationType.TOKEN) - @patch('builtins.input', lambda *args: '') @patch('safety.safety.fetch_database', return_value={'vulnerable_packages': []}) def test_debug_flag_with_value_1(self, mock_get_auth_info, mock_is_valid, mock_get_auth_type, mock_fetch_database): sys.argv = ['safety', '--debug', '1', 'scan'] @@ -576,7 +579,6 @@ def dummy_function(): @patch('safety.auth.cli.get_auth_info', return_value={'email': 'test@test.com'}) @patch.object(Auth, 'is_valid', return_value=True) @patch('safety.auth.utils.SafetyAuthSession.get_authentication_type', return_value=AuthenticationType.TOKEN) - @patch('builtins.input', lambda *args: '') @patch('safety.safety.fetch_database', return_value={'vulnerable_packages': []}) def test_debug_flag_with_value_true(self, mock_get_auth_info, mock_is_valid, mock_get_auth_type, mock_fetch_database): sys.argv = ['safety', '--debug', 'true', 'scan'] diff --git a/tox.ini b/tox.ini deleted file mode 100644 index b2550420..00000000 --- a/tox.ini +++ /dev/null @@ -1,13 +0,0 @@ -[tox] -envlist = py{37,38,39,310,311,313}-packaging{21,22,23}-click{8.1.7} - -isolated_build = true - -[testenv] -deps = - pytest-cov==4.1.0 - pytest==7.4.4 - -commands = - pytest -rP tests/ --cov=safety/ --cov-report=html -